#include "drake/solvers/augmented_lagrangian.h" #include #include #include "drake/common/extract_double.h" #include "drake/common/test_utilities/eigen_matrix_compare.h" #include "drake/math/autodiff_gradient.h" #include "drake/solvers/aggregate_costs_constraints.h" #include "drake/solvers/mathematical_program.h" namespace drake { namespace solvers { namespace { const double kInf = std::numeric_limits::infinity(); // This function is only used in AugmentedLagrangian.TestEmptyProg. template void TestEmptyProg(const AL& dut) { EXPECT_EQ(dut.lagrangian_size(), 0); EXPECT_TRUE(dut.is_equality().empty()); Eigen::Vector2d x_val(1, 2); // Now the program has no cost nor constraint. Its augmented Lagrangian is // zero. Eigen::VectorXd constraint_residue; double cost; double al_val; if constexpr (std::is_same_v) { al_val = dut.template Eval(x_val, Eigen::VectorXd(0), 0.5, &constraint_residue, &cost); } else { al_val = dut.template Eval(x_val, Eigen::VectorXd(0), Eigen::VectorXd(0), 0.5, &constraint_residue, &cost); } EXPECT_EQ(al_val, 0); EXPECT_EQ(constraint_residue.rows(), 0); EXPECT_EQ(cost, 0); EXPECT_TRUE(CompareMatrices(dut.x_lo(), Eigen::Vector2d::Constant(-kInf))); EXPECT_TRUE(CompareMatrices(dut.x_up(), Eigen::Vector2d::Constant(kInf))); } GTEST_TEST(AugmentedLagrangian, TestEmptyProg) { // Construct an un-constrained program. MathematicalProgram prog; auto x = prog.NewContinuousVariables<2>(); const AugmentedLagrangianNonsmooth dut_nonsmooth(&prog, false); TestEmptyProg(dut_nonsmooth); const AugmentedLagrangianSmooth dut_smooth(&prog, false); TestEmptyProg(dut_smooth); } template void TestObjective(const AL& dut) { EXPECT_EQ(dut.lagrangian_size(), 0); EXPECT_TRUE(dut.is_equality().empty()); // Now evaluate the augmented Lagrangian, it should just be the same as the // costs. Eigen::Vector2d x_val(1, 2); double cost_expected = 0; for (const auto& binding : dut.prog().GetAllCosts()) { cost_expected += dut.prog().EvalBinding(binding, x_val)(0); } Eigen::VectorXd constraint_residue; double cost; double al_val; if constexpr (std::is_same_v) { al_val = dut.template Eval(x_val, Eigen::VectorXd(0), 0.5, &constraint_residue, &cost); } else { al_val = dut.template Eval(x_val, Eigen::VectorXd(0), Eigen::VectorXd(0), 0.5, &constraint_residue, &cost); } EXPECT_EQ(al_val, cost_expected); EXPECT_EQ(cost, cost_expected); EXPECT_EQ(constraint_residue.rows(), 0); // Now evaluate the augmented Lagrangian with autodiff. VectorX constraint_residue_ad; AutoDiffXd cost_ad; AutoDiffXd al_ad; const auto x_ad = math::InitializeAutoDiff(x_val); if constexpr (std::is_same_v) { al_ad = dut.template Eval(x_ad, Eigen::VectorXd(0), 0.5, &constraint_residue_ad, &cost_ad); } else { al_ad = dut.template Eval(x_ad, AutoDiffVecXd(0), Eigen::VectorXd(0), 0.5, &constraint_residue_ad, &cost_ad); } AutoDiffXd al_expected(0); for (const auto& binding : dut.prog().GetAllCosts()) { al_expected += dut.prog().EvalBinding(binding, x_ad)(0); } EXPECT_EQ(ExtractDoubleOrThrow(al_ad), ExtractDoubleOrThrow(al_expected)); EXPECT_TRUE(CompareMatrices(al_ad.derivatives(), al_expected.derivatives())); EXPECT_EQ(cost_ad.value(), al_expected.value()); EXPECT_TRUE( CompareMatrices(cost_ad.derivatives(), al_expected.derivatives())); EXPECT_EQ(constraint_residue_ad.rows(), 0); } GTEST_TEST(AugmentedLagrangian, TestObjective1) { // Test with an empty program. MathematicalProgram prog; auto x = prog.NewContinuousVariables<2>(); for (bool include_x_bounds : {false, true}) { const AugmentedLagrangianNonsmooth dut_nonsmooth(&prog, include_x_bounds); TestObjective(dut_nonsmooth); const AugmentedLagrangianSmooth dut_smooth(&prog, include_x_bounds); TestObjective(dut_smooth); } } GTEST_TEST(AugmentedLagrangian, TestObjective2) { // Test a program with costs but no constraints. MathematicalProgram prog; auto x = prog.NewContinuousVariables<2>(); // Now add costs auto cost1 = prog.AddQuadraticCost(x[0] * x[0] + 2 * x[1] + 3); auto cost2 = prog.AddLinearCost(x[0] * 3 + 4); for (bool include_x_bounds : {false, true}) { const AugmentedLagrangianNonsmooth dut_nonsmooth(&prog, include_x_bounds); TestObjective(dut_nonsmooth); const AugmentedLagrangianSmooth dut_smooth(&prog, include_x_bounds); TestObjective(dut_smooth); } } class DummyConstraint final : public solvers::Constraint { public: DummyConstraint() : Constraint(4, 2, Eigen::Vector4d(1, 2, 3, 4), Eigen::Vector4d(1, 2, 3, 4)) {} using Constraint::set_bounds; protected: template void DoEvalGeneric(const Eigen::Ref>& x, VectorX* y) const { y->resize(num_constraints()); (*y)(0) = x(0) * x(1) + 1; (*y)(1) = x(0) + x(1); (*y)(2) = 2 * x(0) + 3; (*y)(3) = 2 * x(0) / x(1) + 4; } void DoEval(const Eigen::Ref& x, Eigen::VectorXd* y) const override { DoEvalGeneric(x, y); } void DoEval(const Eigen::Ref& x, AutoDiffVecXd* y) const override { DoEvalGeneric(x, y); } void DoEval(const Eigen::Ref>& x, VectorX* y) const override { DoEvalGeneric(x.cast(), y); } }; template void CompareAlResult(const T& al, const T& al_expected, const VectorX& constraint_residue, const VectorX& constraint_residue_expected, const T& cost, const T& cost_expected, double tol) { if constexpr (std::is_same_v) { EXPECT_NEAR(al, al_expected, tol); EXPECT_TRUE( CompareMatrices(constraint_residue, constraint_residue_expected, tol)); EXPECT_NEAR(cost, cost_expected, tol); } else { EXPECT_NEAR(al.value(), al_expected.value(), tol); EXPECT_TRUE( CompareMatrices(al.derivatives(), al_expected.derivatives(), tol)); EXPECT_TRUE(CompareMatrices(math::ExtractValue(constraint_residue), math::ExtractValue(constraint_residue_expected), tol)); EXPECT_TRUE(CompareMatrices( math::ExtractGradient(constraint_residue), math::ExtractGradient(constraint_residue_expected), tol)); EXPECT_NEAR(cost.value(), cost_expected.value(), tol); EXPECT_TRUE( CompareMatrices(cost.derivatives(), cost_expected.derivatives(), tol)); } } template T al_equality(const Eigen::Ref>& lhs, const Eigen::Ref& lambda, double mu) { return -lambda.dot(lhs) + mu / 2 * lhs.array().square().sum(); } // This function is only used inside the test // AugmentedLagrangianNonsmooth.EqualityConstraints. Next time we are making // any changes here, consider moving it to live directly within that test case // as a templated lambda. template void CheckAugmentedLagrangianEqualityConstraint( const MathematicalProgram& prog, const Binding& constraint, const Vector3& x_val, const Eigen::Vector4d& lambda_val, double mu_val, double tol_val, bool smooth_flag) { VectorX constraint_residue; T cost; for (bool include_x_bounds : {false, true}) { T al_val; if (smooth_flag) { const AugmentedLagrangianSmooth dut(&prog, include_x_bounds); EXPECT_EQ(dut.lagrangian_size(), 4); EXPECT_EQ(dut.is_equality(), std::vector({true, true, true, true})); EXPECT_EQ(dut.s_size(), 0); al_val = dut.Eval(x_val, VectorX(0), lambda_val, mu_val, &constraint_residue, &cost); } else { const AugmentedLagrangianNonsmooth dut(&prog, include_x_bounds); EXPECT_EQ(dut.lagrangian_size(), 4); EXPECT_EQ(dut.is_equality(), std::vector({true, true, true, true})); al_val = dut.Eval(x_val, lambda_val, mu_val, &constraint_residue, &cost); } const auto constraint_val = prog.EvalBinding(constraint, x_val); const auto& constraint_bound = constraint.evaluator()->lower_bound(); const T al_expected = al_equality(constraint_val - constraint_bound, lambda_val, mu_val); EXPECT_EQ(constraint_residue.rows(), lambda_val.rows()); CompareAlResult(al_val, al_expected, constraint_residue, constraint_val - constraint_bound, cost, T{0}, tol_val); } } GTEST_TEST(AugmentedLagrangian, EqualityConstraints) { // Test a program with on equality constraints. MathematicalProgram prog; auto x = prog.NewContinuousVariables<3>(); auto constraint = prog.AddConstraint(std::make_shared(), x.head<2>()); const double tol = 1E-10; for (bool smooth_flag : {false, true}) { CheckAugmentedLagrangianEqualityConstraint( prog, constraint, Eigen::Vector3d(0.5, 1.5, 2), Eigen::Vector4d(1, 3, -2, 4), 0.2, tol, smooth_flag); CheckAugmentedLagrangianEqualityConstraint( prog, constraint, Eigen::Vector3d(-0.5, 2.5, 2), Eigen::Vector4d(-1, 2, 3, -2), 0.6, tol, smooth_flag); CheckAugmentedLagrangianEqualityConstraint( prog, constraint, math::InitializeAutoDiff(Eigen::Vector3d(0.5, 0.3, 0.2)), Eigen::Vector4d(1, 2, -3, -1), 0.5, tol, smooth_flag); } } // Refer to equation 17.55 of Numerical Optimization by Jorge Nocedal and // Stephen Wright, Edition 1, 1999 (This equation is not presented in Edition // 2). We use the alternative way to compute s first, and then compute psi. Note // that what we use for mu here is 1/μ in equation 17.55. template T psi(const T& c, double lambda, double mu) { T s = c - lambda / mu; if (ExtractDoubleOrThrow(s) < 0) { s = T(0); } return -lambda * (c - s) + mu / 2 * (c - s) * (c - s); } template void CheckAugmentedLagrangianNonsmoothInequalityConstraint( const MathematicalProgram& prog, const Binding& constraint, const Vector3& x_val, const Eigen::Matrix& lambda_val, double mu_val, double tol_val) { VectorX constraint_residue; T cost; for (const bool include_x_bounds : {false, true}) { const AugmentedLagrangianNonsmooth dut(&prog, include_x_bounds); EXPECT_EQ(dut.lagrangian_size(), 5); EXPECT_EQ(dut.is_equality(), std::vector({true, false, false, false, false})); const T al = dut.Eval(x_val, lambda_val, mu_val, &constraint_residue, &cost); const auto constraint_val = prog.EvalBinding(constraint, x_val); const auto& lb = constraint.evaluator()->lower_bound(); const auto& ub = constraint.evaluator()->upper_bound(); using std::pow; T al_expected = -lambda_val(0) * (constraint_val(0) - lb(0)) + mu_val / 2 * pow(constraint_val(0) - lb(0), 2) + psi(constraint_val(1) - lb(1), lambda_val(1), mu_val) + psi(ub(1) - constraint_val(1), lambda_val(2), mu_val) + psi(ub(2) - constraint_val(2), lambda_val(3), mu_val) + psi(constraint_val(3) - lb(3), lambda_val(4), mu_val); EXPECT_EQ(constraint_residue.rows(), lambda_val.rows()); VectorX constraint_residue_expected(constraint_residue.rows()); constraint_residue_expected << constraint_val(0) - lb(0), constraint_val(1) - lb(1), ub(1) - constraint_val(1), ub(2) - constraint_val(2), constraint_val(3) - lb(3); CompareAlResult(al, al_expected, constraint_residue, constraint_residue_expected, cost, T(0), tol_val); } } template void CheckAugmentedLagrangianSmoothInequalityConstraint( const MathematicalProgram& prog, const Binding& constraint, const Vector3& x_val, const Vector4& s_val, const Eigen::Matrix& lambda_val, double mu_val, double tol_val) { VectorX constraint_residue; T cost; for (const bool include_x_bounds : {false, true}) { const AugmentedLagrangianSmooth dut(&prog, include_x_bounds); EXPECT_EQ(dut.lagrangian_size(), 5); EXPECT_EQ(dut.s_size(), 4); EXPECT_EQ(dut.is_equality(), std::vector({true, false, false, false, false})); const T al = dut.Eval(x_val, s_val, lambda_val, mu_val, &constraint_residue, &cost); const auto constraint_val = prog.EvalBinding(constraint, x_val); const auto& lb = constraint.evaluator()->lower_bound(); const auto& ub = constraint.evaluator()->upper_bound(); using std::pow; Eigen::Matrix lhs; lhs << constraint_val(0) - lb(0), constraint_val(1) - lb(1) - s_val(0), ub(1) - constraint_val(1) - s_val(1), ub(2) - constraint_val(2) - s_val(2), constraint_val(3) - lb(3) - s_val(3); T al_expected = al_equality(lhs, lambda_val, mu_val); EXPECT_EQ(constraint_residue.rows(), lambda_val.rows()); VectorX constraint_residue_expected = lhs; CompareAlResult(al, al_expected, constraint_residue, constraint_residue_expected, cost, T(0), tol_val); } } GTEST_TEST(AugmentedLagrangian, InequalityConstraint) { // Test with inequality constraints. MathematicalProgram prog; auto x = prog.NewContinuousVariables<3>(); auto constraint_evaluator = std::make_shared(); constraint_evaluator->set_bounds(Eigen::Vector4d(1, -1, -kInf, 2), Eigen::Vector4d(1, 2, 3, kInf)); auto constraint = prog.AddConstraint(constraint_evaluator, x.tail<2>()); const double tol = 1E-10; CheckAugmentedLagrangianNonsmoothInequalityConstraint( prog, constraint, Eigen::Vector3d(1, 3, 2), (Eigen::Matrix() << 0.5, 0.4, 1.5, 0.2, 3).finished(), 0.5, tol); CheckAugmentedLagrangianNonsmoothInequalityConstraint( prog, constraint, Eigen::Vector3d(-1, 3, -5), (Eigen::Matrix() << -.5, 1.4, 1.5, 0.5, 3).finished(), 0.2, tol); CheckAugmentedLagrangianNonsmoothInequalityConstraint( prog, constraint, math::InitializeAutoDiff(Eigen::Vector3d(-1, 3, -5)), (Eigen::Matrix() << -.5, 1.4, 1.5, 0.5, 3).finished(), 0.2, tol); CheckAugmentedLagrangianSmoothInequalityConstraint( prog, constraint, Eigen::Vector3d(1, 3, 2), Eigen::Vector4d(3, -1, -2, 1), (Eigen::Matrix() << 0.5, 0.4, 1.5, 0.2, 3).finished(), 0.5, tol); CheckAugmentedLagrangianSmoothInequalityConstraint( prog, constraint, math::InitializeAutoDiff(Eigen::Vector3d(1, 3, 2)), Vector4(AutoDiffXd(3), AutoDiffXd(-1), AutoDiffXd(-2), AutoDiffXd(1)), (Eigen::Matrix() << 0.5, 0.4, 1.5, 0.2, 3).finished(), 0.5, tol); } template void CheckAugmentedLagrangianNonsmoothBoundingBoxConstraint( const MathematicalProgram& prog, const Vector4& x_val, const Eigen::Matrix& lambda, double mu, double tol_val) { const AugmentedLagrangianNonsmooth dut(&prog, true); EXPECT_EQ(dut.lagrangian_size(), 5); EXPECT_EQ(dut.is_equality(), std::vector({true, false, false, false, false})); VectorX constraint_residue; T cost; const T al = dut.Eval(x_val, lambda, mu, &constraint_residue, &cost); EXPECT_EQ(constraint_residue.rows(), lambda.rows()); Eigen::VectorXd x_lb, x_ub; AggregateBoundingBoxConstraints(prog, &x_lb, &x_ub); const T al_expected = -lambda(0) * (x_val(0) - x_lb(0)) + mu / 2 * (x_val(0) - x_lb(0)) * (x_val(0) - x_lb(0)) + psi(x_val(1) - x_lb(1), lambda(1), mu) + psi(x_ub(2) - x_val(2), lambda(2), mu) + psi(x_val(3) - x_lb(3), lambda(3), mu) + psi(x_ub(3) - x_val(3), lambda(4), mu); Eigen::Matrix constraint_residue_expected; constraint_residue_expected << x_val(0) - x_lb(0), x_val(1) - x_lb(1), x_ub(2) - x_val(2), x_val(3) - x_lb(3), x_ub(3) - x_val(3); CompareAlResult(al, al_expected, constraint_residue, constraint_residue_expected, cost, T(0), tol_val); Eigen::VectorXd x_lo_expected, x_up_expected; AggregateBoundingBoxConstraints(prog, &x_lo_expected, &x_up_expected); EXPECT_TRUE(CompareMatrices(dut.x_lo(), x_lo_expected)); EXPECT_TRUE(CompareMatrices(dut.x_up(), x_up_expected)); } template void CheckAugmentedLagrangianSmoothBoundingBoxConstraint( const MathematicalProgram& prog, const Vector4& x_val, const Vector4& s_val, const Eigen::Matrix& lambda, double mu, double tol_val) { const AugmentedLagrangianSmooth dut(&prog, true); EXPECT_EQ(dut.lagrangian_size(), 5); EXPECT_EQ(dut.s_size(), 4); EXPECT_EQ(dut.is_equality(), std::vector({true, false, false, false, false})); VectorX constraint_residue; T cost; const T al = dut.Eval(x_val, s_val, lambda, mu, &constraint_residue, &cost); EXPECT_EQ(constraint_residue.rows(), lambda.rows()); Eigen::VectorXd x_lb, x_ub; AggregateBoundingBoxConstraints(prog, &x_lb, &x_ub); Eigen::Matrix lhs; lhs << x_val(0) - x_lb(0), x_val(1) - x_lb(1) - s_val(0), x_ub(2) - x_val(2) - s_val(1), x_val(3) - x_lb(3) - s_val(2), x_ub(3) - x_val(3) - s_val(3); const T al_expected = al_equality(lhs, lambda, mu); Eigen::Matrix constraint_residue_expected = lhs; CompareAlResult(al, al_expected, constraint_residue, constraint_residue_expected, cost, T(0), tol_val); Eigen::VectorXd x_lo_expected, x_up_expected; AggregateBoundingBoxConstraints(prog, &x_lo_expected, &x_up_expected); EXPECT_TRUE(CompareMatrices(dut.x_lo(), x_lo_expected)); EXPECT_TRUE(CompareMatrices(dut.x_up(), x_up_expected)); } // This function should only be called inside // EvalAugmentedLagrangian.BoundingBoxConstraint when include_x_bounds=false. template void CheckBoundingBoxConstraintEmpty(const AL& dut) { DRAKE_DEMAND(dut.include_x_bounds() == false); EXPECT_EQ(dut.lagrangian_size(), 0); EXPECT_TRUE(dut.is_equality().empty()); VectorX constraint_residue; double cost; double al_val; if constexpr (std::is_same_v) { al_val = dut.template Eval(Eigen::Vector4d(1, 2, 3, 4), Eigen::VectorXd(0), 0.5, &constraint_residue, &cost); } else { al_val = dut.template Eval(Eigen::Vector4d(1, 2, 3, 4), Eigen::VectorXd(0), Eigen::VectorXd(0), 0.5, &constraint_residue, &cost); EXPECT_EQ(dut.s_size(), 0); } EXPECT_EQ(al_val, 0); EXPECT_EQ(constraint_residue.rows(), 0); Eigen::VectorXd x_lo_expected, x_up_expected; AggregateBoundingBoxConstraints(dut.prog(), &x_lo_expected, &x_up_expected); EXPECT_TRUE(CompareMatrices(dut.x_lo(), x_lo_expected)); EXPECT_TRUE(CompareMatrices(dut.x_up(), x_up_expected)); } GTEST_TEST(EvalAugmentedLagrangian, BoundingBoxConstraint) { // Test with bounding box constraint. MathematicalProgram prog; auto x = prog.NewContinuousVariables<4>(); auto constraint1 = prog.AddBoundingBoxConstraint( Eigen::Vector4d(1, 3, -kInf, 0), Eigen::Vector4d(2, kInf, -1, 5), x); auto constraint2 = prog.AddBoundingBoxConstraint( Eigen::Vector4d(2, 1, -kInf, 1), Eigen::Vector4d(5, kInf, -1, 3), x); const double tol = 1E-10; CheckAugmentedLagrangianNonsmoothBoundingBoxConstraint( prog, Eigen::Vector4d(1, 3, 5, 2), (Eigen::Matrix() << 0.5, 0.3, 1.5, 2, 1).finished(), 0.5, tol); CheckAugmentedLagrangianNonsmoothBoundingBoxConstraint( prog, math::InitializeAutoDiff(Eigen::Vector4d(1, 3, 5, 2)), (Eigen::Matrix() << 0.5, 0.3, 1.5, 2, 1).finished(), 0.5, tol); CheckAugmentedLagrangianSmoothBoundingBoxConstraint( prog, Eigen::Vector4d(1, 3, 5, 2), Eigen::Vector4d(2, 1, -2, 3), (Eigen::Matrix() << 0.5, 0.3, 1.5, 2, 1).finished(), 0.5, tol); CheckAugmentedLagrangianSmoothBoundingBoxConstraint( prog, math::InitializeAutoDiff(Eigen::Vector4d(1, 3, 5, 2)), math::InitializeAutoDiff(Eigen::Vector4d(2, 1, -2, 3)), (Eigen::Matrix() << 0.5, 0.3, 1.5, 2, 1).finished(), 0.5, tol); // Test include_x_bounds = false, the augmented lagrangian is 0. const AugmentedLagrangianNonsmooth dut_nonsmooth(&prog, false); CheckBoundingBoxConstraintEmpty(dut_nonsmooth); const AugmentedLagrangianSmooth dut_smooth(&prog, false); CheckBoundingBoxConstraintEmpty(dut_smooth); } } // namespace } // namespace solvers } // namespace drake