OR-Tools  9.6
quadratic_program.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 <algorithm>
17 #include <cstdint>
18 #include <limits>
19 #include <string>
20 #include <tuple>
21 #include <utility>
22 #include <vector>
23 
24 #include "Eigen/Core"
25 #include "Eigen/SparseCore"
26 #include "absl/log/check.h"
27 #include "absl/status/status.h"
28 #include "absl/status/statusor.h"
29 #include "absl/strings/str_cat.h"
31 #include "ortools/linear_solver/linear_solver.pb.h"
32 
33 namespace operations_research::pdlp {
34 
35 using ::Eigen::VectorXd;
36 
38  const int64_t var_lb_size = qp.variable_lower_bounds.size();
39  const int64_t con_lb_size = qp.constraint_lower_bounds.size();
40 
41  if (var_lb_size != qp.variable_upper_bounds.size()) {
42  return absl::InvalidArgumentError(absl::StrCat(
43  "Inconsistent dimensions: variable lower bound vector has size ",
44  var_lb_size, " while variable upper bound vector has size ",
45  qp.variable_upper_bounds.size()));
46  }
47  if (var_lb_size != qp.objective_vector.size()) {
48  return absl::InvalidArgumentError(absl::StrCat(
49  "Inconsistent dimensions: variable lower bound vector has size ",
50  var_lb_size, " while objective vector has size ",
51  qp.objective_vector.size()));
52  }
53  if (var_lb_size != qp.constraint_matrix.cols()) {
54  return absl::InvalidArgumentError(absl::StrCat(
55  "Inconsistent dimensions: variable lower bound vector has size ",
56  var_lb_size, " while constraint matrix has ",
57  qp.constraint_matrix.cols(), " columns"));
58  }
59  if (qp.objective_matrix.has_value() &&
60  var_lb_size != qp.objective_matrix->rows()) {
61  return absl::InvalidArgumentError(absl::StrCat(
62  "Inconsistent dimensions: variable lower bound vector has size ",
63  var_lb_size, " while objective matrix has ",
64  qp.objective_matrix->rows(), " rows"));
65  }
66  if (con_lb_size != qp.constraint_upper_bounds.size()) {
67  return absl::InvalidArgumentError(absl::StrCat(
68  "Inconsistent dimensions: constraint lower bound vector has size ",
69  con_lb_size, " while constraint upper bound vector has size ",
70  qp.constraint_upper_bounds.size()));
71  }
72  if (con_lb_size != qp.constraint_matrix.rows()) {
73  return absl::InvalidArgumentError(absl::StrCat(
74  "Inconsistent dimensions: constraint lower bound vector has size ",
75  con_lb_size, " while constraint matrix has ",
76  qp.constraint_matrix.rows(), " rows "));
77  }
78 
79  return absl::OkStatus();
80 }
81 
83  const bool constraint_bounds_valid =
84  (qp.constraint_lower_bounds.array() <= qp.constraint_upper_bounds.array())
85  .all();
86  const bool variable_bounds_valid =
87  (qp.variable_lower_bounds.array() <= qp.variable_upper_bounds.array())
88  .all();
89  return constraint_bounds_valid && variable_bounds_valid;
90 }
91 
92 absl::StatusOr<QuadraticProgram> QpFromMpModelProto(
93  const MPModelProto& proto, bool relax_integer_variables,
94  bool include_names) {
95  if (!proto.general_constraint().empty()) {
96  return absl::InvalidArgumentError("General constraints are not supported.");
97  }
98  const int primal_size = proto.variable_size();
99  const int dual_size = proto.constraint_size();
100  QuadraticProgram qp(primal_size, dual_size);
101  if (include_names) {
102  qp.problem_name = proto.name();
103  qp.variable_names = std::vector<std::string>(primal_size);
104  qp.constraint_names = std::vector<std::string>(dual_size);
105  }
106  for (int i = 0; i < primal_size; ++i) {
107  const auto& var = proto.variable(i);
108  qp.variable_lower_bounds[i] = var.lower_bound();
109  qp.variable_upper_bounds[i] = var.upper_bound();
110  qp.objective_vector[i] = var.objective_coefficient();
111  if (var.is_integer() && !relax_integer_variables) {
112  return absl::InvalidArgumentError(
113  "Integer variable encountered with relax_integer_variables == false");
114  }
115  if (include_names) {
116  (*qp.variable_names)[i] = var.name();
117  }
118  }
119  std::vector<int> nonzeros_by_column(primal_size);
120  for (int i = 0; i < dual_size; ++i) {
121  const auto& con = proto.constraint(i);
122  for (int j = 0; j < con.var_index_size(); ++j) {
123  if (con.var_index(j) < 0 || con.var_index(j) >= primal_size) {
124  return absl::InvalidArgumentError(absl::StrCat(
125  "Variable index of ", i, "th constraint's ", j, "th nonzero is ",
126  con.var_index(j), " which is not in the allowed range [0, ",
127  primal_size, ")"));
128  }
129  nonzeros_by_column[con.var_index(j)]++;
130  }
131  qp.constraint_lower_bounds[i] = con.lower_bound();
132  qp.constraint_upper_bounds[i] = con.upper_bound();
133  if (include_names) {
134  (*qp.constraint_names)[i] = con.name();
135  }
136  }
137  // To reduce peak RAM usage we construct the constraint matrix in-place.
138  // According to the documentation of `SparseMatrix::insert()` it's effecient
139  // to construct a matrix with insert()s as long as reserve() is called first
140  // and the non-zeros are inserted in increasing order of inner index.
141  // The non-zeros in each input constraint may not be sorted so this is only
142  // efficient with column major format.
143  static_assert(qp.constraint_matrix.IsRowMajor == 0, "See comment.");
144  qp.constraint_matrix.reserve(nonzeros_by_column);
145  for (int i = 0; i < dual_size; ++i) {
146  const auto& con = proto.constraint(i);
147  CHECK_EQ(con.var_index_size(), con.coefficient_size())
148  << " in " << i << "th constraint";
149  if (con.var_index_size() != con.coefficient_size()) {
150  return absl::InvalidArgumentError(
151  absl::StrCat(i, "th constraint has ", con.coefficient_size(),
152  " coefficients, expected ", con.var_index_size()));
153  }
154 
155  for (int j = 0; j < con.var_index_size(); ++j) {
156  qp.constraint_matrix.insert(i, con.var_index(j)) = con.coefficient(j);
157  }
158  }
159  if (qp.constraint_matrix.outerSize() > 0) {
160  qp.constraint_matrix.makeCompressed();
161  }
162  // We use triplets-based initialization for the objective matrix because the
163  // objective non-zeros may be in arbitrary order in the input.
164  std::vector<Eigen::Triplet<double, int64_t>> triplets;
165  const auto& quadratic = proto.quadratic_objective();
166  if (quadratic.qvar1_index_size() != quadratic.qvar2_index_size() ||
167  quadratic.qvar1_index_size() != quadratic.coefficient_size()) {
168  return absl::InvalidArgumentError(absl::StrCat(
169  "The quadratic objective has ", quadratic.qvar1_index_size(),
170  " qvar1_indices, ", quadratic.qvar2_index_size(),
171  " qvar2_indices, and ", quadratic.coefficient_size(),
172  " coefficients, expected equal numbers."));
173  }
174  if (quadratic.qvar1_index_size() > 0) {
175  qp.objective_matrix.emplace();
176  qp.objective_matrix->setZero(primal_size);
177  }
178 
179  for (int i = 0; i < quadratic.qvar1_index_size(); ++i) {
180  const int index1 = quadratic.qvar1_index(i);
181  const int index2 = quadratic.qvar2_index(i);
182  if (index1 < 0 || index2 < 0 || index1 >= primal_size ||
183  index2 >= primal_size) {
184  return absl::InvalidArgumentError(absl::StrCat(
185  "The quadratic objective's ", i, "th nonzero has indices ", index1,
186  " and ", index2, ", which are not both in the expected range [0, ",
187  primal_size, ")"));
188  }
189  if (index1 != index2) {
190  return absl::InvalidArgumentError(absl::StrCat(
191  "The quadratic objective's ", i,
192  "th nonzero has off-diagonal element at (", index1, ", ", index2,
193  "). Only diagonal objective matrices are supported."));
194  }
195  // Note: `QuadraticProgram` has an implicit "1/2" in front of the quadratic
196  // term.
197  qp.objective_matrix->diagonal()[index1] = 2 * quadratic.coefficient(i);
198  }
199  qp.objective_offset = proto.objective_offset();
200  if (proto.maximize()) {
201  qp.objective_offset *= -1;
202  qp.objective_vector *= -1;
203  if (qp.objective_matrix.has_value()) {
204  qp.objective_matrix->diagonal() *= -1;
205  }
206  qp.objective_scaling_factor = -1;
207  }
208  return std::move(qp);
209 }
210 
211 absl::Status CanFitInMpModelProto(const QuadraticProgram& qp) {
214 }
215 
216 namespace internal {
218  const int64_t largest_ok_size) {
219  const int64_t primal_size = qp.variable_lower_bounds.size();
220  const int64_t dual_size = qp.constraint_lower_bounds.size();
221  bool primal_too_big = primal_size > largest_ok_size;
222  if (primal_too_big) {
223  return absl::InvalidArgumentError(absl::StrCat(
224  "Too many variables (", primal_size, ") to index with an int32_t."));
225  }
226  bool dual_too_big = dual_size > largest_ok_size;
227  if (dual_too_big) {
228  return absl::InvalidArgumentError(absl::StrCat(
229  "Too many constraints (", dual_size, ") to index with an int32_t."));
230  }
231  return absl::OkStatus();
232 }
233 } // namespace internal
234 
235 absl::StatusOr<MPModelProto> QpToMpModelProto(const QuadraticProgram& qp) {
237  if (qp.objective_scaling_factor == 0) {
238  return absl::InvalidArgumentError(
239  "objective_scaling_factor cannot be zero.");
240  }
241  const int64_t primal_size = qp.variable_lower_bounds.size();
242  const int64_t dual_size = qp.constraint_lower_bounds.size();
243  MPModelProto proto;
244  if (qp.problem_name.has_value() && !qp.problem_name->empty()) {
245  proto.set_name(*qp.problem_name);
246  }
247  proto.set_objective_offset(qp.objective_scaling_factor * qp.objective_offset);
248  if (qp.objective_scaling_factor < 0) {
249  proto.set_maximize(true);
250  } else {
251  proto.set_maximize(false);
252  }
253 
254  proto.mutable_variable()->Reserve(primal_size);
255  for (int64_t i = 0; i < primal_size; ++i) {
256  auto* var = proto.add_variable();
257  var->set_lower_bound(qp.variable_lower_bounds[i]);
258  var->set_upper_bound(qp.variable_upper_bounds[i]);
259  var->set_objective_coefficient(qp.objective_scaling_factor *
260  qp.objective_vector[i]);
261  if (qp.variable_names.has_value() && i < qp.variable_names->size()) {
262  const std::string& name = (*qp.variable_names)[i];
263  if (!name.empty()) {
264  var->set_name(name);
265  }
266  }
267  }
268 
269  proto.mutable_constraint()->Reserve(dual_size);
270  for (int64_t i = 0; i < dual_size; ++i) {
271  auto* con = proto.add_constraint();
272  con->set_lower_bound(qp.constraint_lower_bounds[i]);
273  con->set_upper_bound(qp.constraint_upper_bounds[i]);
274  if (qp.constraint_names.has_value() && i < qp.constraint_names->size()) {
275  const std::string& name = (*qp.constraint_names)[i];
276  if (!name.empty()) {
277  con->set_name(name);
278  }
279  }
280  }
281 
282  using InnerIterator =
283  ::Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>::InnerIterator;
284  for (int64_t col = 0; col < qp.constraint_matrix.cols(); ++col) {
285  for (InnerIterator iter(qp.constraint_matrix, col); iter; ++iter) {
286  auto* con = proto.mutable_constraint(iter.row());
287  // To avoid reallocs during the inserts, we could count the nonzeros
288  // and `reserve()` before filling.
289  con->add_var_index(iter.col());
290  con->add_coefficient(iter.value());
291  }
292  }
293 
294  // Some OR tools decide the objective is quadratic based on
295  // `has_quadratic_objective()` rather than on
296  // `quadratic_objective_size() == 0`, so don't create the quadratic objective
297  // for linear programs.
298  if (!IsLinearProgram(qp)) {
299  auto* quadratic_objective = proto.mutable_quadratic_objective();
300  const auto& diagonal = qp.objective_matrix->diagonal();
301  for (int64_t i = 0; i < diagonal.size(); ++i) {
302  if (diagonal[i] != 0.0) {
303  quadratic_objective->add_qvar1_index(i);
304  quadratic_objective->add_qvar2_index(i);
305  // Undo the implicit (1/2) term in `QuadraticProgram`'s objective.
306  quadratic_objective->add_coefficient(qp.objective_scaling_factor *
307  diagonal[i] / 2.0);
308  }
309  }
310  }
311 
312  return proto;
313 }
314 
316  std::vector<Eigen::Triplet<double, int64_t>> triplets,
317  Eigen::SparseMatrix<double, Eigen::ColMajor, int64_t>& matrix) {
318  using Triplet = Eigen::Triplet<double, int64_t>;
319  std::sort(triplets.begin(), triplets.end(),
320  [](const Triplet& lhs, const Triplet& rhs) {
321  return std::tie(lhs.col(), lhs.row()) <
322  std::tie(rhs.col(), rhs.row());
323  });
324 
325  // The triplets are allowed to contain duplicate entries (and intentionally
326  // do for the diagonals of the objective matrix). For efficiency of insert and
327  // reserve, merge the duplicates first.
329 
330  std::vector<int64_t> num_column_entries(matrix.cols());
331  for (const Triplet& triplet : triplets) {
332  ++num_column_entries[triplet.col()];
333  }
334  // NOTE: `reserve()` takes column counts because matrix is in column major
335  // order.
336  matrix.reserve(num_column_entries);
337  for (const Triplet& triplet : triplets) {
338  matrix.insert(triplet.row(), triplet.col()) = triplet.value();
339  }
340  if (matrix.outerSize() > 0) {
341  matrix.makeCompressed();
342  }
343 }
344 
345 namespace internal {
347  std::vector<Eigen::Triplet<double, int64_t>>& triplets) {
348  if (triplets.empty()) return;
349  auto output_iter = triplets.begin();
350  for (auto p = output_iter + 1; p != triplets.end(); ++p) {
351  if (output_iter->row() == p->row() && output_iter->col() == p->col()) {
352  *output_iter = {output_iter->row(), output_iter->col(),
353  output_iter->value() + p->value()};
354  } else {
355  ++output_iter;
356  if (output_iter != p) { // Small optimization - skip no-op copies.
357  *output_iter = *p;
358  }
359  }
360  }
361  // `*output_iter` is the last output value, so erase everything after that.
362  triplets.erase(output_iter + 1, triplets.end());
363 }
364 } // namespace internal
365 } // namespace operations_research::pdlp
int64_t max
Definition: alldiff_cst.cc:140
#define RETURN_IF_ERROR(expr)
CpModelProto proto
const std::string name
IntVar * var
Definition: expr_array.cc:1874
ColIndex col
Definition: markowitz.cc:186
void CombineRepeatedTripletsInPlace(std::vector< Eigen::Triplet< double, int64_t >> &triplets)
absl::Status TestableCanFitInMpModelProto(const QuadraticProgram &qp, const int64_t largest_ok_size)
absl::StatusOr< QuadraticProgram > QpFromMpModelProto(const MPModelProto &proto, bool relax_integer_variables, bool include_names)
absl::Status ValidateQuadraticProgramDimensions(const QuadraticProgram &qp)
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)
absl::StatusOr< MPModelProto > QpToMpModelProto(const QuadraticProgram &qp)
bool IsLinearProgram(const QuadraticProgram &qp)
std::optional< std::vector< std::string > > constraint_names
std::optional< std::vector< std::string > > variable_names
Eigen::SparseMatrix< double, Eigen::ColMajor, int64_t > constraint_matrix
std::optional< Eigen::DiagonalMatrix< double, Eigen::Dynamic > > objective_matrix