Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
lp_model_solve_parameters_tests.cc
Go to the documentation of this file.
1// Copyright 2010-2024 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
15
16#include <limits>
17#include <ostream>
18#include <vector>
19
20#include "gtest/gtest.h"
21#include "ortools/base/gmock.h"
27
28namespace operations_research {
29namespace math_opt {
30
31std::ostream& operator<<(std::ostream& out,
33 out << "{ solver_type: " << params.solver_type
34 << ", exact_zeros: " << params.exact_zeros
35 << ", supports_duals: " << params.supports_duals
36 << ", supports_primal_only_warm_starts: "
38 << ", parameters: " << ProtobufShortDebugString(params.parameters.Proto())
39 << " }";
40 return out;
41}
42
43namespace {
44
45constexpr double kInf = std::numeric_limits<double>::infinity();
46
47// A basic feasible linear program used in filtering tests below.
48//
49// It has an optimal solution that is unique and has the property that for
50// primal values, dual values and reduced costs, it has one zero value and one
51// non-zero value. This enables testing for filtering zeros and testing
52// filtering with keys.
53//
54// The model is:
55//
56// min 2 * x1 + x2
57// s.t. x1 + x2 >= 1 (constraint y1)
58// x1 + 4 * x2 >= 2 (constraint y2)
59// x1 >= 0, x2 >= 0 (non-negative variables)
60//
61// The solution is:
62// x1 = 0, x2 = 1
63//
64// The use of non-negative variables without upper-bounds and one-sided
65// constraints makes it simpler to write the dual problem:
66//
67// max y1 + 2 * y2
68// s.t. y1 + y2 + r1 = 2
69// y1 + 4 * y2 + r2 = 1
70// y1 >= 0, y2 >= 0
71// r1 >= 0, r2 >= 0
72//
73// The solution of the dual is:
74// y1 = 1, y2 = 0
75// r1 = 1, r2 = 0
76struct FeasibleLP {
77 FeasibleLP()
78 : x1(model.AddContinuousVariable(0, kInf, "x1")),
79 x2(model.AddContinuousVariable(0, kInf, "x2")),
80 y1(model.AddLinearConstraint(x1 + x2 >= 1, "y1")),
81 y2(model.AddLinearConstraint(x1 + 4 * x2 >= 2, "y2")) {
82 model.Minimize(2 * x1 + x2);
83 }
84
85 Model model;
86 const Variable x1;
87 const Variable x2;
88 const LinearConstraint y1;
89 const LinearConstraint y2;
90};
91
92// An unbounded linear program used in filtering tests below.
93//
94// The model is:
95//
96// max 2 * x1 - x2
97// s.t. x1 + x2 >= 1 (constraint y1)
98// x2 <= 0 (constraint y2)
99// x1 >= 0, x2 >= 0 (non-negative variables)
100//
101// It will have a primal ray with a non-zero value for x1 and a zero value for
102// x2.
103//
104// The primal ray will be proportional to:
105// R := (x1 = 1, x2 = 0)
106//
107// A feasible point is:
108// P := (x1 = 1, x2 = 0)
109//
110// For all t >=0, P + t * R will be feasible.
111struct UnboundedLP {
112 UnboundedLP()
113 : x1(model.AddContinuousVariable(0, kInf, "x1")),
114 x2(model.AddContinuousVariable(0, kInf, "x2")),
115 y1(model.AddLinearConstraint(x1 + x2 >= 1, "y1")),
116 y2(model.AddLinearConstraint(x2 <= 0, "y2")) {
117 model.Maximize(2 * x1 - x2);
118 }
119
120 Model model;
121 const Variable x1;
122 const Variable x2;
123 const LinearConstraint y1;
124 const LinearConstraint y2;
125};
126
127// An infeasible linear program used in filtering tests below.
128//
129// The model is:
130//
131// min x1 - x2
132// s.t. x1 <= -2 (constraint y1)
133// x2 <= 3 (constraint y2)
134// x1 >= 0 (non-negative variable)
135//
136// Its dual is:
137//
138// max -2 * y1 + 3 * y2
139// s.t. y1 + r1 = 1
140// y2 + r2 = -1
141// y1 <= 0
142// y2 <= 0
143// r1 >= 0
144// r2 = 0
145//
146// The dual is indeed unbounded. It will have a dual ray with a non-zero value
147// for y1 and a zero value for y2. It also has non-zero reduced cost for x1 and
148// a zero one for x2.
149//
150// The dual ray will be proportional to:
151// R := (y1 = -1, y2 = 0, r1 = 1, r2 = 0)
152//
153// A feasible point of the dual is:
154// P := (y1 = 0, y2 = -1, r1 = 1, r2 = 0)
155//
156// For all t >= 0, P + t * R will be feasible.
157struct InfeasibleLP {
158 InfeasibleLP()
159 : x1(model.AddContinuousVariable(0, kInf, "x1")),
160 x2(model.AddContinuousVariable(-kInf, kInf, "x2")),
161 y1(model.AddLinearConstraint(x1 <= -2, "y1")),
162 y2(model.AddLinearConstraint(x2 <= 3, "y2")) {
163 model.Minimize(x1 - x2);
164 }
165
166 Model model;
167 const Variable x1;
168 const Variable x2;
169 const LinearConstraint y1;
170 const LinearConstraint y2;
171};
172
173TEST_P(LpModelSolveParametersTest, SolutionFilterSkipZerosPrimalVars) {
174 if (!GetParam().exact_zeros) {
175 GTEST_SKIP()
176 << "Solver " << GetParam().solver_type
177 << " does not reliably return exact zeros; this test is disabled.";
178 }
179 FeasibleLP lp;
180
181 const SolveArguments args = {
182 .parameters = GetParam().parameters,
183 .model_parameters = {
184 .variable_values_filter = {.skip_zero_values = true}}};
185 ASSERT_OK_AND_ASSIGN(const SolveResult result,
186 Solve(lp.model, GetParam().solver_type, args));
187 ASSERT_THAT(result, IsOptimal(1.0));
188 EXPECT_THAT(result.variable_values(), IsNear({{lp.x2, 1.0}}));
189 if (GetParam().supports_duals) {
190 const DualSolution expected_dual = {
191 .dual_values = {{lp.y1, 1.0}, {lp.y2, 0.0}},
192 .reduced_costs = {{lp.x1, 1.0}, {lp.x2, 0.0}},
193 .objective_value = 1.0,
194 .feasibility_status = SolutionStatus::kFeasible};
195
196 EXPECT_THAT(result, HasDualSolution(expected_dual));
197 }
198}
199
200TEST_P(LpModelSolveParametersTest, SolutionFilterSkipZerosReducedCosts) {
201 if (!GetParam().exact_zeros) {
202 GTEST_SKIP()
203 << "Solver " << GetParam().solver_type
204 << " does not reliably return exact zeros; this test is disabled.";
205 }
206 if (!GetParam().supports_duals) {
207 GTEST_SKIP() << "Solver " << GetParam().solver_type
208 << " can't produce dual solutions; this test is disabled.";
209 }
210 FeasibleLP lp;
211 const SolveArguments args = {
212 .parameters = GetParam().parameters,
213 .model_parameters = {.reduced_costs_filter = {.skip_zero_values = true}}};
214 ASSERT_OK_AND_ASSIGN(const SolveResult result,
215 Solve(lp.model, GetParam().solver_type, args));
216 ASSERT_THAT(result, IsOptimal(1.0));
217 EXPECT_THAT(result.variable_values(), IsNear({{lp.x1, 0.0}, {lp.x2, 1.0}}));
218 const DualSolution expected_dual = {
219 .dual_values = {{lp.y1, 1.0}, {lp.y2, 0.0}},
220 .reduced_costs = {{lp.x1, 1.0}},
221 .objective_value = 1.0,
222 .feasibility_status = SolutionStatus::kFeasible};
223 EXPECT_THAT(result, HasDualSolution(expected_dual));
224}
225
226TEST_P(LpModelSolveParametersTest, SolutionFilterSkipZerosDualVars) {
227 if (!GetParam().exact_zeros) {
228 GTEST_SKIP()
229 << "Solver " << GetParam().solver_type
230 << " does not reliably return exact zeros; this test is disabled.";
231 }
232 if (!GetParam().supports_duals) {
233 GTEST_SKIP() << "Solver " << GetParam().solver_type
234 << " can't produce dual solutions; this test is disabled.";
235 }
236 FeasibleLP lp;
237
238 const SolveArguments args = {
239 .parameters = GetParam().parameters,
240 .model_parameters = {.dual_values_filter = {.skip_zero_values = true}}};
241 ASSERT_OK_AND_ASSIGN(const SolveResult result,
242 Solve(lp.model, GetParam().solver_type, args));
243 ASSERT_THAT(result, IsOptimal(1.0));
244 EXPECT_THAT(result.variable_values(), IsNear({{lp.x1, 0.0}, {lp.x2, 1.0}}));
245 const DualSolution expected_dual = {
246 .dual_values = {{lp.y1, 1.0}},
247 .reduced_costs = {{lp.x1, 1.0}, {lp.x2, 0.0}},
248 .objective_value = 1.0,
249 .feasibility_status = SolutionStatus::kFeasible};
250 EXPECT_THAT(result, HasDualSolution(expected_dual));
251}
252
253// This test is shared by for all three filters since each filter use a
254// different set of keys.
255TEST_P(LpModelSolveParametersTest, SolutionFilterByKey) {
256 FeasibleLP lp;
257
258 const SolveArguments args = {
259 .parameters = GetParam().parameters,
260 .model_parameters = {
261 .variable_values_filter = MakeKeepKeysFilter({lp.x1}),
262 .dual_values_filter = MakeKeepKeysFilter({lp.y2}),
263 .reduced_costs_filter = MakeKeepKeysFilter({lp.x2})}};
264 ASSERT_OK_AND_ASSIGN(const SolveResult result,
265 Solve(lp.model, GetParam().solver_type, args));
266 ASSERT_THAT(result, IsOptimal(1.0));
267 EXPECT_THAT(result.variable_values(), IsNear({{lp.x1, 0.0}}));
268 if (GetParam().supports_duals) {
269 const DualSolution expected_dual = {
270 .dual_values = {{lp.y2, 0.0}},
271 .reduced_costs = {{lp.x2, 0.0}},
272 .objective_value = 1.0,
273 .feasibility_status = SolutionStatus::kFeasible};
274 EXPECT_THAT(result, HasDualSolution(expected_dual));
275 }
276}
277
278TEST_P(LpModelSolveParametersTest, SolutionFilterSkipZerosPrimalRay) {
279 if (!GetParam().exact_zeros) {
280 GTEST_SKIP()
281 << "Solver " << GetParam().solver_type
282 << " does not reliably return exact zeros; this test is disabled.";
283 }
284 UnboundedLP lp;
285
286 SolveArguments args = {.parameters = GetParam().parameters,
287 .model_parameters = {.variable_values_filter = {
288 .skip_zero_values = true}}};
289
290 if (!ActivatePrimalRay(GetParam().solver_type, args.parameters)) {
291 GTEST_SKIP() << "Solver " << GetParam().solver_type
292 << " can't produce primal rays; this test is disabled.";
293 }
294
295 ASSERT_OK_AND_ASSIGN(const SolveResult result,
296 Solve(lp.model, GetParam().solver_type, args));
297 ASSERT_THAT(result,
300 EXPECT_THAT(result, HasPrimalRay({{lp.x1, 1.0}}));
301}
302
303TEST_P(LpModelSolveParametersTest, SolutionFilterByKeyPrimalRay) {
304 UnboundedLP lp;
305
306 SolveArguments args = {.parameters = GetParam().parameters,
307 .model_parameters = {.variable_values_filter =
308 MakeKeepKeysFilter({lp.x2})}};
309
310 if (!ActivatePrimalRay(GetParam().solver_type, args.parameters)) {
311 GTEST_SKIP() << "Solver " << GetParam().solver_type
312 << " can't produce primal rays; this test is disabled.";
313 }
314
315 ASSERT_OK_AND_ASSIGN(const SolveResult result,
316 Solve(lp.model, GetParam().solver_type, args));
317 ASSERT_THAT(result,
320 EXPECT_THAT(result, HasPrimalRay({{lp.x2, 0.0}}));
321}
322
323TEST_P(LpModelSolveParametersTest, SolutionFilterSkipZerosDualRayDuals) {
324 if (!GetParam().exact_zeros) {
325 GTEST_SKIP()
326 << "Solver " << GetParam().solver_type
327 << " does not reliably return exact zeros; this test is disabled.";
328 }
329 InfeasibleLP lp;
330
331 SolveArguments args = {
332 .parameters = GetParam().parameters,
333 .model_parameters = {.dual_values_filter = {.skip_zero_values = true}}};
334
335 if (!ActivateDualRay(GetParam().solver_type, args.parameters)) {
336 GTEST_SKIP() << "Solver " << GetParam().solver_type
337 << " can't produce dual rays; this test is disabled.";
338 }
339
340 ASSERT_OK_AND_ASSIGN(const SolveResult result,
341 Solve(lp.model, GetParam().solver_type, args));
343 const DualRay expected = {.dual_values = {{lp.y1, -1.0}},
344 .reduced_costs = {{lp.x1, 1.0}, {lp.x2, 0.0}}};
345 EXPECT_THAT(result, HasDualRay(expected));
346}
347
348TEST_P(LpModelSolveParametersTest, SolutionFilterSkipZerosDualRayReducedCosts) {
349 if (!GetParam().exact_zeros) {
350 GTEST_SKIP()
351 << "Solver " << GetParam().solver_type
352 << " does not reliably return exact zeros; this test is disabled.";
353 }
354 InfeasibleLP lp;
355
356 SolveArguments args = {
357 .parameters = GetParam().parameters,
358 .model_parameters = {.reduced_costs_filter = {.skip_zero_values = true}}};
359
360 if (!ActivateDualRay(GetParam().solver_type, args.parameters)) {
361 GTEST_SKIP() << "Solver " << GetParam().solver_type
362 << " can't produce dual rays; this test is disabled.";
363 }
364
365 ASSERT_OK_AND_ASSIGN(const SolveResult result,
366 Solve(lp.model, GetParam().solver_type, args));
368 const DualRay expected = {.dual_values = {{lp.y1, -1.0}, {lp.y2, 0.0}},
369 .reduced_costs = {{lp.x1, 1.0}}};
370 EXPECT_THAT(result, HasDualRay(expected));
371}
372
373TEST_P(LpModelSolveParametersTest, SolutionFilterByKeysDualRay) {
374 InfeasibleLP lp;
375
376 SolveArguments args = {
377 .parameters = GetParam().parameters,
378 .model_parameters = {
379 .dual_values_filter = MakeKeepKeysFilter({lp.y2}),
380 .reduced_costs_filter = MakeKeepKeysFilter({lp.x2})}};
381
382 if (!ActivateDualRay(GetParam().solver_type, args.parameters)) {
383 GTEST_SKIP() << "Solver " << GetParam().solver_type
384 << " can't produce dual rays; this test is disabled.";
385 }
386
387 ASSERT_OK_AND_ASSIGN(const SolveResult result,
388 Solve(lp.model, GetParam().solver_type, args));
390 const DualRay expected = {.dual_values = {{lp.y2, 0.0}},
391 .reduced_costs = {{lp.x2, 0.0}}};
392 EXPECT_THAT(result, HasDualRay(expected));
393}
394
395TEST_P(LpModelSolveParametersTest, PrimalWarmStart) {
396 constexpr int n = 10;
397 const auto model = IndependentSetCompleteGraph(/*integer=*/false, /*n=*/n);
398 int baseline_num_iters = 0;
399 {
400 ASSERT_OK_AND_ASSIGN(const auto result,
401 Solve(*model, GetParam().solver_type,
402 {.parameters = GetParam().parameters}));
403 ASSERT_THAT(result, IsOptimal(n / 2.0));
404 baseline_num_iters = result.solve_stats.simplex_iterations +
405 result.solve_stats.barrier_iterations +
406 result.solve_stats.first_order_iterations;
407 }
408
409 // We seed the optimal primal solution as a warm start.
410 ModelSolveParameters::SolutionHint warm_start;
411 for (const Variable var : model->Variables()) {
412 warm_start.variable_values[var] = 1.0 / 2.0;
413 }
415 const auto result,
416 Solve(*model, GetParam().solver_type,
417 {.parameters = GetParam().parameters,
418 .model_parameters = {.solution_hints = {warm_start}}}));
419 EXPECT_THAT(result, IsOptimal(n / 2.0));
420 const int actual_num_iters = result.solve_stats.simplex_iterations +
421 result.solve_stats.barrier_iterations +
422 result.solve_stats.first_order_iterations;
423 if (!GetParam().supports_primal_only_warm_starts) {
424 EXPECT_EQ(actual_num_iters, baseline_num_iters);
425 return;
426 }
427 EXPECT_LT(actual_num_iters, baseline_num_iters);
428}
429
430} // namespace
431} // namespace math_opt
432} // namespace operations_research
IntVar * var
GRBmodel * model
const Variable x2
const LinearConstraint y1
const Variable x1
const LinearConstraint y2
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}}}})))
Matcher< SolveResult > HasPrimalRay(PrimalRay expected, const double tolerance)
Definition matchers.cc:839
TEST_P(InfeasibleSubsystemTest, CanComputeInfeasibleSubsystem)
@ kInfeasible
The primal problem has no feasible solutions.
ASSERT_THAT(solver->Update(), IsOkAndHolds(DidUpdate()))
Matcher< SolveResult > TerminatesWithOneOf(const std::vector< TerminationReason > &allowed)
Checks that the result has one of the allowed termination reasons.
Definition matchers.cc:615
bool ActivatePrimalRay(SolverType solver_type, SolveParameters &params)
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)
@ kFeasible
Solver claims the solution is feasible.
Matcher< SolveResult > HasDualRay(DualRay expected, const double tolerance)
Definition matchers.cc:851
Matcher< SolveResult > TerminatesWith(const TerminationReason expected)
Definition matchers.cc:621
std::unique_ptr< Model > IndependentSetCompleteGraph(const bool integer, const int n)
Matcher< VariableMap< double > > IsNear(VariableMap< double > expected, const double tolerance)
Definition matchers.cc:221
MapFilter< ValueType > MakeKeepKeysFilter(const Collection &keys)
Definition map_filter.h:167
Matcher< SolveResult > HasDualSolution(DualSolution expected, const double tolerance)
Definition matchers.cc:831
bool ActivateDualRay(SolverType solver_type, SolveParameters &params)
Matcher< SolveResult > IsOptimal(const std::optional< double > expected_primal_objective, const double tolerance)
Definition matchers.cc:762
In SWIG mode, we don't want anything besides these top-level includes.
std::string ProtobufShortDebugString(const P &message)
Definition proto_utils.h:41
#define ASSERT_OK_AND_ASSIGN(lhs, rexpr)
bool supports_primal_only_warm_starts
True if the solver supports warm starts on the primal solution only.