19#include "absl/status/status.h"
20#include "absl/status/statusor.h"
21#include "absl/strings/string_view.h"
22#include "gtest/gtest.h"
27#include "ortools/math_opt/model.pb.h"
28#include "ortools/math_opt/model_update.pb.h"
29#include "ortools/math_opt/result.pb.h"
34using ::testing::AnyOf;
35using ::testing::DoubleNear;
36using ::testing::HasSubstr;
37using ::testing::status::IsOkAndHolds;
38using ::testing::status::StatusIs;
41 "This test is disabled as the solver does not support multiple objective "
48 bool supports_auxiliary_objectives,
49 bool supports_incremental_objective_add_and_delete,
50 bool supports_incremental_objective_modification)
51 : solver_type(solver_type),
53 supports_auxiliary_objectives(supports_auxiliary_objectives),
54 supports_incremental_objective_add_and_delete(
55 supports_incremental_objective_add_and_delete),
56 supports_incremental_objective_modification(
57 supports_incremental_objective_modification) {}
63 <<
", supports_auxiliary_objectives: "
65 <<
", supports_incremental_objective_add_and_delete: "
68 <<
", supports_incremental_objective_modification: "
76TEST_P(SimpleMultiObjectiveTest, CanBuildMultiObjectiveModel) {
78 const Variable
x =
model.AddContinuousVariable(0.0, 1.0,
"x");
79 model.AddMaximizationObjective(
x, 2);
80 model.AddMinimizationObjective(-3.0 *
x + 2.0, 1);
82 if (GetParam().supports_auxiliary_objectives) {
86 StatusIs(AnyOf(absl::StatusCode::kInvalidArgument,
87 absl::StatusCode::kUnimplemented),
88 HasSubstr(
"multiple objectives")));
99TEST_P(SimpleMultiObjectiveTest, SolveMultiObjectiveModel) {
100 if (!GetParam().supports_auxiliary_objectives) {
104 const Variable
x =
model.AddContinuousVariable(0.0, 1.0,
"x");
105 const Variable
y =
model.AddContinuousVariable(0.0, 1.0,
"y");
106 model.AddLinearConstraint(
x +
y <= 1.5);
109 model.AddMaximizationObjective(
x + 3.0 *
y + 2.0, 1);
113 EXPECT_EQ(result.objective_value(o), 4.5);
123TEST_P(SimpleMultiObjectiveTest, MultipleObjectivesWithDifferentSenses) {
124 if (!GetParam().supports_auxiliary_objectives) {
128 const Variable
x =
model.AddContinuousVariable(0.0, 1.0,
"x");
129 const Variable
y =
model.AddContinuousVariable(0.0, 1.0,
"y");
130 model.AddLinearConstraint(
x +
y <= 1.5);
133 model.AddMaximizationObjective(
x + 3.0 *
y + 2.0, 1);
137 EXPECT_EQ(result.objective_value(o), 4.5);
140TEST_P(SimpleMultiObjectiveTest, PrimaryAndAuxiliaryObjectiveSharePriority) {
141 if (!GetParam().supports_auxiliary_objectives) {
145 model.set_objective_priority(
model.primary_objective(), 1);
146 model.AddAuxiliaryObjective(1);
148 StatusIs(absl::StatusCode::kInvalidArgument,
149 HasSubstr(
"repeated objective priority: 1")));
152TEST_P(SimpleMultiObjectiveTest, AuxiliaryObjectivesSharePriority) {
153 if (!GetParam().supports_auxiliary_objectives) {
157 model.AddAuxiliaryObjective(1);
158 model.AddAuxiliaryObjective(1);
160 StatusIs(absl::StatusCode::kInvalidArgument,
161 HasSubstr(
"repeated objective priority: 1")));
165struct SimpleMultiObjectiveSolveResult {
172enum class ObjectiveType { kPrimary, kAuxiliary };
173enum class ToleranceType { kAbsolute, kRelative };
186absl::StatusOr<SimpleMultiObjectiveSolveResult> SolveWithObjectiveDegradation(
188 const ObjectiveType priority_0_type,
const ObjectiveType priority_1_type,
189 const ToleranceType tolerance_type) {
190 if (priority_0_type == ObjectiveType::kPrimary &&
191 priority_1_type == ObjectiveType::kPrimary) {
192 return absl::InvalidArgumentError(
"Both objectives cannot both be primary");
195 const Variable
x =
model.AddIntegerVariable(0.0, 2.0,
"x");
196 const Objective priority_0 = [&]() {
197 switch (priority_0_type) {
198 case ObjectiveType::kPrimary:
200 model.set_objective_priority(
model.primary_objective(), 0);
201 return model.primary_objective();
202 case ObjectiveType::kAuxiliary:
203 return model.AddMaximizationObjective(
x, 0);
206 const Objective priority_1 = [&]() {
207 switch (priority_1_type) {
208 case ObjectiveType::kPrimary:
210 model.set_objective_priority(
model.primary_objective(), 1);
211 return model.primary_objective();
212 case ObjectiveType::kAuxiliary:
213 return model.AddMinimizationObjective(
x, 1);
216 ModelSolveParameters model_parameters;
217 switch (tolerance_type) {
218 case ToleranceType::kAbsolute:
219 model_parameters.objective_parameters[priority_0]
220 .objective_degradation_absolute_tolerance = 1.0;
222 case ToleranceType::kRelative:
223 model_parameters.objective_parameters[priority_0]
224 .objective_degradation_relative_tolerance = 0.5;
227 const SolveResult result,
229 {.parameters =
parameters, .model_parameters = model_parameters}));
230 if (!result.has_primal_feasible_solution()) {
231 return absl::InternalError(
"No feasible solution found");
233 return SimpleMultiObjectiveSolveResult{
234 .termination = result.termination.reason,
235 .solution = result.best_primal_solution().variable_values.at(
x),
236 .priority_0_objective_value = result.objective_value(priority_0),
237 .priority_1_objective_value = result.objective_value(priority_1)};
240TEST_P(SimpleMultiObjectiveTest, PrimaryObjectiveDegradationAbsoluteTolerance) {
241 if (!GetParam().supports_auxiliary_objectives) {
245 SolveWithObjectiveDegradation(
246 GetParam().solver_type, GetParam().
parameters,
247 ObjectiveType::kPrimary,
248 ObjectiveType::kAuxiliary,
249 ToleranceType::kAbsolute));
256TEST_P(SimpleMultiObjectiveTest,
257 AuxiliaryObjectiveDegradationAbsoluteTolerance) {
258 if (!GetParam().supports_auxiliary_objectives) {
262 SolveWithObjectiveDegradation(
263 GetParam().solver_type, GetParam().
parameters,
264 ObjectiveType::kAuxiliary,
265 ObjectiveType::kPrimary,
266 ToleranceType::kAbsolute));
273TEST_P(SimpleMultiObjectiveTest, PrimaryObjectiveDegradationRelativeTolerance) {
274 if (!GetParam().supports_auxiliary_objectives) {
278 SolveWithObjectiveDegradation(
279 GetParam().solver_type, GetParam().
parameters,
280 ObjectiveType::kPrimary,
281 ObjectiveType::kAuxiliary,
282 ToleranceType::kRelative));
291TEST_P(SimpleMultiObjectiveTest,
292 SingleObjectiveModelWithObjectiveDegradationAbsoluteTolerance) {
293 if (!GetParam().supports_auxiliary_objectives) {
297 const Variable
x =
model.AddIntegerVariable(0.0, 1.0,
"x");
299 ModelSolveParameters model_parameters;
300 model_parameters.objective_parameters[
model.primary_objective()]
301 .objective_degradation_absolute_tolerance = 0.5;
304 {.parameters = GetParam().parameters,
305 .model_parameters = model_parameters}));
311TEST_P(SimpleMultiObjectiveTest,
312 SingleObjectiveModelWithObjectiveDegradationRelativeTolerance) {
313 if (!GetParam().supports_auxiliary_objectives) {
317 const Variable
x =
model.AddIntegerVariable(0.0, 1.0,
"x");
319 ModelSolveParameters model_parameters;
320 model_parameters.objective_parameters[
model.primary_objective()]
321 .objective_degradation_relative_tolerance = 0.5;
324 {.parameters = GetParam().parameters,
325 .model_parameters = model_parameters}));
329TEST_P(SimpleMultiObjectiveTest,
330 AuxiliaryObjectiveDegradationRelativeTolerance) {
331 if (!GetParam().supports_auxiliary_objectives) {
335 SolveWithObjectiveDegradation(
336 GetParam().solver_type, GetParam().
parameters,
337 ObjectiveType::kAuxiliary,
338 ObjectiveType::kPrimary,
339 ToleranceType::kRelative));
362TEST_P(IncrementalMultiObjectiveTest, SingleToMultiObjectiveModel) {
364 const Variable
x =
model.AddContinuousVariable(0.0, 1.0,
"x");
365 const Variable
y =
model.AddContinuousVariable(0.0, 1.0,
"y");
366 model.AddLinearConstraint(
x +
y <= 1.5);
374 ASSERT_THAT(solver->Solve({.parameters = GetParam().parameters}),
378 model.AddMaximizationObjective(
x + 3.0 *
y + 2.0, 1);
380 if (!GetParam().supports_auxiliary_objectives) {
389 StatusIs(AnyOf(absl::StatusCode::kInvalidArgument,
390 absl::StatusCode::kUnimplemented),
391 AllOf(HasSubstr(
"multiple objective"),
393 Not(HasSubstr(
"update failed")),
395 HasSubstr(
"solver re-creation failed"))));
401 IsOkAndHolds(GetParam().supports_incremental_objective_add_and_delete
405 const SolveResult result,
406 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}));
408 EXPECT_EQ(result.objective_value(o), 4.5);
427TEST_P(IncrementalMultiObjectiveTest, AddObjectiveToMultiObjectiveModel) {
428 if (!GetParam().supports_auxiliary_objectives) {
432 const Variable
x =
model.AddContinuousVariable(0.0, 1.0,
"x");
433 const Variable
y =
model.AddContinuousVariable(0.0, 1.0,
"y");
434 model.AddLinearConstraint(
x +
y <= 1.5);
436 const Objective o =
model.AddMaximizationObjective(3.0, 2);
443 solver->Solve({.parameters = GetParam().parameters}));
445 EXPECT_EQ(result.objective_value(o), 3.0);
449 model.AddMaximizationObjective(
x + 3.0 *
y + 2.0, 5);
453 IsOkAndHolds(GetParam().supports_incremental_objective_add_and_delete
457 const SolveResult result,
458 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}));
460 EXPECT_EQ(result.objective_value(o), 3.0);
461 EXPECT_EQ(result.objective_value(o2), 4.5);
480TEST_P(IncrementalMultiObjectiveTest, DeleteObjectiveFromMultiObjectiveModel) {
481 if (!GetParam().supports_auxiliary_objectives) {
485 const Variable
x =
model.AddContinuousVariable(0.0, 1.0,
"x");
486 const Variable
y =
model.AddContinuousVariable(0.0, 1.0,
"y");
487 model.AddLinearConstraint(
x +
y <= 1.5);
489 const Objective o =
model.AddMaximizationObjective(3.0, 2);
491 model.AddMaximizationObjective(
x + 3.0 *
y + 2.0, 3);
498 solver->Solve({.parameters = GetParam().parameters}));
500 EXPECT_EQ(result.objective_value(o), 3.0);
501 EXPECT_EQ(result.objective_value(o2), 4.5);
504 model.DeleteAuxiliaryObjective(o);
508 IsOkAndHolds(GetParam().supports_incremental_objective_add_and_delete
512 const SolveResult result,
513 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}));
515 EXPECT_EQ(result.objective_value(o2), 4.5);
534TEST_P(IncrementalMultiObjectiveTest,
535 ModifyPrimaryObjectiveSenseInMultiObjectiveModel) {
536 if (!GetParam().supports_auxiliary_objectives) {
540 const Variable
x =
model.AddContinuousVariable(0.0, 1.0,
"x");
541 const Variable
y =
model.AddContinuousVariable(0.0, 1.0,
"y");
542 model.AddLinearConstraint(
x +
y <= 1.5);
545 model.AddMaximizationObjective(
x + 3.0 *
y + 2.0, 2);
552 solver->Solve({.parameters = GetParam().parameters}));
555 EXPECT_EQ(result.objective_value(o), 4.5);
558 model.set_minimize();
562 IsOkAndHolds(GetParam().supports_incremental_objective_modification
566 const SolveResult result,
567 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}));
569 EXPECT_EQ(result.objective_value(o), 5.0);
574TEST_P(IncrementalMultiObjectiveTest,
575 ModifyAuxiliaryObjectiveSenseInMultiObjectiveModel) {
576 if (!GetParam().supports_auxiliary_objectives) {
580 const Variable
x =
model.AddContinuousVariable(0.0, 1.0,
"x");
581 const Variable
y =
model.AddContinuousVariable(0.0, 1.0,
"y");
582 model.AddLinearConstraint(
x +
y <= 1.5);
583 model.Maximize(
x + 3.0 *
y + 2.0);
584 model.set_objective_priority(
model.primary_objective(), 2);
585 const Objective o =
model.AddMaximizationObjective(
x, 0);
592 solver->Solve({.parameters = GetParam().parameters}));
594 EXPECT_EQ(result.objective_value(o), 1.0);
597 model.set_minimize(o);
601 IsOkAndHolds(GetParam().supports_incremental_objective_modification
605 const SolveResult result,
606 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}));
608 EXPECT_EQ(result.objective_value(o), 0.0);
#define ASSIGN_OR_RETURN(lhs, rexpr)
TerminationReason termination
double priority_0_objective_value
double priority_1_objective_value
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)
constexpr absl::string_view kNoMultiObjectiveSupportMessage
Matcher< UpdateResult > DidUpdate()
Actual UpdateResult.did_update is true.
constexpr double kTolerance
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)
#define EXPECT_OK(expression)
#define ASSERT_OK_AND_ASSIGN(lhs, rexpr)
bool supports_auxiliary_objectives
True if the solver supports auxiliary objectives.
MultiObjectiveTestParameters(SolverType solver_type, SolveParameters parameters, bool supports_auxiliary_objectives, bool supports_incremental_objective_add_and_delete, bool supports_incremental_objective_modification)
SolveParameters parameters
SolverType solver_type
The tested solver.
bool supports_incremental_objective_modification
bool supports_incremental_objective_add_and_delete
SolveParametersProto Proto() const