#include "drake/solvers/clp_solver.h" #include #include #include #include #include "ClpSimplex.hpp" #include "drake/common/text_logging.h" #include "drake/solvers/aggregate_costs_constraints.h" namespace drake { namespace solvers { constexpr double kInf = std::numeric_limits::infinity(); namespace { void ConstructClpModel( const std::vector>& constraint_coeffs, const std::vector& constraint_lower, const std::vector& constraint_upper, const Eigen::VectorXd& xlow, const Eigen::VectorXd& xupp, const Eigen::SparseMatrix& quadratic_matrix, const Eigen::VectorXd& objective_coeff, double constant_cost, ClpModel* model) { Eigen::SparseMatrix constraint_mat(constraint_lower.size(), xlow.rows()); constraint_mat.setFromTriplets(constraint_coeffs.begin(), constraint_coeffs.end()); // Now convert the sparse matrix to Compressed Column Storage format. model->loadProblem(constraint_mat.cols(), constraint_mat.rows(), constraint_mat.outerIndexPtr(), constraint_mat.innerIndexPtr(), constraint_mat.valuePtr(), xlow.data(), xupp.data(), objective_coeff.data(), constraint_lower.data(), constraint_upper.data(), nullptr /* rowObjective=nullptr */); if (quadratic_matrix.nonZeros() > 0) { static const logging::Warn log_once( "Currently CLP solver has a memory issue when solving a QP. The user " "should be aware of this risk."); model->loadQuadraticObjective( quadratic_matrix.cols(), quadratic_matrix.outerIndexPtr(), quadratic_matrix.innerIndexPtr(), quadratic_matrix.valuePtr()); } model->setObjectiveOffset(-constant_cost); } template void AddLinearConstraint( const MathematicalProgram& prog, const Binding& linear_constraint, std::vector>* constraint_coeffs, std::vector* constraint_lower, std::vector* constraint_upper, int* constraint_count, std::unordered_map, int>* constraint_dual_start_index) { const Binding binding_cast = internal::BindingDynamicCast(linear_constraint); constraint_dual_start_index->emplace(binding_cast, *constraint_count); const Eigen::SparseMatrix& A = linear_constraint.evaluator()->get_sparse_A(); // First loop by column to set the coefficient since A is a column major // matrix. for (int j = 0; j < A.cols(); ++j) { const int var_index = prog.FindDecisionVariableIndex(linear_constraint.variables()(j)); for (Eigen::SparseMatrix::InnerIterator it(A, j); it; ++it) { const int row = it.row(); constraint_coeffs->emplace_back(row + *constraint_count, var_index, it.value()); } } // Now loop by rows of the constraints to set the bounds. for (int i = 0; i < linear_constraint.evaluator()->num_constraints(); ++i) { (*constraint_lower)[*constraint_count + i] = linear_constraint.evaluator()->lower_bound()(i); (*constraint_upper)[*constraint_count + i] = linear_constraint.evaluator()->upper_bound()(i); } *constraint_count += linear_constraint.evaluator()->num_constraints(); } void AddLinearConstraints( const MathematicalProgram& prog, std::vector>* constraint_coeffs, std::vector* constraint_lower, std::vector* constraint_upper, std::unordered_map, int>* constraint_dual_start_index) { int num_constraints = 0; int num_nonzero_coeff_max = 0; for (const auto& linear_constraint : prog.linear_constraints()) { num_constraints += linear_constraint.evaluator()->num_constraints(); num_nonzero_coeff_max += linear_constraint.evaluator()->num_constraints() * linear_constraint.variables().rows(); } for (const auto& linear_eq_constraint : prog.linear_equality_constraints()) { num_constraints += linear_eq_constraint.evaluator()->num_constraints(); num_nonzero_coeff_max += linear_eq_constraint.evaluator()->num_constraints() * linear_eq_constraint.variables().rows(); } constraint_lower->resize(num_constraints); constraint_upper->resize(num_constraints); constraint_coeffs->reserve(num_nonzero_coeff_max); int constraint_count = 0; for (const auto& linear_constraint : prog.linear_constraints()) { AddLinearConstraint( prog, linear_constraint, constraint_coeffs, constraint_lower, constraint_upper, &constraint_count, constraint_dual_start_index); } for (const auto& linear_equal_constraint : prog.linear_equality_constraints()) { AddLinearConstraint( prog, linear_equal_constraint, constraint_coeffs, constraint_lower, constraint_upper, &constraint_count, constraint_dual_start_index); } } // @param lambda The dual solution for the whole problem. This can be obtained // through CLP by calling getRowPrice() or getReducedCost() function. void SetConstraintDualSolution( const Eigen::Map& lambda, const std::unordered_map, int>& constraint_dual_start_index, MathematicalProgramResult* result) { for (const auto& [constraint, dual_start_index] : constraint_dual_start_index) { result->set_dual_solution( constraint, lambda.segment(dual_start_index, constraint.evaluator()->num_constraints())); } } void SetBoundingBoxConstraintDualSolution( const Eigen::Map& column_dual_sol, const std::unordered_map, std::pair, std::vector>>& bb_con_dual_variable_indices, MathematicalProgramResult* result) { for (const auto& [bb_con, dual_var_indices] : bb_con_dual_variable_indices) { Eigen::VectorXd bb_con_dual_sol = Eigen::VectorXd::Zero(bb_con.evaluator()->num_constraints()); const std::vector& lower_dual_indices = dual_var_indices.first; const std::vector& upper_dual_indices = dual_var_indices.second; for (int k = 0; k < bb_con.evaluator()->num_constraints(); ++k) { // At most one of the bound is active (If both the lower and upper bounds // are active, then these two bounds have to be equal, then the dual // solution for both bounds are the same). if (lower_dual_indices[k] != -1 && column_dual_sol(lower_dual_indices[k]) > 0) { // If the dual solution is positive, then the lower bound is active // (according to the definition of reduced cost). bb_con_dual_sol[k] = column_dual_sol[lower_dual_indices[k]]; } else if (upper_dual_indices[k] != -1 && column_dual_sol(upper_dual_indices[k]) < 0) { // If the dual solution is negative, then the upper bound is active // (according to the definition of reduced cost). bb_con_dual_sol[k] = column_dual_sol[upper_dual_indices[k]]; } } result->set_dual_solution(bb_con, bb_con_dual_sol); } } void SetSolution( const MathematicalProgram& prog, const ClpModel& model, const std::unordered_map, int>& constraint_dual_start_index, const std::unordered_map, std::pair, std::vector>>& bb_con_dual_variable_indices, MathematicalProgramResult* result) { ClpSolverDetails& solver_details = result->SetSolverDetailsType(); result->set_x_val(Eigen::Map(model.getColSolution(), prog.num_vars())); Eigen::Map lambda(model.dualRowSolution(), model.getNumRows()); SetConstraintDualSolution(lambda, constraint_dual_start_index, result); Eigen::Map column_dual_sol(model.dualColumnSolution(), model.getNumCols()); SetBoundingBoxConstraintDualSolution(column_dual_sol, bb_con_dual_variable_indices, result); solver_details.status = model.status(); SolutionResult solution_result{SolutionResult::kUnknownError}; switch (solver_details.status) { case -1: { solution_result = SolutionResult::kUnknownError; break; } case 0: { solution_result = SolutionResult::kSolutionFound; break; } case 1: { solution_result = SolutionResult::kInfeasibleConstraints; break; } case 2: { solution_result = SolutionResult::kUnbounded; break; } case 3: { solution_result = SolutionResult::kIterationLimit; break; } default: { // Merging multiple CLP status code into one Drake SolutionResult code. solution_result = SolutionResult::kUnknownError; } } double objective_val{-kInf}; if (solution_result == SolutionResult::kInfeasibleConstraints) { objective_val = kInf; } else if (solution_result == SolutionResult::kUnbounded) { objective_val = -kInf; } else { objective_val = model.getObjValue(); } result->set_solution_result(solution_result); result->set_optimal_cost(objective_val); } // bb_con_dual_indices[bb_con] returns the indices of the dual variable for the // lower/upper bound of this bounding box constraint. If this bounding box // constraint cannot be active (some other bounding box constraint has a tighter // bound than this one), then its dual variable index is -1. // @param xlow The aggregated lower bound for all decision variables across all // bounding box constraints. // @param xupp The aggregated upper bound for all decision variables across all // bounding box constraints. void SetBoundingBoxConstraintDualIndices( const MathematicalProgram& prog, const Binding& bb_con, const Eigen::VectorXd& xlow, const Eigen::VectorXd& xupp, std::unordered_map, std::pair, std::vector>>* bbcon_dual_indices) { const int num_vars = bb_con.evaluator()->num_constraints(); std::vector lower_dual_indices(num_vars, -1); std::vector upper_dual_indices(num_vars, -1); for (int k = 0; k < num_vars; ++k) { const int idx = prog.FindDecisionVariableIndex(bb_con.variables()(k)); if (xlow[idx] == bb_con.evaluator()->lower_bound()(k)) { lower_dual_indices[k] = idx; } if (xupp[idx] == bb_con.evaluator()->upper_bound()(k)) { upper_dual_indices[k] = idx; } } bbcon_dual_indices->emplace( bb_con, std::make_pair(lower_dual_indices, upper_dual_indices)); } void ParseModelExceptLinearConstraints( const MathematicalProgram& prog, Eigen::VectorXd* xlow, Eigen::VectorXd* xupp, Eigen::SparseMatrix* quadratic_matrix, Eigen::VectorXd* objective_coeff, double* constant_cost) { // Construct model using loadProblem function. DRAKE_ASSERT(xlow->rows() == prog.num_vars()); DRAKE_ASSERT(xupp->rows() == prog.num_vars()); AggregateBoundingBoxConstraints(prog, xlow, xupp); Eigen::SparseVector linear_coeff; VectorX quadratic_vars, linear_vars; Eigen::SparseMatrix Q_lower; AggregateQuadraticAndLinearCosts(prog.quadratic_costs(), prog.linear_costs(), &Q_lower, &quadratic_vars, &linear_coeff, &linear_vars, constant_cost); // We need to convert 0.5 * quadratic_var.dot(Q_lower * quadratic_var) to // 0.5 * x.dot(quadratic_matrix * x), where x is the vector of all the // decision variables. To do so, we first map each entry of quadratic_var to // its corresponding entry in x. std::unordered_map quadratic_var_to_index; for (int i = 0; i < quadratic_vars.rows(); ++i) { quadratic_var_to_index.emplace( quadratic_vars(i).get_id(), prog.FindDecisionVariableIndex(quadratic_vars(i))); } std::vector> quadratic_matrix_triplets; quadratic_matrix_triplets.reserve(Q_lower.nonZeros()); for (int col = 0; col < Q_lower.outerSize(); ++col) { const int x_col_index = quadratic_var_to_index.at(quadratic_vars(col).get_id()); for (Eigen::SparseMatrix::InnerIterator it(Q_lower, col); it; ++it) { const int x_row_index = quadratic_var_to_index.at(quadratic_vars(it.row()).get_id()); quadratic_matrix_triplets.emplace_back(x_row_index, x_col_index, it.value()); } } quadratic_matrix->setFromTriplets(quadratic_matrix_triplets.begin(), quadratic_matrix_triplets.end()); objective_coeff->resize(prog.num_vars()); for (Eigen::SparseVector::InnerIterator it(linear_coeff); it; ++it) { (*objective_coeff)(prog.FindDecisionVariableIndex(linear_vars(it.row()))) = it.value(); } } int ChooseLogLevel(const SolverOptions& options) { if (options.get_print_to_console()) { // Documented as "factorizations plus a bit more" in ClpModel.hpp. return 3; } return 0; } int ChooseScaling(const SolverOptions& options) { const auto& clp_options = options.GetOptionsInt(ClpSolver::id()); auto it = clp_options.find("scaling"); if (it == clp_options.end()) { // Default scaling is 1. return 1; } else { return it->second; } } } // namespace bool ClpSolver::is_available() { return true; } void ClpSolver::DoSolve(const MathematicalProgram& prog, const Eigen::VectorXd& initial_guess, const SolverOptions& merged_options, MathematicalProgramResult* result) const { // TODO(hongkai.dai): use initial guess and more of the merged options. unused(initial_guess); ClpSimplex model; model.setLogLevel(ChooseLogLevel(merged_options)); Eigen::VectorXd xlow(prog.num_vars()); Eigen::VectorXd xupp(prog.num_vars()); Eigen::VectorXd objective_coeff = Eigen::VectorXd::Zero(prog.num_vars()); Eigen::SparseMatrix quadratic_matrix(prog.num_vars(), prog.num_vars()); double constant_cost{0.}; ParseModelExceptLinearConstraints(prog, &xlow, &xupp, &quadratic_matrix, &objective_coeff, &constant_cost); std::vector> constraint_coeffs; std::vector constraint_lower, constraint_upper; std::unordered_map, int> constraint_dual_start_index; if (!prog.linear_constraints().empty() || !prog.linear_equality_constraints().empty()) { AddLinearConstraints(prog, &constraint_coeffs, &constraint_lower, &constraint_upper, &constraint_dual_start_index); } ConstructClpModel(constraint_coeffs, constraint_lower, constraint_upper, xlow, xupp, quadratic_matrix, objective_coeff, constant_cost, &model); std::unordered_map, std::pair, std::vector>> bb_con_dual_variable_indices; for (const auto& bb_con : prog.bounding_box_constraints()) { SetBoundingBoxConstraintDualIndices(prog, bb_con, xlow, xupp, &bb_con_dual_variable_indices); } // As suggested by the CLP author, we should call scaling() to handle tiny (or // huge) number in program data. See https://github.com/coin-or/Clp/issues/217 // for the discussion. model.scaling(ChooseScaling(merged_options)); // Solve model.primal(); // Set the solution SetSolution(prog, model, constraint_dual_start_index, bb_con_dual_variable_indices, result); } } // namespace solvers } // namespace drake