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