71#include "absl/container/flat_hash_set.h"
72#include "absl/status/status.h"
73#include "absl/status/statusor.h"
74#include "absl/strings/escaping.h"
75#include "absl/strings/str_cat.h"
76#include "absl/strings/str_join.h"
77#include "absl/strings/string_view.h"
78#include "absl/time/time.h"
79#include "absl/types/span.h"
80#include "gtest/gtest.h"
106 ostr <<
"{ " << absl::StrJoin(
supported,
", ") <<
" }";
114 supported.push_back(
"supports_iteration_limit");
117 supported.push_back(
"supports_node_limit");
123 supported.push_back(
"supports_objective_limit");
126 supported.push_back(
"supports_bound_limit");
129 supported.push_back(
"supports_solution_limit_one");
132 supported.push_back(
"supports_one_thread");
135 supported.push_back(
"supports_n_threads");
138 supported.push_back(
"supports_random_seed");
141 supported.push_back(
"supports_absolute_gap_tolerance");
144 supported.push_back(
"supports_lp_algorithm_simplex");
147 supported.push_back(
"supports_lp_algorithm_barrier");
150 supported.push_back(
"supports_presolve");
156 supported.push_back(
"supports_heuristics");
161 ostr <<
"{ " << absl::StrJoin(
supported,
", ") <<
" }";
167 ostr <<
"{ name: " << params.
name;
173 ostr <<
"\", stop_before_optimal: "
180 out <<
"{ name: " << params.
name;
182 out <<
", base_parameters: "
192using ::testing::HasSubstr;
194using ::testing::status::IsOkAndHolds;
195using ::testing::status::StatusIs;
205void AddTriangle(Model&
model, Variable
x, Variable
y, Variable z) {
206 model.AddLinearConstraint(
x +
y >= 1);
207 model.AddLinearConstraint(
y + z >= 1);
208 model.AddLinearConstraint(z +
x >= 1);
226void AddSixTriangles(Model&
model, absl::Span<const Variable> xs,
227 absl::Span<const Variable> ys,
228 absl::Span<const Variable> zs) {
229 AddTriangle(
model, xs[0], xs[1], xs[2]);
230 AddTriangle(
model, ys[0], ys[1], ys[2]);
231 AddTriangle(
model, zs[0], zs[1], zs[2]);
232 AddTriangle(
model, xs[0], ys[0], zs[0]);
233 AddTriangle(
model, xs[1], ys[1], zs[1]);
234 AddTriangle(
model, xs[2], ys[2], zs[2]);
255 xs_ = MakeVars3By3(
"x");
256 ys_ = MakeVars3By3(
"y");
257 zs_ = MakeVars3By3(
"z");
258 LinearExpression objective;
259 for (
int i = 0;
i < 3; ++
i) {
260 objective +=
Sum(xs_[i]) +
Sum(ys_[i]) +
Sum(zs_[i]);
262 model_.Minimize(objective);
263 for (
int i = 0;
i < 3; ++
i) {
264 AddSixTriangles(model_, xs_[i], ys_[i], zs_[i]);
266 for (
int j = 0; j < 3; ++j) {
267 AddTriangle(model_, xs_[0][j], xs_[1][j], xs_[2][j]);
268 AddTriangle(model_, ys_[0][j], ys_[1][j], ys_[2][j]);
269 AddTriangle(model_, zs_[0][j], zs_[1][j], zs_[2][j]);
273 Model&
model() {
return model_; }
275 std::vector<std::string> SolutionFingerprint(
const SolveResult& result) {
276 std::vector<std::string> var_names;
277 for (
const Variable v : model_.Variables()) {
278 double val = result.variable_values().at(v);
280 var_names.push_back(std::string(v.name()));
283 std::sort(var_names.begin(), var_names.end());
287 static absl::StatusOr<std::vector<std::string>> SolveAndFingerprint(
288 const SolverType solver_type,
const SolveParameters& params) {
289 VertexCover vertex_cover;
292 Solve(vertex_cover.model(), solver_type, {.parameters = params}));
294 if (std::abs(result.objective_value() - 18.0) > 1e-4) {
296 <<
"expected objective value of 18, found: "
297 << result.objective_value();
299 return result.has_primal_feasible_solution()
300 ? vertex_cover.SolutionFingerprint(result)
301 : std::vector<std::string>();
306 std::vector<std::vector<Variable>> MakeVars3By3(absl::string_view prefix) {
307 std::vector<std::vector<Variable>> result(3);
308 for (
int i = 0;
i < 3; ++
i) {
309 for (
int j = 0; j < 3; ++j) {
311 model_.AddBinaryVariable(absl::StrCat(prefix,
"_", i,
"_", j)));
318 std::vector<std::vector<Variable>> xs_;
319 std::vector<std::vector<Variable>> ys_;
320 std::vector<std::vector<Variable>> zs_;
335TEST_P(IpParameterTest, RandomSeedIP) {
336 absl::flat_hash_set<std::vector<std::string>> solutions_seen;
337 for (
int seed = 10; seed < 200; seed += 10) {
338 SCOPED_TRACE(absl::StrCat(
"seed ", seed));
339 std::vector<std::string> result_for_seed;
340 for (
int trial = 0; trial < 10; ++trial) {
341 SCOPED_TRACE(absl::StrCat(
"trial ", trial));
342 SolveParameters params = {.random_seed = seed};
343 if (GetParam().parameter_support.supports_one_thread) {
346 absl::StatusOr<std::vector<std::string>> fingerprint =
347 VertexCover::SolveAndFingerprint(TestedSolver(), params);
348 if (!GetParam().parameter_support.supports_random_seed) {
350 HasSubstr(
"random_seed")));
354 result_for_seed = *fingerprint;
355 solutions_seen.insert(result_for_seed);
357 ASSERT_EQ(result_for_seed, *fingerprint);
364 EXPECT_GE(solutions_seen.size(), 3);
376absl::StatusOr<std::pair<SolveStats, std::string>> SolveForIPPresolve(
378 std::ostringstream oss;
379 Model
model(
"easy_presolve_model");
380 model.set_maximize();
381 for (
int i = 0;
i < 4; ++
i) {
382 Variable
x =
model.AddVariable(0.0, 1.0,
true);
383 Variable
y =
model.AddVariable(0.0, 1.0,
true);
385 model.AddLinearConstraint(
x +
y == 1.0);
386 model.AddLinearConstraint(
x >=
y);
394 return std::make_pair(result.solve_stats, oss.str());
406TEST_P(IpParameterTest, PresolveOff) {
407 if (!GetParam().parameter_support.supports_presolve) {
411 absl::StatusOr<std::pair<SolveStats, std::string>> stats_and_logs =
412 SolveForIPPresolve(TestedSolver(),
false);
414 const auto [solve_stats, logs] = stats_and_logs.value();
415 if (GetParam().solve_result_support.iteration_stats) {
416 EXPECT_GE(solve_stats.barrier_iterations + solve_stats.simplex_iterations +
417 solve_stats.first_order_iterations,
420 EXPECT_THAT(logs,
Not(testing::ContainsRegex(GetParam().presolved_regexp)));
423TEST_P(IpParameterTest, PresolveOn) {
424 absl::StatusOr<std::pair<SolveStats, std::string>> stats_and_logs =
425 SolveForIPPresolve(TestedSolver(),
true);
426 if (!GetParam().parameter_support.supports_presolve) {
428 HasSubstr(
"presolve")));
432 const auto [solve_stats, logs] = stats_and_logs.value();
433 if (!GetParam().solve_result_support.iteration_stats) {
434 EXPECT_EQ(solve_stats.barrier_iterations, 0);
435 EXPECT_EQ(solve_stats.simplex_iterations, 0);
436 EXPECT_EQ(solve_stats.first_order_iterations, 0);
438 EXPECT_THAT(logs, testing::ContainsRegex(GetParam().presolved_regexp));
442absl::StatusOr<SolveStats> SolveForCuts(
SolverType solver_type,
bool use_cuts) {
444 model.set_maximize();
445 for (
int i = 0;
i < 10; ++
i) {
446 const Variable
x =
model.AddVariable(0.0, 1.0,
true);
447 const Variable
y =
model.AddVariable(0.0, 1.0,
true);
448 model.AddToObjective(2 *
x +
y);
452 model.AddLinearConstraint(3 *
x + 2 *
y <= 4);
460 return result.solve_stats;
463TEST_P(IpParameterTest, CutsOff) {
464 if (!GetParam().parameter_support.supports_presolve) {
465 GTEST_SKIP() <<
"Skipping test, this test requires disabling presolve "
466 "which is not supported.";
468 if (!GetParam().parameter_support.supports_cuts) {
473 SolveForCuts(TestedSolver(),
false));
474 if (GetParam().solve_result_support.node_count) {
475 EXPECT_GT(solve_stats.node_count, 1);
478TEST_P(IpParameterTest, CutsOn) {
479 if (!GetParam().parameter_support.supports_presolve) {
480 GTEST_SKIP() <<
"Skipping test, this test requires disabling presolve "
481 "which is not supported.";
483 absl::StatusOr<SolveStats> solve_stats =
484 SolveForCuts(TestedSolver(),
true);
485 if (!GetParam().parameter_support.supports_cuts) {
491 if (GetParam().solve_result_support.node_count) {
492 EXPECT_EQ(solve_stats->node_count, 1);
498absl::StatusOr<SolveStats> SolveForRootLp(
500 const ParameterSupport& parameter_support) {
501 VertexCover vertex_cover;
502 SolveParameters params = {.lp_algorithm = algorithm};
504 if (parameter_support.supports_one_thread) {
509 if (parameter_support.supports_presolve) {
514 const SolveResult result,
515 Solve(vertex_cover.model(), solver_type, {.parameters = params}));
517 return result.solve_stats;
520TEST_P(IpParameterTest, RootLPAlgorithmPrimal) {
521 const absl::StatusOr<SolveStats> stats =
523 GetParam().parameter_support);
524 if (!GetParam().parameter_support.supports_lp_algorithm_simplex) {
526 HasSubstr(
"lp_algorithm")));
530 if (GetParam().solve_result_support.iteration_stats) {
531 EXPECT_GT(stats->simplex_iterations, 0);
532 EXPECT_EQ(stats->barrier_iterations, 0);
533 EXPECT_EQ(stats->first_order_iterations, 0);
537TEST_P(IpParameterTest, RootLPAlgorithmDual) {
538 const absl::StatusOr<SolveStats> stats = SolveForRootLp(
540 if (!GetParam().parameter_support.supports_lp_algorithm_simplex) {
542 HasSubstr(
"lp_algorithm")));
546 if (GetParam().solve_result_support.iteration_stats) {
547 EXPECT_GT(stats->simplex_iterations, 0);
548 EXPECT_EQ(stats->barrier_iterations, 0);
549 EXPECT_EQ(stats->first_order_iterations, 0);
553TEST_P(IpParameterTest, RootLPAlgorithmBarrier) {
554 const absl::StatusOr<SolveStats> stats = SolveForRootLp(
556 if (!GetParam().parameter_support.supports_lp_algorithm_barrier) {
558 HasSubstr(
"lp_algorithm")));
562 if (GetParam().solve_result_support.iteration_stats) {
563 EXPECT_GT(stats->barrier_iterations, 0);
571TEST_P(IpParameterTest, RootLPAlgorithmFirstOrder) {
574 GetParam().parameter_support),
575 StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr(
"lp_algorithm")));
578TEST_P(IpParameterTest, IterationLimitIP) {
580 Model
model(
"Iteration limit IP");
581 std::vector<Variable>
x;
583 for (
int i = 0;
i < n; ++
i) {
584 x.push_back(
model.AddIntegerVariable(0.0, 1.0));
586 for (
int i = 0;
i < n; ++
i) {
587 for (
int j = i + 1; j < n; ++j) {
588 model.AddLinearConstraint(3 *
x[i] + 5 *
x[j] <= 7);
592 SolveParameters params = {.iteration_limit = 1};
595 if (GetParam().parameter_support.supports_presolve) {
598 if (GetParam().parameter_support.supports_heuristics) {
601 if (GetParam().parameter_support.supports_one_thread) {
604 const absl::StatusOr<SolveResult> result =
605 Solve(
model, TestedSolver(), {.parameters = params});
606 if (!GetParam().parameter_support.supports_iteration_limit) {
608 HasSubstr(
"iteration_limit")));
614 .solve_result_support.termination_limit)));
617TEST_P(IpParameterTest, NodeLimit) {
620 <<
"This test does not work for HiGHS, which cannot disable cuts and "
621 "where disabling primal heuristics seems to have little effect, see "
622 "https://paste.googleplex.com/5694421105377280";
625 GTEST_SKIP() <<
"This test does not work with SCIP v900";
628 SolveParameters params = {.node_limit = 1};
631 if (GetParam().parameter_support.supports_presolve) {
634 if (GetParam().parameter_support.supports_heuristics) {
637 if (GetParam().parameter_support.supports_cuts) {
640 const absl::StatusOr<SolveResult> result =
641 Solve(*
model, GetParam().solver_type, {.parameters = params});
642 if (!GetParam().parameter_support.supports_node_limit) {
644 HasSubstr(
"node_limit")));
650 .solve_result_support.termination_limit)));
682absl::StatusOr<SolveResult> SolveForGapLimit(
const int k,
const int n,
684 const SolveParameters params) {
685 Model
model(
"Absolute gap limit IP");
686 std::vector<Variable>
x;
687 for (
int i = 0;
i < n; ++
i) {
688 x.push_back(
model.AddBinaryVariable());
691 for (
int i = 0;
i < n; ++
i) {
692 for (
int j = i + 1; j < n; ++j) {
693 model.AddLinearConstraint(
x[i] +
x[j] <= 1);
697 model.AddContinuousVariable(1, std::numeric_limits<double>::infinity());
698 model.AddLinearConstraint(k *
Sum(
x) +
y <= k + 1);
699 model.AddLinearConstraint(k *
Sum(
x) -
y >= -1);
701 return Solve(
model, solver_type, {.parameters = params});
704TEST_P(IpParameterTest, AbsoluteGapLimit) {
707 <<
"This test does not work for HiGHS, we cannot weaken the root node "
708 "bound enough, see https://paste.googleplex.com/6416319233654784";
712 const double lp_opt = k / 2 + 1;
713 const double ip_opt = 1;
714 const double abs_lp_gap = lp_opt - ip_opt;
717 const double best_bound_differentiator = lp_opt - abs_lp_gap / 2.0;
722 const SolveResult default_result,
723 SolveForGapLimit(k, n, TestedSolver(), SolveParameters{}));
725 EXPECT_LT(default_result.termination.objective_bounds.dual_bound,
727 EXPECT_LT(default_result.termination.objective_bounds.dual_bound,
728 best_bound_differentiator);
732 SolveParameters params = {.absolute_gap_tolerance = abs_lp_gap + 0.1};
736 if (GetParam().parameter_support.supports_presolve) {
739 if (GetParam().parameter_support.supports_one_thread) {
742 if (GetParam().parameter_support.supports_cuts) {
745 const absl::StatusOr<SolveResult> gap_tolerance_result =
746 SolveForGapLimit(k, n, TestedSolver(), params);
747 if (!GetParam().parameter_support.supports_absolute_gap_tolerance) {
749 StatusIs(absl::StatusCode::kInvalidArgument,
750 HasSubstr(
"absolute_gap_tolerance")));
753 EXPECT_EQ(gap_tolerance_result->termination.reason,
760 EXPECT_GT(gap_tolerance_result->termination.objective_bounds.dual_bound,
761 best_bound_differentiator);
765TEST_P(IpParameterTest, RelativeGapLimit) {
768 <<
"This test does not work for HiGHS, we cannot weaken the root node "
769 "bound enough, see https://paste.googleplex.com/6416319233654784";
773 <<
"This test does not work for GLPK, not clear why this isn't "
774 "working, logs here: https://paste.googleplex.com/6080056622317568";
778 const double lp_opt = k / 2.0 + 1;
779 const double ip_opt = 1;
780 const double abs_lp_gap = lp_opt - ip_opt;
781 const double rel_lp_gap = (lp_opt - ip_opt) / ip_opt;
784 const double best_bound_differentiator = lp_opt - abs_lp_gap / 2.0;
789 const SolveResult default_result,
790 SolveForGapLimit(k, n, TestedSolver(), SolveParameters()));
792 EXPECT_LT(default_result.termination.objective_bounds.dual_bound,
794 EXPECT_LT(default_result.termination.objective_bounds.dual_bound,
795 best_bound_differentiator);
799 SolveParameters params = {.relative_gap_tolerance = rel_lp_gap + 0.1};
803 if (GetParam().parameter_support.supports_presolve) {
806 if (GetParam().parameter_support.supports_one_thread) {
809 if (GetParam().parameter_support.supports_cuts) {
813 SolveForGapLimit(k, n, TestedSolver(), params));
814 EXPECT_EQ(gap_tolerance_result.termination.reason,
821 EXPECT_GT(gap_tolerance_result.termination.objective_bounds.dual_bound,
822 best_bound_differentiator);
826TEST_P(IpParameterTest, CutoffLimit) {
828 const Variable
x =
model.AddBinaryVariable();
832 const absl::StatusOr<SolveResult> result =
Solve(
833 model, GetParam().solver_type, {.parameters = {.cutoff_limit = -1.0}});
834 if (!GetParam().parameter_support.supports_cutoff) {
836 HasSubstr(
"cutoff")));
846 {.parameters = {.cutoff_limit = 0.5}}),
850TEST_P(IpParameterTest, ObjectiveLimit) {
852 SolveParameters params = {.objective_limit = 3.5};
855 if (GetParam().parameter_support.supports_presolve) {
864 const absl::StatusOr<SolveResult> result =
865 Solve(*
model, GetParam().solver_type, {.parameters = params});
866 if (!GetParam().parameter_support.supports_objective_limit) {
868 HasSubstr(
"objective_limit")));
872 ASSERT_TRUE(result->has_primal_feasible_solution());
873 EXPECT_LE(result->objective_value(), 5.0 + 1.0e-5);
877 params.objective_limit = 20.0;
882TEST_P(IpParameterTest, BestBoundLimit) {
888 SolveParameters params = {.best_bound_limit = 60.0};
891 if (GetParam().parameter_support.supports_presolve) {
895 const absl::StatusOr<SolveResult> result =
896 Solve(*
model, GetParam().solver_type, {.parameters = params});
897 if (!GetParam().parameter_support.supports_bound_limit) {
899 HasSubstr(
"best_bound_limit")));
903 EXPECT_LE(result->termination.objective_bounds.dual_bound, 60.0);
904 EXPECT_GE(result->termination.objective_bounds.dual_bound, 8.0);
908 params.best_bound_limit = 4.0;
913TEST_P(IpParameterTest, SolutionLimitOneWithHint) {
914 if (!GetParam().hint_supported) {
915 GTEST_SKIP() <<
"Test requires a hint";
918 const Variable
x =
model.AddBinaryVariable();
920 ModelSolveParameters model_params;
921 model_params.solution_hints.push_back({.variable_values = {{
x, 1.0}}});
922 SolveParameters params = {.solution_limit = 1};
923 if (GetParam().parameter_support.supports_presolve) {
929 if (GetParam().parameter_support.supports_heuristics) {
932 const absl::StatusOr<SolveResult> result =
934 {.parameters = params, .model_parameters = model_params});
935 if (!GetParam().parameter_support.supports_solution_limit_one) {
937 HasSubstr(
"solution_limit")));
943 .solve_result_support.termination_limit)));
944 EXPECT_EQ(result->solutions.size(), 1);
946 .variable_values = {{
x, 1.0}},
947 .objective_value = 1.0,
951TEST_P(IpParameterTest, SolutionLimitOneAndCutoff) {
952 if (!(GetParam().parameter_support.supports_cutoff &&
953 GetParam().parameter_support.supports_solution_limit_one)) {
957 if (!GetParam().hint_supported) {
958 GTEST_SKIP() <<
"Test requires a hint";
961 const Variable
x =
model.AddBinaryVariable();
962 const Variable
y =
model.AddBinaryVariable();
963 const Variable z =
model.AddBinaryVariable();
964 model.Maximize(
x + 2 *
y + 3 * z);
965 model.AddLinearConstraint(
x +
y + z <= 1);
974 SolveParameters params = {.cutoff_limit = 1.5, .solution_limit = 1};
975 if (GetParam().parameter_support.supports_presolve) {
980 if (GetParam().parameter_support.supports_one_thread) {
983 ModelSolveParameters model_params;
984 model_params.solution_hints.push_back(ModelSolveParameters::SolutionHint{
985 .variable_values = {{
x, 0.0}, {
y, 1.0}, {z, 0.0}}});
986 model_params.solution_hints.push_back(ModelSolveParameters::SolutionHint{
987 .variable_values = {{
x, 1.0}, {
y, 0.0}, {z, 0.0}}});
989 const SolveResult result,
991 {.parameters = params, .model_parameters = model_params}));
995 .solve_result_support.termination_limit));
996 ASSERT_TRUE(result.has_primal_feasible_solution());
997 EXPECT_NEAR(result.objective_value(), 2.0, 1e-5);
998 EXPECT_EQ(result.solutions.size(), 1);
1002TEST_P(IpParameterTest, NoSolutionsBelowCutoffEarlyTermination) {
1004 GTEST_SKIP() <<
"This test does not work with SCIP v900";
1006 if (!(GetParam().parameter_support.supports_cutoff)) {
1010 if (!GetParam().hint_supported) {
1011 GTEST_SKIP() <<
"Test requires a hint";
1014 ModelSolveParameters model_params;
1016 SolveParameters params = GetParam().stop_before_optimal;
1017 params.cutoff_limit = 6.5;
1019 const SolveResult result,
1021 {.parameters = params, .model_parameters = model_params}));
1035absl::StatusOr<std::unique_ptr<Model>> LoadMiplibInstance(
1036 absl::string_view
name) {
1038 const ModelProto model_proto,
1039 ReadMpsFile(absl::StrCat(
"ortools/math_opt/solver_tests/testdata/",
name,
1046absl::StatusOr<std::unique_ptr<Model>>
1048 return LoadMiplibInstance(
"23588");
1051absl::StatusOr<std::unique_ptr<Model>>
1053 return LoadMiplibInstance(
"beavma");
1060 GTEST_SKIP() <<
"Test skipped, too slow unless compiled with -c opt.";
1070 SolveParameters params = GetParam().base_parameters;
1071 params.time_limit = absl::Milliseconds(1);
1073 const SolveResult result,
1074 Solve(*
model, GetParam().solver_type, {.parameters = params}));
1076 GetParam().allow_limit_undetermined));
1080 EXPECT_LE(result.solve_stats.solve_time, absl::Seconds(1));
1083TEST_P(LargeInstanceIpParameterTest, IterationLimit) {
1085 SolveParameters params = GetParam().base_parameters;
1086 params.iteration_limit = 1;
1087 const absl::StatusOr<SolveResult> result =
1088 Solve(*
model, GetParam().solver_type, {.parameters = params});
1089 if (!GetParam().parameter_support.supports_iteration_limit) {
1091 HasSubstr(
"iteration_limit")));
1097 EXPECT_LE(result->solve_stats.simplex_iterations, 1);
1098 EXPECT_LE(result->solve_stats.barrier_iterations, 1);
1101TEST_P(LargeInstanceIpParameterTest, NodeLimit) {
1103 GTEST_SKIP() <<
"Ignoring this test as Highs 1.7+ returns unimplemented";
1106 SolveParameters params = GetParam().base_parameters;
1107 params.node_limit = 1;
1108 const absl::StatusOr<SolveResult> result =
1109 Solve(*
model, GetParam().solver_type, {.parameters = params});
1110 if (!GetParam().parameter_support.supports_node_limit) {
1112 HasSubstr(
"node_limit")));
1117 EXPECT_LE(result->solve_stats.node_count, 1);
1120TEST_P(LargeInstanceIpParameterTest, CutoffLimit) {
1122 SolveParameters params = GetParam().base_parameters;
1125 params.cutoff_limit = kOptimalObjective - 10.0;
1126 const absl::StatusOr<SolveResult> result =
1127 Solve(*
model, GetParam().solver_type, {.parameters = params});
1128 if (!GetParam().parameter_support.supports_cutoff) {
1130 HasSubstr(
"cutoff_limit")));
1137 EXPECT_FALSE(result->has_primal_feasible_solution());
1148TEST_P(LargeInstanceIpParameterTest, ObjectiveLimit) {
1150 SolveParameters params = GetParam().base_parameters;
1153 params.objective_limit = 1.5 * kOptimalObjective;
1154 const absl::StatusOr<SolveResult> result =
1155 Solve(*
model, GetParam().solver_type, {.parameters = params});
1156 if (!GetParam().parameter_support.supports_objective_limit) {
1158 HasSubstr(
"objective_limit")));
1168 EXPECT_LE(result->objective_value(), params.objective_limit);
1172 EXPECT_GE(result->objective_value(), kOptimalObjective + 1.0);
1181 params.objective_limit = kOptimalObjective - 10.0;
1186TEST_P(LargeInstanceIpParameterTest, BestBoundLimit) {
1188 SolveParameters params = GetParam().base_parameters;
1189 params.best_bound_limit = kLpRelaxationObjective - 1.0;
1190 const absl::StatusOr<SolveResult> result =
1191 Solve(*
model, GetParam().solver_type, {.parameters = params});
1192 if (!GetParam().parameter_support.supports_bound_limit) {
1194 HasSubstr(
"best_bound_limit")));
1203 EXPECT_LE(result->solve_stats.node_count, 1);
1206 EXPECT_GE(result->best_objective_bound(), params.best_bound_limit);
1209 EXPECT_GE(result->termination.objective_bounds.primal_bound,
1210 kOptimalObjective - 1.0);
1219 params.best_bound_limit = kOptimalObjective + 10.0;
1224TEST_P(LargeInstanceIpParameterTest, SolutionLimit) {
1226 GTEST_SKIP() <<
"Ignoring this test as Highs 1.7+ returns unimplemented";
1229 SolveParameters params = GetParam().base_parameters;
1230 params.solution_limit = 1;
1231 const absl::StatusOr<SolveResult> result =
1232 Solve(*
model, GetParam().solver_type, {.parameters = params});
1233 if (!GetParam().parameter_support.supports_solution_limit_one) {
1235 HasSubstr(
"solution_limit")));
1245 EXPECT_GE(result->objective_value() - result->best_objective_bound(), 1.0);
1257TEST_P(LargeInstanceIpParameterTest, AbsoluteGapTolerance) {
1259 SolveParameters params = GetParam().base_parameters;
1260 const double absolute_lp_relax_gap =
1261 kOptimalObjective - kLpRelaxationObjective;
1262 params.absolute_gap_tolerance = absolute_lp_relax_gap;
1263 const absl::StatusOr<SolveResult> result =
1264 Solve(*
model, GetParam().solver_type, {.parameters = params});
1265 if (!GetParam().parameter_support.supports_absolute_gap_tolerance) {
1267 HasSubstr(
"absolute_gap_tolerance")));
1272 EXPECT_GE(result->termination.objective_bounds.primal_bound -
1273 result->termination.objective_bounds.dual_bound,
1274 absolute_lp_relax_gap / 40.0);
1282TEST_P(LargeInstanceIpParameterTest, RelativeGapTolerance) {
1284 SolveParameters params = GetParam().base_parameters;
1285 const double absolute_lp_relax_gap =
1286 kOptimalObjective - kLpRelaxationObjective;
1287 params.relative_gap_tolerance = 2 * absolute_lp_relax_gap / kOptimalObjective;
1288 const absl::StatusOr<SolveResult> result =
1289 Solve(*
model, GetParam().solver_type, {.parameters = params});
1295 EXPECT_GE(result->termination.objective_bounds.primal_bound -
1296 result->termination.objective_bounds.dual_bound,
1297 absolute_lp_relax_gap / 40.0);
1300TEST_P(LargeInstanceIpParameterTest, Cuts) {
1301 if (!GetParam().parameter_support.supports_node_limit) {
1302 GTEST_SKIP() <<
"Skipping test, requires node_limit but is not supported.";
1305 SolveParameters params = GetParam().base_parameters;
1308 params.node_limit = 1;
1310 const absl::StatusOr<SolveResult> result_cuts_off =
1311 Solve(*
model, GetParam().solver_type, {.parameters = params});
1312 if (!GetParam().parameter_support.supports_cuts) {
1314 HasSubstr(
"cuts")));
1318 const double bound_cuts_off = result_cuts_off->best_objective_bound();
1322 const SolveResult result_cuts_on,
1323 Solve(*
model, GetParam().solver_type, {.parameters = params}));
1324 const double bound_cuts_on = result_cuts_on.best_objective_bound();
1328 EXPECT_GE(bound_cuts_on, bound_cuts_off + 1.0);
#define ASSIGN_OR_RETURN(lhs, rexpr)
#define RETURN_IF_ERROR(expr)
absl::StatusOr< std::unique_ptr< Model > > LoadBeavma()
absl::StatusOr< std::unique_ptr< Model > > Load23588()
static absl::StatusOr< std::unique_ptr< Model > > FromModelProto(const ModelProto &model_proto)
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.
@ kOptimal
A provably optimal solution (up to numerical tolerances) has been found.
<=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()))
LPAlgorithm
Selects an algorithm for solving linear programs.
When the underlying default is used When the feature cannot be turned kOff will return an error If the feature is enabled by the solver default is typically If the feature is supported
mapped to kMedium.
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)
Matcher< SolveResult > HasSolution(PrimalSolution expected, const double tolerance)
SolveResult has a primal solution matching expected within tolerance.
@ kFeasible
Solver claims the solution is feasible.
testing::Matcher< SolveResult > TerminatesWithReasonFeasible(const Limit expected, const bool allow_limit_undetermined)
Matcher< SolveResult > TerminatesWith(const TerminationReason expected)
MessageCallback PrinterMessageCallback(std::ostream &output_stream, const absl::string_view prefix)
absl::StatusOr< ModelProto > ReadMpsFile(const absl::string_view filename)
testing::Matcher< SolveResult > TerminatesWithLimit(const Limit expected, const bool allow_limit_undetermined)
@ kTime
The algorithm stopped after a user-specified computation time.
std::unique_ptr< Model > DenseIndependentSet(const bool integer, const int n)
absl::string_view EnumToString(E value)
ModelSolveParameters::SolutionHint DenseIndependentSetHint5(const Model &model)
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.
std::string ProtobufShortDebugString(const P &message)
constexpr bool kAnyXsanEnabled
internal::StatusIsMatcher StatusIs(CodeMatcher code_matcher, MessageMatcher message_matcher)
StatusBuilder InternalErrorBuilder()
#define ASSERT_OK(expression)
#define ASSERT_OK_AND_ASSIGN(lhs, rexpr)
ParameterSupport parameter_support
std::string presolved_regexp
SolverType solver_type
The tested solver.
SolveParameters stop_before_optimal
SolveResultSupport solve_result_support
ParameterSupport parameter_support
bool allow_limit_undetermined
SolverType solver_type
The tested solver.
SolveParameters base_parameters
bool supports_n_threads
Supports setting threads to an arbitrary value.
bool supports_one_thread
Supports setting threads = 1 (all but HiGHS support this).
bool supports_lp_algorithm_barrier
bool supports_iteration_limit
bool supports_objective_limit
bool supports_lp_algorithm_simplex
bool supports_absolute_gap_tolerance
bool supports_random_seed
bool supports_solution_limit_one
Indicates if setting solution_limit with value 1 is supported.
bool supports_bound_limit
SolveParametersProto Proto() const
bool node_count
If SolveStats reports the node count.
bool iteration_stats
If SolveStats reports iteration stats for LP/IPM/FOM.