#include "drake/solvers/test/semidefinite_program_examples.h" #include #include #include #include #include "drake/common/test_utilities/eigen_matrix_compare.h" #include "drake/math/matrix_util.h" #include "drake/solvers/test/mathematical_program_test_util.h" namespace drake { namespace solvers { namespace test { using Eigen::Matrix3d; using Eigen::Matrix4d; using Eigen::Vector2d; using Eigen::Vector3d; using symbolic::Expression; const double kInf = std::numeric_limits::infinity(); void TestTrivialSDP(const SolverInterface& solver, double tol) { MathematicalProgram prog; auto S = prog.NewSymmetricContinuousVariables<2>("S"); // S is p.s.d prog.AddPositiveSemidefiniteConstraint(S); // S(1, 0) = 1 prog.AddBoundingBoxConstraint(1, 1, S(1, 0)); // Min S.trace() prog.AddLinearCost(S.cast().trace()); const MathematicalProgramResult result = RunSolver(prog, solver); auto S_value = result.GetSolution(S); EXPECT_TRUE(CompareMatrices(S_value, Eigen::Matrix2d::Ones(), tol)); EXPECT_NEAR(result.get_optimal_cost(), 2.0, tol); } void FindCommonLyapunov(const SolverInterface& solver, const std::optional& solver_options, double tol) { MathematicalProgram prog; auto P = prog.NewSymmetricContinuousVariables<3>("P"); const double psd_epsilon{1E-3}; prog.AddPositiveSemidefiniteConstraint(P - psd_epsilon * Matrix3d::Identity()); Eigen::Matrix3d A1; // clang-format off A1 << -1, -1, -2, 0, -1, -3, 0, 0, -1; // clang-format on auto binding1 = prog.AddPositiveSemidefiniteConstraint( -A1.transpose() * P - P * A1 - psd_epsilon * Matrix3d::Identity()); Eigen::Matrix3d A2; // clang-format off A2 << -1, -1.2, -1.8, 0, -0.7, -2, 0, 0, -0.4; // clang-format on auto binding2 = prog.AddPositiveSemidefiniteConstraint( -A2.transpose() * P - P * A2 - psd_epsilon * Matrix3d::Identity()); const MathematicalProgramResult result = RunSolver(prog, solver, {}, solver_options); const Matrix3d P_value = result.GetSolution(P); const auto Q1_flat_value = result.GetSolution(binding1.variables()); const auto Q2_flat_value = result.GetSolution(binding2.variables()); const Eigen::Map Q1_value(&Q1_flat_value(0)); const Eigen::Map Q2_value(&Q2_flat_value(0)); Eigen::SelfAdjointEigenSolver eigen_solver_P(P_value); // The comparison tolerance is set as 1E-8, to match the Mosek default // feasibility tolerance 1E-8. EXPECT_TRUE(CompareMatrices(P_value, P_value.transpose(), std::numeric_limits::epsilon(), MatrixCompareType::absolute)); EXPECT_GE(eigen_solver_P.eigenvalues().minCoeff(), 0); Eigen::SelfAdjointEigenSolver eigen_solver_Q1(Q1_value); EXPECT_GE(eigen_solver_Q1.eigenvalues().minCoeff(), 0); Eigen::SelfAdjointEigenSolver eigen_solver_Q2(Q2_value); EXPECT_GE(eigen_solver_Q2.eigenvalues().minCoeff(), 0); EXPECT_TRUE(CompareMatrices(A1.transpose() * P_value + P_value * A1 + psd_epsilon * Matrix3d::Identity(), -Q1_value, tol, MatrixCompareType::absolute)); EXPECT_TRUE(CompareMatrices(A2.transpose() * P_value + P_value * A2 + psd_epsilon * Matrix3d::Identity(), -Q2_value, tol, MatrixCompareType::absolute)); } void FindOuterEllipsoid(const SolverInterface& solver, const std::optional& solver_options, double tol) { std::array Q; std::array b; Q[0] = Matrix3d::Identity(); b[0] = Vector3d::Zero(); // clang-format off Q[1] << 1, 0.2, 0.3, 0.2, 2, 0.6, 0.3, 0.6, 3; b[1] << 0.3, 2, 1; Q[2] << 1, -0.1, 0.2, -0.1, 4, 0.3, 0.2, 0.3, 3; b[2] << 2, -1, 3; // clang-format on MathematicalProgram prog; auto P = prog.NewSymmetricContinuousVariables<3>("P"); prog.AddPositiveSemidefiniteConstraint(P); auto s = prog.NewContinuousVariables<3>("s"); prog.AddBoundingBoxConstraint(0, kInf, s); auto c = prog.NewContinuousVariables<3>("c"); for (int i = 0; i < 3; ++i) { Eigen::Matrix M{}; // clang-format off M << s(i) * Q[i] - P, s(i) * b[i] - c, s(i) * b[i].transpose() - c.transpose(), 1 - s(i); // clang-format on prog.AddPositiveSemidefiniteConstraint(M); } prog.AddLinearCost(-P.cast().trace()); const MathematicalProgramResult result = RunSolver(prog, solver, {}, solver_options); const auto P_value = result.GetSolution(P); const auto s_value = result.GetSolution(s); const auto c_value = result.GetSolution(c); EXPECT_NEAR(-P_value.trace(), result.get_optimal_cost(), tol); const Eigen::SelfAdjointEigenSolver es_P(P_value); EXPECT_TRUE((es_P.eigenvalues().array() >= -tol).all()); // The minimal eigen value of M should be 0, since the optimality happens at // the boundary of the PSD cone. double M_min_eigenvalue = kInf; for (int i = 0; i < 3; ++i) { Matrix4d M_value; // clang-format off M_value << s_value(i) * Q[i] - P_value, s_value(i) * b[i] - c_value, s_value(i) * b[i].transpose() - c_value.transpose(), 1 - s_value(i); // clang-format on Eigen::SelfAdjointEigenSolver es_M(M_value); EXPECT_TRUE((es_M.eigenvalues().array() >= -tol).all()); M_min_eigenvalue = std::min(M_min_eigenvalue, es_M.eigenvalues().minCoeff()); } EXPECT_NEAR(M_min_eigenvalue, 0, tol); } void SolveEigenvalueProblem(const SolverInterface& solver, const std::optional& solver_options, double tol) { MathematicalProgram prog; auto x = prog.NewContinuousVariables<2>("x"); Matrix3d F1; // clang-format off F1 << 1, 0.2, 0.3, 0.2, 2, -0.1, 0.3, -0.1, 4; Matrix3d F2; F2 << 2, 0.4, 0.7, 0.4, -1, 0.1, 0.7, 0.1, 5; // clang-format on auto z = prog.NewContinuousVariables<1>("z"); prog.AddLinearMatrixInequalityConstraint( {Matrix3d::Zero(), Matrix3d::Identity(), -F1, -F2}, {z, x}); const Vector2d x_lb(0.1, 1); const Vector2d x_ub(2, 3); prog.AddBoundingBoxConstraint(x_lb, x_ub, x); prog.AddLinearCost(z(0)); const MathematicalProgramResult result = RunSolver(prog, solver, {}, solver_options); const double z_value = result.GetSolution(z(0)); const auto x_value = result.GetSolution(x); const auto xF_sum = x_value(0) * F1 + x_value(1) * F2; EXPECT_NEAR(z_value, result.get_optimal_cost(), tol); Eigen::SelfAdjointEigenSolver eigen_solver_xF(xF_sum); EXPECT_NEAR(z_value, eigen_solver_xF.eigenvalues().maxCoeff(), tol); EXPECT_TRUE(((x_value - x_lb).array() >= -tol).all()); EXPECT_TRUE(((x_value - x_ub).array() <= tol).all()); } void SolveSDPwithSecondOrderConeExample1(const SolverInterface& solver, double tol) { MathematicalProgram prog; auto X = prog.NewSymmetricContinuousVariables<3>(); auto x = prog.NewContinuousVariables<3>(); Eigen::Matrix3d C0; // clang-format off C0 << 2, 1, 0, 1, 2, 1, 0, 1, 2; // clang-format on prog.AddLinearCost((C0 * X.cast()).trace() + x(0)); prog.AddLinearConstraint( (Matrix3d::Identity() * X.cast()).trace() + x(0) == 1); prog.AddLinearConstraint( (Matrix3d::Ones() * X.cast()).trace() + x(1) + x(2) == 0.5); prog.AddPositiveSemidefiniteConstraint(X); prog.AddLorentzConeConstraint(x.cast()); MathematicalProgramResult result; solver.Solve(prog, {}, {}, &result); EXPECT_TRUE(result.is_success()); const auto X_val = result.GetSolution(X); const auto x_val = result.GetSolution(x); EXPECT_NEAR((C0 * X_val).trace() + x_val(0), result.get_optimal_cost(), tol); EXPECT_NEAR((Eigen::Matrix3d::Identity() * X_val).trace() + x_val(0), 1, tol); EXPECT_NEAR((Eigen::Matrix3d::Ones() * X_val).trace() + x_val(1) + x_val(2), 0.5, tol); EXPECT_GE(x_val(0), std::sqrt(x_val(1) * x_val(1) + x_val(2) * x_val(2)) - tol); } void SolveSDPwithSecondOrderConeExample2(const SolverInterface& solver, double tol) { MathematicalProgram prog; const auto X = prog.NewSymmetricContinuousVariables<3>(); const auto x = prog.NewContinuousVariables<1>()(0); prog.AddLinearCost(X(0, 0) + X(1, 1) + x); prog.AddBoundingBoxConstraint(0, kInf, x); prog.AddLinearConstraint(X(0, 0) + 2 * X(1, 1) + X(2, 2) + 3 * x == 3); Vector3 lorentz_cone_expr; lorentz_cone_expr << X(0, 0), X(1, 1) + x, X(1, 1) + X(2, 2); prog.AddLorentzConeConstraint(lorentz_cone_expr); prog.AddLinearConstraint(X(1, 0) + X(2, 1) == 1); prog.AddPositiveSemidefiniteConstraint(X); MathematicalProgramResult result; solver.Solve(prog, {}, {}, &result); EXPECT_TRUE(result.is_success()); const auto X_val = result.GetSolution(X); const auto x_val = result.GetSolution(x); EXPECT_NEAR(result.get_optimal_cost(), X_val(0, 0) + X_val(1, 1) + x_val, tol); Eigen::SelfAdjointEigenSolver es(X_val); EXPECT_TRUE((es.eigenvalues().array() >= -tol).all()); EXPECT_NEAR(X_val(0, 0) + 2 * X_val(1, 1) + X_val(2, 2) + 3 * x_val, 3, tol); EXPECT_GE(X_val(0, 0), std::sqrt(std::pow(X_val(1, 1) + x_val, 2) + std::pow(X_val(1, 1) + X_val(2, 2), 2)) - tol); EXPECT_NEAR(X_val(1, 0) + X_val(2, 1), 1, tol); EXPECT_GE(x_val, -tol); } void SolveSDPwithOverlappingVariables(const SolverInterface& solver, double tol) { MathematicalProgram prog; auto x = prog.NewContinuousVariables<3>(); prog.AddPositiveSemidefiniteConstraint( (Matrix2() << x(0), x(1), x(1), x(0)).finished()); prog.AddPositiveSemidefiniteConstraint( (Matrix2() << x(0), x(2), x(2), x(0)).finished()); prog.AddBoundingBoxConstraint(1, 1, x(1)); prog.AddLinearCost(2 * x(0) + x(2)); MathematicalProgramResult result; solver.Solve(prog, {}, {}, &result); EXPECT_TRUE(result.is_success()); EXPECT_TRUE( CompareMatrices(result.GetSolution(x), Eigen::Vector3d(1, 1, -1), tol)); EXPECT_NEAR(result.get_optimal_cost(), 1, tol); } void SolveSDPwithQuadraticCosts(const SolverInterface& solver, double tol) { MathematicalProgram prog; auto x = prog.NewContinuousVariables<3>(); const Matrix2 X1 = (Matrix2() << x(0), x(1), x(1), x(0)).finished(); auto psd_constraint1 = prog.AddPositiveSemidefiniteConstraint(X1); const Matrix2 X2 = (Matrix2() << x(0), x(2), x(2), x(0)).finished(); auto psd_constraint2 = prog.AddPositiveSemidefiniteConstraint(X2); prog.AddBoundingBoxConstraint(1, 1, x(1)); prog.AddQuadraticCost(x(0) * x(0)); prog.AddLinearCost(2 * x(0) + x(2)); MathematicalProgramResult result; solver.Solve(prog, {}, {}, &result); EXPECT_TRUE(result.is_success()); EXPECT_TRUE( CompareMatrices(result.GetSolution(x), Eigen::Vector3d(1, 1, -1), tol)); EXPECT_NEAR(result.get_optimal_cost(), 2, tol); // Check the complementarity condition for the PSD constraint. const auto psd_dual1 = math::ToSymmetricMatrixFromLowerTriangularColumns( result.GetDualSolution(psd_constraint1)); const auto psd_dual2 = math::ToSymmetricMatrixFromLowerTriangularColumns( result.GetDualSolution(psd_constraint2)); const auto X1_sol = result.GetSolution(X1); const auto X2_sol = result.GetSolution(X2); EXPECT_NEAR((psd_dual1 * X1_sol).trace(), 0, tol); EXPECT_NEAR((psd_dual2 * X2_sol).trace(), 0, tol); } void TestSDPDualSolution1(const SolverInterface& solver, double tol) { MathematicalProgram prog; auto X = prog.NewSymmetricContinuousVariables<2>(); auto psd_con = prog.AddPositiveSemidefiniteConstraint(X); auto bb_con = prog.AddBoundingBoxConstraint( Eigen::Vector2d(kInf, kInf), Eigen::Vector2d(4, 1), Vector2(X(0, 0), X(1, 1))); prog.AddLinearCost(X(1, 0)); MathematicalProgramResult result; solver.Solve(prog, {}, {}, &result); EXPECT_TRUE(result.is_success()); const auto X_sol = result.GetSolution(X); EXPECT_TRUE(CompareMatrices( X_sol, (Eigen::Matrix2d() << 4, -2, -2, 1).finished(), tol)); // The optimal cost is -sqrt(x0 * x2), hence the sensitivity to the // bounding box constraint on x0 is -.25, and the sensitivity to the bounding // box constraint on x2 is -1. const Eigen::Vector2d bb_con_dual_expected(-0.25, -1); EXPECT_TRUE(CompareMatrices(result.GetDualSolution(bb_con), bb_con_dual_expected, tol)); const auto psd_dual = math::ToSymmetricMatrixFromLowerTriangularColumns( result.GetDualSolution(psd_con)); // Complementarity condition ensures the inner product of X and its dual is 0. EXPECT_NEAR((X_sol * psd_dual).trace(), 0, tol); // The problem in the primal form is // min [0 0.5] ● X // [0.5 0] // s.t [1 0] ● X <= 4 // [0 0] // // [0 0] ● X <= 1 // [0 1] // The problem in the dual form (LMI) is // max 4*y1 + y2 // s.t [-y1 0.5] is psd (1) // [0.5 -y2] // The optimal solution is to the dual is y1 = -0.25, y2 = -1. Plug in this // dual solution to the left hand side of (1) is what Mosek/SCS returns as the // dual solution. Eigen::Matrix2d psd_dual_expected; // clang-format off psd_dual_expected << 0.25, 0.5, 0.5, 1; // clang-format on EXPECT_TRUE(CompareMatrices(psd_dual, psd_dual_expected, tol)); } } // namespace test } // namespace solvers } // namespace drake