// Copyright 2010-2025 Google LLC // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include "ortools/pdlp/quadratic_program.h" #include #include #include #include #include #include #include #include "Eigen/Core" #include "Eigen/SparseCore" #include "absl/log/check.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/str_cat.h" #include "absl/strings/string_view.h" #include "ortools/base/status_macros.h" #include "ortools/linear_solver/linear_solver.pb.h" namespace operations_research::pdlp { using ::Eigen::VectorXd; absl::Status ValidateQuadraticProgramDimensions(const QuadraticProgram& qp) { const int64_t var_lb_size = qp.variable_lower_bounds.size(); const int64_t con_lb_size = qp.constraint_lower_bounds.size(); if (var_lb_size != qp.variable_upper_bounds.size()) { return absl::InvalidArgumentError(absl::StrCat( "Inconsistent dimensions: variable lower bound vector has size ", var_lb_size, " while variable upper bound vector has size ", qp.variable_upper_bounds.size())); } if (var_lb_size != qp.objective_vector.size()) { return absl::InvalidArgumentError(absl::StrCat( "Inconsistent dimensions: variable lower bound vector has size ", var_lb_size, " while objective vector has size ", qp.objective_vector.size())); } if (var_lb_size != qp.constraint_matrix.cols()) { return absl::InvalidArgumentError(absl::StrCat( "Inconsistent dimensions: variable lower bound vector has size ", var_lb_size, " while constraint matrix has ", qp.constraint_matrix.cols(), " columns")); } if (qp.objective_matrix.has_value() && var_lb_size != qp.objective_matrix->rows()) { return absl::InvalidArgumentError(absl::StrCat( "Inconsistent dimensions: variable lower bound vector has size ", var_lb_size, " while objective matrix has ", qp.objective_matrix->rows(), " rows")); } if (con_lb_size != qp.constraint_upper_bounds.size()) { return absl::InvalidArgumentError(absl::StrCat( "Inconsistent dimensions: constraint lower bound vector has size ", con_lb_size, " while constraint upper bound vector has size ", qp.constraint_upper_bounds.size())); } if (con_lb_size != qp.constraint_matrix.rows()) { return absl::InvalidArgumentError(absl::StrCat( "Inconsistent dimensions: constraint lower bound vector has size ", con_lb_size, " while constraint matrix has ", qp.constraint_matrix.rows(), " rows ")); } if (qp.variable_names.has_value() && var_lb_size != qp.variable_names->size()) { return absl::InvalidArgumentError(absl::StrCat( "Inconsistent dimensions: variable lower bound vector has size ", var_lb_size, " while variable names has size ", qp.variable_names->size())); } if (qp.constraint_names.has_value() && con_lb_size != qp.constraint_names->size()) { return absl::InvalidArgumentError(absl::StrCat( "Inconsistent dimensions: constraint lower bound vector has size ", con_lb_size, " while constraint names has size ", qp.constraint_names->size())); } return absl::OkStatus(); } absl::StatusOr QpFromMpModelProto( const MPModelProto& proto, bool relax_integer_variables, bool include_names) { if (!proto.general_constraint().empty()) { return absl::InvalidArgumentError("General constraints are not supported."); } const int primal_size = proto.variable_size(); const int dual_size = proto.constraint_size(); QuadraticProgram qp(primal_size, dual_size); if (include_names) { qp.problem_name = proto.name(); qp.variable_names = std::vector(primal_size); qp.constraint_names = std::vector(dual_size); } for (int i = 0; i < primal_size; ++i) { const auto& var = proto.variable(i); qp.variable_lower_bounds[i] = var.lower_bound(); qp.variable_upper_bounds[i] = var.upper_bound(); qp.objective_vector[i] = var.objective_coefficient(); if (var.is_integer() && !relax_integer_variables) { return absl::InvalidArgumentError( "Integer variable encountered with relax_integer_variables == false"); } if (include_names) { (*qp.variable_names)[i] = var.name(); } } std::vector nonzeros_by_column(primal_size); for (int i = 0; i < dual_size; ++i) { const auto& con = proto.constraint(i); for (int j = 0; j < con.var_index_size(); ++j) { if (con.var_index(j) < 0 || con.var_index(j) >= primal_size) { return absl::InvalidArgumentError(absl::StrCat( "Variable index of ", i, "th constraint's ", j, "th nonzero is ", con.var_index(j), " which is not in the allowed range [0, ", primal_size, ")")); } nonzeros_by_column[con.var_index(j)]++; } qp.constraint_lower_bounds[i] = con.lower_bound(); qp.constraint_upper_bounds[i] = con.upper_bound(); if (include_names) { (*qp.constraint_names)[i] = con.name(); } } // To reduce peak RAM usage we construct the constraint matrix in-place. // According to the documentation of `SparseMatrix::insert()` it's efficient // to construct a matrix with insert()s as long as reserve() is called first // and the non-zeros are inserted in increasing order of inner index. // The non-zeros in each input constraint may not be sorted so this is only // efficient with column major format. static_assert(qp.constraint_matrix.IsRowMajor == 0, "See comment."); qp.constraint_matrix.reserve(nonzeros_by_column); for (int i = 0; i < dual_size; ++i) { const auto& con = proto.constraint(i); CHECK_EQ(con.var_index_size(), con.coefficient_size()) << " in " << i << "th constraint"; if (con.var_index_size() != con.coefficient_size()) { return absl::InvalidArgumentError( absl::StrCat(i, "th constraint has ", con.coefficient_size(), " coefficients, expected ", con.var_index_size())); } for (int j = 0; j < con.var_index_size(); ++j) { qp.constraint_matrix.insert(i, con.var_index(j)) = con.coefficient(j); } } if (qp.constraint_matrix.outerSize() > 0) { qp.constraint_matrix.makeCompressed(); } // We use triplets-based initialization for the objective matrix because the // objective non-zeros may be in arbitrary order in the input. std::vector> triplets; const auto& quadratic = proto.quadratic_objective(); if (quadratic.qvar1_index_size() != quadratic.qvar2_index_size() || quadratic.qvar1_index_size() != quadratic.coefficient_size()) { return absl::InvalidArgumentError(absl::StrCat( "The quadratic objective has ", quadratic.qvar1_index_size(), " qvar1_indices, ", quadratic.qvar2_index_size(), " qvar2_indices, and ", quadratic.coefficient_size(), " coefficients, expected equal numbers.")); } if (quadratic.qvar1_index_size() > 0) { qp.objective_matrix.emplace(); qp.objective_matrix->setZero(primal_size); } for (int i = 0; i < quadratic.qvar1_index_size(); ++i) { const int index1 = quadratic.qvar1_index(i); const int index2 = quadratic.qvar2_index(i); if (index1 < 0 || index2 < 0 || index1 >= primal_size || index2 >= primal_size) { return absl::InvalidArgumentError(absl::StrCat( "The quadratic objective's ", i, "th nonzero has indices ", index1, " and ", index2, ", which are not both in the expected range [0, ", primal_size, ")")); } if (index1 != index2) { return absl::InvalidArgumentError(absl::StrCat( "The quadratic objective's ", i, "th nonzero has off-diagonal element at (", index1, ", ", index2, "). Only diagonal objective matrices are supported.")); } // Note: `QuadraticProgram` has an implicit "1/2" in front of the quadratic // term. qp.objective_matrix->diagonal()[index1] = 2 * quadratic.coefficient(i); } qp.objective_offset = proto.objective_offset(); if (proto.maximize()) { qp.objective_offset *= -1; qp.objective_vector *= -1; if (qp.objective_matrix.has_value()) { qp.objective_matrix->diagonal() *= -1; } qp.objective_scaling_factor = -1; } return qp; } absl::Status CanFitInMpModelProto(const QuadraticProgram& qp) { return internal::TestableCanFitInMpModelProto( qp, std::numeric_limits::max()); } namespace internal { absl::Status TestableCanFitInMpModelProto(const QuadraticProgram& qp, const int64_t largest_ok_size) { const int64_t primal_size = qp.variable_lower_bounds.size(); const int64_t dual_size = qp.constraint_lower_bounds.size(); bool primal_too_big = primal_size > largest_ok_size; if (primal_too_big) { return absl::InvalidArgumentError(absl::StrCat( "Too many variables (", primal_size, ") to index with an int32_t.")); } bool dual_too_big = dual_size > largest_ok_size; if (dual_too_big) { return absl::InvalidArgumentError(absl::StrCat( "Too many constraints (", dual_size, ") to index with an int32_t.")); } return absl::OkStatus(); } } // namespace internal absl::StatusOr QpToMpModelProto(const QuadraticProgram& qp) { RETURN_IF_ERROR(CanFitInMpModelProto(qp)); if (qp.objective_scaling_factor == 0) { return absl::InvalidArgumentError( "objective_scaling_factor cannot be zero."); } const int64_t primal_size = qp.variable_lower_bounds.size(); const int64_t dual_size = qp.constraint_lower_bounds.size(); MPModelProto proto; if (qp.problem_name.has_value() && !qp.problem_name->empty()) { proto.set_name(*qp.problem_name); } proto.set_objective_offset(qp.objective_scaling_factor * qp.objective_offset); if (qp.objective_scaling_factor < 0) { proto.set_maximize(true); } else { proto.set_maximize(false); } proto.mutable_variable()->Reserve(primal_size); for (int64_t i = 0; i < primal_size; ++i) { auto* var = proto.add_variable(); var->set_lower_bound(qp.variable_lower_bounds[i]); var->set_upper_bound(qp.variable_upper_bounds[i]); var->set_objective_coefficient(qp.objective_scaling_factor * qp.objective_vector[i]); if (qp.variable_names.has_value() && i < qp.variable_names->size()) { const std::string& name = (*qp.variable_names)[i]; if (!name.empty()) { var->set_name(name); } } } proto.mutable_constraint()->Reserve(dual_size); for (int64_t i = 0; i < dual_size; ++i) { auto* con = proto.add_constraint(); con->set_lower_bound(qp.constraint_lower_bounds[i]); con->set_upper_bound(qp.constraint_upper_bounds[i]); if (qp.constraint_names.has_value() && i < qp.constraint_names->size()) { const std::string& name = (*qp.constraint_names)[i]; if (!name.empty()) { con->set_name(name); } } } using InnerIterator = ::Eigen::SparseMatrix::InnerIterator; for (int64_t col = 0; col < qp.constraint_matrix.cols(); ++col) { for (InnerIterator iter(qp.constraint_matrix, col); iter; ++iter) { auto* con = proto.mutable_constraint(iter.row()); // To avoid reallocs during the inserts, we could count the nonzeros // and `reserve()` before filling. con->add_var_index(iter.col()); con->add_coefficient(iter.value()); } } // Some OR tools decide the objective is quadratic based on // `has_quadratic_objective()` rather than on // `quadratic_objective_size() == 0`, so don't create the quadratic objective // for linear programs. if (!IsLinearProgram(qp)) { auto* quadratic_objective = proto.mutable_quadratic_objective(); const auto& diagonal = qp.objective_matrix->diagonal(); for (int64_t i = 0; i < diagonal.size(); ++i) { if (diagonal[i] != 0.0) { quadratic_objective->add_qvar1_index(i); quadratic_objective->add_qvar2_index(i); // Undo the implicit (1/2) term in `QuadraticProgram`'s objective. quadratic_objective->add_coefficient(qp.objective_scaling_factor * diagonal[i] / 2.0); } } } return proto; } std::string ToString(const QuadraticProgram& qp, int64_t max_size) { auto object_name = [](int64_t index, const std::optional>& names, absl::string_view prefix) { if (names.has_value()) { CHECK_LT(index, names->size()); return (*names)[index]; } return absl::StrCat(prefix, index); }; auto variable_name = [&qp, &object_name](int64_t var_index) { return object_name(var_index, qp.variable_names, "x"); }; auto constraint_name = [&qp, &object_name](int64_t con_index) { return object_name(con_index, qp.constraint_names, "c"); }; if (auto status = ValidateQuadraticProgramDimensions(qp); !status.ok()) { return absl::StrCat("Quadratic program with inconsistent dimensions: ", status.message()); } std::string result; if (qp.problem_name.has_value()) { absl::StrAppend(&result, *qp.problem_name, ":\n"); } absl::StrAppend( &result, (qp.objective_scaling_factor < 0.0 ? "maximize " : "minimize "), qp.objective_scaling_factor, " * (", qp.objective_offset); for (int64_t i = 0; i < qp.objective_vector.size(); ++i) { if (qp.objective_vector[i] != 0.0) { absl::StrAppend(&result, " + ", qp.objective_vector[i], " ", variable_name(i)); if (result.size() >= max_size) break; } } if (qp.objective_matrix.has_value()) { result.append(" + 1/2 * ("); auto obj_diagonal = qp.objective_matrix->diagonal(); for (int64_t i = 0; i < obj_diagonal.size(); ++i) { if (obj_diagonal[i] != 0.0) { absl::StrAppend(&result, " + ", obj_diagonal[i], " ", variable_name(i), "^2"); if (result.size() >= max_size) break; } } // Closes the objective matrix expression. result.append(")"); } // Closes the objective scaling factor expression. result.append(")\n"); Eigen::SparseMatrix constraint_matrix_transpose = qp.constraint_matrix.transpose(); for (int64_t constraint_idx = 0; constraint_idx < constraint_matrix_transpose.outerSize(); ++constraint_idx) { absl::StrAppend(&result, constraint_name(constraint_idx), ":"); if (qp.constraint_lower_bounds[constraint_idx] != -std::numeric_limits::infinity()) { absl::StrAppend(&result, " ", qp.constraint_lower_bounds[constraint_idx], " <="); } for (decltype(constraint_matrix_transpose)::InnerIterator it( constraint_matrix_transpose, constraint_idx); it; ++it) { absl::StrAppend(&result, " + ", it.value(), " ", variable_name(it.index())); if (result.size() >= max_size) break; } if (qp.constraint_upper_bounds[constraint_idx] != std::numeric_limits::infinity()) { absl::StrAppend(&result, " <= ", qp.constraint_upper_bounds[constraint_idx]); } result.append("\n"); } result.append("Bounds\n"); for (int64_t i = 0; i < qp.variable_lower_bounds.size(); ++i) { if (qp.variable_lower_bounds[i] == -std::numeric_limits::infinity()) { if (qp.variable_upper_bounds[i] == std::numeric_limits::infinity()) { absl::StrAppend(&result, variable_name(i), " free\n"); } else { absl::StrAppend(&result, variable_name(i), " <= ", qp.variable_upper_bounds[i], "\n"); } } else { if (qp.variable_upper_bounds[i] == std::numeric_limits::infinity()) { absl::StrAppend(&result, variable_name(i), " >= ", qp.variable_lower_bounds[i], "\n"); } else { absl::StrAppend(&result, qp.variable_lower_bounds[i], " <= ", variable_name(i), " <= ", qp.variable_upper_bounds[i], "\n"); } } if (result.size() >= max_size) break; } if (result.size() > max_size) { result.resize(max_size - 4); result.append("...\n"); } return result; } void SetEigenMatrixFromTriplets( std::vector> triplets, Eigen::SparseMatrix& matrix) { using Triplet = Eigen::Triplet; std::sort(triplets.begin(), triplets.end(), [](const Triplet& lhs, const Triplet& rhs) { return std::tie(lhs.col(), lhs.row()) < std::tie(rhs.col(), rhs.row()); }); // The triplets are allowed to contain duplicate entries (and intentionally // do for the diagonals of the objective matrix). For efficiency of insert and // reserve, merge the duplicates first. internal::CombineRepeatedTripletsInPlace(triplets); std::vector num_column_entries(matrix.cols()); for (const Triplet& triplet : triplets) { ++num_column_entries[triplet.col()]; } // NOTE: `reserve()` takes column counts because matrix is in column major // order. matrix.reserve(num_column_entries); for (const Triplet& triplet : triplets) { matrix.insert(triplet.row(), triplet.col()) = triplet.value(); } if (matrix.outerSize() > 0) { matrix.makeCompressed(); } } namespace internal { void CombineRepeatedTripletsInPlace( std::vector>& triplets) { if (triplets.empty()) return; auto output_iter = triplets.begin(); for (auto p = output_iter + 1; p != triplets.end(); ++p) { if (output_iter->row() == p->row() && output_iter->col() == p->col()) { *output_iter = {output_iter->row(), output_iter->col(), output_iter->value() + p->value()}; } else { ++output_iter; if (output_iter != p) { // Small optimization - skip no-op copies. *output_iter = *p; } } } // `*output_iter` is the last output value, so erase everything after that. triplets.erase(output_iter + 1, triplets.end()); } } // namespace internal } // namespace operations_research::pdlp