Google OR-Tools v9.12
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
multi_objective_tests.cc
Go to the documentation of this file.
1// Copyright 2010-2025 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
15
16#include <memory>
17#include <ostream>
18#include <utility>
19
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"
25#include "ortools/base/gmock.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"
34
36
37using ::testing::AnyOf;
38using ::testing::DoubleNear;
39using ::testing::HasSubstr;
42
43constexpr absl::string_view kNoMultiObjectiveSupportMessage =
44 "This test is disabled as the solver does not support multiple objective "
45 "models";
46constexpr absl::string_view kNoIntegerVariableSupportMessage =
47 "This test is disabled as the solver does not support integer variables";
48
49constexpr double kTolerance = 1.0e-6;
50
65
66std::ostream& operator<<(std::ostream& out,
67 const MultiObjectiveTestParameters& params) {
68 out << "{ solver_type: " << params.solver_type
69 << ", parameters: " << ProtobufShortDebugString(params.parameters.Proto())
70 << ", supports_auxiliary_objectives: "
71 << (params.supports_auxiliary_objectives ? "true" : "false")
72 << ", supports_incremental_objective_add_and_delete: "
74 : "false")
75 << ", supports_incremental_objective_modification: "
76 << (params.supports_incremental_objective_modification ? "true" : "false")
77 << ", supports_integer_variables: "
78 << (params.supports_integer_variables ? "true" : "false") << " }";
79 return out;
80}
81
82namespace {
83
84TEST_P(SimpleMultiObjectiveTest, CanBuildMultiObjectiveModel) {
85 Model model;
86 const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
87 model.AddMaximizationObjective(x, /*priority=*/2);
88 model.AddMinimizationObjective(-3.0 * x + 2.0, /*priority=*/1);
89
90 if (GetParam().supports_auxiliary_objectives) {
91 EXPECT_OK(NewIncrementalSolver(&model, GetParam().solver_type, {}));
92 } else {
93 EXPECT_THAT(NewIncrementalSolver(&model, GetParam().solver_type, {}),
94 StatusIs(AnyOf(absl::StatusCode::kInvalidArgument,
95 absl::StatusCode::kUnimplemented),
96 HasSubstr("multiple objectives")));
97 }
98}
99
100// We consider the two objective model
101// max {x, x + 3*y + 2}
102// s.t. x + y <= 1.5
103// 0 <= x, y <= 1
104//
105// This has the unique optimal solution (x^*, y^*) = (1, 0.5) with objective
106// values (1, 4.5).
107TEST_P(SimpleMultiObjectiveTest, SolveMultiObjectiveModel) {
108 if (!GetParam().supports_auxiliary_objectives) {
109 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
110 }
111 Model model;
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);
115 model.Maximize(x);
116 const Objective o =
117 model.AddMaximizationObjective(x + 3.0 * y + 2.0, /*priority=*/1);
118
119 ASSERT_OK_AND_ASSIGN(const SolveResult result, SimpleSolve(model));
120 ASSERT_THAT(result, IsOptimalWithSolution(1.0, {{x, 1.0}, {y, 0.5}}));
121 EXPECT_EQ(result.objective_value(o), 4.5);
122}
123
124// We consider the two objective model
125// {min(-x), max(x + 3*y + 2)}
126// s.t. x + y <= 1.5
127// 0 <= x, y <= 1
128//
129// This has the unique optimal solution (x^*, y^*) = (1, 0.5) with objective
130// values (-1, 4.5).
131TEST_P(SimpleMultiObjectiveTest, MultipleObjectivesWithDifferentSenses) {
132 if (!GetParam().supports_auxiliary_objectives) {
133 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
134 }
135 Model model;
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);
139 model.Minimize(-x);
140 const Objective o =
141 model.AddMaximizationObjective(x + 3.0 * y + 2.0, /*priority=*/1);
142
143 ASSERT_OK_AND_ASSIGN(const SolveResult result, SimpleSolve(model));
144 ASSERT_THAT(result, IsOptimalWithSolution(-1.0, {{x, 1.0}, {y, 0.5}}));
145 EXPECT_EQ(result.objective_value(o), 4.5);
146}
147
148TEST_P(SimpleMultiObjectiveTest, PrimaryAndAuxiliaryObjectiveSharePriority) {
149 if (!GetParam().supports_auxiliary_objectives) {
150 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
151 }
152 Model model;
153 model.set_objective_priority(model.primary_objective(), 1);
154 model.AddAuxiliaryObjective(1);
155 EXPECT_THAT(NewIncrementalSolver(&model, GetParam().solver_type, {}),
156 StatusIs(absl::StatusCode::kInvalidArgument,
157 HasSubstr("repeated objective priority: 1")));
158}
159
160TEST_P(SimpleMultiObjectiveTest, AuxiliaryObjectivesSharePriority) {
161 if (!GetParam().supports_auxiliary_objectives) {
162 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
163 }
164 Model model;
165 model.AddAuxiliaryObjective(1);
166 model.AddAuxiliaryObjective(1);
167 EXPECT_THAT(NewIncrementalSolver(&model, GetParam().solver_type, {}),
168 StatusIs(absl::StatusCode::kInvalidArgument,
169 HasSubstr("repeated objective priority: 1")));
170}
171
172// For a univariate optimization problem with two objectives.
173struct SimpleMultiObjectiveSolveResult {
175 double solution = 0.0;
176 double priority_0_objective_value = 0.0;
177 double priority_1_objective_value = 0.0;
178};
179
180enum class ObjectiveType { kPrimary, kAuxiliary };
181enum class ToleranceType { kAbsolute, kRelative };
182
183// We consider the two objective model
184// priority 0: max(x)
185// priority 1: min(x)
186// s.t. 0 <= x <= 2
187// x is integer
188//
189// The optimal solution is x^* = 2 with objective values (2, 2). We test the
190// degradation tolerances by setting them on the priority 0 objective such that
191// the optimal solution, (up to tolerances), is x^* = 1 with objective values
192// (1, 1). We can accomplish this with an absolute tolerance of 1 or a relative
193// tolerance of 0.5.
194absl::StatusOr<SimpleMultiObjectiveSolveResult> SolveWithObjectiveDegradation(
195 const SolverType solver_type, const SolveParameters& parameters,
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");
201 }
202 Model model;
203 const Variable x = model.AddIntegerVariable(0.0, 2.0, "x");
204 const Objective priority_0 = [&]() {
205 switch (priority_0_type) {
206 case ObjectiveType::kPrimary:
207 model.Maximize(x);
208 model.set_objective_priority(model.primary_objective(), 0);
209 return model.primary_objective();
210 case ObjectiveType::kAuxiliary:
211 return model.AddMaximizationObjective(x, /*priority=*/0);
212 }
213 }();
214 const Objective priority_1 = [&]() {
215 switch (priority_1_type) {
216 case ObjectiveType::kPrimary:
217 model.Minimize(x);
218 model.set_objective_priority(model.primary_objective(), 1);
219 return model.primary_objective();
220 case ObjectiveType::kAuxiliary:
221 return model.AddMinimizationObjective(x, /*priority=*/1);
222 }
223 }();
224 ModelSolveParameters model_parameters;
225 switch (tolerance_type) {
226 case ToleranceType::kAbsolute:
227 model_parameters.objective_parameters[priority_0]
228 .objective_degradation_absolute_tolerance = 1.0;
229 break;
230 case ToleranceType::kRelative:
231 model_parameters.objective_parameters[priority_0]
232 .objective_degradation_relative_tolerance = 0.5;
233 }
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");
240 }
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)};
246}
247
248TEST_P(SimpleMultiObjectiveTest, PrimaryObjectiveDegradationAbsoluteTolerance) {
249 if (!GetParam().supports_auxiliary_objectives) {
250 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
251 }
252 ASSERT_OK_AND_ASSIGN(const SimpleMultiObjectiveSolveResult result,
253 SolveWithObjectiveDegradation(
254 GetParam().solver_type, GetParam().parameters,
255 /*priority_0_type=*/ObjectiveType::kPrimary,
256 /*priority_1_type=*/ObjectiveType::kAuxiliary,
257 ToleranceType::kAbsolute));
258 EXPECT_EQ(result.termination, TerminationReason::kOptimal);
259 EXPECT_THAT(result.solution, DoubleNear(1.0, kTolerance));
260 EXPECT_THAT(result.priority_0_objective_value, DoubleNear(1.0, kTolerance));
261 EXPECT_THAT(result.priority_1_objective_value, DoubleNear(1.0, kTolerance));
262}
263
265 AuxiliaryObjectiveDegradationAbsoluteTolerance) {
266 if (!GetParam().supports_auxiliary_objectives) {
267 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
268 }
269 ASSERT_OK_AND_ASSIGN(const SimpleMultiObjectiveSolveResult result,
270 SolveWithObjectiveDegradation(
271 GetParam().solver_type, GetParam().parameters,
272 /*priority_0_type=*/ObjectiveType::kAuxiliary,
273 /*priority_1_type=*/ObjectiveType::kPrimary,
274 ToleranceType::kAbsolute));
275 EXPECT_EQ(result.termination, TerminationReason::kOptimal);
276 EXPECT_THAT(result.solution, DoubleNear(1.0, kTolerance));
277 EXPECT_THAT(result.priority_0_objective_value, DoubleNear(1.0, kTolerance));
278 EXPECT_THAT(result.priority_1_objective_value, DoubleNear(1.0, kTolerance));
279}
280
281TEST_P(SimpleMultiObjectiveTest, PrimaryObjectiveDegradationRelativeTolerance) {
282 if (!GetParam().supports_auxiliary_objectives) {
283 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
284 }
285 ASSERT_OK_AND_ASSIGN(const SimpleMultiObjectiveSolveResult result,
286 SolveWithObjectiveDegradation(
287 GetParam().solver_type, GetParam().parameters,
288 /*priority_0_type=*/ObjectiveType::kPrimary,
289 /*priority_1_type=*/ObjectiveType::kAuxiliary,
290 ToleranceType::kRelative));
291 EXPECT_EQ(result.termination, TerminationReason::kOptimal);
292 EXPECT_THAT(result.solution, DoubleNear(1.0, kTolerance));
293 EXPECT_THAT(result.priority_0_objective_value, DoubleNear(1.0, kTolerance));
294 EXPECT_THAT(result.priority_1_objective_value, DoubleNear(1.0, kTolerance));
295}
296
297// You should be able to specify this parameter for a single objective model; it
298// will be ignored.
300 SingleObjectiveModelWithObjectiveDegradationAbsoluteTolerance) {
301 if (!GetParam().supports_auxiliary_objectives) {
302 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
303 }
304 Model model;
305 const Variable x = model.AddIntegerVariable(0.0, 1.0, "x");
306 model.Maximize(x);
307 ModelSolveParameters model_parameters;
308 model_parameters.objective_parameters[model.primary_objective()]
309 .objective_degradation_absolute_tolerance = 0.5;
310 ASSERT_OK_AND_ASSIGN(const SolveResult result,
311 Solve(model, GetParam().solver_type,
312 {.parameters = GetParam().parameters,
313 .model_parameters = model_parameters}));
314 ASSERT_THAT(result, IsOptimalWithSolution(1.0, {{x, 1.0}}));
315}
316
317// You should be able to specify this parameter for a single objective model; it
318// will be ignored.
320 SingleObjectiveModelWithObjectiveDegradationRelativeTolerance) {
321 if (!GetParam().supports_auxiliary_objectives) {
322 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
323 }
324 Model model;
325 const Variable x = model.AddIntegerVariable(0.0, 1.0, "x");
326 model.Maximize(x);
327 ModelSolveParameters model_parameters;
328 model_parameters.objective_parameters[model.primary_objective()]
329 .objective_degradation_relative_tolerance = 0.5;
330 ASSERT_OK_AND_ASSIGN(const SolveResult result,
331 Solve(model, GetParam().solver_type,
332 {.parameters = GetParam().parameters,
333 .model_parameters = model_parameters}));
334 ASSERT_THAT(result, IsOptimalWithSolution(1.0, {{x, 1.0}}));
335}
336
338 AuxiliaryObjectiveDegradationRelativeTolerance) {
339 if (!GetParam().supports_auxiliary_objectives) {
340 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
341 }
342 ASSERT_OK_AND_ASSIGN(const SimpleMultiObjectiveSolveResult result,
343 SolveWithObjectiveDegradation(
344 GetParam().solver_type, GetParam().parameters,
345 /*priority_0_type=*/ObjectiveType::kAuxiliary,
346 /*priority_1_type=*/ObjectiveType::kPrimary,
347 ToleranceType::kRelative));
348 EXPECT_EQ(result.termination, TerminationReason::kOptimal);
349 EXPECT_THAT(result.solution, DoubleNear(1.0, kTolerance));
350 EXPECT_THAT(result.priority_0_objective_value, DoubleNear(1.0, kTolerance));
351 EXPECT_THAT(result.priority_1_objective_value, DoubleNear(1.0, kTolerance));
352}
353
354// Tests time limits on MIPLIB instance 23588. This instance was selected
355// because every supported solver can solve it quickly (a few seconds), but no
356// solver can solve it too quickly (so we can test time limits).
357//
358// Note that this instance is a MIP.
359absl::StatusOr<std::unique_ptr<Model>> Load23588MiplibInstance() {
361 const ModelProto model_proto,
362 ReadMpsFile(absl::StrCat("ortools/math_opt/solver_tests/testdata/"
363 "23588.mps")));
364 return Model::FromModelProto(model_proto);
365}
366
367// We move the "hard" objective to the auxiliary objective, and set a trivial
368// primary objective (0).
370 MultiObjectiveModelWithAuxiliaryObjectiveTimeLimit) {
371 if (!GetParam().supports_integer_variables) {
372 GTEST_SKIP() << kNoIntegerVariableSupportMessage;
373 }
374 ASSERT_OK_AND_ASSIGN(const std::unique_ptr<Model> model,
375 Load23588MiplibInstance());
376 const Objective aux_obj = model->AddMaximizationObjective(
377 model->primary_objective().AsLinearExpression(), /*priority=*/1);
378 model->clear_objective();
379 const SolveArguments args = {
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) {
386 EXPECT_THAT(result, StatusIs(absl::StatusCode::kInvalidArgument,
387 HasSubstr("multiple objectives")));
388 return;
389 }
390 ASSERT_OK(result);
392 // Solvers do not stop very precisely, use a large number to avoid flaky
393 // tests. Do NOT try to fine tune this to be small, it is hard to get right
394 // for all compilation modes (e.g., debug, asan).
395 EXPECT_LE(result->solve_stats.solve_time, absl::Seconds(1));
396}
397
398// We keep the "hard" objective as the primary objective, and add a trivial
399// auxiliary objective (0).
401 MultiObjectiveModelWithPrimaryObjectiveTimeLimit) {
402 if (!GetParam().supports_integer_variables) {
403 GTEST_SKIP() << kNoIntegerVariableSupportMessage;
404 }
405 ASSERT_OK_AND_ASSIGN(const std::unique_ptr<Model> model,
406 Load23588MiplibInstance());
407 model->AddMaximizationObjective(0, /*priority=*/1);
408 const SolveArguments args = {
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) {
415 EXPECT_THAT(result, StatusIs(absl::StatusCode::kInvalidArgument,
416 HasSubstr("multiple objectives")));
417 return;
418 }
419 ASSERT_OK(result);
421 // Solvers do not stop very precisely, use a large number to avoid flaky
422 // tests. Do NOT try to fine tune this to be small, it is hard to get right
423 // for all compilation modes (e.g., debug, asan).
424 EXPECT_LE(result->solve_stats.solve_time, absl::Seconds(1));
425}
426
427// We test that, for a non-multi-objective model, we either respect the primary
428// objective time limit if the solver supports multi-objective models, or error
429// otherwise.
431 SingleObjectiveModelWithPrimaryObjectiveTimeLimit) {
432 if (!GetParam().supports_auxiliary_objectives) {
433 return;
434 }
435 if (!GetParam().supports_integer_variables) {
436 GTEST_SKIP() << kNoIntegerVariableSupportMessage;
437 }
438 ASSERT_OK_AND_ASSIGN(const std::unique_ptr<Model> model,
439 Load23588MiplibInstance());
440 SolveArguments args = {
441 .parameters = GetParam().parameters,
442 .model_parameters = {
443 .objective_parameters = {{model->primary_objective(),
444 {.time_limit = absl::Milliseconds(1)}}}}};
445 args.parameters.time_limit = absl::Seconds(10);
446 ASSERT_OK_AND_ASSIGN(const SolveResult result,
447 Solve(*model, GetParam().solver_type, args));
449 // Solvers do not stop very precisely, use a large number to avoid flaky
450 // tests. Do NOT try to fine tune this to be small, it is hard to get right
451 // for all compilation modes (e.g., debug, asan).
452 EXPECT_LE(result.solve_stats.solve_time, absl::Seconds(1));
453}
454
455// We refine SingleObjectiveModelWithPrimaryObjectiveTimeLimit to ensure that
456// we take the minimum of the global time limit and the primary objective time
457// limit.
459 SingleObjectiveModelTakesMininumFromPrimaryAndGlobalTimeLimits) {
460 if (!GetParam().supports_auxiliary_objectives) {
461 return;
462 }
463 if (!GetParam().supports_integer_variables) {
464 GTEST_SKIP() << kNoIntegerVariableSupportMessage;
465 }
466 ASSERT_OK_AND_ASSIGN(const std::unique_ptr<Model> model,
467 Load23588MiplibInstance());
468 SolveArguments args = {
469 .parameters = GetParam().parameters,
470 .model_parameters = {
471 .objective_parameters = {{model->primary_objective(),
472 {.time_limit = absl::Seconds(10)}}}}};
473 args.parameters.time_limit = absl::Milliseconds(1);
474 ASSERT_OK_AND_ASSIGN(const SolveResult result,
475 Solve(*model, GetParam().solver_type, args));
477 // Solvers do not stop very precisely, use a large number to avoid flaky
478 // tests. Do NOT try to fine tune this to be small, it is hard to get right
479 // for all compilation modes (e.g., debug, asan).
480 EXPECT_LE(result.solve_stats.solve_time, absl::Seconds(1));
481}
482
483// We test that all solvers that do not support multi-objective models error
484// when given primary objective parameters.
486 SolverWithoutSupportErrorsWithPrimaryObjectiveParameters) {
487 if (GetParam().supports_auxiliary_objectives) {
488 return;
489 }
490 Model model;
491 const SolveArguments args = {
492 .parameters = GetParam().parameters,
493 .model_parameters = {
494 .objective_parameters = {
495 {model.primary_objective(), {.time_limit = absl::Seconds(10)}}}}};
496 EXPECT_THAT(Solve(model, GetParam().solver_type, args),
497 StatusIs(absl::StatusCode::kInvalidArgument,
498 HasSubstr("primary_objective_parameters")));
499}
500
501// We start with the single objective model:
502// max x
503// s.t. x + y <= 1.5
504// 0 <= x, y <= 1
505//
506// The optimal objective value is 1, and (x^*, y^*) = (1, a) is optimal for any
507// value a in [0, 1].
508//
509// We then add the secondary objective
510//
511// max {x, x + 3*y + 2}
512// s.t. x + y <= 1.5
513// 0 <= x, y <= 1
514//
515// This has the unique optimal solution (x^*, y^*) = (1, 0.5) with objective
516// values (1, 4.5).
517TEST_P(IncrementalMultiObjectiveTest, SingleToMultiObjectiveModel) {
518 Model model;
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);
522 model.Maximize(x);
523
525 const auto solver,
526 NewIncrementalSolver(&model, GetParam().solver_type, {}));
527 // Since there are multiple optimal solutions we do not match against the
528 // solution value.
529 ASSERT_THAT(solver->Solve({.parameters = GetParam().parameters}),
530 IsOkAndHolds(IsOptimal(1.0)));
531
532 const Objective o =
533 model.AddMaximizationObjective(x + 3.0 * y + 2.0, /*priority=*/1);
534
535 if (!GetParam().supports_auxiliary_objectives) {
536 // Here we test that solvers that don't support auxiliary objectives return
537 // false in SolverInterface::CanUpdate(). Thus they should fail in their
538 // factory function instead of failing in their SolverInterface::Update()
539 // function. To assert we rely on status annotations added by
540 // IncrementalSolver::Update() to the returned status of Solver::Update()
541 // and Solver::New().
543 solver->Update(),
544 StatusIs(AnyOf(absl::StatusCode::kInvalidArgument,
545 absl::StatusCode::kUnimplemented),
546 AllOf(HasSubstr("multiple objective"),
547 // Sub-string expected for Solver::Update() error.
548 Not(HasSubstr("update failed")),
549 // Sub-string expected for Solver::New() error.
550 HasSubstr("solver re-creation failed"))));
551 return;
552 }
553
555 solver->Update(),
556 IsOkAndHolds(GetParam().supports_incremental_objective_add_and_delete
557 ? DidUpdate()
558 : Not(DidUpdate())));
560 const SolveResult result,
561 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}));
562 ASSERT_THAT(result, IsOptimalWithSolution(1.0, {{x, 1.0}, {y, 0.5}}));
563 EXPECT_EQ(result.objective_value(o), 4.5);
564}
565
566// We start with the two objective model:
567// max {x, 3}
568// s.t. x + y <= 1.5
569// 0 <= x, y <= 1
570//
571// The optimal objective values are (1, 3), and (x^*, y^*) = (1, a) is optimal
572// for any value a in [0, 1].
573//
574// We then add the tertiary objective
575//
576// max {x, 3, x + 3*y + 2}
577// s.t. x + y <= 1.5
578// 0 <= x, y <= 1
579//
580// This has the unique optimal solution (x^*, y^*) = (1, 0.5) with objective
581// values (1, 3, 4.5).
582TEST_P(IncrementalMultiObjectiveTest, AddObjectiveToMultiObjectiveModel) {
583 if (!GetParam().supports_auxiliary_objectives) {
584 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
585 }
586 Model model;
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);
590 model.Maximize(x);
591 const Objective o = model.AddMaximizationObjective(3.0, /*priority=*/2);
592
594 const auto solver,
595 NewIncrementalSolver(&model, GetParam().solver_type, {}));
596 {
597 ASSERT_OK_AND_ASSIGN(const SolveResult result,
598 solver->Solve({.parameters = GetParam().parameters}));
599 ASSERT_THAT(result, IsOptimal(1.0));
600 EXPECT_EQ(result.objective_value(o), 3.0);
601 }
602
603 const Objective o2 =
604 model.AddMaximizationObjective(x + 3.0 * y + 2.0, /*priority=*/5);
605
607 solver->Update(),
608 IsOkAndHolds(GetParam().supports_incremental_objective_add_and_delete
609 ? DidUpdate()
610 : Not(DidUpdate())));
612 const SolveResult result,
613 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}));
614 ASSERT_THAT(result, IsOptimalWithSolution(1.0, {{x, 1.0}, {y, 0.5}}));
615 EXPECT_EQ(result.objective_value(o), 3.0);
616 EXPECT_EQ(result.objective_value(o2), 4.5);
617}
618
619// We start with the three objective model:
620// max {x, 3, x + 3*y + 2}
621// s.t. x + y <= 1.5
622// 0 <= x, y <= 1
623//
624// This has the unique optimal solution (x^*, y^*) = (1, 0.5) with objective
625// values (1, 3, 4.5).
626//
627// We then delete the second objective, leaving
628//
629// max {x, x + 3*y + 2}
630// s.t. x + y <= 1.5
631// 0 <= x, y <= 1
632//
633// This has the unique optimal solution (x^*, y^*) = (1, 0.5) with objective
634// values (1, 4.5).
635TEST_P(IncrementalMultiObjectiveTest, DeleteObjectiveFromMultiObjectiveModel) {
636 if (!GetParam().supports_auxiliary_objectives) {
637 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
638 }
639 Model model;
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);
643 model.Maximize(x);
644 const Objective o = model.AddMaximizationObjective(3.0, /*priority=*/2);
645 const Objective o2 =
646 model.AddMaximizationObjective(x + 3.0 * y + 2.0, /*priority=*/3);
647
649 const auto solver,
650 NewIncrementalSolver(&model, GetParam().solver_type, {}));
651 {
652 ASSERT_OK_AND_ASSIGN(const SolveResult result,
653 solver->Solve({.parameters = GetParam().parameters}));
654 ASSERT_THAT(result, IsOptimalWithSolution(1.0, {{x, 1.0}, {y, 0.5}}));
655 EXPECT_EQ(result.objective_value(o), 3.0);
656 EXPECT_EQ(result.objective_value(o2), 4.5);
657 }
658
659 model.DeleteAuxiliaryObjective(o);
660
662 solver->Update(),
663 IsOkAndHolds(GetParam().supports_incremental_objective_add_and_delete
664 ? DidUpdate()
665 : Not(DidUpdate())));
667 const SolveResult result,
668 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}));
669 ASSERT_THAT(result, IsOptimalWithSolution(1.0, {{x, 1.0}, {y, 0.5}}));
670 EXPECT_EQ(result.objective_value(o2), 4.5);
671}
672
673// We start with the two objective model:
674// {max(x), max(x + 3*y + 2)}
675// s.t. x + y <= 1.5
676// 0 <= x, y <= 1
677//
678// This has the unique optimal solution (x^*, y^*) = (1, 0.5) with objective
679// values (1, 4.5).
680//
681// We then flip the sign of the first objective, leaving
682//
683// {min(x), max(x + 3*y + 2)}
684// s.t. x + y <= 1.5
685// 0 <= x, y <= 1
686//
687// This has the unique optimal solution (x^*, y^*) = (0, 1) with objective
688// values (0, 5).
690 ModifyPrimaryObjectiveSenseInMultiObjectiveModel) {
691 if (!GetParam().supports_auxiliary_objectives) {
692 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
693 }
694 Model model;
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);
698 model.Maximize(x);
699 const Objective o =
700 model.AddMaximizationObjective(x + 3.0 * y + 2.0, /*priority=*/2);
701
703 const auto solver,
704 NewIncrementalSolver(&model, GetParam().solver_type, {}));
705 {
706 ASSERT_OK_AND_ASSIGN(const SolveResult result,
707 solver->Solve({.parameters = GetParam().parameters}));
708 ASSERT_THAT(result, IsOptimalWithSolution(1.0, {{x, 1.0}, {y, 0.5}}));
709 ASSERT_OK_AND_ASSIGN(const SolveResultProto result_proto, result.Proto());
710 EXPECT_EQ(result.objective_value(o), 4.5);
711 }
712
713 model.set_minimize();
714
716 solver->Update(),
717 IsOkAndHolds(GetParam().supports_incremental_objective_modification
718 ? DidUpdate()
719 : Not(DidUpdate())));
721 const SolveResult result,
722 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}));
723 ASSERT_THAT(result, IsOptimalWithSolution(0.0, {{x, 0.0}, {y, 1.0}}));
724 EXPECT_EQ(result.objective_value(o), 5.0);
725}
726
727// Same problem as ModifyPrimaryObjectiveSenseInMultiObjectiveModel, except we
728// switch which objective is primary and which is auxiliary.
730 ModifyAuxiliaryObjectiveSenseInMultiObjectiveModel) {
731 if (!GetParam().supports_auxiliary_objectives) {
732 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
733 }
734 Model model;
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);
738 model.Maximize(x + 3.0 * y + 2.0);
739 model.set_objective_priority(model.primary_objective(), 2);
740 const Objective o = model.AddMaximizationObjective(x, /*priority=*/0);
741
743 const auto solver,
744 NewIncrementalSolver(&model, GetParam().solver_type, {}));
745 {
746 ASSERT_OK_AND_ASSIGN(const SolveResult result,
747 solver->Solve({.parameters = GetParam().parameters}));
748 ASSERT_THAT(result, IsOptimalWithSolution(4.5, {{x, 1.0}, {y, 0.5}}));
749 EXPECT_EQ(result.objective_value(o), 1.0);
750 }
751
752 model.set_minimize(o);
753
755 solver->Update(),
756 IsOkAndHolds(GetParam().supports_incremental_objective_modification
757 ? DidUpdate()
758 : Not(DidUpdate())));
760 const SolveResult result,
761 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}));
762 ASSERT_THAT(result, IsOptimalWithSolution(5.0, {{x, 0.0}, {y, 1.0}}));
763 EXPECT_EQ(result.objective_value(o), 0.0);
764}
765
766// TODO(b/214027410): Add these to IncrementalMultiObjectiveTest when needed:
767// * ModifyPrimaryObjectiveOffsetInMultiObjectiveModel
768// * ModifyAuxiliaryObjectiveOffsetInMultiObjectiveModel
769// * ModifyPrimaryObjectiveLinearCoefficientInMultiObjectiveModel
770// * ModifyAuxiliaryObjectiveLinearCoefficientInMultiObjectiveModel
771// * AfterUpdatePrimaryAndAuxiliaryObjectiveSharePriority
772// * AfterUpdateAuxiliaryObjectivesSharePriority
773
774} // namespace
775} // namespace operations_research::math_opt
#define ASSIGN_OR_RETURN(lhs, rexpr)
Definition model.h:341
void Maximize(Variable *obj, std::vector< Annotation > search_annotations)
Definition model.cc:1075
void Minimize(Variable *obj, std::vector< Annotation > search_annotations)
Definition model.cc:1068
static absl::StatusOr< std::unique_ptr< Model > > FromModelProto(const ModelProto &model_proto)
Definition model.cc:54
An object oriented wrapper for quadratic constraints in ModelStorage.
Definition gurobi_isv.cc:28
Matcher< SolveResult > IsOptimalWithSolution(const double expected_objective, const VariableMap< double > expected_variable_values, const double tolerance)
Definition matchers.cc:777
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.
Definition parameters.h:42
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)
Definition solve.cc:62
std::ostream & operator<<(std::ostream &ostr, const IndicatorConstraint &constraint)
absl::StatusOr< std::unique_ptr< IncrementalSolver > > NewIncrementalSolver(Model *model, SolverType solver_type, SolverInitArguments arguments)
Definition solve.cc:82
absl::StatusOr< ModelProto > ReadMpsFile(const absl::string_view filename)
testing::Matcher< SolveResult > TerminatesWithLimit(const Limit expected, const bool allow_limit_undetermined)
Definition matchers.cc:648
constexpr absl::string_view kNoMultiObjectiveSupportMessage
Matcher< UpdateResult > DidUpdate()
Actual UpdateResult.did_update is true.
Definition matchers.cc:1027
@ 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)
Definition matchers.cc:762
BoolVar Not(BoolVar x)
Definition cp_model.cc:87
std::string ProtobufShortDebugString(const P &message)
Definition proto_utils.h:41
STL namespace.
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
Model independent parameters, e.g. time limit.