Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
status_tests.cc
Go to the documentation of this file.
1// Copyright 2010-2024 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
15
16#include <limits>
17#include <memory>
18#include <optional>
19#include <ostream>
20#include <vector>
21
22#include "absl/status/statusor.h"
23#include "absl/strings/str_cat.h"
24#include "absl/strings/string_view.h"
25#include "absl/time/time.h"
26#include "gtest/gtest.h"
27#include "ortools/base/gmock.h"
34
36
37using ::testing::AnyOf;
38using ::testing::Field;
39using ::testing::Matcher;
40
41constexpr double kInf = std::numeric_limits<double>::infinity();
42
43std::ostream& operator<<(std::ostream& out,
44 const StatusTestParameters& params) {
45 out << "{ solver_type: " << params.solver_type
46 << ", parameters: " << ProtobufShortDebugString(params.parameters.Proto())
47 << ", disallow_primal_or_dual_infeasible: "
48 << (params.disallow_primal_or_dual_infeasible ? "true" : "false")
49 << ", supports_iteration_limit: "
50 << (params.supports_iteration_limit ? "true" : "false")
51 << ", use_integer_variables: "
52 << (params.use_integer_variables ? "true" : "false")
53 << ", supports_node_limit: "
54 << (params.supports_node_limit ? "true" : "false")
55 << ", support_interrupter: "
56 << (params.support_interrupter ? "true" : "false")
57 << ", supports_one_thread: "
58 << (params.supports_one_thread ? "true" : "false") << " }";
59 return out;
60}
61
62namespace {
63
64absl::StatusOr<std::unique_ptr<Model>> LoadMiplibInstance(
65 absl::string_view name) {
67 const ModelProto model_proto,
68 ReadMpsFile(absl::StrCat("ortools/math_opt/solver_tests/testdata/", name,
69 ".mps")));
70 return Model::FromModelProto(model_proto);
71}
72
73absl::StatusOr<std::unique_ptr<Model>> Load23588() {
74 return LoadMiplibInstance("23588");
75}
76
77Matcher<ProblemStatus> PrimalStatusIs(FeasibilityStatus expected) {
78 return Field("primal_status", &ProblemStatus::primal_status, expected);
79}
80Matcher<ProblemStatus> DualStatusIs(FeasibilityStatus expected) {
81 return Field("dual_status", &ProblemStatus::dual_status, expected);
82}
83Matcher<ProblemStatus> StatusIsPrimalOrDualInfeasible() {
84 return Field("primal_or_dual_infeasible ",
86}
87
88TEST_P(StatusTest, EmptyModel) {
89 const Model model;
90 ASSERT_OK_AND_ASSIGN(const SolveResult result, SimpleSolve(model));
91 EXPECT_THAT(result, IsOptimal());
92 // Result validators imply primal and dual problem statuses are kFeasible.
93}
94
95TEST_P(StatusTest, PrimalAndDualInfeasible) {
96 if (GetParam().use_integer_variables &&
97 GetParam().solver_type == SolverType::kGlpk) {
98 GTEST_SKIP()
99 << "Ignoring this test as GLPK gets stuck in presolve for IP's "
100 "with a primal-dual infeasible LP relaxation.";
101 }
102 if (GetParam().solver_type == SolverType::kHighs) {
103 GTEST_SKIP() << "Ignoring this test as Highs 1.7+ returns error.";
104 }
105
106 Model model;
107 const Variable x1 =
108 model.AddVariable(0, kInf, GetParam().use_integer_variables, "x1");
109 const Variable x2 =
110 model.AddVariable(0, kInf, GetParam().use_integer_variables, "x2");
111
112 model.Maximize(2 * x1 - x2);
113 model.AddLinearConstraint(x1 - x2 <= 1, "c1");
114 model.AddLinearConstraint(x1 - x2 >= 2, "c2");
115 ASSERT_OK_AND_ASSIGN(const SolveResult result, SimpleSolve(model));
116
117 // Baseline reason and status checks.
118 EXPECT_THAT(result,
121 EXPECT_THAT(result.termination.problem_status,
122 AnyOf(PrimalStatusIs(FeasibilityStatus::kInfeasible),
123 DualStatusIs(FeasibilityStatus::kInfeasible),
124 StatusIsPrimalOrDualInfeasible()));
125
126 // More detailed reason and status checks.
127 if (GetParam().disallow_primal_or_dual_infeasible) {
128 // Solver may only detect the dual infeasibility so we cannot guatantee
129 // TerminationReason::kInfeasible (dual infeasibility is one of cases in
130 // kInfeasibleOrUnbounded go/mathopt-termination-and-statuses#inf-or-unb).
131 // However, the status check can be refined.
132 EXPECT_THAT(result.termination.problem_status,
133 AnyOf(PrimalStatusIs(FeasibilityStatus::kInfeasible),
134 DualStatusIs(FeasibilityStatus::kInfeasible)));
135 }
136
137 // Even more detailed reason and status checks for primal simplex.
138 if (GetParam().disallow_primal_or_dual_infeasible &&
139 GetParam().parameters.lp_algorithm == LPAlgorithm::kPrimalSimplex) {
141 // Result validators imply primal problem status is infeasible.
142 EXPECT_THAT(result.termination.problem_status,
143 Not(DualStatusIs(FeasibilityStatus::kFeasible)));
144 }
145}
146
147TEST_P(StatusTest, PrimalFeasibleAndDualInfeasible) {
148 if (GetParam().solver_type == SolverType::kCpSat) {
149 GTEST_SKIP() << "Ignoring this test as CpSat bounds all variables";
150 }
151
152 Model model;
153 const Variable x1 =
154 model.AddVariable(0, kInf, GetParam().use_integer_variables, "x1");
155 const Variable x2 =
156 model.AddVariable(0, kInf, GetParam().use_integer_variables, "x2");
157 model.Maximize(x1 + x2);
158 // When there is a unique (up to scaling) primal ray SCIP gets stuck (possibly
159 // having trouble scaling the ray to be integer?)
160 if (GetParam().solver_type == SolverType::kGscip) {
161 model.AddLinearConstraint(100 <= x1 - 2 * x2, "c1");
162 } else {
163 model.AddLinearConstraint(100 <= x1 - 2 * x2 <= 200, "c1");
164 }
165 ASSERT_OK_AND_ASSIGN(const SolveResult result, SimpleSolve(model));
166
167 // Baseline reason and status checks.
168 EXPECT_THAT(result,
171 EXPECT_THAT(result.termination.problem_status,
172 Not(PrimalStatusIs(FeasibilityStatus::kInfeasible)));
173 EXPECT_THAT(result.termination.problem_status,
174 AnyOf(DualStatusIs(FeasibilityStatus::kInfeasible),
175 StatusIsPrimalOrDualInfeasible()));
176
177 // More detailed reason and status checks.
178 if (GetParam().disallow_primal_or_dual_infeasible) {
179 // Solver may only detect the dual infeasibility so we cannot guatantee
180 // TerminationReason::kInfeasible (dual infeasibility is one of cases in
181 // kInfeasibleOrUnbounded go/mathopt-termination-and-statuses#inf-or-unb).
182 // However, the dual status check can be refined.
183 EXPECT_EQ(result.termination.problem_status.dual_status,
185 }
186
187 // Even more detailed reason and status checks for pure primal simplex.
188 if (GetParam().parameters.lp_algorithm == LPAlgorithm::kPrimalSimplex &&
189 GetParam().parameters.presolve == Emphasis::kOff) {
190 // For pure primal simplex we expect to have a primal feasible solution.
192 // Result validators imply primal status is kFeasible and dual problem
193 // statuse is kInfeasible.
194 }
195}
196
197TEST_P(StatusTest, PrimalInfeasibleAndDualFeasible) {
198 Model model;
199 const Variable x1 =
200 model.AddVariable(0, kInf, GetParam().use_integer_variables, "x1");
201 const Variable x2 =
202 model.AddVariable(0, kInf, GetParam().use_integer_variables, "x2");
203 model.Minimize(x1 + x2);
204 model.AddLinearConstraint(x1 + x2 <= -1, "c1");
205 ASSERT_OK_AND_ASSIGN(const SolveResult result, SimpleSolve(model));
206
207 // Baseline reason and status checks.
208 EXPECT_THAT(result,
211 EXPECT_THAT(result.termination.problem_status,
212 AnyOf(PrimalStatusIs(FeasibilityStatus::kInfeasible),
213 StatusIsPrimalOrDualInfeasible()));
214 EXPECT_THAT(result.termination.problem_status,
215 Not(DualStatusIs(FeasibilityStatus::kInfeasible)));
216
217 // More detailed reason and status checks.
218 if (GetParam().disallow_primal_or_dual_infeasible) {
220 // Result validators imply primal status is kInfeasible.
221 }
222
223 // Even more detailed reason and status checks for pure duak simplex.
224 if (GetParam().parameters.lp_algorithm == LPAlgorithm::kDualSimplex &&
225 GetParam().parameters.presolve == Emphasis::kOff) {
226 // For pure dual simplex we expect to have a dual feasible solution, so
227 // primal infeasibility must have been detected.
229 // Result validators imply primal status is kInfeasible.
230 EXPECT_EQ(result.termination.problem_status.dual_status,
232 }
233}
234
235TEST_P(StatusTest, PrimalFeasibleAndDualFeasible) {
236 Model model;
237 const Variable x1 =
238 model.AddVariable(0, kInf, GetParam().use_integer_variables, "x1");
239 const Variable x2 =
240 model.AddVariable(0, kInf, GetParam().use_integer_variables, "x2");
241 model.Minimize(x1 + x2);
242 model.AddLinearConstraint(x1 + x2 <= 1, "c1");
243 SolveParameters params = GetParam().parameters;
244 ASSERT_OK_AND_ASSIGN(const SolveResult result,
245 Solve(model, TestedSolver(), {.parameters = params}));
246
247 EXPECT_THAT(result, IsOptimal());
248 // Result validators imply primal and dual problem statuses are kFeasible.
249}
250
251TEST_P(StatusTest, PrimalFeasibleAndDualFeasibleLpIncomplete) {
252 if (!GetParam().supports_iteration_limit ||
253 GetParam().use_integer_variables) {
254 GTEST_SKIP() << "Ignoring this test as it is an LP-only test and requires "
255 "support for iteration limit.";
256 }
257 const int n = 10;
258 const std::unique_ptr<Model> model =
259 IndependentSetCompleteGraph(/*integer=*/false, n);
260
261 SolveParameters params = GetParam().parameters;
262 if (GetParam().supports_one_thread) {
263 params.threads = 1;
264 }
265 params.iteration_limit = 2;
266 ASSERT_OK_AND_ASSIGN(const SolveResult result,
267 Solve(*model, TestedSolver(), {.parameters = params}));
268
269 // Baseline reason and status checks.
271 /*allow_limit_undetermined=*/true));
272 EXPECT_THAT(result.termination.problem_status,
273 Not(PrimalStatusIs(FeasibilityStatus::kInfeasible)));
274 EXPECT_THAT(result.termination.problem_status,
275 Not(DualStatusIs(FeasibilityStatus::kInfeasible)));
276
277 // More detailed reason and status checks for pure primal simplex.
278 if (GetParam().parameters.lp_algorithm == LPAlgorithm::kPrimalSimplex &&
279 GetParam().parameters.presolve == Emphasis::kOff) {
280 // For pure primal simplex we shouldn't have a dual solution (or a dual
281 // feasible status) on early termination, but existence of a primal solution
282 // depends on the phase where the algorithm was terminated.
283 EXPECT_THAT(result.termination.problem_status,
285 }
286
287 // More detailed detailed reason and status checks for pure dual simplex.
288 if (GetParam().parameters.lp_algorithm == LPAlgorithm::kDualSimplex &&
289 GetParam().parameters.presolve == Emphasis::kOff) {
290 // For pure dual simplex we shouldn't have a primal solution (or a primal
291 // feasible status) on early termination, but existence of a dual solution
292 // depends on the phase where the algorithm was terminated.
295 /*allow_limit_undetermined=*/true));
296 EXPECT_THAT(result.termination.problem_status,
297 PrimalStatusIs(FeasibilityStatus::kUndetermined));
298 }
299}
300
301TEST_P(StatusTest, InfeasibleIpWithPrimalDualFeasibleRelaxation) {
302 if (!GetParam().use_integer_variables) {
303 GTEST_SKIP() << "Ignoring this test as it is an IP-only test.";
304 }
305 Model model;
306 const Variable x1 = model.AddIntegerVariable(0.5, kInf, "x1");
307 const Variable x2 = model.AddIntegerVariable(0.5, kInf, "x2");
308 model.Minimize(x1 + x2);
309 model.AddLinearConstraint(x1 + x2 <= 1, "c1");
310
311 ASSERT_OK_AND_ASSIGN(const SolveResult result, SimpleSolve(model));
312
314 // Result validators imply primal problem status is kInfeasible.
315 EXPECT_THAT(result.termination.problem_status,
316 Not(DualStatusIs(FeasibilityStatus::kInfeasible)));
317}
318
319// Some solvers will round the variable bounds of integer variables before
320// starting, which makes the LP relaxation of the above problem infeasible. In
321// the second version of the above test, we make sure the LP relaxation is
322// feasible with integer bounds.
323TEST_P(StatusTest, InfeasibleIpWithPrimalDualFeasibleRelaxation2) {
324 if (!GetParam().use_integer_variables) {
325 GTEST_SKIP() << "Ignoring this test as it is an IP-only test.";
326 }
327 // LP relaxation has optimal solution (0.5, 1.0), while MIP is infeasible.
328 Model model;
329 const Variable x1 = model.AddBinaryVariable("x1");
330 const Variable x2 = model.AddBinaryVariable("x2");
331 model.Minimize(x1);
332 model.AddLinearConstraint(x1 + x2 == 1.5, "c1");
333
334 if (GetParam().solver_type != SolverType::kCpSat) {
335 ASSERT_OK_AND_ASSIGN(const SolveResult result, SimpleSolve(model));
336
338 // Result validators imply primal problem status is kInfeasible.
339 EXPECT_THAT(result.termination.problem_status,
340 Not(DualStatusIs(FeasibilityStatus::kInfeasible)));
341 }
342}
343
344TEST_P(StatusTest, InfeasibleIpWithPrimalFeasibleDualInfeasibleRelaxation) {
345 if (!GetParam().use_integer_variables) {
346 GTEST_SKIP() << "Ignoring this test as it is an IP-only test.";
347 }
348 if (GetParam().solver_type == SolverType::kGlpk) {
349 GTEST_SKIP()
350 << "Ignoring this test as GLPK gets stuck in presolve searching "
351 "for an integer point in the unbounded feasible region of the"
352 "LP relaxation.";
353 }
354 if (GetParam().solver_type == SolverType::kCpSat) {
355 GTEST_SKIP() << "Ignoring this test as CpSat as it returns MODEL_INVALID";
356 }
357 if (GetParam().solver_type == SolverType::kSantorini) {
358 GTEST_SKIP() << "Infinite loop for santorini.";
359 }
360
361 Model model;
362 const Variable x1 = model.AddIntegerVariable(1, kInf, "x1");
363 const Variable x2 = model.AddIntegerVariable(1, kInf, "x2");
364 model.Minimize(x1 + x2);
365 model.AddLinearConstraint(2 * x2 == 2 * x1 + 1, "c1");
366 ASSERT_OK_AND_ASSIGN(const SolveResult result, SimpleSolve(model));
367
369 // Result validators imply primal problem status is kInfeasible.
370 EXPECT_THAT(result.termination.problem_status,
371 Not(DualStatusIs(FeasibilityStatus::kInfeasible)));
372}
373
374TEST_P(StatusTest, IncompleteIpSolve) {
375 if (!GetParam().use_integer_variables || !GetParam().supports_node_limit) {
376 GTEST_SKIP() << "Ignoring this test as it is an IP-only test and requires "
377 "support for node_limit.";
378 }
379 if (GetParam().solver_type == SolverType::kHighs) {
380 GTEST_SKIP() << "Ignoring this test as Highs 1.7+ returns MODEL_INVALID";
381 }
382 ASSERT_OK_AND_ASSIGN(const std::unique_ptr<const Model> model, Load23588());
383 SolveArguments args = {
384 .parameters = {.enable_output = true, .node_limit = 1}};
385 ASSERT_OK_AND_ASSIGN(const SolveResult result,
386 Solve(*model, GetParam().solver_type, args));
387
389 /*allow_limit_undetermined=*/true));
390 // Result validators imply primal problem status is kFeasible.
391 EXPECT_THAT(result.termination.problem_status,
392 DualStatusIs(FeasibilityStatus::kFeasible));
393}
394
395TEST_P(StatusTest, IncompleteIpSolveNoSolution) {
396 if (!GetParam().use_integer_variables) {
397 GTEST_SKIP() << "Ignoring this test as it is an IP-only test.";
398 }
399 // A model where we will not prove optimality immediately.
400 const std::unique_ptr<const Model> model =
401 DenseIndependentSet(/*integer=*/true);
402 // Set additional parameters to ensure we don't even find a feasible solution.
403 SolveInterrupter interrupter;
404 SolveArguments args = {.parameters = {.time_limit = absl::Microseconds(1)}};
405 if (GetParam().supports_one_thread) {
406 args.parameters.threads = 1;
407 }
408 // TODO(b/196132970): support turning off errors for a single parameter, i.e.
409 // set parameter if supported.
410 if (GetParam().solver_type != SolverType::kCpSat &&
411 GetParam().solver_type != SolverType::kGlpk &&
412 GetParam().solver_type != SolverType::kSantorini) {
413 args.parameters.heuristics = Emphasis::kOff;
414 }
415 if (GetParam().solver_type != SolverType::kGlpk &&
416 GetParam().solver_type != SolverType::kHighs &&
417 GetParam().solver_type != SolverType::kSantorini) {
418 args.parameters.cuts = Emphasis::kOff;
419 }
420 if (GetParam().solver_type != SolverType::kGlpk &&
421 GetParam().solver_type != SolverType::kSantorini) {
422 args.parameters.presolve = Emphasis::kOff;
423 }
424 if (GetParam().support_interrupter) {
425 interrupter.Interrupt();
426 args.interrupter = &interrupter;
427 }
428 ASSERT_OK_AND_ASSIGN(const SolveResult result,
429 Solve(*model, GetParam().solver_type, args));
432 /*allow_limit_undetermined=*/true),
435 /*allow_limit_undetermined=*/true)));
436 EXPECT_THAT(result.termination.problem_status,
437 PrimalStatusIs(FeasibilityStatus::kUndetermined));
438 EXPECT_THAT(result.termination.problem_status,
439 Not(DualStatusIs(FeasibilityStatus::kInfeasible)));
440}
441
442} // namespace
443} // namespace operations_research::math_opt
#define ASSIGN_OR_RETURN(lhs, rexpr)
static absl::StatusOr< std::unique_ptr< Model > > FromModelProto(const ModelProto &model_proto)
Definition model.cc:53
SatParameters parameters
const std::string name
A name for logging purposes.
GRBmodel * model
const Variable x2
const Variable x1
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)
@ kInfeasible
The primal problem has no feasible solutions.
ASSERT_THAT(solver->Update(), IsOkAndHolds(DidUpdate()))
Matcher< SolveResult > TerminatesWithOneOf(const std::vector< TerminationReason > &allowed)
Checks that the result has one of the allowed termination reasons.
Definition matchers.cc:615
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 > TerminatesWith(const TerminationReason expected)
Definition matchers.cc:621
std::unique_ptr< Model > IndependentSetCompleteGraph(const bool integer, const int n)
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)
Matcher< SolveResult > IsOptimal(const std::optional< double > expected_primal_objective, const double tolerance)
Definition matchers.cc:762
@ kUndetermined
Solver does not claim a status.
@ kFeasible
Solver claims the problem is feasible.
@ kInfeasible
Solver claims the problem is infeasible.
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
std::string ProtobufShortDebugString(const P &message)
Definition proto_utils.h:41
#define ASSERT_OK_AND_ASSIGN(lhs, rexpr)
FeasibilityStatus primal_status
Status for the primal problem.
FeasibilityStatus dual_status
Status for the dual problem (or for the dual of a continuous relaxation).
bool support_interrupter
True if the solver support SolveInterrupter.
bool use_integer_variables
True if the tests should be performed with integer variables.