diff --git a/include/mp/flat/converter.h b/include/mp/flat/converter.h index b44a0f353..293164868 100644 --- a/include/mp/flat/converter.h +++ b/include/mp/flat/converter.h @@ -887,6 +887,13 @@ class FlatConverter : return ModelAPI::AcceptsNonconvexQC(); } + /// Ask if the solver can recognize SOCP corner cases + /// (non-std representations such as xy>=1, see tests) + /// from quadratic representations + static bool ModelAPICanSOCPCornerCasesFromQC() { + return ModelAPI::CanSOCPCornerCasesFromQC(); + } + /// Whether the solver can mix conic quadratic /// (entered via dedicated API) /// and direct quadratic constraints @@ -895,7 +902,7 @@ class FlatConverter : } /// Whether the ModelAPI accepts quadratic cones - int ModelAPIAcceptsQuadraticCones() { + int ModelAPIAcceptsQuadraticCones() const { return std::max( (int)GetConstraintAcceptance((QuadraticConeConstraint*)nullptr), @@ -908,6 +915,10 @@ class FlatConverter : int NumQC2SOCPAttempted() const { return nQC2SOCPAttempted_; } int NumQC2SOCPSucceeded() const { return nQC2SOCPSucceeded_; } + /// Number of exp cones recognized + void IncExpConeCounter() { ++nExpConesRecognized_; } + int NumExpConesRecognized() const { return nExpConesRecognized_; } + /// Whether the ModelAPI accepts exp cones int ModelAPIAcceptsExponentialCones() { return @@ -925,8 +936,9 @@ class FlatConverter : int passQuadObj_ = ModelAPIAcceptsQuadObj(); int passQuadCon_ = ModelAPIAcceptsQC(); - int passSOCPCones_ = 0; - int passExpCones_ = 0; + int passSOCPCones_ = 0; + int passSOCP2QC_ = 0; + int passExpCones_ = 0; int relax_ = 0; @@ -972,10 +984,42 @@ class FlatConverter : private: - std::string solchkfailtext_ { + const std::string solchkfailtext_ { "Fail on MP solution check violations, with solve result " + std::to_string(sol::MP_SOLUTION_CHECK) + '.' }; + + int DefaultSOCPMode() const { + return + !ModelAPIAcceptsQC() && !ModelAPIAcceptsQuadraticCones() + ? 0 + : ModelAPICanSOCPCornerCasesFromQC() ? 1 + : 2; + } + int DefaultSOCP2QCMode() const { + return + ((!ModelAPIAcceptsQC() || ModelAPICanMixConicQCAndQC()) + && ModelAPIAcceptsQuadraticCones()) + ? 0 + : (!ModelAPICanMixConicQCAndQC() + && ModelAPIAcceptsQuadraticCones()) ? 1 + : 2; + } + std::string socp_mode_text_; + std::string socp2qc_mode_text_; + const mp::OptionValueInfo socp_values_[3] = { + { "0", "Do not recognize SOCP forms", 0}, + { "1", "Recognize from non-quadratic expressions only (sqrt, abs)", 1}, + { "2", "Recognize from quadratic and non-quadratic SOCP forms", 2} + }; + const mp::OptionValueInfo socp2qc_values_[3] = { + { "0", "Do not convert", 0}, + { "1", "Convert if no other cone types found, and " + "not all original quadratics could be recognized as SOC, " + "in particular if the objective is quadratic", 1}, + { "2", "Always convert", 2} + }; + void InitOwnOptions() { /// Should be called after adding all constraint keepers FlatModel::ConsiderAcceptanceOptions(*this, GetModelAPI(), GetEnv()); @@ -1012,23 +1056,36 @@ class FlatConverter : "0*/1: Multiply out and pass quadratic constraint terms to the solver, " "vs. linear approximation.", options_.passQuadCon_, 0, 1); - if (ModelAPIAcceptsExponentialCones()) - GetEnv().AddOption("cvt:expcones expcones", - ModelAPIAcceptsExponentialCones()>1 ? - "0/1*: Recognize exponential cones." : - "0*/1: Recognize exponential cones.", - options_.passExpCones_, 0, 1); - options_.passExpCones_ = ModelAPIAcceptsExponentialCones()>1; - if (ModelAPIAcceptsQuadraticCones()) - GetEnv().AddOption("cvt:socp passsocp socp", - ModelAPIAcceptsQuadraticCones()>1 ? - "0/1*: Recognize quadratic cones vs passing them " - "as pure quadratic constraints." : - "0*/1: Recognize quadratic cones vs passing them " - "as pure quadratic constraints.", - options_.passSOCPCones_, 0, 1); - options_.passSOCPCones_ = ModelAPIAcceptsQuadraticCones()>1; - GetEnv().AddOption("alg:relax relax", + GetEnv().AddOption("cvt:expcones expcones", + ModelAPIAcceptsExponentialCones()>1 ? + "0/1*: Recognize exponential cones." : + "0*/1: Recognize exponential cones.", + options_.passExpCones_, 0, 1); + options_.passExpCones_ = ModelAPIAcceptsExponentialCones()>1; + // Should be after construction + socp_mode_text_ = + "Second-Order Cone recognition mode:\n" + "\n.. value-table::\n" + "Recognized SOCP forms can be further converted to " + "(SOCP-standardized) quadratic constraints, see cvt:socp2qc. " + "Default: " + std::to_string(DefaultSOCPMode()) + "."; + GetEnv().AddStoredOption("cvt:socp socpmode socp", + socp_mode_text_.c_str(), + options_.passSOCPCones_, socp_values_); + options_.passSOCPCones_ = DefaultSOCPMode(); + socp2qc_mode_text_ = + "Mode to convert recognized SOCP forms to " + "SOCP-standardized quadratic constraints:\n" + "\n.. value-table::\n" + "Such conversion can be necessary " + "if the solver does not accept " + "a mix of conic and quadratic constraints/objectives. " + "Default: " + std::to_string(DefaultSOCP2QCMode()) + "."; + GetEnv().AddStoredOption("cvt:socp2qc socp2qcmode socp2qc", + socp2qc_mode_text_.c_str(), + options_.passSOCP2QC_, socp2qc_values_); + options_.passSOCP2QC_ = DefaultSOCP2QCMode(); + GetEnv().AddOption("alg:relax relax", "0*/1: Whether to relax integrality of variables.", options_.relax_, 0, 1); GetEnv().AddStoredOption( @@ -1125,11 +1182,19 @@ class FlatConverter : bool IfQuadratizePowConstPosIntExp() const { return options_.passQuadCon_; } - /// Whether we pass SOCP cones - bool IfPassSOCPCones() const { return options_.passSOCPCones_; } + /// Recognition mode for SOCP cones + int IfPassSOCPCones() const { return options_.passSOCPCones_; } + + /// Mode for SOCP -> QC conversion + int SOCP2QCMode() const { return options_.passSOCP2QC_; } - /// Whether we pass exp cones - bool IfPassExpCones() const { return options_.passExpCones_; } + /// Decide to convert SOCP -> QC + void Setup2ConvertSOCP2QC() { ifCvtSOCP2QC_=true; } + /// If decided to convert SOCP -> QC + bool IfConvertSOCP2QC() const { return ifCvtSOCP2QC_; } + + /// Recognition mode for exp cones + int IfPassExpCones() const { return options_.passExpCones_; } public: @@ -1180,7 +1245,8 @@ class FlatConverter : ConicConverter conic_cvt_ { *static_cast(this) }; int nQC2SOCPAttempted_= 0; int nQC2SOCPSucceeded_= 0; - + int nExpConesRecognized_ = 0; + bool ifCvtSOCP2QC_ = 0; std::vector refcnt_vars_; int constr_depth_ = 0; // tree depth of new constraints diff --git a/include/mp/flat/converter_model.h b/include/mp/flat/converter_model.h index 374a0182f..01c1a00c4 100644 --- a/include/mp/flat/converter_model.h +++ b/include/mp/flat/converter_model.h @@ -179,14 +179,25 @@ class FlatModel ///////////////////////////// OBJECTIVES //////////////////////////// public: + /// List of objectives using ObjList = std::vector; + /// Get list of objectives, const const ObjList& get_objectives() const { return objs_; } + /// Get list of objectives ObjList& get_objectives() { return objs_; } + /// N obj int num_objs() const { return (int)objs_.size(); } + /// Get obj [i] const QuadraticObjective& get_obj(int i) const { return get_objectives().at(i); } - -public: + /// Has a QP objective? + bool HasQPObjective() const { + for (const auto& obj: get_objectives()) + if (!obj.GetQPTerms().empty()) + return true; + return false; + } + /// Add an objective void AddObjective(QuadraticObjective&& obj) { get_objectives().push_back(std::move(obj)); } diff --git a/include/mp/flat/model_api_base.h b/include/mp/flat/model_api_base.h index 7d2f37433..92a22d5f3 100644 --- a/include/mp/flat/model_api_base.h +++ b/include/mp/flat/model_api_base.h @@ -173,6 +173,11 @@ class BasicFlatModelAPI { /// (entered via dedicated API) and direct quadratic constraints static constexpr bool CanMixConicQCAndQC() { return false; } + /// Ask if the solver can recognize SOCP corner cases + /// (non-std representations such as xy>=1, see tests) + /// from quadratic representations + static constexpr bool CanSOCPCornerCasesFromQC() { return false; } + private: const FlatModelInfo* pfmi_ { nullptr }; diff --git a/include/mp/flat/redef/conic/cones.h b/include/mp/flat/redef/conic/cones.h index cab81b242..3b540e41f 100644 --- a/include/mp/flat/redef/conic/cones.h +++ b/include/mp/flat/redef/conic/cones.h @@ -21,7 +21,7 @@ namespace mp { /// /// @param MCType: ModelConverter, e.g., FlatConverter. /// @param Con: the source constraint type. -/// @param ConvertBase: another class +/// @param CvtBase: another class /// providing conversion methods DoRun /// for quadratic and linear constraints. template class CvtBase> @@ -77,51 +77,38 @@ class ConicConverter : public MCKeeper { // We _could_ walk everything just once // and see which cones are there. // Or, even walk expression trees from exponents, etc. - RunQCones(); - RunExpCones(); + // But we walk & convert at once, because later we can + // reconvert SOCP to QC in std forms. + if (MC().IfPassSOCPCones() >= 2) + RunQConesFromQC(); + if (MC().IfPassSOCPCones() >= 1) + RunQConesFromNonQC(); + if (MC().IfPassExpCones()) + RunExpCones(); + if (IfNeedSOCP2QC()) + SetupSOCP2QC(); + WarnOnMix(); } protected: - void RunQCones() { - if (MC().IfPassSOCPCones()) { // convert everything to QuadraticCones. - Walk(); - Walk(); - Walk(); - - if (MC().GetNumberOfAddable((PowConstraint*)0)>0 || - MC().GetNumberOfAddable((AbsConstraint*)0)>0) { - Walk(); - Walk(); - Walk(); - } + void RunQConesFromQC() { + Walk(); + Walk(); + Walk(); + } - if ( ! MC().ModelAPICanMixConicQCAndQC()) { // cannot mix - if (MC().NumQC2SOCPAttempted() > MC().NumQC2SOCPSucceeded() - && MC().NumQC2SOCPSucceeded()) { - MC().AddWarning("Mix QC+SOCP", - "Not all quadratic constraints could " - "be recognized\nas quadratic cones; " - "solver might not accept the model.\n" - "Try option cvt:socp=0 to leave all " - "as quadratic."); - } - } - } else - if (MC().IfPassQuadCon() && - (MC().GetNumberOfAddable((PowConstraint*)0)>0 || - MC().GetNumberOfAddable((AbsConstraint*)0)>0)) { - // Still collect QCones expressed by 2-norms. - // They are to be converted to quadratics. - Walk(); - Walk(); - Walk(); - } + void RunQConesFromNonQC() { + if (MC().GetNumberOfAddable((PowConstraint*)0)>0 || + MC().GetNumberOfAddable((AbsConstraint*)0)>0) { + Walk(); + Walk(); + Walk(); + } } void RunExpCones() { - if (MC().IfPassExpCones() && // convert everything to ExpCones. - MC().GetNumberOfAddable((ExpConstraint*)0)>0) { + if (MC().GetNumberOfAddable((ExpConstraint*)0)>0) { Walk(); Walk(); // also ExpA ?? Walk(); @@ -132,6 +119,47 @@ class ConicConverter : public MCKeeper { } } + bool IfNeedSOCP2QC() { + return + MC().SOCP2QCMode() >= 2 // compulsory + || (1 == MC().SOCP2QCMode() + && 0 == MC().NumExpConesRecognized() + // Might also have SOCP from sqrt(), abs(). + // Some QC -> SOCP but not all, even if 0 succeeded. + && (MC().NumQC2SOCPAttempted() > MC().NumQC2SOCPSucceeded() + || MC().HasQPObjective()) // or a quadratic obj + ); // Mosek 10 considers QP obj as a constraint for this + } + + void SetupSOCP2QC() { + MC().Setup2ConvertSOCP2QC(); + } + + void WarnOnMix() { + if ( !MC().ModelAPICanMixConicQCAndQC()) { // cannot mix + if ((MC().NumExpConesRecognized() // exp cones + && (MC().NumQC2SOCPAttempted() > MC().NumQC2SOCPSucceeded() + || MC().HasQPObjective())) // and quadratics left in + || // Some QC -> SOCP but not all + (((MC().NumQC2SOCPAttempted() > MC().NumQC2SOCPSucceeded() + && !MC().IfConvertSOCP2QC()) // and not decided to convert + || MC().HasQPObjective()) // or a quadratic obj + && MC().NumQC2SOCPSucceeded()) // Warn only if some succeeded + ) { + MC().AddWarning("Mix QC+cones", + "Not all quadratic constraints could " + "be recognized\nas quadratic cones; " + "or, the objective is quadratic;\n" + "additionally, further convertion back to QC\n" + "not desired (option cvt:socp2qc) or other cone types present;\n" + "solver might not accept the model.\n" + "Try to express all SOCP cones in standard forms,\n" + "not in the objective.\n" + "See mp.ampl.com/model-guide.html#"); + } + } + } + /// Walk a single constraint type template class CvtBase> void Walk() { @@ -791,6 +819,7 @@ class Convert1ExpC : public MCKeeper { } MC().AddConstraint( ExponentialConeConstraint(args, coefs)); + MC().IncExpConeCounter(); return true; } diff --git a/include/mp/flat/redef/conic/qcones2qc.h b/include/mp/flat/redef/conic/qcones2qc.h index 3ef1540e4..bbd4a07b5 100644 --- a/include/mp/flat/redef/conic/qcones2qc.h +++ b/include/mp/flat/redef/conic/qcones2qc.h @@ -23,8 +23,7 @@ class QConeConverter : /// Check whether the constraint /// needs to be converted despite being accepted by ModelAPI. bool IfNeedsConversion(const ItemType& , int ) { - return 0==GetMC().IfPassSOCPCones() && - 0!=GetMC().IfPassQuadCon(); + return GetMC().IfConvertSOCP2QC(); } /// Convert to @@ -63,8 +62,7 @@ class RQConeConverter : /// Check whether the constraint /// needs to be converted despite being accepted by ModelAPI. bool IfNeedsConversion(const ItemType& , int ) { - return 0==GetMC().IfPassSOCPCones() && - 0!=GetMC().IfPassQuadCon(); + return GetMC().IfConvertSOCP2QC(); } /// Convert to 2(c[0]*x[0]*c[1]*x[1]) >= sum(i>=2)((c[i]*x[i])^2). diff --git a/solvers/gurobi/gurobimodelapi.h b/solvers/gurobi/gurobimodelapi.h index c77f8eb38..2aeb4d721 100644 --- a/solvers/gurobi/gurobimodelapi.h +++ b/solvers/gurobi/gurobimodelapi.h @@ -65,6 +65,11 @@ class GurobiModelAPI : /// (Gurobi needs option nonconvex=2 for solving) static constexpr bool AcceptsNonconvexQC() { return true; } + /// Ask if the solver can recognize SOCP corner cases + /// (non-std representations such as xy>=1, see tests) + /// from quadratic representations + static constexpr bool CanSOCPCornerCasesFromQC() { return true; } + /// If using quadratics, /// QuadCon(LE/EQ/GE) should have 'Recommended' /// and have an implementation. diff --git a/solvers/visitor/visitorcommon.h b/solvers/visitor/visitorcommon.h index f6e1a01d9..16866784c 100644 --- a/solvers/visitor/visitorcommon.h +++ b/solvers/visitor/visitorcommon.h @@ -2,6 +2,7 @@ #define VISITORCOMMON_H #include +#include #include "mp/backend-to-model-api.h" @@ -79,7 +80,7 @@ namespace Solver { return nobj_; if (a == ISQOBJ) return quadObj_; - + return -1; } void allocateVars(int nvars) { vars_.resize(nvars); diff --git a/solvers/visitor/visitormodelapi.h b/solvers/visitor/visitormodelapi.h index d40020526..d30b2c045 100644 --- a/solvers/visitor/visitormodelapi.h +++ b/solvers/visitor/visitormodelapi.h @@ -86,16 +86,27 @@ class VisitorModelAPI : ACCEPT_CONSTRAINT(QuadConGE, Recommended, CG_Quadratic) void AddConstraint(const QuadConGE& qc); + /// SOCP flags + /////////////////////////////////////////////////////////////////////// /// Ask if the solver can mix conic quadratic /// (entered via dedicated API) and direct quadratic constraints static constexpr bool CanMixConicQCAndQC() { return false; } - /// Cones + /// Ask if the solver can recognize SOCP corner cases + /// (non-std representations such as xy>=1, see tests) + /// from quadratic representations + static constexpr bool CanSOCPCornerCasesFromQC() { return false; } + + /// Cones: SOCP ACCEPT_CONSTRAINT(QuadraticConeConstraint, Recommended, CG_Conic) void AddConstraint(const QuadraticConeConstraint& qc); ACCEPT_CONSTRAINT(RotatedQuadraticConeConstraint, Recommended, CG_Conic) void AddConstraint(const RotatedQuadraticConeConstraint& qc); + // Other cones + ACCEPT_CONSTRAINT(ExponentialConeConstraint, Recommended, CG_Conic) + void AddConstraint(const ExponentialConeConstraint& qc); + /// Linear indicator constraints can be used as /// auxiliary constraints for logical conditions. diff --git a/src/solver.cc b/src/solver.cc index ba347068f..90cb55adf 100644 --- a/src/solver.cc +++ b/src/solver.cc @@ -357,11 +357,7 @@ bool SolverAppOptionParser::ShowSolverOptionsASL() { } bool contains(const char* name, const char* substr) { - std::string n(name); - std::string sub {substr}; - if (sub.empty() || sub.back()!=':') // does not end with ':' - sub.push_back(':'); - return n.find(sub) != std::string::npos; + return std::strstr(name, substr); } bool SolverAppOptionParser::ShowSolverOptions(const char* param) {