33#include "absl/container/flat_hash_set.h"
34#include "absl/log/check.h"
35#include "absl/status/status.h"
36#include "absl/status/statusor.h"
37#include "absl/strings/str_cat.h"
38#include "absl/strings/string_view.h"
39#include "gtest/gtest.h"
49using ::testing::HasSubstr;
50using ::testing::status::IsOkAndHolds;
51using ::testing::status::StatusIs;
66class AssignmentProblem {
68 explicit AssignmentProblem(
const int n) : n_(n) {
69 for (
int i = 0; i < n_; ++i) {
71 for (
int j = 0; j < n_; ++j) {
72 vars_.back().push_back(
73 model_.AddVariable(0.0, 1.0,
false, absl::StrCat(
"x_", i,
"_", j)));
77 for (
int i = 0;
i < n_; ++
i) {
81 for (
int i = 0;
i < n_; ++
i) {
82 model_.AddLinearConstraint(
Sum(vars_[i]) <= 1.0);
84 for (
int j = 0; j < n_; ++j) {
85 LinearExpression j_vars;
86 for (
int i = 0;
i < n_; ++
i) {
89 model_.AddLinearConstraint(j_vars <= 1.0);
93 void MakePresolveOptimal() {
94 for (
int i = 0;
i < n_; ++
i) {
95 LinearExpression terms;
96 for (
int j = 0; j < n_; ++j) {
101 model_.AddLinearConstraint(terms == 0.0);
105 const Model&
model() {
return model_; }
107 std::vector<std::string> SolutionFingerprint(
108 const SolveResult& result)
const {
109 std::vector<std::string> fingerprint;
110 for (
int i = 0;
i < n_; ++
i) {
111 for (
int j = 0; j < n_; ++j) {
112 const double val = result.variable_values().at(vars_[i][j]);
113 CHECK(val <= 0.01 || val >= 0.99)
114 <<
"i: " <<
i <<
" j: " << j <<
" val: " << val;
116 fingerprint.push_back(std::string(vars_[i][j].
name()));
120 std::sort(fingerprint.begin(), fingerprint.end());
127 std::vector<std::vector<Variable>>
vars_;
130std::vector<std::string> LPAssignment(
const SolverType solver_type,
131 const SolveArguments& args) {
133 AssignmentProblem assignment(n);
134 const SolveResult result =
135 Solve(assignment.model(), solver_type, args).value();
136 CHECK_OK(result.termination.EnsureIsOptimal());
137 CHECK_LE(std::abs(result.objective_value() - n), 1e-4)
138 << result.objective_value();
139 return assignment.SolutionFingerprint(result);
142TEST_P(LpParameterTest, RandomSeedLp) {
143 if (!SupportsRandomSeed()) {
144 GTEST_SKIP() <<
"Random seed not supported. Ignoring this test.";
146 absl::flat_hash_set<std::vector<std::string>> solutions_seen;
147 for (
int seed = 10; seed < 200; seed += 10) {
148 std::vector<std::string> result_for_seed;
149 for (
int trial = 0; trial < 10; ++trial) {
151 args.parameters.random_seed = seed;
155 std::vector<std::string> result = LPAssignment(TestedSolver(), args);
157 result_for_seed = result;
158 solutions_seen.insert(result_for_seed);
160 ASSERT_EQ(result_for_seed, result)
161 <<
"seed: " << seed <<
" trail: " << trial;
167 EXPECT_GE(solutions_seen.size(), 3);
171 AssignmentProblem assignment_problem(6);
172 assignment_problem.MakePresolveOptimal();
174 args.parameters.presolve = presolve_emphasis;
175 const SolveResult result =
176 Solve(assignment_problem.model(), solver_type, args).value();
177 CHECK_OK(result.termination.EnsureIsOptimal());
178 return result.solve_stats;
181TEST_P(LpParameterTest, PresolveOff) {
182 if (!SupportsPresolve()) {
183 GTEST_SKIP() <<
"Presolve emphasis not supported. Ignoring this test.";
185 const SolveStats stats = LPForPresolve(TestedSolver(),
Emphasis::kOff);
186 EXPECT_GT(stats.simplex_iterations + stats.barrier_iterations +
187 stats.first_order_iterations,
191TEST_P(LpParameterTest, PresolveOn) {
192 if (!SupportsPresolve()) {
193 GTEST_SKIP() <<
"Presolve emphasis not supported. Ignoring this test.";
196 EXPECT_EQ(stats.simplex_iterations + stats.barrier_iterations +
197 stats.first_order_iterations,
203absl::StatusOr<SolveStats> SolveForLpAlgorithm(
SolverType solver_type,
205 AssignmentProblem assignment_problem(6);
207 args.parameters.lp_algorithm = algorithm;
214 args.parameters.threads = 1;
217 Solve(assignment_problem.model(), solver_type, args));
219 return result.solve_stats;
222TEST_P(LpParameterTest, LPAlgorithmPrimal) {
223 if (!SupportsSimplex()) {
226 StatusIs(absl::StatusCode::kInvalidArgument,
227 AnyOf(HasSubstr(
"LP_ALGORITHM_PRIMAL_SIMPLEX"),
228 HasSubstr(
"lp_algorithm"))));
232 const SolveStats stats,
235 EXPECT_GT(stats.simplex_iterations, 0);
236 EXPECT_EQ(stats.barrier_iterations, 0);
237 EXPECT_EQ(stats.first_order_iterations, 0);
240TEST_P(LpParameterTest, LPAlgorithmDual) {
241 if (!SupportsSimplex()) {
243 StatusIs(absl::StatusCode::kInvalidArgument,
244 AnyOf(HasSubstr(
"LP_ALGORITHM_DUAL_SIMPLEX"),
245 HasSubstr(
"lp_algorithm"))));
249 const SolveStats stats,
252 EXPECT_GT(stats.simplex_iterations, 0);
253 EXPECT_EQ(stats.barrier_iterations, 0);
254 EXPECT_EQ(stats.first_order_iterations, 0);
257TEST_P(LpParameterTest, LPAlgorithmBarrier) {
258 if (!SupportsBarrier()) {
260 StatusIs(absl::StatusCode::kInvalidArgument,
261 AnyOf(HasSubstr(
"LP_ALGORITHM_BARRIER"),
262 HasSubstr(
"lp_algorithm"))));
266 const SolveStats stats,
270 EXPECT_GT(stats.barrier_iterations, 0);
276TEST_P(LpParameterTest, LPAlgorithmFirstOrder) {
277 if (!SupportsFirstOrder()) {
279 StatusIs(absl::StatusCode::kInvalidArgument,
280 AnyOf(HasSubstr(
"LP_ALGORITHM_FIRST_ORDER"),
281 HasSubstr(
"lp_algorithm"))));
285 const SolveStats stats,
287 EXPECT_EQ(stats.simplex_iterations, 0);
288 EXPECT_EQ(stats.barrier_iterations, 0);
289 EXPECT_GT(stats.first_order_iterations, 0);
292absl::StatusOr<SolveResult> LPForIterationLimit(
293 const SolverType solver_type,
const std::optional<LPAlgorithm> algorithm,
294 const int n,
const bool supports_presolve) {
297 Model
model(
"Iteration limit LP");
298 std::vector<Variable>
x;
299 for (
int i = 0;
i < n; ++
i) {
300 x.push_back(
model.AddContinuousVariable(0.0, 1.0));
302 for (
int i = 0;
i < n; ++
i) {
303 for (
int j = i + 1; j < n; ++j) {
304 model.AddLinearConstraint(
x[i] +
x[j] <= 1);
309 args.parameters.lp_algorithm = algorithm;
310 if (supports_presolve) {
313 args.parameters.iteration_limit = 1;
317TEST_P(LpParameterTest, IterationLimitPrimalSimplex) {
318 if (!SupportsSimplex()) {
319 GTEST_SKIP() <<
"Simplex not supported. Ignoring this test.";
322 const SolveResult result,
324 SupportsPresolve()));
328 !GetParam().reports_limits));
331TEST_P(LpParameterTest, IterationLimitDualSimplex) {
332 if (!SupportsSimplex()) {
333 GTEST_SKIP() <<
"Simplex not supported. Ignoring this test.";
336 const SolveResult result,
338 SupportsPresolve()));
342 !GetParam().reports_limits));
345TEST_P(LpParameterTest, IterationLimitBarrier) {
346 if (!SupportsBarrier()) {
347 GTEST_SKIP() <<
"Barrier not supported. Ignoring this test.";
350 const SolveResult result,
352 SupportsPresolve()));
356 !GetParam().reports_limits));
359TEST_P(LpParameterTest, IterationLimitFirstOrder) {
360 if (!SupportsFirstOrder()) {
361 GTEST_SKIP() <<
"First order methods not supported. Ignoring this test.";
364 const SolveResult result,
366 SupportsPresolve()));
370 !GetParam().reports_limits));
373TEST_P(LpParameterTest, IterationLimitUnspecified) {
375 const SolveResult result,
376 LPForIterationLimit(TestedSolver(), std::nullopt, 3, SupportsPresolve()));
380 !GetParam().reports_limits));
385TEST_P(LpParameterTest, ObjectiveLimitMaximization) {
387 if (!GetParam().supports_simplex) {
397 const Variable
x =
model.AddContinuousVariable(0.0, 1.0);
398 const Variable
y =
model.AddContinuousVariable(0.0, 1.0);
399 const Variable z =
model.AddContinuousVariable(0.0, 1.0);
400 model.AddLinearConstraint(
x +
y <= 1.0);
401 model.AddLinearConstraint(
x + z <= 1.0);
402 model.Maximize(10 *
x + 9 *
y + 8 * z);
406 SolveParameters params = {.objective_limit = -0.5,
409 const absl::StatusOr<SolveResult> result =
410 Solve(
model, GetParam().solver_type, {.parameters = params});
411 if (!GetParam().supports_objective_limit) {
413 HasSubstr(
"objective_limit")));
419 !GetParam().reports_limits)));
423 params.objective_limit = 18.0;
430TEST_P(LpParameterTest, ObjectiveLimitMinimization) {
431 if (!GetParam().supports_objective_limit) {
436 if (!GetParam().supports_simplex) {
446 const Variable
x =
model.AddContinuousVariable(0.0, 1.0);
447 const Variable
y =
model.AddContinuousVariable(0.0, 1.0);
448 const Variable z =
model.AddContinuousVariable(0.0, 1.0);
449 model.AddLinearConstraint(
x +
y >= 1.0);
450 model.AddLinearConstraint(
x + z >= 1.0);
451 model.Minimize(10 *
x + 9 *
y + 8 * z);
455 SolveParameters params = {.objective_limit = 30.0,
461 !GetParam().reports_limits)));
465 params.objective_limit = 7.0;
472TEST_P(LpParameterTest, BestBoundLimitMaximize) {
474 if (!GetParam().supports_simplex) {
479 GTEST_SKIP() <<
"TODO(b/272312674): Highs appears to have a bug where "
480 "best_bound_limit is only supported for minimization.";
488 const Variable
x =
model.AddContinuousVariable(0.0, 1.0);
489 const Variable
y =
model.AddContinuousVariable(0.0, 1.0);
490 const Variable z =
model.AddContinuousVariable(0.0, 1.0);
491 model.AddLinearConstraint(
x +
y + z <= 1.5);
492 model.Maximize(3 *
x + 2 *
y + z);
497 SolveParameters params = {.best_bound_limit = 6.5,
500 const absl::StatusOr<SolveResult> result =
501 Solve(
model, GetParam().solver_type, {.parameters = params});
502 if (!GetParam().supports_best_bound_limit) {
504 HasSubstr(
"best_bound_limit")));
510 !GetParam().reports_limits)));
514 params.best_bound_limit = 3.5;
521TEST_P(LpParameterTest, BestBoundLimitMinimize) {
522 if (!GetParam().supports_objective_limit) {
527 if (!GetParam().supports_simplex) {
536 const Variable
x =
model.AddContinuousVariable(0.0, 1.0);
537 const Variable
y =
model.AddContinuousVariable(0.0, 1.0);
538 model.AddLinearConstraint(
x +
y >= 1.0);
544 SolveParameters params = {.best_bound_limit = -0.5,
550 !GetParam().reports_limits)));
554 params.best_bound_limit = 1.5;
559TEST_P(LpParameterTest, CutoffLimitMaximize) {
566 const Variable
x =
model.AddContinuousVariable(0.0, 1.0);
567 const Variable
y =
model.AddContinuousVariable(0.0, 1.0);
568 model.AddLinearConstraint(
x +
y <= 1.0);
572 SolveParameters params = {.cutoff_limit = 3.5, .presolve =
Emphasis::kOff};
573 const absl::StatusOr<SolveResult> result =
574 Solve(
model, GetParam().solver_type, {.parameters = params});
575 if (!GetParam().supports_cutoff) {
577 HasSubstr(
"cutoff_limit")));
586 params.cutoff_limit = 1.5;
591TEST_P(LpParameterTest, CutoffLimitMinimize) {
592 if (!GetParam().supports_cutoff) {
602 const Variable
x =
model.AddContinuousVariable(0.0, 1.0);
603 const Variable
y =
model.AddContinuousVariable(0.0, 1.0);
604 model.AddLinearConstraint(
x +
y >= 1.0);
608 SolveParameters params = {.cutoff_limit = -0.5, .presolve =
Emphasis::kOff};
610 Solve(
model, GetParam().solver_type, {.parameters = params}),
616 params.cutoff_limit = 1.5;
const std::vector< IntVar * > vars_
#define ASSIGN_OR_RETURN(lhs, rexpr)
#define RETURN_IF_ERROR(expr)
const std::string name
A name for logging purposes.
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)))
Emphasis
never give an error, and will map onto their best match.
LPAlgorithm
Selects an algorithm for solving linear programs.
LinearExpression Sum(const Iterable &items)
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)
testing::Matcher< SolveResult > TerminatesWithReasonFeasible(const Limit expected, const bool allow_limit_undetermined)
testing::Matcher< SolveResult > TerminatesWithLimit(const Limit expected, const bool allow_limit_undetermined)
Matcher< SolveResult > IsOptimal(const std::optional< double > expected_primal_objective, const double tolerance)
testing::Matcher< SolveResult > TerminatesWithReasonNoSolutionFound(const Limit expected, const bool allow_limit_undetermined)
In SWIG mode, we don't want anything besides these top-level includes.
internal::StatusIsMatcher StatusIs(CodeMatcher code_matcher, MessageMatcher message_matcher)
#define ASSERT_OK_AND_ASSIGN(lhs, rexpr)
Parameters for the LpParameterTest suite below.
bool supports_presolve
Indicates if the solver supports setting the presolve emphasis.
bool supports_cutoff
Indicates if the solver supports a cutoff value.
SolverType solver_type
The tested solver.
bool supports_barrier
Indicates if the solver supports barrier as an algorithm.
bool supports_random_seed
Indicates if the solver supports setting the random seed.
bool supports_simplex
Indicates if the solver supports simplex as an algorithm (primal and dual).