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