Google OR-Tools v9.9
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
qc_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 <memory>
18#include <ostream>
19#include <string>
20#include <type_traits>
21#include <utility>
22
23#include "absl/status/status.h"
24#include "absl/strings/string_view.h"
25#include "gtest/gtest.h"
26#include "ortools/base/gmock.h"
30
32
34 const SolverType solver_type, SolveParameters parameters,
35 const bool supports_qc, const bool supports_incremental_add_and_deletes,
36 const bool supports_incremental_variable_deletions,
37 const bool use_integer_variables)
38 : solver_type(solver_type),
40 supports_qc(supports_qc),
41 supports_incremental_add_and_deletes(
42 supports_incremental_add_and_deletes),
43 supports_incremental_variable_deletions(
44 supports_incremental_variable_deletions),
45 use_integer_variables(use_integer_variables) {}
46
47std::ostream& operator<<(std::ostream& out, const QcTestParameters& params) {
48 out << "{ solver_type: " << params.solver_type
49 << ", parameters: " << ProtobufShortDebugString(params.parameters.Proto())
50 << ", supports_qc: " << (params.supports_qc ? "true" : "false")
51 << ", supports_incremental_add_and_deletes: "
52 << (params.supports_incremental_add_and_deletes ? "true" : "false")
53 << ", supports_incremental_variable_deletions: "
54 << (params.supports_incremental_variable_deletions ? "true" : "false")
55 << ", use_integer_variables: "
56 << (params.use_integer_variables ? "true" : "false") << " }";
57 return out;
58}
59
60namespace {
61
62using ::testing::AnyOf;
63using ::testing::HasSubstr;
64using ::testing::status::IsOkAndHolds;
65using ::testing::status::StatusIs;
66
67constexpr double kTolerance = 1.0e-4;
68constexpr absl::string_view no_qc_support_message =
69 "This test is disabled as the solver does not support quadratic "
70 "constraints";
71
72// Models the following problem:
73// min_x x + 5
74// s.t. x^2 - x <= 1
75// -1 <= x <= 1
76//
77// along with, if use_integer_variables = true, integrality on x.
78//
79// If use_integer_variables = false, the unique optimal solution is attained at
80// x = (1 - sqrt(5)) / 2 with objective value (1 - sqrt(5)) / 2 + 5. Otherwise,
81// the unique optimal solution is x = 0 with objective value 5.
82struct UnivariateQcProblem {
83 explicit UnivariateQcProblem(bool use_integer_variables)
84 : model(), x(model.AddVariable(-1.0, 1.0, use_integer_variables, "x")) {
85 model.AddQuadraticConstraint(x * x - x <= 1.0);
86 model.Minimize(x + 5.0);
87 }
88
89 Model model;
90 const Variable x;
91};
92
93TEST_P(SimpleQcTest, CanBuildQcModel) {
94 UnivariateQcProblem qc_problem(GetParam().use_integer_variables);
95 if (GetParam().supports_qc) {
97 IncrementalSolver::New(&qc_problem.model, GetParam().solver_type, {}));
98 } else {
100 IncrementalSolver::New(&qc_problem.model, GetParam().solver_type, {}),
101 StatusIs(AnyOf(absl::StatusCode::kInvalidArgument,
102 absl::StatusCode::kUnimplemented),
103 HasSubstr("quadratic constraints")));
104 }
105}
106
107TEST_P(SimpleQcTest, SolveSimpleQc) {
108 if (!GetParam().supports_qc) {
109 GTEST_SKIP() << no_qc_support_message;
110 }
111 const UnivariateQcProblem qc_problem(GetParam().use_integer_variables);
112 const double x_expected =
113 GetParam().use_integer_variables ? 0.0 : -0.618033988748785;
114 EXPECT_THAT(SimpleSolve(qc_problem.model),
116 5.0 + x_expected, {{qc_problem.x, x_expected}})));
117}
118
119// Models the following problem:
120// min_{x,y} y
121// s.t. (x - 1)^2 + (y - 1)^2 + xy == x^2 + xy + y^2 - 2x - 2y + 2 <= 1
122// x <= y
123// 0 <= x <= 0.5
124// 0 <= y <= 1
125//
126// along with, if use_integer_variables = true, integrality on x and y.
127//
128// If use_integer_variables = false, the unique optimal solution is attained at
129// (x, y) = (1/3, 1/3) with objective value 1/3. Otherwise, the unique optimal
130// solution is (x, y) = (0, 1) with objective value 1.
131struct HalfEllipseProblem {
132 explicit HalfEllipseProblem(bool use_integer_variables)
133 : model(),
134 x(model.AddVariable(0.0, 0.5, use_integer_variables, "x")),
135 y(model.AddVariable(0.0, 1.0, use_integer_variables, "y")),
136 q(model.AddQuadraticConstraint(x * x + x * y + y * y - 2 * x - 2 * y <=
137 -1)) {
138 model.Minimize(y);
139 model.AddLinearConstraint(x <= y);
140 }
141
142 Model model;
143 const Variable x;
144 const Variable y;
145 const QuadraticConstraint q;
146};
147
148TEST_P(SimpleQcTest, SolveHalfEllipseQc) {
149 if (!GetParam().supports_qc) {
150 GTEST_SKIP() << no_qc_support_message;
151 }
152 const HalfEllipseProblem qc_problem(GetParam().use_integer_variables);
153 if (GetParam().use_integer_variables) {
154 EXPECT_THAT(SimpleSolve(qc_problem.model),
156 1.0, {{qc_problem.x, 0.0}, {qc_problem.y, 1.0}})));
157 } else {
158 const double value = 1.0 / 3.0;
159 EXPECT_THAT(SimpleSolve(qc_problem.model),
161 value, {{qc_problem.x, value}, {qc_problem.y, value}})));
162 }
163}
164
165// We start with the simple LP:
166// max x + 1
167// s.t. 0 <= x <= 1
168//
169// The optimal value is 2. We then add a quadratic constraint:
170// x^2 <= 0.5
171//
172// The optimal solution is x = sqrt(0.5) with objective value 1 + sqrt(0.5).
173// Additionally, if we impose integrality on x, then the optimal solution is
174// x = 0 with objective value 1.
175TEST_P(IncrementalQcTest, LinearToQuadraticUpdate) {
176 Model model;
177 const Variable x =
178 model.AddVariable(0.0, 1.0, GetParam().use_integer_variables, "x");
179 model.Maximize(x + 1.0);
180
182 const auto solver,
183 IncrementalSolver::New(&model, GetParam().solver_type, {}));
184 ASSERT_THAT(solver->Solve({.parameters = GetParam().parameters}),
185 IsOkAndHolds(IsOptimalWithSolution(2.0, {{x, 1.0}})));
186
187 model.AddQuadraticConstraint(x * x <= 0.5);
188
189 if (!GetParam().supports_qc) {
190 // Here we test that solvers that don't support quadratic constraints return
191 // false in SolverInterface::CanUpdate(). Thus they should fail in their
192 // factory function instead of failing in their SolverInterface::Update()
193 // function. To assert we rely on status annotations added by
194 // IncrementalSolver::Update() to the returned status of Solver::Update()
195 // and Solver::New().
197 solver->Update(),
198 StatusIs(AnyOf(absl::StatusCode::kInvalidArgument,
199 absl::StatusCode::kUnimplemented),
200 AllOf(HasSubstr("quadratic constraint"),
201 // Sub-string expected for Solver::Update() error.
202 Not(HasSubstr("update failed")),
203 // Sub-string expected for Solver::New() error.
204 HasSubstr("solver re-creation failed"))));
205 return;
206 }
207
208 ASSERT_THAT(solver->Update(),
209 IsOkAndHolds(GetParam().supports_incremental_add_and_deletes
210 ? DidUpdate()
211 : Not(DidUpdate())));
212 const double expected_x =
213 GetParam().use_integer_variables ? 0.0 : std::sqrt(0.5);
215 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}),
216 IsOkAndHolds(IsOptimalWithSolution(1.0 + expected_x, {{x, expected_x}})));
217}
218
219// We start with the QCP:
220// min_{x,y} y
221// s.t. (x - 1)^2 + (y - 1)^2 + xy <= 1
222// x <= y
223// 0 <= x <= 0.5
224// 0 <= y <= 1
225//
226// We then delete the quadratic constraint, leaving the LP:
227// min_{x,y} y
228// s.t. x <= y
229// 0 <= x <= 0.5
230// 0 <= y <= 1
231//
232// The optimal solution is attained at (x, y) = (0, 0).
233TEST_P(IncrementalQcTest, UpdateDeletesQuadraticConstraint) {
234 if (!GetParam().supports_qc) {
235 GTEST_SKIP() << no_qc_support_message;
236 }
237 HalfEllipseProblem qc_problem(GetParam().use_integer_variables);
239 const auto solver,
240 IncrementalSolver::New(&qc_problem.model, GetParam().solver_type, {}));
241 // We test that the solution is correct elsewhere.
242 ASSERT_OK(solver->Solve({.parameters = GetParam().parameters}));
243
244 qc_problem.model.DeleteQuadraticConstraint(qc_problem.q);
245
246 ASSERT_THAT(solver->Update(),
247 IsOkAndHolds(GetParam().supports_incremental_add_and_deletes
248 ? DidUpdate()
249 : Not(DidUpdate())));
250 EXPECT_THAT(solver->SolveWithoutUpdate({.parameters = GetParam().parameters}),
252 0.0, {{qc_problem.x, 0.0}, {qc_problem.y, 0.0}})));
253}
254
255// We start with the QCP:
256// min_{x,y} y
257// s.t. (x - 1)^2 + (y - 1)^2 + xy <= 1
258// x <= y
259// 0 <= x, y <= 2
260//
261// We then delete the x variable, leaving the QCP:
262// min_{y} y
263// s.t. 1 + (y - 1)^2 == y^2 - 2y + 2 <= 1
264// 0 <= y <= 2
265//
266// The optimal solution is attained at y = 1 with objective value 1.
267TEST_P(IncrementalQcTest, UpdateDeletesVariableInQuadraticConstraint) {
268 if (!GetParam().supports_qc) {
269 GTEST_SKIP() << no_qc_support_message;
270 }
271 HalfEllipseProblem qc_problem(GetParam().use_integer_variables);
272
274 const auto solver,
275 IncrementalSolver::New(&qc_problem.model, GetParam().solver_type, {}));
276 // We test that the solution is correct elsewhere.
277 ASSERT_OK(solver->Solve({.parameters = GetParam().parameters}));
278
279 qc_problem.model.DeleteVariable(qc_problem.x);
280
281 ASSERT_THAT(solver->Update(),
282 IsOkAndHolds(GetParam().supports_incremental_variable_deletions
283 ? DidUpdate()
284 : Not(DidUpdate())));
285 EXPECT_THAT(solver->SolveWithoutUpdate({.parameters = GetParam().parameters}),
286 IsOkAndHolds(IsOptimalWithSolution(1.0, {{qc_problem.y, 1.0}},
287 kTolerance)));
288}
289
290} // namespace
291} // namespace operations_research::math_opt
static absl::StatusOr< std::unique_ptr< IncrementalSolver > > New(Model *model, SolverType solver_type, SolverInitArguments arguments={})
Definition solve.cc:136
SatParameters parameters
int64_t value
GRBmodel * model
An object oriented wrapper for quadratic constraints in ModelStorage.
Definition gurobi_isv.cc:28
Matcher< SolveResult > IsOptimalWithSolution(const double expected_objective, const VariableMap< double > expected_variable_values, const double tolerance)
Definition matchers.cc:747
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()))
std::ostream & operator<<(std::ostream &ostr, const IndicatorConstraint &constraint)
<=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_=IncrementalSolver::New(&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, IncrementalSolver::New(&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)))
Matcher< UpdateResult > DidUpdate()
Actual UpdateResult.did_update is true.
Definition matchers.cc:979
BoolVar Not(BoolVar x)
Definition cp_model.cc:87
std::string ProtobufShortDebugString(const P &message)
Definition proto_utils.h:41
STL namespace.
internal::StatusIsMatcher StatusIs(CodeMatcher code_matcher, MessageMatcher message_matcher)
const Variable x
Definition qp_tests.cc:127
#define ASSERT_OK(expression)
#define EXPECT_OK(expression)
#define ASSERT_OK_AND_ASSIGN(lhs, rexpr)
bool use_integer_variables
True if the solver supports integer variables.
Definition qc_tests.h:49
SolverType solver_type
The tested solver.
Definition qc_tests.h:33
bool supports_qc
True if the solver supports quadratic constraints.
Definition qc_tests.h:38
QcTestParameters(SolverType solver_type, SolveParameters parameters, bool supports_qc, bool supports_incremental_add_and_deletes, bool supports_incremental_variable_deletions, bool use_integer_variables)
Definition qc_tests.cc:33