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