Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
lp_incomplete_solve_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 <cmath>
17#include <cstdlib>
18#include <limits>
19#include <memory>
20#include <optional>
21#include <ostream>
22#include <vector>
23
24#include "absl/strings/str_cat.h"
25#include "gtest/gtest.h"
26#include "ortools/base/gmock.h"
30#include "ortools/math_opt/parameters.pb.h"
31#include "ortools/math_opt/result.pb.h"
32#include "ortools/math_opt/solution.pb.h"
34
35namespace operations_research {
36namespace math_opt {
37
39 const SolverType solver_type, const std::optional<LPAlgorithm> lp_algorithm,
40 const bool supports_iteration_limit, const bool supports_initial_basis,
41 const bool supports_incremental_solve, const bool supports_basis,
42 const bool supports_presolve, const bool check_primal_objective,
43 const bool primal_solution_status_always_set,
44 const bool dual_solution_status_always_set)
45 : solver_type(solver_type),
46 lp_algorithm(lp_algorithm),
47 supports_iteration_limit(supports_iteration_limit),
48 supports_initial_basis(supports_initial_basis),
49 supports_incremental_solve(supports_incremental_solve),
50 supports_basis(supports_basis),
51 supports_presolve(supports_presolve),
52 check_primal_objective(check_primal_objective),
53 primal_solution_status_always_set(primal_solution_status_always_set),
54 dual_solution_status_always_set(dual_solution_status_always_set) {}
55
56std::ostream& operator<<(std::ostream& out,
57 const LpIncompleteSolveTestParams& params) {
58 out << "{ solver_type: " << params.solver_type
59 << " lp_algorithm: " << params.lp_algorithm
60 << " supports_iteration_limit: " << params.supports_iteration_limit
61 << " supports_initial_basis: " << params.supports_initial_basis
62 << " supports_incremental_solve:" << params.supports_incremental_solve
63 << " supports_basis:" << params.supports_basis
64 << " supports_presolve:" << params.supports_presolve
65 << " check_primal_objective:" << params.check_primal_objective
66 << " primal_solution_status_always_set:"
68 << " dual_solution_status_always_set:"
70
71 return out;
72}
73using ::testing::AnyOf;
74using ::testing::ElementsAreArray;
75using ::testing::Gt;
76using ::testing::Lt;
77
78constexpr double kInf = std::numeric_limits<double>::infinity();
79constexpr double kTolerance = 1e-6;
80
81namespace {
82
83// This tests only assumes that there is a non-optimal primal-dual pair of
84// appropriate dimensions and hence should work for most algorithms.
85TEST_P(LpIncompleteSolveTest, SimpleTest) {
86 if (!GetParam().supports_iteration_limit) {
87 GTEST_SKIP()
88 << "Ignoring this test as it requires support for iteration limit.";
89 }
90
91 const int n = 10;
92 const std::unique_ptr<Model> model =
93 IndependentSetCompleteGraph(/*integer=*/false, n);
94
95 SolveArguments args;
96 args.parameters.threads = 1;
97 args.parameters.lp_algorithm = GetParam().lp_algorithm;
98 if (GetParam().supports_presolve) {
100 }
102
103 ASSERT_OK_AND_ASSIGN(const SolveResult result,
104 Solve(*model, TestedSolver(), args));
106 /*allow_limit_undetermined=*/true));
107 ASSERT_FALSE(result.solutions.empty());
108 ASSERT_TRUE(result.solutions[0].primal_solution.has_value());
109 EXPECT_THAT(SortedKeys(result.solutions[0].primal_solution->variable_values),
110 ElementsAreArray(model->SortedVariables()));
111 ASSERT_TRUE(result.solutions[0].dual_solution.has_value());
112 EXPECT_THAT(SortedKeys(result.solutions[0].dual_solution->reduced_costs),
113 ElementsAreArray(model->SortedVariables()));
114 EXPECT_EQ(SortedKeys(result.solutions[0].dual_solution->dual_values),
115 model->SortedLinearConstraints());
116}
117
118bool ApproxEq(const double first_double, const double second_double,
119 double tolerance = kTolerance) {
120 return std::abs(first_double - second_double) <= tolerance;
121}
122
123// The following detailed simplex tests require parameters that may not be
124// supported by all simplex solvers.
125
126// Algorithm: Dual simplex.
127// Start: Primal/dual infeasible basis with feasible dual solution
128// End: Primal infeasible and dual feasible basis.
129//
130// Primal model:
131// min x[0] + ... + x[n - 1]
132// s.t.
133// Constraints: -1 <= x[i] <= 1 (y[i]) for all i in {0,...,n - 1}
134// Variable bounds: -2 <= x[i] <= 2 (r[i]) for all i in {0,...,n - 1}
135//
136// Dual model (go/mathopt-dual):
137//
138// max -|y[0]| + ... + -|y[n- 1]| + -2|r[0]| + ... + -2|r[n - 1]|
139//
140// y[i] + r[i] == 1 for all i in {0,...,n - 1}
141//
142// Optimal solution:
143//
144// The unique primal/dual optimal pair is
145// * x[i] = -1 for all i in {0,...,n - 1}
146// * y[i] = 1 for all i in {0,...,n - 1}
147// * r[i] = 0 for all i in {0,...,n - 1}
148//
149// All basis can be described by disjoint subsets N1, P1, N2, P2 of
150// {0,...,n - 1} that describes the basis and solutions as follows (The sets
151// indicate the variables fixed at -1, 1, -2 and 2 respectively):
152// * x[i] = -1 for all i in N1, x[i] = 1 for all i in P1, x[i] = -2 for all i
153// in N2, and x[i] = 2 for all i in N2.
154// * r[i] = 0 for all i in N1 or P1, r[i] = 1 for all i in N2 or P2.
155// * y[i] = 1 for all i in N1 or P1, y[i] = 0 for all i in N2 or P2.
156// * x[i] is BASIC for all i in N1 or P1, x[i] is AT_UPPER_BOUND for all i in
157// P2, and x[i] is AT_LOWER_BOUND for all i in N2.
158// * the constraint associated to y[i] is BASIC for all i in N2 or P2,
159// AT_UPPER_BOUND for all i in P1, and AT_LOWER_BOUND for all i in N1.
160//
161// We have the following feasibility conditions:
162// * A basis is primal feasible if and only if both N2 and P2 are empty,
163// * a basis is dual feasible if both P2 and P1 are empty, but
164// * the dual solution associated to any basis is feasible.
165//
166// Test:
167//
168// We initialize the solver to start at solution x[i] = 2 for all i in
169// {0,...,n - 1} using initial basis (i.e. P2 = {0, ..., n - 1}). We then set
170// an iteration limit that should allow at least one pivot away from this
171// solution, but which is not long enough to reach a primal feasible solution.
172// Finally, we check that the primal and dual solution (and basis if supported)
173// obtained under this iteration limit corresponts to a basis with empty P1 and
174// P2 and with 1 < |N1|, |N2| < n.
175//
176// Note: this test assumes the dual simplex algorithms implements dual
177// feasibility correction that (in the first iteration) switches all the x[i]
178// AT_UPPER_BOUND to AT_LOWER_BOUND to match the sign of r[i].
179//
180// TODO(b/208230589): Simplify tests by adding a matcher function that checks
181// basic consistency of the primal, dual and basis.
182TEST_P(LpIncompleteSolveTest, DualSimplexInfeasibleBasis) {
183 if (GetParam().lp_algorithm != LPAlgorithm::kDualSimplex ||
184 !GetParam().supports_iteration_limit ||
185 !GetParam().supports_initial_basis) {
186 GTEST_SKIP()
187 << "Ignoring this test as it requires support for dual simplex, "
188 "iteration limit and initial basis.";
189 }
190 int n = 10;
191 Model model("DualSimplexInfeasibleBasis");
192 std::vector<Variable> x;
193 std::vector<LinearConstraint> c;
194 for (int i = 0; i < n; ++i) {
195 x.push_back(model.AddContinuousVariable(-2.0, 2.0));
196 c.push_back(model.AddLinearConstraint(-1.0 <= x[i] <= 1.0));
197 }
198 model.Minimize(Sum(x));
199 SolveArguments args;
200 args.parameters.lp_algorithm = GetParam().lp_algorithm;
201 if (GetParam().supports_presolve) {
203 }
204 Basis initial_basis;
205 for (int i = 0; i < n; ++i) {
206 initial_basis.variable_status.emplace(x[i], BasisStatus::kAtUpperBound);
207 initial_basis.constraint_status.emplace(c[i], BasisStatus::kBasic);
208 }
209 args.model_parameters.initial_basis = initial_basis;
210
212 ASSERT_OK_AND_ASSIGN(const SolveResult result,
213 Solve(model, TestedSolver(), args));
216 /*allow_limit_undetermined=*/true));
217 if (GetParam().supports_basis) {
218 ASSERT_TRUE(result.has_basis());
219 if (GetParam().dual_solution_status_always_set) {
220 EXPECT_EQ(result.solutions[0].basis->basic_dual_feasibility,
222 } else {
223 EXPECT_NE(result.solutions[0].basis->basic_dual_feasibility,
225 }
226 } else {
227 LOG(INFO) << "Skipping basis check as solver does not return a basis.";
228 }
229 ASSERT_FALSE(result.solutions.empty());
230 ASSERT_TRUE(result.solutions[0].primal_solution.has_value());
231 ASSERT_TRUE(result.solutions[0].dual_solution.has_value());
232 int n1_variables = 0;
233 int p1_variables = 0;
234 int n2_variables = 0;
235 int p2_variables = 0;
236 for (int i = 0; i < n; ++i) {
237 SCOPED_TRACE(absl::StrCat(i));
238 const double variable_value =
239 result.solutions[0].primal_solution->variable_values.at(x[i]);
240 const double reduced_cost =
241 result.solutions[0].dual_solution->reduced_costs.at(x[i]);
242 const double dual_value =
243 result.solutions[0].dual_solution->dual_values.at(c[i]);
244 BasisStatus expected_variable_status;
245 BasisStatus expected_constraint_status;
246 if (ApproxEq(variable_value, -1.0)) {
247 ++n1_variables;
248 EXPECT_NEAR(reduced_cost, 0.0, kTolerance);
249 EXPECT_NEAR(dual_value, 1.0, kTolerance);
250 expected_variable_status = BasisStatus::kBasic;
251 expected_constraint_status = BasisStatus::kAtLowerBound;
252 } else if (ApproxEq(variable_value, 1.0)) {
253 ++p1_variables;
254 EXPECT_NEAR(reduced_cost, 0.0, kTolerance);
255 EXPECT_NEAR(dual_value, 1.0, kTolerance);
256 expected_variable_status = BasisStatus::kBasic;
257 expected_constraint_status = BasisStatus::kAtUpperBound;
258 } else if (ApproxEq(variable_value, -2.0)) {
259 ++n2_variables;
260 EXPECT_NEAR(reduced_cost, 1.0, kTolerance);
261 EXPECT_NEAR(dual_value, 0.0, kTolerance);
262 expected_variable_status = BasisStatus::kAtLowerBound;
263 expected_constraint_status = BasisStatus::kBasic;
264 } else {
265 EXPECT_NEAR(variable_value, 2.0, kTolerance);
266 ++p2_variables;
267 EXPECT_NEAR(reduced_cost, 1.0, kTolerance);
268 EXPECT_NEAR(dual_value, 0.0, kTolerance);
269 expected_variable_status = BasisStatus::kAtUpperBound;
270 expected_constraint_status = BasisStatus::kBasic;
271 }
272 if (GetParam().supports_basis) {
273 ASSERT_TRUE(result.has_basis());
274 EXPECT_EQ(result.solutions[0].basis->variable_status.at(x[i]),
275 expected_variable_status);
276 EXPECT_EQ(result.solutions[0].basis->constraint_status.at(c[i]),
277 expected_constraint_status);
278 }
279 }
280 EXPECT_EQ(p1_variables, 0);
281 EXPECT_EQ(p2_variables, 0);
282 EXPECT_GT(n2_variables, 1);
283 EXPECT_GT(n1_variables, 1);
284 EXPECT_LT(n2_variables, n);
285 EXPECT_LT(n1_variables, n);
286 if (GetParam().primal_solution_status_always_set) {
287 EXPECT_EQ(result.solutions[0].primal_solution->feasibility_status,
289 } else {
290 EXPECT_NE(result.solutions[0].primal_solution->feasibility_status,
292 }
293 ASSERT_TRUE(result.solutions[0].dual_solution.has_value());
294 if (GetParam().dual_solution_status_always_set) {
295 EXPECT_EQ(result.solutions[0].dual_solution->feasibility_status,
297 } else {
298 EXPECT_NE(result.solutions[0].dual_solution->feasibility_status,
300 }
301 if (GetParam().check_primal_objective) {
302 EXPECT_NEAR(-1 * n1_variables - 2 * n2_variables,
303 result.solutions[0].primal_solution->objective_value,
304 kTolerance);
305 if (GetParam().supports_basis &&
306 GetParam().dual_solution_status_always_set) {
307 // Here we know that the basis is dual feasible as checked above, so the
308 // primal and dual objective values match. See
309 // go/mathopt-basis-advanced#cs-obj-dual-feasible-dual-feasible-basis
310 ASSERT_TRUE(
311 result.solutions[0].dual_solution->objective_value.has_value());
312 EXPECT_NEAR(-1 * n1_variables - 2 * n2_variables,
313 *result.solutions[0].dual_solution->objective_value,
314 kTolerance);
315 }
316 } else {
317 LOG(INFO) << "Skipping primal objective check as solver does not "
318 "reliably support it for incomplete solves.";
319 }
320}
321
322// Algorithm: Primal simplex.
323// Start: Primal/dual infeasible basis with feasible dual solution.
324// End: Primal/dual infeasible basis with feasible dual solution.
325//
326// Primal model:
327// min x[0] + ... + x[n - 1]
328// s.t.
329// Constraints: x[0] + ... + x[n - 1] <= 1 (y)
330// Variable bounds: 0 <= x[i] <= 2 (r[i]) for all i in {0,...,n - 1}
331//
332// Dual model (go/mathopt-dual):
333//
334// max {y : y < 0} + 2 {r[0] : r[0] < 0} + ... + 2 {r[n - 1] : r[n - 1] < 0}
335//
336// y + r[i] == 1 for all i in {0,...,n - 1}
337// y <= 0
338//
339// Optimal solution:
340//
341// The unique primal/dual optimal pair is
342// * x[i] = 0 for all i in {0,...,n - 1}
343// * y = 0
344// * r[i] = 1 for all i in {0,...,n - 1}
345//
346// Basic solutions defined by bounds:
347//
348// All basis with a basic y can be described by a subset I of {0,...,n - 1} that
349// describes the basis and solutions as follows (I indicates variables at their
350// upper bounds of 2):
351// * x[i] = 2 for all i in I, x[i] = 0 for all i not in I.
352// * r[i] = 1 for all i in {0, ..., n - 1}.
353// * x[i] is AT_UPPER_BOUND for all i in I, x[i] is AT_LOWER_BOUND for all i
354// not in I.
355// * y = 0.
356// * the constraint associated to y is BASIC.
357//
358// All basis with a basic y are primal and dual infeasible, except for the one
359// associated to an empty I, which is optimal. However, all basis with a basic y
360// yield the same dual solution, which is dual feasible.
361//
362// Test:
363//
364// We initialize the solver to start at solution x[i] = 2 for all i in
365// {0,...,n - 1} using initial basis (i.e. I = {0, ..., n}). We then set an
366// iteration limit that should allow at least one pivot away from this solution,
367// but which is not long enough to reach a primal feasile solution. Finally, we
368// check that the primal and dual solution (and basis if supported) obtained
369// under this iteration limit corresponts to a basis I with 1 < |I| < n (i.e.
370// with k = |I| variables at 2 for 0 < k < n).
371TEST_P(LpIncompleteSolveTest, PrimalSimplexInfeasibleBasis) {
372 if (GetParam().lp_algorithm != LPAlgorithm::kPrimalSimplex ||
373 !GetParam().supports_iteration_limit ||
374 !GetParam().supports_initial_basis) {
375 GTEST_SKIP()
376 << "Ignoring this test as it requires support for primal simplex,"
377 " iteration limit and initial basis.";
378 }
379 int n = 10;
380 Model model("PrimalSimplexInfeasibleBasis");
381 std::vector<Variable> x;
382 for (int i = 0; i < n; ++i) {
383 x.push_back(model.AddContinuousVariable(0.0, 2.0));
384 }
385 LinearConstraint c = model.AddLinearConstraint(Sum(x) <= 1);
386 model.Minimize(Sum(x));
387 SolveArguments args;
388 args.parameters.lp_algorithm = GetParam().lp_algorithm;
389
390 if (GetParam().supports_presolve) {
392 }
393 Basis initial_basis;
394 for (int i = 0; i < n; ++i) {
395 initial_basis.variable_status.emplace(x[i], BasisStatus::kAtUpperBound);
396 }
397 initial_basis.constraint_status.emplace(c, BasisStatus::kBasic);
398 args.model_parameters.initial_basis = initial_basis;
399
401 ASSERT_OK_AND_ASSIGN(const SolveResult result,
402 Solve(model, TestedSolver(), args));
405 /*allow_limit_undetermined=*/true));
406 if (GetParam().supports_basis) {
407 ASSERT_TRUE(result.has_basis());
408 EXPECT_NE(result.solutions[0].basis->basic_dual_feasibility,
410 } else {
411 LOG(INFO) << "Skipping basis check as solver does not return a basis.";
412 }
413 ASSERT_FALSE(result.solutions.empty());
414 ASSERT_TRUE(result.solutions[0].primal_solution.has_value());
415 ASSERT_TRUE(result.solutions[0].dual_solution.has_value());
416 int variable_values_at_two = 0;
417 const double dual_value =
418 result.solutions[0].dual_solution->dual_values.at(c);
419 EXPECT_NEAR(dual_value, 0.0, kTolerance);
420 if (GetParam().supports_basis) {
421 EXPECT_EQ(result.solutions[0].basis->constraint_status.at(c),
423 }
424 for (int i = 0; i < n; ++i) {
425 SCOPED_TRACE(absl::StrCat(i));
426 const double variable_value =
427 result.solutions[0].primal_solution->variable_values.at(x[i]);
428 const double reduced_cost =
429 result.solutions[0].dual_solution->reduced_costs.at(x[i]);
430 // Gurobi returns a value of -999,999 or 999,999 for these reduced costs.
431 // TODO(b/195295177): Create a simple example to file a bug with Gurobi.
432 if (TestedSolver() != SolverType::kGurobi) {
433 EXPECT_NEAR(reduced_cost, 1.0, kTolerance);
434 }
435 BasisStatus expected_status;
436 if (ApproxEq(variable_value, 2.0)) {
437 ++variable_values_at_two;
438 expected_status = BasisStatus::kAtUpperBound;
439 } else {
440 expected_status = BasisStatus::kAtLowerBound;
441 }
442 if (GetParam().supports_basis) {
443 EXPECT_EQ(result.solutions[0].basis->variable_status.at(x[i]),
444 expected_status);
445 }
446 }
447 EXPECT_GT(variable_values_at_two, 1);
448 EXPECT_LT(variable_values_at_two, n);
449 if (GetParam().primal_solution_status_always_set) {
450 EXPECT_EQ(result.solutions[0].primal_solution->feasibility_status,
452 } else {
453 EXPECT_NE(result.solutions[0].primal_solution->feasibility_status,
455 }
456 // Dual solution is feasible, but basis is dual infeasible so most solvers
457 // will return kUndetermined instead of kFeasible
458 EXPECT_NE(result.solutions[0].dual_solution->feasibility_status,
460
461 if (GetParam().check_primal_objective) {
462 EXPECT_NEAR(2 * variable_values_at_two,
463 result.solutions[0].primal_solution->objective_value,
464 kTolerance);
465 } else {
466 LOG(INFO) << "Skipping primal objective check as solver does not "
467 "reliably support it for incomplete solves.";
468 }
469}
470
471// Algorithm: Primal simplex.
472// Start: Primal feasible and dual infeasible basis.
473// End: Primal feasible and dual infeasible basis.
474//
475// Primal model:
476// max x[0] + ... + x[n - 1]
477// s.t.
478// Constraints: x[i] <= 1 (y[i]) for all i in {0,...,n - 1}
479// Variable bounds: 0 <= x[i] (r[i]) for all i in {0,...,n - 1}
480//
481// Dual model (go/mathopt-dual):
482//
483// min y[0] + ... + y[n - 1]
484//
485// y[i] + r[i] == 1 for all i in {0,...,n - 1}
486// y[i] >= 0 for all i in {0,...,n - 1}
487// r[i] <= 0 for all i in {0,...,n - 1}
488//
489// Optimal solution:
490//
491// The unique primal/dual optimal pair is
492// * x[i] = 1 for all i in {0,...,n - 1}
493// * y[i] = 1 for all i in {0,...,n - 1}
494// * r[i] = 0 for all i in {0,...,n - 1}
495//
496// Basic solutions:
497//
498// All basis can be described by a subset I of {0,...,n - 1} that describes the
499// basis and solutions as follows (I indicates variables at their upper bounds):
500// * x[i] = 1 for all i in I, x[i] = 0 for all i not in I.
501// * r[i] = 0 for all i in I, r[i] = 1 for all i not in I.
502// * x[i] is BASIC for all i in I, x[i] is AT_LOWER_BOUND for all i not in I.
503// * y[i] = 1 for all i in I, y[i] = 0 for all i not in I.
504// * the constraint associated to y[i] is AT_UPPER_BOUND for all i in I, and
505// BASIC for all i not in I.
506//
507// All basis are primal feasible, but only I = {0,...,n - 1} is dual feasible.
508//
509// Test:
510//
511// We initialize the solver to start at solution x[i] = 0 for all i in
512// {0,...,n - 1} using initial basis or by minimizing the objective. We then set
513// an iteration limit that should allow at least one pivot away from this
514// solution, but which is not long enough to reach the optimal solution x[i] = 1
515// for all i. Finally, we check that the primal and dual solution (and basis if
516// supported) obtained under this iteration limit corresponts to a basis I with
517// 0 < |I| < n (i.e. with k variables at 1 for 0 < k < n).
518TEST_P(LpIncompleteSolveTest, PrimalSimplexAlgorithm) {
519 if (GetParam().lp_algorithm != LPAlgorithm::kPrimalSimplex ||
520 !GetParam().supports_iteration_limit ||
521 !(GetParam().supports_incremental_solve ||
522 GetParam().supports_initial_basis)) {
523 GTEST_SKIP()
524 << "Ignoring this test as it requires support for primal simplex, "
525 "iteration limit and either incremental solve or initial basis.";
526 }
527 int n = 10;
528 Model model("Primal Feasible Incomplete Solve LP");
529 std::vector<Variable> x;
530 std::vector<LinearConstraint> c;
531 for (int i = 0; i < n; ++i) {
532 x.push_back(model.AddContinuousVariable(0.0, kInf));
533 c.push_back(model.AddLinearConstraint(x[i] <= 1));
534 }
535
537 const std::unique_ptr<IncrementalSolver> incremental_solver,
538 NewIncrementalSolver(&model, TestedSolver()));
539 SolveArguments args;
540 args.parameters.lp_algorithm = GetParam().lp_algorithm;
541 if (GetParam().supports_presolve) {
543 }
544
545 if (GetParam().supports_initial_basis) {
546 Basis initial_basis;
547 for (int i = 0; i < n; ++i) {
548 initial_basis.variable_status.emplace(x[i], BasisStatus::kAtLowerBound);
549 initial_basis.constraint_status.emplace(c[i], BasisStatus::kBasic);
550 }
551 args.model_parameters.initial_basis = initial_basis;
552 } else {
553 model.Minimize(Sum(x));
554 ASSERT_OK(incremental_solver->Solve(args));
555 }
556
557 model.Maximize(Sum(x));
559 ASSERT_OK_AND_ASSIGN(const SolveResult result,
560 incremental_solver->Solve(args));
561 if (GetParam().primal_solution_status_always_set) {
564 /*allow_limit_undetermined=*/true));
565 } else {
567 /*allow_limit_undetermined=*/true));
568 }
569 if (GetParam().supports_basis) {
570 ASSERT_TRUE(result.has_basis());
571 if (GetParam().dual_solution_status_always_set) {
572 EXPECT_EQ(result.solutions[0].basis->basic_dual_feasibility,
574 } else {
575 EXPECT_NE(result.solutions[0].basis->basic_dual_feasibility,
577 }
578 } else {
579 LOG(INFO) << "Skipping basis check as solver does not return a basis.";
580 }
581 ASSERT_FALSE(result.solutions.empty());
582 ASSERT_TRUE(result.solutions[0].primal_solution.has_value());
583 ASSERT_TRUE(result.solutions[0].dual_solution.has_value());
584 int variable_values_at_one = 0;
585 for (int i = 0; i < n; ++i) {
586 SCOPED_TRACE(absl::StrCat(i));
587 double variable_value =
588 result.solutions[0].primal_solution->variable_values.at(x[i]);
589 double reduced_cost =
590 result.solutions[0].dual_solution->reduced_costs.at(x[i]);
591 double dual_value = result.solutions[0].dual_solution->dual_values.at(c[i]);
592 if (std::abs(variable_value - 1.0) <= kTolerance) {
593 ++variable_values_at_one;
594 EXPECT_NEAR(reduced_cost, 0.0, kTolerance);
595 EXPECT_NEAR(dual_value, 1.0, kTolerance);
596 if (GetParam().supports_basis) {
597 EXPECT_EQ(result.solutions[0].basis->variable_status.at(x[i]),
599 EXPECT_EQ(result.solutions[0].basis->constraint_status.at(c[i]),
601 }
602 } else {
603 EXPECT_NEAR(variable_value, 0.0, kTolerance);
604 EXPECT_NEAR(reduced_cost, 1.0, kTolerance);
605 EXPECT_NEAR(dual_value, 0.0, kTolerance);
606 if (GetParam().supports_basis) {
607 EXPECT_EQ(result.solutions[0].basis->variable_status.at(x[i]),
609 EXPECT_EQ(result.solutions[0].basis->constraint_status.at(c[i]),
611 }
612 }
613 }
614 EXPECT_GT(variable_values_at_one, 0);
615 EXPECT_LT(variable_values_at_one, n);
616 if (GetParam().primal_solution_status_always_set) {
617 EXPECT_EQ(result.solutions[0].primal_solution->feasibility_status,
619 } else {
620 EXPECT_NE(result.solutions[0].primal_solution->feasibility_status,
622 }
623 EXPECT_NE(result.solutions[0].dual_solution->feasibility_status,
625 if (GetParam().check_primal_objective) {
626 EXPECT_NEAR(variable_values_at_one,
627 result.solutions[0].primal_solution->objective_value,
628 kTolerance);
629 } else {
630 LOG(INFO) << "Skipping primal objective check as solver does not "
631 "reliably support it for incomplete solves.";
632 }
633}
634
635// Primal model:
636// max x[0] + ... + x[n]
637// s.t.
638// Constraints: x[0] + ... + x[n] >= 1 (y)
639// Variable bounds: 0 <= x[i] <= 2 (r[i]) for all i in {0,...,n}
640//
641// Dual model (go/mathopt-dual):
642//
643// min y + 2 * r[1] + ... + 2 * r[n]
644//
645// y + r[i] == 1 for all i in {1,...,n}
646// y <= 0
647//
648// Basic solutions:
649//
650// All basis can be described by disjoint subsets I, J of {1,...,n} such that
651// 0 <= |J| <= 1 (I indicates variables at their upper bounds, and J indicates a
652// possible basic variable).
653//
654// If |J| = 0 then the basis corresponds to
655// * x[i] = 2 for all i in I, x[i] = 0 for all i not in I.
656// * r[i] = 1 for all i in {1,...,n}.
657// * x[i] is AT_UPPER_BOUND for all i in I, and x[i] is AT_LOWER_BOUND for
658// all i not in I.
659// * y = 0.
660// * the constraint associated to y is BASIC.
661// * this basis is primal feasible if the associated primal solution
662// satisfies the constraint associated to y.
663//
664// If |J| = 1 then the basis corresponds to
665// * x[i] = 2 for all i in I, x[i] = 0 for all i not in I or J, and x[i] for
666// i in J is obtained by enforcing equality in the constraint associated to
667// y.
668// * r[i] = 0 for all i in {1,...,n}.
669// * x[i] is BASIC for all i in J, x[i] is AT_UPPER_BOUND for all i in I, and
670// x[i] is AT_LOWER_BOUND for all i not in I or J.
671// * y = 1.
672// * the constraint associated to y is AT_LOWER_BOUND.
673// * this basis is primal feasible if the value of 0 <= x[i] <= 2 for i in J.
674//
675// The only dual-feasible basis is I = {1,...,n} (with |J| = 0). However, the
676// dual solutions for all basis with |J| = 0 are feasible (for more details on
677// this apparent contradiction see go/mathopt-basis#dual and
678// go/mathopt-basis-advanced).
679// Test:
680//
681// We initialize the solver to start at an arbitrary solution with x[i] in
682// {0, 1} and x[1] + ... + x[n] = 1 using initial basis or by minimizing the
683// objective. We then set an iteration limit that should allow at least one
684// pivot away from this solution, but which is not long enough to reach the
685// optimal solution x[i] = 2 for all i. Finally, we check that the primal and
686// dual solution (and basis if supported) obtained under this iteration limit
687// corresponts to a basis with |J| = 0, and 0 < |I| < n (i.e. with k variables
688// at 2 for 0 < k < n).
689TEST_P(LpIncompleteSolveTest, PrimalSimplexAlgorithmRanged) {
690 if (GetParam().lp_algorithm != LPAlgorithm::kPrimalSimplex ||
691 !GetParam().supports_iteration_limit ||
692 !(GetParam().supports_incremental_solve ||
693 GetParam().supports_initial_basis)) {
694 GTEST_SKIP()
695 << "Ignoring this test as it requires support for primal simplex, "
696 "iteration limit and either incremental solve or initial basis.";
697 }
698 int n = 10;
699 Model model("Primal Feasible Incomplete Solve LP");
700 std::vector<Variable> x;
701 for (int i = 0; i < n; ++i) {
702 x.push_back(model.AddContinuousVariable(0.0, 2.0));
703 }
704 LinearConstraint c = model.AddLinearConstraint(Sum(x) >= 1);
706 const std::unique_ptr<IncrementalSolver> incremental_solver,
707 NewIncrementalSolver(&model, TestedSolver()));
708
709 SolveArguments args;
711 if (GetParam().supports_presolve) {
713 }
714
715 if (GetParam().supports_initial_basis) {
716 Basis initial_basis;
717 initial_basis.variable_status.emplace(x[0], BasisStatus::kBasic);
718 for (int i = 1; i < n; ++i) {
719 initial_basis.variable_status.emplace(x[i], BasisStatus::kAtLowerBound);
720 }
721 initial_basis.constraint_status.emplace(c, BasisStatus::kAtLowerBound);
722 args.model_parameters.initial_basis = initial_basis;
723 } else {
724 model.Minimize(Sum(x));
725 ASSERT_OK(incremental_solver->Solve(args));
726 }
727 model.Maximize(Sum(x));
729 ASSERT_OK_AND_ASSIGN(const SolveResult result,
730 incremental_solver->Solve(args));
731 if (GetParam().primal_solution_status_always_set) {
734 /*allow_limit_undetermined=*/true));
735 } else {
737 /*allow_limit_undetermined=*/true));
738 }
739 if (GetParam().supports_basis) {
740 EXPECT_TRUE(result.has_basis());
741 EXPECT_EQ(result.solutions[0].basis->constraint_status.at(c),
743 } else {
744 LOG(INFO) << "Skipping basis check as solver does not return a basis.";
745 }
746 int variable_values_at_two = 0;
747 ASSERT_FALSE(result.solutions.empty());
748 ASSERT_TRUE(result.solutions[0].primal_solution.has_value());
749 ASSERT_TRUE(result.solutions[0].dual_solution.has_value());
750 EXPECT_NEAR(result.solutions[0].dual_solution->dual_values.at(c), 0.0,
751 kTolerance);
752 for (int i = 0; i < n; ++i) {
753 SCOPED_TRACE(absl::StrCat(i));
754 const double variable_value =
755 result.solutions[0].primal_solution->variable_values.at(x[i]);
756 const double reduced_cost =
757 result.solutions[0].dual_solution->reduced_costs.at(x[i]);
758 // Gurobi is not consistent with reduced cost signs in this test. For some
759 // variables AT_UPPER_BOUND with value 2.0 it returns a reduced cost of
760 // -1.0 and for some it returns 1.0.
761 // TODO(b/195295177): Create a simple example to file a bug with Gurobi.
762 if (TestedSolver() != SolverType::kGurobi) {
763 EXPECT_NEAR(reduced_cost, 1.0, kTolerance);
764 }
765 if (std::abs(variable_value - 2.0) <= kTolerance) {
766 ++variable_values_at_two;
767 if (GetParam().supports_basis && result.has_basis()) {
768 EXPECT_EQ(result.solutions[0].basis->variable_status.at(x[i]),
770 }
771 } else {
772 if (GetParam().supports_basis && result.has_basis()) {
773 EXPECT_EQ(result.solutions[0].basis->variable_status.at(x[i]),
775 }
776 }
777 }
778 EXPECT_GT(variable_values_at_two, 0);
779 EXPECT_LT(variable_values_at_two, n);
780 // We only check the primal feasibility status. As noted above, while the
781 // expected basis is not dual-feasible, the expected dual solution is
782 // feasible. Most solvers evaluate dual feasibility with respect to the
783 // basis and hence return an infeasible status for the dual solution.
784 if (GetParam().primal_solution_status_always_set) {
785 EXPECT_EQ(result.solutions[0].primal_solution->feasibility_status,
787 } else {
788 EXPECT_NE(result.solutions[0].primal_solution->feasibility_status,
790 }
791 if (GetParam().check_primal_objective) {
792 EXPECT_NEAR(2 * variable_values_at_two,
793 result.solutions[0].primal_solution->objective_value,
794 kTolerance);
795 } else {
796 LOG(INFO) << "Skipping primal objective check as solver does not "
797 "reliably support it for incomplete solves.";
798 }
799}
800
801// Primal model:
802// max x[0] + ... + x[n]
803// s.t.
804// Constraints: x[i] <= 1 (y[i]) for all i in {0,...,n}
805// Variable bounds: 0 <= x[i] <= 2 (r[i]) for all i in {0,...,n}
806//
807// Dual model (go/mathopt-dual):
808//
809// min y[0] + ... + y[n]
810//
811// y[i] + r[i] == 1 for all i in {1,...,n}
812// y[i] >= 0 for all i in {1,...,n}
813//
814// Basic solutions:
815//
816// All basis can be described by a subset I1, I2 of {1,...,n} that describes the
817// basis and solutions as follows (I1 indicates variables at 1 and I2 indicates
818// variables at 2):
819// * x[i] = 1 for all i in I1, x[i] = 2 for all i in I2, x[i] = 0 for all i
820// not in I1 or I2.
821// * r[i] = 0 for all i in I1, r[i] = 1 for all i not in I1.
822// * x[i] is BASIC for all i in I1, x[i] is AT_UPPER_BOUND for all i in I2,
823// x[i] is AT_LOWER_BOUND for all i not in I1 or I2.
824// * y[i] = 1 for all i in I1, y[i] = 0 for all i not in I1.
825// * the constraint associated to y[i] is AT_UPPER_BOUND for all i in I1, and
826// BASIC for all i not in I1.
827//
828// All basis are dual feasible, but only basis with empty I2 are primal
829// feasible.
830//
831// Test:
832//
833// We initialize the solver to start at solution x[i] = 2 for all i in {1,...,n}
834// using initial basis (I2 = {1,..,n}). We then set an iteration limit that
835// should allow at least one pivot away from this solution, but which is not
836// long enough to reach the optimal solution x[i] = 1 for all i. Finally, we
837// check that the primal and dual solution (and basis if supported) obtained
838// under this iteration limit corresponts to a basis (I1,I2) with 0 < |I2| < n
839// and |I1| + |I2| = n (i.e. with k variables at 2 and n-k variables at 1 for
840// 0 < k < n).
841TEST_P(LpIncompleteSolveTest, DualSimplexAlgorithmInitialBasis) {
842 if (GetParam().lp_algorithm != LPAlgorithm::kDualSimplex ||
843 !GetParam().supports_iteration_limit ||
844 !GetParam().supports_initial_basis) {
845 GTEST_SKIP()
846 << "Ignoring this test as it requires support for dual simplex, "
847 "iteration limit and initial basis.";
848 }
849 int n = 10;
850 Model model("Dual Feasible Incomplete Solve LP");
851 std::vector<Variable> x;
852 std::vector<LinearConstraint> c;
853 for (int i = 0; i < n; ++i) {
854 x.push_back(model.AddContinuousVariable(0.0, 2.0));
855 c.push_back(model.AddLinearConstraint(x[i] <= 1));
856 }
857 model.Maximize(Sum(x));
858
859 SolveArguments args;
861 if (GetParam().supports_presolve) {
863 }
864 Basis initial_basis;
865 for (int i = 0; i < n; ++i) {
866 initial_basis.variable_status.emplace(x[i], BasisStatus::kAtUpperBound);
867 initial_basis.constraint_status.emplace(c[i], BasisStatus::kBasic);
868 }
869 args.model_parameters.initial_basis = initial_basis;
871
872 ASSERT_OK_AND_ASSIGN(const SolveResult result,
873 Solve(model, TestedSolver(), args));
876 /*allow_limit_undetermined=*/true));
877 if (GetParam().supports_basis) {
878 EXPECT_TRUE(result.has_basis());
879 } else {
880 LOG(INFO) << "Skipping basis check as solver does not return a basis.";
881 }
882 int variable_values_at_two = 0;
883 ASSERT_FALSE(result.solutions.empty());
884 ASSERT_TRUE(result.solutions[0].primal_solution.has_value());
885 ASSERT_TRUE(result.solutions[0].dual_solution.has_value());
886 for (int i = 0; i < n; ++i) {
887 SCOPED_TRACE(absl::StrCat(i));
888 const double variable_value =
889 result.solutions[0].primal_solution->variable_values.at(x[i]);
890 const double reduced_cost =
891 result.solutions[0].dual_solution->reduced_costs.at(x[i]);
892 const double dual_value =
893 result.solutions[0].dual_solution->dual_values.at(c[i]);
894 if (std::abs(variable_value - 2.0) <= kTolerance) {
895 EXPECT_NEAR(reduced_cost, 1.0, kTolerance);
896 EXPECT_NEAR(dual_value, 0.0, kTolerance);
897 ++variable_values_at_two;
898 if (GetParam().supports_basis && result.has_basis()) {
899 EXPECT_EQ(result.solutions[0].basis->variable_status.at(x[i]),
901 EXPECT_EQ(result.solutions[0].basis->constraint_status.at(c[i]),
903 }
904 } else {
905 EXPECT_NEAR(variable_value, 1.0, kTolerance);
906 EXPECT_NEAR(reduced_cost, 0.0, kTolerance);
907 EXPECT_NEAR(dual_value, 1.0, kTolerance);
908 if (GetParam().supports_basis && result.has_basis()) {
909 EXPECT_EQ(result.solutions[0].basis->variable_status.at(x[i]),
911 EXPECT_EQ(result.solutions[0].basis->constraint_status.at(c[i]),
913 }
914 }
915 }
916 EXPECT_GT(variable_values_at_two, 0);
917 EXPECT_LT(variable_values_at_two, n);
918 if (GetParam().primal_solution_status_always_set) {
919 EXPECT_EQ(result.solutions[0].primal_solution->feasibility_status,
921 } else {
922 EXPECT_NE(result.solutions[0].primal_solution->feasibility_status,
924 }
925 if (GetParam().dual_solution_status_always_set) {
926 EXPECT_EQ(result.solutions[0].dual_solution->feasibility_status,
928 } else {
929 EXPECT_NE(result.solutions[0].dual_solution->feasibility_status,
931 }
932 if (GetParam().check_primal_objective) {
933 EXPECT_NEAR(2 * variable_values_at_two + 1 * (n - variable_values_at_two),
934 result.solutions[0].primal_solution->objective_value,
935 kTolerance);
936 } else {
937 LOG(INFO) << "Skipping primal objective check as solver does not "
938 "reliably support it for incomplete solves.";
939 }
940}
941
942// This test is identical to DetailedDualSimplexAlgorithmInitialBasis, but
943// instead of using initial basis to set the starting dual-feasible and
944// primal-infeasible basis it use incremental solve to get the desired effect.
945// This is achieved by first solving the problem without the x[i] <= 2
946// constraints, adding those constraints and re-solving using and incremental
947// solve.
948TEST_P(LpIncompleteSolveTest, DualSimplexAlgorithmIncrementalCut) {
949 if (GetParam().lp_algorithm != LPAlgorithm::kDualSimplex ||
950 !GetParam().supports_iteration_limit ||
951 !GetParam().supports_incremental_solve) {
952 GTEST_SKIP()
953 << "Ignoring this test as it requires support for dual simplex, "
954 "iteration limit and incremental solves.";
955 }
956 int n = 10;
957 Model model("dual Feasible Incomplete Solve LP");
958 std::vector<Variable> x;
959 for (int i = 0; i < n; ++i) {
960 x.push_back(model.AddContinuousVariable(0.0, 2.0));
961 }
962 model.Maximize(Sum(x));
964 const std::unique_ptr<IncrementalSolver> incremental_solver,
965 NewIncrementalSolver(&model, TestedSolver()));
966
967 ASSERT_OK(incremental_solver->Solve());
968
969 std::vector<LinearConstraint> c;
970 for (int i = 0; i < n; ++i) {
971 c.push_back(model.AddLinearConstraint(x[i] <= 1));
972 }
973 SolveArguments args;
975 if (GetParam().supports_presolve) {
977 }
979
980 ASSERT_OK_AND_ASSIGN(const SolveResult result,
981 incremental_solver->Solve(args));
984 /*allow_limit_undetermined=*/true));
985 if (GetParam().supports_basis) {
986 EXPECT_TRUE(result.has_basis());
987 } else {
988 LOG(INFO) << "Skipping basis check as solver does not return a basis.";
989 }
990 // TODO(b/195295177): reduce duplicated code using matchers.
991 int variable_values_at_two = 0;
992 ASSERT_FALSE(result.solutions.empty());
993 ASSERT_TRUE(result.solutions[0].primal_solution.has_value());
994 ASSERT_TRUE(result.solutions[0].dual_solution.has_value());
995 for (int i = 0; i < n; ++i) {
996 double variable_value =
997 result.solutions[0].primal_solution->variable_values.at(x[i]);
998 double reduced_cost =
999 result.solutions[0].dual_solution->reduced_costs.at(x[i]);
1000 double dual_value = result.solutions[0].dual_solution->dual_values.at(c[i]);
1001 if (std::abs(variable_value - 2.0) <= kTolerance) {
1002 EXPECT_NEAR(reduced_cost, 1.0, kTolerance);
1003 EXPECT_NEAR(dual_value, 0.0, kTolerance);
1004 ++variable_values_at_two;
1005 if (GetParam().supports_basis && result.has_basis()) {
1006 EXPECT_EQ(result.solutions[0].basis->variable_status.at(x[i]),
1008 EXPECT_EQ(result.solutions[0].basis->constraint_status.at(c[i]),
1010 }
1011 } else {
1012 EXPECT_NEAR(variable_value, 1.0, kTolerance);
1013 EXPECT_NEAR(reduced_cost, 0.0, kTolerance);
1014 EXPECT_NEAR(dual_value, 1.0, kTolerance);
1015 if (GetParam().supports_basis && result.has_basis()) {
1016 EXPECT_EQ(result.solutions[0].basis->variable_status.at(x[i]),
1018 EXPECT_EQ(result.solutions[0].basis->constraint_status.at(c[i]),
1020 }
1021 }
1022 }
1023 EXPECT_GT(variable_values_at_two, 0);
1024 EXPECT_LT(variable_values_at_two, n);
1025 if (GetParam().primal_solution_status_always_set) {
1026 EXPECT_EQ(result.solutions[0].primal_solution->feasibility_status,
1028 } else {
1029 EXPECT_NE(result.solutions[0].primal_solution->feasibility_status,
1031 }
1032 if (GetParam().dual_solution_status_always_set) {
1033 EXPECT_EQ(result.solutions[0].dual_solution->feasibility_status,
1035 } else {
1036 EXPECT_NE(result.solutions[0].dual_solution->feasibility_status,
1038 }
1039 if (GetParam().check_primal_objective) {
1040 EXPECT_NEAR(2 * variable_values_at_two + 1 * (n - variable_values_at_two),
1041 result.solutions[0].primal_solution->objective_value,
1042 kTolerance);
1043 } else {
1044 LOG(INFO) << "Skipping primal objective check as solver does not "
1045 "reliably support it for incomplete solves.";
1046 }
1047}
1048
1049// Algorithm: Dual simplex.
1050// Start: Primal feasible and dual infeasible basis.
1051// End: Primal feasible and dual infeasible basis.
1052//
1053// Primal model:
1054// max x[0] + ... + x[n - 1]
1055// s.t.
1056// Constraints: x[i] <= 1 (y[i]) for all i in {0,...,n - 1}
1057// Variable bounds: 0 <= x[i] (r[i]) for all i in {0,...,n - 1}
1058//
1059// Dual model (go/mathopt-dual):
1060//
1061// min y[0] + ... + y[n - 1]
1062//
1063// y[i] + r[i] == 1 for all i in {0,...,n - 1}
1064// y[i] >= 0 for all i in {0,...,n - 1}
1065// r[i] <= 0 for all i in {0,...,n - 1}
1066//
1067// Optimal solution:
1068//
1069// The unique primal/dual optimal pair is
1070// * x[i] = 1 for all i in {0,...,n - 1}
1071// * y[i] = 1 for all i in {0,...,n - 1}
1072// * r[i] = 0 for all i in {0,...,n - 1}
1073//
1074// Basic solutions:
1075//
1076// All basis can be described by a subset I of {0,...,n - 1} that describes the
1077// basis and solutions as follows (I indicates variables at their upper bounds):
1078// * x[i] = 1 for all i in I, x[i] = 0 for all i not in I.
1079// * r[i] = 0 for all i in I, r[i] = 1 for all i not in I.
1080// * x[i] is BASIC for all i in I, x[i] is AT_LOWER_BOUND for all i not in I.
1081// * y[i] = 1 for all i in I, y[i] = 0 for all i not in I.
1082// * the constraint associated to y[i] is AT_UPPER_BOUND for all i in I, and
1083// BASIC for all i not in I.
1084//
1085// All basis are primal feasible, but only I = {0,...,n - 1} is dual feasible.
1086//
1087// Test:
1088//
1089// We initialize the solver to start at solution x[i] = 0 for all i in
1090// {0,...,n - 1} using initial basis. We then set an iteration limit that may
1091// prevent phase I of dual simplex to terminate.
1092TEST_P(LpIncompleteSolveTest, PhaseIDualSimplexAlgorithm) {
1093 if (GetParam().lp_algorithm != LPAlgorithm::kDualSimplex ||
1094 !GetParam().supports_iteration_limit ||
1095 !GetParam().supports_initial_basis) {
1096 GTEST_SKIP()
1097 << "Ignoring this test as it requires support for dual simplex, "
1098 "iteration limit and initial basis.";
1099 }
1100 const int n = 10;
1101 Model model("Dual Phase I Incomplete Solve LP");
1102 std::vector<Variable> x;
1103 std::vector<LinearConstraint> c;
1104 for (int i = 0; i < n; ++i) {
1105 x.push_back(model.AddContinuousVariable(0.0, kInf));
1106 c.push_back(model.AddLinearConstraint(x[i] <= 1));
1107 }
1108
1110 const std::unique_ptr<IncrementalSolver> incremental_solver,
1111 NewIncrementalSolver(&model, TestedSolver()));
1112 SolveArguments args;
1113 args.parameters.lp_algorithm = GetParam().lp_algorithm;
1114 if (GetParam().supports_presolve) {
1116 }
1117
1118 Basis initial_basis;
1119 for (int i = 0; i < n; ++i) {
1120 initial_basis.variable_status.emplace(x[i], BasisStatus::kAtLowerBound);
1121 initial_basis.constraint_status.emplace(c[i], BasisStatus::kBasic);
1122 }
1123 args.model_parameters.initial_basis = initial_basis;
1124
1125 model.Maximize(Sum(x));
1126 args.parameters.iteration_limit = 3;
1127 ASSERT_OK_AND_ASSIGN(const SolveResult result,
1128 incremental_solver->Solve(args));
1130 /*allow_limit_undetermined=*/true));
1131
1132 ASSERT_FALSE(result.solutions.empty());
1133 ASSERT_TRUE(result.solutions[0].primal_solution.has_value());
1134 ASSERT_TRUE(result.solutions[0].dual_solution.has_value());
1135 bool primal_feasible = true;
1136 bool dual_feasible = true;
1137 int variable_values_at_one = 0;
1138 for (int i = 0; i < n; ++i) {
1139 SCOPED_TRACE(absl::StrCat(i));
1140 const double variable_value =
1141 result.solutions[0].primal_solution->variable_values.at(x[i]);
1142 const double reduced_cost =
1143 result.solutions[0].dual_solution->reduced_costs.at(x[i]);
1144 const double dual_value =
1145 result.solutions[0].dual_solution->dual_values.at(c[i]);
1146 if (std::abs(variable_value - 1.0) <= kTolerance) {
1147 ++variable_values_at_one;
1148 EXPECT_NEAR(reduced_cost, 0.0, kTolerance);
1149 EXPECT_NEAR(dual_value, 1.0, kTolerance);
1150 if (GetParam().supports_basis) {
1151 EXPECT_EQ(result.solutions[0].basis->variable_status.at(x[i]),
1153 EXPECT_EQ(result.solutions[0].basis->constraint_status.at(c[i]),
1155 }
1156 } else if (std::abs(variable_value) <= kTolerance) {
1157 dual_feasible = false;
1158 EXPECT_NEAR(reduced_cost, 1.0, kTolerance);
1159 EXPECT_NEAR(dual_value, 0.0, kTolerance);
1160 if (GetParam().supports_basis) {
1161 ASSERT_TRUE(result.has_basis());
1162 EXPECT_EQ(result.solutions[0].basis->variable_status.at(x[i]),
1164 EXPECT_EQ(result.solutions[0].basis->constraint_status.at(c[i]),
1166 }
1167 } else {
1168 EXPECT_THAT(variable_value, AnyOf(Lt(0.0), Gt(1.0)));
1169 primal_feasible = false;
1170 if (reduced_cost > kTolerance || dual_value < -kTolerance) {
1171 dual_feasible = false;
1172 }
1173 // TODO(b/195295177): Gurobi's dual simplex returns a value of
1174 // AT_UPPER_BOUND here. This was thought to be a bug, but it is actually
1175 // consistent with Gurobi's phase I dual simplex and the issue described
1176 // in b/201099290. Need to explore more.
1177 if (TestedSolver() == SolverType::kGurobi) {
1178 EXPECT_EQ(result.solutions[0].basis->variable_status.at(x[i]),
1180 }
1181 }
1182 }
1183 EXPECT_FALSE(dual_feasible);
1184 EXPECT_GT(variable_values_at_one, 0);
1185 EXPECT_LT(variable_values_at_one, n);
1186 if (GetParam().supports_basis) {
1187 ASSERT_TRUE(result.has_basis());
1188 if (!dual_feasible) {
1189 if (GetParam().dual_solution_status_always_set) {
1190 EXPECT_EQ(result.solutions[0].basis->basic_dual_feasibility,
1192 } else {
1193 EXPECT_NE(result.solutions[0].basis->basic_dual_feasibility,
1195 }
1196 }
1197 } else {
1198 LOG(INFO) << "Skipping basis check as solver does not return a basis.";
1199 }
1200 if (primal_feasible) {
1201 EXPECT_NE(result.solutions[0].primal_solution->feasibility_status,
1203 } else {
1204 EXPECT_NE(result.solutions[0].primal_solution->feasibility_status,
1206 }
1207 EXPECT_NE(result.solutions[0].dual_solution->feasibility_status,
1209}
1210
1211} // namespace
1212} // namespace math_opt
1213} // namespace operations_research
GRBmodel * model
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
ASSERT_THAT(solver->Update(), IsOkAndHolds(DidUpdate()))
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)
absl::StatusOr< std::unique_ptr< IncrementalSolver > > NewIncrementalSolver(Model *model, SolverType solver_type, SolverInitArguments arguments)
Definition solve.cc:82
@ kFeasible
Solver claims the solution is feasible.
@ kInfeasible
Solver claims the solution is infeasible.
testing::Matcher< SolveResult > TerminatesWithReasonFeasible(const Limit expected, const bool allow_limit_undetermined)
Definition matchers.cc:657
std::unique_ptr< Model > IndependentSetCompleteGraph(const bool integer, const int n)
BasisStatus
Status of a variable/constraint in a LP basis.
@ kBasic
The variable/constraint is basic.
@ kAtLowerBound
The variable/constraint is at its lower bound (which must be finite).
@ kAtUpperBound
The variable/constraint is at its upper bound (which must be finite).
testing::Matcher< SolveResult > TerminatesWithLimit(const Limit expected, const bool allow_limit_undetermined)
Definition matchers.cc:648
std::vector< typename Map::key_type > SortedKeys(const Map &map)
Definition key_types.h:87
testing::Matcher< SolveResult > TerminatesWithReasonNoSolutionFound(const Limit expected, const bool allow_limit_undetermined)
Definition matchers.cc:665
In SWIG mode, we don't want anything besides these top-level includes.
#define ASSERT_OK(expression)
#define ASSERT_OK_AND_ASSIGN(lhs, rexpr)
LinearConstraintMap< BasisStatus > constraint_status
Definition solution.h:234
VariableMap< BasisStatus > variable_status
Definition solution.h:235
Parameters for the LpIncompleteSolveTest suite below.
bool supports_presolve
Indicates if the solver supports setting the presolve emphasis.
LpIncompleteSolveTestParams(SolverType solver_type, std::optional< LPAlgorithm > lp_algorithm, bool supports_iteration_limit, bool supports_initial_basis, bool supports_incremental_solve, bool supports_basis, bool supports_presolve, bool check_primal_objective, bool primal_solution_status_always_set, bool dual_solution_status_always_set)
bool supports_iteration_limit
Indicates if the solver supports iteration limit.
bool supports_basis
Indicates if the solver supports returning a basis.
bool check_primal_objective
Indicates if we should check primal objective values.
std::optional< LPAlgorithm > lp_algorithm
The tested algorithm.
bool supports_incremental_solve
Indicates if the solver supports incremental solves.
bool supports_initial_basis
Indicates if the solver supports initial basis.
ModelSolveParameters model_parameters
Model dependent parameters, e.g. solution hint.
SolveParameters parameters
Model independent parameters, e.g. time limit.
std::optional< LPAlgorithm > lp_algorithm
Definition parameters.h:393
std::optional< int32_t > threads
If unset, use the solver default. If set, it must be >= 1.
Definition parameters.h:332