Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
callback_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 <limits>
17#include <memory>
18#include <optional>
19#include <ostream>
20#include <string>
21#include <utility>
22#include <vector>
23
24#include "absl/container/flat_hash_set.h"
25#include "absl/log/check.h"
26#include "absl/status/status.h"
27#include "absl/status/statusor.h"
28#include "absl/strings/escaping.h"
29#include "absl/strings/str_cat.h"
30#include "absl/strings/str_join.h"
31#include "absl/strings/string_view.h"
32#include "absl/synchronization/mutex.h"
33#include "absl/types/span.h"
34#include "gtest/gtest.h"
35#include "ortools/base/gmock.h"
40#include "ortools/math_opt/callback.pb.h"
47
48namespace operations_research {
49namespace math_opt {
50
52 const SolverType solver_type, const bool support_message_callback,
53 const bool support_interrupter, const bool integer_variables,
54 std::string ending_substring, SolveParameters solve_parameters)
55 : solver_type(solver_type),
56 support_message_callback(support_message_callback),
57 support_interrupter(support_interrupter),
58 integer_variables(integer_variables),
59 ending_substring(std::move(ending_substring)),
60 solve_parameters(std::move(solve_parameters)) {}
61
62std::ostream& operator<<(std::ostream& out,
63 const MessageCallbackTestParams& params) {
64 out << "{ solver_type: " << params.solver_type
65 << ", support_message_callback: " << params.support_message_callback
66 << ", support_interrupter: " << params.support_interrupter
67 << ", integer_variables: " << params.integer_variables
68 << ", ending_substring: \"" << absl::CEscape(params.ending_substring)
69 << "\", solve_parameters: "
71 return out;
72}
73
75 const SolverType solver_type, const bool integer_variables,
76 const bool add_lazy_constraints, const bool add_cuts,
77 absl::flat_hash_set<CallbackEvent> supported_events,
78 std::optional<SolveParameters> all_solutions,
79 std::optional<SolveParameters> reaches_cut_callback)
80 : solver_type(solver_type),
81 integer_variables(integer_variables),
82 add_lazy_constraints(add_lazy_constraints),
83 add_cuts(add_cuts),
84 supported_events(std::move(supported_events)),
85 all_solutions(std::move(all_solutions)),
86 reaches_cut_callback(std::move(reaches_cut_callback)) {}
87
88namespace {
89
90template <typename T>
91struct EnumFormatter {
92 void operator()(std::string* const out, const T value) {
93 out->append(std::string(EnumToString(value)));
94 }
95};
96
97absl::StatusOr<std::unique_ptr<Model>> LoadMiplibInstance(
98 const absl::string_view name) {
100 const ModelProto model_proto,
101 ReadMpsFile(absl::StrCat("ortools/math_opt/solver_tests/testdata/", name,
102 ".mps")));
103 return Model::FromModelProto(model_proto);
104}
105
106}; // namespace
107
108std::ostream& operator<<(std::ostream& out, const CallbackTestParams& params) {
109 out << "{ solver_type: " << params.solver_type
110 << ", integer_variables: " << params.integer_variables
111 << ", add_lazy_constraints: " << params.add_lazy_constraints
112 << ", add_cuts: " << params.add_cuts << ", supported_events: "
113 << absl::StrJoin(params.supported_events, ",",
114 EnumFormatter<CallbackEvent>())
115 << ", all_solutions: "
116 << (params.all_solutions.has_value()
117 ? ProtobufShortDebugString(params.all_solutions->Proto())
118 : "nullopt")
119 << ", reaches_cut_callback: "
120 << (params.reaches_cut_callback.has_value()
122 : "nullopt")
123 << " }";
124 return out;
125}
126
127namespace {
128
129using ::testing::_;
130using ::testing::AllOf;
131using ::testing::AnyOf;
132using ::testing::Each;
133using ::testing::HasSubstr;
134using ::testing::IsEmpty;
135using ::testing::Pair;
136using ::testing::UnorderedElementsAre;
137using ::testing::status::IsOkAndHolds;
138using ::testing::status::StatusIs;
139
140constexpr double kInf = std::numeric_limits<double>::infinity();
141constexpr double kTolerance = 1e-6;
142
143TEST_P(MessageCallbackTest, EmptyIfNotSupported) {
144 Model model("model");
145
146 absl::Mutex mutex;
147 std::vector<std::string> callback_messages;
148 const auto callback = [&](absl::Span<const std::string> messages) {
149 const absl::MutexLock lock(&mutex);
150 for (const auto& message : messages) {
151 callback_messages.push_back(message);
152 }
153 };
154
156 Solve(model, GetParam().solver_type, {.message_callback = callback}),
157 IsOkAndHolds(IsOptimal(0.0)));
158 if (!GetParam().support_message_callback) {
159 EXPECT_THAT(callback_messages, IsEmpty());
160 }
161}
162
163TEST_P(MessageCallbackTest, ObjectiveValueAndEndingSubstring) {
164 Model model("model");
165 const Variable x =
166 model.AddVariable(0, 21.0, GetParam().integer_variables, "x");
167 model.Maximize(2.0 * x);
168
169 absl::Mutex mutex;
170 std::vector<std::string> callback_messages;
171
172 SolveArguments args = {
173 .parameters = GetParam().solve_parameters,
174 .message_callback =
175 [&](absl::Span<const std::string> messages) {
176 const absl::MutexLock lock(&mutex);
177 for (const auto& message : messages) {
178 callback_messages.push_back(message);
179 }
180 },
181 };
182
183 // First test with enable_output being false.
184 args.parameters.enable_output = false;
185 {
186#ifdef OPERATIONS_RESEARCH_OUTPUT_CAPTURE_SUPPORTED
187 ScopedStdStreamCapture stdout_capture(CapturedStream::kStdout);
188#endif // OPERATIONS_RESEARCH_OUTPUT_CAPTURE_SUPPORTED
189 ASSERT_OK_AND_ASSIGN(const SolveResult result,
190 Solve(model, GetParam().solver_type, args));
191#ifdef OPERATIONS_RESEARCH_OUTPUT_CAPTURE_SUPPORTED
193 std::move(stdout_capture).StopCaptureAndReturnContents(),
195 /*is_gurobi=*/GetParam().solver_type == SolverType::kGurobi));
196#endif // OPERATIONS_RESEARCH_OUTPUT_CAPTURE_SUPPORTED
197 ASSERT_THAT(result, IsOptimal(42.0));
198 EXPECT_THAT(callback_messages, Each(Not(HasSubstr("\n"))));
199 if (GetParam().support_message_callback) {
200 EXPECT_THAT(absl::StrJoin(callback_messages, "\n"),
201 AllOf(AnyOf(HasSubstr("42"), HasSubstr("4.2")),
202 HasSubstr(GetParam().ending_substring)));
203 } else {
204 EXPECT_THAT(callback_messages, IsEmpty());
205 }
206 }
207
208 // Then test with enable_output being true.
209 callback_messages.clear();
210 args.parameters.enable_output = true;
211 {
212#ifdef OPERATIONS_RESEARCH_OUTPUT_CAPTURE_SUPPORTED
213 ScopedStdStreamCapture stdout_capture(CapturedStream::kStdout);
214#endif // OPERATIONS_RESEARCH_OUTPUT_CAPTURE_SUPPORTED
215 ASSERT_OK_AND_ASSIGN(const SolveResult result,
216 Solve(model, GetParam().solver_type, args));
217#ifdef OPERATIONS_RESEARCH_OUTPUT_CAPTURE_SUPPORTED
219 std::move(stdout_capture).StopCaptureAndReturnContents(),
221 /*is_gurobi=*/GetParam().solver_type == SolverType::kGurobi));
222#endif // OPERATIONS_RESEARCH_OUTPUT_CAPTURE_SUPPORTED
223 ASSERT_THAT(result, IsOptimal(42.0));
224 EXPECT_THAT(callback_messages, Each(Not(HasSubstr("\n"))));
225 if (GetParam().support_message_callback) {
226 EXPECT_THAT(absl::StrJoin(callback_messages, "\n"),
227 AllOf(AnyOf(HasSubstr("42"), HasSubstr("4.2")),
228 HasSubstr(GetParam().ending_substring)));
229 } else {
230 EXPECT_THAT(callback_messages, IsEmpty());
231 }
232 }
233
234 // Then test without callback and with enable_output being true.
235 args.parameters.enable_output = true;
236 args.message_callback = nullptr;
237 callback_messages.clear();
238
239#ifdef OPERATIONS_RESEARCH_OUTPUT_CAPTURE_SUPPORTED
240 ScopedStdStreamCapture stdout_capture(CapturedStream::kStdout);
241#endif // OPERATIONS_RESEARCH_OUTPUT_CAPTURE_SUPPORTED
242 ASSERT_OK_AND_ASSIGN(const SolveResult result,
243 Solve(model, GetParam().solver_type, args));
244#ifdef OPERATIONS_RESEARCH_OUTPUT_CAPTURE_SUPPORTED
245 EXPECT_THAT(std::move(stdout_capture).StopCaptureAndReturnContents(),
246 AllOf(AnyOf(HasSubstr("42"), HasSubstr("4.2")),
247 HasSubstr(GetParam().ending_substring)));
248#endif // OPERATIONS_RESEARCH_OUTPUT_CAPTURE_SUPPORTED
249 ASSERT_THAT(result, IsOptimal(42.0));
250 EXPECT_THAT(callback_messages, IsEmpty());
251}
252
253TEST_P(MessageCallbackTest, InterruptAtFirstMessage) {
254 if (!GetParam().support_message_callback) {
255 GTEST_SKIP() << "Message callback not supported. Ignoring this test.";
256 }
257 if (!GetParam().support_interrupter) {
258 GTEST_SKIP() << "Solve interrupter not supported. Ignoring this test.";
259 }
260 const std::unique_ptr<const Model> model =
261 SmallModel(GetParam().integer_variables);
262
263 absl::Mutex mutex;
264 std::vector<std::string> callback_messages; // Guarded by mutex.
265 bool first = true; // Guarded by mutex.
266
267 SolveArguments args;
268 SolveInterrupter interrupter;
269 args.interrupter = &interrupter;
270 args.message_callback = [&](absl::Span<const std::string> messages) {
271 const absl::MutexLock lock(&mutex);
272 for (const auto& message : messages) {
273 callback_messages.push_back(message);
274 }
275 if (first) {
276 first = false;
277 interrupter.Interrupt();
278 }
279 };
280 ASSERT_OK_AND_ASSIGN(const SolveResult result,
281 Solve(*model, GetParam().solver_type, args));
283 // We should have stopped before reaching the end.
284 EXPECT_THAT(absl::StrJoin(callback_messages, "\n"),
285 Not(HasSubstr(GetParam().ending_substring)));
286}
287
288// Builds a trivial model that can be solved in presolve, checks that the
289// presolve stats show all variables and constraints are deleted.
290TEST_P(CallbackTest, EventPresolve) {
291 if (!GetParam().supported_events.contains(CallbackEvent::kPresolve)) {
292 GTEST_SKIP()
293 << "Test skipped because this solver does not support this event.";
294 }
295
296 Model model("model");
297 Variable x = model.AddVariable(0, 2.0, GetParam().integer_variables, "x");
298 Variable y = model.AddVariable(0, 3.0, GetParam().integer_variables, "y");
299 model.AddLinearConstraint(y <= 1.0);
300 model.Maximize(2.0 * x + y);
301 SolveArguments args = {
302 .callback_registration = {.events = {CallbackEvent::kPresolve}}};
303 absl::Mutex mutex;
304 std::optional<CallbackData> last_presolve_data; // Guarded by mutex.
305 args.callback = [&](const CallbackData& callback_data) {
306 const absl::MutexLock lock(&mutex);
307 last_presolve_data = callback_data;
308 return CallbackResult();
309 };
310
311 ASSERT_OK_AND_ASSIGN(const SolveResult result,
312 Solve(model, GetParam().solver_type, args));
313 ASSERT_THAT(result, IsOptimal(5.0));
314 ASSERT_TRUE(last_presolve_data.has_value());
315 EXPECT_EQ(last_presolve_data->presolve_stats.removed_variables(), 2);
316 EXPECT_EQ(last_presolve_data->presolve_stats.removed_constraints(), 1);
317}
318
319TEST_P(CallbackTest, EventSimplex) {
320 if (!GetParam().supported_events.contains(CallbackEvent::kSimplex)) {
321 GTEST_SKIP()
322 << "Test skipped because this solver does not support this event.";
323 }
324
325 Model model("model");
326 Variable x1 = model.AddVariable(0, 2.0, false, "x1");
327 Variable x2 = model.AddVariable(0, 3.0, false, "x2");
328 Variable x3 = model.AddVariable(0, 4.0, false, "x3");
329 model.Maximize(x1 - x2 + x3);
330
331 SolveArguments args;
332 args.parameters.presolve = Emphasis::kOff;
333 args.parameters.lp_algorithm = LPAlgorithm::kPrimalSimplex;
334 // Note: we solve and then change the objective to so that on our second
335 // solve, we know the starting basis. It would be simpler to set the starting
336 // basis, once this is supported.
337 ASSERT_OK_AND_ASSIGN(const std::unique_ptr<IncrementalSolver> solver,
338 NewIncrementalSolver(&model, GetParam().solver_type));
339 {
340 ASSERT_OK_AND_ASSIGN(const SolveResult result, solver->Solve(args));
341 ASSERT_THAT(result, IsOptimal(6.0));
342 }
343
344 // We know that from the previous optimal solution, we should take 3 pivots.
345 model.Maximize(-x1 + x2 - x3);
346
347 args.callback_registration.events.insert(CallbackEvent::kSimplex);
348 absl::Mutex mutex;
349 std::vector<CallbackDataProto::SimplexStats> stats; // Guarded-by mutex.
350 args.callback = [&](const CallbackData& callback_data) {
351 const absl::MutexLock lock(&mutex);
352 stats.push_back(callback_data.simplex_stats);
353 return CallbackResult();
354 };
355 ASSERT_OK_AND_ASSIGN(const SolveResult result, solver->Solve(args));
356 ASSERT_THAT(result, IsOptimal(3.0));
357 // It should take at least 3 pivots to move from (2, 0, 4) to (0, 3, 0)
358 ASSERT_GE(stats.size(), 3);
359 for (const CallbackDataProto::SimplexStats& s : stats) {
360 // Because we are using primal simplex and start with a feasible solution,
361 // it should always be feasible
362 EXPECT_NEAR(s.primal_infeasibility(), 0, kTolerance);
363 }
364 // We should begin dual infeasible.
365 EXPECT_EQ(stats[0].iteration_count(), 0);
366 EXPECT_GT(stats[0].dual_infeasibility(), 0.0);
367 EXPECT_NEAR(stats[0].objective_value(), -6.0, kTolerance);
368
369 EXPECT_GE(stats.back().iteration_count(), 3);
370 // The objective value is not updating in the callback, investigate.
371}
372
373TEST_P(CallbackTest, EventBarrier) {
374 if (!GetParam().supported_events.contains(CallbackEvent::kBarrier)) {
375 GTEST_SKIP()
376 << "Test skipped because this solver does not support this event.";
377 }
378
379 // Make a model that requires multiple barrier steps to solve.
380 const std::unique_ptr<const Model> model =
381 SmallModel(GetParam().integer_variables);
382
383 SolveArguments args;
384 args.parameters.presolve = Emphasis::kOff;
385 args.parameters.lp_algorithm = LPAlgorithm::kBarrier;
386 args.callback_registration.events.insert(CallbackEvent::kBarrier);
387 absl::Mutex mutex;
388 std::vector<CallbackDataProto::BarrierStats> stats; // Guarded-by mutex.
389 args.callback = [&](const CallbackData& callback_data) {
390 const absl::MutexLock lock(&mutex);
391 stats.push_back(callback_data.barrier_stats);
392 return CallbackResult();
393 };
394 ASSERT_OK_AND_ASSIGN(const SolveResult result,
395 Solve(*model, GetParam().solver_type, args));
396 ASSERT_THAT(result, IsOptimal(12.0));
397
398 // TODO(b/196035470): test more data from the stats.
399 ASSERT_GE(stats.size(), 1);
400 EXPECT_GE(stats.back().iteration_count(), 3);
401}
402
403TEST_P(CallbackTest, EventSolutionAlwaysCalled) {
404 if (!GetParam().supported_events.contains(CallbackEvent::kMipSolution)) {
405 GTEST_SKIP() << "Test skipped because this solver does not support "
406 "CallbackEvent::kMipSolution.";
407 }
408
409 // This test must use integer variables.
410 ASSERT_TRUE(GetParam().integer_variables);
411
412 Model model("model");
413 const Variable x = model.AddBinaryVariable("x");
414 const Variable y = model.AddBinaryVariable("y");
415 model.AddLinearConstraint(x + y <= 1);
416 model.Maximize(x + 2 * y);
417
418 SolveArguments args = {
419 .callback_registration = {.events = {CallbackEvent::kMipSolution}}};
420 absl::Mutex mutex;
421 bool cb_called = false;
422 bool cb_called_on_optimal = false;
423 args.callback = [&](const CallbackData& callback_data) {
424 const absl::MutexLock lock(&mutex);
425 cb_called = true;
426 EXPECT_EQ(callback_data.event, CallbackEvent::kMipSolution);
427 if (!callback_data.solution.has_value()) {
428 ADD_FAILURE() << "callback_data.solution should always be set at event "
429 "MIP_SOLUTION but was empty";
430 return CallbackResult();
431 }
432 const VariableMap<double>& sol = *callback_data.solution;
434 sol, AnyOf(IsNear({{x, 0.0}, {y, 0.0}}), IsNear({{x, 1.0}, {y, 0.0}}),
435 IsNear({{x, 0.0}, {y, 1.0}})));
436 if (gtl::FindWithDefault(sol, y) > 0.5) {
437 cb_called_on_optimal = true;
438 }
439 return CallbackResult();
440 };
441 EXPECT_THAT(Solve(model, GetParam().solver_type, args),
442 IsOkAndHolds(IsOptimal(2.0)));
443 EXPECT_TRUE(cb_called);
444 EXPECT_TRUE(cb_called_on_optimal);
445}
446
447TEST_P(CallbackTest, EventSolutionInterrupt) {
448 if (!GetParam().supported_events.contains(CallbackEvent::kMipSolution)) {
449 GTEST_SKIP() << "Test skipped because this solver does not support "
450 "CallbackEvent::kMipSolution.";
451 }
452
453 // This test must use integer variables.
454 ASSERT_TRUE(GetParam().integer_variables);
455
456 // A model where we will not prove optimality immediately.
457 const std::unique_ptr<const Model> model =
458 DenseIndependentSet(/*integer=*/true);
459 const SolveArguments args = {
460 // Don't prove optimality in presolve.
461 .parameters = {.presolve = Emphasis::kOff},
462 .callback_registration = {.events = {CallbackEvent::kMipSolution}},
463 .callback = [&](const CallbackData& /*callback_data*/) {
464 return CallbackResult{.terminate = true};
465 }};
466 ASSERT_OK_AND_ASSIGN(const SolveResult result,
467 Solve(*model, GetParam().solver_type, args));
469 EXPECT_TRUE(result.has_primal_feasible_solution());
470}
471
472TEST_P(CallbackTest, EventSolutionCalledMoreThanOnce) {
473 if (!GetParam().supported_events.contains(CallbackEvent::kMipSolution)) {
474 GTEST_SKIP() << "Test skipped because this solver does not support "
475 "CallbackEvent::kMipSolution.";
476 }
477 if (!GetParam().all_solutions.has_value()) {
478 GTEST_SKIP() << "Test skipped because this solver does not support "
479 "getting all solutions.";
480 }
481
482 // This test must use integer variables.
483 ASSERT_TRUE(GetParam().integer_variables);
484
485 Model model("model");
486 const Variable x = model.AddBinaryVariable("x");
487 const Variable y = model.AddBinaryVariable("y");
488 const Variable z = model.AddBinaryVariable("z");
489 model.AddLinearConstraint(x + y + z <= 1);
490
491 SolveArguments args = {
492 .parameters = *GetParam().all_solutions,
493 .callback_registration = {.events = {CallbackEvent::kMipSolution}},
494 };
495 absl::Mutex mutex;
496 bool cb_called_on_zero = false;
497 bool cb_called_on_x = false;
498 bool cb_called_on_y = false;
499 bool cb_called_on_z = false;
500 args.callback = [&](const CallbackData& callback_data) {
501 const absl::MutexLock lock(&mutex);
502 EXPECT_EQ(callback_data.event, CallbackEvent::kMipSolution);
503 if (!callback_data.solution.has_value()) {
504 ADD_FAILURE() << "callback_data.solution should always be set at event "
505 "MIP_SOLUTION but was empty";
506 return CallbackResult();
507 }
508 const VariableMap<double>& sol = *callback_data.solution;
509 EXPECT_THAT(sol, AnyOf(IsNear({{x, 0.0}, {y, 0.0}, {z, 0.0}}),
510 IsNear({{x, 1.0}, {y, 0.0}, {z, 0.0}}),
511 IsNear({{x, 0.0}, {y, 1.0}, {z, 0.0}}),
512 IsNear({{x, 0.0}, {y, 0.0}, {z, 1.0}})));
513 if (gtl::FindWithDefault(sol, x) > 0.5) {
514 cb_called_on_x = true;
515 } else if (gtl::FindWithDefault(sol, y) > 0.5) {
516 cb_called_on_y = true;
517 } else if (gtl::FindWithDefault(sol, z) > 0.5) {
518 cb_called_on_z = true;
519 } else {
520 cb_called_on_zero = true;
521 }
522 return CallbackResult();
523 };
524 EXPECT_THAT(Solve(model, GetParam().solver_type, args),
526 EXPECT_TRUE(cb_called_on_zero);
527 EXPECT_TRUE(cb_called_on_x);
528 EXPECT_TRUE(cb_called_on_y);
529 EXPECT_TRUE(cb_called_on_z);
530}
531
532TEST_P(CallbackTest, EventSolutionLazyConstraint) {
533 if (!GetParam().supported_events.contains(CallbackEvent::kMipSolution)) {
534 GTEST_SKIP() << "Test skipped because this solver does not support "
535 "CallbackEvent::kMipSolution.";
536 }
537 if (!GetParam().add_lazy_constraints) {
538 GTEST_SKIP() << "Test skipped because this solver does not support adding "
539 "lazy constraints.";
540 }
541
542 // This test must use integer variables.
543 ASSERT_TRUE(GetParam().integer_variables);
544
545 Model model("model");
546 const Variable x = model.AddBinaryVariable("x");
547 const Variable y = model.AddBinaryVariable("y");
548 model.Maximize(x + 2 * y);
549
550 SolveArguments args = {
551 .callback_registration = {.events = {CallbackEvent::kMipSolution},
552 .add_lazy_constraints = true}};
553 // Add the constraint x+y <= 1 if it is violated by the current solution.
554 args.callback = [&](const CallbackData& callback_data) -> CallbackResult {
555 if (!callback_data.solution.has_value()) {
556 ADD_FAILURE() << "callback_data.solution should always be set at event "
557 "MIP_SOLUTION but was empty";
558 return {};
559 }
560 const VariableMap<double>& sol = *callback_data.solution;
561 if (sol.size() != 2) {
562 ADD_FAILURE()
563 << "callback_data.solution should have two elements but found: "
564 << sol.size();
565 return {};
566 }
567 const double x_val = sol.at(x);
568 const double y_val = sol.at(y);
569 CallbackResult result;
570 if (x_val + y_val >= 1.0 + 1e-5) {
571 result.AddLazyConstraint(x + y <= 1.0);
572 }
573 return result;
574 };
575 ASSERT_OK_AND_ASSIGN(const SolveResult result,
576 Solve(model, GetParam().solver_type, args));
577 ASSERT_THAT(result, IsOptimal(2.0));
578}
579
580TEST_P(CallbackTest, EventSolutionLazyConstraintWithLinearConstraints) {
581 if (!GetParam().supported_events.contains(CallbackEvent::kMipSolution)) {
582 GTEST_SKIP() << "Test skipped because this solver does not support "
583 "CallbackEvent::kMipSolution.";
584 }
585 if (!GetParam().add_lazy_constraints) {
586 GTEST_SKIP() << "Test skipped because this solver does not support adding "
587 "lazy constraints.";
588 }
589
590 // This test must use integer variables.
591 ASSERT_TRUE(GetParam().integer_variables);
592
593 Model model("model");
594 const Variable x = model.AddBinaryVariable("x");
595 const Variable y = model.AddBinaryVariable("y");
596 const Variable z = model.AddBinaryVariable("z");
597 model.Maximize(x + 2 * y - z);
598 model.AddLinearConstraint(x + y + z >= 1.0);
599
600 SolveArguments args = {
601 .callback_registration = {.events = {CallbackEvent::kMipSolution},
602 .add_lazy_constraints = true}};
603 // Add the constraint x+y <= 1 if it is violated by the current solution.
604 args.callback = [&](const CallbackData& callback_data) -> CallbackResult {
605 if (!callback_data.solution.has_value()) {
606 ADD_FAILURE() << "callback_data.solution should always be set at event "
607 "MIP_SOLUTION but was empty";
608 return {};
609 }
610 const VariableMap<double>& sol = *callback_data.solution;
611 if (sol.size() != 3) {
612 ADD_FAILURE()
613 << "callback_data.solution should have two elements but found: "
614 << sol.size();
615 return {};
616 }
617 const double x_val = sol.at(x);
618 const double y_val = sol.at(y);
619 CallbackResult result;
620 if (x_val + y_val >= 1.0 + 1e-5) {
621 result.AddLazyConstraint(x + y <= 1.0);
622 }
623 return result;
624 };
625 ASSERT_OK_AND_ASSIGN(const SolveResult result,
626 Solve(model, GetParam().solver_type, args));
627 ASSERT_THAT(result, IsOptimal(2.0));
628}
629
630TEST_P(CallbackTest, EventSolutionFilter) {
631 if (!GetParam().supported_events.contains(CallbackEvent::kMipSolution)) {
632 GTEST_SKIP() << "Test skipped because this solver does not support "
633 "CallbackEvent::kMipSolution.";
634 }
635
636 // This test must use integer variables.
637 ASSERT_TRUE(GetParam().integer_variables);
638
639 Model model("model");
640 const Variable x = model.AddBinaryVariable("x");
641 const Variable y = model.AddBinaryVariable("y");
642 model.AddLinearConstraint(x + y <= 1);
643 model.Maximize(x + 2 * y);
644
645 SolveArguments args = {.callback_registration = {
646 .events = {CallbackEvent::kMipSolution},
647 .mip_solution_filter = MakeKeepKeysFilter({y})}};
648 absl::Mutex mutex;
649 bool cb_called = false;
650 bool cb_called_on_optimal = false;
651 args.callback = [&](const CallbackData& callback_data) {
652 const absl::MutexLock lock(&mutex);
653 cb_called = true;
654 EXPECT_EQ(callback_data.event, CallbackEvent::kMipSolution);
655 if (!callback_data.solution.has_value()) {
656 ADD_FAILURE() << "callback_data.solution should always be set at event "
657 "MIP_SOLUTION but was empty";
658 return CallbackResult();
659 }
660 const VariableMap<double>& sol = *callback_data.solution;
661 EXPECT_THAT(sol, AnyOf(IsNear({{y, 0.0}}), IsNear({{y, 1.0}})));
662 if (gtl::FindWithDefault(sol, y) > 0.5) {
663 cb_called_on_optimal = true;
664 }
665 return CallbackResult();
666 };
667 EXPECT_THAT(Solve(model, GetParam().solver_type, args),
668 IsOkAndHolds(IsOptimal(2.0)));
669 EXPECT_TRUE(cb_called);
670 EXPECT_TRUE(cb_called_on_optimal);
671}
672
673TEST_P(CallbackTest, EventNodeCut) {
674 if (!GetParam().supported_events.contains(CallbackEvent::kMipNode)) {
675 GTEST_SKIP() << "Test skipped because this solver does not support "
676 "CallbackEvent::kMipNode.";
677 }
678 if (!GetParam().add_cuts) {
679 GTEST_SKIP() << "Test skipped because this solver does not support adding "
680 "cuts.";
681 }
682 if (!GetParam().reaches_cut_callback.has_value()) {
683 GTEST_SKIP() << "Test skipped, needs reaches_cut_callback to be set.";
684 }
685
686 // This test must use integer variables.
687 ASSERT_TRUE(GetParam().integer_variables);
688
689 // Max sum_i x_i
690 // s.t. x_i + x_j + x_k <= 2 for all i < j < k
691 // x_i binary for all i
692 //
693 // Optimal objective is 2, where any two x_i = 1 and the rest are zero.
694 //
695 // Strengthened by the cut:
696 // sum_i x_i <= 2
697 //
698 // This is basically a clique cut. Note that if we try to use a simpler form
699 // of the problem, where x_i + x_j <= 1 for all i, j, with an optimal
700 // objective of one, then the branching rule in SCIP can do domain reductions
701 // and solve the problem at the root node.
702 Model model("model");
703 constexpr int n = 10;
704 std::vector<Variable> x;
705 for (int i = 0; i < n; ++i) {
706 x.push_back(model.AddBinaryVariable(absl::StrCat("x", i)));
707 }
708 for (int i = 0; i < n; ++i) {
709 for (int j = i + 1; j < n; ++j) {
710 for (int k = j + 1; k < n; ++k) {
711 model.AddLinearConstraint(x[i] + x[j] + x[k] <= 2.0);
712 }
713 }
714 }
715 model.Maximize(Sum(x));
716
717 for (const bool use_cut : {false, true}) {
718 SCOPED_TRACE(absl::StrCat("use_cut:", use_cut));
719 SolveArguments args = {.parameters =
720 GetParam().reaches_cut_callback.value()};
721 args.parameters.node_limit = 1;
722 if (use_cut) {
723 args.callback_registration = {.events = {CallbackEvent::kMipNode},
724 .add_cuts = true};
725
726 args.callback = [&](const CallbackData& callback_data) -> CallbackResult {
727 if (!callback_data.solution.has_value()) {
728 return {};
729 }
730 const VariableMap<double>& sol = *callback_data.solution;
731 CallbackResult result;
732 if (Sum(x).Evaluate(sol) > 2.0 + 1.0e-5) {
733 result.AddUserCut(Sum(x) <= 2.0);
734 }
735 return result;
736 };
737 }
738 ASSERT_OK_AND_ASSIGN(const SolveResult result,
739 Solve(model, GetParam().solver_type, args));
740 // Even with use_cut: False, SCIP v900 return OPTIMAL
741 if (GetParam().solver_type == SolverType::kGscip) {
742 EXPECT_THAT(result, IsOptimal(2.0));
743 } else {
744 if (use_cut) {
745 EXPECT_THAT(result, IsOptimal(2.0));
746 } else {
747 EXPECT_THAT(result.termination, LimitIs(Limit::kNode));
748 }
749 }
750 }
751}
752
753TEST_P(CallbackTest, EventNodeFilter) {
754 if (!GetParam().supported_events.contains(CallbackEvent::kMipNode)) {
755 GTEST_SKIP() << "Test skipped because this solver does not support "
756 "CallbackEvent::kMipNode.";
757 }
758
759 // This test must use integer variables.
760 ASSERT_TRUE(GetParam().integer_variables);
761 // Use the MIPLIB instance 23588, which has optimal solution 8090 and LP
762 // relaxation of 7649.87. This instance was selected because every
763 // supported solver can solve it quickly (a few seconds), but no solver can
764 // solve it in one node (so the node callback will be invoked).
765 ASSERT_OK_AND_ASSIGN(const std::unique_ptr<Model> model,
766 LoadMiplibInstance("23588"));
767 const std::vector<Variable> variables = model->SortedVariables();
768 CHECK_GE(variables.size(), 3);
769 const Variable x0 = variables[0];
770 const Variable x2 = variables[2];
771
772 SolveArguments args = {.callback_registration = {
773 .events = {CallbackEvent::kMipNode},
774 .mip_node_filter = MakeKeepKeysFilter({x0, x2})}};
775 absl::Mutex mutex;
776 std::vector<VariableMap<double>> solutions;
777 int empty_solution_count = 0;
778 args.callback = [&](const CallbackData& callback_data) {
779 EXPECT_EQ(callback_data.event, CallbackEvent::kMipNode);
780 const absl::MutexLock lock(&mutex);
781 if (!callback_data.solution.has_value()) {
782 empty_solution_count++;
783 } else {
784 solutions.push_back(callback_data.solution.value());
785 }
786 return CallbackResult();
787 };
788 EXPECT_THAT(Solve(*model, GetParam().solver_type, args),
789 IsOkAndHolds(IsOptimal(8090)));
790 LOG(INFO) << "callback_data.solution was not set " << empty_solution_count
791 << " times";
792 EXPECT_THAT(solutions, Each(UnorderedElementsAre(Pair(x0, _), Pair(x2, _))));
793}
794
795TEST_P(CallbackTest, StatusPropagation) {
796 if (!GetParam().supported_events.contains(CallbackEvent::kMipSolution)) {
797 GTEST_SKIP() << "Test skipped because this solver does not support "
798 "CallbackEvent::kMipSolution.";
799 }
800 if (!GetParam().add_lazy_constraints) {
801 GTEST_SKIP() << "Test skipped because this solver does not support adding "
802 "lazy constraints.";
803 }
804
805 // This test must use integer variables.
806 ASSERT_TRUE(GetParam().integer_variables);
807
808 // Check status propagation by adding an invalid cut.
809 Model model("model");
810 const Variable x = model.AddBinaryVariable("x");
811 const Variable y = model.AddBinaryVariable("y");
812 model.Maximize(x + 2 * y);
813
814 SolveArguments args = {
815 .callback_registration = {.events = {CallbackEvent::kMipSolution},
816 .add_lazy_constraints = true}};
817 absl::Mutex mutex;
818 bool added_cut = false; // Guarded by mutex.
819 args.callback = [&](const CallbackData& /*callback_data*/) {
820 const absl::MutexLock lock(&mutex);
821 CallbackResult result;
822 if (!added_cut) {
823 result.AddLazyConstraint(x + y <= -kInf);
824 added_cut = true;
825 }
826 return result;
827 };
828 EXPECT_THAT(Solve(model, GetParam().solver_type, args),
829 StatusIs(absl::StatusCode::kInvalidArgument,
830 HasSubstr("Invalid negative infinite value; for "
831 "GeneratedLinearConstraint.upper_bound")));
832}
833
834TEST_P(CallbackTest, UnsupportedEvents) {
835 Model model("model");
836 model.AddVariable(0, 1.0, GetParam().integer_variables, "x");
837
838 for (const CallbackEvent event : Enum<CallbackEvent>::AllValues()) {
839 if (GetParam().supported_events.contains(event)) {
840 continue;
841 }
842 SCOPED_TRACE(absl::StrCat("event: ", EnumToString(event)));
843
844 const SolveArguments args = {
845 .callback_registration = {.events = {event}},
846 .callback = [](const CallbackData&) { return CallbackResult{}; }};
847
848 EXPECT_THAT(Solve(model, GetParam().solver_type, args),
849 StatusIs(absl::StatusCode::kInvalidArgument,
850 HasSubstr(ProtoEnumToString(EnumToProto(event)))));
851 }
852}
853
854} // namespace
855} // namespace math_opt
856} // namespace operations_research
IntegerValue y
#define ASSIGN_OR_RETURN(lhs, rexpr)
static absl::StatusOr< std::unique_ptr< Model > > FromModelProto(const ModelProto &model_proto)
Definition model.cc:53
const std::string name
A name for logging purposes.
int64_t value
GRBmodel * model
MPCallback * callback
const Variable x2
const Variable x1
const MapUtilMappedT< Collection > & FindWithDefault(const Collection &collection, const KeyType &key, const MapUtilMappedT< Collection > &value)
Definition map_util.h:36
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::flat_hash_map< Variable, V > VariableMap
SolverType
The solvers supported by MathOpt.
Definition parameters.h:42
<=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()))
LinearExpression Sum(const Iterable &items)
absl::StatusOr< SolveResult > Solve(const Model &model, const SolverType solver_type, const SolveArguments &solve_args, const SolverInitArguments &init_args)
Definition solve.cc:62
Matcher< Termination > LimitIs(math_opt::Limit limit, const Matcher< std::string > detail_matcher)
Definition matchers.cc:709
std::ostream & operator<<(std::ostream &ostr, const IndicatorConstraint &constraint)
std::unique_ptr< Model > SmallModel(const bool integer)
absl::StatusOr< std::unique_ptr< IncrementalSolver > > NewIncrementalSolver(Model *model, SolverType solver_type, SolverInitArguments arguments)
Definition solve.cc:82
testing::Matcher< SolveResult > TerminatesWithReasonFeasible(const Limit expected, const bool allow_limit_undetermined)
Definition matchers.cc:657
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
CallbackEvent
The supported events during a solve for callbacks.
Definition callback.h:96
Matcher< VariableMap< double > > IsNear(VariableMap< double > expected, const double tolerance)
Definition matchers.cc:221
MapFilter< ValueType > MakeKeepKeysFilter(const Collection &keys)
Definition map_filter.h:167
Enum< E >::Proto EnumToProto(std::optional< E > value)
Definition enums.h:270
std::unique_ptr< Model > DenseIndependentSet(const bool integer, const int n)
absl::string_view EnumToString(E value)
Definition enums.h:289
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
In SWIG mode, we don't want anything besides these top-level includes.
std::string ProtoEnumToString(ProtoEnumType enum_value)
Definition proto_utils.h:50
std::string ProtobufShortDebugString(const P &message)
Definition proto_utils.h:41
testing::Matcher< std::string > EmptyOrGurobiLicenseWarningIfGurobi(const bool is_gurobi)
STL namespace.
internal::StatusIsMatcher StatusIs(CodeMatcher code_matcher, MessageMatcher message_matcher)
#define ASSERT_OK_AND_ASSIGN(lhs, rexpr)
std::optional< SolveParameters > all_solutions
CallbackTestParams(SolverType solver_type, bool integer_variables, bool add_lazy_constraints, bool add_cuts, absl::flat_hash_set< CallbackEvent > supported_events, std::optional< SolveParameters > all_solutions, std::optional< SolveParameters > reaches_cut_callback)
absl::flat_hash_set< CallbackEvent > supported_events
The events that should be supported by the solver.
bool add_lazy_constraints
If the solver supports adding lazy constraints at the MIP_SOLUTION event.
bool integer_variables
True if the tests should be performed with integer variables.
bool add_cuts
If the solver supports adding cuts at the event MIP_NODE.
std::optional< SolveParameters > reaches_cut_callback
static absl::Span< const E > AllValues()
Returns all possible values of the enum.
Parameters for the MessageCallbackTest suite below.
SolveParameters solve_parameters
Additional parameters to control the solve.
bool support_interrupter
True if the solver supports SolveInterrupter.
std::string ending_substring
A sub-string expected to be found on the last log lines.
MessageCallbackTestParams(SolverType solver_type, bool support_message_callback, bool support_interrupter, bool integer_variables, std::string ending_substring, SolveParameters solve_parameters={})
bool integer_variables
True if the tests should be performed with integer variables.
std::string message
Definition trace.cc:397
double objective_value
The value objective_vector^T * (solution - center_point).