diff --git a/ortools/java/com/google/ortools/modelbuilder/ModelSolver.java b/ortools/java/com/google/ortools/modelbuilder/ModelSolver.java index 453d54b3ee..9016384996 100644 --- a/ortools/java/com/google/ortools/modelbuilder/ModelSolver.java +++ b/ortools/java/com/google/ortools/modelbuilder/ModelSolver.java @@ -98,7 +98,7 @@ public final class ModelSolver { return helper.getBestObjectiveBound(); } - /** Checks that the solver has found a solution, and value of the given variable. */ + /** Checks that the solver has found a solution, and returns the value of the given variable. */ public double getValue(Variable var) { if (!helper.hasSolution()) { throw new ModelSolverException( @@ -106,7 +106,10 @@ public final class ModelSolver { } return helper.getVariableValue(var.getIndex()); } - /** Checks that the solver has found a solution, and reduced cost of the given variable. */ + /** + * Checks that the solver has found a solution, and returns the reduced cost of the given + * variable. + */ public double getReducedCost(Variable var) { if (!helper.hasSolution()) { throw new ModelSolverException( @@ -115,7 +118,9 @@ public final class ModelSolver { return helper.getReducedCost(var.getIndex()); } - /** Checks that the solver has found a solution, and dual value of the given constraint. */ + /** + * Checks that the solver has found a solution, and returns the ual value of the given constraint. + */ public double getDualValue(LinearConstraint ct) { if (!helper.hasSolution()) { throw new ModelSolverException( @@ -124,6 +129,17 @@ public final class ModelSolver { return helper.getDualValue(ct.getIndex()); } + /** + * Checks that the solver has found a solution, and returns the activity of the given constraint. + */ + public double getActivity(LinearConstraint ct) { + if (!helper.hasSolution()) { + throw new ModelSolverException( + "ModelSolver.getActivity())", "solve() was not called or no solution was found"); + } + return helper.getActivity(ct.getIndex()); + } + /** Sets the log callback for the solver. */ public void setLogCallback(Consumer cb) { this.logCallback = cb; diff --git a/ortools/linear_solver/java/BUILD.bazel b/ortools/linear_solver/java/BUILD.bazel index 3f042ddee2..ca0c48a23e 100644 --- a/ortools/linear_solver/java/BUILD.bazel +++ b/ortools/linear_solver/java/BUILD.bazel @@ -14,6 +14,8 @@ # Description: java wrapping of the C++ code at ../ load("//bazel:swig_java.bzl", "ortools_java_wrap_cc") +load("@rules_jvm_external//:defs.bzl", "artifact") +load("@contrib_rules_jvm//java:defs.bzl", "java_junit5_test") ortools_java_wrap_cc( name = "modelbuilder", @@ -30,3 +32,20 @@ ortools_java_wrap_cc( "//ortools/linear_solver/wrappers:model_builder_helper", ], ) + +java_junit5_test( + name = "ModelBuilderTest", + srcs = ["ModelBuilderTest.java"], + test_class = "com.google.ortools.modelbuilder.ModelBuilderTest", + deps = [ + "//ortools/java/com/google/ortools:Loader", + "//ortools/java/com/google/ortools/modelbuilder", + "//ortools/linear_solver/java:modelbuilder", + "@maven//:com_google_truth_truth", + artifact("org.junit.jupiter:junit-jupiter-api"), + artifact("org.junit.jupiter:junit-jupiter-params"), + artifact("org.junit.jupiter:junit-jupiter-engine"), + artifact("org.junit.platform:junit-platform-launcher"), + artifact("org.junit.platform:junit-platform-reporting"), + ], +) \ No newline at end of file diff --git a/ortools/linear_solver/java/ModelBuilderTest.java b/ortools/linear_solver/java/ModelBuilderTest.java index 10390ff1c8..f77552bb75 100644 --- a/ortools/linear_solver/java/ModelBuilderTest.java +++ b/ortools/linear_solver/java/ModelBuilderTest.java @@ -84,6 +84,10 @@ public final class ModelBuilderTest { .isWithin(1e-5) .of(4.0 - 1.0 * solver.getDualValue(c0) - 5.0 * solver.getDualValue(c1)); + assertThat(solver.getActivity(c0)).isWithin(1e-5).of(100.0); + assertThat(solver.getActivity(c1)).isWithin(1e-5).of(600.0); + assertThat(solver.getActivity(c2)).isWithin(1e-5).of(200.0); + assertThat(model.exportToLpString(false)).contains("minimal_linear_example"); assertThat(model.exportToMpsString(false)).contains("minimal_linear_example"); } diff --git a/ortools/linear_solver/java/modelbuilder.i b/ortools/linear_solver/java/modelbuilder.i index d15e08b175..9ec019831d 100644 --- a/ortools/linear_solver/java/modelbuilder.i +++ b/ortools/linear_solver/java/modelbuilder.i @@ -168,6 +168,7 @@ class GlobalRefGuard { %rename (getVariableValue) operations_research::ModelSolverHelper::variable_value; %rename (getReducedCost) operations_research::ModelSolverHelper::reduced_cost; %rename (getDualValue) operations_research::ModelSolverHelper::dual_value; +%rename (getActivity) operations_research::ModelSolverHelper::activity; %rename (getStatusString) operations_research::ModelSolverHelper::status_string; %rename (getWallTime) operations_research::ModelSolverHelper::wall_time; %rename (getUserTime) operations_research::ModelSolverHelper::user_time; diff --git a/ortools/linear_solver/python/model_builder.py b/ortools/linear_solver/python/model_builder.py index 2ee47dfb1d..0315ef1680 100644 --- a/ortools/linear_solver/python/model_builder.py +++ b/ortools/linear_solver/python/model_builder.py @@ -694,7 +694,7 @@ def dot_variable_container( if len(container.shape) != 1: raise ValueError( 'dot_variable_container only supports 1D variable containers (shape =' - f' {container.shape}') + f' {container.shape})') indices: npt.NDArray[np.int32] = container.variable_indices if np.isscalar(arg): return _WeightedSum( @@ -1317,6 +1317,11 @@ class ModelSolver: self.__check_has_feasible_solution() return self.__solve_helper.dual_value(ct.index) + def activity(self, ct: LinearConstraint) -> np.double: + """Returns the activity of a linear constraint after solve.""" + self.__check_has_feasible_solution() + return self.__solve_helper.activity(ct.index) + @property def objective_value(self) -> np.double: """Returns the value of the objective after solve.""" diff --git a/ortools/linear_solver/python/model_builder_test.py b/ortools/linear_solver/python/model_builder_test.py index be63caaa9a..528a8efec9 100644 --- a/ortools/linear_solver/python/model_builder_test.py +++ b/ortools/linear_solver/python/model_builder_test.py @@ -97,6 +97,16 @@ class ModelBuilderTest(unittest.TestCase): solver.reduced_cost(x3), places=self.NUM_PLACES) + self.assertAlmostEqual(100.0, + solver.activity(c0), + places=self.NUM_PLACES) + self.assertAlmostEqual(600.0, + solver.activity(c1), + places=self.NUM_PLACES) + self.assertAlmostEqual(200.0, + solver.activity(c2), + places=self.NUM_PLACES) + self.assertIn('minimal_linear_example', model.export_to_lp_string(False)) self.assertIn('minimal_linear_example', diff --git a/ortools/linear_solver/python/pywrap_model_builder_helper.cc b/ortools/linear_solver/python/pywrap_model_builder_helper.cc index 5e14e327a2..4fe626f0c0 100644 --- a/ortools/linear_solver/python/pywrap_model_builder_helper.cc +++ b/ortools/linear_solver/python/pywrap_model_builder_helper.cc @@ -388,6 +388,7 @@ PYBIND11_MODULE(pywrap_model_builder_helper, m) { .def("var_value", &ModelSolverHelper::variable_value, arg("var_index")) .def("reduced_cost", &ModelSolverHelper::reduced_cost, arg("var_index")) .def("dual_value", &ModelSolverHelper::dual_value, arg("ct_index")) + .def("activity", &ModelSolverHelper::activity, arg("ct_index")) .def("variable_values", [](const ModelSolverHelper& helper) { if (!helper.has_response()) { diff --git a/ortools/linear_solver/wrappers/model_builder_helper.cc b/ortools/linear_solver/wrappers/model_builder_helper.cc index a7104b0152..8fad58ccb7 100644 --- a/ortools/linear_solver/wrappers/model_builder_helper.cc +++ b/ortools/linear_solver/wrappers/model_builder_helper.cc @@ -13,7 +13,9 @@ #include "ortools/linear_solver/wrappers/model_builder_helper.h" +#include #include +#include #include #include #include @@ -340,6 +342,12 @@ void ModelSolverHelper::Solve(const ModelBuilderHelper& model) { MPSolverResponseStatus::MPSOLVER_SOLVER_TYPE_UNAVAILABLE); } } + if (response_->status() == MPSOLVER_OPTIMAL || + response_->status() == MPSOLVER_FEASIBLE) { + model_of_last_solve_ = &model.model(); + activities_.assign(model.num_constraints(), + std::numeric_limits::quiet_NaN()); + } } void ModelSolverHelper::SetLogCallback( @@ -410,6 +418,25 @@ double ModelSolverHelper::dual_value(int ct_index) const { return response_.value().dual_value(ct_index); } +double ModelSolverHelper::activity(int ct_index) { + if (!has_response() || ct_index >= activities_.size() || + !model_of_last_solve_.has_value()) { + return 0.0; + } + + if (std::isnan(activities_[ct_index])) { + const MPConstraintProto& ct_proto = + model_of_last_solve_.value()->constraint(ct_index); + double result = 0.0; + for (int i = 0; i < ct_proto.var_index_size(); ++i) { + result += response_->variable_value(ct_proto.var_index(i)) * + ct_proto.coefficient(i); + } + activities_[ct_index] = result; + } + return activities_[ct_index]; +} + std::string ModelSolverHelper::status_string() const { if (!has_response()) return ""; return response_.value().status_str(); diff --git a/ortools/linear_solver/wrappers/model_builder_helper.h b/ortools/linear_solver/wrappers/model_builder_helper.h index 14833e5e84..650aab408d 100644 --- a/ortools/linear_solver/wrappers/model_builder_helper.h +++ b/ortools/linear_solver/wrappers/model_builder_helper.h @@ -160,6 +160,7 @@ class ModelSolverHelper { double variable_value(int var_index) const; double reduced_cost(int var_index) const; double dual_value(int ct_index) const; + double activity(int ct_index); std::string status_string() const; double wall_time() const; @@ -180,6 +181,8 @@ class ModelSolverHelper { std::optional solver_type_; std::optional time_limit_in_second_; std::string solver_specific_parameters_; + std::optional model_of_last_solve_; + std::vector activities_; bool solver_output_ = false; }; diff --git a/ortools/lp_data/sparse_vector.h b/ortools/lp_data/sparse_vector.h index 2502f60b8c..14ba23f179 100644 --- a/ortools/lp_data/sparse_vector.h +++ b/ortools/lp_data/sparse_vector.h @@ -34,6 +34,7 @@ #include #include #include +#include #include "absl/strings/str_format.h" #include "ortools/base/integral_types.h"