Google OR-Tools v9.14
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
generic_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 <cstdint>
17#include <limits>
18#include <memory>
19#include <optional>
20#include <ostream>
21#include <sstream>
22#include <string>
23#include <tuple>
24#include <utility>
25#include <vector>
26
27#include "absl/container/flat_hash_set.h"
28#include "absl/log/log.h"
29#include "absl/status/statusor.h"
30#include "absl/strings/escaping.h"
31#include "absl/strings/str_cat.h"
32#include "absl/synchronization/mutex.h"
33#include "absl/time/clock.h"
34#include "absl/time/time.h"
35#include "gtest/gtest.h"
36#include "ortools/base/gmock.h"
45
47
48std::ostream& operator<<(std::ostream& out,
49 const TimeLimitTestParameters& params) {
50 out << "{ solver_type: " << params.solver_type
51 << ", integer_variables: " << params.integer_variables
52 << ", callback_event: " << params.event << " }";
53 return out;
54}
55
66
67std::ostream& operator<<(std::ostream& out,
68 const GenericTestParameters& params) {
69 out << "{ solver_type: " << params.solver_type
70 << ", support_interrupter: " << params.support_interrupter
71 << ", integer_variables: " << params.integer_variables
72 << ", expected_log: \"" << absl::CEscape(params.expected_log) << "\""
73 << ", solve_parameters: "
75 return out;
76}
77
78namespace {
79
80using ::testing::HasSubstr;
82
83constexpr double kInf = std::numeric_limits<double>::infinity();
84
85TEST_P(GenericTest, EmptyModel) {
86 Model model;
87 EXPECT_THAT(SimpleSolve(model), IsOkAndHolds(IsOptimal(0.0)));
88}
89
90TEST_P(GenericTest, OffsetOnlyMinimization) {
91 Model model;
92 model.Minimize(4.0);
93 EXPECT_THAT(SimpleSolve(model), IsOkAndHolds(IsOptimal(4.0)));
94}
95
96TEST_P(GenericTest, OffsetOnlyMaximization) {
97 Model model;
98 model.Maximize(4.0);
99 EXPECT_THAT(SimpleSolve(model), IsOkAndHolds(IsOptimal(4.0)));
100}
101
102TEST_P(GenericTest, OffsetMinimization) {
103 Model model;
104 const Variable x =
105 model.AddVariable(-1.0, 2.0, GetParam().integer_variables, "x");
106 model.Minimize(2 * x + 4);
107 EXPECT_THAT(SimpleSolve(model), IsOkAndHolds(IsOptimal(2.0)));
108}
109
110TEST_P(GenericTest, OffsetMaximization) {
111 Model model;
112 const Variable x =
113 model.AddVariable(-1.0, 2.0, GetParam().integer_variables, "x");
114 model.Maximize(2 * x + 4);
115 EXPECT_THAT(SimpleSolve(model), IsOkAndHolds(IsOptimal(8.0)));
116}
117
118TEST_P(GenericTest, SolveTime) {
119 // We use a non-trivial problem since on WASM the time resolution is of 1ms
120 // and thus a trivial model could be solved in absl::ZeroDuration().
121 //
122 // We also don't use a constant complexity. The reason is that the solve time
123 // depends on the build flags and the solve algorithm used by the solver (and
124 // the solver itself). And using a unique constant can lead to too short or
125 // too long solve times. Here we just want to make sure that we have a long
126 // enough solve time so that it is not too close to zero.
127 constexpr int kMinN = 10;
128 constexpr int kMaxN = 30;
129 constexpr int kIncrementN = 5;
130 constexpr absl::Duration kMinSolveTime = absl::Milliseconds(5);
131 for (int n = kMinN; n <= kMaxN; n += kIncrementN) {
132 SCOPED_TRACE(absl::StrCat("n = ", n));
133 const std::unique_ptr<const Model> model =
134 DenseIndependentSet(GetParam().integer_variables, /*n=*/n);
135
136 const absl::Time start = absl::Now();
137 ASSERT_OK_AND_ASSIGN(const SolveResult result, SimpleSolve(*model));
138 const absl::Duration expected_max_solve_time = absl::Now() - start;
139
140 if (expected_max_solve_time <= kMinSolveTime && n < kMaxN) {
141 LOG(INFO) << "The solve ended too quickly (" << expected_max_solve_time
142 << ") with n=" << n << "; retrying with a more complex model.";
143 continue;
144 }
145 EXPECT_GT(result.solve_stats.solve_time, absl::ZeroDuration());
146 EXPECT_LE(result.solve_stats.solve_time, expected_max_solve_time);
147
148 break;
149 }
150}
151
152TEST_P(GenericTest, InterruptBeforeSolve) {
153 if (!GetParam().support_interrupter) {
154 GTEST_SKIP() << "Solve interrupter not supported. Ignoring this test.";
155 }
156
157 const std::unique_ptr<Model> model = SmallModel(GetParam().integer_variables);
158
159 SolveInterrupter interrupter;
160 interrupter.Interrupt();
161
162 SolveArguments args;
163 args.parameters = GetParam().solve_parameters;
164 args.interrupter = &interrupter;
165
166 ASSERT_OK_AND_ASSIGN(const SolveResult result,
167 Solve(*model, GetParam().solver_type, args));
169}
170
171TEST_P(GenericTest, InterruptAfterSolve) {
172 if (!GetParam().support_interrupter) {
173 GTEST_SKIP() << "Solve interrupter not supported. Ignoring this test.";
174 }
175
176 const std::unique_ptr<Model> model = SmallModel(GetParam().integer_variables);
177
178 SolveInterrupter interrupter;
179
180 SolveArguments args;
181 args.parameters = GetParam().solve_parameters;
182 args.interrupter = &interrupter;
183
184 ASSERT_OK_AND_ASSIGN(const SolveResult result,
185 Solve(*model, GetParam().solver_type, args));
186
187 // Calling Interrupt after the end of the solve should not break anything.
188 interrupter.Interrupt();
189 EXPECT_THAT(result, IsOptimal());
190}
191
192TEST_P(GenericTest, InterrupterNeverTriggered) {
193 // The rationale for this test is that for Gurobi we have a background thread
194 // that is responsible from calling the Gurobi termination API. We want to
195 // test that this background thread terminates properly even when the
196 // interrupter is not triggered at all.
197
198 if (!GetParam().support_interrupter) {
199 GTEST_SKIP() << "Solve interrupter not supported. Ignoring this test.";
200 }
201
202 const std::unique_ptr<Model> model = SmallModel(GetParam().integer_variables);
203
204 SolveInterrupter interrupter;
205
206 SolveArguments args;
207 args.parameters = GetParam().solve_parameters;
208 args.interrupter = &interrupter;
209
210 ASSERT_OK_AND_ASSIGN(const SolveResult result,
211 Solve(*model, GetParam().solver_type, args));
212 EXPECT_THAT(result, IsOptimal());
213}
214
215#ifdef OPERATIONS_RESEARCH_OUTPUT_CAPTURE_SUPPORTED
216TEST_P(GenericTest, NoStdoutOutputByDefault) {
217 Model model("model");
218 const Variable x =
219 model.AddVariable(0, 21.0, GetParam().integer_variables, "x");
220 model.Maximize(2.0 * x);
221
222 ScopedStdStreamCapture stdout_capture(CapturedStream::kStdout);
223 ASSERT_OK(SimpleSolve(model));
224 EXPECT_THAT(std::move(stdout_capture).StopCaptureAndReturnContents(),
226 /*is_gurobi=*/GetParam().solver_type == SolverType::kGurobi));
227}
228
229TEST_P(GenericTest, EnableOutputPrintsToStdOut) {
230 Model model("model");
231 const Variable x =
232 model.AddVariable(0, 21.0, GetParam().integer_variables, "x");
233 model.Maximize(2.0 * x);
234
235 SolveParameters params = GetParam().solve_parameters;
236 params.enable_output = true;
237
238 ScopedStdStreamCapture stdout_capture(CapturedStream::kStdout);
239
240 EXPECT_THAT(Solve(model, GetParam().solver_type, {.parameters = params}),
241 IsOkAndHolds(IsOptimal(42.0)));
242
243 EXPECT_THAT(std::move(stdout_capture).StopCaptureAndReturnContents(),
244 HasSubstr(GetParam().expected_log));
245}
246#endif // OPERATIONS_RESEARCH_OUTPUT_CAPTURE_SUPPORTED
247
248// Returns a string containing all ASCII 7-bits characters (but 0); i.e. all
249// characters in [1, 0x7f].
250std::string AllAsciiCharacters() {
251 std::ostringstream oss;
252 for (char c = '\x1'; c < '\x80'; ++c) {
253 oss << c;
254 }
255 return oss.str();
256}
257
258// Returns all non-ASCII 7-bits characters, i.e. characters in [0x80, 0xff].
259std::string AllNonAsciiCharacters() {
260 std::ostringstream oss;
261 for (int c = 0x80; c <= 0xff; ++c) {
262 oss << static_cast<char>(c);
263 }
264 return oss.str();
265}
266
267TEST_P(GenericTest, ModelNameTooLong) {
268 // GLPK and Gurobi have a limit for problem name to 255 characters; here we
269 // use long names to validate that it does not raise any assertion (along with
270 // other solvers).
271 EXPECT_THAT(SimpleSolve(Model(std::string(1024, 'x'))),
272 IsOkAndHolds(IsOptimal(0.0)));
273
274 // GLPK refuses control characters (iscntrl()) in the problem name and has a
275 // limit for problem name to 255 characters. Here we validate that the
276 // truncation of the string takes into account the quoting of the control
277 // characters (we pass all 7-bits ASCII characters to make sure they are
278 // accepted).
279 EXPECT_THAT(SimpleSolve(Model(AllAsciiCharacters() + std::string(1024, 'x'))),
280 IsOkAndHolds(IsOptimal(0.0)));
281
282 // GLPK should accept non-ASCII characters (>= 0x80).
284 SimpleSolve(Model(AllNonAsciiCharacters() + std::string(1024, 'x'))),
285 IsOkAndHolds(IsOptimal(0.0)));
286}
287
288TEST_P(GenericTest, VariableNames) {
289 // See rationales in ModelName for these tests.
290 {
291 Model model;
292 model.AddVariable(-1.0, 2.0, GetParam().integer_variables,
293 std::string(1024, 'x'));
294 EXPECT_THAT(SimpleSolve(model), IsOkAndHolds(IsOptimal(0.0)));
295 }
296 {
297 Model model;
298 model.AddVariable(-1.0, 2.0, GetParam().integer_variables,
299 AllAsciiCharacters() + std::string(1024, 'x'));
300 EXPECT_THAT(SimpleSolve(model), IsOkAndHolds(IsOptimal(0.0)));
301 }
302 {
303 Model model;
304 model.AddVariable(-1.0, 2.0, GetParam().integer_variables,
305 AllNonAsciiCharacters() + std::string(1024, 'x'));
306 EXPECT_THAT(SimpleSolve(model), IsOkAndHolds(IsOptimal(0.0)));
307 }
308 // Test two variables that thanks to the truncation will get the same name are
309 // not an issue for the solver.
310 {
311 Model model;
312 model.AddVariable(-1.0, 2.0, GetParam().integer_variables,
313 std::string(1024, '-') + 'x');
314 model.AddVariable(-1.0, 2.0, GetParam().integer_variables,
315 std::string(1024, '-') + 'y');
316 EXPECT_THAT(SimpleSolve(model), IsOkAndHolds(IsOptimal(0.0)));
317 }
318}
319
320TEST_P(GenericTest, LinearConstraintNames) {
321 // See rationales in ModelName for these tests.
322 {
323 Model model;
324 model.AddLinearConstraint(-1.0, 2.0, std::string(1024, 'x'));
325 EXPECT_THAT(SimpleSolve(model), IsOkAndHolds(IsOptimal(0.0)));
326 }
327 {
328 Model model;
329 model.AddLinearConstraint(-1.0, 2.0,
330 AllAsciiCharacters() + std::string(1024, 'x'));
331 EXPECT_THAT(SimpleSolve(model), IsOkAndHolds(IsOptimal(0.0)));
332 }
333 {
334 Model model;
335 model.AddLinearConstraint(-1.0, 2.0,
336 AllNonAsciiCharacters() + std::string(1024, 'x'));
337 EXPECT_THAT(SimpleSolve(model), IsOkAndHolds(IsOptimal(0.0)));
338 }
339 // Test two constraints that thanks to the truncation will get the same name
340 // are not an issue for the solver.
341 {
342 Model model;
343 model.AddLinearConstraint(-1.0, 2.0, std::string(1024, '-') + 'x');
344 model.AddLinearConstraint(-1.0, 2.0, std::string(1024, '-') + 'y');
345 EXPECT_THAT(SimpleSolve(model), IsOkAndHolds(IsOptimal(0.0)));
346 }
347 // Solvers should accept a ModelProto whose linear_constraints.names repeated
348 // field is not set. As of 2023-08-21 this is done by remove_names.
349 {
350 Model model;
351 const Variable x =
352 model.AddVariable(0.0, 1.0, GetParam().integer_variables, "x");
353 model.AddLinearConstraint(x == 1.0, "c");
354 SolverInitArguments init_args;
355 init_args.remove_names = true;
357 const SolveResult result,
358 Solve(model, GetParam().solver_type,
359 {.parameters = GetParam().solve_parameters}, init_args));
360 EXPECT_THAT(result, IsOptimal(0.0));
361 }
362}
363
364// TODO(b/227217735): Add a QuadraticConstraintNames test.
365
366// Test that the solvers properly translates the MathOpt ids to their internal
367// indices by using a model where indices don't start a zero.
368TEST_P(GenericTest, NonZeroIndices) {
369 // To test that solvers don't truncate by mistake numbers in the whole range
370 // of valid id numbers, we force the use of the maximum value by using a input
371 // model proto.
372 ModelProto base_model_proto;
373 constexpr int64_t kMaxValidId = std::numeric_limits<int64_t>::max() - 1;
374 {
375 VariablesProto& variables = *base_model_proto.mutable_variables();
376 variables.add_ids(kMaxValidId - 1);
377 variables.add_lower_bounds(-kInf);
378 variables.add_upper_bounds(kInf);
379 variables.add_integers(false);
380 }
381 {
382 LinearConstraintsProto& linear_constraints =
383 *base_model_proto.mutable_linear_constraints();
384 linear_constraints.add_ids(kMaxValidId - 1);
385 linear_constraints.add_lower_bounds(-kInf);
386 linear_constraints.add_upper_bounds(kInf);
387 }
388
389 ASSERT_OK_AND_ASSIGN(std::unique_ptr<Model> model,
390 Model::FromModelProto(base_model_proto));
391
392 // We remove the temporary variable and constraint we used to offset the id of
393 // the new variables and constraints below.
394 model->DeleteVariable(model->Variables().back());
395 model->DeleteLinearConstraint(model->LinearConstraints().back());
396
397 const Variable x =
398 model->AddVariable(0.0, kInf, GetParam().integer_variables, "x");
399 EXPECT_EQ(x.id(), kMaxValidId);
400
401 model->Maximize(x);
402
403 const LinearConstraint c = model->AddLinearConstraint(2 * x <= 8.0, "c");
404 EXPECT_EQ(c.id(), kMaxValidId);
405
406 EXPECT_THAT(SimpleSolve(*model), IsOkAndHolds(IsOptimal(4.0)));
407}
408
409// Returns a matcher that passes if the solver returns the
410// InvertedBounds::ToStatus() status.
411testing::Matcher<absl::StatusOr<SolveResult>> StatusIsInvertedBounds(
412 const InvertedBounds& inverted_bounds) {
413 return testing::Property("status", &absl::StatusOr<SolveResult>::status,
414 inverted_bounds.ToStatus());
415}
416
417TEST_P(GenericTest, InvertedVariableBounds) {
418 const SolveArguments solve_args = {.parameters = GetParam().solve_parameters};
419
420 // First test with bounds inverted at the construction of the solver.
421 //
422 // Here we test multiple values as some solvers like SCIP can show specific
423 // bugs for variables with bounds in {0.0, 1.0}. Those are upgraded to binary
424 // and changing bounds of these variables later raises assertions.
425 const std::vector<std::pair<double, double>> new_variables_inverted_bounds = {
426 {3.0, 1.0}, {0.0, -1.0}, {1.0, -1.0}, {1.0, 0.0}};
427 for (const auto [lb, ub] : new_variables_inverted_bounds) {
428 SCOPED_TRACE(absl::StrCat("lb: ", lb, " ub: ", ub));
429
430 Model model;
431
432 // Here we add some variables that we immediately remove so that the id of
433 // `x` below won't be 0. This will help making sure bugs in conversion from
434 // column number to MathOpt ids are caught by this test.
435 constexpr int64_t kXId = 13;
436 for (int64_t i = 0; i < kXId; ++i) {
437 model.DeleteVariable(model.AddVariable());
438 }
439
440 const Variable x = model.AddVariable(/*lower_bound=*/lb, /*upper_bound=*/ub,
441 GetParam().integer_variables, "x");
442 ASSERT_EQ(x.id(), kXId);
443
444 model.Maximize(3.0 * x);
445
446 // The instantiation should not fail, even if the bounds are reversed.
447 ASSERT_OK_AND_ASSIGN(const std::unique_ptr<IncrementalSolver> solver,
448 NewIncrementalSolver(&model, GetParam().solver_type));
449
450 // Solving should fail because of the inverted bounds.
451 EXPECT_THAT(solver->Solve(solve_args),
452 StatusIsInvertedBounds({.variables = {x.id()}}));
453 }
454
455 // Then test with bounds inverted during an update.
456 //
457 // See above for why we use various bounds.
458 for (const auto [initial_lb, initial_ub, new_lb, new_ub] :
459 std::vector<std::tuple<double, double, double, double>>{
460 {3.0, 4.0, 5.0, 4.0},
461 {0.0, 1.0, 2.0, 1.0},
462 {1.0, 1.0, 2.0, 1.0},
463 {0.0, 1.0, 0.0, -1.0},
464 {1.0, 1.0, 1.0, 0.0},
465 {1.0, 1.0, 1.0, -1.0}}) {
466 SCOPED_TRACE(absl::StrCat("initial_lb: ", initial_lb,
467 " initial_ub: ", initial_ub, " new_lb: ", new_lb,
468 " new_ub: ", new_ub));
469 Model model;
470
471 // Here we add some variables that we immediately remove so that the id of
472 // `x` below won't be 0. This will help making sure bugs in conversion from
473 // column number to MathOpt ids are caught by this test.
474 constexpr int64_t kXId = 13;
475 for (int64_t i = 0; i < kXId; ++i) {
476 model.DeleteVariable(model.AddVariable());
477 }
478
479 const Variable x = model.AddVariable(/*lower_bound=*/initial_lb,
480 /*upper_bound=*/initial_ub,
481 GetParam().integer_variables, "x");
482 ASSERT_EQ(x.id(), kXId);
483
484 model.Maximize(3.0 * x);
485
486 ASSERT_OK_AND_ASSIGN(const std::unique_ptr<IncrementalSolver> solver,
487 NewIncrementalSolver(&model, GetParam().solver_type));
488
489 // As of 2022-11-17 the glp_interior() algorithm returns GLP_EFAIL when the
490 // model is "empty" (no rows or columns). The issue is that the emptiness is
491 // considered *after* the model has been somewhat pre-processed, in
492 // particular after FIXED variables have been removed.
493 //
494 // TODO(b/259557110): remove this skip once glpk_solver.cc is fixed
495 if (GetParam().solver_type == SolverType::kGlpk &&
496 GetParam().solve_parameters.lp_algorithm == LPAlgorithm::kBarrier) {
497 LOG(INFO) << "Skipping the initial solve as glp_interior() would fail.";
498 } else {
499 EXPECT_THAT(solver->SolveWithoutUpdate(solve_args),
500 IsOkAndHolds(IsOptimal(3.0 * initial_ub)));
501 }
502
503 // Breaking the bounds should make the SolveWithoutUpdate() fail but not the
504 // Update() itself.
505 model.set_lower_bound(x, new_lb);
506 model.set_upper_bound(x, new_ub);
507 ASSERT_OK(solver->Update());
508 EXPECT_THAT(solver->SolveWithoutUpdate(solve_args),
509 StatusIsInvertedBounds({.variables = {x.id()}}));
510 }
511
512 // Finally test with an update adding a variable with inverted bounds.
513 //
514 // See above for why we use various bounds.
515 for (const auto [lb, ub] : new_variables_inverted_bounds) {
516 SCOPED_TRACE(absl::StrCat("lb: ", lb, " ub: ", ub));
517 Model model;
518
519 // Here we add some variables that we immediately remove so that the id of
520 // `x` below won't be 0. This will help making sure bugs in conversion from
521 // column number to MathOpt ids are caught by this test.
522 constexpr int64_t kXId = 13;
523 for (int64_t i = 0; i < kXId; ++i) {
524 model.DeleteVariable(model.AddVariable());
525 }
526
527 const Variable x =
528 model.AddVariable(/*lower_bound=*/3.0, /*upper_bound=*/4.0,
529 GetParam().integer_variables, "x");
530 ASSERT_EQ(x.id(), kXId);
531
532 model.Maximize(3.0 * x);
533
534 ASSERT_OK_AND_ASSIGN(const std::unique_ptr<IncrementalSolver> solver,
535 NewIncrementalSolver(&model, GetParam().solver_type));
536
537 EXPECT_THAT(solver->SolveWithoutUpdate(solve_args),
538 IsOkAndHolds(IsOptimal(3.0 * 4.0)));
539
540 // Test the update using a new variable with inverted bounds (in case the
541 // update code path is not identical to the NewIncrementalSolver() one).
542 const Variable y = model.AddVariable(/*lower_bound=*/lb, /*upper_bound=*/ub,
543 GetParam().integer_variables, "y");
544 model.Maximize(3.0 * x + y);
545 ASSERT_OK(solver->Update());
546 EXPECT_THAT(solver->SolveWithoutUpdate(solve_args),
547 StatusIsInvertedBounds({.variables = {y.id()}}));
548 }
549}
550
551TEST_P(GenericTest, InvertedLinearConstraintBounds) {
552 const SolveArguments solve_args = {.parameters = GetParam().solve_parameters};
553
554 // First test with bounds inverted at the construction of the solver.
555 {
556 Model model;
557 const Variable x =
558 model.AddVariable(/*lower_bound=*/0.0, /*upper_bound=*/10.0,
559 GetParam().integer_variables, "x");
560
561 // Here we add some constraints that we immediately remove so that the id of
562 // `u` below won't be 0. This will help making sure bugs in conversion from
563 // row number to MathOpt ids are caught by this test.
564 constexpr int64_t kUId = 23;
565 for (int64_t i = 0; i < kUId; ++i) {
566 model.DeleteLinearConstraint(model.AddLinearConstraint());
567 }
568
569 const LinearConstraint u = model.AddLinearConstraint(3.0 <= x <= 1.0, "u");
570 ASSERT_EQ(u.id(), kUId);
571
572 model.Maximize(3.0 * x);
573
574 // The instantiation should not fail, even if the bounds are reversed.
575 ASSERT_OK_AND_ASSIGN(const std::unique_ptr<IncrementalSolver> solver,
576 NewIncrementalSolver(&model, GetParam().solver_type));
577
578 // Solving should fail because of the inverted bounds.
579 EXPECT_THAT(solver->Solve(solve_args),
580 StatusIsInvertedBounds({.linear_constraints = {u.id()}}));
581 }
582
583 // Then test with bounds inverted during an update.
584 {
585 Model model;
586 const Variable x =
587 model.AddVariable(/*lower_bound=*/0.0, /*upper_bound=*/10.0,
588 GetParam().integer_variables, "x");
589
590 // Here we add some constraints that we immediately remove so that the id of
591 // `u` below won't be 0. This will help making sure bugs in conversion from
592 // row number to MathOpt ids are caught by this test.
593 constexpr int64_t kUId = 23;
594 for (int64_t i = 0; i < kUId; ++i) {
595 model.DeleteLinearConstraint(model.AddLinearConstraint());
596 }
597
598 const LinearConstraint u = model.AddLinearConstraint(3.0 <= x <= 4.0, "u");
599 ASSERT_EQ(u.id(), kUId);
600
601 model.Maximize(3.0 * x);
602
603 ASSERT_OK_AND_ASSIGN(const std::unique_ptr<IncrementalSolver> solver,
604 NewIncrementalSolver(&model, GetParam().solver_type));
605
606 EXPECT_THAT(solver->SolveWithoutUpdate(solve_args),
607 IsOkAndHolds(IsOptimal(3.0 * 4.0)));
608
609 model.set_lower_bound(u, 5.0);
610
611 // Breaking the bounds should make the SolveWithoutUpdate() fail but not the
612 // Update() itself.
613 ASSERT_OK(solver->Update());
614 EXPECT_THAT(solver->SolveWithoutUpdate(solve_args),
615 StatusIsInvertedBounds({.linear_constraints = {u.id()}}));
616 }
617
618 // Finally test with an update adding a constraint with inverted bounds.
619 {
620 Model model;
621 const Variable x =
622 model.AddVariable(/*lower_bound=*/0.0, /*upper_bound=*/10.0,
623 GetParam().integer_variables, "x");
624
625 // Here we add some constraints that we immediately remove so that the id of
626 // `u` below won't be 0. This will help making sure bugs in conversion from
627 // row number to MathOpt ids are caught by this test.
628 constexpr int64_t kUId = 23;
629 for (int64_t i = 0; i < kUId; ++i) {
630 model.DeleteLinearConstraint(model.AddLinearConstraint());
631 }
632
633 const LinearConstraint u = model.AddLinearConstraint(3.0 <= x <= 4.0, "u");
634 ASSERT_EQ(u.id(), kUId);
635
636 model.Maximize(3.0 * x);
637
638 ASSERT_OK_AND_ASSIGN(const std::unique_ptr<IncrementalSolver> solver,
639 NewIncrementalSolver(&model, GetParam().solver_type));
640
641 EXPECT_THAT(solver->SolveWithoutUpdate(solve_args),
642 IsOkAndHolds(IsOptimal(3.0 * 4.0)));
643
644 // Test the update with a new constraint with inverted bounds (in case the
645 // update code path is not identical to the NewIncrementalSolver() one).
646 const LinearConstraint v = model.AddLinearConstraint(5.0 <= x <= 3.0, "v");
647
648 ASSERT_OK(solver->Update());
649 EXPECT_THAT(solver->SolveWithoutUpdate(solve_args),
650 StatusIsInvertedBounds({.linear_constraints = {v.id()}}));
651 }
652}
653
654TEST_P(TimeLimitTest, DenseIndependentSetNoTimeLimit) {
655 const std::unique_ptr<const Model> model =
656 DenseIndependentSet(GetParam().integer_variables);
657 const double expected_objective =
658 GetParam().integer_variables ? 7 : 10.0 * (5 + 4 + 3) / 2.0;
659 EXPECT_THAT(Solve(*model, GetParam().solver_type),
660 IsOkAndHolds(IsOptimal(expected_objective)));
661}
662
663TEST_P(TimeLimitTest, DenseIndependentSetTimeLimit) {
664 ASSERT_TRUE(GetParam().event.has_value())
665 << "The TimeLimit test requires a callback event is given.";
666 SolveArguments solve_args = {.message_callback =
667 InfoLoggerMessageCallback("[solver] ")};
668 solve_args.parameters.time_limit = absl::Seconds(1);
669 // We want to block all progress while sleeping in the callback, so we limit
670 // the solver to one thread.
671 solve_args.parameters.threads = 1;
672 // Presolve can eliminate the whole problem for some solvers (CP-SAT).
673 solve_args.parameters.presolve = Emphasis::kOff;
674 solve_args.callback_registration.events.insert(*GetParam().event);
675
676 const std::unique_ptr<const Model> model =
677 DenseIndependentSet(GetParam().integer_variables);
678
679 // Callback may be called from multiple threads, serialize access to has_run.
680 absl::Mutex mutex;
681 bool has_run = false;
682 solve_args.callback = [&mutex, &has_run](const CallbackData& data) {
683 const absl::MutexLock lock(&mutex);
684 if (!has_run) {
685 LOG(INFO) << "Waiting two seconds in the callback...";
686 absl::SleepFor(absl::Seconds(2));
687 LOG(INFO) << "Done waiting in callback.";
688 }
689 has_run = true;
690 return CallbackResult();
691 };
692 ASSERT_OK_AND_ASSIGN(const SolveResult result,
693 Solve(*model, GetParam().solver_type, solve_args));
695 /*allow_limit_undetermined=*/true));
696 EXPECT_TRUE(has_run);
697}
698
699} // namespace
700} // namespace operations_research::math_opt
Definition model.h:341
void Maximize(Variable *obj, std::vector< Annotation > search_annotations)
Definition model.cc:1075
Variable * AddVariable(absl::string_view name, const Domain &domain, bool defined)
--— Builder methods --—
Definition model.cc:1025
void Minimize(Variable *obj, std::vector< Annotation > search_annotations)
Definition model.cc:1068
static absl::StatusOr< std::unique_ptr< Model > > FromModelProto(const ModelProto &model_proto)
Definition model.cc:56
An object oriented wrapper for quadratic constraints in ModelStorage.
Definition gurobi_isv.cc:28
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:41
<=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)))
absl::StatusOr< SolveResult > Solve(const Model &model, const SolverType solver_type, const SolveArguments &solve_args, const SolverInitArguments &init_args)
Definition solve.cc:62
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 > TerminatesWithLimit(const Limit expected, const bool allow_limit_undetermined)
Definition matchers.cc:649
MessageCallback InfoLoggerMessageCallback(const absl::string_view prefix, const absl::SourceLocation loc)
std::ostream & operator<<(std::ostream &ostr, const SecondOrderConeConstraint &constraint)
@ kTime
The algorithm stopped after a user-specified computation time.
std::unique_ptr< Model > DenseIndependentSet(const bool integer, const int n)
Matcher< SolveResult > IsOptimal(const std::optional< double > expected_primal_objective, const double tolerance)
Definition matchers.cc:763
std::string ProtobufShortDebugString(const P &message)
Definition proto_utils.h:46
testing::Matcher< std::string > EmptyOrGurobiLicenseWarningIfGurobi(const bool is_gurobi)
STL namespace.
internal::IsOkAndHoldsMatcher< typename std::decay< InnerMatcher >::type > IsOkAndHolds(InnerMatcher &&inner_matcher)
#define ASSERT_OK(expression)
#define ASSERT_OK_AND_ASSIGN(lhs, rexpr)
The value returned by the Callback function.
Definition callback.h:225
bool support_interrupter
True if the solver support SolveInterrupter.
SolveParameters solve_parameters
Additional parameters to control the solve.
std::string expected_log
A message included in the solver logs when an optimal solution is found.
bool integer_variables
True if the tests should be performed with integer variables.
GenericTestParameters(SolverType solver_type, bool support_interrupter, bool integer_variables, std::string expected_log, SolveParameters solve_parameters={})
SolveParameters parameters
Model independent parameters, e.g. time limit.
bool integer_variables
The test problem will be a 0-1 IP if true, otherwise will be an LP.
TimeLimitTestParameters(const SolverType solver_type, const bool integer_variables, const std::optional< CallbackEvent > supported_event=std::nullopt)
std::optional< CallbackEvent > event
A supported callback event, or nullopt if no event is supported.