27#include "absl/container/flat_hash_set.h"
28#include "absl/log/log.h"
29#include "absl/status/statusor.h"
30#include "absl/strings/escaping.h"
31#include "absl/strings/str_cat.h"
32#include "absl/synchronization/mutex.h"
33#include "absl/time/clock.h"
34#include "absl/time/time.h"
35#include "gtest/gtest.h"
52 <<
", callback_event: " << params.
event <<
" }";
72 <<
", expected_log: \"" << absl::CEscape(params.
expected_log) <<
"\""
73 <<
", solve_parameters: "
80using ::testing::HasSubstr;
83constexpr double kInf = std::numeric_limits<double>::infinity();
90TEST_P(GenericTest, OffsetOnlyMinimization) {
96TEST_P(GenericTest, OffsetOnlyMaximization) {
105 model.
AddVariable(-1.0, 2.0, GetParam().integer_variables,
"x");
113 model.
AddVariable(-1.0, 2.0, GetParam().integer_variables,
"x");
127 constexpr int kMinN = 10;
128 constexpr int kMaxN = 30;
129 constexpr int kIncrementN = 5;
130 constexpr absl::Duration kMinSolveTime = absl::Milliseconds(5);
131 for (
int n = kMinN; n <= kMaxN; n += kIncrementN) {
132 SCOPED_TRACE(absl::StrCat(
"n = ", n));
133 const std::unique_ptr<const Model> model =
136 const absl::Time start = absl::Now();
138 const absl::Duration expected_max_solve_time = absl::Now() - start;
140 if (expected_max_solve_time <= kMinSolveTime && n < kMaxN) {
141 LOG(INFO) <<
"The solve ended too quickly (" << expected_max_solve_time
142 <<
") with n=" << n <<
"; retrying with a more complex model.";
145 EXPECT_GT(result.solve_stats.solve_time, absl::ZeroDuration());
146 EXPECT_LE(result.solve_stats.solve_time, expected_max_solve_time);
153 if (!GetParam().support_interrupter) {
154 GTEST_SKIP() <<
"Solve interrupter not supported. Ignoring this test.";
157 const std::unique_ptr<Model> model =
SmallModel(GetParam().integer_variables);
159 SolveInterrupter interrupter;
160 interrupter.Interrupt();
163 args.
parameters = GetParam().solve_parameters;
164 args.interrupter = &interrupter;
167 Solve(*model, GetParam().solver_type, args));
172 if (!GetParam().support_interrupter) {
173 GTEST_SKIP() <<
"Solve interrupter not supported. Ignoring this test.";
176 const std::unique_ptr<Model> model =
SmallModel(GetParam().integer_variables);
178 SolveInterrupter interrupter;
181 args.
parameters = GetParam().solve_parameters;
182 args.interrupter = &interrupter;
185 Solve(*model, GetParam().solver_type, args));
188 interrupter.Interrupt();
198 if (!GetParam().support_interrupter) {
199 GTEST_SKIP() <<
"Solve interrupter not supported. Ignoring this test.";
202 const std::unique_ptr<Model> model =
SmallModel(GetParam().integer_variables);
204 SolveInterrupter interrupter;
207 args.
parameters = GetParam().solve_parameters;
208 args.interrupter = &interrupter;
211 Solve(*model, GetParam().solver_type, args));
215#ifdef OPERATIONS_RESEARCH_OUTPUT_CAPTURE_SUPPORTED
217 Model model(
"model");
219 model.AddVariable(0, 21.0, GetParam().integer_variables,
"x");
220 model.Maximize(2.0 *
x);
222 ScopedStdStreamCapture stdout_capture(CapturedStream::kStdout);
224 EXPECT_THAT(std::move(stdout_capture).StopCaptureAndReturnContents(),
230 Model model(
"model");
232 model.AddVariable(0, 21.0, GetParam().integer_variables,
"x");
233 model.Maximize(2.0 *
x);
238 ScopedStdStreamCapture stdout_capture(CapturedStream::kStdout);
243 EXPECT_THAT(std::move(stdout_capture).StopCaptureAndReturnContents(),
244 HasSubstr(GetParam().expected_log));
250std::string AllAsciiCharacters() {
251 std::ostringstream oss;
252 for (
char c =
'\x1';
c <
'\x80'; ++
c) {
259std::string AllNonAsciiCharacters() {
260 std::ostringstream oss;
261 for (
int c = 0x80;
c <= 0xff; ++
c) {
262 oss << static_cast<char>(
c);
279 EXPECT_THAT(SimpleSolve(
Model(AllAsciiCharacters() + std::string(1024,
'x'))),
284 SimpleSolve(
Model(AllNonAsciiCharacters() + std::string(1024,
'x'))),
292 model.
AddVariable(-1.0, 2.0, GetParam().integer_variables,
293 std::string(1024,
'x'));
298 model.
AddVariable(-1.0, 2.0, GetParam().integer_variables,
299 AllAsciiCharacters() + std::string(1024,
'x'));
304 model.
AddVariable(-1.0, 2.0, GetParam().integer_variables,
305 AllNonAsciiCharacters() + std::string(1024,
'x'));
312 model.
AddVariable(-1.0, 2.0, GetParam().integer_variables,
313 std::string(1024,
'-') +
'x');
314 model.
AddVariable(-1.0, 2.0, GetParam().integer_variables,
315 std::string(1024,
'-') +
'y');
324 model.AddLinearConstraint(-1.0, 2.0, std::string(1024,
'x'));
329 model.AddLinearConstraint(-1.0, 2.0,
330 AllAsciiCharacters() + std::string(1024,
'x'));
335 model.AddLinearConstraint(-1.0, 2.0,
336 AllNonAsciiCharacters() + std::string(1024,
'x'));
343 model.AddLinearConstraint(-1.0, 2.0, std::string(1024,
'-') +
'x');
344 model.AddLinearConstraint(-1.0, 2.0, std::string(1024,
'-') +
'y');
352 model.
AddVariable(0.0, 1.0, GetParam().integer_variables,
"x");
353 model.AddLinearConstraint(
x == 1.0,
"c");
357 const SolveResult result,
358 Solve(model, GetParam().solver_type,
359 {.parameters = GetParam().solve_parameters}, init_args));
373 constexpr int64_t kMaxValidId = std::numeric_limits<int64_t>::max() - 1;
375 VariablesProto& variables = *base_model_proto.mutable_variables();
376 variables.
add_ids(kMaxValidId - 1);
377 variables.add_lower_bounds(-
kInf);
378 variables.add_upper_bounds(
kInf);
379 variables.add_integers(
false);
383 *base_model_proto.mutable_linear_constraints();
384 linear_constraints.
add_ids(kMaxValidId - 1);
385 linear_constraints.add_lower_bounds(-
kInf);
386 linear_constraints.add_upper_bounds(
kInf);
394 model->DeleteVariable(model->Variables().back());
395 model->DeleteLinearConstraint(model->LinearConstraints().back());
398 model->AddVariable(0.0,
kInf, GetParam().integer_variables,
"x");
399 EXPECT_EQ(
x.id(), kMaxValidId);
404 EXPECT_EQ(
c.id(), kMaxValidId);
411testing::Matcher<absl::StatusOr<SolveResult>> StatusIsInvertedBounds(
413 return testing::Property(
"status", &absl::StatusOr<SolveResult>::status,
414 inverted_bounds.ToStatus());
418 const SolveArguments solve_args = {.parameters = GetParam().solve_parameters};
425 const std::vector<std::pair<double, double>> new_variables_inverted_bounds = {
426 {3.0, 1.0}, {0.0, -1.0}, {1.0, -1.0}, {1.0, 0.0}};
427 for (
const auto [lb, ub] : new_variables_inverted_bounds) {
428 SCOPED_TRACE(absl::StrCat(
"lb: ", lb,
" ub: ", ub));
435 constexpr int64_t kXId = 13;
436 for (int64_t i = 0;
i < kXId; ++
i) {
441 GetParam().integer_variables,
"x");
442 ASSERT_EQ(
x.id(), kXId);
452 StatusIsInvertedBounds({.variables = {
x.id()}}));
458 for (
const auto [initial_lb, initial_ub, new_lb, new_ub] :
459 std::vector<std::tuple<double, double, double, double>>{
460 {3.0, 4.0, 5.0, 4.0},
461 {0.0, 1.0, 2.0, 1.0},
462 {1.0, 1.0, 2.0, 1.0},
463 {0.0, 1.0, 0.0, -1.0},
464 {1.0, 1.0, 1.0, 0.0},
465 {1.0, 1.0, 1.0, -1.0}}) {
466 SCOPED_TRACE(absl::StrCat(
"initial_lb: ", initial_lb,
467 " initial_ub: ", initial_ub,
" new_lb: ", new_lb,
468 " new_ub: ", new_ub));
474 constexpr int64_t kXId = 13;
475 for (int64_t i = 0;
i < kXId; ++
i) {
481 GetParam().integer_variables,
"x");
482 ASSERT_EQ(
x.id(), kXId);
497 LOG(INFO) <<
"Skipping the initial solve as glp_interior() would fail.";
499 EXPECT_THAT(solver->SolveWithoutUpdate(solve_args),
505 model.set_lower_bound(
x, new_lb);
506 model.set_upper_bound(
x, new_ub);
508 EXPECT_THAT(solver->SolveWithoutUpdate(solve_args),
509 StatusIsInvertedBounds({.variables = {
x.id()}}));
515 for (
const auto [lb, ub] : new_variables_inverted_bounds) {
516 SCOPED_TRACE(absl::StrCat(
"lb: ", lb,
" ub: ", ub));
522 constexpr int64_t kXId = 13;
523 for (int64_t i = 0;
i < kXId; ++
i) {
529 GetParam().integer_variables,
"x");
530 ASSERT_EQ(
x.id(), kXId);
537 EXPECT_THAT(solver->SolveWithoutUpdate(solve_args),
543 GetParam().integer_variables,
"y");
546 EXPECT_THAT(solver->SolveWithoutUpdate(solve_args),
547 StatusIsInvertedBounds({.variables = {y.id()}}));
552 const SolveArguments solve_args = {.parameters = GetParam().solve_parameters};
559 GetParam().integer_variables,
"x");
564 constexpr int64_t kUId = 23;
565 for (int64_t i = 0;
i < kUId; ++
i) {
566 model.DeleteLinearConstraint(model.AddLinearConstraint());
570 ASSERT_EQ(u.id(), kUId);
580 StatusIsInvertedBounds({.linear_constraints = {u.id()}}));
588 GetParam().integer_variables,
"x");
593 constexpr int64_t kUId = 23;
594 for (int64_t i = 0;
i < kUId; ++
i) {
595 model.DeleteLinearConstraint(model.AddLinearConstraint());
599 ASSERT_EQ(u.id(), kUId);
606 EXPECT_THAT(solver->SolveWithoutUpdate(solve_args),
609 model.set_lower_bound(u, 5.0);
614 EXPECT_THAT(solver->SolveWithoutUpdate(solve_args),
615 StatusIsInvertedBounds({.linear_constraints = {u.id()}}));
623 GetParam().integer_variables,
"x");
628 constexpr int64_t kUId = 23;
629 for (int64_t i = 0;
i < kUId; ++
i) {
630 model.DeleteLinearConstraint(model.AddLinearConstraint());
634 ASSERT_EQ(u.id(), kUId);
641 EXPECT_THAT(solver->SolveWithoutUpdate(solve_args),
649 EXPECT_THAT(solver->SolveWithoutUpdate(solve_args),
650 StatusIsInvertedBounds({.linear_constraints = {v.id()}}));
655 const std::unique_ptr<const Model> model =
657 const double expected_objective =
658 GetParam().integer_variables ? 7 : 10.0 * (5 + 4 + 3) / 2.0;
664 ASSERT_TRUE(GetParam().event.has_value())
665 <<
"The TimeLimit test requires a callback event is given.";
671 solve_args.parameters.threads = 1;
674 solve_args.callback_registration.events.insert(*GetParam().event);
676 const std::unique_ptr<const Model> model =
681 bool has_run =
false;
682 solve_args.callback = [&mutex, &has_run](
const CallbackData& data) {
683 const absl::MutexLock lock(&mutex);
685 LOG(INFO) <<
"Waiting two seconds in the callback...";
686 absl::SleepFor(absl::Seconds(2));
687 LOG(INFO) <<
"Done waiting in callback.";
693 Solve(*model, GetParam().solver_type, solve_args));
696 EXPECT_TRUE(has_run);
void Maximize(Variable *obj, std::vector< Annotation > search_annotations)
Variable * AddVariable(absl::string_view name, const Domain &domain, bool defined)
--— Builder methods --—
void Minimize(Variable *obj, std::vector< Annotation > search_annotations)
void add_ids(::int64_t value)
static absl::StatusOr< std::unique_ptr< Model > > FromModelProto(const ModelProto &model_proto)
void add_ids(::int64_t value)
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::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)
std::ostream & operator<<(std::ostream &ostr, const SecondOrderConeConstraint &constraint)
@ 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)
internal::IsOkAndHoldsMatcher< typename std::decay< InnerMatcher >::type > IsOkAndHolds(InnerMatcher &&inner_matcher)
#define ASSERT_OK(expression)
#define ASSERT_OK_AND_ASSIGN(lhs, rexpr)
The value returned by the Callback function.
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={})
SolveParameters parameters
Model independent parameters, e.g. time limit.
absl::Duration time_limit
SolveParametersProto Proto() const
bool integer_variables
The test problem will be a 0-1 IP if true, otherwise will be an LP.
TimeLimitTestParameters(const SolverType solver_type, const bool integer_variables, const std::optional< CallbackEvent > supported_event=std::nullopt)
std::optional< CallbackEvent > event
A supported callback event, or nullopt if no event is supported.
SolverType solver_type
The tested solver.