#include "drake/solvers/mixed_integer_optimization_util.h" #include #include "drake/common/test_utilities/eigen_matrix_compare.h" #include "drake/math/gray_code.h" #include "drake/solvers/gurobi_solver.h" namespace drake { namespace solvers { namespace { GTEST_TEST(TestMixedIntegerUtil, TestCeilLog2) { // Check that CeilLog2(j) returns i + 1 for all j = 2ⁱ + 1, 2ⁱ + 2, ... , 2ⁱ⁺¹ const int kMaxExponent = 15; EXPECT_EQ(0, CeilLog2(1)); for (int i = 0; i < kMaxExponent; ++i) { for (int j = (1 << i) + 1; j <= (1 << (i + 1)); ++j) { EXPECT_EQ(i + 1, CeilLog2(j)); } } } GTEST_TEST(TestLogarithmicSos2, TestAddSos2) { MathematicalProgram prog; auto lambda1 = prog.NewContinuousVariables(3, "lambda1"); auto y1 = AddLogarithmicSos2Constraint(&prog, lambda1.cast()); static_assert(std::is_same_v, "y1 should be a dynamic-sized vector."); auto lambda2 = prog.NewContinuousVariables<3>("lambda2"); auto y2 = AddLogarithmicSos2Constraint(&prog, lambda2.cast()); static_assert(std::is_same_v>, "y2 should be a static-sized vector."); } void LogarithmicSos2Test(int num_lambda, bool logarithmic_binning) { // Solve the program // min λᵀ * λ // s.t sum λ = 1 // λ in sos2 // We loop over i such that only λ(i) and λ(i+1) can be strictly positive. // The optimal cost is λ(i) = λ(i + 1) = 0.5. MathematicalProgram prog; auto lambda = prog.NewContinuousVariables(num_lambda, "lambda"); prog.AddCost(lambda.cast().dot(lambda)); VectorXDecisionVariable y; if (logarithmic_binning) { y = AddLogarithmicSos2Constraint(&prog, lambda.cast()); } else { y = prog.NewBinaryVariables(num_lambda - 1); AddSos2Constraint(&prog, lambda.cast(), y.cast()); } int num_binary_vars = y.rows(); int num_intervals = num_lambda - 1; auto y_assignment = prog.AddBoundingBoxConstraint(0, 1, y); // If we use logarithmic binning, we will assign the binary variables y with // value i, expressed in Gray code. const auto gray_codes = math::CalculateReflectedGrayCodes(num_binary_vars); Eigen::VectorXd y_val(y.rows()); for (int i = 0; i < num_intervals; ++i) { y_val.setZero(); if (logarithmic_binning) { for (int j = 0; j < num_binary_vars; ++j) { y_val(j) = gray_codes(i, j); } } else { y_val(i) = 1; } y_assignment.evaluator()->UpdateLowerBound(y_val); y_assignment.evaluator()->UpdateUpperBound(y_val); GurobiSolver gurobi_solver; if (gurobi_solver.available()) { auto result = gurobi_solver.Solve(prog, {}, {}); EXPECT_TRUE(result.is_success()); const auto lambda_val = result.GetSolution(lambda); Eigen::VectorXd lambda_val_expected = Eigen::VectorXd::Zero(num_lambda); lambda_val_expected(i) = 0.5; lambda_val_expected(i + 1) = 0.5; EXPECT_TRUE(CompareMatrices(lambda_val, lambda_val_expected, 1E-5, MatrixCompareType::absolute)); } } } GTEST_TEST(TestSos2, TestClosestPointOnLineSegments) { // We will define line segments A₀A₁, ..., A₅A₆ in 2D, where points Aᵢ are // defined as A₀ = (0, 0), A₁ = (1, 1), A₂ = (2, 0), A₃ = (4, 2), A₅ = (6, 0), // A₅ = (7, 1), A₆ = (8, 0). We compute the closest point P = (x, y) on the // line segments to a given point Q. MathematicalProgram prog; auto x = prog.NewContinuousVariables<1>()(0); auto y = prog.NewContinuousVariables<1>()(0); Eigen::Matrix A; // clang-format off A << 0, 1, 2, 4, 6, 7, 8, 0, 1, 0, 2, 0, 1, 0; // clang-format on auto lambda = prog.NewContinuousVariables<7>(); auto z = prog.NewBinaryVariables<6>(); AddSos2Constraint(&prog, lambda.cast(), z.cast()); const Vector2 line_segment = A * lambda; prog.AddLinearConstraint(line_segment(0) == x); prog.AddLinearConstraint(line_segment(1) == y); // Add a dummy cost function, which we will change in the for loop below. Binding cost = prog.AddQuadraticCost(Eigen::Matrix2d::Zero(), Eigen::Vector2d::Zero(), 0, VectorDecisionVariable<2>(x, y)); // We will test with different points Qs, each Q corresponds to a nearest // point P on the line segments. std::vector> Q_and_P; Q_and_P.push_back( std::make_pair(Eigen::Vector2d(1, 1), Eigen::Vector2d(1, 1))); Q_and_P.push_back( std::make_pair(Eigen::Vector2d(1.9, 1), Eigen::Vector2d(1.45, 0.55))); Q_and_P.push_back( std::make_pair(Eigen::Vector2d(3, 1), Eigen::Vector2d(3, 1))); Q_and_P.push_back( std::make_pair(Eigen::Vector2d(5, 1.2), Eigen::Vector2d(4.9, 1.1))); Q_and_P.push_back( std::make_pair(Eigen::Vector2d(7.5, 1.2), Eigen::Vector2d(7.15, 0.85))); for (const auto& QP_pair : Q_and_P) { const Eigen::Vector2d& Q = QP_pair.first; // The cost is |P-Q|² cost.evaluator()->UpdateCoefficients(2 * Eigen::Matrix2d::Identity(), -2 * Q, Q.squaredNorm()); // Any mixed integer convex solver can solve this problem, here we choose // gurobi. GurobiSolver gurobi_solver; if (gurobi_solver.available()) { auto result = gurobi_solver.Solve(prog, {}, {}); EXPECT_TRUE(result.is_success()); const Eigen::Vector2d P( result.GetSolution(VectorDecisionVariable<2>(x, y))); const Eigen::Vector2d P_expected = QP_pair.second; EXPECT_TRUE(CompareMatrices(P, P_expected, 1E-6)); EXPECT_NEAR(result.get_optimal_cost(), (Q - P_expected).squaredNorm(), 1E-12); } } } GTEST_TEST(TestLogarithmicSos2, Test4Lambda) { LogarithmicSos2Test(4, true); } GTEST_TEST(TestLogarithmicSos2, Test5Lambda) { LogarithmicSos2Test(5, true); } GTEST_TEST(TestLogarithmicSos2, Test6Lambda) { LogarithmicSos2Test(6, true); } GTEST_TEST(TestLogarithmicSos2, Test7Lambda) { LogarithmicSos2Test(7, true); } GTEST_TEST(TestLogarithmicSos2, Test8Lambda) { LogarithmicSos2Test(8, true); } GTEST_TEST(TestSos2, Test4Lambda) { LogarithmicSos2Test(4, false); } GTEST_TEST(TestSos2, Test5Lambda) { LogarithmicSos2Test(5, false); } GTEST_TEST(TestSos2, Test6Lambda) { LogarithmicSos2Test(6, false); } GTEST_TEST(TestSos2, Test7Lambda) { LogarithmicSos2Test(7, false); } GTEST_TEST(TestSos2, Test8Lambda) { LogarithmicSos2Test(8, false); } void LogarithmicSos1Test(int num_lambda, const Eigen::Ref& codes) { // Check if we impose the constraint // λ is in sos1 // and assign values to the binary variables, // whether the corresponding λ(i) is 1. MathematicalProgram prog; auto lambda = prog.NewContinuousVariables(num_lambda); int num_digits = CeilLog2(num_lambda); auto y = prog.NewBinaryVariables(num_digits); AddLogarithmicSos1Constraint(&prog, lambda.cast(), y, codes); auto binary_assignment = prog.AddBoundingBoxConstraint(0, 0, y); for (int i = 0; i < num_lambda; ++i) { Eigen::VectorXd code = codes.row(i).cast().transpose(); binary_assignment.evaluator()->UpdateLowerBound(code); binary_assignment.evaluator()->UpdateUpperBound(code); GurobiSolver gurobi_solver; if (gurobi_solver.available()) { auto result = gurobi_solver.Solve(prog, {}, {}); EXPECT_TRUE(result.is_success()); Eigen::VectorXd lambda_expected(num_lambda); lambda_expected.setZero(); lambda_expected(i) = 1; EXPECT_TRUE(CompareMatrices(result.GetSolution(lambda), lambda_expected, 1E-6, MatrixCompareType::absolute)); } } } GTEST_TEST(TestLogarithmicSos1, Test2Lambda) { Eigen::Matrix codes; codes << 0, 1; LogarithmicSos1Test(2, codes); // Test a different codes codes << 1, 0; LogarithmicSos1Test(2, codes); } GTEST_TEST(TestLogarithmicSos1, Test3Lambda) { Eigen::Matrix codes; // clang-format off codes << 0, 0, 0, 1, 1, 0; // clang-format on LogarithmicSos1Test(3, codes); // Test a different codes // clang-format off codes << 0, 0, 0, 1, 1, 1; // clang-format on LogarithmicSos1Test(3, codes); } GTEST_TEST(TestLogarithmicSos1, Test4Lambda) { Eigen::Matrix codes; // clang-format off codes << 0, 0, 0, 1, 1, 0, 1, 1; // clang-format on LogarithmicSos1Test(4, codes); // Test a different codes // clang-format off codes << 0, 0, 0, 1, 1, 1, 1, 0; // clang-format on LogarithmicSos1Test(4, codes); } GTEST_TEST(TestLogarithmicSos1, Test5Lambda) { auto codes = math::CalculateReflectedGrayCodes<3>(); LogarithmicSos1Test(5, codes.topRows<5>()); } GTEST_TEST(TestLogarithmicSos1, Test) { MathematicalProgram prog; VectorX y, lambda; std::tie(lambda, y) = AddLogarithmicSos1Constraint(&prog, 3); EXPECT_EQ(lambda.rows(), 3); EXPECT_EQ(y.rows(), 2); auto check = [&prog, &y, &lambda](const Eigen::VectorXd& lambda_val, const Eigen::VectorXd& y_val, bool satisfied_expected) { Eigen::VectorXd x_val = Eigen::VectorXd::Zero(prog.num_vars()); prog.SetDecisionVariableValueInVector(y, y_val, &x_val); prog.SetDecisionVariableValueInVector(lambda, lambda_val, &x_val); bool satisfied = true; for (const auto& binding : prog.GetAllConstraints()) { satisfied = satisfied && binding.evaluator()->CheckSatisfied( prog.GetBindingVariableValues(binding, x_val)); } EXPECT_EQ(satisfied, satisfied_expected); }; check(Eigen::Vector3d(0, 0, 1), Eigen::Vector2d(1, 1), true); check(Eigen::Vector3d(1, 0, 0), Eigen::Vector2d(1, 0), false); check(Eigen::Vector3d(0, 1, 0), Eigen::Vector2d(0, 1), true); check(Eigen::Vector3d(0, 0.5, 0.5), Eigen::Vector2d(1, 1), false); check(Eigen::Vector3d(0, 0.1, 1), Eigen::Vector2d(1, 0), false); } GTEST_TEST(TestBilinearProductMcCormickEnvelopeSos2, AddConstraint) { // Test if the return argument from // AddBilinearProductMcCormickEnvelopeSos2 has the right type. MathematicalProgram prog; auto x = prog.NewContinuousVariables<1>()(0); auto y = prog.NewContinuousVariables<1>()(0); auto w = prog.NewContinuousVariables<1>()(0); const Eigen::Vector3d phi_x_static(0, 1, 2); Eigen::VectorXd phi_x_dynamic = Eigen::VectorXd::LinSpaced(3, 0, 2); const Eigen::Vector4d phi_y_static = Eigen::Vector4d::LinSpaced(0, 3); Eigen::VectorXd phi_y_dynamic = Eigen::VectorXd::LinSpaced(4, 0, 3); Vector1 Bx = prog.NewBinaryVariables<1>(); Vector2 By = prog.NewBinaryVariables<2>(); auto lambda1 = AddBilinearProductMcCormickEnvelopeSos2( &prog, x, y, w, phi_x_static, phi_y_static, Bx, By, IntervalBinning::kLogarithmic); static_assert(std::is_same_v>, "lambda should be a static matrix"); auto lambda2 = AddBilinearProductMcCormickEnvelopeSos2( &prog, x, y, w, phi_x_dynamic, phi_y_static, Bx, By, IntervalBinning::kLogarithmic); static_assert(std::is_same_v>, "lambda's type is incorrect"); auto lambda3 = AddBilinearProductMcCormickEnvelopeSos2( &prog, x, y, w, phi_x_static, phi_y_dynamic, Bx, By, IntervalBinning::kLogarithmic); static_assert(std::is_same_v>, "lambda's type is incorrect"); auto lambda4 = AddBilinearProductMcCormickEnvelopeSos2( &prog, x, y, w, phi_x_dynamic, phi_y_dynamic, Bx, By, IntervalBinning::kLogarithmic); static_assert( std::is_same_v>, "lambda's type is incorrect"); } class BilinearProductMcCormickEnvelopeTest { public: DRAKE_NO_COPY_NO_MOVE_NO_ASSIGN(BilinearProductMcCormickEnvelopeTest) BilinearProductMcCormickEnvelopeTest(int num_interval_x, int num_interval_y) : prog_{}, prog_result_{}, num_interval_x_{num_interval_x}, num_interval_y_{num_interval_y}, w_{prog_.NewContinuousVariables<1>()(0)}, x_{prog_.NewContinuousVariables<1>()(0)}, y_{prog_.NewContinuousVariables<1>()(0)}, phi_x_{Eigen::VectorXd::LinSpaced(num_interval_x_ + 1, 0, 1)}, phi_y_{Eigen::VectorXd::LinSpaced(num_interval_y_ + 1, 0, 1)} {} virtual ~BilinearProductMcCormickEnvelopeTest() = default; virtual Eigen::VectorXd SetBinaryValue(int active_interval, int num_interval) const = 0; Eigen::VectorXd SetBinaryValueLinearBinning(int active_interval, int num_interval) const { Eigen::VectorXd b = Eigen::VectorXd::Zero(num_interval); b(active_interval) = 1; return b; } void TestFeasiblePoint() { auto x_constraint = prog_.AddBoundingBoxConstraint(0, 0, x_); auto y_constraint = prog_.AddBoundingBoxConstraint(0, 0, y_); auto w_constraint = prog_.AddBoundingBoxConstraint(0, 0, w_); auto CheckFeasibility = [&x_constraint, &y_constraint, &w_constraint]( MathematicalProgram* prog, double x_val, double y_val, double w_val, bool is_feasible) { auto UpdateBound = [](Binding* constraint, double val) { constraint->evaluator()->UpdateLowerBound(Vector1d(val)); constraint->evaluator()->UpdateUpperBound(Vector1d(val)); }; UpdateBound(&x_constraint, x_val); UpdateBound(&y_constraint, y_val); UpdateBound(&w_constraint, w_val); prog->SetSolverOption(GurobiSolver::id(), "DualReductions", 0); GurobiSolver solver; if (solver.available()) { MathematicalProgramResult result; solver.Solve(*prog, {}, {}, &result); EXPECT_EQ(result.get_solution_result(), is_feasible ? SolutionResult::kSolutionFound : SolutionResult::kInfeasibleConstraints); } }; // Feasible points CheckFeasibility(&prog_, 0, 0, 0, true); CheckFeasibility(&prog_, 0, 1, 0, true); CheckFeasibility(&prog_, 1, 0, 0, true); CheckFeasibility(&prog_, 1, 1, 1, true); CheckFeasibility(&prog_, 0.5, 1, 0.5, true); CheckFeasibility(&prog_, 1, 0.5, 0.5, true); CheckFeasibility(&prog_, 0.5, 0.5, 0.25, true); if (num_interval_x_ == 2 && num_interval_y_ == 2) { CheckFeasibility(&prog_, 0.5, 0.5, 0.26, false); CheckFeasibility(&prog_, 0.25, 0.25, 0.1, true); CheckFeasibility(&prog_, 0.25, 0.25, 0.126, false); CheckFeasibility(&prog_, 0.5, 0.6, 0.301, false); } if (num_interval_x_ == 3 && num_interval_y_ == 3) { CheckFeasibility(&prog_, 1.0 / 3, 1.0 / 3, 0.1, false); CheckFeasibility(&prog_, 0.5, 0.5, 0.26, true); CheckFeasibility(&prog_, 1.0 / 3, 2.0 / 3, 2.0 / 9, true); CheckFeasibility(&prog_, 0.5, 0.5, 5.0 / 18, true); CheckFeasibility(&prog_, 0.5, 0.5, 5.0 / 18 + 0.001, false); } } void TestLinearObjective() { // We will assign the binary variables Bx_ and By_ to determine which // interval is active. If we use logarithmic binning, then Bx_ and By_ take // values in the gray code, representing integer i and j, such that x is // constrained in [φx(i), φx(i+1)], y is constrained in [φy(j), φy(j+1)]. auto Bx_constraint = prog_.AddBoundingBoxConstraint(Eigen::VectorXd::Zero(Bx_.rows()), Eigen::VectorXd::Zero(Bx_.rows()), Bx_); auto By_constraint = prog_.AddBoundingBoxConstraint(Eigen::VectorXd::Zero(By_.rows()), Eigen::VectorXd::Zero(By_.rows()), By_); VectorDecisionVariable<3> xyw{x_, y_, w_}; auto cost = prog_.AddLinearCost(Eigen::Vector3d::Zero(), xyw); Eigen::Matrix a; // clang-format off a << 1, 1, 1, 1, -1, -1, -1, -1, 1, 1, -1, -1, 1, 1, -1, -1, 1, -1, 1, -1, 1, -1, 1, -1; // clang-format on for (int i = 0; i < num_interval_x_; ++i) { const auto Bx_val = SetBinaryValue(i, num_interval_x_); Bx_constraint.evaluator()->UpdateLowerBound(Bx_val); Bx_constraint.evaluator()->UpdateUpperBound(Bx_val); for (int j = 0; j < num_interval_y_; ++j) { const auto By_val = SetBinaryValue(j, num_interval_y_); By_constraint.evaluator()->UpdateLowerBound(By_val); By_constraint.evaluator()->UpdateUpperBound(By_val); // vertices.col(l) is the l'th vertex of the tetrahedron. Eigen::Matrix vertices; vertices.row(0) << phi_x_(i), phi_x_(i), phi_x_(i + 1), phi_x_(i + 1); vertices.row(1) << phi_y_(j), phi_y_(j + 1), phi_y_(j), phi_y_(j + 1); vertices.row(2) = vertices.row(0).cwiseProduct(vertices.row(1)); for (int k = 0; k < a.cols(); ++k) { cost.evaluator()->UpdateCoefficients(a.col(k)); GurobiSolver gurobi_solver; if (gurobi_solver.available()) { gurobi_solver.Solve(prog_, {}, {}, &prog_result_); EXPECT_TRUE(prog_result_.is_success()); Eigen::Matrix cost_at_vertices = a.col(k).transpose() * vertices; EXPECT_NEAR(prog_result_.get_optimal_cost(), cost_at_vertices.minCoeff(), 1E-4); TestLinearObjectiveCheck(i, j, k); } } } } } virtual void TestLinearObjectiveCheck(int i, int j, int k) const {} protected: MathematicalProgram prog_; MathematicalProgramResult prog_result_; const int num_interval_x_; const int num_interval_y_; const symbolic::Variable w_; const symbolic::Variable x_; const symbolic::Variable y_; const Eigen::VectorXd phi_x_; const Eigen::VectorXd phi_y_; VectorXDecisionVariable Bx_; VectorXDecisionVariable By_; }; class BilinearProductMcCormickEnvelopeSos2Test : public ::testing::TestWithParam>, public BilinearProductMcCormickEnvelopeTest { public: DRAKE_NO_COPY_NO_MOVE_NO_ASSIGN(BilinearProductMcCormickEnvelopeSos2Test) BilinearProductMcCormickEnvelopeSos2Test() : BilinearProductMcCormickEnvelopeTest(std::get<0>(GetParam()), std::get<1>(GetParam())), binning_{std::get<2>(GetParam())}, Bx_size_{binning_ == IntervalBinning::kLogarithmic ? CeilLog2(num_interval_x_) : num_interval_x_}, By_size_{binning_ == IntervalBinning::kLogarithmic ? CeilLog2(num_interval_y_) : num_interval_y_} { Bx_ = prog_.NewBinaryVariables(Bx_size_); By_ = prog_.NewBinaryVariables(By_size_); lambda_ = AddBilinearProductMcCormickEnvelopeSos2( &prog_, x_, y_, w_, phi_x_, phi_y_, Bx_.cast(), By_.cast(), binning_); } Eigen::VectorXd SetBinaryValueLogarithmicBinning(int active_interval, int num_interval) const { const Eigen::MatrixXi gray_codes = math::CalculateReflectedGrayCodes(solvers::CeilLog2(num_interval)); return gray_codes.row(active_interval).cast().transpose(); } Eigen::VectorXd SetBinaryValue(int active_interval, int num_interval) const override { switch (binning_) { case IntervalBinning::kLinear: { return SetBinaryValueLinearBinning(active_interval, num_interval); } case IntervalBinning::kLogarithmic: { return SetBinaryValueLogarithmicBinning(active_interval, num_interval); } default: { throw std::runtime_error( "This default case should not be reached. We add the default case " "due to a gcc-5 pitfall."); } } } void TestLinearObjectiveCheck(int i, int j, int k) const override { // Check that λ has the correct value, except λ(i, j), λ(i, j+1), // λ(i+1, j) and λ(i+1, j+1), all other entries in λ are zero. double w{0}; double x{0}; double y{0}; for (int m = 0; m <= num_interval_x_; ++m) { for (int n = 0; n <= num_interval_y_; ++n) { if (!((m == i && n == j) || (m == i && n == (j + 1)) || (m == (i + 1) && n == j) || (m == (i + 1) && n == (j + 1)))) { EXPECT_NEAR(prog_result_.GetSolution(lambda_(m, n)), 0, 1E-5); } else { double lambda_mn{prog_result_.GetSolution(lambda_(m, n))}; x += lambda_mn * phi_x_(m); y += lambda_mn * phi_y_(n); w += lambda_mn * phi_x_(m) * phi_y_(n); } } } EXPECT_NEAR(prog_result_.GetSolution(x_), x, 1E-4); EXPECT_NEAR(prog_result_.GetSolution(y_), y, 1E-4); EXPECT_NEAR(prog_result_.GetSolution(w_), w, 1E-4); } protected: const IntervalBinning binning_; const int Bx_size_; const int By_size_; MatrixXDecisionVariable lambda_; }; TEST_P(BilinearProductMcCormickEnvelopeSos2Test, LinearObjectiveTest) { TestLinearObjective(); } TEST_P(BilinearProductMcCormickEnvelopeSos2Test, FeasiblePointTest) { TestFeasiblePoint(); } INSTANTIATE_TEST_SUITE_P( TestMixedIntegerUtil, BilinearProductMcCormickEnvelopeSos2Test, ::testing::Combine(::testing::ValuesIn(std::vector{2, 3}), ::testing::ValuesIn(std::vector{2, 3}), ::testing::ValuesIn(std::vector{ IntervalBinning::kLogarithmic, IntervalBinning::kLinear}))); class BilinearProductMcCormickEnvelopeMultipleChoiceTest : public ::testing::TestWithParam>, public BilinearProductMcCormickEnvelopeTest { public: DRAKE_NO_COPY_NO_MOVE_NO_ASSIGN( BilinearProductMcCormickEnvelopeMultipleChoiceTest) BilinearProductMcCormickEnvelopeMultipleChoiceTest() : BilinearProductMcCormickEnvelopeTest(std::get<0>(GetParam()), std::get<1>(GetParam())) { Bx_ = prog_.NewBinaryVariables(num_interval_x_); By_ = prog_.NewBinaryVariables(num_interval_y_); AddBilinearProductMcCormickEnvelopeMultipleChoice( &prog_, x_, y_, w_, phi_x_, phi_y_, Bx_.cast(), By_.cast()); } Eigen::VectorXd SetBinaryValue(int active_interval, int num_interval) const override { return SetBinaryValueLinearBinning(active_interval, num_interval); } }; TEST_P(BilinearProductMcCormickEnvelopeMultipleChoiceTest, LinearObjectiveTest) { TestLinearObjective(); } TEST_P(BilinearProductMcCormickEnvelopeMultipleChoiceTest, FeasiblePointTest) { TestFeasiblePoint(); } INSTANTIATE_TEST_SUITE_P( TestMixedIntegerUtil, BilinearProductMcCormickEnvelopeMultipleChoiceTest, ::testing::Combine(::testing::ValuesIn(std::vector{2, 3}), ::testing::ValuesIn(std::vector{2, 3}))); } // namespace } // namespace solvers } // namespace drake