Google OR-Tools v9.12
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-2025 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
15
16#include <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
103 Model model;
104 const Variable x1 =
105 model.AddVariable(0, kInf, GetParam().use_integer_variables, "x1");
106 const Variable x2 =
107 model.AddVariable(0, kInf, GetParam().use_integer_variables, "x2");
108
109 model.Maximize(2 * x1 - x2);
110 model.AddLinearConstraint(x1 - x2 <= 1, "c1");
111 model.AddLinearConstraint(x1 - x2 >= 2, "c2");
112 ASSERT_OK_AND_ASSIGN(const SolveResult result, SimpleSolve(model));
113
114 // Baseline reason and status checks.
115 EXPECT_THAT(result,
118 EXPECT_THAT(result.termination.problem_status,
119 AnyOf(PrimalStatusIs(FeasibilityStatus::kInfeasible),
120 DualStatusIs(FeasibilityStatus::kInfeasible),
121 StatusIsPrimalOrDualInfeasible()));
122
123 // More detailed reason and status checks.
124 if (GetParam().disallow_primal_or_dual_infeasible) {
125 // Solver may only detect the dual infeasibility so we cannot guatantee
126 // TerminationReason::kInfeasible (dual infeasibility is one of cases in
127 // kInfeasibleOrUnbounded go/mathopt-termination-and-statuses#inf-or-unb).
128 // However, the status check can be refined.
129 EXPECT_THAT(result.termination.problem_status,
130 AnyOf(PrimalStatusIs(FeasibilityStatus::kInfeasible),
131 DualStatusIs(FeasibilityStatus::kInfeasible)));
132 }
133
134 // Even more detailed reason and status checks for primal simplex.
135 if (GetParam().disallow_primal_or_dual_infeasible &&
136 GetParam().parameters.lp_algorithm == LPAlgorithm::kPrimalSimplex) {
138 // Result validators imply primal problem status is infeasible.
139 EXPECT_THAT(result.termination.problem_status,
140 Not(DualStatusIs(FeasibilityStatus::kFeasible)));
141 }
142}
143
144TEST_P(StatusTest, PrimalFeasibleAndDualInfeasible) {
145 if (GetParam().solver_type == SolverType::kCpSat) {
146 GTEST_SKIP() << "Ignoring this test as CpSat bounds all variables";
147 }
148
149 Model model;
150 const Variable x1 =
151 model.AddVariable(0, kInf, GetParam().use_integer_variables, "x1");
152 const Variable x2 =
153 model.AddVariable(0, kInf, GetParam().use_integer_variables, "x2");
154 model.Maximize(x1 + x2);
155 // When there is a unique (up to scaling) primal ray SCIP gets stuck (possibly
156 // having trouble scaling the ray to be integer?)
157 if (GetParam().solver_type == SolverType::kGscip) {
158 model.AddLinearConstraint(100 <= x1 - 2 * x2, "c1");
159 } else {
160 model.AddLinearConstraint(100 <= x1 - 2 * x2 <= 200, "c1");
161 }
162 ASSERT_OK_AND_ASSIGN(const SolveResult result, SimpleSolve(model));
163
164 // Baseline reason and status checks.
165 EXPECT_THAT(result,
168 EXPECT_THAT(result.termination.problem_status,
169 Not(PrimalStatusIs(FeasibilityStatus::kInfeasible)));
170 EXPECT_THAT(result.termination.problem_status,
171 AnyOf(DualStatusIs(FeasibilityStatus::kInfeasible),
172 StatusIsPrimalOrDualInfeasible()));
173
174 // More detailed reason and status checks.
175 if (GetParam().disallow_primal_or_dual_infeasible) {
176 // Solver may only detect the dual infeasibility so we cannot guatantee
177 // TerminationReason::kInfeasible (dual infeasibility is one of cases in
178 // kInfeasibleOrUnbounded go/mathopt-termination-and-statuses#inf-or-unb).
179 // However, the dual status check can be refined.
180 EXPECT_EQ(result.termination.problem_status.dual_status,
182 }
183
184 // Even more detailed reason and status checks for pure primal simplex.
185 if (GetParam().parameters.lp_algorithm == LPAlgorithm::kPrimalSimplex &&
186 GetParam().parameters.presolve == Emphasis::kOff) {
187 // For pure primal simplex we expect to have a primal feasible solution.
189 // Result validators imply primal status is kFeasible and dual problem
190 // statuse is kInfeasible.
191 }
192}
193
194TEST_P(StatusTest, PrimalInfeasibleAndDualFeasible) {
195 Model model;
196 const Variable x1 =
197 model.AddVariable(0, kInf, GetParam().use_integer_variables, "x1");
198 const Variable x2 =
199 model.AddVariable(0, kInf, GetParam().use_integer_variables, "x2");
200 model.Minimize(x1 + x2);
201 model.AddLinearConstraint(x1 + x2 <= -1, "c1");
202 ASSERT_OK_AND_ASSIGN(const SolveResult result, SimpleSolve(model));
203
204 // Baseline reason and status checks.
205 EXPECT_THAT(result,
208 EXPECT_THAT(result.termination.problem_status,
209 AnyOf(PrimalStatusIs(FeasibilityStatus::kInfeasible),
210 StatusIsPrimalOrDualInfeasible()));
211 EXPECT_THAT(result.termination.problem_status,
212 Not(DualStatusIs(FeasibilityStatus::kInfeasible)));
213
214 // More detailed reason and status checks.
215 if (GetParam().disallow_primal_or_dual_infeasible) {
217 // Result validators imply primal status is kInfeasible.
218 }
219
220 // Even more detailed reason and status checks for pure duak simplex.
221 if (GetParam().parameters.lp_algorithm == LPAlgorithm::kDualSimplex &&
222 GetParam().parameters.presolve == Emphasis::kOff) {
223 // For pure dual simplex we expect to have a dual feasible solution, so
224 // primal infeasibility must have been detected.
226 // Result validators imply primal status is kInfeasible.
227 EXPECT_EQ(result.termination.problem_status.dual_status,
229 }
230}
231
232TEST_P(StatusTest, PrimalFeasibleAndDualFeasible) {
233 Model model;
234 const Variable x1 =
235 model.AddVariable(0, kInf, GetParam().use_integer_variables, "x1");
236 const Variable x2 =
237 model.AddVariable(0, kInf, GetParam().use_integer_variables, "x2");
238 model.Minimize(x1 + x2);
239 model.AddLinearConstraint(x1 + x2 <= 1, "c1");
240 SolveParameters params = GetParam().parameters;
241 ASSERT_OK_AND_ASSIGN(const SolveResult result,
242 Solve(model, TestedSolver(), {.parameters = params}));
243
244 EXPECT_THAT(result, IsOptimal());
245 // Result validators imply primal and dual problem statuses are kFeasible.
246}
247
248TEST_P(StatusTest, PrimalFeasibleAndDualFeasibleLpIncomplete) {
249 if (!GetParam().supports_iteration_limit ||
250 GetParam().use_integer_variables) {
251 GTEST_SKIP() << "Ignoring this test as it is an LP-only test and requires "
252 "support for iteration limit.";
253 }
254 const int n = 10;
255 const std::unique_ptr<Model> model =
256 IndependentSetCompleteGraph(/*integer=*/false, n);
257
258 SolveParameters params = GetParam().parameters;
259 if (GetParam().supports_one_thread) {
260 params.threads = 1;
261 }
262 params.iteration_limit = 2;
263 ASSERT_OK_AND_ASSIGN(const SolveResult result,
264 Solve(*model, TestedSolver(), {.parameters = params}));
265
266 // Baseline reason and status checks.
268 /*allow_limit_undetermined=*/true));
269 EXPECT_THAT(result.termination.problem_status,
270 Not(PrimalStatusIs(FeasibilityStatus::kInfeasible)));
271 EXPECT_THAT(result.termination.problem_status,
272 Not(DualStatusIs(FeasibilityStatus::kInfeasible)));
273
274 // More detailed reason and status checks for pure primal simplex.
275 if (GetParam().parameters.lp_algorithm == LPAlgorithm::kPrimalSimplex &&
276 GetParam().parameters.presolve == Emphasis::kOff) {
277 // For pure primal simplex we shouldn't have a dual solution (or a dual
278 // feasible status) on early termination, but existence of a primal solution
279 // depends on the phase where the algorithm was terminated.
280 EXPECT_THAT(result.termination.problem_status,
282 }
283
284 // More detailed detailed reason and status checks for pure dual simplex.
285 if (GetParam().parameters.lp_algorithm == LPAlgorithm::kDualSimplex &&
286 GetParam().parameters.presolve == Emphasis::kOff) {
287 // For pure dual simplex we shouldn't have a primal solution (or a primal
288 // feasible status) on early termination, but existence of a dual solution
289 // depends on the phase where the algorithm was terminated.
292 /*allow_limit_undetermined=*/true));
293 EXPECT_THAT(result.termination.problem_status,
294 PrimalStatusIs(FeasibilityStatus::kUndetermined));
295 }
296}
297
298TEST_P(StatusTest, InfeasibleIpWithPrimalDualFeasibleRelaxation) {
299 if (!GetParam().use_integer_variables) {
300 GTEST_SKIP() << "Ignoring this test as it is an IP-only test.";
301 }
302 Model model;
303 const Variable x1 = model.AddIntegerVariable(0.5, kInf, "x1");
304 const Variable x2 = model.AddIntegerVariable(0.5, kInf, "x2");
305 model.Minimize(x1 + x2);
306 model.AddLinearConstraint(x1 + x2 <= 1, "c1");
307
308 ASSERT_OK_AND_ASSIGN(const SolveResult result, SimpleSolve(model));
309
311 // Result validators imply primal problem status is kInfeasible.
312 EXPECT_THAT(result.termination.problem_status,
313 Not(DualStatusIs(FeasibilityStatus::kInfeasible)));
314}
315
316// Some solvers will round the variable bounds of integer variables before
317// starting, which makes the LP relaxation of the above problem infeasible. In
318// the second version of the above test, we make sure the LP relaxation is
319// feasible with integer bounds.
320TEST_P(StatusTest, InfeasibleIpWithPrimalDualFeasibleRelaxation2) {
321 if (!GetParam().use_integer_variables) {
322 GTEST_SKIP() << "Ignoring this test as it is an IP-only test.";
323 }
324 // LP relaxation has optimal solution (0.5, 1.0), while MIP is infeasible.
325 Model model;
326 const Variable x1 = model.AddBinaryVariable("x1");
327 const Variable x2 = model.AddBinaryVariable("x2");
328 model.Minimize(x1);
329 model.AddLinearConstraint(x1 + x2 == 1.5, "c1");
330
331 if (GetParam().solver_type != SolverType::kCpSat) {
332 ASSERT_OK_AND_ASSIGN(const SolveResult result, SimpleSolve(model));
333
335 // Result validators imply primal problem status is kInfeasible.
336 EXPECT_THAT(result.termination.problem_status,
337 Not(DualStatusIs(FeasibilityStatus::kInfeasible)));
338 }
339}
340
341TEST_P(StatusTest, InfeasibleIpWithPrimalFeasibleDualInfeasibleRelaxation) {
342 if (!GetParam().use_integer_variables) {
343 GTEST_SKIP() << "Ignoring this test as it is an IP-only test.";
344 }
345 if (GetParam().solver_type == SolverType::kGlpk) {
346 GTEST_SKIP()
347 << "Ignoring this test as GLPK gets stuck in presolve searching "
348 "for an integer point in the unbounded feasible region of the"
349 "LP relaxation.";
350 }
351 if (GetParam().solver_type == SolverType::kCpSat) {
352 GTEST_SKIP() << "Ignoring this test as CpSat as it returns MODEL_INVALID";
353 }
354 if (GetParam().solver_type == SolverType::kSantorini) {
355 GTEST_SKIP() << "Infinite loop for santorini.";
356 }
357
358 Model model;
359 const Variable x1 = model.AddIntegerVariable(1, kInf, "x1");
360 const Variable x2 = model.AddIntegerVariable(1, kInf, "x2");
361 model.Minimize(x1 + x2);
362 model.AddLinearConstraint(2 * x2 == 2 * x1 + 1, "c1");
363 ASSERT_OK_AND_ASSIGN(const SolveResult result, SimpleSolve(model));
364
366 // Result validators imply primal problem status is kInfeasible.
367 EXPECT_THAT(result.termination.problem_status,
368 Not(DualStatusIs(FeasibilityStatus::kInfeasible)));
369}
370
371TEST_P(StatusTest, IncompleteIpSolve) {
372 if (!GetParam().use_integer_variables || !GetParam().supports_node_limit) {
373 GTEST_SKIP() << "Ignoring this test as it is an IP-only test and requires "
374 "support for node_limit.";
375 }
376 if (GetParam().solver_type == SolverType::kHighs) {
377 GTEST_SKIP() << "Ignoring this test as Highs 1.7+ returns MODEL_INVALID";
378 }
379 ASSERT_OK_AND_ASSIGN(const std::unique_ptr<const Model> model, Load23588());
380 SolveArguments args = {
381 .parameters = {.enable_output = true, .node_limit = 1}};
382 ASSERT_OK_AND_ASSIGN(const SolveResult result,
383 Solve(*model, GetParam().solver_type, args));
384
386 /*allow_limit_undetermined=*/true));
387 // Result validators imply primal problem status is kFeasible.
388 EXPECT_THAT(result.termination.problem_status,
389 DualStatusIs(FeasibilityStatus::kFeasible));
390}
391
392TEST_P(StatusTest, IncompleteIpSolveNoSolution) {
393 if (!GetParam().use_integer_variables) {
394 GTEST_SKIP() << "Ignoring this test as it is an IP-only test.";
395 }
396 // A model where we will not prove optimality immediately.
397 const std::unique_ptr<const Model> model =
398 DenseIndependentSet(/*integer=*/true);
399 // Set additional parameters to ensure we don't even find a feasible solution.
400 SolveInterrupter interrupter;
401 SolveArguments args = {.parameters = {.time_limit = absl::Microseconds(1)}};
402 if (GetParam().supports_one_thread) {
403 args.parameters.threads = 1;
404 }
405 // TODO(b/196132970): support turning off errors for a single parameter, i.e.
406 // set parameter if supported.
407 if (GetParam().solver_type != SolverType::kCpSat &&
408 GetParam().solver_type != SolverType::kGlpk &&
409 GetParam().solver_type != SolverType::kSantorini) {
410 args.parameters.heuristics = Emphasis::kOff;
411 }
412 if (GetParam().solver_type != SolverType::kGlpk &&
413 GetParam().solver_type != SolverType::kHighs &&
414 GetParam().solver_type != SolverType::kSantorini) {
415 args.parameters.cuts = Emphasis::kOff;
416 }
417 if (GetParam().solver_type != SolverType::kGlpk &&
418 GetParam().solver_type != SolverType::kSantorini) {
419 args.parameters.presolve = Emphasis::kOff;
420 }
421 if (GetParam().support_interrupter) {
422 interrupter.Interrupt();
423 args.interrupter = &interrupter;
424 }
425 ASSERT_OK_AND_ASSIGN(const SolveResult result,
426 Solve(*model, GetParam().solver_type, args));
429 /*allow_limit_undetermined=*/true),
432 /*allow_limit_undetermined=*/true)));
433 EXPECT_THAT(result.termination.problem_status,
434 PrimalStatusIs(FeasibilityStatus::kUndetermined));
435 EXPECT_THAT(result.termination.problem_status,
436 Not(DualStatusIs(FeasibilityStatus::kInfeasible)));
437}
438
439} // namespace
440} // namespace operations_research::math_opt
#define ASSIGN_OR_RETURN(lhs, rexpr)
void Maximize(Variable *obj, std::vector< Annotation > search_annotations)
Definition model.cc:1075
Variable * AddVariable(absl::string_view name, const Domain &domain, bool defined)
--— Builder methods --—
Definition model.cc:1025
void Minimize(Variable *obj, std::vector< Annotation > search_annotations)
Definition model.cc:1068
static absl::StatusOr< std::unique_ptr< Model > > FromModelProto(const ModelProto &model_proto)
Definition model.cc:54
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).
StatusTestParameters(const SolverType solver_type, SolveParameters parameters, const bool disallow_primal_or_dual_infeasible, const bool supports_iteration_limit, const bool use_integer_variables, const bool supports_node_limit, const bool support_interrupter, const bool supports_one_thread)
bool support_interrupter
True if the solver support SolveInterrupter.
bool use_integer_variables
True if the tests should be performed with integer variables.