Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
ip_parameter_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
14// How each parameter is tested:
15// Parameter | IpParameterTest | generic_test.h | LargeInstanceTest
16// -------------------|-----------------|-----------------|-------------------
17// time_limit | | x | x
18// iteration_limit | x | | x
19// node_limit | x | | x
20// cutoff_limit | x | | x
21// objective_limit | x | | x
22// best_bound_limit | x | | x
23// solution_limit | x | | x
24// enable_output | | x |
25// threads | | |
26// random_seed | x | |
27// absolute_gap_tol | x | | x
28// relative_gap_tol | x | | x
29// solution_pool_size | | |
30// lp_algorithm | x (bad) | |
31// presolve | x | |
32// cuts | x | | x
33// heuristics | | |
34// scaling | | |
35//
36// solution_pool_size is tested in ip_multiple_solutions_tests.cc.
37//
38// Testing some parameters requires that other parameters/stats are supported:
39// * presolve (IpParameterTest) requires message callbacks
40// * cuts (IpParameterTest) must disable presolve
41// * cuts (LargeInstanceTest) needs node_limit=1.
42// * lp_algorithm (IpParameterTest) test is just a no-crash test without
43// supporting iteration counts in SolveStats.
44// * solution_limit (IpParameterTest) requires a hint.
45//
46// TODO(b/180024054): add tests for:
47// * threads
48// * heuristics
49// * scaling
50// * lp_algorithm, differentiate between primal and dual simplex. E.g. find a
51// problem with LP relaxation that is both infeasible and dual infeasible,
52// disable presolve, and solve. When using primal simplex, we should get
53// termination reason kInfeasible, but dual simplex should give
54// kInfeasibleOrUnbounded.
55// * TODO(b/272268188): test the interaction between cutoff and primal + dual
56// infeasibility.
57//
59
60#include <algorithm>
61#include <cmath>
62#include <cstdlib>
63#include <limits>
64#include <memory>
65#include <ostream>
66#include <sstream>
67#include <string>
68#include <utility>
69#include <vector>
70
71#include "absl/container/flat_hash_set.h"
72#include "absl/status/status.h"
73#include "absl/status/statusor.h"
74#include "absl/strings/escaping.h"
75#include "absl/strings/str_cat.h"
76#include "absl/strings/str_join.h"
77#include "absl/strings/string_view.h"
78#include "absl/time/time.h"
79#include "absl/types/span.h"
80#include "gtest/gtest.h"
81#include "ortools/base/gmock.h"
90
91namespace operations_research {
92namespace math_opt {
93
94std::ostream& operator<<(std::ostream& ostr,
95 const SolveResultSupport& solve_result_support) {
96 std::vector<std::string> supported;
97 if (solve_result_support.termination_limit) {
98 supported.push_back("termination_limit");
99 }
100 if (solve_result_support.iteration_stats) {
101 supported.push_back("iteration_stats");
102 }
103 if (solve_result_support.node_count) {
104 supported.push_back("node_count");
105 }
106 ostr << "{ " << absl::StrJoin(supported, ", ") << " }";
107 return ostr;
108}
109
110std::ostream& operator<<(std::ostream& ostr,
111 const ParameterSupport& param_support) {
112 std::vector<std::string> supported;
113 if (param_support.supports_iteration_limit) {
114 supported.push_back("supports_iteration_limit");
115 }
116 if (param_support.supports_node_limit) {
117 supported.push_back("supports_node_limit");
118 }
119 if (param_support.supports_cutoff) {
120 supported.push_back("supports_cutoff");
121 }
122 if (param_support.supports_objective_limit) {
123 supported.push_back("supports_objective_limit");
124 }
125 if (param_support.supports_bound_limit) {
126 supported.push_back("supports_bound_limit");
127 }
128 if (param_support.supports_solution_limit_one) {
129 supported.push_back("supports_solution_limit_one");
130 }
131 if (param_support.supports_one_thread) {
132 supported.push_back("supports_one_thread");
133 }
134 if (param_support.supports_n_threads) {
135 supported.push_back("supports_n_threads");
136 }
137 if (param_support.supports_random_seed) {
138 supported.push_back("supports_random_seed");
139 }
140 if (param_support.supports_absolute_gap_tolerance) {
141 supported.push_back("supports_absolute_gap_tolerance");
142 }
143 if (param_support.supports_lp_algorithm_simplex) {
144 supported.push_back("supports_lp_algorithm_simplex");
145 }
146 if (param_support.supports_lp_algorithm_barrier) {
147 supported.push_back("supports_lp_algorithm_barrier");
148 }
149 if (param_support.supports_presolve) {
150 supported.push_back("supports_presolve");
151 }
152 if (param_support.supports_cuts) {
153 supported.push_back("supports_cuts");
154 }
155 if (param_support.supports_heuristics) {
156 supported.push_back("supports_heuristics");
157 }
158 if (param_support.supports_scaling) {
159 supported.push_back("supports_scaling");
160 }
161 ostr << "{ " << absl::StrJoin(supported, ", ") << " }";
162 return ostr;
163}
164
165std::ostream& operator<<(std::ostream& ostr,
166 const IpParameterTestParameters& params) {
167 ostr << "{ name: " << params.name;
168 ostr << ", solver_type: " << EnumToString(params.solver_type);
169 ostr << ", parameter_support: " << params.parameter_support;
170 ostr << ", hint_supported: " << params.hint_supported;
171 ostr << ", solve_result_support: " << params.solve_result_support;
172 ostr << ", presolved_regexp: \"" << absl::CEscape(params.presolved_regexp);
173 ostr << "\", stop_before_optimal: "
175 return ostr;
176}
177
178std::ostream& operator<<(std::ostream& out,
179 const LargeInstanceTestParams& params) {
180 out << "{ name: " << params.name;
181 out << ", solver_type: " << EnumToString(params.solver_type);
182 out << ", base_parameters: "
184 out << ", parameter_support: " << params.parameter_support;
185 out << ", allow_limit_undetermined: " << params.allow_limit_undetermined
186 << " }";
187 return out;
188}
189
190namespace {
191
192using ::testing::HasSubstr;
193using ::testing::Not;
194using ::testing::status::IsOkAndHolds;
195using ::testing::status::StatusIs;
196
197// Adds the trio of constraints:
198// x + y >= 1
199// y + z >= 1
200// x + z >= 1
201// In the vertex cover problem, you have a graph and must pick a subset of the
202// nodes so that every edge has at least one endpoint selected. If x, y, and z
203// are nodes in this graph, adding Triangle(x, y, z) ensures that two of x, y,
204// and z must be selected in any vertex cover.
205void AddTriangle(Model& model, Variable x, Variable y, Variable z) {
206 model.AddLinearConstraint(x + y >= 1);
207 model.AddLinearConstraint(y + z >= 1);
208 model.AddLinearConstraint(z + x >= 1);
209}
210
211// xs, ys, and zs must have all size 3. Adds the constraints:
212// Triangle(x1, x2, x3),
213// Triangle(y1, y2, y3),
214// Triangle(z1, z2, z3),
215// Triangle(x1, y1, z1),
216// Triangle(x2, y2, z2),
217// Triangle(x3, y3, z3),
218//
219// Adding this constraint ensures that the minimum vertex cover (pick a subset
220// of the 9 nodes such that for every edge, at least one node is selected) has
221// size at least 6. There are many possible symmetric solutions, any solution
222// has two xs, two ys, two z2, two ones, two twos, and two threes, e.g.
223// x1, y1,
224// z2, z3,
225// x2, y3.
226void AddSixTriangles(Model& model, absl::Span<const Variable> xs,
227 absl::Span<const Variable> ys,
228 absl::Span<const Variable> zs) {
229 AddTriangle(model, xs[0], xs[1], xs[2]);
230 AddTriangle(model, ys[0], ys[1], ys[2]);
231 AddTriangle(model, zs[0], zs[1], zs[2]);
232 AddTriangle(model, xs[0], ys[0], zs[0]);
233 AddTriangle(model, xs[1], ys[1], zs[1]);
234 AddTriangle(model, xs[2], ys[2], zs[2]);
235}
236
237// A MIP with an LP relaxation of 13.5 and a best integer solution of 18. The
238// MIP has a large number of symmetric solutions. It is given by:
239//
240// min sum_{i=1}^3 sum_{j=1}^3 x_ij + y_ij + z_ij
241//
242// SixTriangles(x11, x12, x13, y11, y12, y13, z11, z12, z13)
243// SixTriangles(x21, x22, x23, y21, y22, y23, z21, z22, z23)
244// SixTriangles(x31, x32, x33, y31, y32, y33, z31, z32, z33)
245//
246// for j = 1..3:
247// Triangle(x1j, x2j, x3j)
248// Triangle(y1j, y2j, y3j)
249// Triangle(z1j, z2j, z3j)
250//
251// It can be interpreted as a large instance of vertex cover on 27 nodes.
252class VertexCover {
253 public:
254 VertexCover() {
255 xs_ = MakeVars3By3("x");
256 ys_ = MakeVars3By3("y");
257 zs_ = MakeVars3By3("z");
258 LinearExpression objective;
259 for (int i = 0; i < 3; ++i) {
260 objective += Sum(xs_[i]) + Sum(ys_[i]) + Sum(zs_[i]);
261 }
262 model_.Minimize(objective);
263 for (int i = 0; i < 3; ++i) {
264 AddSixTriangles(model_, xs_[i], ys_[i], zs_[i]);
265 }
266 for (int j = 0; j < 3; ++j) {
267 AddTriangle(model_, xs_[0][j], xs_[1][j], xs_[2][j]);
268 AddTriangle(model_, ys_[0][j], ys_[1][j], ys_[2][j]);
269 AddTriangle(model_, zs_[0][j], zs_[1][j], zs_[2][j]);
270 }
271 }
272
273 Model& model() { return model_; }
274
275 std::vector<std::string> SolutionFingerprint(const SolveResult& result) {
276 std::vector<std::string> var_names;
277 for (const Variable v : model_.Variables()) {
278 double val = result.variable_values().at(v);
279 if (val > 0.5) {
280 var_names.push_back(std::string(v.name()));
281 }
282 }
283 std::sort(var_names.begin(), var_names.end());
284 return var_names;
285 }
286
287 static absl::StatusOr<std::vector<std::string>> SolveAndFingerprint(
288 const SolverType solver_type, const SolveParameters& params) {
289 VertexCover vertex_cover;
291 SolveResult result,
292 Solve(vertex_cover.model(), solver_type, {.parameters = params}));
293 RETURN_IF_ERROR(result.termination.EnsureIsOptimal());
294 if (std::abs(result.objective_value() - 18.0) > 1e-4) {
296 << "expected objective value of 18, found: "
297 << result.objective_value();
298 }
299 return result.has_primal_feasible_solution()
300 ? vertex_cover.SolutionFingerprint(result)
301 : std::vector<std::string>();
302 }
303
304 private:
305 // Adds 9 binary variables to the model and returns them in a 3x3 array.
306 std::vector<std::vector<Variable>> MakeVars3By3(absl::string_view prefix) {
307 std::vector<std::vector<Variable>> result(3);
308 for (int i = 0; i < 3; ++i) {
309 for (int j = 0; j < 3; ++j) {
310 result[i].push_back(
311 model_.AddBinaryVariable(absl::StrCat(prefix, "_", i, "_", j)));
312 }
313 }
314 return result;
315 }
316
317 Model model_;
318 std::vector<std::vector<Variable>> xs_;
319 std::vector<std::vector<Variable>> ys_;
320 std::vector<std::vector<Variable>> zs_;
321};
322
323// On a symmetric MIP with multiple optimal solutions, test that:
324// * If we use the same seed, we get the same result.
325// * If we use different seeds, we at least sometimes get different results.
326//
327// Warning: this test is quite fragile. I don't understand why, but if presolve
328// is disabled, the test fails for gSCIP and Gurobi, where both solvers always
329// find the same solution regardless of the seed.
330//
331// WARNING: the solve must be deterministic for this test to work. We set
332// threads=1 if supported, as usually multi-threaded solves are not
333// deterministic. HiGHS does not yet support this, but appears to still be
334// deterministic.
335TEST_P(IpParameterTest, RandomSeedIP) {
336 absl::flat_hash_set<std::vector<std::string>> solutions_seen;
337 for (int seed = 10; seed < 200; seed += 10) {
338 SCOPED_TRACE(absl::StrCat("seed ", seed));
339 std::vector<std::string> result_for_seed;
340 for (int trial = 0; trial < 10; ++trial) {
341 SCOPED_TRACE(absl::StrCat("trial ", trial));
342 SolveParameters params = {.random_seed = seed};
343 if (GetParam().parameter_support.supports_one_thread) {
344 params.threads = 1;
345 }
346 absl::StatusOr<std::vector<std::string>> fingerprint =
347 VertexCover::SolveAndFingerprint(TestedSolver(), params);
348 if (!GetParam().parameter_support.supports_random_seed) {
349 EXPECT_THAT(fingerprint, StatusIs(absl::StatusCode::kInvalidArgument,
350 HasSubstr("random_seed")));
351 return;
352 }
353 if (trial == 0) {
354 result_for_seed = *fingerprint;
355 solutions_seen.insert(result_for_seed);
356 } else {
357 ASSERT_EQ(result_for_seed, *fingerprint);
358 }
359 }
360 }
361 if (GetParam().solver_type != SolverType::kCpSat) {
362 // Drawing 20 items from a very large number with replacement, the
363 // probability of getting at least 3 unique is very high.
364 EXPECT_GE(solutions_seen.size(), 3);
365 }
366}
367
368// Solves the problem
369// max sum_i y_i
370// x_i >= y_i
371// x_i + y_1 == 1
372// x_i, y_i \in {0, 1}
373//
374// This is linearly separable in i, and we must have x_i = 1, y_i = 0 for all i.
375// Presolve can typically detect this.
376absl::StatusOr<std::pair<SolveStats, std::string>> SolveForIPPresolve(
377 SolverType solver_type, bool do_presolve) {
378 std::ostringstream oss;
379 Model model("easy_presolve_model");
380 model.set_maximize();
381 for (int i = 0; i < 4; ++i) {
382 Variable x = model.AddVariable(0.0, 1.0, true);
383 Variable y = model.AddVariable(0.0, 1.0, true);
384 model.AddToObjective(y);
385 model.AddLinearConstraint(x + y == 1.0);
386 model.AddLinearConstraint(x >= y);
387 }
388
389 SolveArguments args;
390 args.parameters.presolve = do_presolve ? Emphasis::kMedium : Emphasis::kOff;
391 args.message_callback = PrinterMessageCallback(oss);
392 ASSIGN_OR_RETURN(const SolveResult result, Solve(model, solver_type, args));
393 RETURN_IF_ERROR(result.termination.EnsureIsOptimal());
394 return std::make_pair(result.solve_stats, oss.str());
395}
396
397// On asserting that presolve is working:
398// We have created a problem and can be entirely solved in presolve (all
399// variables and constraints removed) using basic reductions that should be
400// supported by most solvers. It would be easiest to simply look at the size of
401// the problem after presolve, but this not universally supported (with Gurobi,
402// it is only visible in a callback). Instead, we check the logs for a solver
403// specific string that occurs only if presolve solves the model. Further, for
404// solvers that return simplex/barrier iterations, we check that there are no
405// such iterations iff presolve is on.
406TEST_P(IpParameterTest, PresolveOff) {
407 if (!GetParam().parameter_support.supports_presolve) {
408 // We test presolve being unsupported in IpParameterTest.PresolveOn
409 return;
410 }
411 absl::StatusOr<std::pair<SolveStats, std::string>> stats_and_logs =
412 SolveForIPPresolve(TestedSolver(), /*do_presolve=*/false);
413 ASSERT_OK(stats_and_logs);
414 const auto [solve_stats, logs] = stats_and_logs.value();
415 if (GetParam().solve_result_support.iteration_stats) {
416 EXPECT_GE(solve_stats.barrier_iterations + solve_stats.simplex_iterations +
417 solve_stats.first_order_iterations,
418 1);
419 }
420 EXPECT_THAT(logs, Not(testing::ContainsRegex(GetParam().presolved_regexp)));
421}
422
423TEST_P(IpParameterTest, PresolveOn) {
424 absl::StatusOr<std::pair<SolveStats, std::string>> stats_and_logs =
425 SolveForIPPresolve(TestedSolver(), /*do_presolve=*/true);
426 if (!GetParam().parameter_support.supports_presolve) {
427 EXPECT_THAT(stats_and_logs, StatusIs(absl::StatusCode::kInvalidArgument,
428 HasSubstr("presolve")));
429 return;
430 }
431 ASSERT_OK(stats_and_logs);
432 const auto [solve_stats, logs] = stats_and_logs.value();
433 if (!GetParam().solve_result_support.iteration_stats) {
434 EXPECT_EQ(solve_stats.barrier_iterations, 0);
435 EXPECT_EQ(solve_stats.simplex_iterations, 0);
436 EXPECT_EQ(solve_stats.first_order_iterations, 0);
437 }
438 EXPECT_THAT(logs, testing::ContainsRegex(GetParam().presolved_regexp));
439}
440
441// Requires disabling presolve and cuts is supported (or status errors).
442absl::StatusOr<SolveStats> SolveForCuts(SolverType solver_type, bool use_cuts) {
443 Model model;
444 model.set_maximize();
445 for (int i = 0; i < 10; ++i) {
446 const Variable x = model.AddVariable(0.0, 1.0, true);
447 const Variable y = model.AddVariable(0.0, 1.0, true);
448 model.AddToObjective(2 * x + y);
449 // With the addition of the knapsack cut:
450 // x + y <= 1
451 // the problem becomes integral.
452 model.AddLinearConstraint(3 * x + 2 * y <= 4);
453 }
454
455 SolveArguments args;
456 args.parameters.presolve = Emphasis::kOff;
457 args.parameters.cuts = use_cuts ? Emphasis::kMedium : Emphasis::kOff;
458 ASSIGN_OR_RETURN(const SolveResult result, Solve(model, solver_type, args));
459 RETURN_IF_ERROR(result.termination.EnsureIsOptimal());
460 return result.solve_stats;
461}
462
463TEST_P(IpParameterTest, CutsOff) {
464 if (!GetParam().parameter_support.supports_presolve) {
465 GTEST_SKIP() << "Skipping test, this test requires disabling presolve "
466 "which is not supported.";
467 }
468 if (!GetParam().parameter_support.supports_cuts) {
469 // If cuts are not supported we test this in IpParameterTest.CutsOn
470 return;
471 }
472 ASSERT_OK_AND_ASSIGN(const SolveStats solve_stats,
473 SolveForCuts(TestedSolver(), /*use_cuts=*/false));
474 if (GetParam().solve_result_support.node_count) {
475 EXPECT_GT(solve_stats.node_count, 1);
476 }
477}
478TEST_P(IpParameterTest, CutsOn) {
479 if (!GetParam().parameter_support.supports_presolve) {
480 GTEST_SKIP() << "Skipping test, this test requires disabling presolve "
481 "which is not supported.";
482 }
483 absl::StatusOr<SolveStats> solve_stats =
484 SolveForCuts(TestedSolver(), /*use_cuts=*/true);
485 if (!GetParam().parameter_support.supports_cuts) {
486 EXPECT_THAT(solve_stats, StatusIs(absl::StatusCode::kInvalidArgument,
487 HasSubstr("cuts")));
488 return;
489 }
490 ASSERT_OK(solve_stats);
491 if (GetParam().solve_result_support.node_count) {
492 EXPECT_EQ(solve_stats->node_count, 1);
493 }
494}
495
496// This method doesn't give any way to distinguish between primal and dual
497// simplex, a better test is needed, see comment at top of file for an idea.
498absl::StatusOr<SolveStats> SolveForRootLp(
499 const SolverType solver_type, const LPAlgorithm algorithm,
500 const ParameterSupport& parameter_support) {
501 VertexCover vertex_cover;
502 SolveParameters params = {.lp_algorithm = algorithm};
503 // Try to make sure that only one algorithm is used
504 if (parameter_support.supports_one_thread) {
505 params.threads = 1;
506 }
507 // Avoid making too much progress before the LP, we are testing based on use
508 // of the LP solver.
509 if (parameter_support.supports_presolve) {
510 params.presolve = Emphasis::kOff;
511 }
512
514 const SolveResult result,
515 Solve(vertex_cover.model(), solver_type, {.parameters = params}));
516 RETURN_IF_ERROR(result.termination.EnsureIsOptimal());
517 return result.solve_stats;
518}
519
520TEST_P(IpParameterTest, RootLPAlgorithmPrimal) {
521 const absl::StatusOr<SolveStats> stats =
522 SolveForRootLp(TestedSolver(), LPAlgorithm::kPrimalSimplex,
523 GetParam().parameter_support);
524 if (!GetParam().parameter_support.supports_lp_algorithm_simplex) {
525 EXPECT_THAT(stats, StatusIs(absl::StatusCode::kInvalidArgument,
526 HasSubstr("lp_algorithm")));
527 return;
528 }
529 ASSERT_OK(stats);
530 if (GetParam().solve_result_support.iteration_stats) {
531 EXPECT_GT(stats->simplex_iterations, 0);
532 EXPECT_EQ(stats->barrier_iterations, 0);
533 EXPECT_EQ(stats->first_order_iterations, 0);
534 }
535}
536
537TEST_P(IpParameterTest, RootLPAlgorithmDual) {
538 const absl::StatusOr<SolveStats> stats = SolveForRootLp(
539 TestedSolver(), LPAlgorithm::kDualSimplex, GetParam().parameter_support);
540 if (!GetParam().parameter_support.supports_lp_algorithm_simplex) {
541 EXPECT_THAT(stats, StatusIs(absl::StatusCode::kInvalidArgument,
542 HasSubstr("lp_algorithm")));
543 return;
544 }
545 ASSERT_OK(stats);
546 if (GetParam().solve_result_support.iteration_stats) {
547 EXPECT_GT(stats->simplex_iterations, 0);
548 EXPECT_EQ(stats->barrier_iterations, 0);
549 EXPECT_EQ(stats->first_order_iterations, 0);
550 }
551}
552
553TEST_P(IpParameterTest, RootLPAlgorithmBarrier) {
554 const absl::StatusOr<SolveStats> stats = SolveForRootLp(
555 TestedSolver(), LPAlgorithm::kBarrier, GetParam().parameter_support);
556 if (!GetParam().parameter_support.supports_lp_algorithm_barrier) {
557 EXPECT_THAT(stats, StatusIs(absl::StatusCode::kInvalidArgument,
558 HasSubstr("lp_algorithm")));
559 return;
560 }
561 ASSERT_OK(stats);
562 if (GetParam().solve_result_support.iteration_stats) {
563 EXPECT_GT(stats->barrier_iterations, 0);
564 // We make no assertions on simplex iterations, we do not specify if
565 // crossover takes place.
566 }
567}
568
569// No IP solver supports FOM for solving the root LP yet, update this test test
570// once supported.
571TEST_P(IpParameterTest, RootLPAlgorithmFirstOrder) {
573 SolveForRootLp(TestedSolver(), LPAlgorithm::kFirstOrder,
574 GetParam().parameter_support),
575 StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("lp_algorithm")));
576}
577
578TEST_P(IpParameterTest, IterationLimitIP) {
579 const int n = 10;
580 Model model("Iteration limit IP");
581 std::vector<Variable> x;
582 x.reserve(n);
583 for (int i = 0; i < n; ++i) {
584 x.push_back(model.AddIntegerVariable(0.0, 1.0));
585 }
586 for (int i = 0; i < n; ++i) {
587 for (int j = i + 1; j < n; ++j) {
588 model.AddLinearConstraint(3 * x[i] + 5 * x[j] <= 7);
589 }
590 }
591 model.Maximize(Sum(x));
592 SolveParameters params = {.iteration_limit = 1};
593 // Weaken the solver as much as possible to make sure we make it to solving
594 // the root LP.
595 if (GetParam().parameter_support.supports_presolve) {
596 params.presolve = Emphasis::kOff;
597 }
598 if (GetParam().parameter_support.supports_heuristics) {
599 params.heuristics = Emphasis::kOff;
600 }
601 if (GetParam().parameter_support.supports_one_thread) {
602 params.threads = 1;
603 }
604 const absl::StatusOr<SolveResult> result =
605 Solve(model, TestedSolver(), {.parameters = params});
606 if (!GetParam().parameter_support.supports_iteration_limit) {
607 EXPECT_THAT(result, StatusIs(absl::StatusCode::kInvalidArgument,
608 HasSubstr("iteration_limit")));
609 return;
610 }
613 /*allow_limit_undetermined=*/!GetParam()
614 .solve_result_support.termination_limit)));
615}
616
617TEST_P(IpParameterTest, NodeLimit) {
618 if (GetParam().solver_type == SolverType::kHighs) {
619 GTEST_SKIP()
620 << "This test does not work for HiGHS, which cannot disable cuts and "
621 "where disabling primal heuristics seems to have little effect, see "
622 "https://paste.googleplex.com/5694421105377280";
623 }
624 if (GetParam().solver_type == SolverType::kGscip) {
625 GTEST_SKIP() << "This test does not work with SCIP v900";
626 }
627 const std::unique_ptr<const Model> model = DenseIndependentSet(true);
628 SolveParameters params = {.node_limit = 1};
629 // Weaken the solver as much as possible so it does not solve the problem to
630 // optimality at the root.
631 if (GetParam().parameter_support.supports_presolve) {
632 params.presolve = Emphasis::kOff;
633 }
634 if (GetParam().parameter_support.supports_heuristics) {
635 params.heuristics = Emphasis::kOff;
636 }
637 if (GetParam().parameter_support.supports_cuts) {
638 params.cuts = Emphasis::kOff;
639 }
640 const absl::StatusOr<SolveResult> result =
641 Solve(*model, GetParam().solver_type, {.parameters = params});
642 if (!GetParam().parameter_support.supports_node_limit) {
643 EXPECT_THAT(result, StatusIs(absl::StatusCode::kInvalidArgument,
644 HasSubstr("node_limit")));
645 return;
646 }
647 EXPECT_THAT(result,
649 Limit::kNode, /*allow_limit_undetermined=*/!GetParam()
650 .solve_result_support.termination_limit)));
651}
652
653// Problem statement:
654// max y
655// s.t. x[i] + x[j] <= 1 for i = 1..n, j = 1..n.
656// k * Sum(x) + y <= k + 1
657// k * Sum(x) - y >= -1
658// y >= 1
659// x[i, j] binary for i = 1..n, j = 1..n.
660//
661// Note that:
662// k * z + y <= k + 1
663// k * z - y >= -1
664// y >= 1
665//
666// describes the convex hull of (z, y) = (0, 1), (z, y) = (1, 1), and
667// (z, y) = (1/2, k/2 + 1). Then for the problem in the (x, y) variables we
668// have:
669// * The optimal objective value for the LP relaxation is k/2 + 1 and the set
670// of optimal solutions is any (x, y) such that
671// 1. Sum(x) = 1/2
672// 2. 0 <= x <= 1
673// 3. x[i] + x[j] <= 1, for i = 1..n, j = 1..n.
674// 3. y = k/2 + 1
675// (e.g. one solution is x[1] = 1/2, x[i] = 0 for i != 1 and y = k/2 + 1)
676// * The optimal objective value of the MIP is 1 and any integer feasible
677// solution is optimal. (e.g. one solution is x[1] = 1, x[i] = 0 for i != 1
678// and y = k/2 + 1)
679//
680// Then the LP relaxation is enough to validate any integer feasible solution to
681// a relative or absolute gap of k/2.
682absl::StatusOr<SolveResult> SolveForGapLimit(const int k, const int n,
683 const SolverType solver_type,
684 const SolveParameters params) {
685 Model model("Absolute gap limit IP");
686 std::vector<Variable> x;
687 for (int i = 0; i < n; ++i) {
688 x.push_back(model.AddBinaryVariable());
689 }
690
691 for (int i = 0; i < n; ++i) {
692 for (int j = i + 1; j < n; ++j) {
693 model.AddLinearConstraint(x[i] + x[j] <= 1);
694 }
695 }
696 const Variable y =
697 model.AddContinuousVariable(1, std::numeric_limits<double>::infinity());
698 model.AddLinearConstraint(k * Sum(x) + y <= k + 1);
699 model.AddLinearConstraint(k * Sum(x) - y >= -1);
700 model.Maximize(y);
701 return Solve(model, solver_type, {.parameters = params});
702}
703
704TEST_P(IpParameterTest, AbsoluteGapLimit) {
705 if (GetParam().solver_type == SolverType::kHighs) {
706 GTEST_SKIP()
707 << "This test does not work for HiGHS, we cannot weaken the root node "
708 "bound enough, see https://paste.googleplex.com/6416319233654784";
709 }
710 const int k = 10;
711 const int n = 2;
712 const double lp_opt = k / 2 + 1;
713 const double ip_opt = 1;
714 const double abs_lp_gap = lp_opt - ip_opt;
715 // We will set a target gap that can be validated by lp_opt, but best_bound
716 // may end up being slightly better for some solvers.
717 const double best_bound_differentiator = lp_opt - abs_lp_gap / 2.0;
718
719 // Check that best bound on default solve is close to ip_opt and hence
720 // strictly smaller than best_bound_differentiator.
722 const SolveResult default_result,
723 SolveForGapLimit(k, n, TestedSolver(), SolveParameters{}));
724 EXPECT_EQ(default_result.termination.reason, TerminationReason::kOptimal);
725 EXPECT_LT(default_result.termination.objective_bounds.dual_bound,
726 ip_opt + 0.1);
727 EXPECT_LT(default_result.termination.objective_bounds.dual_bound,
728 best_bound_differentiator);
729
730 // Set target gap slightly larger than abs_lp_gap and check that best bound
731 // is strictly larger than best_bound_differentiator.
732 SolveParameters params = {.absolute_gap_tolerance = abs_lp_gap + 0.1};
733 // Weaken the solver so that we actually have a chance to run the root LP
734 // and terminate early (if we solve the problem in presolve there will be no
735 // gap).
736 if (GetParam().parameter_support.supports_presolve) {
737 params.presolve = Emphasis::kOff;
738 }
739 if (GetParam().parameter_support.supports_one_thread) {
740 params.threads = 1;
741 }
742 if (GetParam().parameter_support.supports_cuts) {
743 params.cuts = Emphasis::kOff;
744 }
745 const absl::StatusOr<SolveResult> gap_tolerance_result =
746 SolveForGapLimit(k, n, TestedSolver(), params);
747 if (!GetParam().parameter_support.supports_absolute_gap_tolerance) {
748 EXPECT_THAT(gap_tolerance_result,
749 StatusIs(absl::StatusCode::kInvalidArgument,
750 HasSubstr("absolute_gap_tolerance")));
751 return;
752 }
753 EXPECT_EQ(gap_tolerance_result->termination.reason,
755
756 // This test is too brittle for CP-SAT, as we cannot get a bound that is just
757 // the LP relaxation and nothing more. This test is already brittle and may
758 // break on solver upgrades.
759 if (TestedSolver() != SolverType::kCpSat) {
760 EXPECT_GT(gap_tolerance_result->termination.objective_bounds.dual_bound,
761 best_bound_differentiator);
762 }
763}
764
765TEST_P(IpParameterTest, RelativeGapLimit) {
766 if (GetParam().solver_type == SolverType::kHighs) {
767 GTEST_SKIP()
768 << "This test does not work for HiGHS, we cannot weaken the root node "
769 "bound enough, see https://paste.googleplex.com/6416319233654784";
770 }
771 if (GetParam().solver_type == SolverType::kGlpk) {
772 GTEST_SKIP()
773 << "This test does not work for GLPK, not clear why this isn't "
774 "working, logs here: https://paste.googleplex.com/6080056622317568";
775 }
776 const int k = 10;
777 const int n = 2;
778 const double lp_opt = k / 2.0 + 1;
779 const double ip_opt = 1;
780 const double abs_lp_gap = lp_opt - ip_opt;
781 const double rel_lp_gap = (lp_opt - ip_opt) / ip_opt;
782 // We will set a target gap that can be validated by lp_opt, but best_bound
783 // may end up being slightly better for some solvers.
784 const double best_bound_differentiator = lp_opt - abs_lp_gap / 2.0;
785
786 // Check that best bound on default solve is close to ip_opt and hence
787 // strictly smaller than best_bound_differentiator.
789 const SolveResult default_result,
790 SolveForGapLimit(k, n, TestedSolver(), SolveParameters()));
791 EXPECT_THAT(default_result, IsOptimal());
792 EXPECT_LT(default_result.termination.objective_bounds.dual_bound,
793 ip_opt + 0.1);
794 EXPECT_LT(default_result.termination.objective_bounds.dual_bound,
795 best_bound_differentiator);
796
797 // Set target gap slightly larger than rel_lp_gap and check that best bound
798 // is strictly larger than best_bound_differentiator.
799 SolveParameters params = {.relative_gap_tolerance = rel_lp_gap + 0.1};
800 // Weaken the solver so that we actually have a chance to run the root LP
801 // and terminate early (if we solve the problem in presolve there will be no
802 // gap).
803 if (GetParam().parameter_support.supports_presolve) {
804 params.presolve = Emphasis::kOff;
805 }
806 if (GetParam().parameter_support.supports_one_thread) {
807 params.threads = 1;
808 }
809 if (GetParam().parameter_support.supports_cuts) {
810 params.cuts = Emphasis::kOff;
811 }
812 ASSERT_OK_AND_ASSIGN(const SolveResult gap_tolerance_result,
813 SolveForGapLimit(k, n, TestedSolver(), params));
814 EXPECT_EQ(gap_tolerance_result.termination.reason,
816
817 // This test is too brittle for CP-SAT, as we cannot get a bound that is just
818 // the LP relaxation and nothing more. This test is already brittle and may
819 // break on solver upgrades.
820 if (TestedSolver() != SolverType::kCpSat) {
821 EXPECT_GT(gap_tolerance_result.termination.objective_bounds.dual_bound,
822 best_bound_differentiator);
823 }
824}
825
826TEST_P(IpParameterTest, CutoffLimit) {
827 Model model;
828 const Variable x = model.AddBinaryVariable();
829 model.Minimize(x);
830 // When the optimal solution is worse than cutoff, no solution information is
831 // returned and we return Limit::kCutoff.
832 const absl::StatusOr<SolveResult> result = Solve(
833 model, GetParam().solver_type, {.parameters = {.cutoff_limit = -1.0}});
834 if (!GetParam().parameter_support.supports_cutoff) {
835 EXPECT_THAT(result, StatusIs(absl::StatusCode::kInvalidArgument,
836 HasSubstr("cutoff")));
837 return;
838 }
841 // When the optimal solution is better than cutoff, the parameter has no
842 // effect on the returned SolveResult (at least for problems with a unique
843 // solution, it may change the nodes visited still) and we return the optimal
844 // solution.
845 EXPECT_THAT(Solve(model, GetParam().solver_type,
846 {.parameters = {.cutoff_limit = 0.5}}),
847 IsOkAndHolds(IsOptimal(0.0)));
848}
849
850TEST_P(IpParameterTest, ObjectiveLimit) {
851 const std::unique_ptr<Model> model = DenseIndependentSet(/*integer=*/true);
852 SolveParameters params = {.objective_limit = 3.5};
853 // If we solve in presolve we don't get a chance to stop early with the
854 // limit.
855 if (GetParam().parameter_support.supports_presolve) {
856 params.presolve = Emphasis::kOff;
857 }
858 {
859 // The model has an optimal solution of 7 which is hard to find, and many
860 // easy to find solutions with objective value 5. Solve with permission to
861 // stop early once an easy solution is found, and verify that we terminate
862 // from the objective limit.
863
864 const absl::StatusOr<SolveResult> result =
865 Solve(*model, GetParam().solver_type, {.parameters = params});
866 if (!GetParam().parameter_support.supports_objective_limit) {
867 EXPECT_THAT(result, StatusIs(absl::StatusCode::kInvalidArgument,
868 HasSubstr("objective_limit")));
869 return;
870 }
872 ASSERT_TRUE(result->has_primal_feasible_solution());
873 EXPECT_LE(result->objective_value(), 5.0 + 1.0e-5);
874 }
875 // Resolve the same model with objective limit 20. Since the true objective
876 // is 7, we will just solve to optimality.
877 params.objective_limit = 20.0;
878 EXPECT_THAT(Solve(*model, GetParam().solver_type, {.parameters = params}),
880}
881
882TEST_P(IpParameterTest, BestBoundLimit) {
883 const std::unique_ptr<Model> model = DenseIndependentSet(/*integer=*/true);
884 // The model has an LP relaxation of 60 and an optimal solution of 7.
885 // Solve with permission to stop early, when the best bound is equal to 60.
886 // Check the termination reason, and that the bound is indeed worse than
887 // optimal.
888 SolveParameters params = {.best_bound_limit = 60.0};
889 // If we solve in presolve we don't get a chance to stop early with the
890 // limit.
891 if (GetParam().parameter_support.supports_presolve) {
892 params.presolve = Emphasis::kOff;
893 }
894 {
895 const absl::StatusOr<SolveResult> result =
896 Solve(*model, GetParam().solver_type, {.parameters = params});
897 if (!GetParam().parameter_support.supports_bound_limit) {
898 EXPECT_THAT(result, StatusIs(absl::StatusCode::kInvalidArgument,
899 HasSubstr("best_bound_limit")));
900 return;
901 }
903 EXPECT_LE(result->termination.objective_bounds.dual_bound, 60.0);
904 EXPECT_GE(result->termination.objective_bounds.dual_bound, 8.0);
905 }
906 // Solve again but now with permission to stop only when the bound is 4 or
907 // smaller. Since the optimal solution is 7, we will just solve to optimality.
908 params.best_bound_limit = 4.0;
909 EXPECT_THAT(Solve(*model, GetParam().solver_type, {.parameters = params}),
910 IsOkAndHolds(IsOptimal(7.0)));
911}
912
913TEST_P(IpParameterTest, SolutionLimitOneWithHint) {
914 if (!GetParam().hint_supported) {
915 GTEST_SKIP() << "Test requires a hint";
916 }
917 Model model;
918 const Variable x = model.AddBinaryVariable();
919 model.Minimize(x);
920 ModelSolveParameters model_params;
921 model_params.solution_hints.push_back({.variable_values = {{x, 1.0}}});
922 SolveParameters params = {.solution_limit = 1};
923 if (GetParam().parameter_support.supports_presolve) {
924 params.presolve = Emphasis::kOff;
925 }
926 // SCIP fails to stop based on the hinted solution, runs the "trivial
927 // heuristic" and finds a better solution, then returns limit feasible with
928 // the wrong solution, unless heuristics are disabled.
929 if (GetParam().parameter_support.supports_heuristics) {
930 params.heuristics = Emphasis::kOff;
931 }
932 const absl::StatusOr<SolveResult> result =
933 Solve(model, GetParam().solver_type,
934 {.parameters = params, .model_parameters = model_params});
935 if (!GetParam().parameter_support.supports_solution_limit_one) {
936 EXPECT_THAT(result, StatusIs(absl::StatusCode::kInvalidArgument,
937 HasSubstr("solution_limit")));
938 return;
939 }
942 /*allow_limit_undetermined=*/!GetParam()
943 .solve_result_support.termination_limit)));
944 EXPECT_EQ(result->solutions.size(), 1);
945 EXPECT_THAT(*result, HasSolution(PrimalSolution{
946 .variable_values = {{x, 1.0}},
947 .objective_value = 1.0,
948 .feasibility_status = SolutionStatus::kFeasible}));
949}
950
951TEST_P(IpParameterTest, SolutionLimitOneAndCutoff) {
952 if (!(GetParam().parameter_support.supports_cutoff &&
953 GetParam().parameter_support.supports_solution_limit_one)) {
954 // We have already tested when these parameters are unsupported.
955 return;
956 }
957 if (!GetParam().hint_supported) {
958 GTEST_SKIP() << "Test requires a hint";
959 }
960 Model model;
961 const Variable x = model.AddBinaryVariable();
962 const Variable y = model.AddBinaryVariable();
963 const Variable z = model.AddBinaryVariable();
964 model.Maximize(x + 2 * y + 3 * z);
965 model.AddLinearConstraint(x + y + z <= 1);
966
967 // Exclude (0, 0, 0) and (1, 0, 0) with cutoff = 1.5.
968 // Hint (1, 0, 0) and (0, 1, 0).
969 // Set a solution limit of 1. The first hint should be ignored, and the second
970 // suboptimal hint should be returned.
971 //
972 // NOTE: CP-SAT only allows one hint (the first one suggested). We put hint
973 // (0, 1, 0) first so the test still passes, but we are not testing as much.
974 SolveParameters params = {.cutoff_limit = 1.5, .solution_limit = 1};
975 if (GetParam().parameter_support.supports_presolve) {
976 params.presolve = Emphasis::kOff;
977 }
978 // Not 100% clear why this is needed, but CP-SAT will sometimes return
979 // a solution better than the hint without this.
980 if (GetParam().parameter_support.supports_one_thread) {
981 params.threads = 1;
982 }
983 ModelSolveParameters model_params;
984 model_params.solution_hints.push_back(ModelSolveParameters::SolutionHint{
985 .variable_values = {{x, 0.0}, {y, 1.0}, {z, 0.0}}});
986 model_params.solution_hints.push_back(ModelSolveParameters::SolutionHint{
987 .variable_values = {{x, 1.0}, {y, 0.0}, {z, 0.0}}});
989 const SolveResult result,
990 Solve(model, GetParam().solver_type,
991 {.parameters = params, .model_parameters = model_params}));
994 /*allow_limit_undetermined=*/!GetParam()
995 .solve_result_support.termination_limit));
996 ASSERT_TRUE(result.has_primal_feasible_solution());
997 EXPECT_NEAR(result.objective_value(), 2.0, 1e-5);
998 EXPECT_EQ(result.solutions.size(), 1);
999}
1000
1001// Tests the interaction between cutoff and an additional limit.
1002TEST_P(IpParameterTest, NoSolutionsBelowCutoffEarlyTermination) {
1003 if (GetParam().solver_type == SolverType::kGscip) {
1004 GTEST_SKIP() << "This test does not work with SCIP v900";
1005 }
1006 if (!(GetParam().parameter_support.supports_cutoff)) {
1007 // We have already tested that the right error message is returned.
1008 return;
1009 }
1010 if (!GetParam().hint_supported) {
1011 GTEST_SKIP() << "Test requires a hint";
1012 }
1013 const std::unique_ptr<const Model> model = DenseIndependentSet(true);
1014 ModelSolveParameters model_params;
1015 model_params.solution_hints.push_back(DenseIndependentSetHint5(*model));
1016 SolveParameters params = GetParam().stop_before_optimal;
1017 params.cutoff_limit = 6.5;
1019 const SolveResult result,
1020 Solve(*model, GetParam().solver_type,
1021 {.parameters = params, .model_parameters = model_params}));
1022 // There is a solution with objective 7, but it is hard to find.
1023 // NOTE: if this becomes flaky, we can increase to cutoff to 7.5.
1025}
1026
1027} // namespace
1028
1030// LargeInstanceIpParameterTest
1032
1033namespace {
1034
1035absl::StatusOr<std::unique_ptr<Model>> LoadMiplibInstance(
1036 absl::string_view name) {
1038 const ModelProto model_proto,
1039 ReadMpsFile(absl::StrCat("ortools/math_opt/solver_tests/testdata/", name,
1040 ".mps")));
1041 return Model::FromModelProto(model_proto);
1042}
1043
1044}; // namespace
1045
1046absl::StatusOr<std::unique_ptr<Model>>
1048 return LoadMiplibInstance("23588");
1050
1051absl::StatusOr<std::unique_ptr<Model>>
1053 return LoadMiplibInstance("beavma");
1055
1056namespace {
1057
1058TEST_P(LargeInstanceIpParameterTest, SolvesInstanceNoLimits) {
1059 if (DEBUG_MODE || kAnyXsanEnabled) {
1060 GTEST_SKIP() << "Test skipped, too slow unless compiled with -c opt.";
1061 }
1062 ASSERT_OK_AND_ASSIGN(std::unique_ptr<Model> model, Load23588());
1063 const SolveParameters params = GetParam().base_parameters;
1064 EXPECT_THAT(Solve(*model, GetParam().solver_type, {.parameters = params}),
1065 IsOkAndHolds(IsOptimal(kOptimalObjective)));
1066}
1067
1068TEST_P(LargeInstanceIpParameterTest, TimeLimit) {
1069 ASSERT_OK_AND_ASSIGN(std::unique_ptr<Model> model, Load23588());
1070 SolveParameters params = GetParam().base_parameters;
1071 params.time_limit = absl::Milliseconds(1);
1073 const SolveResult result,
1074 Solve(*model, GetParam().solver_type, {.parameters = params}));
1076 GetParam().allow_limit_undetermined));
1077 // Solvers do not stop very precisely, use a large number to avoid flaky
1078 // tests. Do NOT try to fine tune this to be small, it is hard to get right
1079 // for all compilation modes (e.g. debug, asan).
1080 EXPECT_LE(result.solve_stats.solve_time, absl::Seconds(1));
1081}
1082
1083TEST_P(LargeInstanceIpParameterTest, IterationLimit) {
1084 ASSERT_OK_AND_ASSIGN(std::unique_ptr<Model> model, Load23588());
1085 SolveParameters params = GetParam().base_parameters;
1086 params.iteration_limit = 1;
1087 const absl::StatusOr<SolveResult> result =
1088 Solve(*model, GetParam().solver_type, {.parameters = params});
1089 if (!GetParam().parameter_support.supports_iteration_limit) {
1090 EXPECT_THAT(result, StatusIs(absl::StatusCode::kInvalidArgument,
1091 HasSubstr("iteration_limit")));
1092 return;
1093 }
1094 ASSERT_THAT(result,
1096 Limit::kIteration, GetParam().allow_limit_undetermined)));
1097 EXPECT_LE(result->solve_stats.simplex_iterations, 1);
1098 EXPECT_LE(result->solve_stats.barrier_iterations, 1);
1099}
1100
1101TEST_P(LargeInstanceIpParameterTest, NodeLimit) {
1102 if (GetParam().solver_type == SolverType::kHighs) {
1103 GTEST_SKIP() << "Ignoring this test as Highs 1.7+ returns unimplemented";
1104 }
1105 ASSERT_OK_AND_ASSIGN(std::unique_ptr<Model> model, Load23588());
1106 SolveParameters params = GetParam().base_parameters;
1107 params.node_limit = 1;
1108 const absl::StatusOr<SolveResult> result =
1109 Solve(*model, GetParam().solver_type, {.parameters = params});
1110 if (!GetParam().parameter_support.supports_node_limit) {
1111 EXPECT_THAT(result, StatusIs(absl::StatusCode::kInvalidArgument,
1112 HasSubstr("node_limit")));
1113 return;
1114 }
1116 Limit::kNode, GetParam().allow_limit_undetermined)));
1117 EXPECT_LE(result->solve_stats.node_count, 1);
1118}
1119
1120TEST_P(LargeInstanceIpParameterTest, CutoffLimit) {
1121 ASSERT_OK_AND_ASSIGN(std::unique_ptr<Model> model, Load23588());
1122 SolveParameters params = GetParam().base_parameters;
1123 // 23588.mps is minimization, set the cutoff below the optimal solution so
1124 // that no solutions are found.
1125 params.cutoff_limit = kOptimalObjective - 10.0;
1126 const absl::StatusOr<SolveResult> result =
1127 Solve(*model, GetParam().solver_type, {.parameters = params});
1128 if (!GetParam().parameter_support.supports_cutoff) {
1129 EXPECT_THAT(result, StatusIs(absl::StatusCode::kInvalidArgument,
1130 HasSubstr("cutoff_limit")));
1131 return;
1132 }
1133 ASSERT_THAT(result,
1135 Limit::kCutoff, GetParam().allow_limit_undetermined)));
1136 // All solutions are worse than the cutoff value
1137 EXPECT_FALSE(result->has_primal_feasible_solution());
1138
1139 // Solve again with a cutoff above the optimal solution, make sure we get the
1140 // optimal solution back.
1141 //
1142 // This requires a full solve, which is slow in debug/asan.
1143 if (DEBUG_MODE || kAnyXsanEnabled) {
1144 return;
1145 }
1146}
1147
1148TEST_P(LargeInstanceIpParameterTest, ObjectiveLimit) {
1149 ASSERT_OK_AND_ASSIGN(std::unique_ptr<Model> model, Load23588());
1150 SolveParameters params = GetParam().base_parameters;
1151 // 23588.mps is minimization, set the objective limit above the optimal
1152 // solution so we terminate early.
1153 params.objective_limit = 1.5 * kOptimalObjective;
1154 const absl::StatusOr<SolveResult> result =
1155 Solve(*model, GetParam().solver_type, {.parameters = params});
1156 if (!GetParam().parameter_support.supports_objective_limit) {
1157 EXPECT_THAT(result, StatusIs(absl::StatusCode::kInvalidArgument,
1158 HasSubstr("objective_limit")));
1159 return;
1160 }
1161
1162 // This assertion is a bit fragile, the solver could prove optimality.
1163 ASSERT_THAT(result,
1165 Limit::kObjective, GetParam().allow_limit_undetermined)));
1166 // The objective value should be in the interval:
1167 // [kOptimalObjective, params.objective_limit].
1168 EXPECT_LE(result->objective_value(), params.objective_limit);
1169 // This assertion is fragile, the solver could find an optimal solution, but
1170 // we want to ensure that the objective limit is actually making us stop
1171 // early.
1172 EXPECT_GE(result->objective_value(), kOptimalObjective + 1.0);
1173
1174 // Solve again with an objective limit below the optimal solution, make sure
1175 // we get the optimal solution back.
1176 //
1177 // This requires a full solve, which is slow in debug/asan.
1178 if (DEBUG_MODE || kAnyXsanEnabled) {
1179 return;
1180 }
1181 params.objective_limit = kOptimalObjective - 10.0;
1182 EXPECT_THAT(Solve(*model, GetParam().solver_type, {.parameters = params}),
1183 IsOkAndHolds(IsOptimal(kOptimalObjective)));
1184}
1185
1186TEST_P(LargeInstanceIpParameterTest, BestBoundLimit) {
1187 ASSERT_OK_AND_ASSIGN(std::unique_ptr<Model> model, Load23588());
1188 SolveParameters params = GetParam().base_parameters;
1189 params.best_bound_limit = kLpRelaxationObjective - 1.0;
1190 const absl::StatusOr<SolveResult> result =
1191 Solve(*model, GetParam().solver_type, {.parameters = params});
1192 if (!GetParam().parameter_support.supports_bound_limit) {
1193 EXPECT_THAT(result, StatusIs(absl::StatusCode::kInvalidArgument,
1194 HasSubstr("best_bound_limit")));
1195 return;
1196 }
1197 // This assertion is a bit fragile, the solver could prove optimality.
1198 ASSERT_THAT(result,
1200 Limit::kObjective, GetParam().allow_limit_undetermined)));
1201 // Since we should get a bound at least as strong as the LP relaxation at
1202 // the root node
1203 EXPECT_LE(result->solve_stats.node_count, 1);
1204 // The objective value should be in the interval:
1205 // [params.best_bound_limit, kOptimalObjective].
1206 EXPECT_GE(result->best_objective_bound(), params.best_bound_limit);
1207 // This assertion is fragile, the solver could prove optimality, but
1208 // we want to ensure that the bound limit is actually making us stop early.
1209 EXPECT_GE(result->termination.objective_bounds.primal_bound,
1210 kOptimalObjective - 1.0);
1211
1212 // Solve again with a bound limit above the optimal solution, make sure we
1213 // get the optimal solution back.
1214 //
1215 // This requires a full solve, which is slow in debug/asan.
1216 if (DEBUG_MODE || kAnyXsanEnabled) {
1217 return;
1218 }
1219 params.best_bound_limit = kOptimalObjective + 10.0;
1220 EXPECT_THAT(Solve(*model, GetParam().solver_type, {.parameters = params}),
1221 IsOkAndHolds(IsOptimal(kOptimalObjective)));
1222}
1223
1224TEST_P(LargeInstanceIpParameterTest, SolutionLimit) {
1225 if (GetParam().solver_type == SolverType::kHighs) {
1226 GTEST_SKIP() << "Ignoring this test as Highs 1.7+ returns unimplemented";
1227 }
1228 ASSERT_OK_AND_ASSIGN(std::unique_ptr<Model> model, Load23588());
1229 SolveParameters params = GetParam().base_parameters;
1230 params.solution_limit = 1;
1231 const absl::StatusOr<SolveResult> result =
1232 Solve(*model, GetParam().solver_type, {.parameters = params});
1233 if (!GetParam().parameter_support.supports_solution_limit_one) {
1234 EXPECT_THAT(result, StatusIs(absl::StatusCode::kInvalidArgument,
1235 HasSubstr("solution_limit")));
1236 return;
1237 }
1238 // This assertion is a bit fragile, the solver could prove optimality.
1239 ASSERT_THAT(result,
1241 Limit::kSolution, GetParam().allow_limit_undetermined)));
1242 // This test is a bit fragile, but typically we cannot prove optimality at
1243 // the time of first feasible solution (note that CP-SATs first primal
1244 // solution is optimal roughly 1/100 solves).
1245 EXPECT_GE(result->objective_value() - result->best_objective_bound(), 1.0);
1246}
1247
1248// Set the absolute gap to the difference between the optimal objective
1249// and the root LP (~441), and check that there is at least a gap of ~10
1250// between the objective and best bound at termination.
1251//
1252// The root LP should bring us within the gap. Do NOT assert that there
1253// is at most one node as:
1254// * There may be multiple nodes due to restarts.
1255// * There is no guarantee (Without hints) that we find a good primal
1256// solution at the root.
1257TEST_P(LargeInstanceIpParameterTest, AbsoluteGapTolerance) {
1258 ASSERT_OK_AND_ASSIGN(std::unique_ptr<Model> model, Load23588());
1259 SolveParameters params = GetParam().base_parameters;
1260 const double absolute_lp_relax_gap =
1261 kOptimalObjective - kLpRelaxationObjective;
1262 params.absolute_gap_tolerance = absolute_lp_relax_gap;
1263 const absl::StatusOr<SolveResult> result =
1264 Solve(*model, GetParam().solver_type, {.parameters = params});
1265 if (!GetParam().parameter_support.supports_absolute_gap_tolerance) {
1266 EXPECT_THAT(result, StatusIs(absl::StatusCode::kInvalidArgument,
1267 HasSubstr("absolute_gap_tolerance")));
1268 return;
1269 }
1271 // There should be some space between our optimal solution and best bound
1272 EXPECT_GE(result->termination.objective_bounds.primal_bound -
1273 result->termination.objective_bounds.dual_bound,
1274 absolute_lp_relax_gap / 40.0);
1275}
1276
1277// Set the relative gap to 2*(8090 - 7649)/8090 ~= 0.1 and check there is
1278// a gap of at least ~10 between the objective and best bound at termination.
1279//
1280// The root LP should bring us within the gap, but not assert on the node count,
1281// see above.
1282TEST_P(LargeInstanceIpParameterTest, RelativeGapTolerance) {
1283 ASSERT_OK_AND_ASSIGN(std::unique_ptr<Model> model, Load23588());
1284 SolveParameters params = GetParam().base_parameters;
1285 const double absolute_lp_relax_gap =
1286 kOptimalObjective - kLpRelaxationObjective;
1287 params.relative_gap_tolerance = 2 * absolute_lp_relax_gap / kOptimalObjective;
1288 const absl::StatusOr<SolveResult> result =
1289 Solve(*model, GetParam().solver_type, {.parameters = params});
1290
1292 // The root LP should bring us within the gap. Do NOT assert that there
1293 // is at most one node, there may be multiple due to restarts.
1294 // There should be some space between our optimal solution and best bound
1295 EXPECT_GE(result->termination.objective_bounds.primal_bound -
1296 result->termination.objective_bounds.dual_bound,
1297 absolute_lp_relax_gap / 40.0);
1298}
1299
1300TEST_P(LargeInstanceIpParameterTest, Cuts) {
1301 if (!GetParam().parameter_support.supports_node_limit) {
1302 GTEST_SKIP() << "Skipping test, requires node_limit but is not supported.";
1303 }
1304 ASSERT_OK_AND_ASSIGN(std::unique_ptr<Model> model, LoadBeavma());
1305 SolveParameters params = GetParam().base_parameters;
1306 // Run only the root node so we can compare the bound quality with and without
1307 // cuts on by checking the best bound on the SolveResult.
1308 params.node_limit = 1;
1309 params.cuts = Emphasis::kOff;
1310 const absl::StatusOr<SolveResult> result_cuts_off =
1311 Solve(*model, GetParam().solver_type, {.parameters = params});
1312 if (!GetParam().parameter_support.supports_cuts) {
1313 EXPECT_THAT(result_cuts_off, StatusIs(absl::StatusCode::kInvalidArgument,
1314 HasSubstr("cuts")));
1315 return;
1316 }
1317 ASSERT_OK(result_cuts_off);
1318 const double bound_cuts_off = result_cuts_off->best_objective_bound();
1319
1320 params.cuts = Emphasis::kMedium;
1322 const SolveResult result_cuts_on,
1323 Solve(*model, GetParam().solver_type, {.parameters = params}));
1324 const double bound_cuts_on = result_cuts_on.best_objective_bound();
1325
1326 // Problem is minimization, so a larger bound is better. Using cuts should
1327 // improve the bound.
1328 EXPECT_GE(bound_cuts_on, bound_cuts_off + 1.0);
1329}
1330
1331} // namespace
1332
1333} // namespace math_opt
1334} // namespace operations_research
IntegerValue y
#define ASSIGN_OR_RETURN(lhs, rexpr)
#define RETURN_IF_ERROR(expr)
absl::StatusOr< std::unique_ptr< Model > > LoadBeavma()
absl::StatusOr< std::unique_ptr< Model > > Load23588()
static absl::StatusOr< std::unique_ptr< Model > > FromModelProto(const ModelProto &model_proto)
Definition model.cc:53
const std::string name
A name for logging purposes.
GRBmodel * model
const bool DEBUG_MODE
Definition macros.h:24
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
@ kOptimal
A provably optimal solution (up to numerical tolerances) has been found.
<=x<=1 IncrementalMipTest::IncrementalMipTest() :model_("incremental_solve_test"), x_(model_.AddContinuousVariable(0.0, 1.0, "x")), y_(model_.AddIntegerVariable(0.0, 2.0, "y")), c_(model_.AddLinearConstraint(0<=x_+y_<=1.5, "c")) { model_.Maximize(3.0 *x_+2.0 *y_+0.1);solver_=NewIncrementalSolver(&model_, TestedSolver()).value();const SolveResult first_solve=solver_->Solve().value();CHECK(first_solve.has_primal_feasible_solution());CHECK_LE(std::abs(first_solve.objective_value() - 3.6), kTolerance)<< first_solve.objective_value();} namespace { TEST_P(SimpleMipTest, OneVarMax) { Model model;const Variable x=model.AddVariable(0.0, 4.0, false, "x");model.Maximize(2.0 *x);ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(model, GetParam().solver_type));ASSERT_THAT(result, IsOptimal(8.0));EXPECT_THAT(result.variable_values(), IsNear({{x, 4.0}}));} TEST_P(SimpleMipTest, OneVarMin) { Model model;const Variable x=model.AddVariable(-2.4, 4.0, false, "x");model.Minimize(2.0 *x);ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(model, GetParam().solver_type));ASSERT_THAT(result, IsOptimal(-4.8));EXPECT_THAT(result.variable_values(), IsNear({{x, -2.4}}));} TEST_P(SimpleMipTest, OneIntegerVar) { Model model;const Variable x=model.AddVariable(0.0, 4.5, true, "x");model.Maximize(2.0 *x);ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(model, GetParam().solver_type));ASSERT_THAT(result, IsOptimal(8.0));EXPECT_THAT(result.variable_values(), IsNear({{x, 4.0}}));} TEST_P(SimpleMipTest, SimpleLinearConstraint) { Model model;const Variable x=model.AddBinaryVariable("x");const Variable y=model.AddBinaryVariable("y");model.Maximize(2.0 *x+y);model.AddLinearConstraint(0.0<=x+y<=1.5, "c");ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(model, GetParam().solver_type));ASSERT_THAT(result, IsOptimal(2.0));EXPECT_THAT(result.variable_values(), IsNear({{x, 1}, {y, 0}}));} TEST_P(SimpleMipTest, Unbounded) { Model model;const Variable x=model.AddVariable(0.0, kInf, true, "x");model.Maximize(2.0 *x);ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(model, GetParam().solver_type));if(GetParam().report_unboundness_correctly) { ASSERT_THAT(result, TerminatesWithOneOf({TerminationReason::kUnbounded, TerminationReason::kInfeasibleOrUnbounded}));} else { ASSERT_THAT(result, TerminatesWith(TerminationReason::kOtherError));} } TEST_P(SimpleMipTest, Infeasible) { Model model;const Variable x=model.AddVariable(0.0, 3.0, true, "x");model.Maximize(2.0 *x);model.AddLinearConstraint(x >=4.0);ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(model, GetParam().solver_type));ASSERT_THAT(result, TerminatesWith(TerminationReason::kInfeasible));} TEST_P(SimpleMipTest, FractionalBoundsContainNoInteger) { if(GetParam().solver_type==SolverType::kGurobi) { GTEST_SKIP()<< "TODO(b/272298816): Gurobi bindings are broken here.";} Model model;const Variable x=model.AddIntegerVariable(0.5, 0.6, "x");model.Maximize(x);EXPECT_THAT(Solve(model, GetParam().solver_type), IsOkAndHolds(TerminatesWith(TerminationReason::kInfeasible)));} TEST_P(IncrementalMipTest, EmptyUpdate) { ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));ASSERT_OK_AND_ASSIGN(const SolveResult result, solver_->SolveWithoutUpdate());ASSERT_THAT(result, IsOptimal(3.6));EXPECT_THAT(result.variable_values(), IsNear({{x_, 0.5}, {y_, 1.0}}));} TEST_P(IncrementalMipTest, MakeContinuous) { model_.set_continuous(y_);ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));ASSERT_OK_AND_ASSIGN(const SolveResult result, solver_->SolveWithoutUpdate());ASSERT_THAT(result, IsOptimal(4.1));EXPECT_THAT(result.variable_values(), IsNear({{x_, 1.0}, {y_, 0.5}}));} TEST_P(IncrementalMipTest, DISABLED_MakeContinuousWithNonIntegralBounds) { solver_.reset();Model model("bounds");const Variable x=model.AddIntegerVariable(0.5, 1.5, "x");model.Maximize(x);ASSERT_OK_AND_ASSIGN(const auto solver, NewIncrementalSolver(&model, TestedSolver()));ASSERT_THAT(solver->Solve(), IsOkAndHolds(IsOptimal(1.0)));model.set_continuous(x);ASSERT_THAT(solver->Update(), IsOkAndHolds(DidUpdate()));ASSERT_THAT(solver-> IsOkAndHolds(IsOptimal(1.5)))
ASSERT_THAT(solver->Update(), IsOkAndHolds(DidUpdate()))
LPAlgorithm
Selects an algorithm for solving linear programs.
Definition parameters.h:128
When the underlying default is used When the feature cannot be turned kOff will return an error If the feature is enabled by the solver default is typically If the feature is supported
mapped to kMedium.
Definition parameters.h:177
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
std::ostream & operator<<(std::ostream &ostr, const IndicatorConstraint &constraint)
Matcher< SolveResult > HasSolution(PrimalSolution expected, const double tolerance)
SolveResult has a primal solution matching expected within tolerance.
Definition matchers.cc:823
@ kFeasible
Solver claims the solution is feasible.
testing::Matcher< SolveResult > TerminatesWithReasonFeasible(const Limit expected, const bool allow_limit_undetermined)
Definition matchers.cc:657
Matcher< SolveResult > TerminatesWith(const TerminationReason expected)
Definition matchers.cc:621
MessageCallback PrinterMessageCallback(std::ostream &output_stream, const absl::string_view prefix)
absl::StatusOr< ModelProto > ReadMpsFile(const absl::string_view filename)
testing::Matcher< SolveResult > TerminatesWithLimit(const Limit expected, const bool allow_limit_undetermined)
Definition matchers.cc:648
@ kTime
The algorithm stopped after a user-specified computation time.
std::unique_ptr< Model > DenseIndependentSet(const bool integer, const int n)
absl::string_view EnumToString(E value)
Definition enums.h:289
ModelSolveParameters::SolutionHint DenseIndependentSetHint5(const Model &model)
Matcher< SolveResult > IsOptimal(const std::optional< double > expected_primal_objective, const double tolerance)
Definition matchers.cc:762
testing::Matcher< SolveResult > TerminatesWithReasonNoSolutionFound(const Limit expected, const bool allow_limit_undetermined)
Definition matchers.cc:665
BoolVar Not(BoolVar x)
Definition cp_model.cc:87
In SWIG mode, we don't want anything besides these top-level includes.
std::string ProtobufShortDebugString(const P &message)
Definition proto_utils.h:41
constexpr bool kAnyXsanEnabled
internal::StatusIsMatcher StatusIs(CodeMatcher code_matcher, MessageMatcher message_matcher)
StatusBuilder InternalErrorBuilder()
#define ASSERT_OK(expression)
#define ASSERT_OK_AND_ASSIGN(lhs, rexpr)
bool supports_n_threads
Supports setting threads to an arbitrary value.
bool supports_one_thread
Supports setting threads = 1 (all but HiGHS support this).
bool supports_solution_limit_one
Indicates if setting solution_limit with value 1 is supported.
bool node_count
If SolveStats reports the node count.
bool iteration_stats
If SolveStats reports iteration stats for LP/IPM/FOM.