27#include "absl/container/flat_hash_set.h"
28#include "absl/status/statusor.h"
29#include "absl/strings/escaping.h"
30#include "absl/strings/str_cat.h"
31#include "absl/synchronization/mutex.h"
32#include "absl/time/clock.h"
33#include "absl/time/time.h"
34#include "gtest/gtest.h"
51 <<
", callback_event: " << params.
event <<
" }";
56 const bool support_interrupter,
57 const bool integer_variables,
58 std::string expected_log,
60 : solver_type(solver_type),
61 support_interrupter(support_interrupter),
62 integer_variables(integer_variables),
63 expected_log(
std::move(expected_log)),
64 solve_parameters(
std::move(solve_parameters)) {}
71 <<
", expected_log: \"" << absl::CEscape(params.
expected_log) <<
"\""
72 <<
", solve_parameters: "
79using ::testing::HasSubstr;
80using ::testing::status::IsOkAndHolds;
82constexpr double kInf = std::numeric_limits<double>::infinity();
89TEST_P(GenericTest, OffsetOnlyMinimization) {
95TEST_P(GenericTest, OffsetOnlyMaximization) {
101TEST_P(GenericTest, OffsetMinimization) {
104 model.AddVariable(-1.0, 2.0, GetParam().integer_variables,
"x");
105 model.Minimize(2 *
x + 4);
109TEST_P(GenericTest, OffsetMaximization) {
112 model.AddVariable(-1.0, 2.0, GetParam().integer_variables,
"x");
113 model.Maximize(2 *
x + 4);
117TEST_P(GenericTest, SolveTime) {
126 constexpr int kMinN = 10;
127 constexpr int kMaxN = 30;
128 constexpr int kIncrementN = 5;
129 constexpr absl::Duration kMinSolveTime = absl::Milliseconds(5);
130 for (
int n = kMinN; n <= kMaxN; n += kIncrementN) {
131 SCOPED_TRACE(absl::StrCat(
"n = ", n));
132 const std::unique_ptr<const Model>
model =
135 const absl::Time
start = absl::Now();
137 const absl::Duration expected_max_solve_time = absl::Now() -
start;
139 if (expected_max_solve_time <= kMinSolveTime && n < kMaxN) {
140 LOG(INFO) <<
"The solve ended too quickly (" << expected_max_solve_time
141 <<
") with n=" << n <<
"; retrying with a more complex model.";
144 EXPECT_GT(result.solve_stats.solve_time, absl::ZeroDuration());
145 EXPECT_LE(result.solve_stats.solve_time, expected_max_solve_time);
151TEST_P(GenericTest, InterruptBeforeSolve) {
152 if (!GetParam().support_interrupter) {
153 GTEST_SKIP() <<
"Solve interrupter not supported. Ignoring this test.";
156 const std::unique_ptr<Model>
model =
SmallModel(GetParam().integer_variables);
158 SolveInterrupter interrupter;
159 interrupter.Interrupt();
162 args.parameters = GetParam().solve_parameters;
163 args.interrupter = &interrupter;
170TEST_P(GenericTest, InterruptAfterSolve) {
171 if (!GetParam().support_interrupter) {
172 GTEST_SKIP() <<
"Solve interrupter not supported. Ignoring this test.";
175 const std::unique_ptr<Model>
model =
SmallModel(GetParam().integer_variables);
177 SolveInterrupter interrupter;
180 args.parameters = GetParam().solve_parameters;
181 args.interrupter = &interrupter;
187 interrupter.Interrupt();
191TEST_P(GenericTest, InterrupterNeverTriggered) {
197 if (!GetParam().support_interrupter) {
198 GTEST_SKIP() <<
"Solve interrupter not supported. Ignoring this test.";
201 const std::unique_ptr<Model>
model =
SmallModel(GetParam().integer_variables);
203 SolveInterrupter interrupter;
206 args.parameters = GetParam().solve_parameters;
207 args.interrupter = &interrupter;
214#ifdef OPERATIONS_RESEARCH_OUTPUT_CAPTURE_SUPPORTED
215TEST_P(GenericTest, NoStdoutOutputByDefault) {
216 Model
model(
"model");
218 model.AddVariable(0, 21.0, GetParam().integer_variables,
"x");
221 ScopedStdStreamCapture stdout_capture(CapturedStream::kStdout);
223 EXPECT_THAT(std::move(stdout_capture).StopCaptureAndReturnContents(),
228TEST_P(GenericTest, EnableOutputPrintsToStdOut) {
229 Model
model(
"model");
231 model.AddVariable(0, 21.0, GetParam().integer_variables,
"x");
234 SolveParameters params = GetParam().solve_parameters;
235 params.enable_output =
true;
237 ScopedStdStreamCapture stdout_capture(CapturedStream::kStdout);
242 EXPECT_THAT(std::move(stdout_capture).StopCaptureAndReturnContents(),
243 HasSubstr(GetParam().expected_log));
249std::string AllAsciiCharacters() {
250 std::ostringstream oss;
251 for (
char c =
'\x1';
c <
'\x80'; ++
c) {
258std::string AllNonAsciiCharacters() {
259 std::ostringstream oss;
260 for (
int c = 0x80;
c <= 0xff; ++
c) {
261 oss << static_cast<char>(
c);
266TEST_P(GenericTest, ModelNameTooLong) {
270 EXPECT_THAT(SimpleSolve(Model(std::string(1024,
'x'))),
278 EXPECT_THAT(SimpleSolve(Model(AllAsciiCharacters() + std::string(1024,
'x'))),
283 SimpleSolve(Model(AllNonAsciiCharacters() + std::string(1024,
'x'))),
287TEST_P(GenericTest, VariableNames) {
291 model.AddVariable(-1.0, 2.0, GetParam().integer_variables,
292 std::string(1024,
'x'));
297 model.AddVariable(-1.0, 2.0, GetParam().integer_variables,
298 AllAsciiCharacters() + std::string(1024,
'x'));
303 model.AddVariable(-1.0, 2.0, GetParam().integer_variables,
304 AllNonAsciiCharacters() + std::string(1024,
'x'));
311 model.AddVariable(-1.0, 2.0, GetParam().integer_variables,
312 std::string(1024,
'-') +
'x');
313 model.AddVariable(-1.0, 2.0, GetParam().integer_variables,
314 std::string(1024,
'-') +
'y');
319TEST_P(GenericTest, LinearConstraintNames) {
323 model.AddLinearConstraint(-1.0, 2.0, std::string(1024,
'x'));
328 model.AddLinearConstraint(-1.0, 2.0,
329 AllAsciiCharacters() + std::string(1024,
'x'));
334 model.AddLinearConstraint(-1.0, 2.0,
335 AllNonAsciiCharacters() + std::string(1024,
'x'));
342 model.AddLinearConstraint(-1.0, 2.0, std::string(1024,
'-') +
'x');
343 model.AddLinearConstraint(-1.0, 2.0, std::string(1024,
'-') +
'y');
351 model.AddVariable(0.0, 1.0, GetParam().integer_variables,
"x");
352 model.AddLinearConstraint(
x == 1.0,
"c");
353 SolverInitArguments init_args;
354 init_args.remove_names =
true;
356 const SolveResult result,
358 {.parameters = GetParam().solve_parameters}, init_args));
367TEST_P(GenericTest, NonZeroIndices) {
371 ModelProto base_model_proto;
372 constexpr int64_t kMaxValidId = std::numeric_limits<int64_t>::max() - 1;
374 VariablesProto& variables = *base_model_proto.mutable_variables();
375 variables.add_ids(kMaxValidId - 1);
376 variables.add_lower_bounds(-
kInf);
377 variables.add_upper_bounds(
kInf);
378 variables.add_integers(
false);
381 LinearConstraintsProto& linear_constraints =
382 *base_model_proto.mutable_linear_constraints();
383 linear_constraints.add_ids(kMaxValidId - 1);
384 linear_constraints.add_lower_bounds(-
kInf);
385 linear_constraints.add_upper_bounds(
kInf);
393 model->DeleteVariable(
model->Variables().back());
394 model->DeleteLinearConstraint(
model->LinearConstraints().back());
397 model->AddVariable(0.0,
kInf, GetParam().integer_variables,
"x");
398 EXPECT_EQ(
x.
id(), kMaxValidId);
402 const LinearConstraint
c =
model->AddLinearConstraint(2 *
x <= 8.0,
"c");
403 EXPECT_EQ(
c.
id(), kMaxValidId);
410testing::Matcher<absl::StatusOr<SolveResult>> StatusIsInvertedBounds(
411 const InvertedBounds& inverted_bounds) {
412 return testing::Property(
"status", &absl::StatusOr<SolveResult>::status,
413 inverted_bounds.ToStatus());
416TEST_P(GenericTest, InvertedVariableBounds) {
417 const SolveArguments solve_args = {.parameters = GetParam().solve_parameters};
424 const std::vector<std::pair<double, double>> new_variables_inverted_bounds = {
425 {3.0, 1.0}, {0.0, -1.0}, {1.0, -1.0}, {1.0, 0.0}};
426 for (
const auto [lb, ub] : new_variables_inverted_bounds) {
427 SCOPED_TRACE(absl::StrCat(
"lb: ", lb,
" ub: ", ub));
434 constexpr int64_t kXId = 13;
435 for (int64_t i = 0;
i < kXId; ++
i) {
439 const Variable
x =
model.AddVariable(lb, ub,
440 GetParam().integer_variables,
"x");
441 ASSERT_EQ(
x.
id(), kXId);
451 StatusIsInvertedBounds({.variables = {
x.
id()}}));
457 for (
const auto [initial_lb, initial_ub, new_lb, new_ub] :
458 std::vector<std::tuple<double, double, double, double>>{
459 {3.0, 4.0, 5.0, 4.0},
460 {0.0, 1.0, 2.0, 1.0},
461 {1.0, 1.0, 2.0, 1.0},
462 {0.0, 1.0, 0.0, -1.0},
463 {1.0, 1.0, 1.0, 0.0},
464 {1.0, 1.0, 1.0, -1.0}}) {
465 SCOPED_TRACE(absl::StrCat(
"initial_lb: ", initial_lb,
466 " initial_ub: ", initial_ub,
" new_lb: ", new_lb,
467 " new_ub: ", new_ub));
473 constexpr int64_t kXId = 13;
474 for (int64_t i = 0;
i < kXId; ++
i) {
478 const Variable
x =
model.AddVariable(initial_lb,
480 GetParam().integer_variables,
"x");
481 ASSERT_EQ(
x.
id(), kXId);
496 LOG(INFO) <<
"Skipping the initial solve as glp_interior() would fail.";
498 EXPECT_THAT(solver->SolveWithoutUpdate(solve_args),
504 model.set_lower_bound(
x, new_lb);
505 model.set_upper_bound(
x, new_ub);
507 EXPECT_THAT(solver->SolveWithoutUpdate(solve_args),
508 StatusIsInvertedBounds({.variables = {
x.
id()}}));
514 for (
const auto [lb, ub] : new_variables_inverted_bounds) {
515 SCOPED_TRACE(absl::StrCat(
"lb: ", lb,
" ub: ", ub));
521 constexpr int64_t kXId = 13;
522 for (int64_t i = 0;
i < kXId; ++
i) {
527 model.AddVariable(3.0, 4.0,
528 GetParam().integer_variables,
"x");
529 ASSERT_EQ(
x.
id(), kXId);
536 EXPECT_THAT(solver->SolveWithoutUpdate(solve_args),
541 const Variable
y =
model.AddVariable(lb, ub,
542 GetParam().integer_variables,
"y");
545 EXPECT_THAT(solver->SolveWithoutUpdate(solve_args),
546 StatusIsInvertedBounds({.variables = {
y.id()}}));
550TEST_P(GenericTest, InvertedLinearConstraintBounds) {
551 const SolveArguments solve_args = {.parameters = GetParam().solve_parameters};
557 model.AddVariable(0.0, 10.0,
558 GetParam().integer_variables,
"x");
563 constexpr int64_t kUId = 23;
564 for (int64_t i = 0;
i < kUId; ++
i) {
565 model.DeleteLinearConstraint(
model.AddLinearConstraint());
568 const LinearConstraint u =
model.AddLinearConstraint(3.0 <=
x <= 1.0,
"u");
569 ASSERT_EQ(u.id(), kUId);
579 StatusIsInvertedBounds({.linear_constraints = {u.id()}}));
586 model.AddVariable(0.0, 10.0,
587 GetParam().integer_variables,
"x");
592 constexpr int64_t kUId = 23;
593 for (int64_t i = 0;
i < kUId; ++
i) {
594 model.DeleteLinearConstraint(
model.AddLinearConstraint());
597 const LinearConstraint u =
model.AddLinearConstraint(3.0 <=
x <= 4.0,
"u");
598 ASSERT_EQ(u.id(), kUId);
605 EXPECT_THAT(solver->SolveWithoutUpdate(solve_args),
608 model.set_lower_bound(u, 5.0);
613 EXPECT_THAT(solver->SolveWithoutUpdate(solve_args),
614 StatusIsInvertedBounds({.linear_constraints = {u.id()}}));
621 model.AddVariable(0.0, 10.0,
622 GetParam().integer_variables,
"x");
627 constexpr int64_t kUId = 23;
628 for (int64_t i = 0;
i < kUId; ++
i) {
629 model.DeleteLinearConstraint(
model.AddLinearConstraint());
632 const LinearConstraint u =
model.AddLinearConstraint(3.0 <=
x <= 4.0,
"u");
633 ASSERT_EQ(u.id(), kUId);
640 EXPECT_THAT(solver->SolveWithoutUpdate(solve_args),
645 const LinearConstraint v =
model.AddLinearConstraint(5.0 <=
x <= 3.0,
"v");
648 EXPECT_THAT(solver->SolveWithoutUpdate(solve_args),
649 StatusIsInvertedBounds({.linear_constraints = {v.id()}}));
653TEST_P(TimeLimitTest, DenseIndependentSetNoTimeLimit) {
654 const std::unique_ptr<const Model>
model =
656 const double expected_objective =
657 GetParam().integer_variables ? 7 : 10.0 * (5 + 4 + 3) / 2.0;
662TEST_P(TimeLimitTest, DenseIndependentSetTimeLimit) {
663 ASSERT_TRUE(GetParam().event.has_value())
664 <<
"The TimeLimit test requires a callback event is given.";
665 SolveArguments solve_args = {.message_callback =
667 solve_args.parameters.time_limit = absl::Seconds(1);
670 solve_args.parameters.threads = 1;
673 solve_args.callback_registration.events.insert(*GetParam().event);
675 const std::unique_ptr<const Model>
model =
680 bool has_run =
false;
681 solve_args.callback = [&mutex, &has_run](
const CallbackData& data) {
682 const absl::MutexLock lock(&mutex);
684 LOG(INFO) <<
"Waiting two seconds in the callback...";
685 absl::SleepFor(absl::Seconds(2));
686 LOG(INFO) <<
"Done waiting in callback.";
689 return CallbackResult();
692 Solve(*
model, GetParam().solver_type, solve_args));
695 EXPECT_TRUE(has_run);
static absl::StatusOr< std::unique_ptr< Model > > FromModelProto(const ModelProto &model_proto)
An object oriented wrapper for quadratic constraints in ModelStorage.
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)))
absl::StatusOr< SolveResult > Solve(const Model &model, const SolverType solver_type, const SolveArguments &solve_args, const SolverInitArguments &init_args)
std::ostream & operator<<(std::ostream &ostr, const IndicatorConstraint &constraint)
std::unique_ptr< Model > SmallModel(const bool integer)
absl::StatusOr< std::unique_ptr< IncrementalSolver > > NewIncrementalSolver(Model *model, SolverType solver_type, SolverInitArguments arguments)
testing::Matcher< SolveResult > TerminatesWithLimit(const Limit expected, const bool allow_limit_undetermined)
MessageCallback InfoLoggerMessageCallback(const absl::string_view prefix, const absl::SourceLocation loc)
@ kTime
The algorithm stopped after a user-specified computation time.
std::unique_ptr< Model > DenseIndependentSet(const bool integer, const int n)
Matcher< SolveResult > IsOptimal(const std::optional< double > expected_primal_objective, const double tolerance)
std::string ProtobufShortDebugString(const P &message)
testing::Matcher< std::string > EmptyOrGurobiLicenseWarningIfGurobi(const bool is_gurobi)
#define ASSERT_OK(expression)
#define ASSERT_OK_AND_ASSIGN(lhs, rexpr)
bool support_interrupter
True if the solver support SolveInterrupter.
SolverType solver_type
The tested solver.
SolveParameters solve_parameters
Additional parameters to control the solve.
std::string expected_log
A message included in the solver logs when an optimal solution is found.
bool integer_variables
True if the tests should be performed with integer variables.
GenericTestParameters(SolverType solver_type, bool support_interrupter, bool integer_variables, std::string expected_log, SolveParameters solve_parameters={})
SolveParametersProto Proto() const
bool integer_variables
The test problem will be a 0-1 IP if true, otherwise will be an LP.
std::optional< CallbackEvent > event
A supported callback event, or nullopt if no event is supported.
SolverType solver_type
The tested solver.