Google OR-Tools v9.15
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/base/optimization.h"
21#include "absl/status/status.h"
22#include "absl/status/statusor.h"
23#include "absl/strings/string_view.h"
24#include "absl/time/time.h"
25#include "gtest/gtest.h"
26#include "ortools/base/gmock.h"
35
37
38using ::testing::AnyOf;
39using ::testing::DoubleNear;
40using ::testing::HasSubstr;
41using ::testing::status::IsOkAndHolds;
42using ::testing::status::StatusIs;
43
44constexpr absl::string_view kNoMultiObjectiveSupportMessage =
45 "This test is disabled as the solver does not support multiple objective "
46 "models";
47constexpr absl::string_view kNoIntegerVariableSupportMessage =
48 "This test is disabled as the solver does not support integer variables";
49
50constexpr double kTolerance = 1.0e-6;
51
66
67std::ostream& operator<<(std::ostream& out,
68 const MultiObjectiveTestParameters& params) {
69 out << "{ solver_type: " << params.solver_type
70 << ", parameters: " << ProtobufShortDebugString(params.parameters.Proto())
71 << ", supports_auxiliary_objectives: "
72 << (params.supports_auxiliary_objectives ? "true" : "false")
73 << ", supports_incremental_objective_add_and_delete: "
75 : "false")
76 << ", supports_incremental_objective_modification: "
77 << (params.supports_incremental_objective_modification ? "true" : "false")
78 << ", supports_integer_variables: "
79 << (params.supports_integer_variables ? "true" : "false") << " }";
80 return out;
81}
82
83namespace {
84
85TEST_P(SimpleMultiObjectiveTest, CanBuildMultiObjectiveModel) {
86 Model model;
87 const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
88 model.AddMaximizationObjective(x, /*priority=*/2);
89 model.AddMinimizationObjective(-3.0 * x + 2.0, /*priority=*/1);
90
91 if (GetParam().supports_auxiliary_objectives) {
92 EXPECT_OK(NewIncrementalSolver(&model, GetParam().solver_type, {}));
93 } else {
94 EXPECT_THAT(NewIncrementalSolver(&model, GetParam().solver_type, {}),
95 StatusIs(AnyOf(absl::StatusCode::kInvalidArgument,
96 absl::StatusCode::kUnimplemented),
97 HasSubstr("multiple objectives")));
98 }
99}
100
101// We consider the two objective model
102// max {x, x + 3*y + 2}
103// s.t. x + y <= 1.5
104// 0 <= x, y <= 1
105//
106// This has the unique optimal solution (x^*, y^*) = (1, 0.5) with objective
107// values (1, 4.5).
108TEST_P(SimpleMultiObjectiveTest, SolveMultiObjectiveModel) {
109 if (!GetParam().supports_auxiliary_objectives) {
110 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
111 }
112 Model model;
113 const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
114 const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
115 model.AddLinearConstraint(x + y <= 1.5);
116 model.Maximize(x);
117 const Objective o =
118 model.AddMaximizationObjective(x + 3.0 * y + 2.0, /*priority=*/1);
119
120 ASSERT_OK_AND_ASSIGN(const SolveResult result, SimpleSolve(model));
121 ASSERT_THAT(result, IsOptimalWithSolution(1.0, {{x, 1.0}, {y, 0.5}}));
122 EXPECT_EQ(result.objective_value(o), 4.5);
123}
124
125// We consider the two objective model
126// {min(-x), max(x + 3*y + 2)}
127// s.t. x + y <= 1.5
128// 0 <= x, y <= 1
129//
130// This has the unique optimal solution (x^*, y^*) = (1, 0.5) with objective
131// values (-1, 4.5).
132TEST_P(SimpleMultiObjectiveTest, MultipleObjectivesWithDifferentSenses) {
133 if (!GetParam().supports_auxiliary_objectives) {
134 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
135 }
136 Model model;
137 const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
138 const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
139 model.AddLinearConstraint(x + y <= 1.5);
140 model.Minimize(-x);
141 const Objective o =
142 model.AddMaximizationObjective(x + 3.0 * y + 2.0, /*priority=*/1);
143
144 ASSERT_OK_AND_ASSIGN(const SolveResult result, SimpleSolve(model));
145 ASSERT_THAT(result, IsOptimalWithSolution(-1.0, {{x, 1.0}, {y, 0.5}}));
146 EXPECT_EQ(result.objective_value(o), 4.5);
147}
148
149TEST_P(SimpleMultiObjectiveTest, PrimaryAndAuxiliaryObjectiveSharePriority) {
150 if (!GetParam().supports_auxiliary_objectives) {
151 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
152 }
153 Model model;
154 model.set_objective_priority(model.primary_objective(), 1);
155 model.AddAuxiliaryObjective(1);
156 EXPECT_THAT(NewIncrementalSolver(&model, GetParam().solver_type, {}),
157 StatusIs(absl::StatusCode::kInvalidArgument,
158 HasSubstr("repeated objective priority: 1")));
159}
160
161TEST_P(SimpleMultiObjectiveTest, AuxiliaryObjectivesSharePriority) {
162 if (!GetParam().supports_auxiliary_objectives) {
163 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
164 }
165 Model model;
166 model.AddAuxiliaryObjective(1);
167 model.AddAuxiliaryObjective(1);
168 EXPECT_THAT(NewIncrementalSolver(&model, GetParam().solver_type, {}),
169 StatusIs(absl::StatusCode::kInvalidArgument,
170 HasSubstr("repeated objective priority: 1")));
171}
172
173// For a univariate optimization problem with two objectives.
174struct SimpleMultiObjectiveSolveResult {
176 double solution = 0.0;
177 double priority_0_objective_value = 0.0;
178 double priority_1_objective_value = 0.0;
179};
180
181enum class ObjectiveType { kPrimary, kAuxiliary };
182enum class ToleranceType { kAbsolute, kRelative };
183
184// We consider the two objective model
185// priority 0: max(x)
186// priority 1: min(x)
187// s.t. 0 <= x <= 2
188// x is integer
189//
190// The optimal solution is x^* = 2 with objective values (2, 2). We test the
191// degradation tolerances by setting them on the priority 0 objective such that
192// the optimal solution, (up to tolerances), is x^* = 1 with objective values
193// (1, 1). We can accomplish this with an absolute tolerance of 1 or a relative
194// tolerance of 0.5.
195absl::StatusOr<SimpleMultiObjectiveSolveResult> SolveWithObjectiveDegradation(
196 const SolverType solver_type, const SolveParameters& parameters,
197 const ObjectiveType priority_0_type, const ObjectiveType priority_1_type,
198 const ToleranceType tolerance_type) {
199 if (priority_0_type == ObjectiveType::kPrimary &&
200 priority_1_type == ObjectiveType::kPrimary) {
201 return absl::InvalidArgumentError("Both objectives cannot both be primary");
202 }
203 Model model;
204 const Variable x = model.AddIntegerVariable(0.0, 2.0, "x");
205 const Objective priority_0 = [&]() {
206 switch (priority_0_type) {
207 case ObjectiveType::kPrimary:
208 model.Maximize(x);
209 model.set_objective_priority(model.primary_objective(), 0);
210 return model.primary_objective();
211 case ObjectiveType::kAuxiliary:
212 return model.AddMaximizationObjective(x, /*priority=*/0);
213 }
214 ABSL_UNREACHABLE();
215 }();
216 const Objective priority_1 = [&]() {
217 switch (priority_1_type) {
218 case ObjectiveType::kPrimary:
219 model.Minimize(x);
220 model.set_objective_priority(model.primary_objective(), 1);
221 return model.primary_objective();
222 case ObjectiveType::kAuxiliary:
223 return model.AddMinimizationObjective(x, /*priority=*/1);
224 }
225 ABSL_UNREACHABLE();
226 }();
227 ModelSolveParameters model_parameters;
228 switch (tolerance_type) {
229 case ToleranceType::kAbsolute:
230 model_parameters.objective_parameters[priority_0]
231 .objective_degradation_absolute_tolerance = 1.0;
232 break;
233 case ToleranceType::kRelative:
234 model_parameters.objective_parameters[priority_0]
235 .objective_degradation_relative_tolerance = 0.5;
236 }
238 const SolveResult result,
239 Solve(model, solver_type,
240 {.parameters = parameters, .model_parameters = model_parameters}));
241 if (!result.has_primal_feasible_solution()) {
242 return absl::InternalError("No feasible solution found");
243 }
244 return SimpleMultiObjectiveSolveResult{
245 .termination = result.termination.reason,
246 .solution = result.best_primal_solution().variable_values.at(x),
247 .priority_0_objective_value = result.objective_value(priority_0),
248 .priority_1_objective_value = result.objective_value(priority_1)};
249}
250
251TEST_P(SimpleMultiObjectiveTest, PrimaryObjectiveDegradationAbsoluteTolerance) {
252 if (!GetParam().supports_auxiliary_objectives) {
253 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
254 }
255 ASSERT_OK_AND_ASSIGN(const SimpleMultiObjectiveSolveResult result,
256 SolveWithObjectiveDegradation(
257 GetParam().solver_type, GetParam().parameters,
258 /*priority_0_type=*/ObjectiveType::kPrimary,
259 /*priority_1_type=*/ObjectiveType::kAuxiliary,
260 ToleranceType::kAbsolute));
261 EXPECT_EQ(result.termination, TerminationReason::kOptimal);
262 EXPECT_THAT(result.solution, DoubleNear(1.0, kTolerance));
263 EXPECT_THAT(result.priority_0_objective_value, DoubleNear(1.0, kTolerance));
264 EXPECT_THAT(result.priority_1_objective_value, DoubleNear(1.0, kTolerance));
265}
266
268 AuxiliaryObjectiveDegradationAbsoluteTolerance) {
269 if (!GetParam().supports_auxiliary_objectives) {
270 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
271 }
272 ASSERT_OK_AND_ASSIGN(const SimpleMultiObjectiveSolveResult result,
273 SolveWithObjectiveDegradation(
274 GetParam().solver_type, GetParam().parameters,
275 /*priority_0_type=*/ObjectiveType::kAuxiliary,
276 /*priority_1_type=*/ObjectiveType::kPrimary,
277 ToleranceType::kAbsolute));
278 EXPECT_EQ(result.termination, TerminationReason::kOptimal);
279 EXPECT_THAT(result.solution, DoubleNear(1.0, kTolerance));
280 EXPECT_THAT(result.priority_0_objective_value, DoubleNear(1.0, kTolerance));
281 EXPECT_THAT(result.priority_1_objective_value, DoubleNear(1.0, kTolerance));
282}
283
284TEST_P(SimpleMultiObjectiveTest, PrimaryObjectiveDegradationRelativeTolerance) {
285 if (!GetParam().supports_auxiliary_objectives) {
286 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
287 }
288 ASSERT_OK_AND_ASSIGN(const SimpleMultiObjectiveSolveResult result,
289 SolveWithObjectiveDegradation(
290 GetParam().solver_type, GetParam().parameters,
291 /*priority_0_type=*/ObjectiveType::kPrimary,
292 /*priority_1_type=*/ObjectiveType::kAuxiliary,
293 ToleranceType::kRelative));
294 EXPECT_EQ(result.termination, TerminationReason::kOptimal);
295 EXPECT_THAT(result.solution, DoubleNear(1.0, kTolerance));
296 EXPECT_THAT(result.priority_0_objective_value, DoubleNear(1.0, kTolerance));
297 EXPECT_THAT(result.priority_1_objective_value, DoubleNear(1.0, kTolerance));
298}
299
300// You should be able to specify this parameter for a single objective model; it
301// will be ignored.
303 SingleObjectiveModelWithObjectiveDegradationAbsoluteTolerance) {
304 if (!GetParam().supports_auxiliary_objectives) {
305 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
306 }
307 Model model;
308 const Variable x = model.AddIntegerVariable(0.0, 1.0, "x");
309 model.Maximize(x);
310 ModelSolveParameters model_parameters;
311 model_parameters.objective_parameters[model.primary_objective()]
312 .objective_degradation_absolute_tolerance = 0.5;
314 Solve(model, GetParam().solver_type,
315 {.parameters = GetParam().parameters,
316 .model_parameters = model_parameters}));
317 ASSERT_THAT(result, IsOptimalWithSolution(1.0, {{x, 1.0}}));
318}
319
320// You should be able to specify this parameter for a single objective model; it
321// will be ignored.
323 SingleObjectiveModelWithObjectiveDegradationRelativeTolerance) {
324 if (!GetParam().supports_auxiliary_objectives) {
325 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
326 }
327 Model model;
328 const Variable x = model.AddIntegerVariable(0.0, 1.0, "x");
329 model.Maximize(x);
330 ModelSolveParameters model_parameters;
331 model_parameters.objective_parameters[model.primary_objective()]
332 .objective_degradation_relative_tolerance = 0.5;
334 Solve(model, GetParam().solver_type,
335 {.parameters = GetParam().parameters,
336 .model_parameters = model_parameters}));
337 ASSERT_THAT(result, IsOptimalWithSolution(1.0, {{x, 1.0}}));
338}
339
341 AuxiliaryObjectiveDegradationRelativeTolerance) {
342 if (!GetParam().supports_auxiliary_objectives) {
343 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
344 }
345 ASSERT_OK_AND_ASSIGN(const SimpleMultiObjectiveSolveResult result,
346 SolveWithObjectiveDegradation(
347 GetParam().solver_type, GetParam().parameters,
348 /*priority_0_type=*/ObjectiveType::kAuxiliary,
349 /*priority_1_type=*/ObjectiveType::kPrimary,
350 ToleranceType::kRelative));
351 EXPECT_EQ(result.termination, TerminationReason::kOptimal);
352 EXPECT_THAT(result.solution, DoubleNear(1.0, kTolerance));
353 EXPECT_THAT(result.priority_0_objective_value, DoubleNear(1.0, kTolerance));
354 EXPECT_THAT(result.priority_1_objective_value, DoubleNear(1.0, kTolerance));
355}
356
357// Tests time limits on MIPLIB instance 23588. This instance was selected
358// because every supported solver can solve it quickly (a few seconds), but no
359// solver can solve it too quickly (so we can test time limits).
360//
361// Note that this instance is a MIP.
362absl::StatusOr<std::unique_ptr<Model>> Load23588MiplibInstance() {
364 const ModelProto model_proto,
365 ReadMpsFile(absl::StrCat("ortools/math_opt/solver_tests/testdata/"
366 "23588.mps")));
367 return Model::FromModelProto(model_proto);
368}
369
370// We move the "hard" objective to the auxiliary objective, and set a trivial
371// primary objective (0).
373 MultiObjectiveModelWithAuxiliaryObjectiveTimeLimit) {
374 if (!GetParam().supports_integer_variables) {
375 GTEST_SKIP() << kNoIntegerVariableSupportMessage;
376 }
377 if (GetParam().solver_type == SolverType::kXpress) {
378 GTEST_SKIP() << "Ignoring this test because Xpress does not support per "
379 "objective time limits at the moment";
380 }
381 ASSERT_OK_AND_ASSIGN(const std::unique_ptr<Model> model,
382 Load23588MiplibInstance());
383 const Objective aux_obj = model->AddMaximizationObjective(
384 model->primary_objective().AsLinearExpression(), /*priority=*/1);
385 model->clear_objective();
386 const SolveArguments args = {
387 .parameters = GetParam().parameters,
388 .model_parameters = {
389 .objective_parameters = {
390 {aux_obj, {.time_limit = absl::Milliseconds(1)}}}}};
391 const auto result = Solve(*model, GetParam().solver_type, args);
392 if (!GetParam().supports_auxiliary_objectives) {
393 EXPECT_THAT(result, StatusIs(absl::StatusCode::kInvalidArgument,
394 HasSubstr("multiple objectives")));
395 return;
396 }
397 ASSERT_OK(result);
399 // Solvers do not stop very precisely, use a large number to avoid flaky
400 // tests. Do NOT try to fine tune this to be small, it is hard to get right
401 // for all compilation modes (e.g., debug, asan).
402 EXPECT_LE(result->solve_stats.solve_time, absl::Seconds(1));
403}
404
405// We keep the "hard" objective as the primary objective, and add a trivial
406// auxiliary objective (0).
408 MultiObjectiveModelWithPrimaryObjectiveTimeLimit) {
409 if (!GetParam().supports_integer_variables) {
410 GTEST_SKIP() << kNoIntegerVariableSupportMessage;
411 }
412 if (GetParam().solver_type == SolverType::kXpress) {
413 GTEST_SKIP() << "Ignoring this test because Xpress does not support per "
414 "objective time limits at the moment";
415 }
416 ASSERT_OK_AND_ASSIGN(const std::unique_ptr<Model> model,
417 Load23588MiplibInstance());
418 model->AddMaximizationObjective(0, /*priority=*/1);
419 const SolveArguments args = {
420 .parameters = GetParam().parameters,
421 .model_parameters = {
422 .objective_parameters = {{model->primary_objective(),
423 {.time_limit = absl::Milliseconds(1)}}}}};
424 const auto result = Solve(*model, GetParam().solver_type, args);
425 if (!GetParam().supports_auxiliary_objectives) {
426 EXPECT_THAT(result, StatusIs(absl::StatusCode::kInvalidArgument,
427 HasSubstr("multiple objectives")));
428 return;
429 }
430 ASSERT_OK(result);
432 // Solvers do not stop very precisely, use a large number to avoid flaky
433 // tests. Do NOT try to fine tune this to be small, it is hard to get right
434 // for all compilation modes (e.g., debug, asan).
435 EXPECT_LE(result->solve_stats.solve_time, absl::Seconds(1));
436}
437
438// We test that, for a non-multi-objective model, we either respect the primary
439// objective time limit if the solver supports multi-objective models, or error
440// otherwise.
442 SingleObjectiveModelWithPrimaryObjectiveTimeLimit) {
443 if (!GetParam().supports_auxiliary_objectives) {
444 return;
445 }
446 if (!GetParam().supports_integer_variables) {
447 GTEST_SKIP() << kNoIntegerVariableSupportMessage;
448 }
449 ASSERT_OK_AND_ASSIGN(const std::unique_ptr<Model> model,
450 Load23588MiplibInstance());
451 SolveArguments args = {
452 .parameters = GetParam().parameters,
453 .model_parameters = {
454 .objective_parameters = {{model->primary_objective(),
455 {.time_limit = absl::Milliseconds(1)}}}}};
456 args.parameters.time_limit = absl::Seconds(10);
458 Solve(*model, GetParam().solver_type, args));
460 // Solvers do not stop very precisely, use a large number to avoid flaky
461 // tests. Do NOT try to fine tune this to be small, it is hard to get right
462 // for all compilation modes (e.g., debug, asan).
463 EXPECT_LE(result.solve_stats.solve_time, absl::Seconds(1));
464}
465
466// We refine SingleObjectiveModelWithPrimaryObjectiveTimeLimit to ensure that
467// we take the minimum of the global time limit and the primary objective time
468// limit.
470 SingleObjectiveModelTakesMininumFromPrimaryAndGlobalTimeLimits) {
471 if (!GetParam().supports_auxiliary_objectives) {
472 return;
473 }
474 if (!GetParam().supports_integer_variables) {
475 GTEST_SKIP() << kNoIntegerVariableSupportMessage;
476 }
477 if (GetParam().solver_type == SolverType::kXpress) {
478 GTEST_SKIP() << "Ignoring this test because Xpress does not support per "
479 "objective time limits at the moment";
480 }
481 ASSERT_OK_AND_ASSIGN(const std::unique_ptr<Model> model,
482 Load23588MiplibInstance());
483 SolveArguments args = {
484 .parameters = GetParam().parameters,
485 .model_parameters = {
486 .objective_parameters = {{model->primary_objective(),
487 {.time_limit = absl::Seconds(10)}}}}};
488 args.parameters.time_limit = absl::Milliseconds(1);
490 Solve(*model, GetParam().solver_type, args));
492 // Solvers do not stop very precisely, use a large number to avoid flaky
493 // tests. Do NOT try to fine tune this to be small, it is hard to get right
494 // for all compilation modes (e.g., debug, asan).
495 EXPECT_LE(result.solve_stats.solve_time, absl::Seconds(1));
496}
497
498// We test that all solvers that do not support multi-objective models error
499// when given primary objective parameters.
501 SolverWithoutSupportErrorsWithPrimaryObjectiveParameters) {
502 if (GetParam().supports_auxiliary_objectives) {
503 return;
504 }
505 Model model;
506 const SolveArguments args = {
507 .parameters = GetParam().parameters,
508 .model_parameters = {
509 .objective_parameters = {
510 {model.primary_objective(), {.time_limit = absl::Seconds(10)}}}}};
511 EXPECT_THAT(Solve(model, GetParam().solver_type, args),
512 StatusIs(absl::StatusCode::kInvalidArgument,
513 HasSubstr("primary_objective_parameters")));
514}
515
516// We start with the single objective model:
517// max x
518// s.t. x + y <= 1.5
519// 0 <= x, y <= 1
520//
521// The optimal objective value is 1, and (x^*, y^*) = (1, a) is optimal for any
522// value a in [0, 1].
523//
524// We then add the secondary objective
525//
526// max {x, x + 3*y + 2}
527// s.t. x + y <= 1.5
528// 0 <= x, y <= 1
529//
530// This has the unique optimal solution (x^*, y^*) = (1, 0.5) with objective
531// values (1, 4.5).
532TEST_P(IncrementalMultiObjectiveTest, SingleToMultiObjectiveModel) {
533 Model model;
534 const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
535 const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
536 model.AddLinearConstraint(x + y <= 1.5);
537 model.Maximize(x);
538
540 const auto solver,
541 NewIncrementalSolver(&model, GetParam().solver_type, {}));
542 // Since there are multiple optimal solutions we do not match against the
543 // solution value.
544 ASSERT_THAT(solver->Solve({.parameters = GetParam().parameters}),
545 IsOkAndHolds(IsOptimal(1.0)));
546
547 const Objective o =
548 model.AddMaximizationObjective(x + 3.0 * y + 2.0, /*priority=*/1);
549
550 if (!GetParam().supports_auxiliary_objectives) {
551 // Here we test that solvers that don't support auxiliary objectives return
552 // false in SolverInterface::CanUpdate(). Thus they should fail in their
553 // factory function instead of failing in their SolverInterface::Update()
554 // function. To assert we rely on status annotations added by
555 // IncrementalSolver::Update() to the returned status of Solver::Update()
556 // and Solver::New().
558 solver->Update(),
559 StatusIs(AnyOf(absl::StatusCode::kInvalidArgument,
560 absl::StatusCode::kUnimplemented),
561 AllOf(HasSubstr("multiple objective"),
562 // Sub-string expected for Solver::Update() error.
563 Not(HasSubstr("update failed")),
564 // Sub-string expected for Solver::New() error.
565 HasSubstr("solver re-creation failed"))));
566 return;
567 }
568
569 ASSERT_THAT(
570 solver->Update(),
571 IsOkAndHolds(GetParam().supports_incremental_objective_add_and_delete
572 ? DidUpdate()
573 : Not(DidUpdate())));
575 const SolveResult result,
576 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}));
577 ASSERT_THAT(result, IsOptimalWithSolution(1.0, {{x, 1.0}, {y, 0.5}}));
578 EXPECT_EQ(result.objective_value(o), 4.5);
579}
580
581// We start with the two objective model:
582// max {x, 3}
583// s.t. x + y <= 1.5
584// 0 <= x, y <= 1
585//
586// The optimal objective values are (1, 3), and (x^*, y^*) = (1, a) is optimal
587// for any value a in [0, 1].
588//
589// We then add the tertiary objective
590//
591// max {x, 3, x + 3*y + 2}
592// s.t. x + y <= 1.5
593// 0 <= x, y <= 1
594//
595// This has the unique optimal solution (x^*, y^*) = (1, 0.5) with objective
596// values (1, 3, 4.5).
597TEST_P(IncrementalMultiObjectiveTest, AddObjectiveToMultiObjectiveModel) {
598 if (!GetParam().supports_auxiliary_objectives) {
599 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
600 }
601 if (!GetParam().supports_incremental_objective_add_and_delete) {
602 GTEST_SKIP()
603 << "Ignoring this test as it requires support for incremental solve";
604 }
605 Model model;
606 const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
607 const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
608 model.AddLinearConstraint(x + y <= 1.5);
609 model.Maximize(x);
610 const Objective o = model.AddMaximizationObjective(3.0, /*priority=*/2);
611
613 const auto solver,
614 NewIncrementalSolver(&model, GetParam().solver_type, {}));
615 {
617 solver->Solve({.parameters = GetParam().parameters}));
618 ASSERT_THAT(result, IsOptimal(1.0));
619 EXPECT_EQ(result.objective_value(o), 3.0);
620 }
621
622 const Objective o2 =
623 model.AddMaximizationObjective(x + 3.0 * y + 2.0, /*priority=*/5);
624
625 ASSERT_THAT(
626 solver->Update(),
627 IsOkAndHolds(GetParam().supports_incremental_objective_add_and_delete
628 ? DidUpdate()
629 : Not(DidUpdate())));
631 const SolveResult result,
632 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}));
633 ASSERT_THAT(result, IsOptimalWithSolution(1.0, {{x, 1.0}, {y, 0.5}}));
634 EXPECT_EQ(result.objective_value(o), 3.0);
635 EXPECT_EQ(result.objective_value(o2), 4.5);
636}
637
638// We start with the three objective model:
639// max {x, 3, x + 3*y + 2}
640// s.t. x + y <= 1.5
641// 0 <= x, y <= 1
642//
643// This has the unique optimal solution (x^*, y^*) = (1, 0.5) with objective
644// values (1, 3, 4.5).
645//
646// We then delete the second objective, leaving
647//
648// max {x, x + 3*y + 2}
649// s.t. x + y <= 1.5
650// 0 <= x, y <= 1
651//
652// This has the unique optimal solution (x^*, y^*) = (1, 0.5) with objective
653// values (1, 4.5).
654TEST_P(IncrementalMultiObjectiveTest, DeleteObjectiveFromMultiObjectiveModel) {
655 if (!GetParam().supports_auxiliary_objectives) {
656 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
657 }
658 if (!GetParam().supports_incremental_objective_add_and_delete) {
659 GTEST_SKIP()
660 << "Ignoring this test as it requires support for incremental solve";
661 }
662 Model model;
663 const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
664 const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
665 model.AddLinearConstraint(x + y <= 1.5);
666 model.Maximize(x);
667 const Objective o = model.AddMaximizationObjective(3.0, /*priority=*/2);
668 const Objective o2 =
669 model.AddMaximizationObjective(x + 3.0 * y + 2.0, /*priority=*/3);
670
672 const auto solver,
673 NewIncrementalSolver(&model, GetParam().solver_type, {}));
674 {
676 solver->Solve({.parameters = GetParam().parameters}));
677 ASSERT_THAT(result, IsOptimalWithSolution(1.0, {{x, 1.0}, {y, 0.5}}));
678 EXPECT_EQ(result.objective_value(o), 3.0);
679 EXPECT_EQ(result.objective_value(o2), 4.5);
680 }
681
682 model.DeleteAuxiliaryObjective(o);
683
684 ASSERT_THAT(
685 solver->Update(),
686 IsOkAndHolds(GetParam().supports_incremental_objective_add_and_delete
687 ? DidUpdate()
688 : Not(DidUpdate())));
690 const SolveResult result,
691 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}));
692 ASSERT_THAT(result, IsOptimalWithSolution(1.0, {{x, 1.0}, {y, 0.5}}));
693 EXPECT_EQ(result.objective_value(o2), 4.5);
694}
695
696// We start with the two objective model:
697// {max(x), max(x + 3*y + 2)}
698// s.t. x + y <= 1.5
699// 0 <= x, y <= 1
700//
701// This has the unique optimal solution (x^*, y^*) = (1, 0.5) with objective
702// values (1, 4.5).
703//
704// We then flip the sign of the first objective, leaving
705//
706// {min(x), max(x + 3*y + 2)}
707// s.t. x + y <= 1.5
708// 0 <= x, y <= 1
709//
710// This has the unique optimal solution (x^*, y^*) = (0, 1) with objective
711// values (0, 5).
713 ModifyPrimaryObjectiveSenseInMultiObjectiveModel) {
714 if (!GetParam().supports_auxiliary_objectives) {
715 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
716 }
717 Model model;
718 const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
719 const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
720 model.AddLinearConstraint(x + y <= 1.5);
721 model.Maximize(x);
722 const Objective o =
723 model.AddMaximizationObjective(x + 3.0 * y + 2.0, /*priority=*/2);
724
726 const auto solver,
727 NewIncrementalSolver(&model, GetParam().solver_type, {}));
728 {
730 solver->Solve({.parameters = GetParam().parameters}));
731 ASSERT_THAT(result, IsOptimalWithSolution(1.0, {{x, 1.0}, {y, 0.5}}));
732 ASSERT_OK_AND_ASSIGN(const SolveResultProto result_proto, result.Proto());
733 EXPECT_EQ(result.objective_value(o), 4.5);
734 }
735
736 model.set_minimize();
737
738 ASSERT_THAT(
739 solver->Update(),
740 IsOkAndHolds(GetParam().supports_incremental_objective_modification
741 ? DidUpdate()
742 : Not(DidUpdate())));
744 const SolveResult result,
745 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}));
746 ASSERT_THAT(result, IsOptimalWithSolution(0.0, {{x, 0.0}, {y, 1.0}}));
747 EXPECT_EQ(result.objective_value(o), 5.0);
748}
749
750// Same problem as ModifyPrimaryObjectiveSenseInMultiObjectiveModel, except we
751// switch which objective is primary and which is auxiliary.
753 ModifyAuxiliaryObjectiveSenseInMultiObjectiveModel) {
754 if (!GetParam().supports_auxiliary_objectives) {
755 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
756 }
757 Model model;
758 const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
759 const Variable y = model.AddContinuousVariable(0.0, 1.0, "y");
760 model.AddLinearConstraint(x + y <= 1.5);
761 model.Maximize(x + 3.0 * y + 2.0);
762 model.set_objective_priority(model.primary_objective(), 2);
763 const Objective o = model.AddMaximizationObjective(x, /*priority=*/0);
764
766 const auto solver,
767 NewIncrementalSolver(&model, GetParam().solver_type, {}));
768 {
770 solver->Solve({.parameters = GetParam().parameters}));
771 ASSERT_THAT(result, IsOptimalWithSolution(4.5, {{x, 1.0}, {y, 0.5}}));
772 EXPECT_EQ(result.objective_value(o), 1.0);
773 }
774
775 model.set_minimize(o);
776
777 ASSERT_THAT(
778 solver->Update(),
779 IsOkAndHolds(GetParam().supports_incremental_objective_modification
780 ? DidUpdate()
781 : Not(DidUpdate())));
783 const SolveResult result,
784 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}));
785 ASSERT_THAT(result, IsOptimalWithSolution(5.0, {{x, 0.0}, {y, 1.0}}));
786 EXPECT_EQ(result.objective_value(o), 0.0);
787}
788
789// TODO(b/214027410): Add these to IncrementalMultiObjectiveTest when needed:
790// * ModifyPrimaryObjectiveOffsetInMultiObjectiveModel
791// * ModifyAuxiliaryObjectiveOffsetInMultiObjectiveModel
792// * ModifyPrimaryObjectiveLinearCoefficientInMultiObjectiveModel
793// * ModifyAuxiliaryObjectiveLinearCoefficientInMultiObjectiveModel
794// * AfterUpdatePrimaryAndAuxiliaryObjectiveSharePriority
795// * AfterUpdateAuxiliaryObjectivesSharePriority
796
797} // namespace
798} // namespace operations_research::math_opt
#define ASSIGN_OR_RETURN(lhs, rexpr)
Definition model.h:345
void Maximize(Variable *obj, std::vector< Annotation > search_annotations)
Definition model.cc:1074
void Minimize(Variable *obj, std::vector< Annotation > search_annotations)
Definition model.cc:1067
static absl::StatusOr< std::unique_ptr< Model > > FromModelProto(const ModelProto &model_proto)
Definition model.cc:56
#define ASSERT_OK(expression)
Definition gmock.h:46
#define EXPECT_OK(expression)
Definition gmock.h:45
#define ASSERT_OK_AND_ASSIGN(lhs, rexpr)
Definition gmock.h:53
Matcher< SolveResult > IsOptimalWithSolution(const double expected_objective, const VariableMap< double > expected_variable_values, const double tolerance)
Definition matchers.cc:778
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)
absl::StatusOr< SolveResult > Solve(const Model &model, const SolverType solver_type, const SolveArguments &solve_args, const SolverInitArguments &init_args)
Definition solve.cc:62
<=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.";} if(GetParam().solver_type==SolverType::kXpress) { GTEST_SKIP()<< "Xpress does not support contradictory bounds.";} 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->SolveWithoutUpdate(), IsOkAndHolds(IsOptimal(1.5)));model.Minimize(x);ASSERT_THAT(solver->Update(), IsOkAndHolds(DidUpdate()));ASSERT_THAT(solver-> IsOkAndHolds(IsOptimal(0.5)))
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:649
std::ostream & operator<<(std::ostream &ostr, const SecondOrderConeConstraint &constraint)
constexpr absl::string_view kNoMultiObjectiveSupportMessage
Matcher< UpdateResult > DidUpdate()
Definition matchers.cc:1028
constexpr absl::string_view kNoIntegerVariableSupportMessage
Matcher< SolveResult > IsOptimal(const std::optional< double > expected_primal_objective, const double tolerance)
Definition matchers.cc:763
BoolVar Not(BoolVar x)
Definition cp_model.cc:87
std::string ProtobufShortDebugString(const P &message)
Definition proto_utils.h:46
STL namespace.
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)