From 063687c807c99db587c44707a7f52cfe585aceb7 Mon Sep 17 00:00:00 2001 From: Gleb Belov Date: Fri, 24 May 2024 20:50:49 +1000 Subject: [PATCH] obj:multi:weight: intuitive objective weights #240 --- include/mp/backend-std.h | 7 +++--- include/mp/converter-base.h | 3 +++ include/mp/flat/converter.h | 3 +++ include/mp/flat/converter_multiobj.h | 23 +++++++++++++++++-- include/mp/flat/obj_std.h | 2 ++ include/mp/flat/problem_flattener.h | 4 ++++ include/mp/model-mgr-base.h | 3 +++ include/mp/model-mgr-with-pb.h | 4 ++++ include/mp/solver-base.h | 4 ++++ solvers/visitor/visitorbackend.h | 4 +++- src/solver.cc | 21 +++++++++++++++-- .../categorized/fast/multi_obj/modellist.json | 12 ++++++++++ 12 files changed, 82 insertions(+), 8 deletions(-) diff --git a/include/mp/backend-std.h b/include/mp/backend-std.h index ba20ecf79..ded1e83a4 100644 --- a/include/mp/backend-std.h +++ b/include/mp/backend-std.h @@ -116,7 +116,9 @@ class StdBackend : virtual void ObjPriorities(ArrayRef) { MP_UNSUPPORTED("Backend::ObjPriorities"); } /// Placeholder: set objective weights. - /// Presolve the values if needed + /// Presolve the values if needed. + /// @note The weights are provided in the format common for many solver APIs, + /// corresponding to the legacy setting of the option obj:multi:weight. virtual void ObjWeights(ArrayRef) { } /// Placeholder: set objective abs tol /// Presolve the values if needed @@ -290,7 +292,7 @@ class StdBackend : if (multiobj() && multiobj_has_native()) { if (auto suf = ReadSuffix(suf_objpriority)) ObjPriorities( suf ); - if (auto suf = ReadSuffix(suf_objweight)) + if (auto suf = GetMM().GetObjWeightsAdapted()) ObjWeights( suf ); if (auto suf = ReadSuffix(suf_objabstol)) ObjAbsTol( suf ); @@ -957,7 +959,6 @@ class StdBackend : ////////////////////////////////////////////////////////////////////////////// private: const SuffixDef suf_objpriority = { "objpriority", suf::OBJ | suf::INPUT }; - const SuffixDef suf_objweight = { "objweight", suf::OBJ | suf::INPUT }; const SuffixDef suf_objabstol = { "objabstol", suf::OBJ | suf::INPUT }; const SuffixDef suf_objreltol = { "objreltol", suf::OBJ | suf::INPUT }; diff --git a/include/mp/converter-base.h b/include/mp/converter-base.h index c1f37da4a..37dd516fc 100644 --- a/include/mp/converter-base.h +++ b/include/mp/converter-base.h @@ -38,6 +38,9 @@ class BasicConverter : public EnvKeeper { /// Process solve iteration solution virtual void ProcessIterationSolution(const Solution& , int status) = 0; + /// Objective weights + virtual ArrayRef GetObjWeightsAdapted() = 0; + /// Fill model traits virtual void FillModelTraits(AMPLS_ModelTraits& ) = 0; diff --git a/include/mp/flat/converter.h b/include/mp/flat/converter.h index 8289346f9..fbd16ac54 100644 --- a/include/mp/flat/converter.h +++ b/include/mp/flat/converter.h @@ -243,6 +243,9 @@ class FlatConverter : MPD( ProcessMOIterationPostsolvedSolution(sol, status) ); } + /// Objective weights + ArrayRef GetObjWeightsAdapted() { return MPD( GetMOWeightsLegacy() ); } + protected: //////////////////////////// CUSTOM CONSTRAINTS CONVERSION //////////////////////////// diff --git a/include/mp/flat/converter_multiobj.h b/include/mp/flat/converter_multiobj.h index 2c0c5c1ce..f5c9fb60e 100644 --- a/include/mp/flat/converter_multiobj.h +++ b/include/mp/flat/converter_multiobj.h @@ -108,8 +108,11 @@ class MOManager { ///////////////// Read / set default suffixes /////////////////// std::vector objpr = MPD( ReadDblSuffix( {"objpriority", suf::OBJ} ) ); objpr.resize(obj_orig.size(), 0.0); // blend objectives by default - std::vector objwgt = MPD( ReadDblSuffix( {"objweight", suf::OBJ} ) ); - objwgt.resize(obj_orig.size(), 1.0); + std::vector objwgt = MPD( GetMOWeightsLegacy() ); + if (objwgt.empty()) { + objwgt.resize(obj_orig.size(), 1.0); // Default "intuitive" weights + FlipDiffSenseSigns(objwgt); // We handle "legacy" format below + } std::vector objtola = MPD( ReadDblSuffix( {"objabstol", suf::OBJ} ) ); objtola.resize(obj_orig.size(), 0.0); std::vector objtolr = MPD( ReadDblSuffix( {"objreltol", suf::OBJ} ) ); @@ -124,6 +127,7 @@ class MOManager { for (const auto& pr_level: pr_map) { const auto& i0_vec = pr_level.second; obj_new_.push_back(obj_orig.at(i0_vec.front())); + obj_new_.back().set_sense(obj_orig.front().obj_sense()); // "Legacy" obj:multi:weight obj_new_.back().GetLinTerms() *= objwgt.at(i0_vec.front()); // Use weight obj_new_.back().GetQPTerms() *= objwgt.at(i0_vec.front()); obj_new_tola_.push_back(objtola.at(i0_vec.front())); @@ -210,6 +214,21 @@ class MOManager { MPD( SetObjectiveTo( MPD(GetModelAPI()), 0, obj_new_[i_current_obj_]) ); } + ArrayRef GetMOWeightsLegacy() { + std::vector objw = MPD( ReadDblSuffix( {"objweight", suf::OBJ} ) ); + if (objw.size() && 2==MPD( GetEnv() ).multiobj_weight()) { // user wants "intuitive" + FlipDiffSenseSigns(objw); // Backend / Emulator want "legacy" + } + return objw; + } + + /// Convert between the options of obj:multi:weight + void FlipDiffSenseSigns(std::vector& objw) { + const auto& obj = MPD( get_objectives() ); + for (auto i=obj.size(); --i; ) // forall i>1 + if (obj[i].obj_sense() != obj.front().obj_sense()) + objw[i] = -objw[i]; + } private: MOManagerStatus status_ {MOManagerStatus::NOT_SET}; diff --git a/include/mp/flat/obj_std.h b/include/mp/flat/obj_std.h index 6a4c785f4..768335c9e 100644 --- a/include/mp/flat/obj_std.h +++ b/include/mp/flat/obj_std.h @@ -26,6 +26,8 @@ class LinearObjective { name_(std::move(nm)){ } /// Get sense obj::Type obj_sense() const { return sense_; } + /// Set sense + void set_sense(obj::Type s) { sense_ = s; } /// Get lin terms, const const LinTerms& GetLinTerms() const { return lt_; } /// Get lin terms diff --git a/include/mp/flat/problem_flattener.h b/include/mp/flat/problem_flattener.h index c074e50e8..b43264bf3 100644 --- a/include/mp/flat/problem_flattener.h +++ b/include/mp/flat/problem_flattener.h @@ -134,6 +134,10 @@ class ProblemFlattener : void ProcessIterationSolution(const Solution& sol, int status) override { GetFlatCvt().ProcessSolveIterationSolution(sol, status); } + /// Objective weights + ArrayRef GetObjWeightsAdapted() override + { return GetFlatCvt().GetObjWeightsAdapted(); } + /// Fill model traits void FillModelTraits(AMPLS_ModelTraits& mt) override { diff --git a/include/mp/model-mgr-base.h b/include/mp/model-mgr-base.h index e5138213d..56f00b99f 100644 --- a/include/mp/model-mgr-base.h +++ b/include/mp/model-mgr-base.h @@ -76,6 +76,9 @@ class BasicModelManager { /// Process solve iteration solution virtual void ProcessIterationSolution(const Solution& , int status) = 0; + /// Objective weights in the 'legacy' format of the obj:multi:weight option + virtual ArrayRef GetObjWeightsAdapted() = 0; + /// Integrality flags of the variables in the original instance. /// Used for solution rounding virtual const std::vector& IsVarInt() const = 0; diff --git a/include/mp/model-mgr-with-pb.h b/include/mp/model-mgr-with-pb.h index aebb6c74b..888781df8 100644 --- a/include/mp/model-mgr-with-pb.h +++ b/include/mp/model-mgr-with-pb.h @@ -294,6 +294,10 @@ class ModelManagerWithProblemBuilder : void ProcessIterationSolution(const Solution& sol, int status) override { GetCvt().ProcessIterationSolution(sol, status); } + /// Objective weights + ArrayRef GetObjWeightsAdapted() override + { return GetCvt().GetObjWeightsAdapted(); } + const std::vector& IsVarInt() const override { return GetModel().IsVarInt(); diff --git a/include/mp/solver-base.h b/include/mp/solver-base.h index 1377900cb..f86f93dd8 100644 --- a/include/mp/solver-base.h +++ b/include/mp/solver-base.h @@ -318,6 +318,9 @@ class BasicSolver : private ErrorHandler, /// Whether the solver natively supports multiobj bool multiobj_has_native() const { return multiobj_has_native_; } + /// Option obj:multi:weight + int multiobj_weight() const { return multiobj_weight_; } + /// >0 if the timing is enabled int timing() const { return timing_; } @@ -611,6 +614,7 @@ class BasicSolver : private ErrorHandler, bool multiobj_ {false}; bool multiobj_has_native_ {false}; + int multiobj_weight_ {2}; bool has_errors_ {false}; OutputHandler *output_handler_ {this}; diff --git a/solvers/visitor/visitorbackend.h b/solvers/visitor/visitorbackend.h index 42fdbb5df..41c9fc84d 100644 --- a/solvers/visitor/visitorbackend.h +++ b/solvers/visitor/visitorbackend.h @@ -54,7 +54,9 @@ class VisitorBackend : /////////////// OPTIONAL STANDARD FEATURES ///////////////// //////////////////////////////////////////////////////////// // Use this section to declare and implement some standard features - // that may or may not need additional functions. + // that may or may not need additional functions. + // For a full list of features possible, + // grep "STD_FEATURE". USING_STD_FEATURES; /** diff --git a/src/solver.cc b/src/solver.cc index bf80c777f..ae756623e 100644 --- a/src/solver.cc +++ b/src/solver.cc @@ -652,10 +652,27 @@ void BasicSolver::InitMetaInfoAndOptions( ".objpriority, .objweight, .objreltol, and .objabstol on the " "objectives are relevant. Objectives with greater .objpriority " "values (integer values) have higher priority. Objectives with " - "the same .objpriority are weighted by .objweight. Objectives " + "the same .objpriority are weighted by .objweight, " + "according to the option obj:multi:weight.\n" + "\n" + "Objectives " "with positive .objabstol or .objreltol are allowed to be " "degraded by lower priority objectives by amounts not exceeding " - "the .objabstol (absolute) and .objreltol (relative) limits. " ))); + "the .objabstol (absolute) and .objreltol (relative) limits. "))); + + static const mp::OptionValueInfo values_multiobjweight_[] = { + { "1", "relative to the sense of the 1st objective", 1}, + { "2", "relative to its own sense (default)", 2} + }; + AddStoredOption("obj:multi:weight multiobjweight obj:multi:weights multiobjweights", + "How to interpret each objective's weight sign:\n" + "\n.. value-table::\n\n" + "With the 1st option (legacy behaviour), negative .objweight " + "for objective i would make " + "objective i's sense the opposite of the model's 1st objective. " + "Otherwise, it would make objective i's sense the opposite to its sense " + "defined in the model.", + multiobj_weight_, values_multiobjweight_); } AddIntOption("tech:timing timing tech:report_times report_times", diff --git a/test/end2end/cases/categorized/fast/multi_obj/modellist.json b/test/end2end/cases/categorized/fast/multi_obj/modellist.json index 8e377779b..05fc26ccb 100644 --- a/test/end2end/cases/categorized/fast/multi_obj/modellist.json +++ b/test/end2end/cases/categorized/fast/multi_obj/modellist.json @@ -85,6 +85,18 @@ "name" : "obj_suf_01 multiobj=1", "tags" : ["linear", "continuous", "multiobj"], "options": { "ANYSOLVER_options": "multiobj=1" }, + "values": { + "x": 1, + "y": 0, + "z": 0, + "_sobj[1]": 1, + "_sobj[2]": 0 + } + }, + { + "name" : "obj_suf_01 multiobj=1 obj:multi:weight=1", + "tags" : ["linear", "continuous", "multiobj"], + "options": { "ANYSOLVER_options": "multiobj=1 obj:multi:weight=1" }, "values": { "x": 0, "y": 1,