OR-Tools  9.3
quadratic_program_test.cc
Go to the documentation of this file.
1// Copyright 2010-2021 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 <cstdint>
17#include <limits>
18#include <string>
19#include <tuple>
20#include <utility>
21#include <vector>
22
23#include "Eigen/Core"
24#include "Eigen/SparseCore"
25#include "absl/status/status.h"
26#include "absl/status/statusor.h"
27#include "absl/types/optional.h"
28#include "gmock/gmock.h"
29#include "gtest/gtest.h"
32#include "ortools/linear_solver/linear_solver.pb.h"
34
36namespace {
37
40using ::testing::ElementsAre;
41using ::testing::Eq;
42using ::testing::HasSubstr;
43using ::testing::IsEmpty;
44using ::testing::Optional;
45using ::testing::PrintToString;
46const double kInfinity = std::numeric_limits<double>::infinity();
47
48TEST(QuadraticProgram, DefaultConstructorWorks) { QuadraticProgram qp; }
49
50TEST(QuadraticProgram, MoveConstructor) {
51 QuadraticProgram qp1 = TestDiagonalQp1();
52 QuadraticProgram qp2(std::move(qp1));
53 VerifyTestQp(qp2);
54}
55
56TEST(QuadraticProgram, MoveAssignment) {
57 QuadraticProgram qp1 = TestDiagonalQp1();
58 QuadraticProgram qp2;
59 qp2 = std::move(qp1);
60 VerifyTestQp(qp2);
61}
62
64 const absl::Status status =
66 EXPECT_TRUE(status.ok()) << status;
67}
68
69TEST(ValidateQuadraticProgramDimensions, ConstraintLowerBoundsInconsistent) {
70 QuadraticProgram qp;
71 qp.ResizeAndInitialize(/*num_variables=*/2, /*num_constraints=*/3);
72 qp.constraint_lower_bounds.resize(10);
73 EXPECT_EQ(ValidateQuadraticProgramDimensions(qp).code(),
74 absl::StatusCode::kInvalidArgument);
75}
76
77TEST(ValidateQuadraticProgramDimensions, ConstraintUpperBoundsInconsistent) {
78 QuadraticProgram qp;
79 qp.ResizeAndInitialize(/*num_variables=*/2, /*num_constraints=*/3);
80 qp.constraint_upper_bounds.resize(10);
81 EXPECT_EQ(ValidateQuadraticProgramDimensions(qp).code(),
82 absl::StatusCode::kInvalidArgument);
83}
84
85TEST(ValidateQuadraticProgramDimensions, ObjectiveVectorInconsistent) {
86 QuadraticProgram qp;
87 qp.ResizeAndInitialize(/*num_variables=*/2, /*num_constraints=*/3);
88 qp.objective_vector.resize(10);
89 EXPECT_EQ(ValidateQuadraticProgramDimensions(qp).code(),
90 absl::StatusCode::kInvalidArgument);
91}
92
93TEST(ValidateQuadraticProgramDimensions, VariableLowerBoundsInconsistent) {
94 QuadraticProgram qp;
95 qp.ResizeAndInitialize(/*num_variables=*/2, /*num_constraints=*/3);
96 qp.variable_lower_bounds.resize(10);
97 EXPECT_EQ(ValidateQuadraticProgramDimensions(qp).code(),
98 absl::StatusCode::kInvalidArgument);
99}
100
101TEST(ValidateQuadraticProgramDimensions, VariableUpperBoundsInconsistent) {
102 QuadraticProgram qp;
103 qp.ResizeAndInitialize(/*num_variables=*/2, /*num_constraints=*/3);
104 qp.variable_upper_bounds.resize(10);
105 EXPECT_EQ(ValidateQuadraticProgramDimensions(qp).code(),
106 absl::StatusCode::kInvalidArgument);
107}
108
109TEST(ValidateQuadraticProgramDimensions, ConstraintMatrixRowsInconsistent) {
110 QuadraticProgram qp;
111 qp.ResizeAndInitialize(/*num_variables=*/2, /*num_constraints=*/3);
112 qp.constraint_matrix.resize(10, 2);
113 EXPECT_EQ(ValidateQuadraticProgramDimensions(qp).code(),
114 absl::StatusCode::kInvalidArgument);
115}
116
117TEST(ValidateQuadraticProgramDimensions, ConstraintMatrixColsInconsistent) {
118 QuadraticProgram qp;
119 qp.ResizeAndInitialize(/*num_variables=*/2, /*num_constraints=*/3);
120 qp.constraint_matrix.resize(2, 10);
121 EXPECT_EQ(ValidateQuadraticProgramDimensions(qp).code(),
122 absl::StatusCode::kInvalidArgument);
123}
124
125TEST(ValidateQuadraticProgramDimensions, ObjectiveMatrixRowsInconsistent) {
126 QuadraticProgram qp;
127 qp.ResizeAndInitialize(/*num_variables=*/2, /*num_constraints=*/3);
128 qp.objective_matrix.emplace();
129 qp.objective_matrix->resize(10);
130 EXPECT_EQ(ValidateQuadraticProgramDimensions(qp).code(),
131 absl::StatusCode::kInvalidArgument);
132}
133
134TEST(HasValidBoundsTest, InconsistentConstraintBounds) {
135 QuadraticProgram invalid_lp = SmallInvalidProblemLp();
136 EXPECT_FALSE(HasValidBounds(invalid_lp));
137}
138
139TEST(HasValidBoundsTest, InconsistentVariableBounds) {
140 QuadraticProgram invalid_lp = SmallInconsistentVariableBoundsLp();
141 EXPECT_FALSE(HasValidBounds(invalid_lp));
142}
143
144TEST(HasValidBoundsTest, SmallValidLp) {
145 QuadraticProgram valid_lp = SmallPrimalInfeasibleLp();
146 EXPECT_TRUE(HasValidBounds(valid_lp));
147}
148
149class ConvertQpMpModelProtoTest : public testing::TestWithParam<bool> {};
150
151// The LP:
152// optimize 5.5 x_0 + 2 x_1 - x_2 + x_3 - 14 s.t.
153// 2 x_0 + x_1 + x_2 + 2 x_3 = 12
154// x_0 + x_2 >= 7
155// 3.5 x_0 <= -4
156// -1 <= 1.5 x_2 - x_3 <= 1
157// -infinity <= x_0 <= infinity
158// -2 <= x_1 <= infinity
159// -infinity <= x_2 <= 6
160// 2.5 <= x_3 <= 3.5
161MPModelProto TestLpProto(bool maximize) {
162 auto proto = ParseTextOrDie<MPModelProto>(R"pb(variable {
163 lower_bound: -inf
164 upper_bound: inf
165 objective_coefficient: 5.5
166 }
167 variable {
168 lower_bound: -2
169 upper_bound: inf
170 objective_coefficient: -2
171 }
172 variable {
173 lower_bound: -inf
174 upper_bound: 6
175 objective_coefficient: -1
176 }
177 variable {
178 lower_bound: 2.5
179 upper_bound: 3.5
180 objective_coefficient: 1
181 }
182 constraint {
183 lower_bound: 12
184 upper_bound: 12
185 var_index: [ 0, 1, 2, 3 ]
186 coefficient: [ 2, 1, 1, 2 ]
187 }
188 constraint {
189 lower_bound: -inf
190 upper_bound: 7
191 var_index: [ 0, 2 ]
192 coefficient: [ 1, 1 ]
193 }
194 constraint {
195 lower_bound: -4
196 upper_bound: inf
197 var_index: [ 0 ]
198 coefficient: [ 4 ]
199 }
200 constraint {
201 lower_bound: -1
202 upper_bound: 1
203 var_index: [ 2, 3 ]
204 coefficient: [ 1.5, -1 ]
205 }
206 objective_offset: -14)pb");
207 proto.set_maximize(maximize);
208 return proto;
209}
210
211// This is tested for both minimization and maximization.
212TEST_P(ConvertQpMpModelProtoTest, LpFromMpModelProto) {
213 const bool maximize = GetParam();
214 MPModelProto proto = TestLpProto(maximize);
215 const auto lp = QpFromMpModelProto(proto, /*relax_integer_variables=*/false);
216 ASSERT_TRUE(lp.ok()) << lp.status();
217
218 VerifyTestLp(*lp, maximize);
219}
220
221// The QP:
222// optimize x_0^2 + x_1^2 + 3 x_0 - 4 s.t.
223// x_0 + x_1 <= 42
224// -1 <= x_0 <= 2
225// -2 <= x_1 <= 3
226MPModelProto TestQpProto(bool maximize) {
227 auto proto = ParseTextOrDie<MPModelProto>(
228 R"pb(variable { lower_bound: -1 upper_bound: 2 objective_coefficient: 3 }
229 variable { lower_bound: -2 upper_bound: 3 objective_coefficient: 0 }
230 constraint {
231 lower_bound: -inf
232 upper_bound: 42
233 var_index: [ 0, 1 ]
234 coefficient: [ 1, 1 ]
235 }
236 objective_offset: -4
237 quadratic_objective {
238 qvar1_index: [ 0, 1 ]
239 qvar2_index: [ 0, 1 ]
240 coefficient: [ 1, 1 ]
241 }
242 )pb");
243 proto.set_maximize(maximize);
244 return proto;
245}
246
247// This is tested for both minimization and maximization.
248TEST_P(ConvertQpMpModelProtoTest, QpFromMpModelProto) {
249 const bool maximize = GetParam();
250 MPModelProto proto = TestQpProto(maximize);
251 const auto qp = QpFromMpModelProto(proto, /*relax_integer_variables=*/false);
252 ASSERT_TRUE(qp.ok()) << qp.status();
253
254 EXPECT_THAT(qp->constraint_lower_bounds, ElementsAre(-kInfinity));
255 EXPECT_THAT(qp->constraint_upper_bounds, ElementsAre(42));
256 EXPECT_THAT(qp->variable_lower_bounds, ElementsAre(-1, -2));
257 EXPECT_THAT(qp->variable_upper_bounds, ElementsAre(2, 3));
258 EXPECT_THAT(ToDense(qp->constraint_matrix), EigenArrayEq<double>({{1, 1}}));
259 EXPECT_TRUE(qp->constraint_matrix.isCompressed());
260
261 double sign = maximize ? -1 : 1;
262 EXPECT_EQ(sign * qp->objective_offset, -4);
263 EXPECT_EQ(qp->objective_scaling_factor, sign);
264 EXPECT_THAT(sign * qp->objective_vector, ElementsAre(3, 0));
265 EXPECT_THAT(sign * (qp->objective_matrix->diagonal()),
266 EigenArrayEq<double>({2, 2}));
267}
268
269TEST(QpFromMpModelProto, ErrorsOnOffDiagonalTerms) {
270 auto proto = ParseTextOrDie<MPModelProto>(
271 R"pb(variable { lower_bound: -1 upper_bound: 2 objective_coefficient: 3 }
272 variable { lower_bound: -2 upper_bound: 3 objective_coefficient: 0 }
273 constraint {
274 lower_bound: -inf
275 upper_bound: 42
276 var_index: [ 0, 1 ]
277 coefficient: [ 1, 1 ]
278 }
279 objective_offset: -4
280 quadratic_objective {
281 qvar1_index: [ 0 ]
282 qvar2_index: [ 1 ]
283 coefficient: [ 1 ]
284 }
285 )pb");
286 EXPECT_EQ(QpFromMpModelProto(proto, /*relax_integer_variables=*/false)
287 .status()
288 .code(),
289 absl::StatusCode::kInvalidArgument);
290}
291
292TEST(CanFitInMpModelProto, SmallQpOk) {
293 // QpFromMpModelProtoTest verifies that qp is as expected.
294 const auto qp = QpFromMpModelProto(TestQpProto(/*maximize=*/false),
295 /*relax_integer_variables=*/false);
296 ASSERT_TRUE(qp.ok()) << qp.status();
297 EXPECT_TRUE(CanFitInMpModelProto(*qp).ok());
298}
299
300// The ILP:
301// optimize x_0 + 2 * x_1 s.t.
302// x_0 + x_1 <= 1
303// -1 <= x_0 <= 2
304// -2 <= x_1 <= 3
305// x_1 integer
306// This is tested for both minimization and maximization.
307TEST_P(ConvertQpMpModelProtoTest, IntegerVariablesFromMpModelProto) {
308 const bool maximize = GetParam();
309 auto proto = ParseTextOrDie<MPModelProto>(
310 R"pb(variable { lower_bound: -1 upper_bound: 2 objective_coefficient: 1 }
311 variable {
312 lower_bound: -2
313 upper_bound: 3
314 objective_coefficient: 2
315 is_integer: true
316 }
317 constraint {
318 lower_bound: -inf
319 upper_bound: 1
320 var_index: [ 0, 1 ]
321 coefficient: [ 1, 1 ]
322 }
323 )pb");
324 proto.set_maximize(maximize);
325 EXPECT_EQ(QpFromMpModelProto(proto, /*relax_integer_variables=*/false)
326 .status()
327 .code(),
328 absl::StatusCode::kInvalidArgument);
329 const auto lp = QpFromMpModelProto(proto, /*relax_integer_variables=*/true);
330 ASSERT_TRUE(lp.ok()) << lp.status();
331
332 EXPECT_THAT(lp->constraint_lower_bounds, ElementsAre(-kInfinity));
333 EXPECT_THAT(lp->constraint_upper_bounds, ElementsAre(1));
334 EXPECT_THAT(lp->variable_lower_bounds, ElementsAre(-1, -2));
335 EXPECT_THAT(lp->variable_upper_bounds, ElementsAre(2, 3));
336 EXPECT_THAT(ToDense(lp->constraint_matrix), EigenArrayEq<double>({{1, 1}}));
337 EXPECT_TRUE(lp->constraint_matrix.isCompressed());
338
339 double sign = maximize ? -1 : 1;
340 EXPECT_EQ(lp->objective_offset, 0);
341 EXPECT_THAT(sign * lp->objective_vector, ElementsAre(1, 2));
342 EXPECT_FALSE(lp->objective_matrix.has_value());
343}
344
345MPModelProto TinyModelWithNames() {
346 return ParseTextOrDie<MPModelProto>(
347 R"pb(name: "problem"
348 variable {
349 name: "x_0"
350 lower_bound: -1
351 upper_bound: 2
352 objective_coefficient: 1
353 }
354 variable {
355 name: "x_1"
356 lower_bound: -2
357 upper_bound: 3
358 objective_coefficient: 2
359 }
360 constraint {
361 name: "c_0"
362 lower_bound: -inf
363 upper_bound: 1
364 var_index: [ 0, 1 ]
365 coefficient: [ 1, 1 ]
366 }
367 )pb");
368}
369
370TEST(QpFromMpModelProtoTest, EmptyQp) {
371 MPModelProto proto;
372 const auto qp = QpFromMpModelProto(proto, /*relax_integer_variables=*/false);
373 ASSERT_TRUE(qp.ok()) << qp.status();
374
375 EXPECT_THAT(qp->constraint_lower_bounds, ElementsAre());
376 EXPECT_THAT(qp->constraint_upper_bounds, ElementsAre());
377 EXPECT_THAT(qp->variable_lower_bounds, ElementsAre());
378 EXPECT_THAT(qp->variable_upper_bounds, ElementsAre());
379 EXPECT_EQ(qp->constraint_matrix.cols(), 0);
380 EXPECT_EQ(qp->constraint_matrix.rows(), 0);
381 EXPECT_EQ(qp->objective_offset, 0);
382 EXPECT_EQ(qp->objective_scaling_factor, 1);
383 EXPECT_FALSE(qp->objective_matrix.has_value());
384 EXPECT_THAT(qp->objective_vector, ElementsAre());
385}
386
387TEST(QpFromMpModelProtoTest, DoesNotIncludeNames) {
388 const auto lp =
389 QpFromMpModelProto(TinyModelWithNames(), /*relax_integer_variables=*/true,
390 /*include_names=*/false);
391 ASSERT_TRUE(lp.ok()) << lp.status();
392 EXPECT_EQ(lp->problem_name, absl::nullopt);
393 EXPECT_EQ(lp->variable_names, absl::nullopt);
394 EXPECT_EQ(lp->constraint_names, absl::nullopt);
395}
396
397TEST(QpFromMpModelProtoTest, IncludesNames) {
398 const auto lp =
399 QpFromMpModelProto(TinyModelWithNames(), /*relax_integer_variables=*/true,
400 /*include_names=*/true);
401 ASSERT_TRUE(lp.ok()) << lp.status();
402 EXPECT_THAT(lp->problem_name, Optional(Eq("problem")));
403 EXPECT_THAT(lp->variable_names, Optional(ElementsAre("x_0", "x_1")));
404 EXPECT_THAT(lp->constraint_names, Optional(ElementsAre("c_0")));
405}
406
407INSTANTIATE_TEST_SUITE_P(
408 ConvertQpMpModelProtoTests, ConvertQpMpModelProtoTest, testing::Bool(),
409 [](const testing::TestParamInfo<ConvertQpMpModelProtoTest::ParamType>&
410 info) {
411 if (info.param) {
412 return "maximize";
413 } else {
414 return "minimize";
415 }
416 });
417
418// A matcher for Eigen Triplets.
419MATCHER_P3(IsEigenTriplet, row, col, value,
420 std::string(negation ? "isn't" : "is") + " the triplet " +
421 PrintToString(row) + "," + PrintToString(col) + "=" +
422 PrintToString(value)) {
423 return arg.row() == row && arg.col() == col && arg.value() == value;
424}
425
426TEST(CombineRepeatedTripletsInPlace, HandlesEmptyTriplets) {
427 std::vector<Eigen::Triplet<double, int64_t>> triplets;
429 EXPECT_THAT(triplets, IsEmpty());
430}
431
432TEST(CombineRepeatedTripletsInPlace, CorrectForSingleTriplet) {
433 std::vector<Eigen::Triplet<double, int64_t>> triplets = {{1, 2, 3.0}};
435 EXPECT_THAT(triplets, ElementsAre(IsEigenTriplet(1, 2, 3.0)));
436}
437
438TEST(CombineRepeatedTripletsInPlace, CorrectForDistinctTriplets) {
439 std::vector<Eigen::Triplet<double, int64_t>> triplets = {
440 {1, 2, 3.0}, {2, 1, 1.0}, {1, 1, 0.0}};
442 EXPECT_THAT(triplets,
443 ElementsAre(IsEigenTriplet(1, 2, 3.0), IsEigenTriplet(2, 1, 1.0),
444 IsEigenTriplet(1, 1, 0.0)));
445}
446
447TEST(CombineRepeatedTripletsInPlace, CombinesDuplicatesAtStart) {
448 std::vector<Eigen::Triplet<double, int64_t>> triplets = {
449 {1, 2, 3.0}, {1, 2, -1.0}, {1, 1, 0.0}};
451 EXPECT_THAT(triplets, ElementsAre(IsEigenTriplet(1, 2, 2.0),
452 IsEigenTriplet(1, 1, 0.0)));
453}
454
455TEST(CombineRepeatedTripletsInPlace, CombinesDuplicatesAtEnd) {
456 std::vector<Eigen::Triplet<double, int64_t>> triplets = {
457 {1, 2, 3.0}, {2, 1, 1.0}, {2, 1, 1.0}};
459 EXPECT_THAT(triplets, ElementsAre(IsEigenTriplet(1, 2, 3.0),
460 IsEigenTriplet(2, 1, 2.0)));
461}
462
463TEST(CombineRepeatedTripletsInPlace, CombinesToSingleton) {
464 std::vector<Eigen::Triplet<double, int64_t>> triplets = {
465 {1, 2, 3.0}, {1, 2, 1.0}, {1, 2, 2.0}};
467 EXPECT_THAT(triplets, ElementsAre(IsEigenTriplet(1, 2, 6.0)));
468}
469
470TEST(SetEigenMatrixFromTriplets, HandlesEmptyMatrix) {
471 std::vector<Eigen::Triplet<double, int64_t>> triplets;
472 Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t> matrix(2, 2);
473 SetEigenMatrixFromTriplets(std::move(triplets), matrix);
474 EXPECT_THAT(ToDense(matrix), EigenArrayEq<double>({{0, 0}, //
475 {0, 0}}));
476}
477
478TEST(SetEigenMatrixFromTriplets, CorrectForTinyMatrix) {
479 std::vector<Eigen::Triplet<double, int64_t>> triplets = {
480 {0, 0, 1.0}, {1, 0, -1.0}, {0, 0, 0.0}, {1, 1, 1.0}, {0, 0, 1.0}};
481 Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t> matrix(2, 2);
482 SetEigenMatrixFromTriplets(std::move(triplets), matrix);
483 EXPECT_THAT(ToDense(matrix), EigenArrayEq<double>({{2, 0}, //
484 {-1, 1}}));
485}
486
487} // namespace
488} // namespace operations_research::pdlp
CpModelProto proto
int64_t value
absl::Status status
Definition: g_gurobi.cc:35
ColIndex col
Definition: markowitz.cc:183
RowIndex row
Definition: markowitz.cc:182
T ParseTextOrDie(const std::string &input)
Definition: protobuf_util.h:77
void CombineRepeatedTripletsInPlace(std::vector< Eigen::Triplet< double, int64_t > > &triplets)
absl::StatusOr< QuadraticProgram > QpFromMpModelProto(const MPModelProto &proto, bool relax_integer_variables, bool include_names)
absl::Status ValidateQuadraticProgramDimensions(const QuadraticProgram &qp)
::Eigen::ArrayXXd ToDense(const Eigen::SparseMatrix< double, Eigen::ColMajor, int64_t > &sparse_mat)
Definition: test_util.cc:284
void VerifyTestQp(const QuadraticProgram &qp, bool maximize)
Definition: test_util.cc:269
absl::Status CanFitInMpModelProto(const QuadraticProgram &qp)
void SetEigenMatrixFromTriplets(std::vector< Eigen::Triplet< double, int64_t > > triplets, Eigen::SparseMatrix< double, Eigen::ColMajor, int64_t > &matrix)
bool HasValidBounds(const QuadraticProgram &qp)
void VerifyTestLp(const QuadraticProgram &qp, bool maximize)
Definition: test_util.cc:48
QuadraticProgram SmallInvalidProblemLp()
Definition: test_util.cc:190
QuadraticProgram SmallPrimalInfeasibleLp()
Definition: test_util.cc:216
QuadraticProgram SmallInconsistentVariableBoundsLp()
Definition: test_util.cc:203
QuadraticProgram TestDiagonalQp1()
Definition: test_util.cc:142
TEST(LinearAssignmentTest, NullMatrix)