21#include "absl/status/status.h"
22#include "absl/strings/string_view.h"
23#include "gtest/gtest.h"
33 const bool supports_qc,
const bool supports_incremental_add_and_deletes,
34 const bool supports_incremental_variable_deletions,
35 const bool use_integer_variables)
36 : solver_type(solver_type),
38 supports_qc(supports_qc),
39 supports_incremental_add_and_deletes(
40 supports_incremental_add_and_deletes),
41 supports_incremental_variable_deletions(
42 supports_incremental_variable_deletions),
43 use_integer_variables(use_integer_variables) {}
48 <<
", supports_qc: " << (params.
supports_qc ?
"true" :
"false")
49 <<
", supports_incremental_add_and_deletes: "
51 <<
", supports_incremental_variable_deletions: "
53 <<
", use_integer_variables: "
60using ::testing::AnyOf;
61using ::testing::HasSubstr;
62using ::testing::status::IsOkAndHolds;
63using ::testing::status::StatusIs;
66constexpr absl::string_view no_qc_support_message =
67 "This test is disabled as the solver does not support quadratic "
80struct UnivariateQcProblem {
81 explicit UnivariateQcProblem(
bool use_integer_variables)
82 :
model(),
x(
model.AddVariable(-1.0, 1.0, use_integer_variables,
"x")) {
83 model.AddQuadraticConstraint(
x *
x -
x <= 1.0);
91TEST_P(SimpleQcTest, CanBuildQcModel) {
92 UnivariateQcProblem qc_problem(GetParam().use_integer_variables);
93 if (GetParam().supports_qc) {
99 StatusIs(AnyOf(absl::StatusCode::kInvalidArgument,
100 absl::StatusCode::kUnimplemented),
101 HasSubstr(
"quadratic constraints")));
105TEST_P(SimpleQcTest, SolveSimpleQc) {
106 if (!GetParam().supports_qc) {
107 GTEST_SKIP() << no_qc_support_message;
109 const UnivariateQcProblem qc_problem(GetParam().use_integer_variables);
110 const double x_expected =
111 GetParam().use_integer_variables ? 0.0 : -0.618033988748785;
114 5.0 + x_expected, {{qc_problem.x, x_expected}})));
129struct HalfEllipseProblem {
130 explicit HalfEllipseProblem(
bool use_integer_variables)
132 x(
model.AddVariable(0.0, 0.5, use_integer_variables,
"x")),
133 y(
model.AddVariable(0.0, 1.0, use_integer_variables,
"y")),
134 q(
model.AddQuadraticConstraint(
x *
x +
x *
y +
y *
y - 2 *
x - 2 *
y <=
137 model.AddLinearConstraint(
x <=
y);
143 const QuadraticConstraint q;
146TEST_P(SimpleQcTest, SolveHalfEllipseQc) {
147 if (!GetParam().supports_qc) {
148 GTEST_SKIP() << no_qc_support_message;
150 const HalfEllipseProblem qc_problem(GetParam().use_integer_variables);
151 if (GetParam().use_integer_variables) {
154 1.0, {{qc_problem.x, 0.0}, {qc_problem.y, 1.0}})));
156 const double value = 1.0 / 3.0;
173TEST_P(IncrementalQcTest, LinearToQuadraticUpdate) {
176 model.AddVariable(0.0, 1.0, GetParam().use_integer_variables,
"x");
182 ASSERT_THAT(solver->Solve({.parameters = GetParam().parameters}),
185 model.AddQuadraticConstraint(
x *
x <= 0.5);
187 if (!GetParam().supports_qc) {
196 StatusIs(AnyOf(absl::StatusCode::kInvalidArgument,
197 absl::StatusCode::kUnimplemented),
198 AllOf(HasSubstr(
"quadratic constraint"),
200 Not(HasSubstr(
"update failed")),
202 HasSubstr(
"solver re-creation failed"))));
207 IsOkAndHolds(GetParam().supports_incremental_add_and_deletes
210 const double expected_x =
211 GetParam().use_integer_variables ? 0.0 : std::sqrt(0.5);
213 solver->SolveWithoutUpdate({.parameters = GetParam().parameters}),
231TEST_P(IncrementalQcTest, UpdateDeletesQuadraticConstraint) {
232 if (!GetParam().supports_qc) {
233 GTEST_SKIP() << no_qc_support_message;
235 HalfEllipseProblem qc_problem(GetParam().use_integer_variables);
240 ASSERT_OK(solver->Solve({.parameters = GetParam().parameters}));
242 qc_problem.model.DeleteQuadraticConstraint(qc_problem.q);
245 IsOkAndHolds(GetParam().supports_incremental_add_and_deletes
248 EXPECT_THAT(solver->SolveWithoutUpdate({.parameters = GetParam().parameters}),
250 0.0, {{qc_problem.x, 0.0}, {qc_problem.y, 0.0}})));
265TEST_P(IncrementalQcTest, UpdateDeletesVariableInQuadraticConstraint) {
266 if (!GetParam().supports_qc) {
267 GTEST_SKIP() << no_qc_support_message;
269 HalfEllipseProblem qc_problem(GetParam().use_integer_variables);
275 ASSERT_OK(solver->Solve({.parameters = GetParam().parameters}));
277 qc_problem.model.DeleteVariable(qc_problem.x);
280 IsOkAndHolds(GetParam().supports_incremental_variable_deletions
283 EXPECT_THAT(solver->SolveWithoutUpdate({.parameters = GetParam().parameters}),
303TEST_P(QcDualsTest, OnlyQuadraticConstraintLess) {
304 if (!GetParam().supports_qc) {
308 const Variable
x =
model.AddVariable();
309 const QuadraticConstraint mu =
model.AddQuadraticConstraint(
x *
x <= 1);
313 const double expected_objective_value = -1.0;
318 {{mu, -0.5}}, {{
x, 0.0}}));
336TEST_P(QcDualsTest, OnlyQuadraticConstraintGreater) {
337 if (!GetParam().supports_qc) {
341 const Variable
x =
model.AddVariable();
342 const QuadraticConstraint mu =
model.AddQuadraticConstraint(-
x *
x >= -1);
346 const double expected_objective_value = -1.0;
351 {{mu, 0.5}}, {{
x, 0.0}}));
376TEST_P(QcDualsTest, QuadraticObjectiveAndLinearAndQuadraticConstraints) {
377 if (!GetParam().supports_qc) {
381 const Variable x0 =
model.AddVariable();
382 const Variable
x1 =
model.AddVariable();
383 const LinearConstraint y0 =
model.AddLinearConstraint(
x1 - x0 <= 0.0);
384 const LinearConstraint
y1 =
model.AddLinearConstraint(-
x1 - x0 <= 0.0);
385 const QuadraticConstraint mu =
386 model.AddQuadraticConstraint(
x1 *
x1 + x0 <= 2);
390 const double expected_objective_value = -9.0;
392 {{x0, 1.0}, {
x1, 1.0}}));
395 expected_objective_value, {{y0, -8.0 / 3.0}, {
y1, 0.0}},
396 {{mu, -8.0 / 3.0}}, {{x0, 0.0}, {
x1, 0.0}}));
421TEST_P(QcDualsTest, MaxAndVariableBounds) {
422 if (!GetParam().supports_qc) {
426 const Variable x0 =
model.AddVariable();
427 const Variable
x1 =
model.AddVariable();
428 const Variable
x2 =
model.AddContinuousVariable(1.0, 1.0);
429 const LinearConstraint
y =
model.AddLinearConstraint(
x1 == 1.0);
430 const QuadraticConstraint mu =
431 model.AddQuadraticConstraint(x0 * x0 +
x1 *
x1 +
x2 *
x2 <= 3.0);
432 model.Maximize(-x0 * x0 + 4.0 * x0);
435 const double expected_objective_value = 3.0;
438 {{x0, 1.0}, {
x1, 1.0}, {
x2, 1.0}}));
442 {{x0, 0.0}, {
x1, 0.0}, {
x2, -2.0}}));
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}}}})))
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::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)
std::string ProtobufShortDebugString(const P &message)
internal::StatusIsMatcher StatusIs(CodeMatcher code_matcher, MessageMatcher message_matcher)
#define ASSERT_OK(expression)
#define EXPECT_OK(expression)
#define ASSERT_OK_AND_ASSIGN(lhs, rexpr)
bool supports_incremental_add_and_deletes
bool use_integer_variables
True if the solver supports integer variables.
SolverType solver_type
The tested solver.
bool supports_qc
True if the solver supports quadratic constraints.
bool supports_incremental_variable_deletions
SolveParameters parameters
QcTestParameters(SolverType solver_type, SolveParameters parameters, bool supports_qc, bool supports_incremental_add_and_deletes, bool supports_incremental_variable_deletions, bool use_integer_variables)
SolveParametersProto Proto() const