23#include "absl/status/status.h"
24#include "gtest/gtest.h"
36using ::testing::AnyOf;
37using ::testing::HasSubstr;
38using ::testing::status::IsOkAndHolds;
39using ::testing::status::StatusIs;
42 "This test is disabled as the solver does not support quadratic objectives";
45 "This test is disabled as the solver does not support non-diagonal "
46 "quadratic objectives";
48constexpr double kInf = std::numeric_limits<double>::infinity();
54 const bool supports_incrementalism_not_modifying_qp,
55 const bool supports_qp_incrementalism,
const bool use_integer_variables)
56 : solver_type(solver_type),
58 qp_support(qp_support),
59 supports_incrementalism_not_modifying_qp(
60 supports_incrementalism_not_modifying_qp),
61 supports_qp_incrementalism(supports_qp_incrementalism),
62 use_integer_variables(use_integer_variables) {}
67 return "No QP support";
69 return "Diagonal QP only";
73 LOG(FATAL) <<
"Invalid QpSupportType";
81 <<
", supports_incrementalism_not_modifying_qp: "
83 <<
", supports_qp_incrementalism: "
85 <<
", use_integer_variables: "
92TEST_P(SimpleQpTest, CanBuildQpModel) {
95 model.AddVariable(0, 1, GetParam().use_integer_variables,
"x");
96 model.Minimize(
x *
x - 0.5 *
x + 0.0625);
102 GetParam().use_integer_variables ? 0.0625 : 0.0)));
105 StatusIs(AnyOf(absl::StatusCode::kInvalidArgument,
106 absl::StatusCode::kUnimplemented),
107 HasSubstr(
"quadratic objective")));
120struct UnivariateQpProblem {
121 explicit UnivariateQpProblem(
bool use_integer_variables)
122 :
model(),
x(
model.AddVariable(0, 1, use_integer_variables,
"x")) {
123 model.Minimize(x * x - 0.5 * x + 0.0625);
149struct SimplexConstrainedQpProblem {
150 explicit SimplexConstrainedQpProblem(
bool use_integer_variables)
152 x(
model.AddVariable(0, 1, use_integer_variables,
"x")),
153 y(
model.AddVariable(0, 1, use_integer_variables,
"y")) {
154 model.Minimize(
x *
x +
x *
y - 1.2 *
x +
y *
y - 1.8 *
y + 0.84);
155 model.AddLinearConstraint(
x +
y == 1);
163TEST_P(SimpleQpTest, SolveUnivariateQp) {
167 const UnivariateQpProblem qp_problem(GetParam().use_integer_variables);
169 if (GetParam().use_integer_variables) {
178TEST_P(SimpleQpTest, SolveSimplexConstrainedQp) {
183 const SimplexConstrainedQpProblem qp_problem(
184 GetParam().use_integer_variables);
187 if (GetParam().use_integer_variables) {
189 0.04, {{qp_problem.x, 0.0}, {qp_problem.y, 1.0}},
193 0.0, {{qp_problem.x, 0.2}, {qp_problem.y, 0.8}},
198TEST_P(IncrementalQpTest, EmptyUpdate) {
203 UnivariateQpProblem qp_problem(GetParam().use_integer_variables);
208 solver->Solve({.parameters = GetParam().parameters}));
210 IsOptimal(GetParam().use_integer_variables ? 0.0625 : 0.0));
215 solver->SolveWithoutUpdate());
217 if (GetParam().use_integer_variables) {
223 if (GetParam().supports_incrementalism_not_modifying_qp &&
225 EXPECT_EQ(second_result.solve_stats.barrier_iterations, 0);
226 EXPECT_EQ(second_result.solve_stats.simplex_iterations, 0);
227 EXPECT_EQ(second_result.solve_stats.first_order_iterations, 0);
232TEST_P(IncrementalQpTest, LinearToQuadraticUpdate) {
234 UnivariateQpProblem qp_problem(GetParam().use_integer_variables);
235 qp_problem.model.set_objective_coefficient(qp_problem.x, qp_problem.x, 0);
239 solver->Solve({.parameters = GetParam().parameters}));
244 qp_problem.model.set_objective_coefficient(qp_problem.x, qp_problem.x, 1);
255 StatusIs(AnyOf(absl::StatusCode::kInvalidArgument,
256 absl::StatusCode::kUnimplemented),
257 AllOf(HasSubstr(
"quadratic objective"),
259 Not(HasSubstr(
"update failed")),
261 HasSubstr(
"solver re-creation failed"))));
266 IsOkAndHolds(GetParam().supports_incrementalism_not_modifying_qp
270 if (GetParam().use_integer_variables) {
272 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}),
277 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}),
283TEST_P(IncrementalQpTest, ModifyQuadraticObjective) {
288 UnivariateQpProblem qp_problem(GetParam().use_integer_variables);
293 solver->Solve({.parameters = GetParam().parameters}));
295 IsOptimal(GetParam().use_integer_variables ? 0.0625 : 0.0));
300 const Variable
x = qp_problem.x;
301 qp_problem.model.Minimize(
x *
x - 1.5 *
x + 0.5625);
303 if (!GetParam().supports_qp_incrementalism) {
309 solver->SolveWithoutUpdate());
311 if (GetParam().use_integer_variables) {
320TEST_P(IncrementalQpTest, DeleteVariable) {
325 SimplexConstrainedQpProblem qp_problem(GetParam().use_integer_variables);
330 solver->Solve({.parameters = GetParam().parameters}));
332 IsOptimal(GetParam().use_integer_variables ? 0.04 : 0.0));
336 qp_problem.model.DeleteVariable(qp_problem.x);
338 if (!GetParam().supports_qp_incrementalism) {
345 solver->SolveWithoutUpdate());
367TEST_P(QpDualsTest, DiagonalQp1) {
372 const Variable x0 =
model.AddContinuousVariable(1, 2);
373 const Variable
x1 =
model.AddContinuousVariable(-2, 4);
374 const LinearConstraint y0 =
model.AddLinearConstraint(x0 +
x1 <= 1);
375 model.Minimize(2 * x0 * x0 + 0.5 *
x1 *
x1 - x0 -
x1 + 5);
378 const double expected_objective_value = 6.0;
380 {{x0, 1.0}, {
x1, 0.0}}));
383 {{x0, 4.0}, {
x1, 0.0}}));
403TEST_P(QpDualsTest, DiagonalQp2) {
408 const Variable x0 =
model.AddContinuousVariable(0,
kInf);
409 const Variable
x1 =
model.AddContinuousVariable(0,
kInf);
410 const LinearConstraint y0 =
model.AddLinearConstraint(x0 -
x1 == 2);
411 model.Minimize(0.5 * x0 * x0 + 0.5 *
x1 *
x1 - 3 * x0 -
x1);
414 const double expected_objective_value = -5.0;
416 {{x0, 3.0}, {
x1, 1.0}}));
419 {{x0, 0.0}, {
x1, 0.0}}));
443TEST_P(QpDualsTest, DiagonalQp3) {
448 const Variable x0 =
model.AddContinuousVariable(0,
kInf);
449 const Variable
x1 =
model.AddContinuousVariable(0,
kInf);
450 const Variable
x2 =
model.AddContinuousVariable(0,
kInf);
451 const LinearConstraint y0 =
model.AddLinearConstraint(x0 -
x2 == 1);
452 const LinearConstraint
y1 =
model.AddLinearConstraint(2 * x0 == 4);
456 const double expected_objective_value = 2.0;
459 {{x0, 2.0}, {
x1, 0.0}, {
x2, 1.0}}));
462 {{y0, -1.0}, {
y1, 1.0}},
463 {{x0, 0.0}, {
x1, 0.0}, {
x2, 0.0}}));
483TEST_P(QpDualsTest, GeneralQp1) {
488 const Variable x0 =
model.AddContinuousVariable(0,
kInf);
489 const Variable
x1 =
model.AddContinuousVariable(0,
kInf);
490 const LinearConstraint y0 =
model.AddLinearConstraint(x0 + 2 *
x1 >= 2);
491 model.Minimize(x0 * x0 + x0 *
x1 + 3 *
x1 *
x1 - 2 * x0);
494 const double expected_objective_value = -0.2;
496 {{x0, 1.6}, {
x1, 0.2}}));
499 {{x0, 0.0}, {
x1, 0.0}}));
517TEST_P(QpDualsTest, GeneralQp2) {
522 const Variable x0 =
model.AddContinuousVariable(0, 1);
523 const Variable
x1 =
model.AddContinuousVariable(1, 2);
524 const LinearConstraint y0 =
model.AddLinearConstraint(x0 + 2 *
x1 >= 2);
525 model.Minimize(x0 * x0 + x0 *
x1 + 3 *
x1 *
x1 - 2 * x0);
528 const double expected_objective_value = 2.75;
530 {{x0, 0.5}, {
x1, 1}}));
533 {{x0, 0.0}, {
x1, 6.5}}));
const LinearConstraint y1
An object oriented wrapper for quadratic constraints in ModelStorage.
Matcher< SolveResult > IsOptimalWithSolution(const double expected_objective, const VariableMap< double > expected_variable_values, const double tolerance)
EXPECT_THAT(ComputeInfeasibleSubsystem(model, GetParam().solver_type), IsOkAndHolds(IsInfeasible(true, ModelSubset{ .variable_bounds={{x, ModelSubset::Bounds{.lower=false,.upper=true}}},.linear_constraints={ {c, ModelSubset::Bounds{.lower=true,.upper=false}}}})))
constexpr std::string_view kNoQpSupportMessage
TEST_P(InfeasibleSubsystemTest, CanComputeInfeasibleSubsystem)
SolverType
The solvers supported by MathOpt.
<=x<=1 IncrementalMipTest::IncrementalMipTest() :model_("incremental_solve_test"), x_(model_.AddContinuousVariable(0.0, 1.0, "x")), y_(model_.AddIntegerVariable(0.0, 2.0, "y")), c_(model_.AddLinearConstraint(0<=x_+y_<=1.5, "c")) { model_.Maximize(3.0 *x_+2.0 *y_+0.1);solver_=NewIncrementalSolver(&model_, TestedSolver()).value();const SolveResult first_solve=solver_->Solve().value();CHECK(first_solve.has_primal_feasible_solution());CHECK_LE(std::abs(first_solve.objective_value() - 3.6), kTolerance)<< first_solve.objective_value();} namespace { TEST_P(SimpleMipTest, OneVarMax) { Model model;const Variable x=model.AddVariable(0.0, 4.0, false, "x");model.Maximize(2.0 *x);ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(model, GetParam().solver_type));ASSERT_THAT(result, IsOptimal(8.0));EXPECT_THAT(result.variable_values(), IsNear({{x, 4.0}}));} TEST_P(SimpleMipTest, OneVarMin) { Model model;const Variable x=model.AddVariable(-2.4, 4.0, false, "x");model.Minimize(2.0 *x);ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(model, GetParam().solver_type));ASSERT_THAT(result, IsOptimal(-4.8));EXPECT_THAT(result.variable_values(), IsNear({{x, -2.4}}));} TEST_P(SimpleMipTest, OneIntegerVar) { Model model;const Variable x=model.AddVariable(0.0, 4.5, true, "x");model.Maximize(2.0 *x);ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(model, GetParam().solver_type));ASSERT_THAT(result, IsOptimal(8.0));EXPECT_THAT(result.variable_values(), IsNear({{x, 4.0}}));} TEST_P(SimpleMipTest, SimpleLinearConstraint) { Model model;const Variable x=model.AddBinaryVariable("x");const Variable y=model.AddBinaryVariable("y");model.Maximize(2.0 *x+y);model.AddLinearConstraint(0.0<=x+y<=1.5, "c");ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(model, GetParam().solver_type));ASSERT_THAT(result, IsOptimal(2.0));EXPECT_THAT(result.variable_values(), IsNear({{x, 1}, {y, 0}}));} TEST_P(SimpleMipTest, Unbounded) { Model model;const Variable x=model.AddVariable(0.0, kInf, true, "x");model.Maximize(2.0 *x);ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(model, GetParam().solver_type));if(GetParam().report_unboundness_correctly) { ASSERT_THAT(result, TerminatesWithOneOf({TerminationReason::kUnbounded, TerminationReason::kInfeasibleOrUnbounded}));} else { ASSERT_THAT(result, TerminatesWith(TerminationReason::kOtherError));} } TEST_P(SimpleMipTest, Infeasible) { Model model;const Variable x=model.AddVariable(0.0, 3.0, true, "x");model.Maximize(2.0 *x);model.AddLinearConstraint(x >=4.0);ASSERT_OK_AND_ASSIGN(const SolveResult result, Solve(model, GetParam().solver_type));ASSERT_THAT(result, TerminatesWith(TerminationReason::kInfeasible));} TEST_P(SimpleMipTest, FractionalBoundsContainNoInteger) { if(GetParam().solver_type==SolverType::kGurobi) { GTEST_SKIP()<< "TODO(b/272298816): Gurobi bindings are broken here.";} Model model;const Variable x=model.AddIntegerVariable(0.5, 0.6, "x");model.Maximize(x);EXPECT_THAT(Solve(model, GetParam().solver_type), IsOkAndHolds(TerminatesWith(TerminationReason::kInfeasible)));} TEST_P(IncrementalMipTest, EmptyUpdate) { ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));ASSERT_OK_AND_ASSIGN(const SolveResult result, solver_->SolveWithoutUpdate());ASSERT_THAT(result, IsOptimal(3.6));EXPECT_THAT(result.variable_values(), IsNear({{x_, 0.5}, {y_, 1.0}}));} TEST_P(IncrementalMipTest, MakeContinuous) { model_.set_continuous(y_);ASSERT_THAT(solver_->Update(), IsOkAndHolds(DidUpdate()));ASSERT_OK_AND_ASSIGN(const SolveResult result, solver_->SolveWithoutUpdate());ASSERT_THAT(result, IsOptimal(4.1));EXPECT_THAT(result.variable_values(), IsNear({{x_, 1.0}, {y_, 0.5}}));} TEST_P(IncrementalMipTest, DISABLED_MakeContinuousWithNonIntegralBounds) { solver_.reset();Model model("bounds");const Variable x=model.AddIntegerVariable(0.5, 1.5, "x");model.Maximize(x);ASSERT_OK_AND_ASSIGN(const auto solver, NewIncrementalSolver(&model, TestedSolver()));ASSERT_THAT(solver->Solve(), IsOkAndHolds(IsOptimal(1.0)));model.set_continuous(x);ASSERT_THAT(solver->Update(), IsOkAndHolds(DidUpdate()));ASSERT_THAT(solver-> IsOkAndHolds(IsOptimal(1.5)))
ASSERT_THAT(solver->Update(), IsOkAndHolds(DidUpdate()))
std::string ToString(QpSupportType qp_support)
constexpr std::string_view kNoNonDiagonalQpSupportMessage
std::ostream & operator<<(std::ostream &ostr, const IndicatorConstraint &constraint)
absl::StatusOr< std::unique_ptr< IncrementalSolver > > NewIncrementalSolver(Model *model, SolverType solver_type, SolverInitArguments arguments)
Matcher< UpdateResult > DidUpdate()
Actual UpdateResult.did_update is true.
constexpr double kTolerance
Matcher< SolveResult > IsOptimalWithDualSolution(const double expected_objective, const LinearConstraintMap< double > expected_dual_values, const VariableMap< double > expected_reduced_costs, const double tolerance)
Matcher< SolveResult > IsOptimal(const std::optional< double > expected_primal_objective, const double tolerance)
std::string ProtobufShortDebugString(const P &message)
internal::StatusIsMatcher StatusIs(CodeMatcher code_matcher, MessageMatcher message_matcher)
#define ASSERT_OK_AND_ASSIGN(lhs, rexpr)
bool use_integer_variables
True if the solver supports integer variables.
SolveParameters parameters
bool supports_qp_incrementalism
bool supports_incrementalism_not_modifying_qp
QpTestParameters(SolverType solver_type, SolveParameters parameters, QpSupportType qp_support, bool supports_incrementalism_not_modifying_qp, bool supports_qp_incrementalism, bool use_integer_variables)
SolverType solver_type
The tested solver.
SolveParametersProto Proto() const