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;
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) {
87 j_vars += vars_[
i][j];
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,
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);
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) {
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();
175 const SolveResult result =
176 Solve(assignment_problem.model(), solver_type, args).value();
177 CHECK_OK(result.termination.EnsureIsOptimal());
178 return result.solve_stats;
182 if (!SupportsPresolve()) {
183 GTEST_SKIP() <<
"Presolve emphasis not supported. Ignoring this test.";
186 EXPECT_GT(stats.simplex_iterations + stats.barrier_iterations +
187 stats.first_order_iterations,
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);
214 args.parameters.threads = 1;
217 Solve(assignment_problem.model(), solver_type, args));
219 return result.solve_stats;
223 if (!SupportsSimplex()) {
226 StatusIs(absl::StatusCode::kInvalidArgument,
227 AnyOf(HasSubstr(
"LP_ALGORITHM_PRIMAL_SIMPLEX"),
228 HasSubstr(
"lp_algorithm"))));
235 EXPECT_GT(stats.simplex_iterations, 0);
236 EXPECT_EQ(stats.barrier_iterations, 0);
237 EXPECT_EQ(stats.first_order_iterations, 0);
241 if (!SupportsSimplex()) {
243 StatusIs(absl::StatusCode::kInvalidArgument,
244 AnyOf(HasSubstr(
"LP_ALGORITHM_DUAL_SIMPLEX"),
245 HasSubstr(
"lp_algorithm"))));
252 EXPECT_GT(stats.simplex_iterations, 0);
253 EXPECT_EQ(stats.barrier_iterations, 0);
254 EXPECT_EQ(stats.first_order_iterations, 0);
258 if (!SupportsBarrier()) {
260 StatusIs(absl::StatusCode::kInvalidArgument,
261 AnyOf(HasSubstr(
"LP_ALGORITHM_BARRIER"),
262 HasSubstr(
"lp_algorithm"))));
270 EXPECT_GT(stats.barrier_iterations, 0);
277 if (!SupportsFirstOrder()) {
279 StatusIs(absl::StatusCode::kInvalidArgument,
280 AnyOf(HasSubstr(
"LP_ALGORITHM_FIRST_ORDER"),
281 HasSubstr(
"lp_algorithm"))));
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);
307 model.Maximize(
Sum(
x));
310 if (supports_presolve) {
313 args.parameters.iteration_limit = 1;
314 return Solve(model, solver_type, args);
318 if (!SupportsSimplex()) {
319 GTEST_SKIP() <<
"Simplex not supported. Ignoring this test.";
322 const SolveResult result,
324 SupportsPresolve()));
328 !GetParam().reports_limits));
332 if (!SupportsSimplex()) {
333 GTEST_SKIP() <<
"Simplex not supported. Ignoring this test.";
336 const SolveResult result,
338 SupportsPresolve()));
342 !GetParam().reports_limits));
346 if (!SupportsBarrier()) {
347 GTEST_SKIP() <<
"Barrier not supported. Ignoring this test.";
350 const SolveResult result,
352 SupportsPresolve()));
356 !GetParam().reports_limits));
360 if (!SupportsFirstOrder()) {
361 GTEST_SKIP() <<
"First order methods not supported. Ignoring this test.";
364 const SolveResult result,
366 SupportsPresolve()));
370 !GetParam().reports_limits));
375 const SolveResult result,
376 LPForIterationLimit(TestedSolver(), std::nullopt, 3, SupportsPresolve()));
380 !GetParam().reports_limits));
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);
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;
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);
461 !GetParam().reports_limits)));
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);
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;
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);
550 !GetParam().reports_limits)));
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);
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;
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);
610 Solve(model, GetParam().solver_type, {.parameters = params}),
#define ASSIGN_OR_RETURN(lhs, rexpr)
#define RETURN_IF_ERROR(expr)
void Maximize(Variable *obj, std::vector< Annotation > search_annotations)
void Minimize(Variable *obj, std::vector< Annotation > search_annotations)
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)))
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)
internal::StatusIsMatcher StatusIs(CodeMatcher code_matcher, MessageMatcher message_matcher)
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)
internal::IsOkAndHoldsMatcher< typename std::decay< InnerMatcher >::type > IsOkAndHolds(InnerMatcher &&inner_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).
SolveParameters parameters
Model independent parameters, e.g. time limit.
std::optional< LPAlgorithm > lp_algorithm
std::optional< int32_t > random_seed
std::optional< double > objective_limit
std::optional< Emphasis > presolve
std::optional< double > cutoff_limit
std::optional< double > best_bound_limit