#pragma once // For external users, please do not include this header file. It only exists so // that we can expose the internals to mosek_solver_internal_test.cc #include #include #include #include #include #include #include "drake/solvers/constraint.h" #include "drake/solvers/mathematical_program.h" #include "drake/solvers/mathematical_program_result.h" namespace drake { namespace solvers { namespace internal { // Mosek treats psd matrix variables in a special manner. // Check https://docs.mosek.com/10.0/capi/tutorial-sdo-shared.html for more // details. To summarize, Mosek stores a positive semidefinite (psd) matrix // variable as a "bar var" (as called in Mosek's API, for example // https://docs.mosek.com/10.0/capi/tutorial-sdo-shared.html). Inside Mosek, it // accesses each of the psd matrix variable with a unique ID. Moreover, the // Mosek user cannot access the entries of the psd matrix variable individually; // instead, the user can only access the matrix X̅ as a whole. To impose // linear constraints on psd matrix variable X̅ entries, the user must specify a // "coefficient matrix" A̅ that multiplies X̅ (using the matrix inner product) to // yield the linear constraint lower ≤ ≤ upper. For example, to impose // the constraint X̅(0, 0) + X̅(1, 0) = 1, where X̅ is a 2 x 2 psd matrix, Mosek // requires the user to write it as = 1, where // A̅ = ⌈1 0.5⌉ // ⌊0.5 0⌋ // On the other hand, drake::solvers::MathematicalProgram doesn't treat psd // matrix variables in a special manner. // MatrixVariableEntry stores the data needed to refer to a particular entry of // a Mosek matrix variable. class MatrixVariableEntry { public: typedef size_t Id; MatrixVariableEntry(MSKint64t bar_matrix_index, MSKint32t row_index, MSKint32t col_index, int num_matrix_rows) : bar_matrix_index_{bar_matrix_index}, row_index_{row_index}, col_index_{col_index}, num_matrix_rows_{num_matrix_rows}, id_{get_next_id()} { // Mosek only stores the lower triangular part of the symmetric matrix. DRAKE_ASSERT(row_index_ >= col_index_); } MSKint64t bar_matrix_index() const { return bar_matrix_index_; } MSKint32t row_index() const { return row_index_; } MSKint32t col_index() const { return col_index_; } int num_matrix_rows() const { return num_matrix_rows_; } Id id() const { return id_; } // Returns the index of the entry in a vector formed by stacking the lower // triangular part of the symmetric matrix column by column. int IndexInLowerTrianglePart() const { return (2 * num_matrix_rows_ - col_index_ + 1) * col_index_ / 2 + row_index_ - col_index_; } private: static size_t get_next_id(); MSKint64t bar_matrix_index_; MSKint32t row_index_; MSKint32t col_index_; int num_matrix_rows_; Id id_; }; // Mosek stores dual variable in different categories, called slc, suc, slx, sux // and snx. Refer to // https://docs.mosek.com/10.0/capi/alphabetic-functionalities.html#mosek.task.getsolution // for more information. enum class DualVarType { kLinearConstraint, ///< Corresponds to Mosek's slc and suc. kVariableBound, ///< Corresponds to Mosek's slx and sux. kNonlinearConic, ///< Corresponds to Mosek's snx. }; struct ConstraintDualIndex { // Type of the dual variable. DualVarType type; // Index of the dual variable. We will use -1 to indicate that a constraint // can never be active. int index{}; }; using ConstraintDualIndices = std::vector; template using DualMap = std::unordered_map, ConstraintDualIndices>; enum class LinearConstraintBoundType { kEquality, kInequality, }; // Mosek treats matrix variables (variables in the psd matrix) in a special // manner, while MathematicalProgram doesn't. Hence we need to pick out the // variables in prog.positive_semidefinite_constraint(), record how they will // be stored in Mosek, and also how the remaining non-matrix variable will be // stored in Mosek. Note that we only loop through // PositiveSemidefiniteConstraint, not LinearMatrixInequalityConstraint. struct MapDecisionVariableToMosekVariable { public: explicit MapDecisionVariableToMosekVariable(const MathematicalProgram& prog); std::unordered_map decision_variable_to_mosek_matrix_variable{}; std::unordered_map decision_variable_to_mosek_nonmatrix_variable{}; // Multiple entries in Mosek matrix variables could correspond to the same // decision variable. We will need to add linear equality constraints to // equate these entries. std::unordered_map> matrix_variable_entries_for_same_decision_variable{}; }; // MosekSolverProgram is a temporary object used by our MosekSolver // implementation that is created (and destroyed) once per Solve() operation. It // provides individual, program-specific helper functions to translate a // MathematicalProgram into Mosek's API. We've separated it from // MosekSolver::DoSolve both to make it easier to understand and to unit test // each function one by one. class MosekSolverProgram { public: MosekSolverProgram(const MathematicalProgram& prog, MSKenv_t env); ~MosekSolverProgram(); // If a matrix variable entry X̅(m, n) appears in a cost or a constraint // (except the psd constraint), then we need a matrix Eₘₙ stored inside Mosek, // such that = X̅(m, n). In this function we add the symmetric matrix // Eₘₙ into Mosek, and record the index of Eₘₙ in Mosek. // @param[out] E_idx The index of Eₘₙ for @p matrix_variable_entry. MSKrescodee AddMatrixVariableEntryCoefficientMatrixIfNonExistent( const MatrixVariableEntry& matrix_variable_entry, MSKint64t* E_index); // Add the product c * X̅(i, j) to a constraint. // This function should be called only if that Mosek matrix variable X̅ appear // only once in this constraint. Otherwise we should call // AddLinearConstraintToMosek, which first collects all the // entries X̅(i, j) belonging to this matrix variable X̅ in this constraint, and // then forms a matrix A, such that contains the weighted sum of all // entries of X̅ in this constraint. MSKrescodee AddScalarTimesMatrixVariableEntryToMosek( MSKint32t constraint_index, const MatrixVariableEntry& matrix_variable_entry, MSKrealt scalar); // Determine the sense of each constraint. The sense can be equality // constraint, less than, greater than, or bounded on both side. MSKrescodee SetMosekLinearConstraintBound( int linear_constraint_index, double lower, double upper, LinearConstraintBoundType bound_type); // Add the linear constraint lower <= A * decision_vars + B * slack_vars <= // upper. MSKrescodee AddLinearConstraintToMosek( const MathematicalProgram& prog, const Eigen::SparseMatrix& A, const Eigen::SparseMatrix& B, const Eigen::VectorXd& lower, const Eigen::VectorXd& upper, const VectorX& decision_vars, const std::vector& slack_vars_mosek_indices, LinearConstraintBoundType bound_type); // Convert the expression A * decicion_vars + B * slack_vars to Mosek affine // expression format // ∑ⱼ fᵢⱼ xⱼ + ∑ⱼ // Please refer to // https://docs.mosek.com/latest/capi/tutorial-acc-optimizer.html for an // introduction on Mosek affine expression, and // https://docs.mosek.com/latest/capi/tutorial-sdo-shared.html for the affine // expression with matrix variables. // F̅ᵢⱼ is a weighted sum of the symmetric matrix E stored inside Mosek. // bar_F[i][j] stores the indices of E and the weights of E. // namely F̅ᵢⱼ = ∑ₖbar_F[i][j][k].second* E[bar_F[i][j][k].first] MSKrescodee ParseLinearExpression( const solvers::MathematicalProgram& prog, const Eigen::SparseMatrix& A, const Eigen::SparseMatrix& B, const VectorX& decision_vars, const std::vector& slack_vars_mosek_indices, std::vector* F_subi, std::vector* F_subj, std::vector* F_valij, std::vector, std::vector>>>* bar_F); // Same as ParseLinearExpression, but assumes that each entry in // `decision_vars` is unique. MSKrescodee ParseLinearExpressionNoDuplication( const solvers::MathematicalProgram& prog, const Eigen::SparseMatrix& A, const Eigen::SparseMatrix& B, const VectorX& decision_vars, const std::vector& slack_vars_mosek_indices, std::vector* F_subi, std::vector* F_subj, std::vector* F_valij, std::vector, std::vector>>>* bar_F); // Add LinearConstraints and LinearEqualityConstraints to the Mosek task. // @param[out] dual_indices maps each linear constraint to its dual variable // indices. template MSKrescodee AddLinearConstraintsFromBindings( const std::vector>& constraint_list, LinearConstraintBoundType bound_type, const MathematicalProgram& prog, std::unordered_map, ConstraintDualIndices>* dual_indices); // @param[out] lin_eq_con_dual_indices maps each linear equality constraint to // its dual variable indices. MSKrescodee AddLinearConstraints( const MathematicalProgram& prog, std::unordered_map, ConstraintDualIndices>* linear_con_dual_indices, std::unordered_map, ConstraintDualIndices>* lin_eq_con_dual_indices); // Add the bounds on the decision variables in @p prog. Note that if a // decision variable in positive definite matrix has a bound, we need to add // new linear constraint to Mosek to bound that variable. // @param[out] dual_indices Map each bounding box constraint to its dual // variable indices. MSKrescodee AddBoundingBoxConstraints( const MathematicalProgram& prog, std::unordered_map, std::pair>* dual_indices); /** Adds the constraint that A * decision_vars + B * slack_vars + c is in an affine cone. @param[out] acc_index The index of the newly added affine cone. */ MSKrescodee AddAffineConeConstraint( const MathematicalProgram& prog, const Eigen::SparseMatrix& A, const Eigen::SparseMatrix& B, const VectorX& decision_vars, const std::vector& slack_vars_mosek_indices, const Eigen::VectorXd& c, MSKconetypee cone_type, MSKint64t* acc_index); /* * This is the helper function to add three types of conic constraints * 1. A Lorentz cone constraint: * z = A*x+b * z0 >= sqrt(z1^2 + .. zN^2) * 2. A rotated Lorentz cone constraint: * z = A*x+b * z0*z1 >= z2^2 + .. + zN^2, * z0 >= 0, z1 >=0 * 3. An exonential cone constraint: * z = A*x+b * z0 >= z1 * exp(z2 / z1) * @param[out] acc_indices Maps each conic constraint to its Affine Cone * Constraint indices. */ template MSKrescodee AddConeConstraints( const MathematicalProgram& prog, const std::vector>& cone_constraints, std::unordered_map, MSKint64t>* acc_indices); // @param[out] psd_barvar_indices maps each psd constraint to Mosek matrix // variable MSKrescodee AddPositiveSemidefiniteConstraints( const MathematicalProgram& prog, std::unordered_map, MSKint32t>* psd_barvar_indices); MSKrescodee AddLinearMatrixInequalityConstraint( const MathematicalProgram& prog); MSKrescodee AddLinearCost(const Eigen::SparseVector& linear_coeff, const VectorX& linear_vars, const MathematicalProgram& prog); MSKrescodee AddQuadraticCostAsLinearCost( const Eigen::SparseMatrix& Q_lower, const VectorX& quadratic_vars, const MathematicalProgram& prog); MSKrescodee AddQuadraticCost( const Eigen::SparseMatrix& Q_quadratic_vars, const VectorX& quadratic_vars, const MathematicalProgram& prog); MSKrescodee AddCosts(const MathematicalProgram& prog); // @param[out] with_integer_or_binary_variables True if the program has // integer or binary variables. MSKrescodee SpecifyVariableType(const MathematicalProgram& prog, bool* with_integer_or_binary_variables); // Some entries in Mosek matrix variables might correspond to the same // decision variable in MathematicalProgram. Add the equality constraint // between these matrix variable entries. MSKrescodee AddEqualityConstraintBetweenMatrixVariablesForSameDecisionVariable(); // @param[in/out] result Update the dual solution for psd constraints in // result. MSKrescodee SetPositiveSemidefiniteConstraintDualSolution( const MathematicalProgram& prog, const std::unordered_map, MSKint32t>& psd_barvar_indices, MSKsoltypee whichsol, MathematicalProgramResult* result) const; // @param[in/out] result Update the dual solution in result. MSKrescodee SetDualSolution( MSKsoltypee which_sol, const MathematicalProgram& prog, const std::unordered_map< Binding, std::pair>& bb_con_dual_indices, const DualMap& linear_con_dual_indices, const DualMap& lin_eq_con_dual_indices, const std::unordered_map, MSKint64t>& lorentz_cone_acc_indices, const std::unordered_map, MSKint64t>& rotated_lorentz_cone_acc_indices, const std::unordered_map, MSKint64t>& exp_cone_acc_indices, const std::unordered_map, MSKint32t>& psd_barvar_indices, MathematicalProgramResult* result) const; // @param[out] print_to_console Set to true if solver options requires // printing the log to the console. // @param[out] print_file_name Set to the name of the print file store in // solver options. If solver options doesn't store the print file name, then // set *print_file_name to an empty string. // @param[out] msk_writedata If solver options stores the file for writing // data, then put the file name to msk_writedata for later use. MSKrescodee UpdateOptions(const SolverOptions& solver_options, SolverId mosek_id, bool* print_to_console, std::string* print_file_name, std::optional* msk_writedata); MSKtask_t task() const { return task_; } // For each entry of the matrix variable X̅ᵢ(m,n), if this entry is also used // in the cost or constraint, then we will use the term , where // the inner product = X̅ᵢ(m,n). The "selection matrix" E̅ₘₙ helps // to select the (m, n)'th entry of the psd matrix variable. E̅ₘₙ is stored // inside Mosek with a unique ID. // matrix_variable_entry_to_selection_matrix_id maps the matrix variable // entry to the ID of E̅ₘₙ. // TODO(hongkai.dai): do not map the matrix variable entry to the selection // matrix, instead maps the tuple (matrix_size, row_index, column_index) to // the selection matrix. This will create fewer selection matrices, when // multiple matrix variables have the same size. const std::unordered_map& matrix_variable_entry_to_selection_matrix_id() const { return matrix_variable_entry_to_selection_matrix_id_; } const std::unordered_map& decision_variable_to_mosek_matrix_variable() const { return map_decision_var_to_mosek_var_ .decision_variable_to_mosek_matrix_variable; } const std::unordered_map& decision_variable_to_mosek_nonmatrix_variable() const { return map_decision_var_to_mosek_var_ .decision_variable_to_mosek_nonmatrix_variable; } const std::unordered_map>& matrix_variable_entries_for_same_decision_variable() const { return map_decision_var_to_mosek_var_ .matrix_variable_entries_for_same_decision_variable; } private: MSKtask_t task_{nullptr}; std::unordered_map matrix_variable_entry_to_selection_matrix_id_{}; const MapDecisionVariableToMosekVariable map_decision_var_to_mosek_var_; }; template MSKrescodee MosekSolverProgram::AddLinearConstraintsFromBindings( const std::vector>& constraint_list, LinearConstraintBoundType bound_type, const MathematicalProgram& prog, std::unordered_map, ConstraintDualIndices>* dual_indices) { MSKrescodee rescode{MSK_RES_OK}; for (const auto& binding : constraint_list) { const auto& constraint = binding.evaluator(); const Eigen::SparseMatrix& A = constraint->get_sparse_A(); const Eigen::VectorXd& lb = constraint->lower_bound(); const Eigen::VectorXd& ub = constraint->upper_bound(); Eigen::SparseMatrix B_zero(A.rows(), 0); B_zero.setZero(); int num_linear_constraints{-1}; rescode = MSK_getnumcon(task_, &num_linear_constraints); if (rescode != MSK_RES_OK) { return rescode; } rescode = AddLinearConstraintToMosek(prog, A, B_zero, lb, ub, binding.variables(), {}, bound_type); if (rescode != MSK_RES_OK) { return rescode; } ConstraintDualIndices constraint_dual_indices(lb.rows()); for (int i = 0; i < lb.rows(); ++i) { constraint_dual_indices[i].type = DualVarType::kLinearConstraint; constraint_dual_indices[i].index = num_linear_constraints + i; } dual_indices->emplace(binding, constraint_dual_indices); } return rescode; } template MSKrescodee MosekSolverProgram::AddConeConstraints( const MathematicalProgram& prog, const std::vector>& cone_constraints, std::unordered_map, MSKint64t>* acc_indices) { static_assert(std::is_same_v || std::is_same_v || std::is_same_v, "Should be either Lorentz cone constraint, rotated Lorentz " "cone or exponential cone constraint"); const bool is_rotated_cone = std::is_same_v; MSKrescodee rescode = MSK_RES_OK; for (auto const& binding : cone_constraints) { MSKint64t acc_index; if (is_rotated_cone) { // Drake's RotatedLorentzConeConstraint imposes z₀ * z₁ ≥ z₂² + z₃² + ... // zₙ₋₁² where z being an affine expression of x, while Mosek's rotated // quadratic cone imposes 2 * z₀ * z₁ ≥ z₂² + z₃² + ... zₙ₋₁² (notice the // factor of 2). Hence we will impose the constraint that the vector // ⌈0.5*(A.row(0)*x + b(0))⌉ // | A.row(1)*x + b(1) | // | ... | // ⌊ A.row(n-1)*x + b(n-1)⌋ // is in Mosek's rotated quadratic cone. Namely we multiply 0.5 to the // first row of A, b. Eigen::SparseMatrix A = binding.evaluator()->A(); for (int k = 0; k < A.outerSize(); ++k) { for (Eigen::SparseMatrix::InnerIterator it(A, k); it; ++it) { if (it.row() == 0) { it.valueRef() *= 0.5; } } } Eigen::VectorXd b = binding.evaluator()->b(); b(0) *= 0.5; rescode = this->AddAffineConeConstraint( prog, A, Eigen::SparseMatrix(A.rows(), 0), binding.variables(), {}, b, MSK_CT_RQUAD, &acc_index); if (rescode != MSK_RES_OK) { return rescode; } } else { MSKconetypee cone_type; if (std::is_same_v) { cone_type = MSK_CT_QUAD; } else if (std::is_same_v) { cone_type = MSK_CT_PEXP; } rescode = this->AddAffineConeConstraint( prog, binding.evaluator()->A(), Eigen::SparseMatrix(binding.evaluator()->A().rows(), 0), binding.variables(), {}, binding.evaluator()->b(), cone_type, &acc_index); if (rescode != MSK_RES_OK) { return rescode; } } acc_indices->emplace(binding, acc_index); } return rescode; } // @param slx Mosek dual variables for variable lower bound. See // https://docs.mosek.com/10.0/capi/alphabetic-functionalities.html#mosek.task.getslx // @param sux Mosek dual variables for variable upper bound. See // https://docs.mosek.com/10.0/capi/alphabetic-functionalities.html#mosek.task.getsux // @param slc Mosek dual variables for linear constraint lower bound. See // https://docs.mosek.com/10.0/capi/alphabetic-functionalities.html#mosek.task.getslc // @param suc Mosek dual variables for linear constraint upper bound. See // https://docs.mosek.com/10.0/capi/alphabetic-functionalities.html#mosek.task.getsuc void SetBoundingBoxDualSolution( const std::vector>& constraints, const std::vector& slx, const std::vector& sux, const std::vector& slc, const std::vector& suc, const std::unordered_map< Binding, std::pair>& bb_con_dual_indices, MathematicalProgramResult* result); template void SetLinearConstraintDualSolution( const std::vector>& bindings, const std::vector& slc, const std::vector& suc, const std::unordered_map, ConstraintDualIndices>& linear_con_dual_indices, MathematicalProgramResult* result) { for (const auto& binding : bindings) { const ConstraintDualIndices duals = linear_con_dual_indices.at(binding); Eigen::VectorXd dual_sol = Eigen::VectorXd::Zero(binding.evaluator()->num_constraints()); for (int i = 0; i < dual_sol.rows(); ++i) { DRAKE_DEMAND(duals[i].type == DualVarType::kLinearConstraint); // Mosek defines all dual solutions as non-negative. However we use // "reduced cost" as the dual solution, so the dual solution for a // lower bound should be non-negative, while the dual solution for // an upper bound should be non-positive. if (slc[duals[i].index] > suc[duals[i].index]) { dual_sol[i] = slc[duals[i].index]; } else { dual_sol[i] = -suc[duals[i].index]; } } result->set_dual_solution(binding, dual_sol); } } /* * Sets the dual solution for the affine cone constraints. * @param bindings The constraint for which the dual solution will be set. * @param task The Mosek task. * @param whichsol The solution type. See * https://docs.mosek.com/latest/capi/constants.html#mosek.soltype * @param acc_indices Maps each constraint to the affine cone constraint * indices. * @param[out] result The dual solution in `result` will be set. */ template MSKrescodee SetAffineConeConstraintDualSolution( const std::vector>& bindings, MSKtask_t task, MSKsoltypee whichsol, const std::unordered_map, MSKint64t>& acc_indices, MathematicalProgramResult* result) { for (const auto& binding : bindings) { const MSKint64t acc_index = acc_indices.at(binding); MSKint64t acc_dim; auto rescode = MSK_getaccn(task, acc_index, &acc_dim); if (rescode != MSK_RES_OK) { return rescode; } Eigen::VectorXd dual_sol = Eigen::VectorXd::Zero(acc_dim); rescode = MSK_getaccdoty(task, whichsol, acc_index, dual_sol.data()); if (rescode != MSK_RES_OK) { return rescode; } if constexpr (std::is_same_v) { // Drake's rotated Lorentz cone constraint on z = Ax+b is // K_drake={ z | z₀z₁≥ z₂² + ... zₙ₋₁², z₀≥0, z₁≥0} // On the other hand, Mosek's rotated Lorentz cone constraint has a // multiplier of 2 // K_mosek={ z | 2z₀z₁≥ z₂² + ... zₙ₋₁², z₀≥0, z₁≥0} Hence // we can write K_drake = C*K_mosek where C = diag([2, 1, ..., 1]). By // duality we know K_drake_dual = C⁻ᵀ*K_mosek_dual, where K_drake_dual is // the dual cone of K_drake, likewise K_mosek_dual is the dual cone of // K_mosek. dual_sol(0) *= 0.5; } result->set_dual_solution(binding, dual_sol); } return MSK_RES_OK; } } // namespace internal } // namespace solvers } // namespace drake