20#include "absl/status/status.h"
21#include "absl/status/statusor.h"
22#include "absl/strings/string_view.h"
23#include "absl/time/time.h"
24#include "gtest/gtest.h"
30#include "ortools/math_opt/model.pb.h"
31#include "ortools/math_opt/model_update.pb.h"
32#include "ortools/math_opt/result.pb.h"
37using ::testing::AnyOf;
38using ::testing::DoubleNear;
39using ::testing::HasSubstr;
44 "This test is disabled as the solver does not support multiple objective "
47 "This test is disabled as the solver does not support integer variables";
70 <<
", supports_auxiliary_objectives: "
72 <<
", supports_incremental_objective_add_and_delete: "
75 <<
", supports_incremental_objective_modification: "
77 <<
", supports_integer_variables: "
84TEST_P(SimpleMultiObjectiveTest, CanBuildMultiObjectiveModel) {
86 const Variable x = model.AddContinuousVariable(0.0, 1.0,
"x");
87 model.AddMaximizationObjective(
x, 2);
88 model.AddMinimizationObjective(-3.0 *
x + 2.0, 1);
90 if (GetParam().supports_auxiliary_objectives) {
94 StatusIs(AnyOf(absl::StatusCode::kInvalidArgument,
95 absl::StatusCode::kUnimplemented),
96 HasSubstr(
"multiple objectives")));
108 if (!GetParam().supports_auxiliary_objectives) {
112 const Variable x = model.AddContinuousVariable(0.0, 1.0,
"x");
113 const Variable y = model.AddContinuousVariable(0.0, 1.0,
"y");
114 model.AddLinearConstraint(
x + y <= 1.5);
117 model.AddMaximizationObjective(
x + 3.0 * y + 2.0, 1);
121 EXPECT_EQ(result.objective_value(o), 4.5);
132 if (!GetParam().supports_auxiliary_objectives) {
136 const Variable x = model.AddContinuousVariable(0.0, 1.0,
"x");
137 const Variable y = model.AddContinuousVariable(0.0, 1.0,
"y");
138 model.AddLinearConstraint(
x + y <= 1.5);
141 model.AddMaximizationObjective(
x + 3.0 * y + 2.0, 1);
145 EXPECT_EQ(result.objective_value(o), 4.5);
149 if (!GetParam().supports_auxiliary_objectives) {
153 model.set_objective_priority(model.primary_objective(), 1);
154 model.AddAuxiliaryObjective(1);
156 StatusIs(absl::StatusCode::kInvalidArgument,
157 HasSubstr(
"repeated objective priority: 1")));
161 if (!GetParam().supports_auxiliary_objectives) {
165 model.AddAuxiliaryObjective(1);
166 model.AddAuxiliaryObjective(1);
168 StatusIs(absl::StatusCode::kInvalidArgument,
169 HasSubstr(
"repeated objective priority: 1")));
173struct SimpleMultiObjectiveSolveResult {
175 double solution = 0.0;
176 double priority_0_objective_value = 0.0;
177 double priority_1_objective_value = 0.0;
180enum class ObjectiveType { kPrimary, kAuxiliary };
181enum class ToleranceType { kAbsolute, kRelative };
194absl::StatusOr<SimpleMultiObjectiveSolveResult> SolveWithObjectiveDegradation(
196 const ObjectiveType priority_0_type,
const ObjectiveType priority_1_type,
197 const ToleranceType tolerance_type) {
198 if (priority_0_type == ObjectiveType::kPrimary &&
199 priority_1_type == ObjectiveType::kPrimary) {
200 return absl::InvalidArgumentError(
"Both objectives cannot both be primary");
203 const Variable x = model.AddIntegerVariable(0.0, 2.0,
"x");
205 switch (priority_0_type) {
206 case ObjectiveType::kPrimary:
208 model.set_objective_priority(model.primary_objective(), 0);
209 return model.primary_objective();
210 case ObjectiveType::kAuxiliary:
211 return model.AddMaximizationObjective(
x, 0);
215 switch (priority_1_type) {
216 case ObjectiveType::kPrimary:
218 model.set_objective_priority(model.primary_objective(), 1);
219 return model.primary_objective();
220 case ObjectiveType::kAuxiliary:
221 return model.AddMinimizationObjective(
x, 1);
225 switch (tolerance_type) {
226 case ToleranceType::kAbsolute:
227 model_parameters.objective_parameters[priority_0]
228 .objective_degradation_absolute_tolerance = 1.0;
230 case ToleranceType::kRelative:
231 model_parameters.objective_parameters[priority_0]
232 .objective_degradation_relative_tolerance = 0.5;
235 const SolveResult result,
236 Solve(model, solver_type,
237 {.parameters = parameters, .model_parameters = model_parameters}));
238 if (!result.has_primal_feasible_solution()) {
239 return absl::InternalError(
"No feasible solution found");
241 return SimpleMultiObjectiveSolveResult{
242 .termination = result.termination.reason,
243 .solution = result.best_primal_solution().variable_values.at(
x),
244 .priority_0_objective_value = result.objective_value(priority_0),
245 .priority_1_objective_value = result.objective_value(priority_1)};
249 if (!GetParam().supports_auxiliary_objectives) {
253 SolveWithObjectiveDegradation(
254 GetParam().solver_type, GetParam().parameters,
255 ObjectiveType::kPrimary,
256 ObjectiveType::kAuxiliary,
257 ToleranceType::kAbsolute));
265 AuxiliaryObjectiveDegradationAbsoluteTolerance) {
266 if (!GetParam().supports_auxiliary_objectives) {
270 SolveWithObjectiveDegradation(
271 GetParam().solver_type, GetParam().parameters,
272 ObjectiveType::kAuxiliary,
273 ObjectiveType::kPrimary,
274 ToleranceType::kAbsolute));
282 if (!GetParam().supports_auxiliary_objectives) {
286 SolveWithObjectiveDegradation(
287 GetParam().solver_type, GetParam().parameters,
288 ObjectiveType::kPrimary,
289 ObjectiveType::kAuxiliary,
290 ToleranceType::kRelative));
300 SingleObjectiveModelWithObjectiveDegradationAbsoluteTolerance) {
301 if (!GetParam().supports_auxiliary_objectives) {
305 const Variable x = model.AddIntegerVariable(0.0, 1.0,
"x");
309 .objective_degradation_absolute_tolerance = 0.5;
311 Solve(model, GetParam().solver_type,
312 {.parameters = GetParam().parameters,
313 .model_parameters = model_parameters}));
320 SingleObjectiveModelWithObjectiveDegradationRelativeTolerance) {
321 if (!GetParam().supports_auxiliary_objectives) {
325 const Variable x = model.AddIntegerVariable(0.0, 1.0,
"x");
329 .objective_degradation_relative_tolerance = 0.5;
331 Solve(model, GetParam().solver_type,
332 {.parameters = GetParam().parameters,
333 .model_parameters = model_parameters}));
338 AuxiliaryObjectiveDegradationRelativeTolerance) {
339 if (!GetParam().supports_auxiliary_objectives) {
343 SolveWithObjectiveDegradation(
344 GetParam().solver_type, GetParam().parameters,
345 ObjectiveType::kAuxiliary,
346 ObjectiveType::kPrimary,
347 ToleranceType::kRelative));
359absl::StatusOr<std::unique_ptr<Model>> Load23588MiplibInstance() {
361 const ModelProto model_proto,
362 ReadMpsFile(absl::StrCat(
"ortools/math_opt/solver_tests/testdata/"
370 MultiObjectiveModelWithAuxiliaryObjectiveTimeLimit) {
371 if (!GetParam().supports_integer_variables) {
375 Load23588MiplibInstance());
376 const Objective aux_obj = model->AddMaximizationObjective(
377 model->primary_objective().AsLinearExpression(), 1);
378 model->clear_objective();
380 .parameters = GetParam().parameters,
381 .model_parameters = {
382 .objective_parameters = {
383 {aux_obj, {.time_limit = absl::Milliseconds(1)}}}}};
384 const auto result =
Solve(*model, GetParam().solver_type, args);
385 if (!GetParam().supports_auxiliary_objectives) {
387 HasSubstr(
"multiple objectives")));
395 EXPECT_LE(result->solve_stats.solve_time, absl::Seconds(1));
401 MultiObjectiveModelWithPrimaryObjectiveTimeLimit) {
402 if (!GetParam().supports_integer_variables) {
406 Load23588MiplibInstance());
407 model->AddMaximizationObjective(0, 1);
409 .parameters = GetParam().parameters,
410 .model_parameters = {
411 .objective_parameters = {{model->primary_objective(),
412 {.time_limit = absl::Milliseconds(1)}}}}};
413 const auto result =
Solve(*model, GetParam().solver_type, args);
414 if (!GetParam().supports_auxiliary_objectives) {
416 HasSubstr(
"multiple objectives")));
424 EXPECT_LE(result->solve_stats.solve_time, absl::Seconds(1));
431 SingleObjectiveModelWithPrimaryObjectiveTimeLimit) {
432 if (!GetParam().supports_auxiliary_objectives) {
435 if (!GetParam().supports_integer_variables) {
439 Load23588MiplibInstance());
441 .parameters = GetParam().parameters,
442 .model_parameters = {
443 .objective_parameters = {{model->primary_objective(),
444 {.time_limit = absl::Milliseconds(1)}}}}};
447 Solve(*model, GetParam().solver_type, args));
452 EXPECT_LE(result.solve_stats.solve_time, absl::Seconds(1));
459 SingleObjectiveModelTakesMininumFromPrimaryAndGlobalTimeLimits) {
460 if (!GetParam().supports_auxiliary_objectives) {
463 if (!GetParam().supports_integer_variables) {
467 Load23588MiplibInstance());
469 .parameters = GetParam().parameters,
470 .model_parameters = {
471 .objective_parameters = {{model->primary_objective(),
472 {.time_limit = absl::Seconds(10)}}}}};
475 Solve(*model, GetParam().solver_type, args));
480 EXPECT_LE(result.solve_stats.solve_time, absl::Seconds(1));
486 SolverWithoutSupportErrorsWithPrimaryObjectiveParameters) {
487 if (GetParam().supports_auxiliary_objectives) {
492 .parameters = GetParam().parameters,
493 .model_parameters = {
494 .objective_parameters = {
495 {model.primary_objective(), {.time_limit = absl::Seconds(10)}}}}};
497 StatusIs(absl::StatusCode::kInvalidArgument,
498 HasSubstr(
"primary_objective_parameters")));
519 const Variable x = model.AddContinuousVariable(0.0, 1.0,
"x");
520 const Variable y = model.AddContinuousVariable(0.0, 1.0,
"y");
521 model.AddLinearConstraint(
x + y <= 1.5);
529 ASSERT_THAT(solver->Solve({.parameters = GetParam().parameters}),
533 model.AddMaximizationObjective(
x + 3.0 * y + 2.0, 1);
535 if (!GetParam().supports_auxiliary_objectives) {
544 StatusIs(AnyOf(absl::StatusCode::kInvalidArgument,
545 absl::StatusCode::kUnimplemented),
546 AllOf(HasSubstr(
"multiple objective"),
548 Not(HasSubstr(
"update failed")),
550 HasSubstr(
"solver re-creation failed"))));
556 IsOkAndHolds(GetParam().supports_incremental_objective_add_and_delete
560 const SolveResult result,
561 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}));
563 EXPECT_EQ(result.objective_value(o), 4.5);
583 if (!GetParam().supports_auxiliary_objectives) {
587 const Variable x = model.AddContinuousVariable(0.0, 1.0,
"x");
588 const Variable y = model.AddContinuousVariable(0.0, 1.0,
"y");
589 model.AddLinearConstraint(
x + y <= 1.5);
591 const Objective o = model.AddMaximizationObjective(3.0, 2);
598 solver->Solve({.parameters = GetParam().parameters}));
600 EXPECT_EQ(result.objective_value(o), 3.0);
604 model.AddMaximizationObjective(
x + 3.0 * y + 2.0, 5);
608 IsOkAndHolds(GetParam().supports_incremental_objective_add_and_delete
612 const SolveResult result,
613 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}));
615 EXPECT_EQ(result.objective_value(o), 3.0);
616 EXPECT_EQ(result.objective_value(o2), 4.5);
636 if (!GetParam().supports_auxiliary_objectives) {
640 const Variable x = model.AddContinuousVariable(0.0, 1.0,
"x");
641 const Variable y = model.AddContinuousVariable(0.0, 1.0,
"y");
642 model.AddLinearConstraint(
x + y <= 1.5);
644 const Objective o = model.AddMaximizationObjective(3.0, 2);
646 model.AddMaximizationObjective(
x + 3.0 * y + 2.0, 3);
653 solver->Solve({.parameters = GetParam().parameters}));
655 EXPECT_EQ(result.objective_value(o), 3.0);
656 EXPECT_EQ(result.objective_value(o2), 4.5);
659 model.DeleteAuxiliaryObjective(o);
663 IsOkAndHolds(GetParam().supports_incremental_objective_add_and_delete
667 const SolveResult result,
668 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}));
670 EXPECT_EQ(result.objective_value(o2), 4.5);
690 ModifyPrimaryObjectiveSenseInMultiObjectiveModel) {
691 if (!GetParam().supports_auxiliary_objectives) {
695 const Variable x = model.AddContinuousVariable(0.0, 1.0,
"x");
696 const Variable y = model.AddContinuousVariable(0.0, 1.0,
"y");
697 model.AddLinearConstraint(
x + y <= 1.5);
700 model.AddMaximizationObjective(
x + 3.0 * y + 2.0, 2);
707 solver->Solve({.parameters = GetParam().parameters}));
710 EXPECT_EQ(result.objective_value(o), 4.5);
713 model.set_minimize();
717 IsOkAndHolds(GetParam().supports_incremental_objective_modification
721 const SolveResult result,
722 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}));
724 EXPECT_EQ(result.objective_value(o), 5.0);
730 ModifyAuxiliaryObjectiveSenseInMultiObjectiveModel) {
731 if (!GetParam().supports_auxiliary_objectives) {
735 const Variable x = model.AddContinuousVariable(0.0, 1.0,
"x");
736 const Variable y = model.AddContinuousVariable(0.0, 1.0,
"y");
737 model.AddLinearConstraint(
x + y <= 1.5);
739 model.set_objective_priority(model.primary_objective(), 2);
740 const Objective o = model.AddMaximizationObjective(
x, 0);
747 solver->Solve({.parameters = GetParam().parameters}));
749 EXPECT_EQ(result.objective_value(o), 1.0);
752 model.set_minimize(o);
756 IsOkAndHolds(GetParam().supports_incremental_objective_modification
760 const SolveResult result,
761 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}));
763 EXPECT_EQ(result.objective_value(o), 0.0);
#define ASSIGN_OR_RETURN(lhs, rexpr)
void Maximize(Variable *obj, std::vector< Annotation > search_annotations)
void Minimize(Variable *obj, std::vector< Annotation > search_annotations)
static absl::StatusOr< std::unique_ptr< Model > > FromModelProto(const ModelProto &model_proto)
An object oriented wrapper for quadratic constraints in ModelStorage.
Matcher< SolveResult > IsOptimalWithSolution(const double expected_objective, const VariableMap< double > expected_variable_values, const double tolerance)
EXPECT_THAT(ComputeInfeasibleSubsystem(model, GetParam().solver_type), IsOkAndHolds(IsInfeasible(true, ModelSubset{ .variable_bounds={{x, ModelSubset::Bounds{.lower=false,.upper=true}}},.linear_constraints={ {c, ModelSubset::Bounds{.lower=true,.upper=false}}}})))
TEST_P(InfeasibleSubsystemTest, CanComputeInfeasibleSubsystem)
SolverType
The solvers supported by MathOpt.
TerminationReason
The reason a call to Solve() terminates.
@ kOptimal
A provably optimal solution (up to numerical tolerances) has been found.
<=x<=1 IncrementalMipTest::IncrementalMipTest() :model_("incremental_solve_test"), x_(model_.AddContinuousVariable(0.0, 1.0, "x")), y_(model_.AddIntegerVariable(0.0, 2.0, "y")), c_(model_.AddLinearConstraint(0<=x_+y_<=1.5, "c")) { model_.Maximize(3.0 *x_+2.0 *y_+0.1);solver_=NewIncrementalSolver(&model_, TestedSolver()).value();const SolveResult first_solve=solver_->Solve().value();CHECK(first_solve.has_primal_feasible_solution());CHECK_LE(std::abs(first_solve.objective_value() - 3.6), kTolerance)<< first_solve.objective_value();} namespace { TEST_P(SimpleMipTest, OneVarMax) { Model model;const Variable x=model.AddVariable(0.0, 4.0, false, "x");model.Maximize(2.0 *x);ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(model, GetParam().solver_type));ASSERT_THAT(result, IsOptimal(8.0));EXPECT_THAT(result.variable_values(), IsNear({{x, 4.0}}));} TEST_P(SimpleMipTest, OneVarMin) { Model model;const Variable x=model.AddVariable(-2.4, 4.0, false, "x");model.Minimize(2.0 *x);ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(model, GetParam().solver_type));ASSERT_THAT(result, IsOptimal(-4.8));EXPECT_THAT(result.variable_values(), IsNear({{x, -2.4}}));} TEST_P(SimpleMipTest, OneIntegerVar) { Model model;const Variable x=model.AddVariable(0.0, 4.5, true, "x");model.Maximize(2.0 *x);ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(model, GetParam().solver_type));ASSERT_THAT(result, IsOptimal(8.0));EXPECT_THAT(result.variable_values(), IsNear({{x, 4.0}}));} TEST_P(SimpleMipTest, SimpleLinearConstraint) { Model model;const Variable x=model.AddBinaryVariable("x");const Variable y=model.AddBinaryVariable("y");model.Maximize(2.0 *x+y);model.AddLinearConstraint(0.0<=x+y<=1.5, "c");ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(model, GetParam().solver_type));ASSERT_THAT(result, IsOptimal(2.0));EXPECT_THAT(result.variable_values(), IsNear({{x, 1}, {y, 0}}));} TEST_P(SimpleMipTest, Unbounded) { Model model;const Variable x=model.AddVariable(0.0, kInf, true, "x");model.Maximize(2.0 *x);ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(model, GetParam().solver_type));if(GetParam().report_unboundness_correctly) { ASSERT_THAT(result, TerminatesWithOneOf({TerminationReason::kUnbounded, TerminationReason::kInfeasibleOrUnbounded}));} else { ASSERT_THAT(result, TerminatesWith(TerminationReason::kOtherError));} } TEST_P(SimpleMipTest, Infeasible) { Model model;const Variable x=model.AddVariable(0.0, 3.0, true, "x");model.Maximize(2.0 *x);model.AddLinearConstraint(x >=4.0);ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(model, GetParam().solver_type));ASSERT_THAT(result, TerminatesWith(TerminationReason::kInfeasible));} TEST_P(SimpleMipTest, FractionalBoundsContainNoInteger) { if(GetParam().solver_type==SolverType::kGurobi) { GTEST_SKIP()<< "TODO(b/272298816): Gurobi bindings are broken here.";} Model model;const Variable x=model.AddIntegerVariable(0.5, 0.6, "x");model.Maximize(x);EXPECT_THAT(Solve(model, GetParam().solver_type), IsOkAndHolds(TerminatesWith(TerminationReason::kInfeasible)));} TEST_P(IncrementalMipTest, EmptyUpdate) { ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));ASSERT_OK_AND_ASSIGN(const SolveResult result, solver_->SolveWithoutUpdate());ASSERT_THAT(result, IsOptimal(3.6));EXPECT_THAT(result.variable_values(), IsNear({{x_, 0.5}, {y_, 1.0}}));} TEST_P(IncrementalMipTest, MakeContinuous) { model_.set_continuous(y_);ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));ASSERT_OK_AND_ASSIGN(const SolveResult result, solver_->SolveWithoutUpdate());ASSERT_THAT(result, IsOptimal(4.1));EXPECT_THAT(result.variable_values(), IsNear({{x_, 1.0}, {y_, 0.5}}));} TEST_P(IncrementalMipTest, DISABLED_MakeContinuousWithNonIntegralBounds) { solver_.reset();Model model("bounds");const Variable x=model.AddIntegerVariable(0.5, 1.5, "x");model.Maximize(x);ASSERT_OK_AND_ASSIGN(const auto solver, NewIncrementalSolver(&model, TestedSolver()));ASSERT_THAT(solver->Solve(), IsOkAndHolds(IsOptimal(1.0)));model.set_continuous(x);ASSERT_THAT(solver->Update(), IsOkAndHolds(DidUpdate()));ASSERT_THAT(solver-> IsOkAndHolds(IsOptimal(1.5)))
ASSERT_THAT(solver->Update(), IsOkAndHolds(DidUpdate()))
absl::StatusOr< SolveResult > Solve(const Model &model, const SolverType solver_type, const SolveArguments &solve_args, const SolverInitArguments &init_args)
std::ostream & operator<<(std::ostream &ostr, const IndicatorConstraint &constraint)
absl::StatusOr< std::unique_ptr< IncrementalSolver > > NewIncrementalSolver(Model *model, SolverType solver_type, SolverInitArguments arguments)
absl::StatusOr< ModelProto > ReadMpsFile(const absl::string_view filename)
testing::Matcher< SolveResult > TerminatesWithLimit(const Limit expected, const bool allow_limit_undetermined)
constexpr absl::string_view kNoMultiObjectiveSupportMessage
Matcher< UpdateResult > DidUpdate()
Actual UpdateResult.did_update is true.
constexpr double kTolerance
@ kTime
The algorithm stopped after a user-specified computation time.
internal::StatusIsMatcher StatusIs(CodeMatcher code_matcher, MessageMatcher message_matcher)
constexpr absl::string_view kNoIntegerVariableSupportMessage
Matcher< SolveResult > IsOptimal(const std::optional< double > expected_primal_objective, const double tolerance)
std::string ProtobufShortDebugString(const P &message)
internal::StatusIsMatcher StatusIs(CodeMatcher code_matcher, MessageMatcher message_matcher)
internal::IsOkAndHoldsMatcher< typename std::decay< InnerMatcher >::type > IsOkAndHolds(InnerMatcher &&inner_matcher)
#define ASSERT_OK(expression)
#define EXPECT_OK(expression)
#define ASSERT_OK_AND_ASSIGN(lhs, rexpr)
ObjectiveMap< ObjectiveParameters > objective_parameters
Parameters for individual objectives in a multi-objective model.
bool supports_integer_variables
True if the solver supports integer variables.
MultiObjectiveTestParameters(SolverType solver_type, SolveParameters parameters, bool supports_auxiliary_objectives, bool supports_incremental_objective_add_and_delete, bool supports_incremental_objective_modification, bool supports_integer_variables)
bool supports_auxiliary_objectives
True if the solver supports auxiliary objectives.
SolveParameters parameters
SolverType solver_type
The tested solver.
bool supports_incremental_objective_modification
bool supports_incremental_objective_add_and_delete
SolveParameters parameters
Model independent parameters, e.g. time limit.
absl::Duration time_limit
SolveParametersProto Proto() const