#include "drake/solvers/create_constraint.h" #include #include #include #include #include "drake/common/symbolic/decompose.h" #include "drake/math/quadratic_form.h" #include "drake/solvers/decision_variable.h" namespace drake { namespace solvers { namespace internal { using std::find; using std::isfinite; using std::make_shared; using std::numeric_limits; using std::ostringstream; using std::runtime_error; using std::set; using std::shared_ptr; using std::unordered_map; using std::vector; using symbolic::Expression; using symbolic::Formula; using symbolic::Polynomial; using symbolic::Variable; using symbolic::Variables; Binding ParseConstraint( const Eigen::Ref>& v, const Eigen::Ref& lb, const Eigen::Ref& ub) { DRAKE_ASSERT(v.rows() == lb.rows() && v.rows() == ub.rows()); if (!IsAffine(v)) { auto constraint = make_shared(v, lb, ub); return CreateBinding(constraint, constraint->vars()); } // else, continue on to linear-specific version below. if ((ub - lb).isZero()) { return ParseLinearEqualityConstraint(v, lb); } // Setup map_var_to_index and vars. // such that map_var_to_index[vars(i)] = i VectorXDecisionVariable vars; unordered_map map_var_to_index; std::tie(vars, map_var_to_index) = symbolic::ExtractVariablesFromExpression(v); // Construct A, new_lb, new_ub. map_var_to_index is used here. Eigen::MatrixXd A{Eigen::MatrixXd::Zero(v.size(), vars.size())}; Eigen::VectorXd new_lb{v.size()}; Eigen::VectorXd new_ub{v.size()}; // We will determine if lb <= v <= ub is a bounding box constraint, namely // x_lb <= x <= x_ub. bool is_v_bounding_box = true; Eigen::RowVectorXd Ai(A.cols()); for (int i = 0; i < v.size(); ++i) { double constant_term = 0; int num_vi_variables = symbolic::DecomposeAffineExpression( v(i), map_var_to_index, &Ai, &constant_term); A.row(i) = Ai; if (num_vi_variables == 0 && !(lb(i) <= constant_term && constant_term <= ub(i))) { // Unsatisfiable constraint with no variables, such as 1 <= 0 <= 2 throw std::runtime_error( fmt::format("Constraint {} <= {} <= {} is unsatisfiable but called " "with ParseConstraint.", lb(i), v(i).to_string(), ub(i))); } else { new_lb(i) = lb(i) - constant_term; new_ub(i) = ub(i) - constant_term; DRAKE_DEMAND(!std::isnan(new_lb(i))); DRAKE_DEMAND(!std::isnan(new_ub(i))); if (num_vi_variables != 1) { is_v_bounding_box = false; } } } if (is_v_bounding_box) { // If every lb(i) <= v(i) <= ub(i) is a bounding box constraint, then // formulate a bounding box constraint x_lb <= x <= x_ub VectorXDecisionVariable bounding_box_x(v.size()); for (int i = 0; i < v.size(); ++i) { // v(i) is in the form of c * x double x_coeff = 0; for (const auto& x : v(i).GetVariables()) { const double coeff = A(i, map_var_to_index[x.get_id()]); if (coeff != 0) { x_coeff += coeff; bounding_box_x(i) = x; } } if (x_coeff > 0) { new_lb(i) /= x_coeff; new_ub(i) /= x_coeff; } else { const double lb_i = new_lb(i); new_lb(i) = new_ub(i) / x_coeff; new_ub(i) = lb_i / x_coeff; } DRAKE_DEMAND(!std::isnan(new_lb(i))); DRAKE_DEMAND(!std::isnan(new_ub(i))); } return CreateBinding(make_shared(new_lb, new_ub), bounding_box_x); } return CreateBinding(make_shared(A, new_lb, new_ub), vars); } std::unique_ptr> MaybeParseLinearConstraint( const symbolic::Expression& e, double lb, double ub) { if (!e.is_polynomial()) { return std::unique_ptr>{nullptr}; } const Polynomial p{e}; if (p.TotalDegree() > 1) { return std::unique_ptr>{nullptr}; } // If p only has one indeterminates, then we can always return a bounding box // constraint. if (p.indeterminates().size() == 1) { // We decompose the polynomial `p` into `constant_term + coeff * var`. double coeff = 0; double constant_term = 0; for (const auto& term : p.monomial_to_coefficient_map()) { if (term.first.total_degree() == 0) { constant_term += get_constant_value(term.second); } else { coeff += get_constant_value(term.second); } } // coeff should not be 0. The symbolic polynomial should be able to detect // when the coefficient is 0, and remove it from // monomial_to_coefficient_map. DRAKE_DEMAND(coeff != 0); double var_lower{}, var_upper{}; if (coeff > 0) { var_lower = (lb - constant_term) / coeff; var_upper = (ub - constant_term) / coeff; } else { var_lower = (ub - constant_term) / coeff; var_upper = (lb - constant_term) / coeff; } return std::make_unique>( std::make_shared(Vector1d(var_lower), Vector1d(var_upper)), Vector1(*(p.indeterminates().begin()))); } VectorX bound_variables(p.indeterminates().size()); std::unordered_map map_var_to_index; int index = 0; for (const auto& var : p.indeterminates()) { bound_variables(index) = var; map_var_to_index.emplace(var.get_id(), index++); } Eigen::RowVectorXd a(p.indeterminates().size()); a.setZero(); double lower = lb; double upper = ub; for (const auto& term : p.monomial_to_coefficient_map()) { if (term.first.total_degree() == 0) { const double coeff = get_constant_value(term.second); lower -= coeff; upper -= coeff; } else { const int var_index = map_var_to_index.at(term.first.GetVariables().begin()->get_id()); a(var_index) = get_constant_value(term.second); } } if (lower == upper) { return std::make_unique>( std::make_shared(a, Vector1d(lower)), bound_variables); } else { return std::make_unique>( std::make_shared(a, Vector1d(lower), Vector1d(upper)), bound_variables); } } namespace { // Given two symbolic expressions, e1 and e2, finds an equi-satisfiable // constraint `e <= c` for `e1 <= e2`. First, it decomposes e1 and e2 into `e1 = // c1 + e1'` and `e2 = c2 + e2'`. Then it does the following case analysis. // // Case 1: If c1 or c2 are finite, we use the following derivations: // // e1 <= e2 // -> c1 + e1' <= c2 + e2' // -> e1' - e2' <= c2 - c1. // // and set e := e1' - e2' and c := c2 - c1. // // Case 2: If both c1 and c2 are infinite. We use the following table // // c1 c2 // -------------------------- // +∞ <= +∞ Trivially holds. // +∞ <= -∞ Infeasible. // -∞ <= +∞ Trivially holds. // -∞ <= -∞ Trivially holds. // // and throw an exception for all the cases. // // Note that c1 (resp. c2) can be infinite only if e1 (resp. e2) is zero. // Otherwise, it throws an exception. To understand this side-condition, // consider the following example: // // e1 = 0 // e2 = x + ∞ // // e1 <= e2 := 0 <= x + ∞ -- (1) // // Without the side-condition, we might derive the following (wrong) // equi-satisfiable constraint: // // -x <= ∞ -- (2) // // This is problematic because x ↦ -∞ is a satisfying constraint of // (2) but it's not for (1) since we have: // // 0 <= -∞ + ∞ // 0 <= nan // False. // void FindBound(const Expression& e1, const Expression& e2, Expression* const e, double* const c) { DRAKE_ASSERT(e != nullptr); DRAKE_ASSERT(c != nullptr); double c1 = 0; double c2 = 0; const Expression e1_expanded{e1.Expand()}; if (is_constant(e1_expanded)) { c1 = get_constant_value(e1_expanded); } else if (is_addition(e1_expanded)) { c1 = get_constant_in_addition(e1_expanded); if (!isfinite(c1)) { ostringstream oss; oss << "FindBound() cannot handle the constraint: " << e1 << " <= " << e2 << " because " << e1 << " has infinity in the constant term after expansion."; throw runtime_error{oss.str()}; } *e = Expression::Zero(); for (const auto& p : get_expr_to_coeff_map_in_addition(e1_expanded)) { *e += p.first * p.second; } } else { *e = e1_expanded; } const Expression e2_expanded{e2.Expand()}; if (is_constant(e2_expanded)) { c2 = get_constant_value(e2_expanded); } else if (is_addition(e2_expanded)) { c2 = get_constant_in_addition(e2_expanded); if (!isfinite(c2)) { ostringstream oss; oss << "FindBound() cannot handle the constraint: " << e1 << " <= " << e2 << " because " << e2 << " has infinity in the constant term after expansion."; throw runtime_error{oss.str()}; } for (const auto& p : get_expr_to_coeff_map_in_addition(e2_expanded)) { *e -= p.first * p.second; } } else { *e -= e2_expanded; } if (isfinite(c1) || isfinite(c2)) { *c = c2 - c1; return; } // Handle special cases where both of `c1` and `c2` are infinite. // c1 c2 // -------------------------- // +∞ <= +∞ Trivially holds. // +∞ <= -∞ Infeasible. // -∞ <= +∞ Trivially holds. // -∞ <= -∞ Trivially holds. ostringstream oss; if (c1 == numeric_limits::infinity() && c2 == -numeric_limits::infinity()) { oss << "FindBound() detects an infeasible constraint: " << e1 << " <= " << e2 << "."; throw runtime_error{oss.str()}; } else { oss << "FindBound() detects a trivial constraint: " << e1 << " <= " << e2 << "."; throw runtime_error{oss.str()}; } } } // namespace Binding ParseConstraint( const Eigen::Ref>& formulas) { const int n = formulas.rows() * formulas.cols(); // Decomposes 2D-array of formulas into 1D-vector of expression, `v`, and two // 1D-vector of double `lb` and `ub`. VectorX v{n}; Eigen::VectorXd lb{n}; Eigen::VectorXd ub{n}; int k{0}; // index variable for 1D components. for (int j{0}; j < formulas.cols(); ++j) { for (int i{0}; i < formulas.rows(); ++i) { const symbolic::Formula& f{formulas(i, j)}; if (symbolic::is_false(f)) { throw std::runtime_error( fmt::format("ParseConstraint is called with formulas({}, {}) being " "always false", i, j)); } else if (symbolic::is_true(f)) { continue; } else if (is_equal_to(f)) { // f(i) := (lhs == rhs) // (lhs - rhs == 0) v(k) = get_lhs_expression(f) - get_rhs_expression(f); lb(k) = 0.0; ub(k) = 0.0; } else if (is_less_than_or_equal_to(f)) { // f(i) := (lhs <= rhs) // (-∞ <= lhs - rhs <= 0) v(k) = get_lhs_expression(f) - get_rhs_expression(f); lb(k) = -std::numeric_limits::infinity(); ub(k) = 0.0; } else if (is_greater_than_or_equal_to(f)) { // f(i) := (lhs >= rhs) // (∞ >= lhs - rhs >= 0) v(k) = get_lhs_expression(f) - get_rhs_expression(f); lb(k) = 0.0; ub(k) = std::numeric_limits::infinity(); } else { std::ostringstream oss; oss << "ParseConstraint is called with an " "array of formulas which includes a formula " << f << " which is not a relational formula using one of {==, <=, >=} " "operators."; throw std::runtime_error(oss.str()); } ++k; } } if (k == 0) { // All formulas are always True, return an empty bounding box constraint. return internal::CreateBinding(std::make_shared( Eigen::VectorXd(0), Eigen::VectorXd(0)), VectorXDecisionVariable(0)); } return ParseConstraint(v.head(k), lb.head(k), ub.head(k)); } Binding ParseConstraint(const Formula& f) { if (symbolic::is_false(f)) { throw std::runtime_error( "ParseConstraint is called with a formula being always false."); } else if (symbolic::is_true(f)) { return internal::CreateBinding(std::make_shared( Eigen::VectorXd(0), Eigen::VectorXd(0)), VectorXDecisionVariable(0)); } else if (is_equal_to(f)) { // e1 == e2 const Expression& e1{get_lhs_expression(f)}; const Expression& e2{get_rhs_expression(f)}; return ParseConstraint(e1 - e2, 0.0, 0.0); } else if (is_greater_than_or_equal_to(f)) { // e1 >= e2 const Expression& e1{get_lhs_expression(f)}; const Expression& e2{get_rhs_expression(f)}; Expression e; double ub = 0.0; FindBound(e2, e1, &e, &ub); return ParseConstraint(e, -numeric_limits::infinity(), ub); } else if (is_less_than_or_equal_to(f)) { // e1 <= e2 const Expression& e1{get_lhs_expression(f)}; const Expression& e2{get_rhs_expression(f)}; Expression e; double ub = 0.0; FindBound(e1, e2, &e, &ub); return ParseConstraint(e, -numeric_limits::infinity(), ub); } else if (is_conjunction(f)) { const std::set& operands = get_operands(f); // TODO(jwnimmer-tri) We should use an absl::InlinedVector here. const std::vector vec_operands(operands.begin(), operands.end()); const Eigen::Map> map_operands(vec_operands.data(), vec_operands.size()); return ParseConstraint(map_operands); } ostringstream oss; oss << "ParseConstraint is called with a formula " << f << " which is neither a relational formula using one of {==, <=, >=} " "operators nor a conjunction of those relational formulas."; throw runtime_error(oss.str()); } Binding ParseLinearEqualityConstraint( const set& formulas) { const auto n = formulas.size(); // Decomposes a set of formulas, `{e₁₁ == e₁₂, ..., eₙ₁ == eₙ₂}` // into a 1D-vector of expressions, `v = [e₁₁ - e₁₂, ..., eₙ₁ - eₙ₂]`. VectorX v{n}; int i{0}; // index variable used in the loop for (const symbolic::Formula& f : formulas) { if (symbolic::is_false(f)) { throw std::runtime_error( "ParseLinearEqualityConstraint is called with one of formulas being " "always false."); } else if (symbolic::is_true(f)) { continue; } else if (is_equal_to(f)) { // f := (lhs == rhs) // (lhs - rhs == 0) v(i) = get_lhs_expression(f) - get_rhs_expression(f); } else { ostringstream oss; oss << "ParseLinearEqualityConstraint(const " << "set& formulas) is called while its argument 'formulas' " << "includes a non-equality formula " << f << "."; throw runtime_error(oss.str()); } ++i; } if (i == 0) { // All formulas are always true, return an empty linear equality constraint. return internal::CreateBinding( std::make_shared( Eigen::Matrix(), Eigen::Matrix()), Eigen::Matrix()); } return ParseLinearEqualityConstraint(v.head(i), Eigen::VectorXd::Zero(i)); } Binding ParseLinearEqualityConstraint( const Formula& f) { if (symbolic::is_false(f)) { throw std::runtime_error( "ParseLinearEqualityConstraint is called with a formula being always " "false."); } if (symbolic::is_true(f)) { // The formula is always true, return an empty linear equality constraint. return internal::CreateBinding( std::make_shared( Eigen::Matrix(), Eigen::Matrix()), Eigen::Matrix()); } else if (is_equal_to(f)) { // e1 == e2 const Expression& e1{get_lhs_expression(f)}; const Expression& e2{get_rhs_expression(f)}; return ParseLinearEqualityConstraint(e1 - e2, 0.0); } if (is_conjunction(f)) { return ParseLinearEqualityConstraint(get_operands(f)); } ostringstream oss; oss << "ParseLinearConstraint is called with a formula " << f << " which is neither an equality formula nor a conjunction of equality " "formulas."; throw runtime_error(oss.str()); } Binding DoParseLinearEqualityConstraint( const Eigen::Ref>& v, const Eigen::Ref& b) { DRAKE_DEMAND(v.rows() == b.rows()); VectorX vars; unordered_map map_var_to_index; std::tie(vars, map_var_to_index) = symbolic::ExtractVariablesFromExpression(v); // TODO(hongkai.dai): use sparse matrix. Eigen::MatrixXd A = Eigen::MatrixXd::Zero(v.rows(), vars.rows()); Eigen::VectorXd beq = Eigen::VectorXd::Zero(v.rows()); Eigen::RowVectorXd Ai(A.cols()); for (int i = 0; i < v.rows(); ++i) { double constant_term(0); symbolic::DecomposeAffineExpression(v(i), map_var_to_index, &Ai, &constant_term); A.row(i) = Ai; beq(i) = b(i) - constant_term; } return CreateBinding(make_shared(A, beq), vars); } Binding ParseQuadraticConstraint( const symbolic::Expression& e, double lower_bound, double upper_bound, std::optional hessian_type) { // First build an Eigen vector that contains all the bound variables. auto p = symbolic::ExtractVariablesFromExpression(e); const auto& vars_vec = p.first; const auto& map_var_to_index = p.second; // Now decompose the expression into coefficients and monomials. const symbolic::Polynomial poly{e}; Eigen::MatrixXd Q(vars_vec.size(), vars_vec.size()); Eigen::VectorXd b(vars_vec.size()); double constant_term; // Decompose the polynomial as 0.5xᵀQx + bᵀx + k. symbolic::DecomposeQuadraticPolynomial(poly, map_var_to_index, &Q, &b, &constant_term); // The constraint to be imposed is // lb - k ≤ 0.5 xᵀQx + bᵀx ≤ ub - k return CreateBinding(make_shared( Q, b, lower_bound - constant_term, upper_bound - constant_term, hessian_type), vars_vec); } shared_ptr MakePolynomialConstraint( const VectorXPoly& polynomials, const vector& poly_vars, const Eigen::VectorXd& lb, const Eigen::VectorXd& ub) { // Polynomials that are actually affine (a sum of linear terms + a // constant) can be special-cased. Other polynomials are treated as // generic for now. // TODO(ggould-tri) There may be other such special easy cases. bool all_affine = true; for (int i = 0; i < polynomials.rows(); i++) { if (!polynomials[i].IsAffine()) { all_affine = false; break; } } if (all_affine) { Eigen::MatrixXd linear_constraint_matrix = Eigen::MatrixXd::Zero(polynomials.rows(), poly_vars.size()); Eigen::VectorXd linear_constraint_lb = lb; Eigen::VectorXd linear_constraint_ub = ub; for (int poly_num = 0; poly_num < polynomials.rows(); poly_num++) { for (const auto& monomial : polynomials[poly_num].GetMonomials()) { if (monomial.terms.size() == 0) { linear_constraint_lb[poly_num] -= monomial.coefficient; linear_constraint_ub[poly_num] -= monomial.coefficient; } else { DRAKE_DEMAND(monomial.terms.size() == 1); // Because isAffine(). const Polynomiald::VarType term_var = monomial.terms[0].var; int var_num = (find(poly_vars.begin(), poly_vars.end(), term_var) - poly_vars.begin()); DRAKE_ASSERT(var_num < static_cast(poly_vars.size())); linear_constraint_matrix(poly_num, var_num) = monomial.coefficient; } } } if (ub == lb) { return make_shared(linear_constraint_matrix, linear_constraint_ub); } else { return make_shared( linear_constraint_matrix, linear_constraint_lb, linear_constraint_ub); } } else { return make_shared(polynomials, poly_vars, lb, ub); } } Binding ParseLorentzConeConstraint( const Eigen::Ref>& v, LorentzConeConstraint::EvalType eval_type) { DRAKE_DEMAND(v.rows() >= 2); Eigen::MatrixXd A{}; Eigen::VectorXd b(v.size()); VectorXDecisionVariable vars{}; symbolic::DecomposeAffineExpressions(v, &A, &b, &vars); DRAKE_DEMAND(vars.rows() >= 1); return CreateBinding(make_shared(A, b, eval_type), vars); } Binding ParseLorentzConeConstraint( const Expression& linear_expr, const Expression& quadratic_expr, double tol, LorentzConeConstraint::EvalType eval_type) { const auto& quadratic_p = symbolic::ExtractVariablesFromExpression(quadratic_expr); const auto& quadratic_vars = quadratic_p.first; const auto& quadratic_var_to_index_map = quadratic_p.second; const symbolic::Polynomial poly{quadratic_expr}; Eigen::MatrixXd Q(quadratic_vars.size(), quadratic_vars.size()); Eigen::VectorXd b(quadratic_vars.size()); double a; symbolic::DecomposeQuadraticPolynomial(poly, quadratic_var_to_index_map, &Q, &b, &a); // The constraint that the linear expression v1 satisfying // v1 >= sqrt(0.5 * x' * Q * x + b' * x + a), is equivalent to the vector // [z; y] being within a Lorentz cone, where // z = v1 // y = C * x + d // such that yᵀy = 0.5xᵀQx + bᵀx + a VectorX expr{}; Eigen::MatrixXd C; Eigen::VectorXd d; std::tie(C, d) = math::DecomposePositiveQuadraticForm(0.5 * Q, b, a, tol); expr.resize(1 + C.rows()); // expr(0) is z expr(0) = linear_expr; // expr.segment(1, C.rows()) = y expr.segment(1, C.rows()) = C * quadratic_vars + d; return ParseLorentzConeConstraint(expr, eval_type); } Binding ParseRotatedLorentzConeConstraint( const Eigen::Ref>& v) { DRAKE_DEMAND(v.rows() >= 3); Eigen::MatrixXd A{}; Eigen::VectorXd b(v.size()); VectorXDecisionVariable vars{}; symbolic::DecomposeAffineExpressions(v, &A, &b, &vars); DRAKE_DEMAND(vars.rows() >= 1); return CreateBinding(std::make_shared(A, b), vars); } Binding ParseRotatedLorentzConeConstraint( const symbolic::Expression& linear_expr1, const symbolic::Expression& linear_expr2, const symbolic::Expression& quadratic_expr, double tol) { const auto& quadratic_p = symbolic::ExtractVariablesFromExpression(quadratic_expr); const auto& quadratic_vars = quadratic_p.first; const auto& quadratic_var_to_index_map = quadratic_p.second; const symbolic::Polynomial poly{quadratic_expr}; Eigen::MatrixXd Q(quadratic_vars.size(), quadratic_vars.size()); Eigen::VectorXd b(quadratic_vars.size()); double a; symbolic::DecomposeQuadraticPolynomial(poly, quadratic_var_to_index_map, &Q, &b, &a); Eigen::MatrixXd C; Eigen::VectorXd d; std::tie(C, d) = math::DecomposePositiveQuadraticForm(0.5 * Q, b, a, tol); VectorX expr(2 + C.rows()); expr(0) = linear_expr1; expr(1) = linear_expr2; expr.tail(C.rows()) = C * quadratic_vars + d; return ParseRotatedLorentzConeConstraint(expr); } std::shared_ptr ParseQuadraticAsRotatedLorentzConeConstraint( const Eigen::Ref& Q, const Eigen::Ref& b, double c, double zero_tol) { // [-bᵀx-c, 1, Fx] is in the rotated Lorentz cone, where FᵀF = 0.5 * Q const Eigen::MatrixXd F = math::DecomposePSDmatrixIntoXtransposeTimesX( (Q + Q.transpose()) / 4, zero_tol); // A_lorentz * x + b_lorentz = [-bᵀx-c, 1, Fx] Eigen::MatrixXd A_lorentz = Eigen::MatrixXd::Zero(2 + F.rows(), F.cols()); Eigen::VectorXd b_lorentz = Eigen::VectorXd::Zero(2 + F.rows()); A_lorentz.row(0) = -b.transpose(); b_lorentz(0) = -c; b_lorentz(1) = 1; A_lorentz.bottomRows(F.rows()) = F; return std::make_shared(A_lorentz, b_lorentz); } } // namespace internal } // namespace solvers } // namespace drake