OR-Tools  9.6
quadratic_program_test.cc
Go to the documentation of this file.
1 // Copyright 2010-2022 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 <optional>
19 #include <string>
20 #include <tuple>
21 #include <utility>
22 #include <vector>
23 
24 #include "Eigen/Core"
25 #include "Eigen/SparseCore"
26 #include "absl/status/status.h"
27 #include "absl/status/statusor.h"
28 #include "gmock/gmock.h"
29 #include "gtest/gtest.h"
32 #include "ortools/linear_solver/linear_solver.pb.h"
33 #include "ortools/pdlp/test_util.h"
34 
35 namespace operations_research::pdlp {
36 namespace {
37 
40 using ::testing::ElementsAre;
41 using ::testing::Eq;
42 using ::testing::HasSubstr;
43 using ::testing::IsEmpty;
44 using ::testing::Optional;
45 using ::testing::PrintToString;
46 const double kInfinity = std::numeric_limits<double>::infinity();
47 
48 TEST(QuadraticProgram, DefaultConstructorWorks) { QuadraticProgram qp; }
49 
50 TEST(QuadraticProgram, MoveConstructor) {
51  QuadraticProgram qp1 = TestDiagonalQp1();
52  QuadraticProgram qp2(std::move(qp1));
54 }
55 
56 TEST(QuadraticProgram, MoveAssignment) {
57  QuadraticProgram qp1 = TestDiagonalQp1();
58  QuadraticProgram qp2;
59  qp2 = std::move(qp1);
61 }
62 
64  const absl::Status status =
66  EXPECT_TRUE(status.ok()) << status;
67 }
68 
69 TEST(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 
77 TEST(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 
85 TEST(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 
93 TEST(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 
101 TEST(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 
109 TEST(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 
117 TEST(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 
125 TEST(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 
134 TEST(HasValidBoundsTest, InconsistentConstraintBounds) {
135  QuadraticProgram invalid_lp = SmallInvalidProblemLp();
136  EXPECT_FALSE(HasValidBounds(invalid_lp));
137 }
138 
139 TEST(HasValidBoundsTest, InconsistentVariableBounds) {
140  QuadraticProgram invalid_lp = SmallInconsistentVariableBoundsLp();
141  EXPECT_FALSE(HasValidBounds(invalid_lp));
142 }
143 
144 TEST(HasValidBoundsTest, SmallValidLp) {
145  QuadraticProgram valid_lp = SmallPrimalInfeasibleLp();
146  EXPECT_TRUE(HasValidBounds(valid_lp));
147 }
148 
149 class 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
161 MPModelProto 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.
212 TEST_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
226 MPModelProto 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.
248 TEST_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 
269 TEST(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 
292 TEST(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.
307 TEST_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 
345 MPModelProto 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 
370 TEST(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 
387 TEST(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, std::nullopt);
393  EXPECT_EQ(lp->variable_names, std::nullopt);
394  EXPECT_EQ(lp->constraint_names, std::nullopt);
395 }
396 
397 TEST(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 
407 INSTANTIATE_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.
419 MATCHER_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 
426 TEST(CombineRepeatedTripletsInPlace, HandlesEmptyTriplets) {
427  std::vector<Eigen::Triplet<double, int64_t>> triplets;
429  EXPECT_THAT(triplets, IsEmpty());
430 }
431 
432 TEST(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 
438 TEST(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 
447 TEST(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 
455 TEST(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 
463 TEST(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 
470 TEST(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 
478 TEST(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:41
ColIndex col
Definition: markowitz.cc:186
RowIndex row
Definition: markowitz.cc:185
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:285
void SetEigenMatrixFromTriplets(std::vector< Eigen::Triplet< double, int64_t >> triplets, Eigen::SparseMatrix< double, Eigen::ColMajor, int64_t > &matrix)
absl::Status CanFitInMpModelProto(const QuadraticProgram &qp)
bool HasValidBounds(const QuadraticProgram &qp)
void VerifyTestLp(const QuadraticProgram &qp, bool maximize)
Definition: test_util.cc:49
QuadraticProgram SmallInvalidProblemLp()
Definition: test_util.cc:191
void VerifyTestDiagonalQp1(const QuadraticProgram &qp, bool maximize)
Definition: test_util.cc:270
QuadraticProgram SmallPrimalInfeasibleLp()
Definition: test_util.cc:217
QuadraticProgram SmallInconsistentVariableBoundsLp()
Definition: test_util.cc:204
QuadraticProgram TestDiagonalQp1()
Definition: test_util.cc:143
TEST(LinearAssignmentTest, NullMatrix)