Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
lp_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// TODO(user): These tests are incomplete in a few ways; see mip_tests.cc
15// TODO(user): Expand tests so they check primal, dual and/or primal-dual
16// infeasible cases as appropriate.
18
19#include <cmath>
20#include <limits>
21#include <memory>
22#include <optional>
23#include <ostream>
24#include <utility>
25#include <vector>
26
27#include "absl/log/check.h"
28#include "absl/status/statusor.h"
29#include "gtest/gtest.h"
30#include "ortools/base/gmock.h"
34#include "ortools/math_opt/solution.pb.h"
36
37namespace operations_research {
38namespace math_opt {
39
40using ::testing::AnyOf;
41using ::testing::status::IsOkAndHolds;
42
43constexpr double kInf = std::numeric_limits<double>::infinity();
44constexpr double kTolerance = 1.0e-5;
45
47 std::unique_ptr<Model> model;
48 SolveResult expected_result;
49};
50
51std::ostream& operator<<(std::ostream& out,
52 const SimpleLpTestParameters& params) {
53 out << "{ solver_type: " << params.solver_type
54 << ", parameters: " << ProtobufShortDebugString(params.parameters.Proto())
55 << ", supports_duals: " << params.supports_duals
56 << ", supports_basis: " << params.supports_basis
57 << ", ensures_primal_ray: " << params.ensures_primal_ray
58 << ", ensures_dual_ray: " << params.ensures_dual_ray
59 << " disallows_infeasible_or_unbounded: "
60 << params.disallows_infeasible_or_unbounded << " }";
61 return out;
62}
63
64// max 0.1 + sum_{i=1}^3 (3.0 *x_i + 2.0 * y_i)
65// s.t. x_i + y_i <= 1.5 for all i \in {1,2,3} (c_i)
66// 0 <= x_i <= 1
67// 0 <= y_i <= 1 for all i \in {1,2,3}
68//
69// Optimal solution is (x_i,y_i)=(1.0, 0.5) for all i \in {0,1,2}, with
70// objective value 3*4+0.1 = 12.1.
72 : model_("incremental_solve_test"),
73 zero_(model_.AddContinuousVariable(0, 0, "zero")),
74 x_1_(model_.AddContinuousVariable(0, 1, "x_1")),
75 y_1_(model_.AddContinuousVariable(0, 1, "y_1")),
76 c_1_(model_.AddLinearConstraint(x_1_ + y_1_ <= 1.5, "c_1")),
77 x_2_(model_.AddContinuousVariable(0, 1, "x_2")),
78 y_2_(model_.AddContinuousVariable(0, 1, "y_2")),
79 c_2_(model_.AddLinearConstraint(x_2_ + y_2_ <= 1.5, "c_2")),
80 x_3_(model_.AddContinuousVariable(0, 1, "x_3")),
81 y_3_(model_.AddContinuousVariable(0, 1, "y_3")),
82 c_3_(model_.AddLinearConstraint(x_3_ + y_3_ <= 1.5, "c_3")) {
83 model_.Maximize(0.1 + 3 * (x_1_ + x_2_ + x_3_) + 2 * (y_1_ + y_2_ + y_3_));
84 solver_ = NewIncrementalSolver(&model_, TestedSolver()).value();
85 const SolveResult first_solve = solver_->Solve().value();
86 CHECK_OK(first_solve.termination.EnsureIsOptimal());
87 CHECK_LE(std::abs(first_solve.objective_value() - 12.1), kTolerance);
88}
89
90namespace {
91
92TEST_P(SimpleLpTest, ProtoNonIncrementalSolve) {
93 Model model;
94 const Variable x = model.AddContinuousVariable(0, 1, "x");
95 model.Maximize(2 * x);
96 const ModelProto proto = model.ExportModel();
98 const SolveResultProto result,
100 proto, EnumToProto(TestedSolver()),
101 /*init_args=*/{},
102 /*solve_args=*/{.parameters = GetParam().parameters.Proto()}));
103 ASSERT_EQ(result.termination().reason(), TERMINATION_REASON_OPTIMAL)
104 << ProtobufDebugString(result.termination());
105 ASSERT_GE(result.solutions_size(), 1);
106 ASSERT_TRUE(result.solutions(0).has_primal_solution());
107 EXPECT_NEAR(result.solutions(0).primal_solution().objective_value(), 2.0,
108 kTolerance);
109 EXPECT_EQ(result.solutions(0).primal_solution().feasibility_status(),
110 SOLUTION_STATUS_FEASIBLE);
111 if (GetParam().supports_duals) {
112 ASSERT_TRUE(result.solutions(0).has_dual_solution());
113 ASSERT_TRUE(result.solutions(0).dual_solution().has_objective_value());
114 EXPECT_NEAR(result.solutions(0).dual_solution().objective_value(), 2.0,
115 kTolerance);
116 EXPECT_EQ(result.solutions(0).dual_solution().feasibility_status(),
117 SOLUTION_STATUS_FEASIBLE);
118 }
119}
120
121// TODO(b/184447031): change descriptions to avoid d(y, r)/d_max(y,r) and
122// go/mathopt-doc-math#dual
123
124// Primal:
125// max 2.0*x
126// s.t.
127// 0 <= x <= 4.0
128//
129// Dual (go/mathopt-doc-math#dual):
130// min d(y, r)
131// r == 2.0
132//
133// Unique optimal primal solution is x* = 4.0.
134// Complementary slackness conditions for x*
135// (go/mathopt-dual#primal-dual-optimal-pairs) imply:
136//
137// r == 2.0,
138//
139// which has the unique solution r* = 2.0.
140TEST_P(SimpleLpTest, OneVarMax) {
141 Model model;
142 const Variable x = model.AddContinuousVariable(0.0, 4.0, "x");
143 model.Maximize(2 * x);
144 ASSERT_OK_AND_ASSIGN(const SolveResult result, SimpleSolve(model));
145
146 EXPECT_THAT(result, IsOptimalWithSolution(8.0, {{x, 4.0}}, kTolerance));
147 if (GetParam().supports_duals) {
148 EXPECT_THAT(result,
149 IsOptimalWithDualSolution(8.0, {}, {{x, 2.0}}, kTolerance));
150 }
151}
152
153// Primal:
154// min 2.0*x
155// s.t.
156// -2.4 <= x <= 4.0
157//
158// Dual (go/mathopt-doc-math#dual):
159// max d_max(y, r)
160// r == 2.0
161//
162// Unique optimal primal solution is x* = -2.4.
163// Complementary slackness conditions for x*
164// (go/mathopt-dual#primal-dual-optimal-pairs) imply:
165//
166// r == 2.0,
167//
168// which has the unique solution r* = 2.0.
169TEST_P(SimpleLpTest, OneVarMin) {
170 Model model;
171 const Variable x = model.AddContinuousVariable(-2.4, 4.0, "x");
172 model.Minimize(2 * x);
173 ASSERT_OK_AND_ASSIGN(const SolveResult result, SimpleSolve(model));
174 EXPECT_THAT(result, IsOptimalWithSolution(-4.8, {{x, -2.4}}));
175 if (GetParam().supports_duals) {
176 EXPECT_THAT(result, IsOptimalWithDualSolution(-4.8, {}, {{x, 2.0}}));
177 }
178}
179
180// For any parameter p in [-kInf, 0]
181// Primal:
182// max 2.0*x_1 + 1.0*x_2
183// s.t. p <= x_1 + x_2 <= 1.5 (y)
184// 0.0 <= x_1 <= 1.0
185// 0.0 <= x_2 <= 1.0
186//
187// Dual (go/mathopt-doc-math#dual):
188// min d(y, r)
189// y + r_1 == 2.0
190// y + r_2 == 1.0
191//
192// Unique optimal primal solution is (x*_1, x*_2) = (1.0. 0.5).
193// Complementary slackness conditions for x*
194// (go/mathopt-dual#primal-dual-optimal-pairs) imply:
195//
196// y + r_1 == 2.0
197// y + r_2 == 1.0
198// r_2 == 0.0
199//
200// which has the unique solution (y*, r*_1, r*_2) = (1.0, 1.0, 0.0).
201SolvedModel SimpleLinearConstraint(double p) {
202 auto model = std::make_unique<Model>();
203 const Variable x_1 = model->AddVariable(0.0, 1.0, false, "x_1");
204 const Variable x_2 = model->AddVariable(0.0, 1.0, false, "x_2");
205 model->Maximize(2 * x_1 + x_2);
206 const LinearConstraint y =
207 model->AddLinearConstraint(p <= x_1 + x_2 <= 1.5, "y");
208 SolveResult result{Termination::Optimal(2.5)};
209 result.solutions.push_back(Solution{
210 .primal_solution =
211 PrimalSolution{.variable_values = {{x_1, 1.0}, {x_2, 0.5}},
212 .objective_value = 2.5,
213 .feasibility_status = SolutionStatus::kFeasible},
214 .dual_solution =
215 DualSolution{.dual_values = {{y, 1.0}},
216 .reduced_costs = {{x_1, 1.0}, {x_2, 0.0}},
217 .objective_value = 2.5,
218 .feasibility_status = SolutionStatus::kFeasible}});
219 return {.model = std::move(model), .expected_result = result};
220}
221
222TEST_P(SimpleLpTest, SimpleLinearConstraintRanged) {
223 const SolvedModel solved_model = SimpleLinearConstraint(0.0);
224 EXPECT_THAT(SimpleSolve(*solved_model.model),
226 IsConsistentWith(solved_model.expected_result,
227 {.check_dual = GetParam().supports_duals})));
228}
229
230TEST_P(SimpleLpTest, SimpleLinearConstraintNonRanged) {
231 const SolvedModel solved_model = SimpleLinearConstraint(-kInf);
232 EXPECT_THAT(SimpleSolve(*solved_model.model),
234 IsConsistentWith(solved_model.expected_result,
235 {.check_dual = GetParam().supports_duals})));
236}
237
238// First extra check for possible sign issues with duals
239// For any parameter p in [1.5, kInf]
240// Primal:
241// min 2.0*x_1 + 1.0*x_2
242// s.t. 0.5 <= x_1 + x_2 <= p (y)
243// 0.0 <= x_1 <= 1.0
244// 0.0 <= x_2 <= 1.0
245//
246// Dual (go/mathopt-doc-math#dual):
247// max d_max(y, r)
248// y + r_1 == 2.0
249// y + r_2 == 1.0
250//
251// Unique optimal primal solution is (x*_1, x*_2) = (0.0. 0.5).
252// Complementary slackness conditions for x*
253// (go/mathopt-dual#primal-dual-optimal-pairs) imply:
254//
255// y + r_1 == 2.0
256// y + r_2 == 1.0
257// r_2 == 0.0
258//
259// which has the unique solution (y*, r*_1, r*_2) = (1.0, 1.0, 0.0).
260SolvedModel SimpleLinearConstraintDualMin(double p) {
261 auto model = std::make_unique<Model>();
262 const Variable x_1 = model->AddVariable(0.0, 1.0, false, "x_1");
263 const Variable x_2 = model->AddVariable(0.0, 1.0, false, "x_2");
264 model->Minimize(2 * x_1 + x_2);
265 const LinearConstraint y =
266 model->AddLinearConstraint(0.5 <= x_1 + x_2 <= p, "y");
267 SolveResult result{Termination::Optimal(0.5)};
268 result.solutions.push_back(Solution{
269 .primal_solution =
270 PrimalSolution{.variable_values = {{x_1, 0.0}, {x_2, 0.5}},
271 .objective_value = 0.5,
272 .feasibility_status = SolutionStatus::kFeasible},
273 .dual_solution =
274 DualSolution{.dual_values = {{y, 1.0}},
275 .reduced_costs = {{x_1, 1.0}, {x_2, 0.0}},
276 .objective_value = 0.5,
277 .feasibility_status = SolutionStatus::kFeasible}});
278 return {std::move(model), result};
279}
280
281TEST_P(SimpleLpTest, SimpleLinearConstraintDualMinRanged) {
282 const SolvedModel solved_model = SimpleLinearConstraintDualMin(1.5);
283 EXPECT_THAT(SimpleSolve(*solved_model.model),
285 IsConsistentWith(solved_model.expected_result,
286 {.check_dual = GetParam().supports_duals})));
287}
288
289TEST_P(SimpleLpTest, SimpleLinearConstraintDualMinNonRanged) {
290 const SolvedModel solved_model = SimpleLinearConstraintDualMin(kInf);
291 EXPECT_THAT(SimpleSolve(*solved_model.model),
293 IsConsistentWith(solved_model.expected_result,
294 {.check_dual = GetParam().supports_duals})));
295}
296
297// Second extra checks for possible sign issues with duals
298// For any parameter p in [1.5, kInf]
299// Primal:
300// max -2.0*x_1 - 1.0*x_2
301// s.t. 0.5 <= x_1 + x_2 <= p (y)
302// 0.0 <= x_1 <= 1.0
303// 0.0 <= x_2 <= 1.0
304//
305// Dual (go/mathopt-doc-math#dual):
306// min d(y, r)
307// y + r_1 == -2.0
308// y + r_2 == -1.0
309//
310// Unique optimal primal solution is (x*_1, x*_2) = (0.0. 0.5).
311// Complementary slackness conditions for x*
312// (go/mathopt-dual#primal-dual-optimal-pairs) imply:
313//
314// y + r_1 == -2.0
315// y + r_2 == -1.0
316// r_2 == 0.0
317//
318// which has the unique solution (y*, r*_1, r*_2) = (-1.0, -1.0, 0.0).
319SolvedModel SimpleLinearConstraintDualLowerBounds(double p) {
320 auto model = std::make_unique<Model>();
321 const Variable x_1 = model->AddVariable(0.0, 1.0, false, "x_1");
322 const Variable x_2 = model->AddVariable(0.0, 1.0, false, "x_2");
323 model->Maximize(-2 * x_1 - x_2);
324 const LinearConstraint y =
325 model->AddLinearConstraint(0.5 <= x_1 + x_2 <= p, "y");
326 SolveResult result{Termination::Optimal(-0.5)};
327 result.solutions.push_back(Solution{
328 .primal_solution =
329 PrimalSolution{.variable_values = {{x_1, 0.0}, {x_2, 0.5}},
330 .objective_value = -0.5,
331 .feasibility_status = SolutionStatus::kFeasible},
332 .dual_solution =
333 DualSolution{.dual_values = {{y, -1.0}},
334 .reduced_costs = {{x_1, -1.0}, {x_2, 0.0}},
335 .objective_value = -0.5,
336 .feasibility_status = SolutionStatus::kFeasible}});
337 return {std::move(model), result};
338}
339
340TEST_P(SimpleLpTest, SimpleLinearConstraintDualLowerBoundsRanged) {
341 const SolvedModel solved_model = SimpleLinearConstraintDualLowerBounds(1.5);
342 EXPECT_THAT(SimpleSolve(*solved_model.model),
344 IsConsistentWith(solved_model.expected_result,
345 {.check_dual = GetParam().supports_duals})));
346}
347
348TEST_P(SimpleLpTest, SimpleLinearConstraintDualLowerBoundsNonRanged) {
349 const SolvedModel solved_model = SimpleLinearConstraintDualLowerBounds(kInf);
350 EXPECT_THAT(SimpleSolve(*solved_model.model),
352 IsConsistentWith(solved_model.expected_result,
353 {.check_dual = GetParam().supports_duals})));
354}
355
356// Primal:
357// max 2.0*x_1 + 1.0*x_2
358// s.t. -1.0 <= x_1 - x_2 <= 1.0 (y)
359// x_1 >= 0
360// x_2 >= 0
361//
362// Problem is unbounded: the only ray (up to scaling) is (x_1, x_2) = (1.0, 1.0)
363// If ranged = false, separate (y) into two single-sided inequalities.
364SolvedModel SimpleUnboundedLP(bool ranged) {
365 auto model = std::make_unique<Model>();
366 const Variable x_1 = model->AddContinuousVariable(0.0, kInf, "x_1");
367 const Variable x_2 = model->AddContinuousVariable(0.0, kInf, "x_2");
368 model->Maximize(2 * x_1 + x_2);
369 if (ranged) {
370 model->AddLinearConstraint(-1 <= x_1 - x_2 <= 1, "y");
371 } else {
372 model->AddLinearConstraint(-1 <= x_1 - x_2, "y_1");
373 model->AddLinearConstraint(x_1 - x_2 <= 1, "y_2");
374 }
375 SolveResult result{Termination::Unbounded(/*is_maximize=*/true)};
376 result.primal_rays.emplace_back().variable_values = {{x_1, 1.0}, {x_2, 1.0}};
377 return {std::move(model), result};
378}
379
380SolveResultMatcherOptions PrimalRayMatchOptions(
381 const SimpleLpTestParameters& test_params, const SolveResult& actual) {
382 SolveResultMatcherOptions matcher_options;
383 matcher_options.inf_or_unb_soft_match =
384 !test_params.disallows_infeasible_or_unbounded;
385 matcher_options.check_rays =
386 test_params.ensures_primal_ray || actual.has_ray();
387 return matcher_options;
388}
389
390TEST_P(SimpleLpTest, SimpleRangedRay) {
391 const SolvedModel solved_model = SimpleUnboundedLP(true);
392 ASSERT_OK_AND_ASSIGN(const SolveResult actual,
393 SimpleSolve(*solved_model.model));
394 EXPECT_THAT(actual,
395 IsConsistentWith(solved_model.expected_result,
396 PrimalRayMatchOptions(GetParam(), actual)));
397}
398
399TEST_P(SimpleLpTest, SimpleNonRangedRay) {
400 const SolvedModel solved_model = SimpleUnboundedLP(false);
401 ASSERT_OK_AND_ASSIGN(const SolveResult actual,
402 SimpleSolve(*solved_model.model));
403 EXPECT_THAT(actual,
404 IsConsistentWith(solved_model.expected_result,
405 PrimalRayMatchOptions(GetParam(), actual)));
406}
407
408// TODO(b/183600770): add simple version of these tests.
409// For any parameter p in [-kInf, -1.0]
410// Primal:
411// min 2.0*x_1 + 1.0*x_2
412// s.t. p <= x_1 + x_2 <= -1.0 (y)
413// 0.0 <= x_1 <= 3.0
414// 0.0 <= x_2 <= 3.0
415//
416// Dual (go/mathopt-doc-math#dual):
417// max d(y, r)
418// y + r_1 == 2.0
419// y + r_2 == 1.0
420//
421// The primal is infeasible and the dual is unbounded.
422//
423// Dual ray / primal infeasibility certificate must satisfy
424// (go/mathopt-solutions#primal-inf-cert):
425//
426// y + r_1 == 0.0
427// y + r_2 == 0.0
428// {p*y : y > 0} + {-1.0*y : y < 0}
429// + {3.0*r_1 : r_1 < 0} + {3.0*r_2 : r_2 < 0} > 0
430//
431// Because p <= -1.0, the only solution (up to scaling) is
432// (y, r_1, r_2) = (-1.0, 1.0, 1.0).
433SolvedModel SimpleInfeasibleLPMin(double p) {
434 auto model = std::make_unique<Model>();
435 const Variable x_1 = model->AddContinuousVariable(0, 3, "x_1");
436 const Variable x_2 = model->AddContinuousVariable(0, 3, "x_2");
437 model->Minimize(2 * x_1 + x_2);
438 const LinearConstraint y =
439 model->AddLinearConstraint(p <= x_1 + x_2 <= -1, "y");
440 SolveResult result{Termination::Infeasible(
441 /*is_maximize=*/false,
442 /*dual_feasibility_status=*/FeasibilityStatus::kFeasible)};
443 result.dual_rays.push_back(
444 {.dual_values = {{y, -1.0}}, .reduced_costs = {{x_1, 1.0}, {x_2, 1.0}}});
445 return {std::move(model), result};
446}
447
448SolveResultMatcherOptions DualUnboundedMatchOptions(
449 const SimpleLpTestParameters& test_params, const SolveResult& actual) {
450 SolveResultMatcherOptions matcher_options;
451 // NOTE: this assumes that primal is infeasible and the dual is unbounded, see
452 // inf_or_unb_soft_match documentation for details.
453 matcher_options.inf_or_unb_soft_match = false;
454 // TODO(b/211045017): remove this hardcoded edge case
455 if (test_params.solver_type == SolverType::kGlpk &&
456 test_params.parameters.lp_algorithm == LPAlgorithm::kBarrier) {
457 matcher_options.inf_or_unb_soft_match = true;
458 }
459 matcher_options.check_rays =
460 test_params.ensures_dual_ray || actual.has_dual_ray();
461 return matcher_options;
462}
463
464TEST_P(SimpleLpTest, SimpleRangedInfeasibleMin) {
465 const SolvedModel solved_model = SimpleInfeasibleLPMin(-2.0);
466 ASSERT_OK_AND_ASSIGN(const SolveResult actual,
467 SimpleSolve(*solved_model.model));
468 EXPECT_THAT(actual,
469 IsConsistentWith(solved_model.expected_result,
470 DualUnboundedMatchOptions(GetParam(), actual)));
471}
472
473TEST_P(SimpleLpTest, SimpleNonRangedInfeasibleMin) {
474 const SolvedModel solved_model = SimpleInfeasibleLPMin(-kInf);
475 ASSERT_OK_AND_ASSIGN(const SolveResult actual,
476 SimpleSolve(*solved_model.model));
477 EXPECT_THAT(actual,
478 IsConsistentWith(solved_model.expected_result,
479 DualUnboundedMatchOptions(GetParam(), actual)));
480}
481
482// For any parameter p in [-kInf, -1.0]
483// Primal:
484// max 2.0*x_1 + 1.0*x_2
485// s.t. p <= x_1 + x_2 <= -1.0 (y)
486// 0.0 <= x_1 <= 3.0
487// 0.0 <= x_2 <= 3.0
488//
489// Dual (go/mathopt-doc-math#dual):
490// min d_max(y, r)
491// y + r_1 == 2.0
492// y + r_2 == 1.0
493//
494// Problem is primal infeasible and dual unbounded.
495//
496// Dual ray / primal infeasibility certificate must satisfy
497// (go/mathopt-solutions#primal-inf-cert):
498//
499// y + r_1 == 0.0
500// y + r_2 == 0.0
501// {-1.0*y : y > 0} + {p*y : y < 0}
502// + {3.0*r_1 : r_1 > 0} + {3.0*r_2 : r_2 > 0} < 0
503//
504// Because p <= -1.0, the only solution (up to scaling) is
505// (y, r_1, r_2) = (1.0, -1.0, -1.0).
506SolvedModel SimpleInfeasibleLPMax(double p) {
507 auto model = std::make_unique<Model>();
508 const Variable x_1 = model->AddContinuousVariable(0, 3, "x_1");
509 const Variable x_2 = model->AddContinuousVariable(0, 3, "x_2");
510 model->Maximize(2 * x_1 + x_2);
511 const LinearConstraint y =
512 model->AddLinearConstraint(p <= x_1 + x_2 <= -1, "y");
513 SolveResult result{Termination::Infeasible(
514 /*is_maximize=*/true,
515 /*dual_feasibility_status=*/FeasibilityStatus::kFeasible)};
516 result.dual_rays.push_back(
517 {.dual_values = {{y, 1.0}}, .reduced_costs = {{x_1, -1.0}, {x_2, -1.0}}});
518 return {std::move(model), result};
519}
520
521TEST_P(SimpleLpTest, SimpleRangedInfeasibleMax) {
522 const SolvedModel solved_model = SimpleInfeasibleLPMax(-2.0);
523 ASSERT_OK_AND_ASSIGN(const SolveResult actual,
524 SimpleSolve(*solved_model.model));
525 EXPECT_THAT(actual,
526 IsConsistentWith(solved_model.expected_result,
527 DualUnboundedMatchOptions(GetParam(), actual)));
528}
529
530TEST_P(SimpleLpTest, SimpleNonRangedInfeasibleMax) {
531 const SolvedModel solved_model = SimpleInfeasibleLPMax(-kInf);
532 ASSERT_OK_AND_ASSIGN(const SolveResult actual,
533 SimpleSolve(*solved_model.model));
534 EXPECT_THAT(actual,
535 IsConsistentWith(solved_model.expected_result,
536 DualUnboundedMatchOptions(GetParam(), actual)));
537}
538
539// For p in [2.0, kInf]
540// Primal:
541// max x_2
542// s.t. - p <= x_1 + x_2 <= 2.0 (y_1)
543// -2.0 <= x_1 - x_2 <= p (y_2)
544// -1.0 <= x_1 <= 1.0
545// 0.0 <= x_2 <= kInf
546//
547// Dual (go/mathopt-doc-math#dual):
548// min d_max(y, r)
549// y_1 + y_2 + r_1 == 0.0
550// y_1 - y_2 + r_2 == 1.0
551//
552// Unique optimal primal solution is (x*_1, x*_2) = (0.0. 2.0).
553// Complementary slackness conditions for x*
554// (go/mathopt-dual#primal-dual-optimal-pairs) imply (note that we have
555// a maximization problem so the inequalities in the TIP environment hold):
556//
557// y_1 + y_2 + r_1 == 0.0
558// y_1 - y_2 + r_2 == 1.0
559// r_1 == 0.0
560// r_2 == 0.0
561// y_1 >= 0.0
562// y_2 <= 0.0
563//
564// which has the unique solution
565// (y*_1, y*_2, r*_1, r*_2) = (0.5, -0.5, 0.0, 0.0).
566//
567// From go/mathopt-basis#primal we have C = {1, 2}, V = {},
568//
569// s^c_1 = AT_UPPER_BOUND
570// s^c_2 = AT_LOWER_BOUND
571// s^v_1 = BASIC
572// s^v_2 = BASIC
573//
574// We can check that these statuses are compatible with the dual feasibility
575// conditions in go/mathopt-basis#dual (note again that we have
576// a maximization problem so the inequalities in the IMPORTANT environment
577// hold).
578SolvedModel ConstraintDefinedBasisLP(const double p) {
579 auto model = std::make_unique<Model>();
580 const Variable x_1 = model->AddContinuousVariable(-1, 1, "x_1");
581 const Variable x_2 = model->AddContinuousVariable(0, kInf, "x_2");
582 model->Maximize(x_2);
583 const LinearConstraint y_1 =
584 model->AddLinearConstraint(-p <= x_1 + x_2 <= 2, "y_1");
585 const LinearConstraint y_2 =
586 model->AddLinearConstraint(-2 <= x_1 - x_2 <= p, "y_2");
587 SolveResult result{Termination::Optimal(2.0)};
588 result.solutions.push_back(Solution{
589 .primal_solution =
590 PrimalSolution{.variable_values = {{x_1, 0.0}, {x_2, 2.0}},
591 .objective_value = 2.0,
592 .feasibility_status = SolutionStatus::kFeasible},
593 .dual_solution =
594 DualSolution{.dual_values = {{y_1, 0.5}, {y_2, -0.5}},
595 .reduced_costs = {{x_1, 0.0}, {x_2, 0.0}},
596 .objective_value = 2.0,
597 .feasibility_status = SolutionStatus::kFeasible},
598 .basis = Basis{.constraint_status = {{y_1, BasisStatus::kAtUpperBound},
600 .variable_status = {{x_1, BasisStatus::kBasic},
601 {x_2, BasisStatus::kBasic}},
602 .basic_dual_feasibility = SolutionStatus::kFeasible}});
603 return {std::move(model), result};
604}
605
606TEST_P(SimpleLpTest, ConstraintDefinedBasisLPRanged) {
607 if (!GetParam().supports_basis) {
608 GTEST_SKIP()
609 << "Getting the basis is not supported for this config, skipping test.";
610 }
611 const SolvedModel solved_model = ConstraintDefinedBasisLP(2.0);
612 EXPECT_THAT(Solve(*solved_model.model, TestedSolver()),
613 IsOkAndHolds(IsConsistentWith(solved_model.expected_result,
614 {.check_basis = true})));
615}
616
617TEST_P(SimpleLpTest, ConstraintDefinedBasisLPNonRanged) {
618 if (!GetParam().supports_basis) {
619 GTEST_SKIP()
620 << "Getting the basis is not supported for this config, skipping test.";
621 }
622 const SolvedModel solved_model = ConstraintDefinedBasisLP(kInf);
623 EXPECT_THAT(Solve(*solved_model.model, TestedSolver()),
624 IsOkAndHolds(IsConsistentWith(solved_model.expected_result,
625 {.check_basis = true})));
626}
627
628// For p in [2.0, kInf]
629// Primal:
630// max 2.0*x_1 + x_2
631// s.t. - p <= x_1 + x_2 <= 2.0 (y_1)
632// -2.0 <= x_1 - x_2 <= p (y_2)
633// -1.0 <= x_1 <= 1.0
634// 0.0 <= x_2 <= kInf
635//
636// Dual (go/mathopt-doc-math#dual):
637// min d_max(y, r)
638// y_1 + y_2 + r_1 == 2.0
639// y_1 - y_2 + r_2 == 1.0
640//
641// Unique optimal primal solution is (x*_1, x*_2) = (1.0. 1.0).
642// Complementary slackness conditions for x*
643// (go/mathopt-dual#primal-dual-optimal-pairs) imply (note that we have
644// a maximization problem so the inequalities in the TIP environment hold):
645//
646// y_1 + y_2 + r_1 == 2.0
647// y_1 - y_2 + r_2 == 1.0
648// y_2 == 0.0
649// r_2 == 0.0
650// y_1 >= 0.0
651// r_1 >= 0.0
652//
653// which has the unique solution
654// (y*_1, y*_2, r*_1, r*_2) = (1.0, 0.0, 1.0, 0.0).
655//
656// From go/mathopt-basis#primal we have C = {1}, V = {1},
657//
658// s^c_1 = AT_UPPER_BOUND
659// s^c_2 = BASIC
660// s^v_1 = AT_UPPER_BOUND
661// s^v_2 = BASIC
662//
663// We can check that these statuses are compatible with the dual feasibility
664// conditions in go/mathopt-basis#dual (note again that we have
665// a maximization problem so the inequalities in the TIP environment hold).
666SolvedModel ConstraintVariableDefinedBasisLP(const double p) {
667 auto model = std::make_unique<Model>();
668 model->set_is_maximize(true);
669 const Variable x_1 = model->AddContinuousVariable(-1.0, 1.0, "x_1");
670 const Variable x_2 = model->AddContinuousVariable(0.0, kInf, "x_2");
671 model->Maximize(2 * x_1 + x_2);
672 const LinearConstraint y_1 =
673 model->AddLinearConstraint(-p <= x_1 + x_2 <= 2.0, "y_1");
674 const LinearConstraint y_2 =
675 model->AddLinearConstraint(-2.0 <= x_1 - x_2 <= p, "y_2");
676
677 SolveResult result{Termination::Optimal(3.0)};
678 result.solutions.push_back(Solution{
679 .primal_solution =
680 PrimalSolution{.variable_values = {{x_1, 1.0}, {x_2, 1.0}},
681 .objective_value = 3,
682 .feasibility_status = SolutionStatus::kFeasible},
683 .dual_solution =
684 DualSolution{.dual_values = {{y_1, 1.0}, {y_2, 0.0}},
685 .reduced_costs = {{x_1, 1.0}, {x_2, 0.0}},
686 .objective_value = 3.0,
687 .feasibility_status = SolutionStatus::kFeasible},
688 .basis = Basis{.constraint_status = {{y_1, BasisStatus::kAtUpperBound},
689 {y_2, BasisStatus::kBasic}},
690 .variable_status = {{x_1, BasisStatus::kAtUpperBound},
691 {x_2, BasisStatus::kBasic}},
692 .basic_dual_feasibility = SolutionStatus::kFeasible}});
693
694 return {std::move(model), result};
695}
696
697TEST_P(SimpleLpTest, ConstraintVariableDefinedBasisLPRanged) {
698 if (!GetParam().supports_basis) {
699 GTEST_SKIP()
700 << "Getting the basis is not supported for this config, skipping test.";
701 }
702 const SolvedModel solved_model = ConstraintVariableDefinedBasisLP(2.0);
703 EXPECT_THAT(Solve(*solved_model.model, TestedSolver()),
704 IsOkAndHolds(IsConsistentWith(solved_model.expected_result,
705 {.check_basis = true})));
706}
707
708TEST_P(SimpleLpTest, ConstraintVariableDefinedBasisLPNonRanged) {
709 if (!GetParam().supports_basis) {
710 GTEST_SKIP()
711 << "Getting the basis is not supported for this config, skipping test.";
712 }
713 const SolvedModel solved_model = ConstraintVariableDefinedBasisLP(kInf);
714 EXPECT_THAT(Solve(*solved_model.model, TestedSolver()),
715 IsOkAndHolds(IsConsistentWith(solved_model.expected_result,
716 {.check_basis = true})));
717}
718
719// For p in [2.0, kInf]
720// Primal:
721// min x_1 + x_2
722// s.t. - p <= x_1 + x_2 <= 2.0 (y_1)
723// -2.0 <= x_1 - x_2 <= p (y_2)
724// -1.0 <= x_1 <= 1.0
725// 0.0 <= x_2 <= kInf
726//
727// Dual (go/mathopt-doc-math#dual):
728// min d(y, r)
729// y_1 + y_2 + r_1 == 1.0
730// y_1 - y_2 + r_2 == 1.0
731//
732// Unique optimal primal solution is (x*_1, x*_2) = (-1.0. 0.0).
733// Complementary slackness conditions for x*
734// (go/mathopt-dual#primal-dual-optimal-pairs) imply:
735//
736// y_1 + y_2 + r_1 == 1.0
737// y_1 - y_2 + r_2 == 1.0
738// y_1 == 0.0
739// y_2 == 0.0
740// r_1 >= 0.0
741// r_2 >= 0.0
742//
743// which has the unique solution
744// (y*_1, y*_2, r*_1, r*_2) = (0.0, 0.0, 1.0, 1.0).
745//
746// From go/mathopt-basis#primal we have C = {}, V = {1, 2},
747//
748// s^c_1 = BASIC
749// s^c_2 = BASIC
750// s^v_1 = AT_LOWER_BOUND
751// s^v_2 = AT_LOWER_BOUND
752//
753// We can check that these statuses are compatible with the dual feasibility
754// conditions in go/mathopt-basis#dual.
755SolvedModel VariableDefinedBasisLP(const double p) {
756 auto model = std::make_unique<Model>();
757 const Variable x_1 = model->AddContinuousVariable(-1.0, 1.0, "x_1");
758 const Variable x_2 = model->AddContinuousVariable(0.0, kInf, "x_2");
759 model->Minimize(x_1 + x_2);
760 const LinearConstraint y_1 =
761 model->AddLinearConstraint(-p <= x_1 + x_2 <= 2.0, "y_1");
762 const LinearConstraint y_2 =
763 model->AddLinearConstraint(-2.0 <= x_1 - x_2 <= p, "y_2");
764
765 SolveResult result{Termination::Optimal(-1.0)};
766 result.solutions.push_back(Solution{
767 .primal_solution =
768 PrimalSolution{.variable_values = {{x_1, -1.0}, {x_2, 0.0}},
769 .objective_value = -1,
770 .feasibility_status = SolutionStatus::kFeasible},
771 .dual_solution =
772 DualSolution{.dual_values = {{y_1, 0.0}, {y_2, 0.0}},
773 .reduced_costs = {{x_1, 1.0}, {x_2, 1.0}},
774 .objective_value = -1,
775 .feasibility_status = SolutionStatus::kFeasible},
776 .basis = Basis{.constraint_status = {{y_1, BasisStatus::kBasic},
777 {y_2, BasisStatus::kBasic}},
778 .variable_status = {{x_1, BasisStatus::kAtLowerBound},
780 .basic_dual_feasibility = SolutionStatus::kFeasible}});
781 return {std::move(model), result};
782}
783
784TEST_P(SimpleLpTest, VariableDefinedBasisLPRanged) {
785 if (!GetParam().supports_basis) {
786 GTEST_SKIP()
787 << "Getting the basis is not supported for this config, skipping test.";
788 }
789 const SolvedModel solved_model = VariableDefinedBasisLP(2.0);
790 EXPECT_THAT(Solve(*solved_model.model, TestedSolver()),
791 IsOkAndHolds(IsConsistentWith(solved_model.expected_result,
792 {.check_basis = true})));
793}
794
795TEST_P(SimpleLpTest, VariableDefinedBasisLPNonRanged) {
796 if (!GetParam().supports_basis) {
797 GTEST_SKIP()
798 << "Getting the basis is not supported for this config, skipping test.";
799 }
800 const SolvedModel solved_model = VariableDefinedBasisLP(kInf);
801 EXPECT_THAT(Solve(*solved_model.model, TestedSolver()),
802 IsOkAndHolds(IsConsistentWith(solved_model.expected_result,
803 {.check_basis = true})));
804}
805
806// Primal:
807// max x_1 + x_2
808// s.t. 0.0 <= - x_1 + x_2 <= 0.0 (y_1)
809// 0.0 <= x_1 <= 1.0
810// -kInf <= x_2 <= kInf
811//
812// Dual (go/mathopt-doc-math#dual):
813// min d_max(y, r)
814// -y_1 + r_1 == 1.0
815// y_1 + r_2 == 1.0
816//
817// Unique optimal primal solution is (x*_1, x*_2) = (1.0. 1.0).
818// Complementary slackness conditions for x*
819// (go/mathopt-dual#primal-dual-optimal-pairs) imply (note that we have
820// a maximization problem so the inequalities in the TIP environment hold):
821//
822// -y_1 + r_1 == 1.0
823// y_1 + r_2 == 1.0
824// r_2 == 0.0
825// r_1 >= 0.0
826//
827// which has the unique solution (y_1*, r*_1, r*_2) = (1.0, 2.0, 0.0).
828// Dual feasibility of the basis (go/mathopt-basis#dual) and the sign
829// of y*_1 imply that if the status of constraint (y_1) is not FIXED_VALUE, then
830// it must be AT_UPPER_BOUND (note again that we have a maximization problem so
831// the inequalities in the TIP environment hold). We can confirm this logic by
832// noting that if we only keep the upper bound of constraint (y_1), the problem
833// is unchanged.
834TEST_P(SimpleLpTest, FixedBasis) {
835 if (!GetParam().supports_basis) {
836 GTEST_SKIP()
837 << "Getting the basis is not supported for this config, skipping test.";
838 }
839
840 Model model;
841 const Variable x_1 = model.AddContinuousVariable(0.0, 1.0, "x_1");
842 const Variable x_2 = model.AddContinuousVariable(-kInf, kInf, "x_2");
843 model.Maximize(x_1 + x_2);
844 const LinearConstraint y_1 =
845 model.AddLinearConstraint(-x_1 + x_2 == 0.0, "y_1");
846 ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(model, TestedSolver()));
847 ASSERT_THAT(result, IsOptimalWithSolution(2.0, {{x_1, 1.0}, {x_2, 1.0}}));
848 ASSERT_THAT(result, IsOptimalWithDualSolution(2.0, {{y_1, 1.0}},
849 {{x_1, 2.0}, {x_2, 0.0}}));
850 const Basis expected_basis_alternative_one = {
851 .constraint_status = {{y_1, BasisStatus::kFixedValue}},
852 .variable_status = {{x_1, BasisStatus::kAtUpperBound},
853 {x_2, BasisStatus::kBasic}},
854 .basic_dual_feasibility = SolutionStatus::kFeasible};
855 const Basis expected_basis_alternative_two = {
856 .constraint_status = {{y_1, BasisStatus::kAtUpperBound}},
857 .variable_status = {{x_1, BasisStatus::kAtUpperBound},
858 {x_2, BasisStatus::kBasic}},
859 .basic_dual_feasibility = SolutionStatus::kFeasible};
860
861 ASSERT_TRUE(result.has_basis());
862 EXPECT_THAT(*result.solutions[0].basis,
863 AnyOf(BasisIs(expected_basis_alternative_one),
864 BasisIs(expected_basis_alternative_two)));
865}
866
867// Primal:
868// max 0.0
869// s.t. -kInf <= 2.0 * x_2 <= kInf (y_1)
870// -kInf <= x_1 <= kInf
871// -kInf <= x_2 <= kInf
872//
873// Dual (go/mathopt-doc-math#dual):
874// min d_max(y, r)
875// r_1 == 0.0
876// 2.0*y_1 + r_2 == 0.0
877//
878// Any value for (x*_1, x*_2) yields an optimal solution. Complementary
879// slackness conditions for any of these x*
880// (go/mathopt-dual#primal-dual-optimal-pairs) imply:
881//
882// r_1 == 0.0
883// 2.0*y_1 + r_2 == 0.0
884// y_1 == 0.0
885// r_1 == 0.0
886// r_2 == 0.0
887
888// By the cardinality and dimension requirements for a basis
889// (go/mathopt-basis#primal) we have two possibilities for a basis:
890//
891// 1) C = {1} and V = {1}
892// 2) C = {} and V = {1, 2}
893//
894// For case 1), the finite/infinite bound conditions imply both y_1 and x_1
895// must be BasisStatus::kFree. For y_1 this forces 2.0 * x_2 = 0 and for x_1 it
896// forces x_1 = 0 yielding the basic solution (x_1, x_2) = (0.0, 0.0).
897// (x_2 is BasisStatus::kBasic because 1 is not in V).
898//
899// For case 2), the finite/infinite bound conditions imply both x_1 and x_2
900// must be BasisStatus::kFree. For x_1 this forces x_1 = 0 and for x_2 it
901// forces x_2 = 0 yielding the basic solution (x_1, x_2) = (0.0, 0.0).
902// (y_1 is BasisStatus::kBasic because 1 is not in C).
903//
904
905TEST_P(SimpleLpTest, FreeBasis) {
906 if (!GetParam().supports_basis) {
907 GTEST_SKIP()
908 << "Getting the basis is not supported for this config, skipping test.";
909 }
910
911 Model model;
912 model.Maximize(0.0);
913 const Variable x_1 = model.AddContinuousVariable(-kInf, kInf, "x_1");
914 const Variable x_2 = model.AddContinuousVariable(-kInf, kInf, "x_2");
915 const LinearConstraint y_1 =
916 model.AddLinearConstraint(-kInf <= 2 * x_2 <= kInf, "y_1");
917 ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(model, TestedSolver()));
918 ASSERT_THAT(result, IsOptimalWithSolution(0.0, {{x_1, 0.0}, {x_2, 0.0}}));
919 ASSERT_THAT(result, IsOptimalWithDualSolution(0.0, {{y_1, 0.0}},
920 {{x_1, 0.0}, {x_2, 0.0}}));
921
922 const Basis expected_basis_alternative_one = {
923 .constraint_status = {{y_1, BasisStatus::kFree}},
924 .variable_status = {{x_1, BasisStatus::kFree},
925 {x_2, BasisStatus::kBasic}},
926 .basic_dual_feasibility = SolutionStatus::kFeasible};
927 const Basis expected_basis_alternative_two = {
928 .constraint_status = {{y_1, BasisStatus::kBasic}},
929 .variable_status = {{x_1, BasisStatus::kFree}, {x_2, BasisStatus::kFree}},
930 .basic_dual_feasibility = SolutionStatus::kFeasible};
931
932 ASSERT_TRUE(result.has_basis());
933 EXPECT_THAT(*result.solutions[0].basis,
934 AnyOf(BasisIs(expected_basis_alternative_one),
935 BasisIs(expected_basis_alternative_two)));
936}
937
938// Two simple incremental tests that check solver-result-structures are cleared
939// between solves. Would have caught b/225153929 and a Gurobi issue resolved
940// in cl/436321712. Using SimpleLpTest fixture to test multiple solve
941// parameters.
942TEST_P(SimpleLpTest, OptimalAfterInfeasible) {
943 // TODO(b/226146622): Check if this is a GLPK bug.
944 if (TestedSolver() == SolverType::kGlpk &&
945 GetParam().parameters.lp_algorithm == LPAlgorithm::kBarrier) {
946 GTEST_SKIP() << "Glpk returns [GLP_EFAIL] for the first solve.";
947 }
948 Model model;
949 const Variable x = model.AddContinuousVariable(0, 1, "x");
950 model.Minimize(x);
951 model.AddLinearConstraint(x >= 2);
952
953 const SolveArguments arguments{.parameters = GetParam().parameters};
954
955 ASSERT_OK_AND_ASSIGN(auto solver,
956 NewIncrementalSolver(&model, TestedSolver()));
957 EXPECT_THAT(solver->Solve(arguments),
961 model.set_upper_bound(x, 3);
962 EXPECT_THAT(solver->Solve(arguments), IsOkAndHolds(IsOptimal(2.0)));
963}
964
965TEST_P(SimpleLpTest, OptimalAfterUnbounded) {
966 // TODO(b/226146622): Check if this is a GLPK bug.
967 if (TestedSolver() == SolverType::kGlpk &&
968 GetParam().parameters.lp_algorithm == LPAlgorithm::kBarrier) {
969 GTEST_SKIP() << "Glpk returns [GLP_EFAIL] for the first solve.";
970 }
971 Model model;
972 const Variable x = model.AddContinuousVariable(-kInf, 1, "x");
973 model.Minimize(x);
974
975 const SolveArguments arguments{.parameters = GetParam().parameters};
976
977 ASSERT_OK_AND_ASSIGN(auto solver,
978 NewIncrementalSolver(&model, TestedSolver()));
979 EXPECT_THAT(solver->Solve(arguments),
983 model.set_lower_bound(x, 0);
984 EXPECT_THAT(solver->Solve(arguments), IsOkAndHolds(IsOptimal(0.0)));
985}
986
987TEST_P(IncrementalLpTest, EmptyUpdate) {
989 EXPECT_THAT(solver_->SolveWithoutUpdate(), IsOkAndHolds(IsOptimal(12.1)));
990}
991
992TEST_P(IncrementalLpTest, ObjDir) {
993 model_.set_minimize();
995 EXPECT_THAT(solver_->SolveWithoutUpdate(), IsOkAndHolds(IsOptimal(0.1)));
996}
997
998TEST_P(IncrementalLpTest, ObjOffset) {
999 model_.set_objective_offset(1.1);
1000 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
1001 EXPECT_THAT(solver_->SolveWithoutUpdate(), IsOkAndHolds(IsOptimal(13.1)));
1002}
1003
1004TEST_P(IncrementalLpTest, LinearObjCoef) {
1005 model_.set_objective_coefficient(x_1_, 5.0);
1006 model_.set_objective_coefficient(x_2_, 5.0);
1007 model_.set_objective_coefficient(x_3_, 5.0);
1008 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
1009 EXPECT_THAT(solver_->SolveWithoutUpdate(),
1010 IsOkAndHolds(IsOptimal(3 * 6 + 0.1)));
1011}
1012
1013TEST_P(IncrementalLpTest, LinearObjCoefAndRemove) {
1014 model_.DeleteVariable(zero_);
1015 model_.set_objective_coefficient(x_1_, 5.0);
1016 model_.set_objective_coefficient(x_2_, 5.0);
1017 model_.set_objective_coefficient(x_3_, 5.0);
1018 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
1019 EXPECT_THAT(solver_->SolveWithoutUpdate(),
1020 IsOkAndHolds(IsOptimal(3 * 6 + 0.1)));
1021}
1022
1023TEST_P(IncrementalLpTest, LinearObjCoefAfterRemove) {
1024 model_.DeleteVariable(zero_);
1025 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
1026
1027 model_.set_objective_coefficient(x_1_, 5.0);
1028 model_.set_objective_coefficient(x_2_, 5.0);
1029 model_.set_objective_coefficient(x_3_, 5.0);
1030 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
1031 EXPECT_THAT(solver_->SolveWithoutUpdate(),
1032 IsOkAndHolds(IsOptimal(3 * 6 + 0.1)));
1033}
1034
1035// max 0.1 + sum_{i=0}^2 (3.0 *x_i + 2.0 * y_i)
1036// s.t. x_i + y_i <= 1.5 for all i \in {0,1,2} (c_i)
1037// 0 <= x_i <= 1
1038// 0 <= y_i <= 1 for all i \in {0,1,2}
1039//
1040// Optimal solution is (x_i,y_i)=(1.0, 0.5) for all i \in {0,1,2}, with
1041// objective value 3*4+0.1 = 12.1.
1042
1043TEST_P(IncrementalLpTest, VariableLb) {
1044 model_.set_lower_bound(y_1_, 0.75);
1045 model_.set_lower_bound(y_2_, 0.75);
1046 model_.set_lower_bound(y_3_, 0.75);
1047 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
1048 EXPECT_THAT(solver_->SolveWithoutUpdate(),
1049 IsOkAndHolds(IsOptimal(3 * (3 * 0.75 + 2 * 0.75) + 0.1)));
1050}
1051
1052TEST_P(IncrementalLpTest, VariableLbAndRemove) {
1053 model_.DeleteVariable(zero_);
1054 model_.set_lower_bound(y_1_, 0.75);
1055 model_.set_lower_bound(y_2_, 0.75);
1056 model_.set_lower_bound(y_3_, 0.75);
1057 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
1058 EXPECT_THAT(solver_->SolveWithoutUpdate(),
1059 IsOkAndHolds(IsOptimal(3 * (3 * 0.75 + 2 * 0.75) + 0.1)));
1060}
1061
1062TEST_P(IncrementalLpTest, VariableLbAfterRemove) {
1063 model_.DeleteVariable(zero_);
1064 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
1065
1066 model_.set_lower_bound(y_1_, 0.75);
1067 model_.set_lower_bound(y_2_, 0.75);
1068 model_.set_lower_bound(y_3_, 0.75);
1069 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
1070 EXPECT_THAT(solver_->SolveWithoutUpdate(),
1071 IsOkAndHolds(IsOptimal(3 * (3 * 0.75 + 2 * 0.75) + 0.1)));
1072}
1073
1074TEST_P(IncrementalLpTest, VariableUb) {
1075 model_.set_upper_bound(x_1_, 0.5);
1076 model_.set_upper_bound(x_2_, 0.5);
1077 model_.set_upper_bound(x_3_, 0.5);
1078 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
1079 EXPECT_THAT(solver_->SolveWithoutUpdate(),
1080 IsOkAndHolds(IsOptimal(3 * (3 * 0.5 + 2 * 1) + 0.1)));
1081}
1082
1083TEST_P(IncrementalLpTest, LinearConstraintLb) {
1084 model_.set_lower_bound(c_1_, 1.0);
1085 model_.set_lower_bound(c_2_, 1.0);
1086 model_.set_lower_bound(c_3_, 1.0);
1087 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
1088 ASSERT_OK_AND_ASSIGN(const SolveResult result, solver_->SolveWithoutUpdate());
1089 EXPECT_THAT(result, IsOptimal(12.1));
1090 // Changing the lower bound does not effect the optimal solution, an
1091 // incremental solve does no work.
1092 EXPECT_EQ(result.solve_stats.simplex_iterations, 0);
1093 EXPECT_EQ(result.solve_stats.barrier_iterations, 0);
1094 EXPECT_EQ(result.solve_stats.first_order_iterations, 0);
1095}
1096
1097// TODO(b/184447031): Consider more cases (e.g. induced by upper-bound changes).
1098TEST_P(IncrementalLpTest, ConstraintTypeSwitch) {
1099 // Check constraint-type changes by adding or removing finite lower bounds.
1100 // For some solvers this results in addition/deletion of slacks.
1101
1102 // Single one-sided to two-sided change:
1103 // * c_1_ from one-sided to two-sided
1104 model_.set_lower_bound(c_1_, 1.0);
1105 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
1106 ASSERT_OK_AND_ASSIGN(const SolveResult first_result,
1107 solver_->SolveWithoutUpdate());
1108 EXPECT_THAT(first_result, IsOptimal(12.1));
1109 // Changing the lower bound does not effect the optimal solution, an
1110 // incremental solve does no work.
1111 EXPECT_EQ(first_result.solve_stats.simplex_iterations, 0);
1112 EXPECT_EQ(first_result.solve_stats.barrier_iterations, 0);
1113 EXPECT_EQ(first_result.solve_stats.first_order_iterations, 0);
1114
1115 // Simultaneous changes in both directions:
1116 // * c_1_ from two-sided to one-sided
1117 // * c_2_ from one-sided to two-sided
1118 model_.set_lower_bound(c_1_, -kInf);
1119 model_.set_lower_bound(c_2_, 1.0);
1120 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
1121 ASSERT_OK_AND_ASSIGN(const SolveResult second_result,
1122 solver_->SolveWithoutUpdate());
1123 EXPECT_THAT(second_result, IsOptimal(12.1));
1124 // Changing the lower bound does not effect the optimal solution, an
1125 // incremental solve does no work.
1126 EXPECT_EQ(second_result.solve_stats.simplex_iterations, 0);
1127 EXPECT_EQ(second_result.solve_stats.barrier_iterations, 0);
1128 EXPECT_EQ(second_result.solve_stats.first_order_iterations, 0);
1129 // Single two-sided to one-sided change:
1130 // * c_2_ from two-sided to one-sided
1131 model_.set_lower_bound(c_2_, -kInf);
1132 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
1133 ASSERT_OK_AND_ASSIGN(const SolveResult third_result,
1134 solver_->SolveWithoutUpdate());
1135 EXPECT_THAT(third_result, IsOptimal(12.1));
1136 // Changing the lower bound does not effect the optimal solution, an
1137 // incremental solve does no work.
1138 EXPECT_EQ(third_result.solve_stats.simplex_iterations, 0);
1139 EXPECT_EQ(third_result.solve_stats.barrier_iterations, 0);
1140 EXPECT_EQ(third_result.solve_stats.first_order_iterations, 0);
1141}
1142
1143TEST_P(IncrementalLpTest, LinearConstraintUb) {
1144 model_.set_upper_bound(c_1_, 2.0);
1145 model_.set_upper_bound(c_2_, 2.0);
1146 model_.set_upper_bound(c_3_, 2.0);
1147 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
1148 EXPECT_THAT(solver_->SolveWithoutUpdate(),
1149 IsOkAndHolds(IsOptimal(3 * (3 * 1 + 2 * 1) + 0.1)));
1150}
1151
1152TEST_P(IncrementalLpTest, LinearConstraintCoefficient) {
1153 model_.set_coefficient(c_1_, y_1_, 0.5);
1154 model_.set_coefficient(c_2_, y_2_, 0.5);
1155 model_.set_coefficient(c_3_, y_3_, 0.5);
1156 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
1157 EXPECT_THAT(solver_->SolveWithoutUpdate(),
1158 IsOkAndHolds(IsOptimal(3 * (3 * 1 + 2 * 1) + 0.1)));
1159}
1160
1161TEST_P(IncrementalLpTest, AddVariable) {
1162 const Variable z_1 = model_.AddContinuousVariable(0.0, 1.0, "z_1");
1163 model_.set_objective_coefficient(z_1, 10.0);
1164 model_.set_coefficient(c_1_, z_1, 1.0);
1165 const Variable z_2 = model_.AddContinuousVariable(0.0, 1.0, "z_2");
1166 model_.set_objective_coefficient(z_2, 10.0);
1167 model_.set_coefficient(c_2_, z_2, 1.0);
1168 const Variable z_3 = model_.AddContinuousVariable(0.0, 1.0, "z_3");
1169 model_.set_objective_coefficient(z_3, 10.0);
1170 model_.set_coefficient(c_3_, z_3, 1.0);
1171 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
1172 EXPECT_THAT(solver_->SolveWithoutUpdate(),
1173 IsOkAndHolds(IsOptimal(3 * (3 * 0.5 + 2 * 0 + 10 * 1) + 0.1)));
1174}
1175
1176TEST_P(IncrementalLpTest, AddLinearConstraint) {
1177 const LinearConstraint d_1 = model_.AddLinearConstraint(0.0, 2.0, "d_1");
1178 model_.set_coefficient(d_1, x_1_, 1.0);
1179 model_.set_coefficient(d_1, y_1_, 2.0);
1180 const LinearConstraint d_2 = model_.AddLinearConstraint(0.0, 2.0, "d_2");
1181 model_.set_coefficient(d_2, x_2_, 1.0);
1182 model_.set_coefficient(d_2, y_2_, 2.0);
1183 const LinearConstraint d_3 = model_.AddLinearConstraint(0.0, 2.0, "d_3");
1184 model_.set_coefficient(d_3, x_3_, 1.0);
1185 model_.set_coefficient(d_3, y_3_, 2.0);
1186 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
1187 EXPECT_THAT(solver_->SolveWithoutUpdate(),
1188 IsOkAndHolds(IsOptimal(3 * (3 * 1 + 2 * 0.5) + 0.1)));
1189}
1190
1191TEST_P(IncrementalLpTest, DeleteVariable) {
1192 model_.DeleteVariable(x_1_);
1193 model_.DeleteVariable(x_2_);
1194 model_.DeleteVariable(x_3_);
1195 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
1196 EXPECT_THAT(solver_->SolveWithoutUpdate(),
1197 IsOkAndHolds(IsOptimal(3 * (2 * 1) + 0.1)));
1198}
1199
1200TEST_P(IncrementalLpTest, DeleteLinearConstraint) {
1201 model_.DeleteLinearConstraint(c_1_);
1202 model_.DeleteLinearConstraint(c_2_);
1203 model_.DeleteLinearConstraint(c_3_);
1204 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
1205 EXPECT_THAT(solver_->SolveWithoutUpdate(),
1206 IsOkAndHolds(IsOptimal(3 * (3 * 1 + 2 * 1) + 0.1)));
1207}
1208
1209TEST_P(IncrementalLpTest, ChangeBoundsWithTemporaryInversion) {
1210 model_.set_lower_bound(x_1_, 3.0);
1211 // At this point x_1_ lower bound is 3.0 and upper bound is 1.0.
1212 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
1213
1214 model_.set_upper_bound(x_1_, 5.0);
1215 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
1216 // At this point x_1_ upper bound is 5.0 and so is greater than the new lower
1217 // bound.
1218
1219 // To make the problem feasible we update the bound of the constraint that
1220 // contains x_1_; we take this opportunity to also test inverting bounds of
1221 // constraints.
1222 model_.set_lower_bound(c_1_, 4.0);
1223 // At this point c_1_ lower bound is 4.0 and upper bound is 1.5.
1224 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
1225
1226 // We restore valid bounds by setting c_1_ upper bound to 5.5.
1227 model_.set_upper_bound(c_1_, 5.5);
1228 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
1229
1231 solver_->SolveWithoutUpdate(),
1232 IsOkAndHolds(IsOptimal((3 * 5 + 2 * 0.5) + 2 * (3 * 1 + 2 * 0.5) + 0.1)));
1233}
1234
1235} // namespace
1236} // namespace math_opt
1237} // namespace operations_research
IntegerValue y
static absl::StatusOr< SolveResultProto > NonIncrementalSolve(const ModelProto &model, SolverTypeProto solver_type, const InitArgs &init_args, const SolveArgs &solve_args)
A shortcut for calling Solver::New() and then Solver::Solve().
Definition solver.cc:70
absl::StatusOr< bool > Update(ModelUpdateProto model_update) override
Definition solver.cc:158
SatParameters parameters
CpModelProto proto
The output proto.
GRBmodel * model
Matcher< SolveResult > IsOptimalWithSolution(const double expected_objective, const VariableMap< double > expected_variable_values, const double tolerance)
Definition matchers.cc:777
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.
<=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()))
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 > IsConsistentWith(const SolveResult &expected, const SolveResultMatcherOptions &options)
Definition matchers.cc:942
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.
@ kFixedValue
The variable/constraint has identical finite lower and upper bounds.
@ kBasic
The variable/constraint is basic.
@ kFree
The variable/constraint is free (it has no finite bounds).
@ 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).
Enum< E >::Proto EnumToProto(std::optional< E > value)
Definition enums.h:270
Matcher< UpdateResult > DidUpdate()
Actual UpdateResult.did_update is true.
Definition matchers.cc:1027
Matcher< SolveResult > IsOptimalWithDualSolution(const double expected_objective, const LinearConstraintMap< double > expected_dual_values, const VariableMap< double > expected_reduced_costs, const double tolerance)
Definition matchers.cc:790
Matcher< Basis > BasisIs(const Basis &expected)
Definition matchers.cc:449
Matcher< SolveResult > IsOptimal(const std::optional< double > expected_primal_objective, const double tolerance)
Definition matchers.cc:762
@ kFeasible
Solver claims the problem is feasible.
In SWIG mode, we don't want anything besides these top-level includes.
std::string ProtobufShortDebugString(const P &message)
Definition proto_utils.h:41
std::string ProtobufDebugString(const P &message)
Definition proto_utils.h:32
#define ASSERT_OK_AND_ASSIGN(lhs, rexpr)
bool supports_basis
True if the solver produces a basis.
Definition lp_tests.h:38
bool supports_duals
True if a dual solution is returned.
Definition lp_tests.h:35