Google OR-Tools v9.11
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-2024 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
19#include "absl/status/status.h"
20#include "absl/status/statusor.h"
21#include "absl/strings/string_view.h"
22#include "gtest/gtest.h"
23#include "ortools/base/gmock.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"
31
33
34using ::testing::AnyOf;
35using ::testing::DoubleNear;
36using ::testing::HasSubstr;
37using ::testing::status::IsOkAndHolds;
38using ::testing::status::StatusIs;
39
40constexpr absl::string_view kNoMultiObjectiveSupportMessage =
41 "This test is disabled as the solver does not support multiple objective "
42 "models";
43
44constexpr double kTolerance = 1.0e-6;
45
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) {}
58
59std::ostream& operator<<(std::ostream& out,
60 const MultiObjectiveTestParameters& params) {
61 out << "{ solver_type: " << params.solver_type
62 << ", parameters: " << ProtobufShortDebugString(params.parameters.Proto())
63 << ", supports_auxiliary_objectives: "
64 << (params.supports_auxiliary_objectives ? "true" : "false")
65 << ", supports_incremental_objective_add_and_delete: "
67 : "false")
68 << ", supports_incremental_objective_modification: "
69 << (params.supports_incremental_objective_modification ? "true" : "false")
70 << " }";
71 return out;
72}
73
74namespace {
75
76TEST_P(SimpleMultiObjectiveTest, CanBuildMultiObjectiveModel) {
77 Model model;
78 const Variable x = model.AddContinuousVariable(0.0, 1.0, "x");
79 model.AddMaximizationObjective(x, /*priority=*/2);
80 model.AddMinimizationObjective(-3.0 * x + 2.0, /*priority=*/1);
81
82 if (GetParam().supports_auxiliary_objectives) {
83 EXPECT_OK(NewIncrementalSolver(&model, GetParam().solver_type, {}));
84 } else {
85 EXPECT_THAT(NewIncrementalSolver(&model, GetParam().solver_type, {}),
86 StatusIs(AnyOf(absl::StatusCode::kInvalidArgument,
87 absl::StatusCode::kUnimplemented),
88 HasSubstr("multiple objectives")));
89 }
90}
91
92// We consider the two objective model
93// max {x, x + 3*y + 2}
94// s.t. x + y <= 1.5
95// 0 <= x, y <= 1
96//
97// This has the unique optimal solution (x^*, y^*) = (1, 0.5) with objective
98// values (1, 4.5).
99TEST_P(SimpleMultiObjectiveTest, SolveMultiObjectiveModel) {
100 if (!GetParam().supports_auxiliary_objectives) {
101 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
102 }
103 Model model;
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);
107 model.Maximize(x);
108 const Objective o =
109 model.AddMaximizationObjective(x + 3.0 * y + 2.0, /*priority=*/1);
110
111 ASSERT_OK_AND_ASSIGN(const SolveResult result, SimpleSolve(model));
112 ASSERT_THAT(result, IsOptimalWithSolution(1.0, {{x, 1.0}, {y, 0.5}}));
113 EXPECT_EQ(result.objective_value(o), 4.5);
114}
115
116// We consider the two objective model
117// {min(-x), max(x + 3*y + 2)}
118// s.t. x + y <= 1.5
119// 0 <= x, y <= 1
120//
121// This has the unique optimal solution (x^*, y^*) = (1, 0.5) with objective
122// values (-1, 4.5).
123TEST_P(SimpleMultiObjectiveTest, MultipleObjectivesWithDifferentSenses) {
124 if (!GetParam().supports_auxiliary_objectives) {
125 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
126 }
127 Model model;
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);
131 model.Minimize(-x);
132 const Objective o =
133 model.AddMaximizationObjective(x + 3.0 * y + 2.0, /*priority=*/1);
134
135 ASSERT_OK_AND_ASSIGN(const SolveResult result, SimpleSolve(model));
136 ASSERT_THAT(result, IsOptimalWithSolution(-1.0, {{x, 1.0}, {y, 0.5}}));
137 EXPECT_EQ(result.objective_value(o), 4.5);
138}
139
140TEST_P(SimpleMultiObjectiveTest, PrimaryAndAuxiliaryObjectiveSharePriority) {
141 if (!GetParam().supports_auxiliary_objectives) {
142 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
143 }
144 Model model;
145 model.set_objective_priority(model.primary_objective(), 1);
146 model.AddAuxiliaryObjective(1);
147 EXPECT_THAT(NewIncrementalSolver(&model, GetParam().solver_type, {}),
148 StatusIs(absl::StatusCode::kInvalidArgument,
149 HasSubstr("repeated objective priority: 1")));
150}
151
152TEST_P(SimpleMultiObjectiveTest, AuxiliaryObjectivesSharePriority) {
153 if (!GetParam().supports_auxiliary_objectives) {
154 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
155 }
156 Model model;
157 model.AddAuxiliaryObjective(1);
158 model.AddAuxiliaryObjective(1);
159 EXPECT_THAT(NewIncrementalSolver(&model, GetParam().solver_type, {}),
160 StatusIs(absl::StatusCode::kInvalidArgument,
161 HasSubstr("repeated objective priority: 1")));
162}
163
164// For a univariate optimization problem with two objectives.
165struct SimpleMultiObjectiveSolveResult {
167 double solution = 0.0;
170};
171
172enum class ObjectiveType { kPrimary, kAuxiliary };
173enum class ToleranceType { kAbsolute, kRelative };
174
175// We consider the two objective model
176// priority 0: max(x)
177// priority 1: min(x)
178// s.t. 0 <= x <= 2
179// x is integer
180//
181// The optimal solution is x^* = 2 with objective values (2, 2). We test the
182// degradation tolerances by setting them on the priority 0 objective such that
183// the optimal solution, (up to tolerances), is x^* = 1 with objective values
184// (1, 1). We can accomplish this with an absolute tolerance of 1 or a relative
185// tolerance of 0.5.
186absl::StatusOr<SimpleMultiObjectiveSolveResult> SolveWithObjectiveDegradation(
187 const SolverType solver_type, const SolveParameters& parameters,
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");
193 }
194 Model model;
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:
199 model.Maximize(x);
200 model.set_objective_priority(model.primary_objective(), 0);
201 return model.primary_objective();
202 case ObjectiveType::kAuxiliary:
203 return model.AddMaximizationObjective(x, /*priority=*/0);
204 }
205 }();
206 const Objective priority_1 = [&]() {
207 switch (priority_1_type) {
208 case ObjectiveType::kPrimary:
209 model.Minimize(x);
210 model.set_objective_priority(model.primary_objective(), 1);
211 return model.primary_objective();
212 case ObjectiveType::kAuxiliary:
213 return model.AddMinimizationObjective(x, /*priority=*/1);
214 }
215 }();
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;
221 break;
222 case ToleranceType::kRelative:
223 model_parameters.objective_parameters[priority_0]
224 .objective_degradation_relative_tolerance = 0.5;
225 }
227 const SolveResult result,
228 Solve(model, solver_type,
229 {.parameters = parameters, .model_parameters = model_parameters}));
230 if (!result.has_primal_feasible_solution()) {
231 return absl::InternalError("No feasible solution found");
232 }
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)};
238}
239
240TEST_P(SimpleMultiObjectiveTest, PrimaryObjectiveDegradationAbsoluteTolerance) {
241 if (!GetParam().supports_auxiliary_objectives) {
242 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
243 }
244 ASSERT_OK_AND_ASSIGN(const SimpleMultiObjectiveSolveResult result,
245 SolveWithObjectiveDegradation(
246 GetParam().solver_type, GetParam().parameters,
247 /*priority_0_type=*/ObjectiveType::kPrimary,
248 /*priority_1_type=*/ObjectiveType::kAuxiliary,
249 ToleranceType::kAbsolute));
250 EXPECT_EQ(result.termination, TerminationReason::kOptimal);
251 EXPECT_THAT(result.solution, DoubleNear(1.0, kTolerance));
252 EXPECT_THAT(result.priority_0_objective_value, DoubleNear(1.0, kTolerance));
253 EXPECT_THAT(result.priority_1_objective_value, DoubleNear(1.0, kTolerance));
254}
255
256TEST_P(SimpleMultiObjectiveTest,
257 AuxiliaryObjectiveDegradationAbsoluteTolerance) {
258 if (!GetParam().supports_auxiliary_objectives) {
259 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
260 }
261 ASSERT_OK_AND_ASSIGN(const SimpleMultiObjectiveSolveResult result,
262 SolveWithObjectiveDegradation(
263 GetParam().solver_type, GetParam().parameters,
264 /*priority_0_type=*/ObjectiveType::kAuxiliary,
265 /*priority_1_type=*/ObjectiveType::kPrimary,
266 ToleranceType::kAbsolute));
267 EXPECT_EQ(result.termination, TerminationReason::kOptimal);
268 EXPECT_THAT(result.solution, DoubleNear(1.0, kTolerance));
269 EXPECT_THAT(result.priority_0_objective_value, DoubleNear(1.0, kTolerance));
270 EXPECT_THAT(result.priority_1_objective_value, DoubleNear(1.0, kTolerance));
271}
272
273TEST_P(SimpleMultiObjectiveTest, PrimaryObjectiveDegradationRelativeTolerance) {
274 if (!GetParam().supports_auxiliary_objectives) {
275 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
276 }
277 ASSERT_OK_AND_ASSIGN(const SimpleMultiObjectiveSolveResult result,
278 SolveWithObjectiveDegradation(
279 GetParam().solver_type, GetParam().parameters,
280 /*priority_0_type=*/ObjectiveType::kPrimary,
281 /*priority_1_type=*/ObjectiveType::kAuxiliary,
282 ToleranceType::kRelative));
283 EXPECT_EQ(result.termination, TerminationReason::kOptimal);
284 EXPECT_THAT(result.solution, DoubleNear(1.0, kTolerance));
285 EXPECT_THAT(result.priority_0_objective_value, DoubleNear(1.0, kTolerance));
286 EXPECT_THAT(result.priority_1_objective_value, DoubleNear(1.0, kTolerance));
287}
288
289// You should be able to specify this parameter for a single objective model; it
290// will be ignored.
291TEST_P(SimpleMultiObjectiveTest,
292 SingleObjectiveModelWithObjectiveDegradationAbsoluteTolerance) {
293 if (!GetParam().supports_auxiliary_objectives) {
294 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
295 }
296 Model model;
297 const Variable x = model.AddIntegerVariable(0.0, 1.0, "x");
298 model.Maximize(x);
299 ModelSolveParameters model_parameters;
300 model_parameters.objective_parameters[model.primary_objective()]
301 .objective_degradation_absolute_tolerance = 0.5;
302 ASSERT_OK_AND_ASSIGN(const SolveResult result,
303 Solve(model, GetParam().solver_type,
304 {.parameters = GetParam().parameters,
305 .model_parameters = model_parameters}));
306 ASSERT_THAT(result, IsOptimalWithSolution(1.0, {{x, 1.0}}));
307}
308
309// You should be able to specify this parameter for a single objective model; it
310// will be ignored.
311TEST_P(SimpleMultiObjectiveTest,
312 SingleObjectiveModelWithObjectiveDegradationRelativeTolerance) {
313 if (!GetParam().supports_auxiliary_objectives) {
314 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
315 }
316 Model model;
317 const Variable x = model.AddIntegerVariable(0.0, 1.0, "x");
318 model.Maximize(x);
319 ModelSolveParameters model_parameters;
320 model_parameters.objective_parameters[model.primary_objective()]
321 .objective_degradation_relative_tolerance = 0.5;
322 ASSERT_OK_AND_ASSIGN(const SolveResult result,
323 Solve(model, GetParam().solver_type,
324 {.parameters = GetParam().parameters,
325 .model_parameters = model_parameters}));
326 ASSERT_THAT(result, IsOptimalWithSolution(1.0, {{x, 1.0}}));
327}
328
329TEST_P(SimpleMultiObjectiveTest,
330 AuxiliaryObjectiveDegradationRelativeTolerance) {
331 if (!GetParam().supports_auxiliary_objectives) {
332 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
333 }
334 ASSERT_OK_AND_ASSIGN(const SimpleMultiObjectiveSolveResult result,
335 SolveWithObjectiveDegradation(
336 GetParam().solver_type, GetParam().parameters,
337 /*priority_0_type=*/ObjectiveType::kAuxiliary,
338 /*priority_1_type=*/ObjectiveType::kPrimary,
339 ToleranceType::kRelative));
340 EXPECT_EQ(result.termination, TerminationReason::kOptimal);
341 EXPECT_THAT(result.solution, DoubleNear(1.0, kTolerance));
342 EXPECT_THAT(result.priority_0_objective_value, DoubleNear(1.0, kTolerance));
343 EXPECT_THAT(result.priority_1_objective_value, DoubleNear(1.0, kTolerance));
344}
345
346// We start with the single objective model:
347// max x
348// s.t. x + y <= 1.5
349// 0 <= x, y <= 1
350//
351// The optimal objective value is 1, and (x^*, y^*) = (1, a) is optimal for any
352// value a in [0, 1].
353//
354// We then add the secondary objective
355//
356// max {x, x + 3*y + 2}
357// s.t. x + y <= 1.5
358// 0 <= x, y <= 1
359//
360// This has the unique optimal solution (x^*, y^*) = (1, 0.5) with objective
361// values (1, 4.5).
362TEST_P(IncrementalMultiObjectiveTest, SingleToMultiObjectiveModel) {
363 Model model;
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);
367 model.Maximize(x);
368
370 const auto solver,
371 NewIncrementalSolver(&model, GetParam().solver_type, {}));
372 // Since there are multiple optimal solutions we do not match against the
373 // solution value.
374 ASSERT_THAT(solver->Solve({.parameters = GetParam().parameters}),
375 IsOkAndHolds(IsOptimal(1.0)));
376
377 const Objective o =
378 model.AddMaximizationObjective(x + 3.0 * y + 2.0, /*priority=*/1);
379
380 if (!GetParam().supports_auxiliary_objectives) {
381 // Here we test that solvers that don't support auxiliary objectives return
382 // false in SolverInterface::CanUpdate(). Thus they should fail in their
383 // factory function instead of failing in their SolverInterface::Update()
384 // function. To assert we rely on status annotations added by
385 // IncrementalSolver::Update() to the returned status of Solver::Update()
386 // and Solver::New().
388 solver->Update(),
389 StatusIs(AnyOf(absl::StatusCode::kInvalidArgument,
390 absl::StatusCode::kUnimplemented),
391 AllOf(HasSubstr("multiple objective"),
392 // Sub-string expected for Solver::Update() error.
393 Not(HasSubstr("update failed")),
394 // Sub-string expected for Solver::New() error.
395 HasSubstr("solver re-creation failed"))));
396 return;
397 }
398
400 solver->Update(),
401 IsOkAndHolds(GetParam().supports_incremental_objective_add_and_delete
402 ? DidUpdate()
403 : Not(DidUpdate())));
405 const SolveResult result,
406 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}));
407 ASSERT_THAT(result, IsOptimalWithSolution(1.0, {{x, 1.0}, {y, 0.5}}));
408 EXPECT_EQ(result.objective_value(o), 4.5);
409}
410
411// We start with the two objective model:
412// max {x, 3}
413// s.t. x + y <= 1.5
414// 0 <= x, y <= 1
415//
416// The optimal objective values are (1, 3), and (x^*, y^*) = (1, a) is optimal
417// for any value a in [0, 1].
418//
419// We then add the tertiary objective
420//
421// max {x, 3, x + 3*y + 2}
422// s.t. x + y <= 1.5
423// 0 <= x, y <= 1
424//
425// This has the unique optimal solution (x^*, y^*) = (1, 0.5) with objective
426// values (1, 3, 4.5).
427TEST_P(IncrementalMultiObjectiveTest, AddObjectiveToMultiObjectiveModel) {
428 if (!GetParam().supports_auxiliary_objectives) {
429 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
430 }
431 Model model;
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);
435 model.Maximize(x);
436 const Objective o = model.AddMaximizationObjective(3.0, /*priority=*/2);
437
439 const auto solver,
440 NewIncrementalSolver(&model, GetParam().solver_type, {}));
441 {
442 ASSERT_OK_AND_ASSIGN(const SolveResult result,
443 solver->Solve({.parameters = GetParam().parameters}));
444 ASSERT_THAT(result, IsOptimal(1.0));
445 EXPECT_EQ(result.objective_value(o), 3.0);
446 }
447
448 const Objective o2 =
449 model.AddMaximizationObjective(x + 3.0 * y + 2.0, /*priority=*/5);
450
452 solver->Update(),
453 IsOkAndHolds(GetParam().supports_incremental_objective_add_and_delete
454 ? DidUpdate()
455 : Not(DidUpdate())));
457 const SolveResult result,
458 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}));
459 ASSERT_THAT(result, IsOptimalWithSolution(1.0, {{x, 1.0}, {y, 0.5}}));
460 EXPECT_EQ(result.objective_value(o), 3.0);
461 EXPECT_EQ(result.objective_value(o2), 4.5);
462}
463
464// We start with the three objective model:
465// max {x, 3, x + 3*y + 2}
466// s.t. x + y <= 1.5
467// 0 <= x, y <= 1
468//
469// This has the unique optimal solution (x^*, y^*) = (1, 0.5) with objective
470// values (1, 3, 4.5).
471//
472// We then delete the second objective, leaving
473//
474// max {x, x + 3*y + 2}
475// s.t. x + y <= 1.5
476// 0 <= x, y <= 1
477//
478// This has the unique optimal solution (x^*, y^*) = (1, 0.5) with objective
479// values (1, 4.5).
480TEST_P(IncrementalMultiObjectiveTest, DeleteObjectiveFromMultiObjectiveModel) {
481 if (!GetParam().supports_auxiliary_objectives) {
482 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
483 }
484 Model model;
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);
488 model.Maximize(x);
489 const Objective o = model.AddMaximizationObjective(3.0, /*priority=*/2);
490 const Objective o2 =
491 model.AddMaximizationObjective(x + 3.0 * y + 2.0, /*priority=*/3);
492
494 const auto solver,
495 NewIncrementalSolver(&model, GetParam().solver_type, {}));
496 {
497 ASSERT_OK_AND_ASSIGN(const SolveResult result,
498 solver->Solve({.parameters = GetParam().parameters}));
499 ASSERT_THAT(result, IsOptimalWithSolution(1.0, {{x, 1.0}, {y, 0.5}}));
500 EXPECT_EQ(result.objective_value(o), 3.0);
501 EXPECT_EQ(result.objective_value(o2), 4.5);
502 }
503
504 model.DeleteAuxiliaryObjective(o);
505
507 solver->Update(),
508 IsOkAndHolds(GetParam().supports_incremental_objective_add_and_delete
509 ? DidUpdate()
510 : Not(DidUpdate())));
512 const SolveResult result,
513 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}));
514 ASSERT_THAT(result, IsOptimalWithSolution(1.0, {{x, 1.0}, {y, 0.5}}));
515 EXPECT_EQ(result.objective_value(o2), 4.5);
516}
517
518// We start with the two objective model:
519// {max(x), max(x + 3*y + 2)}
520// s.t. x + y <= 1.5
521// 0 <= x, y <= 1
522//
523// This has the unique optimal solution (x^*, y^*) = (1, 0.5) with objective
524// values (1, 4.5).
525//
526// We then flip the sign of the first objective, leaving
527//
528// {min(x), max(x + 3*y + 2)}
529// s.t. x + y <= 1.5
530// 0 <= x, y <= 1
531//
532// This has the unique optimal solution (x^*, y^*) = (0, 1) with objective
533// values (0, 5).
534TEST_P(IncrementalMultiObjectiveTest,
535 ModifyPrimaryObjectiveSenseInMultiObjectiveModel) {
536 if (!GetParam().supports_auxiliary_objectives) {
537 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
538 }
539 Model model;
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);
543 model.Maximize(x);
544 const Objective o =
545 model.AddMaximizationObjective(x + 3.0 * y + 2.0, /*priority=*/2);
546
548 const auto solver,
549 NewIncrementalSolver(&model, GetParam().solver_type, {}));
550 {
551 ASSERT_OK_AND_ASSIGN(const SolveResult result,
552 solver->Solve({.parameters = GetParam().parameters}));
553 ASSERT_THAT(result, IsOptimalWithSolution(1.0, {{x, 1.0}, {y, 0.5}}));
554 ASSERT_OK_AND_ASSIGN(const SolveResultProto result_proto, result.Proto());
555 EXPECT_EQ(result.objective_value(o), 4.5);
556 }
557
558 model.set_minimize();
559
561 solver->Update(),
562 IsOkAndHolds(GetParam().supports_incremental_objective_modification
563 ? DidUpdate()
564 : Not(DidUpdate())));
566 const SolveResult result,
567 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}));
568 ASSERT_THAT(result, IsOptimalWithSolution(0.0, {{x, 0.0}, {y, 1.0}}));
569 EXPECT_EQ(result.objective_value(o), 5.0);
570}
571
572// Same problem as ModifyPrimaryObjectiveSenseInMultiObjectiveModel, except we
573// switch which objective is primary and which is auxiliary.
574TEST_P(IncrementalMultiObjectiveTest,
575 ModifyAuxiliaryObjectiveSenseInMultiObjectiveModel) {
576 if (!GetParam().supports_auxiliary_objectives) {
577 GTEST_SKIP() << kNoMultiObjectiveSupportMessage;
578 }
579 Model model;
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, /*priority=*/0);
586
588 const auto solver,
589 NewIncrementalSolver(&model, GetParam().solver_type, {}));
590 {
591 ASSERT_OK_AND_ASSIGN(const SolveResult result,
592 solver->Solve({.parameters = GetParam().parameters}));
593 ASSERT_THAT(result, IsOptimalWithSolution(4.5, {{x, 1.0}, {y, 0.5}}));
594 EXPECT_EQ(result.objective_value(o), 1.0);
595 }
596
597 model.set_minimize(o);
598
600 solver->Update(),
601 IsOkAndHolds(GetParam().supports_incremental_objective_modification
602 ? DidUpdate()
603 : Not(DidUpdate())));
605 const SolveResult result,
606 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}));
607 ASSERT_THAT(result, IsOptimalWithSolution(5.0, {{x, 0.0}, {y, 1.0}}));
608 EXPECT_EQ(result.objective_value(o), 0.0);
609}
610
611// TODO(b/214027410): Add these to IncrementalMultiObjectiveTest when needed:
612// * ModifyPrimaryObjectiveOffsetInMultiObjectiveModel
613// * ModifyAuxiliaryObjectiveOffsetInMultiObjectiveModel
614// * ModifyPrimaryObjectiveLinearCoefficientInMultiObjectiveModel
615// * ModifyAuxiliaryObjectiveLinearCoefficientInMultiObjectiveModel
616// * AfterUpdatePrimaryAndAuxiliaryObjectiveSharePriority
617// * AfterUpdateAuxiliaryObjectivesSharePriority
618
619} // namespace
620} // namespace operations_research::math_opt
IntegerValue y
#define ASSIGN_OR_RETURN(lhs, rexpr)
SatParameters parameters
GRBmodel * model
TerminationReason termination
double priority_0_objective_value
double solution
double priority_1_objective_value
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
constexpr absl::string_view kNoMultiObjectiveSupportMessage
Matcher< UpdateResult > DidUpdate()
Actual UpdateResult.did_update is true.
Definition matchers.cc:1027
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
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)