Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
mip_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 <initializer_list>
18#include <limits>
19#include <ostream>
20
21#include "absl/log/check.h"
22#include "absl/status/statusor.h"
23#include "gtest/gtest.h"
24#include "ortools/base/gmock.h"
28
29namespace operations_research {
30namespace math_opt {
31namespace {
32
33using ::testing::status::IsOkAndHolds;
34
35constexpr double kTolerance = 1e-5;
36constexpr double kInf = std::numeric_limits<double>::infinity();
37
38} // namespace
39
41 SolverType solver_type, bool report_unboundness_correctly)
42 : solver_type(solver_type),
43 report_unboundness_correctly(report_unboundness_correctly) {}
44
45std::ostream& operator<<(std::ostream& out,
46 const SimpleMipTestParameters& params) {
47 out << "{ solver_type: " << params.solver_type
48 << ", report_unboundness_correctly: "
49 << (params.report_unboundness_correctly ? "true" : "false") << "}";
50 return out;
51}
52
53// max 3.0 *x + 2.0 * y + 0.1
54// s.t. 0 <= x + y <= 1.5 (c)
55// 0 <= x <= 1
56// y in {0, 1, 2}
57//
58// Optimal solution is (0.5, 1.0), objective value 3.6
60 : model_("incremental_solve_test"),
61 x_(model_.AddContinuousVariable(0.0, 1.0, "x")),
62 y_(model_.AddIntegerVariable(0.0, 2.0, "y")),
63 c_(model_.AddLinearConstraint(0 <= x_ + y_ <= 1.5, "c")) {
64 model_.Maximize(3.0 * x_ + 2.0 * y_ + 0.1);
65 solver_ = NewIncrementalSolver(&model_, TestedSolver()).value();
66 const SolveResult first_solve = solver_->Solve().value();
67 CHECK(first_solve.has_primal_feasible_solution());
68 CHECK_LE(std::abs(first_solve.objective_value() - 3.6), kTolerance)
69 << first_solve.objective_value();
70}
71
72namespace {
73
74TEST_P(SimpleMipTest, OneVarMax) {
75 Model model;
76 const Variable x = model.AddVariable(0.0, 4.0, false, "x");
77 model.Maximize(2.0 * x);
78 ASSERT_OK_AND_ASSIGN(const SolveResult result,
79 Solve(model, GetParam().solver_type));
80 ASSERT_THAT(result, IsOptimal(8.0));
81 EXPECT_THAT(result.variable_values(), IsNear({{x, 4.0}}));
82}
83
84TEST_P(SimpleMipTest, OneVarMin) {
85 Model model;
86 const Variable x = model.AddVariable(-2.4, 4.0, false, "x");
87 model.Minimize(2.0 * x);
88 ASSERT_OK_AND_ASSIGN(const SolveResult result,
89 Solve(model, GetParam().solver_type));
90 ASSERT_THAT(result, IsOptimal(-4.8));
91 EXPECT_THAT(result.variable_values(), IsNear({{x, -2.4}}));
92}
93
94TEST_P(SimpleMipTest, OneIntegerVar) {
95 Model model;
96 const Variable x = model.AddVariable(0.0, 4.5, true, "x");
97 model.Maximize(2.0 * x);
98 ASSERT_OK_AND_ASSIGN(const SolveResult result,
99 Solve(model, GetParam().solver_type));
100 ASSERT_THAT(result, IsOptimal(8.0));
101 EXPECT_THAT(result.variable_values(), IsNear({{x, 4.0}}));
102}
103
104TEST_P(SimpleMipTest, SimpleLinearConstraint) {
105 Model model;
106 const Variable x = model.AddBinaryVariable("x");
107 const Variable y = model.AddBinaryVariable("y");
108 model.Maximize(2.0 * x + y);
109 model.AddLinearConstraint(0.0 <= x + y <= 1.5, "c");
110 ASSERT_OK_AND_ASSIGN(const SolveResult result,
111 Solve(model, GetParam().solver_type));
112 ASSERT_THAT(result, IsOptimal(2.0));
113 EXPECT_THAT(result.variable_values(), IsNear({{x, 1}, {y, 0}}));
114}
115
116TEST_P(SimpleMipTest, Unbounded) {
117 Model model;
118 const Variable x = model.AddVariable(0.0, kInf, true, "x");
119 model.Maximize(2.0 * x);
120 ASSERT_OK_AND_ASSIGN(const SolveResult result,
121 Solve(model, GetParam().solver_type));
122 if (GetParam().report_unboundness_correctly) {
124 {TerminationReason::kUnbounded,
125 TerminationReason::kInfeasibleOrUnbounded}));
126 } else {
127 ASSERT_THAT(result, TerminatesWith(TerminationReason::kOtherError));
128 }
129}
130
131TEST_P(SimpleMipTest, Infeasible) {
132 Model model;
133 const Variable x = model.AddVariable(0.0, 3.0, true, "x");
134 model.Maximize(2.0 * x);
135 model.AddLinearConstraint(x >= 4.0);
136 ASSERT_OK_AND_ASSIGN(const SolveResult result,
137 Solve(model, GetParam().solver_type));
138 ASSERT_THAT(result, TerminatesWith(TerminationReason::kInfeasible));
139}
140
141TEST_P(SimpleMipTest, FractionalBoundsContainNoInteger) {
142 if (GetParam().solver_type == SolverType::kGurobi) {
143 // TODO(b/272298816): Gurobi bindings are broken here.
144 GTEST_SKIP() << "TODO(b/272298816): Gurobi bindings are broken here.";
145 }
146 Model model;
147 const Variable x = model.AddIntegerVariable(0.5, 0.6, "x");
148 model.Maximize(x);
149 EXPECT_THAT(Solve(model, GetParam().solver_type),
150 IsOkAndHolds(TerminatesWith(TerminationReason::kInfeasible)));
151}
152
153TEST_P(IncrementalMipTest, EmptyUpdate) {
154 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
155 ASSERT_OK_AND_ASSIGN(const SolveResult result, solver_->SolveWithoutUpdate());
156 ASSERT_THAT(result, IsOptimal(3.6));
157 EXPECT_THAT(result.variable_values(), IsNear({{x_, 0.5}, {y_, 1.0}}));
158}
159
160TEST_P(IncrementalMipTest, MakeContinuous) {
161 model_.set_continuous(y_);
162 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
163 ASSERT_OK_AND_ASSIGN(const SolveResult result, solver_->SolveWithoutUpdate());
164 ASSERT_THAT(result, IsOptimal(4.1));
165 EXPECT_THAT(result.variable_values(), IsNear({{x_, 1.0}, {y_, 0.5}}));
166}
167
168// TODO(b/202494808): Enable this test once this bug is resolved. Today Gurobi
169// and Scip both fail in that case. See the bug for details why.
170TEST_P(IncrementalMipTest, DISABLED_MakeContinuousWithNonIntegralBounds) {
171 // With Gurobi we can only have one solver at a time.
172 solver_.reset();
173
174 Model model("bounds");
175 const Variable x = model.AddIntegerVariable(0.5, 1.5, "x");
176 model.Maximize(x);
177
178 ASSERT_OK_AND_ASSIGN(const auto solver,
179 NewIncrementalSolver(&model, TestedSolver()));
180 ASSERT_THAT(solver->Solve(), IsOkAndHolds(IsOptimal(1.0)));
181
182 // Switching to continuous should use the fractional bound. For solvers that
183 // mandates integral bounds for integer variables this may require updating
184 // the bound to its actual fractional value.
185 model.set_continuous(x);
186 ASSERT_THAT(solver->Update(), IsOkAndHolds(DidUpdate()));
187 ASSERT_THAT(solver->SolveWithoutUpdate(), IsOkAndHolds(IsOptimal(1.5)));
188
189 model.Minimize(x);
190 ASSERT_THAT(solver->Update(), IsOkAndHolds(DidUpdate()));
191 ASSERT_THAT(solver->SolveWithoutUpdate(), IsOkAndHolds(IsOptimal(0.5)));
192}
193
194TEST_P(IncrementalMipTest, MakeIntegralWithNonIntegralBounds) {
195 // With Gurobi we can only have one solver at a time.
196 solver_.reset();
197
198 Model model("bounds");
199 const Variable x = model.AddContinuousVariable(0.5, 1.5, "x");
200 model.Maximize(x);
201
202 ASSERT_OK_AND_ASSIGN(const auto solver,
203 NewIncrementalSolver(&model, TestedSolver()));
204 ASSERT_THAT(solver->Solve(), IsOkAndHolds(IsOptimal(1.5)));
205
206 model.set_integer(x);
207 ASSERT_THAT(solver->Update(), IsOkAndHolds(DidUpdate()));
208 ASSERT_THAT(solver->SolveWithoutUpdate(), IsOkAndHolds(IsOptimal(1.0)));
209
210 model.Minimize(x);
211 ASSERT_THAT(solver->Update(), IsOkAndHolds(DidUpdate()));
212 ASSERT_THAT(solver->SolveWithoutUpdate(), IsOkAndHolds(IsOptimal(1.0)));
213}
214
216 model_.set_integer(x_);
217 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
218 ASSERT_OK_AND_ASSIGN(const SolveResult result, solver_->SolveWithoutUpdate());
219 ASSERT_THAT(result, IsOptimal(3.1));
220 EXPECT_THAT(result.variable_values(), IsNear({{x_, 1.0}, {y_, 0.0}}));
221}
222
224 model_.set_minimize();
225 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
226 ASSERT_OK_AND_ASSIGN(const SolveResult result, solver_->SolveWithoutUpdate());
227 ASSERT_THAT(result, IsOptimal(0.1));
228 EXPECT_THAT(result.variable_values(), IsNear({{x_, 0.0}, {y_, 0.0}}));
229}
230
232 model_.set_objective_offset(0.2);
233 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
234 ASSERT_OK_AND_ASSIGN(const SolveResult result, solver_->SolveWithoutUpdate());
235 ASSERT_THAT(result, IsOptimal(3.7));
236 EXPECT_THAT(result.variable_values(), IsNear({{x_, 0.5}, {y_, 1.0}}));
237}
238
239TEST_P(IncrementalMipTest, LinearObjCoef) {
240 model_.set_objective_coefficient(x_, 5.0);
241 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
242 ASSERT_OK_AND_ASSIGN(const SolveResult result, solver_->SolveWithoutUpdate());
243 ASSERT_THAT(result, IsOptimal(5.1));
244 EXPECT_THAT(result.variable_values(), IsNear({{x_, 1.0}, {y_, 0.0}}));
245}
246
248 model_.set_lower_bound(x_, 0.75);
249 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
250 ASSERT_OK_AND_ASSIGN(const SolveResult result, solver_->SolveWithoutUpdate());
251 ASSERT_THAT(result, IsOptimal(3.1));
252 EXPECT_THAT(result.variable_values(), IsNear({{x_, 1.0}, {y_, 0.0}}));
253}
254
256 model_.set_upper_bound(x_, 2.0);
257 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
258 ASSERT_OK_AND_ASSIGN(const SolveResult result, solver_->SolveWithoutUpdate());
259 ASSERT_THAT(result, IsOptimal(4.6));
260 EXPECT_THAT(result.variable_values(), IsNear({{x_, 1.5}, {y_, 0.0}}));
261}
262
263TEST_P(IncrementalMipTest, LinearConstraintLb) {
264 model_.set_lower_bound(c_, 1.0);
265 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
266 ASSERT_OK_AND_ASSIGN(const SolveResult result, solver_->SolveWithoutUpdate());
267 ASSERT_THAT(result, IsOptimal(3.6));
268 EXPECT_THAT(result.variable_values(), IsNear({{x_, 0.5}, {y_, 1.0}}));
269 // For this change, feasibility is preserved, so the solver should do no extra
270 // work (SCIP enumerates one node, though).
271 if (TestedSolver() != SolverType::kGscip) {
272 EXPECT_EQ(result.solve_stats.node_count, 0);
273 }
274 EXPECT_EQ(result.solve_stats.simplex_iterations, 0);
275 EXPECT_EQ(result.solve_stats.barrier_iterations, 0);
276}
277
278TEST_P(IncrementalMipTest, LinearConstraintUb) {
279 model_.set_upper_bound(c_, 1.0);
280 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
281 ASSERT_OK_AND_ASSIGN(const SolveResult result, solver_->SolveWithoutUpdate());
282 ASSERT_THAT(result, IsOptimal(3.1));
283 EXPECT_THAT(result.variable_values(), IsNear({{x_, 1.0}, {y_, 0.0}}));
284}
285
286TEST_P(IncrementalMipTest, LinearConstraintCoefficient) {
287 model_.set_coefficient(c_, x_, 0.5);
288 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
289 ASSERT_OK_AND_ASSIGN(const SolveResult result, solver_->SolveWithoutUpdate());
290 ASSERT_THAT(result, IsOptimal(5.1));
291 EXPECT_THAT(result.variable_values(), IsNear({{x_, 1.0}, {y_, 1.0}}));
292}
293
295 Variable z = model_.AddVariable(0.0, 1.0, true, "z");
296 model_.set_objective_coefficient(z, 10.0);
297 model_.set_coefficient(c_, z, 1.0);
298 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
299 ASSERT_OK_AND_ASSIGN(const SolveResult result, solver_->SolveWithoutUpdate());
300 ASSERT_THAT(result, IsOptimal(11.6));
301 EXPECT_THAT(result.variable_values(),
302 IsNear({{x_, 0.5}, {y_, 0.0}, {z, 1.0}}));
303}
304
305TEST_P(IncrementalMipTest, AddLinearConstraint) {
306 model_.AddLinearConstraint(0.0 <= x_ + 2.0 * y_ <= 2.0, "d");
307 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
308 ASSERT_OK_AND_ASSIGN(const SolveResult result, solver_->SolveWithoutUpdate());
309 ASSERT_THAT(result, IsOptimal(3.1));
310 EXPECT_THAT(result.variable_values(), IsNear({{x_, 1.0}, {y_, 0.0}}));
311}
312
313TEST_P(IncrementalMipTest, DeleteVariable) {
314 model_.DeleteVariable(x_);
315 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
316 ASSERT_OK_AND_ASSIGN(const SolveResult result, solver_->SolveWithoutUpdate());
317 ASSERT_THAT(result, IsOptimal(2.1));
318 EXPECT_THAT(result.variable_values(), IsNear({{y_, 1.0}}));
319}
320
321TEST_P(IncrementalMipTest, DeleteLinearConstraint) {
322 model_.DeleteLinearConstraint(c_);
323 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
324 ASSERT_OK_AND_ASSIGN(const SolveResult result, solver_->SolveWithoutUpdate());
325 ASSERT_THAT(result, IsOptimal(7.1));
326 EXPECT_THAT(result.variable_values(), IsNear({{x_, 1.0}, {y_, 2.0}}));
327}
328
329TEST_P(IncrementalMipTest, ChangeBoundsWithTemporaryInversion) {
330 model_.set_lower_bound(x_, 3.0);
331 // At this point x_ lower bound is 3.0 and upper bound is 1.0.
332 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
333
334 model_.set_upper_bound(x_, 5.0);
335 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
336 // At this point x_ upper bound is 5.0 and so is greater than the new lower
337 // bound.
338
339 // To make the problem feasible we update the bound of the constraint that
340 // contains x_; we take this opportunity to also test inverting bounds of
341 // constraints.
342 model_.set_lower_bound(c_, 4.0);
343 // At this point c_ lower bound is 4.0 and upper bound is 1.5.
344 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
345
346 // We restore valid bounds by setting c_ upper bound to 5.5.
347 model_.set_upper_bound(c_, 5.5);
348 ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));
349
350 EXPECT_THAT(solver_->SolveWithoutUpdate(),
351 IsOkAndHolds(IsOptimal(3 * 4.5 + 2 * 1 + 0.1)));
352}
353
354} // namespace
355} // namespace math_opt
356} // namespace operations_research
IntegerValue y
absl::StatusOr< SolveResultProto > Solve(const SolveArgs &arguments) override
Solves the current model (including all updates).
Definition solver.cc:101
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()))
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)
absl::StatusOr< std::unique_ptr< IncrementalSolver > > NewIncrementalSolver(Model *model, SolverType solver_type, SolverInitArguments arguments)
Definition solve.cc:82
Matcher< SolveResult > TerminatesWith(const TerminationReason expected)
Definition matchers.cc:621
Matcher< VariableMap< double > > IsNear(VariableMap< double > expected, const double tolerance)
Definition matchers.cc:221
Matcher< UpdateResult > DidUpdate()
Actual UpdateResult.did_update is true.
Definition matchers.cc:1027
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.
internal::IsOkAndHoldsMatcher< typename std::decay< InnerMatcher >::type > IsOkAndHolds(InnerMatcher &&inner_matcher)
const Variable x
Definition qp_tests.cc:127
#define ASSERT_OK_AND_ASSIGN(lhs, rexpr)
SimpleMipTestParameters(SolverType solver_type, bool report_unboundness_correctly=true)
Definition mip_tests.cc:40