From b54a8adbee3970fb379aa39304bbd2291f543e4a Mon Sep 17 00:00:00 2001 From: Laurent Perron Date: Sat, 5 Oct 2024 10:54:51 +0200 Subject: [PATCH 01/11] add __repr__ to Domain: fix #4399 --- ortools/util/python/BUILD.bazel | 1 + ortools/util/python/sorted_interval_list.cc | 5 +++++ ortools/util/python/sorted_interval_list_test.py | 5 +++++ 3 files changed, 11 insertions(+) diff --git a/ortools/util/python/BUILD.bazel b/ortools/util/python/BUILD.bazel index 925cf571dce..c67d864d577 100644 --- a/ortools/util/python/BUILD.bazel +++ b/ortools/util/python/BUILD.bazel @@ -31,6 +31,7 @@ pybind_extension( deps = [ ":sorted_interval_list_doc", "//ortools/util:sorted_interval_list", + "@com_google_absl//absl/strings", ], ) diff --git a/ortools/util/python/sorted_interval_list.cc b/ortools/util/python/sorted_interval_list.cc index 55e82d54445..d2a15ec55e5 100644 --- a/ortools/util/python/sorted_interval_list.cc +++ b/ortools/util/python/sorted_interval_list.cc @@ -15,6 +15,7 @@ #include +#include "absl/strings/str_cat.h" #include "ortools/util/python/sorted_interval_list_doc.h" #include "pybind11/cast.h" #include "pybind11/pybind11.h" @@ -57,6 +58,10 @@ PYBIND11_MODULE(sorted_interval_list, m) { .def("union_with", &Domain::UnionWith, DOC(operations_research, Domain, UnionWith), arg("domain")) .def("__str__", &Domain::ToString) + .def("__repr__", + [](const Domain& domain) { + return absl::StrCat("Domain(", domain.ToString(), ")"); + }) // Compatibility with pre PEP8 APIs. .def_static("AllValues", &Domain::AllValues, DOC(operations_research, Domain, AllValues)) diff --git a/ortools/util/python/sorted_interval_list_test.py b/ortools/util/python/sorted_interval_list_test.py index b33eb7d878b..2f03099842f 100755 --- a/ortools/util/python/sorted_interval_list_test.py +++ b/ortools/util/python/sorted_interval_list_test.py @@ -85,6 +85,11 @@ def testComplement(self): self.assertEqual([-9223372036854775808, 5], d1.flattened_intervals()) self.assertEqual([6, 9223372036854775807], d2.flattened_intervals()) + def testStr(self): + d1 = sorted_interval_list.Domain(0, 5) + self.assertEqual(str(d1), "[0,5]") + self.assertEqual(repr(d1), "Domain([0,5])") + if __name__ == "__main__": absltest.main() From df0a6f479daa9527d61cbb29d9470cadc4c2fd68 Mon Sep 17 00:00:00 2001 From: Laurent Perron Date: Sat, 5 Oct 2024 11:44:53 +0200 Subject: [PATCH 02/11] fz accepts -i --- ortools/flatzinc/cp-sat.msc.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ortools/flatzinc/cp-sat.msc.in b/ortools/flatzinc/cp-sat.msc.in index 82bfbf842af..f06573430cd 100644 --- a/ortools/flatzinc/cp-sat.msc.in +++ b/ortools/flatzinc/cp-sat.msc.in @@ -6,7 +6,7 @@ "mznlib": "../cp-sat", "executable": "@FZ_REL_INSTALL_BINARY@", "tags": ["cp-sat", "cp", "lcg", "int"], - "stdFlags": ["-a", "-f", "-p", "-r", "-s", "-v"], + "stdFlags": ["-a", "-i", "-f", "-p", "-r", "-s", "-v"], "extraFlags": [ ["--params", "Provide parameters interpreted as a text SatParameters proto", "string", ""] ], From cbb5660c649136b1c59166b61457b6cf075b6f6a Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Mon, 30 Sep 2024 15:30:44 +0200 Subject: [PATCH 03/11] bazel: don't use native rules for samples --- ortools/constraint_solver/samples/code_samples.bzl | 6 ++++-- ortools/linear_solver/samples/code_samples.bzl | 8 +++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/ortools/constraint_solver/samples/code_samples.bzl b/ortools/constraint_solver/samples/code_samples.bzl index 6c86853446f..be6dc11bf15 100644 --- a/ortools/constraint_solver/samples/code_samples.bzl +++ b/ortools/constraint_solver/samples/code_samples.bzl @@ -13,8 +13,10 @@ """Helper macro to compile and test code samples.""" +load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_test") + def code_sample_cc(name): - native.cc_binary( + cc_binary( name = name + "_cc", srcs = [name + ".cc"], deps = [ @@ -25,7 +27,7 @@ def code_sample_cc(name): ], ) - native.cc_test( + cc_test( name = name + "_cc_test", size = "small", srcs = [name + ".cc"], diff --git a/ortools/linear_solver/samples/code_samples.bzl b/ortools/linear_solver/samples/code_samples.bzl index aab743a6903..d91202efec4 100644 --- a/ortools/linear_solver/samples/code_samples.bzl +++ b/ortools/linear_solver/samples/code_samples.bzl @@ -14,10 +14,12 @@ """Helper macro to compile and test code samples.""" load("@pip_deps//:requirements.bzl", "requirement") +load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_test") +load("@rules_java//java:defs.bzl", "java_test") load("@rules_python//python:defs.bzl", "py_binary", "py_test") def code_sample_cc(name): - native.cc_binary( + cc_binary( name = name + "_cc", srcs = [name + ".cc"], deps = [ @@ -28,7 +30,7 @@ def code_sample_cc(name): ], ) - native.cc_test( + cc_test( name = name + "_cc_test", size = "small", srcs = [name + ".cc"], @@ -78,7 +80,7 @@ def code_sample_py(name): ) def code_sample_java(name): - native.java_test( + java_test( name = name + "_java_test", size = "small", srcs = [name + ".java"], From a72b8b25ff9e73fdf0ff6b9338ac142516f6ffe8 Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Thu, 3 Oct 2024 10:59:28 +0200 Subject: [PATCH 04/11] sat: export tests from google3 --- ortools/base/BUILD.bazel | 10 + ortools/base/parse_test_proto.h | 53 + ortools/sat/2d_packing_brute_force_test.cc | 361 ++++ ortools/sat/BUILD.bazel | 265 +++ ortools/sat/cp_constraints.h | 2 +- ortools/sat/cp_model_checker_test.cc | 666 ++++++++ ortools/sat/cp_model_expand_test.cc | 1793 ++++++++++++++++++++ ortools/sat/cp_model_lns.cc | 47 +- ortools/sat/cp_model_postsolve_test.cc | 347 ++++ ortools/sat/cp_model_search_test.cc | 297 ++++ ortools/sat/cp_model_solver.cc | 2 +- ortools/sat/cuts.cc | 95 +- ortools/sat/flaky_models_test.cc | 101 ++ ortools/sat/integer_expr_test.cc | 1644 ++++++++++++++++++ ortools/sat/linear_model_test.cc | 144 ++ ortools/sat/linear_relaxation_test.cc | 1200 +++++++++++++ ortools/sat/presolve_context_test.cc | 1038 +++++++++++ ortools/sat/presolve_util_test.cc | 513 ++++++ ortools/sat/rins_test.cc | 173 ++ ortools/sat/table_test.cc | 603 +++++++ ortools/sat/var_domination_test.cc | 614 +++++++ 21 files changed, 9909 insertions(+), 59 deletions(-) create mode 100644 ortools/base/parse_test_proto.h create mode 100644 ortools/sat/2d_packing_brute_force_test.cc create mode 100644 ortools/sat/cp_model_checker_test.cc create mode 100644 ortools/sat/cp_model_expand_test.cc create mode 100644 ortools/sat/cp_model_postsolve_test.cc create mode 100644 ortools/sat/cp_model_search_test.cc create mode 100644 ortools/sat/flaky_models_test.cc create mode 100644 ortools/sat/integer_expr_test.cc create mode 100644 ortools/sat/linear_model_test.cc create mode 100644 ortools/sat/linear_relaxation_test.cc create mode 100644 ortools/sat/presolve_context_test.cc create mode 100644 ortools/sat/presolve_util_test.cc create mode 100644 ortools/sat/rins_test.cc create mode 100644 ortools/sat/table_test.cc create mode 100644 ortools/sat/var_domination_test.cc diff --git a/ortools/base/BUILD.bazel b/ortools/base/BUILD.bazel index 9e3a855c25e..1d632f6df77 100644 --- a/ortools/base/BUILD.bazel +++ b/ortools/base/BUILD.bazel @@ -410,6 +410,16 @@ cc_library( ], ) +cc_library( + name = "parse_test_proto", + hdrs = ["parse_test_proto.h"], + deps = [ + ":gmock", + "@com_google_absl//absl/log:check", + "@com_google_protobuf//:protobuf", + ], +) + cc_library( name = "path", srcs = ["path.cc"], diff --git a/ortools/base/parse_test_proto.h b/ortools/base/parse_test_proto.h new file mode 100644 index 00000000000..e7979b7c16c --- /dev/null +++ b/ortools/base/parse_test_proto.h @@ -0,0 +1,53 @@ +// Copyright 2010-2024 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OR_TOOLS_BASE_PARSE_TEST_PROTO_H_ +#define OR_TOOLS_BASE_PARSE_TEST_PROTO_H_ + +#include +#include +#include + +#include "google/protobuf/message.h" +#include "google/protobuf/text_format.h" +#include "gtest/gtest.h" + +namespace google::protobuf::contrib::parse_proto { + +namespace parse_proto_internal { + +class ParseProtoHelper { + public: + explicit ParseProtoHelper(std::string_view asciipb) : asciipb_(asciipb) {} + template + operator T() { // NOLINT(runtime/explicit) + T result; + const bool ok = ::google::protobuf::TextFormat::TextFormat::ParseFromString( + asciipb_, &result); + EXPECT_TRUE(ok) << "Failed to parse text proto: " << asciipb_; + return result; + } + + private: + const std::string asciipb_; +}; + +} // namespace parse_proto_internal + +parse_proto_internal::ParseProtoHelper ParseTestProto(std::string_view input) { + return parse_proto_internal::ParseProtoHelper(input); +} + +} // namespace google::protobuf::contrib::parse_proto + +#endif // OR_TOOLS_BASE_PARSE_TEST_PROTO_H_ diff --git a/ortools/sat/2d_packing_brute_force_test.cc b/ortools/sat/2d_packing_brute_force_test.cc new file mode 100644 index 00000000000..0efbbbeb272 --- /dev/null +++ b/ortools/sat/2d_packing_brute_force_test.cc @@ -0,0 +1,361 @@ +// Copyright 2010-2024 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/sat/2d_packing_brute_force.h" + +#include +#include +#include +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/random/bit_gen_ref.h" +#include "absl/random/distributions.h" +#include "absl/random/random.h" +#include "absl/types/span.h" +#include "benchmark/benchmark.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/sat/cp_model.h" +#include "ortools/sat/cp_model_solver.h" +#include "ortools/sat/diffn_util.h" +#include "ortools/sat/integer.h" + +namespace operations_research { +namespace sat { +namespace { + +std::vector SolveOrthogonalPacking( + absl::Span sizes_x, + absl::Span sizes_y, + std::pair bounding_box_size) { + const int num_items = sizes_x.size(); + CHECK_EQ(num_items, sizes_y.size()); + CHECK_GT(bounding_box_size.first, 0); + CHECK_GT(bounding_box_size.second, 0); + CpModelBuilder cp_model; + NoOverlap2DConstraint no_overlap_2d = cp_model.AddNoOverlap2D(); + std::vector start_x_vars; + std::vector start_y_vars; + for (int item = 0; item < num_items; ++item) { + IntVar start_x = cp_model.NewIntVar( + {0, bounding_box_size.first.value() - sizes_x[item].value()}); + IntVar start_y = cp_model.NewIntVar( + {0, bounding_box_size.second.value() - sizes_y[item].value()}); + start_x_vars.push_back(start_x); + start_y_vars.push_back(start_y); + + IntervalVar interval_x = + cp_model.NewFixedSizeIntervalVar(start_x, sizes_x[item].value()); + IntervalVar interval_y = + cp_model.NewFixedSizeIntervalVar(start_y, sizes_y[item].value()); + + no_overlap_2d.AddRectangle(interval_x, interval_y); + } + SatParameters parameters; + // Disable the propagator so we don't use the code we want to test. + parameters.set_use_area_energetic_reasoning_in_no_overlap_2d(false); + const CpSolverResponse response = + SolveWithParameters(cp_model.Build(), parameters); + if (response.status() != CpSolverStatus::OPTIMAL) { + return {}; + } + std::vector solution; + for (int i = 0; i < num_items; ++i) { + const IntegerValue start_x = + SolutionIntegerValue(response, start_x_vars[i]); + const IntegerValue start_y = + SolutionIntegerValue(response, start_y_vars[i]); + solution.push_back({.x_min = start_x, + .x_max = start_x + sizes_x[i], + .y_min = start_y, + .y_max = start_y + sizes_y[i]}); + } + return solution; +} + +bool CumulativeIsFeasible( + absl::Span sizes_x, + absl::Span sizes_y, + std::pair bounding_box_size, bool both_sides) { + const int num_items = sizes_x.size(); + CHECK_EQ(num_items, sizes_y.size()); + CHECK_GT(bounding_box_size.first, 0); + CHECK_GT(bounding_box_size.second, 0); + + CpModelBuilder cp_model; + CumulativeConstraint cumulative = + cp_model.AddCumulative(bounding_box_size.second.value()); + + for (int item = 0; item < num_items; ++item) { + const IntVar start_time = cp_model.NewIntVar( + {0, bounding_box_size.first.value() - sizes_x[item].value()}); + const IntervalVar start_time_interval_x = + cp_model.NewFixedSizeIntervalVar(start_time, sizes_x[item].value()); + cumulative.AddDemand(start_time_interval_x, sizes_y[item].value()); + } + if (both_sides) { + CumulativeConstraint cumulative_y = + cp_model.AddCumulative(bounding_box_size.first.value()); + for (int item = 0; item < num_items; ++item) { + const IntVar start_time = cp_model.NewIntVar( + {0, bounding_box_size.second.value() - sizes_y[item].value()}); + const IntervalVar start_time_interval_y = + cp_model.NewFixedSizeIntervalVar(start_time, sizes_y[item].value()); + cumulative_y.AddDemand(start_time_interval_y, sizes_x[item].value()); + } + } + const CpSolverResponse response = Solve(cp_model.Build()); + return (response.status() == CpSolverStatus::OPTIMAL); +} + +struct OppProblem { + std::vector items_x_sizes; + std::vector items_y_sizes; + std::pair bb_sizes; +}; + +OppProblem CreateRandomOppProblem(absl::BitGenRef random, int num_items) { + OppProblem result; + std::vector& items_x_sizes = result.items_x_sizes; + std::vector& items_y_sizes = result.items_y_sizes; + const int size = 300; + items_x_sizes.clear(); + items_y_sizes.clear(); + IntegerValue area = 0; + for (int i = 0; i < num_items; ++i) { + const IntegerValue x_size = absl::Uniform(random, 1, size); + const IntegerValue y_size = absl::Uniform(random, 1, size); + items_x_sizes.push_back(x_size); + items_y_sizes.push_back(y_size); + area += x_size * y_size; + } + const IntegerValue box_x_size = + absl::Uniform(random, size, static_cast(sqrt(num_items) * size)); + const IntegerValue box_y_size = + std::max(IntegerValue(size), (area + box_x_size - 1) / box_x_size); + result.bb_sizes = {box_x_size, box_y_size}; + return result; +} + +void CheckSolution(const OppProblem& problem, + absl::Span solution) { + CHECK_EQ(problem.items_x_sizes.size(), solution.size()); + for (const Rectangle& item : solution) { + CHECK_GE(item.x_min, 0); + CHECK_LE(item.x_max, problem.bb_sizes.first); + CHECK_GE(item.y_min, 0); + CHECK_LE(item.y_max, problem.bb_sizes.second); + } + + for (int i = 0; i < problem.items_x_sizes.size(); ++i) { + CHECK_EQ(problem.items_x_sizes[i], solution[i].SizeX()); + CHECK_EQ(problem.items_y_sizes[i], solution[i].SizeY()); + + for (int j = i + 1; j < problem.items_x_sizes.size(); ++j) { + CHECK(solution[i].IsDisjoint(solution[j])) + << " for solution: " + << RenderDot(Rectangle{.x_min = 0, + .x_max = problem.bb_sizes.first, + .y_min = 0, + .y_max = problem.bb_sizes.second}, + solution); + } + } +} + +TEST(CheckSolutionTest, CheckWithCPSat) { + EXPECT_TRUE( + SolveOrthogonalPacking({4, 4, 8, 8}, {6, 6, 5, 5}, {13, 10}).empty()); + EXPECT_FALSE( + SolveOrthogonalPacking({4, 4, 8, 8}, {6, 6, 5, 5}, {12, 12}).empty()); + + absl::BitGen random; + int feasible = 0; + for (int i = 0; i < 1000; ++i) { + const OppProblem problem = CreateRandomOppProblem(random, 6); + const auto brute_force_solution = BruteForceOrthogonalPacking( + problem.items_x_sizes, problem.items_y_sizes, problem.bb_sizes, + problem.items_x_sizes.size()); + CHECK(brute_force_solution.status != BruteForceResult::Status::kTooBig); + feasible += + brute_force_solution.status == BruteForceResult::Status::kFoundSolution; + if (brute_force_solution.status == + BruteForceResult::Status::kFoundSolution) { + CheckSolution(problem, brute_force_solution.positions_for_solution); + } + const auto solution = SolveOrthogonalPacking( + problem.items_x_sizes, problem.items_y_sizes, problem.bb_sizes); + if (!solution.empty()) { + CheckSolution(problem, solution); + } + EXPECT_EQ(brute_force_solution.status == + BruteForceResult::Status::kNoSolutionExists, + solution.empty()); + } + std::cout << "feasible: " << feasible << "\n"; +} + +// Example that is feasible for the cumulative in one dimension but not for 2d +// packing: +// +// digraph { +// graph [ bgcolor=lightgray width=18 height=12] +// node [style=filled] +// bb [fillcolor="grey" pos="9,6!" shape=box width=18 height=12] +// 0 [fillcolor="green" pos="5,2!" shape=box width=10 height=4] +// 1 [fillcolor="purple" pos="13,3!" shape=box width=6 height=6] +// 2 [fillcolor="red" pos="17,4!" shape=box width=2 height=8] +// 3 [fillcolor="blue" pos="13,10!" shape=box width=10 height=4] +// 4 [fillcolor="yellow" pos="5,9!" shape=box width=6 height=6] +// 5 [fillcolor="cyan" pos="1,8!" shape=box width=2 height=8] +// 6 [fillcolor="red" pos="7,5!" shape=box width=10 height=2] +// } +// +// +--------------------------------------------------------+ +// |****** @@@@@@@@@@@@@@@@@@ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx| +// |****** @@@@@@@@@@@@@@@@@@ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx| +// |****** @@@@@@@@@@@@@@@@@@ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx| +// |****** @@@@@@@@@@@@@@@@@@ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx| +// |****** @@@@@@@@@@@@@@@@@@ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx| +// |****** @@@@@@@@@@@@@@@@@@ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx| +// |****** @@@@@@@@@@@@@@@@@@ ++++++ ......| +// |****** @@@@@@@@@@@@@@@@@@ ++++++ ......| +// |****** @@@@@@@@@@@@@@@@@@ ++++++ ......| +// |****** ++++++++++++++++++++++++"""""""""""""""""" ......| <----- +// |****** ++++++++++++++++++++++++"""""""""""""""""" ......| +// |****** ++++++++++++++++++++++++"""""""""""""""""" ......| +// |000000000000000000000000000000 """""""""""""""""" ......| +// |000000000000000000000000000000 """""""""""""""""" ......| +// |000000000000000000000000000000 """""""""""""""""" ......| +// |000000000000000000000000000000 """""""""""""""""" ......| +// |000000000000000000000000000000 """""""""""""""""" ......| +// |000000000000000000000000000000 """""""""""""""""" ......| +// +--------------------------------------------------------+ +// ^ +// | +TEST(CheckSolutionTest, CumulativeFeasiblePackingInfeasibleExample) { + std::vector items_x_sizes = {5, 3, 1, 5, 3, 1, 5}; + std::vector items_y_sizes = {2, 3, 4, 2, 3, 4, 1}; + std::pair bb_sizes = {9, 6}; + + EXPECT_TRUE( + SolveOrthogonalPacking(items_x_sizes, items_y_sizes, bb_sizes).empty()); + EXPECT_TRUE( + CumulativeIsFeasible(items_x_sizes, items_y_sizes, bb_sizes, false)); + // Note that it is infeasible if we take the cumulative on the y though: + EXPECT_FALSE(CumulativeIsFeasible(items_y_sizes, items_x_sizes, + {bb_sizes.second, bb_sizes.first}, false)); +} + +// Example that is feasible for the cumulative in both dimensions but not for 2d +// packing. +// +// digraph { +// graph [ bgcolor=lightgray width=30 height=30] +// node [style=filled] +// bb [fillcolor="grey" pos="15,15!" shape=box width=30 height=30] +// 0 [fillcolor="red" pos="29,17!" shape=box width=2 height=26] +// 1 [fillcolor="green" pos="22,10!" shape=box width=4 height=12] +// 2 [fillcolor="blue" pos="26,14!" shape=box width=4 height=20] +// 3 [fillcolor="cyan" pos="10,7!" shape=box width=20 height=14] +// 4 [fillcolor="yellow" pos="15,19!" shape=box width=14 height=10] +// 5 [fillcolor="purple" pos="26,2!" shape=box width=8 height=4] +// 6 [fillcolor="red" pos="4,23!" shape=box width=8 height=14] +// 7 [fillcolor="green" pos="18,27!" shape=box width=20 height=6] +// } +TEST(CheckSolutionTest, CumulativeFeasiblePackingInfeasibleBothExample) { + std::vector items_x_sizes = {1, 2, 2, 10, 7, 4, 4, 10}; + std::vector items_y_sizes = {13, 6, 10, 7, 5, 2, 7, 3}; + std::pair bb_sizes = {15, 15}; + EXPECT_TRUE( + SolveOrthogonalPacking(items_x_sizes, items_y_sizes, bb_sizes).empty()); + EXPECT_TRUE( + CumulativeIsFeasible(items_x_sizes, items_y_sizes, bb_sizes, true)); +} + +MATCHER_P(FieldEq, field, "") { + return testing::Matches(testing::Field(field, ::testing::get<1>(arg)))( + ::testing::get<0>(arg)); +} + +TEST(TestPreprocessing, Works) { + std::vector bb_sizes = {100, 40}; + OppProblem problem = {.items_x_sizes = {95, 93, 90, 5, 7, 8, 20, 30, 20, 20}, + .items_y_sizes = {5, 5, 6, 8, 2, 4, 10, 10, 10, 10}, + .bb_sizes = {100, 40}}; + std::vector items; + for (int i = 0; i < problem.items_x_sizes.size(); ++i) { + items.push_back({.size_x = problem.items_x_sizes[i], + .size_y = problem.items_y_sizes[i]}); + } + absl::Span preprocessed_items = absl::MakeSpan(items); + EXPECT_TRUE(Preprocess(preprocessed_items, problem.bb_sizes, 10)); + // We expect that 95x5, 93x5, 90x6, 5x8, 7x2 and 8x4 be removed. + EXPECT_EQ(preprocessed_items.size(), 4); + EXPECT_THAT(preprocessed_items, + testing::UnorderedPointwise(FieldEq(&PermutableItem::size_x), + {20, 30, 20, 20})); + // Original items don't disappeared, but the order might have been changed. + EXPECT_THAT(items, + testing::UnorderedPointwise(FieldEq(&PermutableItem::size_x), + problem.items_x_sizes)); +} + +void BM_BruteForceOrthogonalPacking(benchmark::State& state) { + absl::BitGen random; + static constexpr int kNumProblems = 100; + std::vector problems; + const bool feasible = state.range(1); + while (problems.size() < kNumProblems) { + OppProblem problem = CreateRandomOppProblem(random, state.range(0)); + if ((BruteForceOrthogonalPacking(problem.items_x_sizes, + problem.items_y_sizes, problem.bb_sizes, + problem.items_x_sizes.size()) + .status == BruteForceResult::Status::kFoundSolution) == feasible) { + problems.push_back(problem); + } + } + int index = 0; + for (auto s : state) { + const auto& problem = problems[index]; + BruteForceOrthogonalPacking(problem.items_x_sizes, problem.items_y_sizes, + problem.bb_sizes, problem.items_x_sizes.size()); + ++index; + if (index == problems.size()) { + index = 0; + } + } +} + +BENCHMARK(BM_BruteForceOrthogonalPacking) + ->ArgPair(3, false) + ->ArgPair(4, false) + ->ArgPair(5, false) + ->ArgPair(6, false) + ->ArgPair(7, false) + ->ArgPair(8, false) + ->ArgPair(9, false) + ->ArgPair(3, true) + ->ArgPair(4, true) + ->ArgPair(5, true) + ->ArgPair(6, true) + ->ArgPair(7, true) + ->ArgPair(8, true) + ->ArgPair(9, true); + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/BUILD.bazel b/ortools/sat/BUILD.bazel index c21059e05e9..5e1b5ba3d75 100644 --- a/ortools/sat/BUILD.bazel +++ b/ortools/sat/BUILD.bazel @@ -204,6 +204,20 @@ cc_library( ], ) +cc_test( + name = "cp_model_checker_test", + size = "small", + srcs = ["cp_model_checker_test.cc"], + deps = [ + ":cp_model_cc_proto", + ":cp_model_checker", + "//ortools/base:gmock_main", + "//ortools/base:parse_test_proto", + "@com_google_absl//absl/log:check", + "@com_google_absl//absl/types:span", + ], +) + cc_library( name = "constraint_violation", srcs = ["constraint_violation.cc"], @@ -286,6 +300,24 @@ cc_library( ], ) +cc_test( + name = "linear_model_test", + size = "small", + srcs = ["linear_model_test.cc"], + deps = [ + ":cp_model_cc_proto", + ":cp_model_presolve", + ":linear_model", + ":model", + ":presolve_context", + ":sat_parameters_cc_proto", + "//ortools/base:gmock_main", + "//ortools/base:parse_test_proto", + "//ortools/util:logging", + "@com_google_absl//absl/log", + ], +) + cc_library( name = "parameters_validation", srcs = ["parameters_validation.cc"], @@ -337,6 +369,21 @@ cc_library( ], ) +cc_test( + name = "cp_model_search_test", + size = "small", + srcs = ["cp_model_search_test.cc"], + deps = [ + ":cp_model_cc_proto", + ":cp_model_search", + ":cp_model_solver", + ":model", + ":sat_parameters_cc_proto", + "//ortools/base:gmock_main", + "//ortools/base:parse_test_proto", + ], +) + cc_library( name = "cp_model_solver_helpers", srcs = ["cp_model_solver_helpers.cc"], @@ -651,6 +698,27 @@ cc_library( ], ) +cc_test( + name = "presolve_util_test", + size = "small", + srcs = ["presolve_util_test.cc"], + deps = [ + ":cp_model", + ":cp_model_cc_proto", + ":cp_model_solver", + ":cp_model_utils", + ":presolve_util", + ":sat_parameters_cc_proto", + "//ortools/base:gmock_main", + "//ortools/base:parse_test_proto", + "//ortools/util:sorted_interval_list", + "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/log", + "@com_google_absl//absl/random", + "@com_google_absl//absl/types:span", + ], +) + cc_library( name = "presolve_context", srcs = ["presolve_context.cc"], @@ -691,6 +759,25 @@ cc_library( ], ) +cc_test( + name = "presolve_context_test", + size = "small", + srcs = ["presolve_context_test.cc"], + deps = [ + ":cp_model_cc_proto", + ":cp_model_utils", + ":model", + ":presolve_context", + "//ortools/base:gmock_main", + "//ortools/base:parse_test_proto", + "//ortools/base:types", + "//ortools/util:affine_relation", + "//ortools/util:sorted_interval_list", + "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/types:span", + ], +) + cc_library( name = "cp_model_presolve", srcs = [ @@ -774,6 +861,20 @@ cc_library( ], ) +cc_test( + name = "cp_model_postsolve_test", + size = "small", + srcs = ["cp_model_postsolve_test.cc"], + deps = [ + ":cp_model_cc_proto", + ":cp_model_postsolve", + "//ortools/base:gmock_main", + "//ortools/base:parse_test_proto", + "//ortools/util:logging", + "//ortools/util:sorted_interval_list", + ], +) + cc_library( name = "cp_model_expand", srcs = ["cp_model_expand.cc"], @@ -802,6 +903,29 @@ cc_library( ], ) +cc_test( + name = "cp_model_expand_test", + size = "small", + srcs = ["cp_model_expand_test.cc"], + deps = [ + ":cp_model_cc_proto", + ":cp_model_checker", + ":cp_model_expand", + ":cp_model_solver", + ":cp_model_utils", + ":model", + ":presolve_context", + ":sat_parameters_cc_proto", + "//ortools/base", + "//ortools/base:container_logging", + "//ortools/base:gmock_main", + "//ortools/base:parse_test_proto", + "//ortools/util:sorted_interval_list", + "@com_google_absl//absl/container:btree", + "@com_google_absl//absl/strings", + ], +) + cc_library( name = "sat_base", hdrs = ["sat_base.h"], @@ -1209,6 +1333,22 @@ cc_library( ], ) +cc_test( + name = "var_domination_test", + size = "small", + srcs = ["var_domination_test.cc"], + deps = [ + ":cp_model_cc_proto", + ":integer", + ":model", + ":presolve_context", + ":var_domination", + "//ortools/base:gmock_main", + "//ortools/base:parse_test_proto", + "//ortools/util:sorted_interval_list", + ], +) + cc_library( name = "integer", srcs = ["integer.cc"], @@ -1491,6 +1631,38 @@ cc_library( ], ) +cc_test( + name = "integer_expr_test", + size = "medium", + srcs = ["integer_expr_test.cc"], + deps = [ + "cp_model_checker", + "cp_model_solver", + ":cp_model_cc_proto", + ":cp_model_utils", + ":integer", + ":integer_expr", + ":linear_constraint", + ":model", + ":sat_base", + ":sat_parameters_cc_proto", + ":sat_solver", + "//ortools/base", + "//ortools/base:gmock_main", + "//ortools/base:parse_test_proto", + "//ortools/port:proto_utils", + "//ortools/util:saturated_arithmetic", + "//ortools/util:sorted_interval_list", + "//ortools/util:strong_integers", + "@com_google_absl//absl/container:btree", + "@com_google_absl//absl/log:check", + "@com_google_absl//absl/random", + "@com_google_absl//absl/random:distributions", + "@com_google_absl//absl/strings", + "@com_google_absl//absl/types:span", + ], +) + cc_library( name = "linear_propagation", srcs = ["linear_propagation.cc"], @@ -1886,6 +2058,27 @@ cc_library( ], ) +cc_test( + name = "linear_relaxation_test", + srcs = ["linear_relaxation_test.cc"], + deps = [ + ":cp_model_cc_proto", + ":cp_model_loader", + ":cp_model_mapping", + ":integer", + ":intervals", + ":linear_constraint", + ":linear_relaxation", + ":model", + ":sat_base", + "//ortools/base:gmock_main", + "//ortools/base:parse_test_proto", + "//ortools/util:sorted_interval_list", + "@com_google_absl//absl/base:core_headers", + "@com_google_absl//absl/types:span", + ], +) + cc_library( name = "linear_constraint", srcs = ["linear_constraint.cc"], @@ -2437,6 +2630,28 @@ cc_library( ], ) +cc_test( + name = "table_test", + srcs = ["table_test.cc"], + deps = [ + ":cp_model", + ":cp_model_cc_proto", + ":cp_model_solver", + ":integer", + ":model", + ":sat_base", + ":sat_parameters_cc_proto", + ":sat_solver", + ":table", + "//ortools/base", + "//ortools/base:container_logging", + "//ortools/base:gmock_main", + "//ortools/base:parse_test_proto", + "@com_google_absl//absl/container:btree", + "@com_google_absl//absl/types:span", + ], +) + cc_library( name = "cp_constraints", srcs = ["cp_constraints.cc"], @@ -2527,6 +2742,25 @@ cc_library( ], ) +cc_test( + name = "2d_packing_brute_force_test", + srcs = ["2d_packing_brute_force_test.cc"], + deps = [ + ":2d_packing_brute_force", + ":cp_model", + ":cp_model_solver", + ":diffn_util", + ":integer", + "//ortools/base:gmock_main", + "@com_google_absl//absl/log:check", + "@com_google_absl//absl/random", + "@com_google_absl//absl/random:bit_gen_ref", + "@com_google_absl//absl/random:distributions", + "@com_google_absl//absl/types:span", + "@com_google_benchmark//:benchmark", + ], +) + cc_library( name = "2d_rectangle_presolve", srcs = ["2d_rectangle_presolve.cc"], @@ -2772,6 +3006,7 @@ cc_library( ":cp_model_presolve", ":cp_model_solver_helpers", ":cp_model_utils", + ":diffn_util", ":integer", ":linear_constraint_manager", ":linear_programming_constraint", @@ -2857,6 +3092,23 @@ cc_library( ], ) +cc_test( + name = "rins_test", + size = "small", + srcs = ["rins_test.cc"], + deps = [ + ":cp_model_cc_proto", + ":cp_model_loader", + ":model", + ":rins", + ":synchronization", + "//ortools/base:gmock_main", + "//ortools/base:parse_test_proto", + "//ortools/util:random_engine", + "@com_google_absl//absl/types:span", + ], +) + cc_library( name = "subsolver", srcs = ["subsolver.cc"], @@ -3164,3 +3416,16 @@ cc_test( "@com_google_absl//absl/types:span", ], ) + +cc_test( + name = "flaky_models_test", + size = "small", + srcs = ["flaky_models_test.cc"], + deps = [ + ":cp_model_cc_proto", + ":cp_model_solver", + ":sat_parameters_cc_proto", + "//ortools/base:gmock_main", + "//ortools/base:parse_test_proto", + ], +) diff --git a/ortools/sat/cp_constraints.h b/ortools/sat/cp_constraints.h index 67a3a5fad67..3090489d1c0 100644 --- a/ortools/sat/cp_constraints.h +++ b/ortools/sat/cp_constraints.h @@ -112,7 +112,7 @@ class GreaterThanAtLeastOneOfPropagator : public PropagatorInterface, // ============================================================================ inline std::vector ToIntegerValueVector( - const std::vector& input) { + absl::Span input) { std::vector result(input.size()); for (int i = 0; i < input.size(); ++i) { result[i] = IntegerValue(input[i]); diff --git a/ortools/sat/cp_model_checker_test.cc b/ortools/sat/cp_model_checker_test.cc new file mode 100644 index 00000000000..ccc1f3f8875 --- /dev/null +++ b/ortools/sat/cp_model_checker_test.cc @@ -0,0 +1,666 @@ +// Copyright 2010-2024 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/sat/cp_model_checker.h" + +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/parse_test_proto.h" +#include "ortools/sat/cp_model.pb.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::google::protobuf::contrib::parse_proto::ParseTestProto; +using ::testing::HasSubstr; + +// This just checks that the code is at least properly executed. +TEST(SolutionIsFeasibleTest, BasicExample) { + const CpModelProto model = ParseTestProto(R"pb( + variables { name: 'x' domain: 0 domain: 10 } + variables { name: 'y' domain: 0 domain: 10 } + constraints { + linear { vars: 0 coeffs: 1 vars: 1 coeffs: 1 domain: 0 domain: 10 } + } + )pb"); + EXPECT_FALSE(SolutionIsFeasible(model, {8, 8})); + EXPECT_FALSE(SolutionIsFeasible(model, {11, -1})); + EXPECT_TRUE(SolutionIsFeasible(model, {5, 5})); +} + +TEST(SolutionIsFeasibleTest, LinMax) { + const CpModelProto model = ParseTestProto(R"pb( + variables { name: 'x' domain: 0 domain: 10 } + variables { name: 'y' domain: 0 domain: 30 } + constraints { + lin_max { + target { vars: 1 coeffs: 1 offset: 0 } + exprs { vars: 0 coeffs: 2 offset: 1 } + exprs { vars: 0 coeffs: 3 offset: -5 } + } + } + )pb"); + EXPECT_FALSE(SolutionIsFeasible(model, {2, 4})); + EXPECT_FALSE(SolutionIsFeasible(model, {11, -1})); + EXPECT_TRUE(SolutionIsFeasible(model, {2, 5})); + EXPECT_TRUE(SolutionIsFeasible(model, {8, 19})); +} + +TEST(SolutionIsFeasibleTest, OrToolsIssue3769) { + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: [ 1, 2 ] } + variables { domain: [ 0, 1 ] } + constraints { + no_overlap_2d { + x_intervals: [ 1, 2 ] + y_intervals: [ 3, 4 ] + } + } + constraints { + interval { + start { offset: 2 } + end { + vars: [ 1 ] + coeffs: [ 1 ] + offset: 2 + } + size { + vars: [ 1 ] + coeffs: [ 1 ] + } + } + } + constraints { + interval { + start { offset: 1 } + end { offset: 3 } + size { offset: 2 } + } + } + constraints { + interval { + start { offset: 1 } + end { + vars: [ 0 ] + coeffs: [ 1 ] + offset: 1 + } + size { + vars: [ 0 ] + coeffs: [ 1 ] + } + } + } + constraints { + interval { + start { offset: 2 } + end { offset: 2 } + size {} + } + } + )pb"); + EXPECT_TRUE(SolutionIsFeasible(model, {1, 0})); + EXPECT_TRUE(SolutionIsFeasible(model, {1, 1})); + EXPECT_FALSE(SolutionIsFeasible(model, {2, 0})); +} + +TEST(SolutionIsFeasibleTest, Reservoir) { + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 1, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + reservoir { + time_exprs: { vars: 0 coeffs: 1 } + time_exprs: { vars: 1 coeffs: 1 } + level_changes: { offset: -1 } + level_changes: { offset: 1 } + active_literals: [ 2, 3 ] + min_level: 0 + max_level: 2 + } + } + )pb"); + EXPECT_FALSE(SolutionIsFeasible(model, {0, 0, 1, 0})); + EXPECT_TRUE(SolutionIsFeasible(model, {0, 0, 1, 1})); + EXPECT_TRUE(SolutionIsFeasible(model, {1, 0, 1, 1})); + EXPECT_FALSE(SolutionIsFeasible(model, {0, 1, 1, 1})); + EXPECT_FALSE(SolutionIsFeasible(model, {0, 0, 1, 0})); +} + +TEST(SolutionIsFeasibleTest, ReservoirWithNegativeTime) { + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: [ -2, 2 ] } + variables { domain: [ -2, 2 ] } + constraints { + reservoir { + time_exprs: { vars: 0 coeffs: 1 } + time_exprs: { vars: 1 coeffs: 1 } + level_changes: { offset: 2 } + level_changes: { offset: -2 } + min_level: 0 + max_level: 2 + } + } + )pb"); + EXPECT_TRUE(SolutionIsFeasible(model, {1, 1})); + EXPECT_TRUE(SolutionIsFeasible(model, {0, 0})); + EXPECT_FALSE(SolutionIsFeasible(model, {1, 0})); + EXPECT_TRUE(SolutionIsFeasible(model, {0, 1})); + EXPECT_TRUE(SolutionIsFeasible(model, {-2, 2})); +} + +TEST(SolutionIsFeasibleTest, SelfArcAreOk) { + // The literal -1 is the negation of the first variable. + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + circuit { + literals: [ -1, 1, 2, 3, 0 ] + tails: [ 0, 1, 2, 3, 0 ] + heads: [ 0, 2, 3, 1, 2 ] + } + } + )pb"); + EXPECT_TRUE(SolutionIsFeasible(model, {0, 1, 1, 1})); + EXPECT_FALSE(SolutionIsFeasible(model, {1, 1, 1, 1})); +} + +TEST(SolutionIsFeasibleTest, SparseCircuit) { + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + circuit { + literals: [ 0, 1, 2, 3 ] + tails: [ -10, 10, 9, 1000 ] + heads: [ 10, 9, 1000, -10 ] + } + } + )pb"); + EXPECT_TRUE(SolutionIsFeasible(model, {1, 1, 1, 1})); + EXPECT_FALSE(SolutionIsFeasible(model, {1, 0, 1, 1})); +} + +TEST(SolutionIsFeasibleTest, BoolXor) { + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { bool_xor { literals: [ 0, 1, 2, 3 ] } } + )pb"); + EXPECT_TRUE(SolutionIsFeasible(model, {1, 0, 0, 0})); + EXPECT_TRUE(SolutionIsFeasible(model, {1, 1, 1, 0})); + EXPECT_FALSE(SolutionIsFeasible(model, {1, 1, 1, 1})); + EXPECT_FALSE(SolutionIsFeasible(model, {1, 0, 1, 0})); +} + +TEST(SolutionIsFeasibleTest, WithEnforcement) { + const CpModelProto model = ParseTestProto(R"pb( + variables { name: 'a' domain: 0 domain: 1 } + variables { name: 'b' domain: 0 domain: 1 } + variables { name: 'y' domain: 0 domain: 10 } + constraints { + enforcement_literal: [ 0, 1 ] + linear { vars: 2 coeffs: 1 domain: 7 domain: 7 } + } + )pb"); + EXPECT_TRUE(SolutionIsFeasible(model, {0, 0, 5})); + EXPECT_TRUE(SolutionIsFeasible(model, {0, 1, 5})); + EXPECT_TRUE(SolutionIsFeasible(model, {1, 0, 5})); + EXPECT_FALSE(SolutionIsFeasible(model, {1, 1, 5})); + EXPECT_TRUE(SolutionIsFeasible(model, {1, 1, 7})); +} + +TEST(SolutionIsFeasibleTest, ObjectiveDomain) { + const CpModelProto model = ParseTestProto(R"pb( + variables { name: 'x' domain: 0 domain: 10 } + variables { name: 'y' domain: 0 domain: 10 } + objective { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + domain: [ 5, 15 ] + } + )pb"); + EXPECT_FALSE(SolutionIsFeasible(model, {8, 8})); + EXPECT_TRUE(SolutionIsFeasible(model, {5, 5})); + EXPECT_FALSE(SolutionIsFeasible(model, {0, 0})); +} + +TEST(ValidateCpModelTest, BadVariableDomain1) { + const CpModelProto model = ParseTestProto(R"pb( + variables { name: 'a' domain: 0 domain: 1 domain: 3 } + )pb"); + EXPECT_THAT(ValidateCpModel(model), HasSubstr("odd domain")); +} + +TEST(ValidateCpModelTest, VariableUpperBoundTooLarge) { + const CpModelProto model = ParseTestProto(R"pb( + variables { + name: 'a' + domain: [ 0, 9223372036854775807 ] + } + )pb"); + EXPECT_THAT(ValidateCpModel(model), HasSubstr("do not fall in")); +} + +TEST(ValidateCpModelTest, VariableLowerBoundTooLarge1) { + const CpModelProto model = ParseTestProto(R"pb( + variables { + name: 'a' + domain: [ -9223372036854775807, 0 ] + } + )pb"); + EXPECT_THAT(ValidateCpModel(model), HasSubstr("do not fall in")); +} + +TEST(ValidateCpModelTest, VariableLowerBoundTooLarge2) { + const CpModelProto model = ParseTestProto(R"pb( + variables { + name: 'a' + domain: [ -9223372036854775808, 0 ] + } + )pb"); + EXPECT_THAT(ValidateCpModel(model), HasSubstr("do not fall in")); +} + +TEST(ValidateCpModelTest, VariableDomainOverflow) { + CHECK_EQ(std::numeric_limits::max() - 1, + int64_t{9223372036854775806}); + + const CpModelProto model_ok = ParseTestProto(R"pb( + variables { name: 'a' domain: 0 domain: 9223372036854775806 } + )pb"); + EXPECT_TRUE(ValidateCpModel(model_ok).empty()); + + const CpModelProto model_bad0 = ParseTestProto(R"pb( + variables { name: 'a' domain: -1 domain: 9223372036854775806 } + )pb"); + EXPECT_THAT(ValidateCpModel(model_bad0), HasSubstr("overflow")); + + const CpModelProto model_bad1 = ParseTestProto(R"pb( + variables { name: 'a' domain: -2 domain: 9223372036854775806 } + )pb"); + EXPECT_THAT(ValidateCpModel(model_bad1), HasSubstr("overflow")); + + CHECK_EQ(std::numeric_limits::min() + 2, + int64_t{-9223372036854775806}); + const CpModelProto model_bad2 = ParseTestProto(R"pb( + variables { name: 'a' domain: -9223372036854775806 domain: 2 } + )pb"); + EXPECT_THAT(ValidateCpModel(model_bad2), HasSubstr("overflow")); +} + +TEST(ValidateCpModelTest, ObjectiveOverflow) { + CHECK_EQ(std::numeric_limits::max() / 4, + int64_t{2305843009213693951}); + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: [ -2305843009213693951, 2305843009213693951 ] } + variables { domain: [ -2305843009213693951, 2305843009213693951 ] } + variables { domain: [ -2305843009213693951, 2305843009213693951 ] } + objective { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 1, 1 ] + } + )pb"); + + // The min/max sum do not overflow, but their difference do. + EXPECT_THAT(ValidateCpModel(model), HasSubstr("overflow")); +} + +TEST(ValidateCpModelTest, ValidSolutionHint) { + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + solution_hint { + vars: [ 0, 1 ] + values: [ 1, 2 ] + } + )pb"); + EXPECT_TRUE(ValidateCpModel(model).empty()); +} + +TEST(ValidateCpModelTest, SolutionHint1) { + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + solution_hint { + vars: [ 0, 1, 2 ] + values: [ 1, 2, 3, 4 ] + } + )pb"); + EXPECT_THAT(ValidateCpModel(model), HasSubstr("same size")); +} + +TEST(ValidateCpModelTest, SolutionHint2) { + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + solution_hint { + vars: [ 0, 10, 2 ] + values: [ 1, 2, 3 ] + } + )pb"); + EXPECT_THAT(ValidateCpModel(model), HasSubstr("Invalid variable")); +} + +TEST(ValidateCpModelTest, SolutionHint3) { + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + solution_hint { + vars: [ 0, 2, 0 ] + values: [ 1, 2, 3 ] + } + )pb"); + EXPECT_THAT(ValidateCpModel(model), HasSubstr("duplicate")); +} + +TEST(ValidateCpModelTest, Assumptions) { + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + assumptions: [ 0, 1, 4 ] + )pb"); + EXPECT_THAT(ValidateCpModel(model), + "Invalid literal reference 4 in the 'assumptions' field."); +} + +TEST(ValidateCpModelTest, NegativeValueInIntervalSizeDomain) { + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: [ 0, 0 ] } + variables { domain: [ -7, -7, 0, 0 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + end { vars: 1 coeffs: 1 } + size { vars: 1 coeffs: 1 } + } + } + )pb"); + EXPECT_THAT(ValidateCpModel(model), + HasSubstr("The size of a performed interval must be >= 0")); +} + +TEST(ValidateCpModelTest, ParallelVectorMustHaveTheSameSize) { + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: 0 domain: 4503599627370529 } + constraints { + interval { + start { offset: 1 } + size { offset: 2 } + end { offset: 3 } + } + } + constraints { + no_overlap_2d { x_intervals: 0 y_intervals: 0 y_intervals: 0 } + } + )pb"); + EXPECT_THAT(ValidateCpModel(model), HasSubstr("must have the same size")); +} + +TEST(ValidateCpModelTest, InvalidDomainInLinear) { + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: -288230376151711744 domain: 262144 } + variables { domain: 0 domain: 5 } + constraints { + linear { + vars: [ 1, 0 ] + coeffs: [ 1, 2 ] + domain: [ 1, 3, 5 ] + } + } + )pb"); + EXPECT_THAT(ValidateCpModel(model), HasSubstr("Invalid domain")); +} + +TEST(ValidateCpModelTest, InvalidDomainInLinear2) { + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: -288230376151711744 domain: 262144 } + variables { domain: 0 domain: 5 } + constraints { + name: "T" + linear { + vars: [ 1, 0 ] + coeffs: [ 1, 2 ] + domain: [ 3, 0 ] + } + } + )pb"); + + EXPECT_THAT(ValidateCpModel(model), HasSubstr("Invalid domain")); +} + +TEST(ValidateCpModelTest, NegatedReferenceInLinear) { + const CpModelProto model = ParseTestProto(R"pb( + variables { name: "c" domain: 1 domain: 1 } + variables { domain: 0 domain: 1 } + constraints { + int_div { + target {} + exprs {} + exprs { vars: -2 coeffs: 792633495762501632 } + } + } + )pb"); + + EXPECT_THAT(ValidateCpModel(model), HasSubstr("Invalid negated variable")); +} + +TEST(ValidateCpModelTest, ArityOneInIntProd) { + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + constraints { + int_prod { + target { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + EXPECT_TRUE(ValidateCpModel(model).empty()); +} + +TEST(ValidateCpModelTest, ArityThreeInIntProd) { + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + constraints { + int_prod { + target { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + EXPECT_TRUE(ValidateCpModel(model).empty()); +} + +TEST(ValidateCpModelTest, WrongArityInIntDiv) { + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + constraints { + int_div { + target { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + EXPECT_THAT(ValidateCpModel(model), HasSubstr("have exactly 2 terms")); +} + +TEST(ValidateCpModelTest, DivisorDomainContainsZero) { + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ -3, 3 ] } + constraints { + int_div { + target { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + } + )pb"); + EXPECT_THAT(ValidateCpModel(model), + HasSubstr("The domain of the divisor cannot contain 0")); +} + +TEST(ValidateCpModelTest, DivisorSpanningAcrossZero) { + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ -3, 3 ] } + constraints { + int_div { + target { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 2 offset: -3 } + } + } + )pb"); + EXPECT_TRUE(ValidateCpModel(model).empty()); +} + +TEST(ValidateCpModelTest, DivisorIsZero) { + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + constraints { + int_div { + target { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs {} + } + } + )pb"); + EXPECT_THAT(ValidateCpModel(model), HasSubstr("Division by 0")); +} + +TEST(ValidateCpModelTest, WrongArityInIntMod) { + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + constraints { + int_mod { + target { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + EXPECT_THAT(ValidateCpModel(model), HasSubstr("have exactly 2 terms")); +} + +TEST(ValidateCpModelTest, NegativeModulo) { + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ -3, 3 ] } + constraints { + int_mod { + target { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + } + )pb"); + EXPECT_THAT(ValidateCpModel(model), + HasSubstr("strictly positive modulo argument")); +} + +TEST(ValidateCpModelTest, IncompatibleAutomatonTransitions) { + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: 0 domain: 1 } + constraints { + automaton { + final_states: 0 + transition_tail: 0 + transition_tail: 0 + transition_head: 0 + transition_head: 1 + transition_label: 0 + transition_label: 0 + vars: 0 + } + } + )pb"); + EXPECT_THAT(ValidateCpModel(model), + HasSubstr("automaton: incompatible transitions")); +} + +TEST(ValidateCpModelTest, DuplicateAutomatonTransitions) { + const CpModelProto model = ParseTestProto(R"pb( + variables { domain: 0 domain: 1 } + constraints { + automaton { + final_states: 0 + transition_tail: 0 + transition_tail: 0 + transition_head: 0 + transition_head: 0 + transition_label: 0 + transition_label: 0 + vars: 0 + } + } + )pb"); + EXPECT_THAT(ValidateCpModel(model), + HasSubstr("automaton: duplicate transition")); +} + +TEST(ValidateCpModelTest, IntervalMustAppearBeforeTheyAreUsed) { + const CpModelProto model = ParseTestProto(R"pb( + constraints { no_overlap { intervals: [ 1, 2 ] } } + constraints { + interval { + start { offset: 0 } + end { offset: 4 } + size { offset: 4 } + } + } + constraints { + interval { + start { offset: 4 } + end { offset: 5 } + size { offset: 1 } + } + } + )pb"); + EXPECT_THAT(ValidateCpModel(model, /*after_presolve=*/true), + HasSubstr("must appear before")); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/cp_model_expand_test.cc b/ortools/sat/cp_model_expand_test.cc new file mode 100644 index 00000000000..b3b12dfad98 --- /dev/null +++ b/ortools/sat/cp_model_expand_test.cc @@ -0,0 +1,1793 @@ +// Copyright 2010-2024 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/sat/cp_model_expand.h" + +#include +#include +#include + +#include "absl/container/btree_set.h" +#include "absl/strings/string_view.h" +#include "gtest/gtest.h" +#include "ortools/base/container_logging.h" +#include "ortools/base/gmock.h" +#include "ortools/base/logging.h" +#include "ortools/base/parse_test_proto.h" +#include "ortools/sat/cp_model.pb.h" +#include "ortools/sat/cp_model_checker.h" +#include "ortools/sat/cp_model_solver.h" +#include "ortools/sat/cp_model_utils.h" +#include "ortools/sat/model.h" +#include "ortools/sat/presolve_context.h" +#include "ortools/sat/sat_parameters.pb.h" +#include "ortools/util/sorted_interval_list.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::google::protobuf::contrib::parse_proto::ParseTestProto; + +CpSolverResponse SolveAndCheck( + const CpModelProto& initial_model, absl::string_view extra_parameters = "", + absl::btree_set>* solutions = nullptr) { + SatParameters params; + params.set_enumerate_all_solutions(true); + if (!extra_parameters.empty()) { + params.MergeFromString(extra_parameters); + } + auto observer = [&](const CpSolverResponse& response) { + VLOG(1) << response; + EXPECT_TRUE(SolutionIsFeasible( + initial_model, std::vector(response.solution().begin(), + response.solution().end()))); + if (solutions != nullptr) { + std::vector solution; + for (int var = 0; var < initial_model.variables_size(); ++var) { + solution.push_back(response.solution(var)); + } + solutions->insert(solution); + } + }; + Model model; + model.Add(NewSatParameters(params)); + model.Add(NewFeasibleSolutionObserver(observer)); + return SolveCpModel(initial_model, &model); +} + +TEST(ReservoirExpandTest, NoOptionalAndInitiallyFeasible) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { name: 'x' domain: 0 domain: 2 } + variables { name: 'y' domain: 0 domain: 2 } + variables { name: 'z' domain: 0 domain: 2 } + constraints { + reservoir { + time_exprs { vars: 0 coeffs: 1 } + time_exprs { vars: 1 coeffs: 1 } + time_exprs { vars: 2 coeffs: 1 } + level_changes: { offset: 1 } + level_changes: { offset: 1 } + level_changes: { offset: 2 } + min_level: 0 + max_level: 4 + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + EXPECT_EQ(27, solutions.size()); +} + +TEST(ReservoirExpandTest, GizaReport) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: 0 domain: 10 } + variables { domain: 0 domain: 10 } + variables { domain: 0 domain: 10 } + constraints { + reservoir { + time_exprs { vars: 0 coeffs: 1 } + time_exprs { vars: 1 coeffs: 1 } + time_exprs { vars: 2 coeffs: 1 } + level_changes: { offset: 10 } + level_changes: { offset: 1 } + level_changes: { offset: -1 } + min_level: 0 + max_level: 10 + } + } + constraints { + linear { + vars: 0 + coeffs: 1 + domain: [ 0, 0 ] + } + } + constraints { + linear { + vars: 1 + coeffs: 1 + domain: [ 1, 1 ] + } + } + constraints { + linear { + vars: 2 + coeffs: 1 + domain: [ 1, 1 ] + } + } + )pb"); + SatParameters params; + params.set_cp_model_presolve(false); + const CpSolverResponse response = SolveWithParameters(initial_model, params); + EXPECT_EQ(OPTIMAL, response.status()); +} + +TEST(ReservoirExpandTest, GizaReportReverse) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: 0 domain: 10 } + variables { domain: 0 domain: 10 } + variables { domain: 0 domain: 10 } + constraints { + reservoir { + time_exprs { vars: 0 coeffs: 1 } + time_exprs { vars: 1 coeffs: 1 } + time_exprs { vars: 2 coeffs: 1 } + level_changes: { offset: 10 } + level_changes: { offset: 1 } + level_changes: { offset: -1 } + min_level: 0 + max_level: 10 + } + } + constraints { + linear { + vars: 0 + coeffs: 1 + domain: [ 0, 0 ] + } + } + constraints { + linear { + vars: 1 + coeffs: 1 + domain: [ 1, 1 ] + } + } + constraints { + linear { + vars: 2 + coeffs: 1 + domain: [ 1, 1 ] + } + } + )pb"); + SatParameters params; + params.set_cp_model_presolve(false); + const CpSolverResponse response = SolveWithParameters(initial_model, params); + EXPECT_EQ(OPTIMAL, response.status()); +} + +TEST(ReservoirExpandTest, RepeatedTimesWithDifferentActivationVariables) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: 0 domain: 2 } + variables { domain: 0 domain: 2 } + variables { domain: 1 domain: 1 } + variables { domain: 0 domain: 1 } + constraints { + reservoir { + time_exprs { vars: 0 coeffs: 1 } + time_exprs { vars: 1 coeffs: 1 } + time_exprs { vars: 1 coeffs: 1 } + level_changes: { offset: 1 } + level_changes: { offset: 1 } + level_changes: { offset: -10 } + active_literals: [ 2, 2, 3 ] + min_level: 0 + max_level: 2 + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + // First two time variables should be unconstrained giving us 3x3 solutions. + EXPECT_EQ(9, solutions.size()); +} + +TEST(ReservoirExpandTest, NoOptionalAndInitiallyFeasibleWithConsumption) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { name: 'x' domain: 0 domain: 2 } + variables { name: 'y' domain: 0 domain: 2 } + variables { name: 'z' domain: 0 domain: 2 } + constraints { + reservoir { + time_exprs { vars: 0 coeffs: 1 } + time_exprs { vars: 1 coeffs: 1 } + time_exprs { vars: 2 coeffs: 1 } + level_changes: { offset: -1 } + level_changes: { offset: -1 } + level_changes: { offset: 2 } + min_level: 0 + max_level: 2 + } + } + constraints { + all_diff { + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + EXPECT_EQ(2, solutions.size()); +} + +TEST(ReservoirExpandTest, NoOptionalAndInitiallyFeasibleAndOverloaded) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { name: 'x' domain: 0 domain: 2 } + variables { name: 'y' domain: 0 domain: 2 } + variables { name: 'z' domain: 0 domain: 2 } + constraints { + reservoir { + time_exprs { vars: 0 coeffs: 1 } + time_exprs { vars: 1 coeffs: 1 } + time_exprs { vars: 2 coeffs: 1 } + level_changes: { offset: 1 } + level_changes: { offset: 1 } + level_changes: { offset: 2 } + min_level: 0 + max_level: 2 + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &solutions); + EXPECT_EQ(INFEASIBLE, response.status()); + EXPECT_EQ(0, solutions.size()); +} + +TEST(ReservoirExpandTest, OneUnschedulableOptionalAndInitiallyFeasible) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { name: 'true' domain: 1 domain: 1 } + variables { name: 'x' domain: 0 domain: 2 } + variables { name: 'presence_y' domain: 0 domain: 1 } + variables { name: 'y' domain: 0 domain: 2 } + constraints { + reservoir { + time_exprs { vars: 1 coeffs: 1 } + time_exprs { vars: 3 coeffs: 1 } + level_changes: { offset: 1 } + level_changes: { offset: 2 } + active_literals: 0 + active_literals: 2 + min_level: 0 + max_level: 2 + } + } + constraints { + enforcement_literal: -3 + linear { vars: 3 coeffs: 1 domain: 0 domain: 0 } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + EXPECT_EQ(3, solutions.size()); +} + +TEST(ReservoirExpandTest, OptionalWithConsumption) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { name: 'presence_x' domain: 0 domain: 1 } + variables { name: 'x' domain: 0 domain: 1 } + variables { name: 'presence_y' domain: 0 domain: 1 } + variables { name: 'y' domain: 0 domain: 1 } + constraints { + reservoir { + time_exprs { vars: 1 coeffs: 1 } + time_exprs { vars: 3 coeffs: 1 } + level_changes: { offset: 1 } + level_changes: { offset: -1 } + active_literals: 0 + active_literals: 2 + min_level: 0 + max_level: 2 + } + } + constraints { + enforcement_literal: -1 + linear { vars: 1 coeffs: 1 domain: 0 domain: 0 } + } + constraints { + enforcement_literal: -3 + linear { vars: 3 coeffs: 1 domain: 0 domain: 0 } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + EXPECT_EQ(6, solutions.size()); +} + +TEST(ReservoirExpandTest, FalseActive) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { name: "x12" domain: 0 domain: 1 } + variables { name: "start" domain: 0 domain: 0 } + variables { name: "fill_time_2" domain: 2 domain: 2 } + variables { name: "empty_time_8" domain: 12 domain: 12 } + variables { domain: 1 domain: 1 } + constraints { + reservoir { + max_level: 20 + time_exprs { vars: 1 coeffs: 1 } + time_exprs { vars: 2 coeffs: 1 } + time_exprs { vars: 3 coeffs: 1 } + level_changes: { offset: 10 } + level_changes: { offset: 5 } + level_changes: { offset: -3 } + active_literals: 4 + active_literals: 1 + active_literals: 0 + } + } + )pb"); + const CpSolverResponse response = Solve(initial_model); + EXPECT_EQ(OPTIMAL, response.status()); +} + +TEST(IntModExpandTest, FzTest) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { name: 'x' domain: 50 domain: 60 } + variables { name: 'y' domain: 1 domain: 5 } + variables { name: 'mod' domain: 5 domain: 5 } + constraints { + int_mod { + target { vars: 1 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + EXPECT_EQ(8, solutions.size()); +} + +TEST(IntModExpandTest, FzTestVariableMod) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { name: 'x' domain: 50 domain: 60 } + variables { name: 'y' domain: 1 domain: 5 } + variables { name: 'mod' domain: 4 domain: 5 } + constraints { + int_mod { + target { vars: 1 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + } + )pb"); + absl::btree_set> found_solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &found_solutions); + EXPECT_EQ(OPTIMAL, response.status()); + absl::btree_set> expected{ + {50, 2, 4}, {51, 1, 5}, {51, 3, 4}, {52, 2, 5}, {53, 1, 4}, {53, 3, 5}, + {54, 2, 4}, {54, 4, 5}, {55, 3, 4}, {56, 1, 5}, {57, 1, 4}, {57, 2, 5}, + {58, 2, 4}, {58, 3, 5}, {59, 3, 4}, {59, 4, 5}}; + EXPECT_EQ(found_solutions, expected); + EXPECT_EQ(16, found_solutions.size()); +} + +TEST(IntModExpandTest, Issue2420) { + CpModelProto initial_model = ParseTestProto(R"pb( + variables { name: "b" domain: 0 domain: 65535 } + variables { domain: 192 domain: 192 } + variables { name: "x" domain: 127 domain: 137 } + constraints { + int_mod { + target { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + } + )pb"); + absl::btree_set> found_solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &found_solutions); + EXPECT_EQ(CpSolverStatus::OPTIMAL, response.status()); + absl::btree_set> expected{ + {55, 192, 137}, {56, 192, 136}, {57, 192, 135}, {58, 192, 134}, + {59, 192, 133}, {60, 192, 132}, {61, 192, 131}, {62, 192, 130}, + {63, 192, 129}, {64, 192, 128}, {65, 192, 127}}; + EXPECT_EQ(found_solutions, expected); + EXPECT_EQ(11, found_solutions.size()); +} + +TEST(IntModExpandTest, VariableMod) { + CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: 0 domain: 10 } + variables { domain: 3 domain: 10 } + variables { domain: 1 domain: 4 } + constraints { + int_mod { + target { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + } + )pb"); + absl::btree_set> found_solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &found_solutions); + EXPECT_EQ(CpSolverStatus::OPTIMAL, response.status()); + absl::btree_set> expected{ + {0, 3, 1}, {0, 3, 3}, {0, 4, 1}, {0, 4, 2}, {0, 4, 4}, {0, 5, 1}, + {0, 6, 1}, {0, 6, 2}, {0, 6, 3}, {0, 7, 1}, {0, 8, 1}, {0, 8, 2}, + {0, 8, 4}, {0, 9, 1}, {0, 9, 3}, {0, 10, 1}, {0, 10, 2}, {1, 3, 2}, + {1, 4, 3}, {1, 5, 2}, {1, 5, 4}, {1, 7, 2}, {1, 7, 3}, {1, 9, 2}, + {1, 9, 4}, {1, 10, 3}, {2, 5, 3}, {2, 6, 4}, {2, 8, 3}, {2, 10, 4}, + {3, 3, 4}, {3, 7, 4}}; + EXPECT_EQ(found_solutions, expected); + EXPECT_EQ(32, found_solutions.size()); +} + +TEST(IntProdExpandTest, LeftCase) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { name: 'x' domain: -50 domain: -40 domain: 10 domain: 20 } + variables { name: 'y' domain: 0 domain: 1 } + variables { name: 'p' domain: -100 domain: 100 } + constraints { + int_prod { + target { vars: 2 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + EXPECT_EQ(44, solutions.size()); +} + +TEST(IntProdExpandTest, RightCase) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { name: 'x' domain: -50 domain: -40 domain: 10 domain: 20 } + variables { name: 'y' domain: 0 domain: 1 } + variables { name: 'p' domain: -100 domain: 100 } + constraints { + int_prod { + target { vars: 2 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + EXPECT_EQ(44, solutions.size()); +} + +TEST(IntProdExpandTest, LeftAcrossZero) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { name: 'x' domain: -6 domain: 6 } + variables { name: 'y' domain: 2 domain: 4 } + variables { name: 'p' domain: -30 domain: 30 } + constraints { + int_prod { + target { vars: 2 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + absl::btree_set> expected{ + {-6, 2, -12}, {-6, 3, -18}, {-6, 4, -24}, {-5, 2, -10}, {-5, 3, -15}, + {-5, 4, -20}, {-4, 2, -8}, {-4, 3, -12}, {-4, 4, -16}, {-3, 2, -6}, + {-3, 3, -9}, {-3, 4, -12}, {-2, 2, -4}, {-2, 3, -6}, {-2, 4, -8}, + {-1, 2, -2}, {-1, 3, -3}, {-1, 4, -4}, {0, 2, 0}, {0, 3, 0}, + {0, 4, 0}, {1, 2, 2}, {1, 3, 3}, {1, 4, 4}, {2, 2, 4}, + {2, 3, 6}, {2, 4, 8}, {3, 2, 6}, {3, 3, 9}, {3, 4, 12}, + {4, 2, 8}, {4, 3, 12}, {4, 4, 16}, {5, 2, 10}, {5, 3, 15}, + {5, 4, 20}, {6, 2, 12}, {6, 3, 18}, {6, 4, 24}, + }; + EXPECT_EQ(solutions.size(), 13 * 3); + EXPECT_EQ(solutions, expected); +} + +TEST(IntProdExpandTest, TestLargerArity) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { name: 'x' domain: -6 domain: 6 } + variables { name: 'y' domain: 2 domain: 4 } + variables { name: 'z' domain: 1 domain: 2 } + variables { name: 'p' domain: -30 domain: 30 } + constraints { + int_prod { + target { vars: 3 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + const Domain dx = ReadDomainFromProto(initial_model.variables(0)); + const Domain dy = ReadDomainFromProto(initial_model.variables(1)); + const Domain dz = ReadDomainFromProto(initial_model.variables(2)); + const Domain dp = ReadDomainFromProto(initial_model.variables(3)); + + absl::btree_set> expected; + for (const int vx : dx.Values()) { + for (const int vy : dy.Values()) { + for (const int vz : dz.Values()) { + if (dp.Contains(vx * vy * vz)) { + expected.insert(std::vector{vx, vy, vz, vx * vy * vz}); + } + } + } + } + + EXPECT_EQ(solutions, expected); +} + +TEST(IntProdExpandTest, TestLargerAffineProd) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { name: 'x' domain: -6 domain: 6 } + variables { name: 'y' domain: 2 domain: 4 } + variables { name: 'z' domain: 1 domain: 2 } + variables { name: 'p' domain: -30 domain: 30 } + constraints { + int_prod { + target { vars: 3 coeffs: 1 } + exprs { vars: 0 coeffs: 1 offset: 2 } + exprs { vars: 0 coeffs: 3 offset: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 1 coeffs: 2 offset: -1 } + exprs { vars: 1 coeffs: 3 } + exprs { vars: 2 coeffs: 1 offset: 1 } + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + const Domain dx = ReadDomainFromProto(initial_model.variables(0)); + const Domain dy = ReadDomainFromProto(initial_model.variables(1)); + const Domain dz = ReadDomainFromProto(initial_model.variables(2)); + const Domain dp = ReadDomainFromProto(initial_model.variables(3)); + + absl::btree_set> expected; + for (const int vx : dx.Values()) { + for (const int vy : dy.Values()) { + for (const int vz : dz.Values()) { + const int p = (vx + 2) * (2 * vx + 1) * vy * (2 * vy - 1) * (3 * vy) * + (2 * vz + 1); + if (dp.Contains(p)) { + expected.insert(std::vector{vx, vy, vz, p}); + } + } + } + } + + EXPECT_EQ(solutions, expected); +} + +TEST(ElementExpandTest, ConstantArray) { + CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -1, 5 ] } + variables { domain: [ 1, 1 ] } + variables { domain: [ 3, 3 ] } + variables { domain: [ 4, 4 ] } + variables { domain: [ 5, 5 ] } + variables { domain: [ 0, 7 ] } + constraints { + element { + index: 0, + vars: [ 1, 2, 3, 4, 1 ], + target: 5 + } + } + )pb"); + + absl::btree_set> found_solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &found_solutions); + absl::btree_set> expected{ + {0, 1, 3, 4, 5, 1}, {1, 1, 3, 4, 5, 3}, {2, 1, 3, 4, 5, 4}, + {3, 1, 3, 4, 5, 5}, {4, 1, 3, 4, 5, 1}, + }; + EXPECT_EQ(found_solutions, expected); +} + +TEST(AutomatonExpandTest, NonogramRule) { + // Accept sequences with 3 '1', then 2 '1', then 1 '1', separated by at least + // one '0'. + CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + automaton { + starting_state: 1, + final_states: [ 9 ], + transition_tail: [ 1, 1, 2, 3, 4, 5, 5, 6, 7, 8, 8, 9 ], + transition_head: [ 1, 2, 3, 4, 5, 5, 6, 7, 8, 8, 9, 9 ], + transition_label: [ 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0 ], + vars: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ], + } + } + )pb"); + absl::btree_set> found_solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &found_solutions); + absl::btree_set> expected{ + {0, 0, 1, 1, 1, 0, 1, 1, 0, 1}, {0, 1, 1, 1, 0, 0, 1, 1, 0, 1}, + {0, 1, 1, 1, 0, 1, 1, 0, 0, 1}, {0, 1, 1, 1, 0, 1, 1, 0, 1, 0}, + {1, 1, 1, 0, 0, 0, 1, 1, 0, 1}, {1, 1, 1, 0, 0, 1, 1, 0, 0, 1}, + {1, 1, 1, 0, 0, 1, 1, 0, 1, 0}, {1, 1, 1, 0, 1, 1, 0, 0, 0, 1}, + {1, 1, 1, 0, 1, 1, 0, 0, 1, 0}, {1, 1, 1, 0, 1, 1, 0, 1, 0, 0}}; + EXPECT_EQ(found_solutions, expected); +} + +TEST(AutomatonExpandTest, Bug1753_1) { + CpModelProto initial_model = ParseTestProto(R"pb( + variables { name: "0" domain: 0 domain: 2 } + variables { name: "1" domain: 0 domain: 2 } + variables { name: "2" domain: 0 domain: 2 } + constraints { + automaton { + starting_state: 1 + final_states: 1 + final_states: 2 + transition_tail: 1 + transition_tail: 2 + transition_head: 2 + transition_head: 1 + transition_label: 1 + transition_label: 2 + vars: 0 + vars: 1 + vars: 2 + } + } + )pb"); + absl::btree_set> found_solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &found_solutions); + absl::btree_set> expected{{1, 2, 1}}; + EXPECT_EQ(found_solutions, expected); +} + +TEST(AutomatonExpandTest, Bug1753_2) { + CpModelProto initial_model = ParseTestProto(R"pb( + variables { name: "0" domain: 0 domain: 2 } + variables { name: "1" domain: 0 domain: 2 } + variables { name: "2" domain: 0 domain: 2 } + constraints { linear { vars: 2 coeffs: 1 domain: 1 domain: 1 } } + constraints { + automaton { + starting_state: 1 + final_states: 1 + final_states: 2 + transition_tail: 1 + transition_tail: 0 + transition_tail: 1 + transition_tail: 2 + transition_tail: 0 + transition_tail: 2 + transition_head: 2 + transition_head: 2 + transition_head: 1 + transition_head: 1 + transition_head: 1 + transition_head: 2 + transition_label: 1 + transition_label: 1 + transition_label: 0 + transition_label: 2 + transition_label: 2 + transition_label: 0 + vars: 0 + vars: 1 + vars: 2 + } + } + )pb"); + absl::btree_set> found_solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &found_solutions); + absl::btree_set> expected{{0, 0, 1}, {1, 2, 1}}; + EXPECT_EQ(found_solutions, expected); +} + +TEST(AutomatonExpandTest, EverythingZero) { + CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + automaton { + starting_state: 1, + final_states: [ 1 ], + transition_tail: 1, + transition_head: 1, + transition_label: 0, + vars: [ 0, 1, 2, 3, 4, 5 ], + } + } + )pb"); + Model model; + PresolveContext context(&model, &initial_model, nullptr); + ExpandCpModel(&context); + + const CpModelProto expected_model = ParseTestProto(R"pb( + variables { domain: 0 domain: 0 } + variables { domain: 0 domain: 0 } + variables { domain: 0 domain: 0 } + variables { domain: 0 domain: 0 } + variables { domain: 0 domain: 0 } + variables { domain: 0 domain: 0 } + constraints {} + )pb"); + EXPECT_THAT(initial_model, testing::EqualsProto(expected_model)); +} + +TEST(AutomatonExpandTest, LoopingAutomatonMultipleFinalStates) { + // These tuples accept "0*(12)+0*". + CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + constraints { + automaton { + starting_state: 1, + final_states: [ 3, 4 ], + transition_tail: [ 1, 1, 2, 3, 3, 4 ], + transition_head: [ 1, 2, 3, 2, 4, 4 ], + transition_label: [ 0, 1, 2, 1, 0, 0 ], + vars: [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ], + } + } + )pb"); + absl::btree_set> found_solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &found_solutions); + EXPECT_EQ(CpSolverStatus::OPTIMAL, response.status()); + absl::btree_set> expected{ + {0, 0, 0, 0, 0, 0, 0, 0, 1, 2}, {0, 0, 0, 0, 0, 0, 0, 1, 2, 0}, + {0, 0, 0, 0, 0, 0, 1, 2, 0, 0}, {0, 0, 0, 0, 0, 0, 1, 2, 1, 2}, + {0, 0, 0, 0, 0, 1, 2, 0, 0, 0}, {0, 0, 0, 0, 0, 1, 2, 1, 2, 0}, + {0, 0, 0, 0, 1, 2, 0, 0, 0, 0}, {0, 0, 0, 0, 1, 2, 1, 2, 0, 0}, + {0, 0, 0, 0, 1, 2, 1, 2, 1, 2}, {0, 0, 0, 1, 2, 0, 0, 0, 0, 0}, + {0, 0, 0, 1, 2, 1, 2, 0, 0, 0}, {0, 0, 0, 1, 2, 1, 2, 1, 2, 0}, + {0, 0, 1, 2, 0, 0, 0, 0, 0, 0}, {0, 0, 1, 2, 1, 2, 0, 0, 0, 0}, + {0, 0, 1, 2, 1, 2, 1, 2, 0, 0}, {0, 0, 1, 2, 1, 2, 1, 2, 1, 2}, + {0, 1, 2, 0, 0, 0, 0, 0, 0, 0}, {0, 1, 2, 1, 2, 0, 0, 0, 0, 0}, + {0, 1, 2, 1, 2, 1, 2, 0, 0, 0}, {0, 1, 2, 1, 2, 1, 2, 1, 2, 0}, + {1, 2, 0, 0, 0, 0, 0, 0, 0, 0}, {1, 2, 1, 2, 0, 0, 0, 0, 0, 0}, + {1, 2, 1, 2, 1, 2, 0, 0, 0, 0}, {1, 2, 1, 2, 1, 2, 1, 2, 0, 0}, + {1, 2, 1, 2, 1, 2, 1, 2, 1, 2}}; + EXPECT_EQ(found_solutions, expected); + EXPECT_EQ(25, found_solutions.size()); +} + +TEST(AutomatonExpandTest, LoopingAutomatonMultipleFinalStatesNegatedVariables) { + // These automaton accept "0*(12)+0*". + CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 2 ] } + variables { domain: [ -2, 0 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + constraints { + automaton { + starting_state: 1, + final_states: [ 3, 4 ], + transition_tail: [ 1, 1, 2, 3, 3, 4 ], + transition_head: [ 1, 2, 3, 2, 4, 4 ], + transition_label: [ 0, 1, 2, 1, 0, 0 ], + vars: [ 0, -2, 2, 3, 4, 5, 6, 7, 8, 9 ], + } + } + )pb"); + absl::btree_set> found_solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &found_solutions); + EXPECT_EQ(CpSolverStatus::OPTIMAL, response.status()); + + absl::btree_set> expected{ + {0, 0, 0, 0, 0, 0, 0, 0, 1, 2}, {0, 0, 0, 0, 0, 0, 0, 1, 2, 0}, + {0, 0, 0, 0, 0, 0, 1, 2, 0, 0}, {0, 0, 0, 0, 0, 0, 1, 2, 1, 2}, + {0, 0, 0, 0, 0, 1, 2, 0, 0, 0}, {0, 0, 0, 0, 0, 1, 2, 1, 2, 0}, + {0, 0, 0, 0, 1, 2, 0, 0, 0, 0}, {0, 0, 0, 0, 1, 2, 1, 2, 0, 0}, + {0, 0, 0, 0, 1, 2, 1, 2, 1, 2}, {0, 0, 0, 1, 2, 0, 0, 0, 0, 0}, + {0, 0, 0, 1, 2, 1, 2, 0, 0, 0}, {0, 0, 0, 1, 2, 1, 2, 1, 2, 0}, + {0, 0, 1, 2, 0, 0, 0, 0, 0, 0}, {0, 0, 1, 2, 1, 2, 0, 0, 0, 0}, + {0, 0, 1, 2, 1, 2, 1, 2, 0, 0}, {0, 0, 1, 2, 1, 2, 1, 2, 1, 2}, + {0, -1, 2, 0, 0, 0, 0, 0, 0, 0}, {0, -1, 2, 1, 2, 0, 0, 0, 0, 0}, + {0, -1, 2, 1, 2, 1, 2, 0, 0, 0}, {0, -1, 2, 1, 2, 1, 2, 1, 2, 0}, + {1, -2, 0, 0, 0, 0, 0, 0, 0, 0}, {1, -2, 1, 2, 0, 0, 0, 0, 0, 0}, + {1, -2, 1, 2, 1, 2, 0, 0, 0, 0}, {1, -2, 1, 2, 1, 2, 1, 2, 0, 0}, + {1, -2, 1, 2, 1, 2, 1, 2, 1, 2}}; + EXPECT_EQ(found_solutions, expected); +} + +TEST(AutomatonExpandTest, AnotherAutomaton) { + // This accept everything that does not contain 4 consecutives 1 or 4 + // consecutives 2. + CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + constraints { + automaton { + starting_state: 1, + final_states: [ 1, 2, 3, 4, 5, 6, 7 ], + transition_tail: [ 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7 ], + transition_head: [ 2, 5, 3, 5, 4, 5, 0, 5, 2, 6, 2, 7, 2, 0 ], + transition_label: [ 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2 ], + vars: [ 0, 1, 2, 3, 4, 5, 6 ], + } + } + )pb"); + absl::btree_set> found_solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &found_solutions); + + EXPECT_EQ(CpSolverStatus::OPTIMAL, response.status()); + + // Out of the 2**7 tuples, the one that contains 4 consecutive 1 are: + // - 1111??? (8) + // - 21111?? (4) + // - ?21111? (4) + // - ??21111 (4) + EXPECT_EQ(128 - 2 * 20, found_solutions.size()); +} + +TEST(ExpandTableTest, EnumerationAndEncoding) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 4 ] } + variables { domain: [ 0, 4 ] } + variables { domain: [ 0, 4 ] } + variables { domain: [ 0, 4 ] } + constraints { table { vars: 0 vars: 2 values: 0 values: 1 } } + constraints { table { vars: 1 vars: 3 values: 4 values: 0 } } + constraints { table { vars: 2 vars: 1 values: 1 values: 4 } } + )pb"); + + Model model; + model.Add(NewSatParameters("enumerate_all_solutions:true")); + int count = 0; + model.Add( + NewFeasibleSolutionObserver([&count](const CpSolverResponse& response) { + LOG(INFO) << gtl::LogContainer(response.solution()); + ++count; + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + + // There should be just one solution [0, 4, 1, 0], but the solver used to + // report more because of extra "free" variable used in the encoding. + EXPECT_EQ(count, 1); +} + +TEST(ExpandTableTest, EnumerationAndEncodingTwoVars) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { + name: "X1" + domain: [ 0, 4 ] + } + variables { + name: "X3" + domain: [ 0, 4 ] + } + constraints { + table { + vars: [ 0, 1 ] + values: [ 0, 0, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4 ] + } + } + )pb"); + + Model model; + model.Add(NewSatParameters("enumerate_all_solutions:true")); + int count = 0; + model.Add( + NewFeasibleSolutionObserver([&count](const CpSolverResponse& response) { + LOG(INFO) << gtl::LogContainer(response.solution()); + ++count; + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(count, 7); +} + +TEST(ExpandTableTest, EnumerationAndEncodingFullPrefix) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + constraints { + table { + vars: [ 0, 1, 2 ] + values: [ + 0, 0, 0, 0, 1, 1, 0, 2, 2, 1, 0, 1, 1, 1, + 2, 1, 2, 0, 2, 0, 2, 2, 1, 0, 2, 2, 1 + ] + } + } + )pb"); + + Model model; + model.Add(NewSatParameters("enumerate_all_solutions:true")); + int count = 0; + model.Add( + NewFeasibleSolutionObserver([&count](const CpSolverResponse& response) { + LOG(INFO) << gtl::LogContainer(response.solution()); + ++count; + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + + EXPECT_EQ(count, 9); +} + +TEST(ExpandTableTest, EnumerationAndEncodingPartialPrefix) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + constraints { + table { + vars: [ 0, 1, 2 ] + values: [ + 0, 0, 0, 0, 2, 2, 1, 0, 1, 1, 1, 2, 1, 2, 0, 2, 0, 2, 2, 1, 0 + ] + } + } + )pb"); + + Model model; + model.Add(NewSatParameters("enumerate_all_solutions:true")); + int count = 0; + model.Add( + NewFeasibleSolutionObserver([&count](const CpSolverResponse& response) { + LOG(INFO) << gtl::LogContainer(response.solution()); + ++count; + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + + EXPECT_EQ(count, 7); +} + +TEST(ExpandTableTest, EnumerationAndEncodingInvalidTuples) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + constraints { + table { + vars: [ 0, 1, 2 ] + values: [ + 0, 0, 4, 0, 2, 2, 1, 0, 1, 1, 1, 2, 1, 2, 0, 2, 0, 2, 2, 1, 4 + ] + } + } + )pb"); + + Model model; + model.Add( + NewSatParameters("enumerate_all_solutions:true,cp_model_presolve:false")); + int count = 0; + model.Add( + NewFeasibleSolutionObserver([&count](const CpSolverResponse& response) { + LOG(INFO) << gtl::LogContainer(response.solution()); + ++count; + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + + // There should be exactly one solution per valid tuple. + EXPECT_EQ(count, 5); +} + +TEST(ExpandTableTest, EnumerationAndEncodingOneTupleWithAny) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 3 ] } + variables { domain: [ 0, 3 ] } + variables { domain: [ 0, 3 ] } + constraints { + table { + vars: [ 0, 1, 2 ] + values: [ 1, 0, 2, 1, 1, 2, 1, 2, 2 ] + } + } + )pb"); + + Model model; + model.Add( + NewSatParameters("enumerate_all_solutions:true,cp_model_presolve:false")); + int count = 0; + model.Add( + NewFeasibleSolutionObserver([&count](const CpSolverResponse& response) { + LOG(INFO) << gtl::LogContainer(response.solution()); + ++count; + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + + EXPECT_EQ(count, 3); +} + +TEST(ExpandTableTest, EnumerationAndEncodingPrefixWithLargeNegatedPart) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 5 ] } + variables { domain: [ 0, 5 ] } + variables { domain: [ 0, 5 ] } + constraints { + table { + vars: [ 0, 1, 2 ] + values: [ 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5 ] + } + } + )pb"); + + Model model; + model.Add( + NewSatParameters("enumerate_all_solutions:true,cp_model_presolve:false")); + int count = 0; + model.Add( + NewFeasibleSolutionObserver([&count](const CpSolverResponse& response) { + LOG(INFO) << gtl::LogContainer(response.solution()); + ++count; + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + + EXPECT_EQ(count, 6); +} + +TEST(ExpandTableTest, EnforcedPositiveTable) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 1, 3 ] } + variables { domain: [ 1, 3 ] } + variables { domain: [ 1, 3 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: [ 3 ] + table { + vars: [ 0, 1, 2 ] + values: [ 1, 2, 3, 2, 2, 2, 3, 2, 1 ] + } + } + )pb"); + + Model model; + model.Add( + NewSatParameters("enumerate_all_solutions:true,cp_model_presolve:false")); + int count = 0; + model.Add( + NewFeasibleSolutionObserver([&count](const CpSolverResponse& response) { + LOG(INFO) << gtl::LogContainer(response.solution()); + ++count; + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + + EXPECT_EQ(count, 30); +} + +TEST(ExpandTableTest, EnforcedPositiveEmptyTable) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 1, 3 ] } + variables { domain: [ 1, 3 ] } + variables { domain: [ 1, 3 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: [ 3 ] + table { + vars: [ 0, 1, 2 ] + values: [] + } + } + )pb"); + + Model model; + model.Add( + NewSatParameters("enumerate_all_solutions:true,cp_model_presolve:false")); + int count = 0; + model.Add( + NewFeasibleSolutionObserver([&count](const CpSolverResponse& response) { + LOG(INFO) << gtl::LogContainer(response.solution()); + ++count; + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + + EXPECT_EQ(count, 27); +} + +TEST(ExpandTableTest, DualEnforcedPositiveTable) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 1, 3 ] } + variables { domain: [ 1, 3 ] } + variables { domain: [ 1, 3 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: [ 3, 4 ] + table { + vars: [ 0, 1, 2 ] + values: [ 1, 2, 3, 2, 2, 2, 3, 2, 1 ] + } + } + )pb"); + + Model model; + model.Add(NewSatParameters("enumerate_all_solutions:true")); + int count = 0; + model.Add( + NewFeasibleSolutionObserver([&count](const CpSolverResponse& response) { + LOG(INFO) << gtl::LogContainer(response.solution()); + ++count; + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + + EXPECT_EQ(count, 84); +} + +TEST(ExpandTableTest, EnforcedNegativeTable) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 1, 3 ] } + variables { domain: [ 1, 3 ] } + variables { domain: [ 1, 3 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: [ 3 ] + table { + vars: [ 0, 1, 2 ] + values: [ 1, 2, 3, 2, 2, 2, 3, 2, 1 ] + negated: true + } + } + )pb"); + + Model model; + model.Add( + NewSatParameters("enumerate_all_solutions:true,cp_model_presolve:false")); + int count = 0; + model.Add( + NewFeasibleSolutionObserver([&count](const CpSolverResponse& response) { + LOG(INFO) << gtl::LogContainer(response.solution()); + ++count; + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + + EXPECT_EQ(count, 51); +} + +TEST(ExpandTableTest, UnsatTable) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 4 ] } + variables { domain: [ 5, 9 ] } + constraints { table { vars: 0 vars: 1 values: 3 values: 3 } } + )pb"); + + Model model; + model.Add(NewSatParameters("cp_model_presolve:false")); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(ExpandTableTest, UnsatNegatedTable) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + constraints { + table { + vars: 0 + values: [ 0, 1 ] + negated: true + } + } + )pb"); + + const CpSolverResponse response = Solve(model_proto); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(ExpandAllDiffTest, Permutation) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 1 ] } + constraints { + all_diff { + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + } + } + )pb"); + + absl::btree_set> found_solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "presolve_cp_model:false", &found_solutions); + absl::btree_set> expected{ + {0, 2, 1}, {2, 0, 1}, {1, 2, 0}, {2, 1, 0}}; + EXPECT_EQ(found_solutions, expected); +} + +TEST(ExpandInverseTest, CountInvolution) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + constraints { + inverse { + f_direct: [ 0, 1, 2 ] + f_inverse: [ 0, 1, 2 ] + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + + // On 3 elements, we either have the identity or one of the 3 two cycle. + EXPECT_EQ(4, solutions.size()); +} + +TEST(ExpandInverseTest, DuplicateAtDifferentPosition) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + constraints { + inverse { + f_direct: [ 0, 1, 2, 3 ] + f_inverse: [ 4, 5, 6, 0 ] + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + + // f(0) = 1 has 2 solutions, same with f(0) = 2. + EXPECT_EQ(4, solutions.size()); +} + +TEST(ExpandSmallLinearTest, ReplaceNonEqual) { + CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 5 ] } + variables { domain: [ 0, 5 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + linear { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + domain: [ 0, 4, 6, 10 ] + } + } + )pb"); + Model model; + PresolveContext context(&model, &initial_model, nullptr); + context.InitializeNewDomains(); + context.InsertVarValueEncoding(2, 0, 0); + context.InsertVarValueEncoding(3, 0, 1); + context.InsertVarValueEncoding(4, 0, 2); + context.InsertVarValueEncoding(5, 0, 3); + context.InsertVarValueEncoding(6, 0, 4); + context.InsertVarValueEncoding(7, 0, 5); + context.InsertVarValueEncoding(8, 1, 0); + context.InsertVarValueEncoding(9, 1, 1); + context.InsertVarValueEncoding(10, 1, 2); + context.InsertVarValueEncoding(11, 1, 3); + context.InsertVarValueEncoding(12, 1, 4); + context.InsertVarValueEncoding(13, 1, 5); + ExpandCpModel(&context); + + const CpModelProto expected_model = ParseTestProto(R"pb( + variables { domain: 0 domain: 5 } + variables { domain: 0 domain: 5 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + constraints {} + constraints { + enforcement_literal: 2 + linear { vars: 0 coeffs: 1 domain: 0 domain: 0 } + } + constraints { + enforcement_literal: -3 + linear { + vars: 0 + coeffs: 1 + domain: -9223372036854775808 + domain: -1 + domain: 1 + domain: 9223372036854775807 + } + } + constraints { + enforcement_literal: 3 + linear { vars: 0 coeffs: 1 domain: 1 domain: 1 } + } + constraints { + enforcement_literal: -4 + linear { + vars: 0 + coeffs: 1 + domain: -9223372036854775808 + domain: 0 + domain: 2 + domain: 9223372036854775807 + } + } + constraints { + enforcement_literal: 4 + linear { vars: 0 coeffs: 1 domain: 2 domain: 2 } + } + constraints { + enforcement_literal: -5 + linear { + vars: 0 + coeffs: 1 + domain: -9223372036854775808 + domain: 1 + domain: 3 + domain: 9223372036854775807 + } + } + constraints { + enforcement_literal: 5 + linear { vars: 0 coeffs: 1 domain: 3 domain: 3 } + } + constraints { + enforcement_literal: -6 + linear { + vars: 0 + coeffs: 1 + domain: -9223372036854775808 + domain: 2 + domain: 4 + domain: 9223372036854775807 + } + } + constraints { + enforcement_literal: 6 + linear { vars: 0 coeffs: 1 domain: 4 domain: 4 } + } + constraints { + enforcement_literal: -7 + linear { + vars: 0 + coeffs: 1 + domain: -9223372036854775808 + domain: 3 + domain: 5 + domain: 9223372036854775807 + } + } + constraints { + enforcement_literal: 7 + linear { vars: 0 coeffs: 1 domain: 5 domain: 5 } + } + constraints { + enforcement_literal: -8 + linear { + vars: 0 + coeffs: 1 + domain: -9223372036854775808 + domain: 4 + domain: 6 + domain: 9223372036854775807 + } + } + constraints { + enforcement_literal: 8 + linear { vars: 1 coeffs: 1 domain: 0 domain: 0 } + } + constraints { + enforcement_literal: -9 + linear { + vars: 1 + coeffs: 1 + domain: -9223372036854775808 + domain: -1 + domain: 1 + domain: 9223372036854775807 + } + } + constraints { + enforcement_literal: 9 + linear { vars: 1 coeffs: 1 domain: 1 domain: 1 } + } + constraints { + enforcement_literal: -10 + linear { + vars: 1 + coeffs: 1 + domain: -9223372036854775808 + domain: 0 + domain: 2 + domain: 9223372036854775807 + } + } + constraints { + enforcement_literal: 10 + linear { vars: 1 coeffs: 1 domain: 2 domain: 2 } + } + constraints { + enforcement_literal: -11 + linear { + vars: 1 + coeffs: 1 + domain: -9223372036854775808 + domain: 1 + domain: 3 + domain: 9223372036854775807 + } + } + constraints { + enforcement_literal: 11 + linear { vars: 1 coeffs: 1 domain: 3 domain: 3 } + } + constraints { + enforcement_literal: -12 + linear { + vars: 1 + coeffs: 1 + domain: -9223372036854775808 + domain: 2 + domain: 4 + domain: 9223372036854775807 + } + } + constraints { + enforcement_literal: 12 + linear { vars: 1 coeffs: 1 domain: 4 domain: 4 } + } + constraints { + enforcement_literal: -13 + linear { + vars: 1 + coeffs: 1 + domain: -9223372036854775808 + domain: 3 + domain: 5 + domain: 9223372036854775807 + } + } + constraints { + enforcement_literal: 13 + linear { vars: 1 coeffs: 1 domain: 5 domain: 5 } + } + constraints { + enforcement_literal: -14 + linear { + vars: 1 + coeffs: 1 + domain: -9223372036854775808 + domain: 4 + domain: 6 + domain: 9223372036854775807 + } + } + constraints { bool_or { literals: -3 literals: -14 } } + constraints { bool_or { literals: -4 literals: -13 } } + constraints { bool_or { literals: -5 literals: -12 } } + constraints { bool_or { literals: -6 literals: -11 } } + constraints { bool_or { literals: -7 literals: -10 } } + constraints { bool_or { literals: -8 literals: -9 } } + )pb"); + EXPECT_THAT(initial_model, testing::EqualsProto(expected_model)); +} + +TEST(TableExpandTest, UsedToFail) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 3 ] } + variables { domain: [ 0, 3 ] } + variables { domain: [ 0, 3 ] } + variables { domain: [ 0, 3 ] } + variables { domain: [ 0, 3 ] } + variables { domain: [ 0, 3 ] } + variables { domain: [ 0, 3 ] } + variables { domain: [ 0, 3 ] } + constraints { + table { + vars: [ 0, 4, 1, 5 ] + values: [ 0, 0, 2, 2 ] + values: [ 1, 0, 3, 0 ] + values: [ 2, 2, 0, 0 ] + values: [ 3, 0, 1, 0 ] + } + } + constraints { + table { + vars: [ 1, 5, 3, 6 ] + values: [ 0, 0, 2, 2 ] + values: [ 1, 0, 3, 0 ] + values: [ 2, 2, 0, 0 ] + values: [ 3, 0, 1, 0 ] + } + } + constraints { + table { + vars: [ 2, 6, 3, 7 ] + values: [ 0, 0, 2, 2 ] + values: [ 1, 0, 3, 0 ] + values: [ 2, 2, 0, 0 ] + values: [ 3, 0, 1, 0 ] + } + } + constraints { + table { + vars: [ 3, 7, 0, 4 ] + values: [ 0, 0, 2, 2 ] + values: [ 1, 0, 3, 0 ] + values: [ 2, 2, 0, 0 ] + values: [ 3, 0, 1, 0 ] + } + } + )pb"); + + SatParameters params; + params.set_cp_model_presolve(false); + const CpSolverResponse response = SolveWithParameters(initial_model, params); + EXPECT_EQ(INFEASIBLE, response.status()); +} + +TEST(LinMaxExpansionTest, SimpleEnumeration) { + CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 5 ] } + variables { domain: [ 0, 5 ] } + variables { domain: [ 0, 6 ] } + constraints { + lin_max { + target { vars: 0 coeffs: 1 offset: 1 } + exprs { vars: 1 coeffs: 2 } + exprs: { vars: 2 coeffs: 1 offset: -3 } + } + } + )pb"); + absl::btree_set> found_solutions; + const CpSolverResponse response = SolveAndCheck( + initial_model, "max_lin_max_size_for_expansion:4", &found_solutions); + absl::btree_set> expected{ + {0, 0, 4}, {1, 0, 5}, {1, 1, 0}, {1, 1, 1}, {1, 1, 2}, {1, 1, 3}, + {1, 1, 4}, {1, 1, 5}, {2, 0, 6}, {2, 1, 6}, {3, 2, 0}, {3, 2, 1}, + {3, 2, 2}, {3, 2, 3}, {3, 2, 4}, {3, 2, 5}, {3, 2, 6}, {5, 3, 0}, + {5, 3, 1}, {5, 3, 2}, {5, 3, 3}, {5, 3, 4}, {5, 3, 5}, {5, 3, 6}}; + EXPECT_EQ(found_solutions, expected); +} + +TEST(LinMaxExpansionTest, GoldenTest) { + CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 5 ] } + variables { domain: [ 0, 5 ] } + variables { domain: [ 0, 6 ] } + constraints { + lin_max { + target { vars: 0 coeffs: 1 offset: 1 } + exprs { vars: 1 coeffs: 2 } + exprs: { vars: 2 coeffs: 1 offset: -3 } + } + } + )pb"); + Model model; + model.GetOrCreate()->set_max_lin_max_size_for_expansion(4); + PresolveContext context(&model, &initial_model, nullptr); + ExpandCpModel(&context); + + const CpModelProto expected_model = ParseTestProto(R"pb( + variables { domain: 0 domain: 5 } + variables { domain: 0 domain: 5 } + variables { domain: 0 domain: 6 } + variables { domain: 0 domain: 1 } + constraints {} + constraints { + linear { + vars: 0 + vars: 1 + coeffs: 1 + coeffs: -2 + domain: -1 + domain: 9223372036854775806 + } + } + constraints { + linear { + vars: 0 + vars: 2 + coeffs: 1 + coeffs: -1 + domain: -4 + domain: 9223372036854775803 + } + } + constraints { + enforcement_literal: 3 + linear { + vars: 0 + vars: 1 + coeffs: 1 + coeffs: -2 + domain: -9223372036854775808 + domain: -1 + } + } + constraints { + enforcement_literal: -4 + linear { + vars: 0 + vars: 2 + coeffs: 1 + coeffs: -1 + domain: -9223372036854775808 + domain: -4 + } + } + )pb"); + EXPECT_THAT(initial_model, testing::EqualsProto(expected_model)); +} + +TEST(FinalExpansionForLinearConstraintTest, ComplexLinearExpansion) { + CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + constraints { + linear { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + domain: [ 0, 2, 4, 6, 8, 10 ] + } + } + solution_hint { + vars: [ 0, 1 ] + values: [ 1, 5 ] + } + )pb"); + Model model; + PresolveContext context(&model, &initial_model, nullptr); + + context.InitializeNewDomains(); + context.LoadSolutionHint(); + + FinalExpansionForLinearConstraint(&context); + + const CpModelProto expected_model = ParseTestProto(R"pb( + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints {} + constraints { bool_or { literals: [ 2, 3, 4 ] } } + constraints { + enforcement_literal: 2 + linear { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + domain: [ 0, 2 ] + } + } + constraints { + enforcement_literal: 3 + linear { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + domain: [ 4, 6 ] + } + } + constraints { + enforcement_literal: 4 + linear { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + domain: [ 8, 10 ] + } + } + solution_hint { + vars: [ 0, 1 ] + values: [ 1, 5 ] + } + )pb"); + EXPECT_THAT(initial_model, testing::EqualsProto(expected_model)); + + // We should properly complete the hint and choose the bucket [4, 6]. + EXPECT_THAT(context.SolutionHint(), ::testing::ElementsAre(1, 5, 0, 1, 0)); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/cp_model_lns.cc b/ortools/sat/cp_model_lns.cc index 5e8104a47fe..a3ccabe003c 100644 --- a/ortools/sat/cp_model_lns.cc +++ b/ortools/sat/cp_model_lns.cc @@ -46,6 +46,7 @@ #include "ortools/sat/cp_model_presolve.h" #include "ortools/sat/cp_model_solver_helpers.h" #include "ortools/sat/cp_model_utils.h" +#include "ortools/sat/diffn_util.h" #include "ortools/sat/integer.h" #include "ortools/sat/linear_constraint_manager.h" #include "ortools/sat/linear_programming_constraint.h" @@ -806,33 +807,31 @@ void InsertCumulativePrecedences( } } -struct Rectangle { +struct IndexedRectangle { int interval_index; - int64_t x_start; - int64_t x_end; - int64_t y_start; - int64_t y_end; + Rectangle r; - bool operator<(const Rectangle& other) const { - return std::tie(x_start, x_end) < std::tie(other.x_start, other.x_end); + bool operator<(const IndexedRectangle& other) const { + return std::tie(r.x_min, r.x_max) < std::tie(other.r.x_min, other.r.x_max); } }; void InsertRectanglePredecences( - const std::vector& rectangles, + const std::vector& rectangles, absl::flat_hash_set>* precedences) { // TODO(user): Refine set of interesting points. - std::vector interesting_points; - for (const Rectangle& r : rectangles) { - interesting_points.push_back(r.y_end - 1); + std::vector interesting_points; + for (const IndexedRectangle& idx_r : rectangles) { + interesting_points.push_back(idx_r.r.y_max - 1); } gtl::STLSortAndRemoveDuplicates(&interesting_points); std::vector demands; - for (const int64_t t : interesting_points) { + for (const IntegerValue t : interesting_points) { demands.clear(); - for (const Rectangle& r : rectangles) { - if (r.y_start > t || r.y_end <= t) continue; - demands.push_back({r.interval_index, r.x_start, r.x_end, 1}); + for (const IndexedRectangle& idx_r : rectangles) { + if (idx_r.r.y_min > t || idx_r.r.y_max <= t) continue; + demands.push_back({idx_r.interval_index, idx_r.r.x_min.value(), + idx_r.r.x_max.value(), 1}); } std::sort(demands.begin(), demands.end()); InsertPrecedencesFromSortedListOfNonOverlapingIntervals(demands, @@ -848,8 +847,8 @@ void InsertNoOverlap2dPrecedences( std::vector demands; const NoOverlap2DConstraintProto& no_overlap_2d = model_proto.constraints(no_overlap_2d_index).no_overlap_2d(); - std::vector x_main; - std::vector y_main; + std::vector x_main; + std::vector y_main; for (int i = 0; i < no_overlap_2d.x_intervals_size(); ++i) { // Ignore unperformed rectangles. const int x_interval_index = no_overlap_2d.x_intervals(i); @@ -876,10 +875,16 @@ void InsertNoOverlap2dPrecedences( // Ignore rectangles with zero area. if (x_start_value == x_end_value || y_start_value == y_end_value) continue; - x_main.push_back({x_interval_index, x_start_value, x_end_value, - y_start_value, y_end_value}); - y_main.push_back({y_interval_index, y_start_value, y_end_value, - x_start_value, x_end_value}); + x_main.push_back({.interval_index = x_interval_index, + .r = {.x_min = x_start_value, + .x_max = x_end_value, + .y_min = y_start_value, + .y_max = y_end_value}}); + y_main.push_back({.interval_index = y_interval_index, + .r = {.x_min = y_start_value, + .x_max = y_end_value, + .y_min = x_start_value, + .y_max = x_end_value}}); } if (x_main.empty() || y_main.empty()) return; diff --git a/ortools/sat/cp_model_postsolve_test.cc b/ortools/sat/cp_model_postsolve_test.cc new file mode 100644 index 00000000000..dee1d25448b --- /dev/null +++ b/ortools/sat/cp_model_postsolve_test.cc @@ -0,0 +1,347 @@ +// Copyright 2010-2024 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/sat/cp_model_postsolve.h" + +#include +#include + +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/parse_test_proto.h" +#include "ortools/sat/cp_model.pb.h" +#include "ortools/util/logging.h" +#include "ortools/util/sorted_interval_list.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::google::protobuf::contrib::parse_proto::ParseTestProto; + +// Note that the postsolve is already tested in many of our solver tests or +// random presolve tests. We just have a small unit test here. +TEST(PostsolveResponseTest, BasicExample) { + // Fixing z will allow the postsolve code to reconstruct all values. + const CpModelProto mapping_proto = ParseTestProto(R"pb( + variables { name: 'x' domain: 0 domain: 10 } + variables { name: 'y' domain: 0 domain: 10 } + variables { name: 'z' domain: 0 domain: 10 } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 2, -3 ] + domain: [ 5, 5 ] + } + } + constraints { + linear { + vars: [ 1, 2 ] + coeffs: [ 3, -1 ] + domain: [ 5, 5 ] + } + } + )pb"); + + std::vector solution = {1}; + std::vector postsolve_mapping = {2}; // The solution fix z. + PostsolveResponse(/*num_variables_in_original_model=*/3, mapping_proto, + postsolve_mapping, &solution); + + // x + 2y - 3z = 5 + // 3y - z = 5 + // z = 1 + EXPECT_THAT(solution, ::testing::ElementsAre(4, 2, 1)); +} + +TEST(PostsolveResponseTest, ExactlyOneExample1) { + const CpModelProto mapping_proto = ParseTestProto(R"pb( + variables { name: 'x' domain: 0 domain: 1 } + variables { name: 'y' domain: 0 domain: 1 } + variables { name: 'z' domain: 0 domain: 1 } + constraints { exactly_one { literals: [ 0, 1, 2 ] } } + )pb"); + + std::vector solution = {1}; + std::vector postsolve_mapping = {2}; // The solution fix z. + PostsolveResponse(/*num_variables_in_original_model=*/3, mapping_proto, + postsolve_mapping, &solution); + EXPECT_THAT(solution, ::testing::ElementsAre(0, 0, 1)); +} + +TEST(PostsolveResponseTest, ExactlyOneExample2) { + const CpModelProto mapping_proto = ParseTestProto(R"pb( + variables { name: 'x' domain: 0 domain: 1 } + variables { name: 'y' domain: 0 domain: 1 } + variables { name: 'z' domain: 0 domain: 1 } + constraints { exactly_one { literals: [ 0, 1, 2 ] } } + )pb"); + + std::vector solution = {0}; + std::vector postsolve_mapping = {2}; // The solution fix z. + PostsolveResponse(/*num_variables_in_original_model=*/3, mapping_proto, + postsolve_mapping, &solution); + + // One variable is set to one. + EXPECT_THAT(solution, ::testing::ElementsAre(0, 1, 0)); +} + +TEST(PostsolveResponseTest, Element) { + // Fixing z will allow the postsolve code to reconstruct all values. + const CpModelProto mapping_proto = ParseTestProto(R"pb( + variables { + name: 'index' + domain: [ 0, 1 ] + } + variables { + name: 'a' + domain: [ 1, 10 ] + } + variables { + name: 'b' + domain: [ 0, 10 ] + } + variables { + name: 'target' + domain: [ 0, 10 ] + } + constraints { + element { + index: 0 + vars: [ 1, 2 ] + target: 3 + } + } + )pb"); + + std::vector solution; + std::vector postsolve_mapping = {}; + PostsolveResponse(/*num_variables_in_original_model=*/4, mapping_proto, + postsolve_mapping, &solution); + EXPECT_THAT(solution, ::testing::ElementsAre(0, 1, 0, 1)); +} + +TEST(PostsolveResponseTest, VariableElement) { + const CpModelProto mapping_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 129 ] } + variables { domain: [ 1, 5 ] } + variables { domain: [ 0, 129 ] } + variables { domain: [ 2, 2 ] } + constraints { element { index: 3 target: 2 vars: 0 vars: 1 vars: 0 } } + )pb"); + + std::vector solution; + std::vector postsolve_mapping = {}; + PostsolveResponse(/*num_variables_in_original_model=*/4, mapping_proto, + postsolve_mapping, &solution); + EXPECT_THAT(solution, ::testing::ElementsAre(0, 1, 0, 2)); +} + +// Note that our postolve code is "limited" when it come to solving a single +// linear equation since we should only encounter "simple" case. +TEST(PostsolveResponseTest, TrickyLinearCase) { + // The equation is 2x + y = z + // + // It mostly work all the time, except if we decide to make z - y not a + // multiple of two. This is not necessarily detected by our presolve since + // 2 * [0, 124] is too complex to represent. Yet for any value of x and y + // there is a possible z, but the reverse is not true, since y = 1, z = 0 is + // not feasible. + // + // The preosolve should deal with that by putting z first so that the + // postsolve code do not fail. + const CpModelProto mapping_proto = ParseTestProto(R"pb( + variables { + name: 'x' + domain: [ 0, 124 ] + } + variables { + name: 'y' + domain: [ 0, 1 ] + } + variables { + name: 'z' + domain: [ 0, 255 ] + } + constraints { + linear { + vars: [ 2, 0, 1 ] + coeffs: [ -1, 2, 1 ] + domain: [ 0, 0 ] + } + } + )pb"); + + // The likely response (there are many possible). + std::vector solution; + CpSolverResponse response; + response.set_status(OPTIMAL); + std::vector postsolve_mapping; + PostsolveResponse(/*num_variables_in_original_model=*/3, mapping_proto, + postsolve_mapping, &solution); + EXPECT_THAT(solution, ::testing::ElementsAre(0, 0, 0)); +} + +// This used to fail because we where computing the EXACT domain atteignable +// by the sum of discrete domains, which have a lot of disjoint part. +// +// But our presolve was fine, because adding each of them to the loose rhs +// domain just result in a domain with a small complexity. +TEST(PostsolveResponseTest, ComplexityIssue) { + CpModelProto mapping_proto; + + // N variables such that their sum can be and even number. If we try to + // compute the exact domains of their sum, we are quadratic in compexity. + const int num_variables = 30; + for (int i = 0; i < num_variables; ++i) { + IntegerVariableProto* var = mapping_proto.add_variables(); + var->add_domain(0); + var->add_domain(0); + const int value = 1 << (1 + i); + var->add_domain(value); + var->add_domain(value); + } + + // A linear constraint sum variable in [0, 1e9]. + ConstraintProto* ct = mapping_proto.add_constraints(); + ct->mutable_linear()->add_domain(0); + ct->mutable_linear()->add_domain(1e9); + for (int i = 0; i < num_variables; ++i) { + ct->mutable_linear()->add_vars(i); + ct->mutable_linear()->add_coeffs(1); + } + + // The likely response (there are many possible). + std::vector solution; + std::vector postsolve_mapping; + PostsolveResponse(num_variables, mapping_proto, postsolve_mapping, &solution); + ASSERT_EQ(solution.size(), num_variables); +} + +TEST(FillTightenedDomainInResponseTest, BasicBehavior) { + // Original model. + const CpModelProto original_model = ParseTestProto(R"pb( + variables { + name: 'x' + domain: [ 0, 124 ] + } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 255 ] } + )pb"); + + // We might have more variable there. + // Also the domains might be tighter. + const CpModelProto mapping_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 100 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 255 ] } + variables { domain: [ 0, 17 ] } + variables { domain: [ 0, 18 ] } + )pb"); + + // Lets assume the presolved mode contains 3 variables, 2 in common. + std::vector postsolve_mapping{0, 2, 4}; + std::vector search_bounds{Domain(0, 100), Domain(0, 0), Domain(3, 7)}; + + // Call the postsolving. + SolverLogger logger; + CpSolverResponse response; + FillTightenedDomainInResponse(original_model, mapping_proto, + postsolve_mapping, search_bounds, &response, + &logger); + + // Lets test by constructing a model for easy comparison. + CpModelProto returned_model; + for (const IntegerVariableProto& var : response.tightened_variables()) { + *returned_model.add_variables() = var; + } + + const CpModelProto expected_model = ParseTestProto(R"pb( + variables { + name: 'x' + domain: [ 0, 100 ] + } # presolve reduced the domain. + variables { domain: [ 0, 1 ] } # no info. + variables { domain: [ 0, 0 ] } # was fixed by search. + )pb"); + EXPECT_THAT(returned_model, testing::EqualsProto(expected_model)); +} + +TEST(FillTightenedDomainInResponseTest, WithAffine) { + // Original model. + const CpModelProto original_model = ParseTestProto(R"pb( + variables { domain: [ 0, 124 ] } + variables { domain: [ 0, 50 ] } + variables { domain: [ 0, 255 ] } + )pb"); + + // We might have more variable there. + // Also the domains might be tighter. + const CpModelProto mapping_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 100 ] } + variables { domain: [ 0, 50 ] } + variables { domain: [ 0, 100 ] } + variables { domain: [ 0, 17 ] } + variables { domain: [ 0, 18 ] } + variables { domain: [ 0, 19 ] } + constraints { + linear { + vars: [ 0, 3 ] + coeffs: [ 2, 1 ] + domain: [ 10, 10 ] + } + } + constraints { + linear { + vars: [ 1, 4 ] + coeffs: [ 1, 1 ] + domain: [ 10, 10 ] + } + } + constraints { + linear { + vars: [ 5, 2 ] + coeffs: [ 2, 1 ] + domain: [ 10, 10 ] + } + } + )pb"); + + std::vector postsolve_mapping{3, 4, 5}; + std::vector search_bounds{Domain(0, 20), Domain(0, 20), Domain(3, 5)}; + + // Call the postsolving. + SolverLogger logger; + logger.EnableLogging(true); + CpSolverResponse response; + FillTightenedDomainInResponse(original_model, mapping_proto, + postsolve_mapping, search_bounds, &response, + &logger); + + // Lets test by constructing a model for easy comparison. + CpModelProto returned_model; + for (const IntegerVariableProto& var : response.tightened_variables()) { + *returned_model.add_variables() = var; + } + + const CpModelProto expected_model = ParseTestProto(R"pb( + variables { domain: [ 0, 5 ] } # 2 * v = 10 - [0, 17] + variables { domain: [ 0, 10 ] } # v = 10 - [0, 18] + variables { domain: [ 0, 4 ] } # v = 10 - 2 * [3, 5] + )pb"); + EXPECT_THAT(returned_model, testing::EqualsProto(expected_model)); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/cp_model_search_test.cc b/ortools/sat/cp_model_search_test.cc new file mode 100644 index 00000000000..c2961a523d1 --- /dev/null +++ b/ortools/sat/cp_model_search_test.cc @@ -0,0 +1,297 @@ +// Copyright 2010-2024 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/sat/cp_model_search.h" + +#include +#include + +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/parse_test_proto.h" +#include "ortools/sat/cp_model.pb.h" +#include "ortools/sat/cp_model_solver.h" +#include "ortools/sat/model.h" +#include "ortools/sat/sat_parameters.pb.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::google::protobuf::contrib::parse_proto::ParseTestProto; + +CpModelProto CreateExactlyOneTrueBooleanCpModel(int size) { + CpModelProto model_proto; + auto* exactly_one_constraint = + model_proto.add_constraints()->mutable_exactly_one(); + DecisionStrategyProto* const search_strategy = + model_proto.add_search_strategy(); + + for (int i = 0; i < size; ++i) { + IntegerVariableProto* const var = model_proto.add_variables(); + var->add_domain(0); + var->add_domain(1); + exactly_one_constraint->add_literals(i); + search_strategy->add_variables(i); + } + return model_proto; +} + +TEST(RandomSearchTest, CheckDistribution) { + const int kSize = 50; + std::vector winners(kSize, 0); + const int kLoops = 100; + for (int l = 0; l < kLoops; ++l) { + const CpModelProto model_proto = CreateExactlyOneTrueBooleanCpModel(kSize); + Model model; + SatParameters parameters; + parameters.set_search_random_variable_pool_size(10); + parameters.set_cp_model_presolve(false); + parameters.set_search_branching(SatParameters::FIXED_SEARCH); + parameters.set_random_seed(l); + parameters.set_num_workers(1); + model.Add(NewSatParameters(parameters)); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + for (int i = 0; i < kSize; ++i) { + if (response.solution(i)) { + winners[i]++; + } + } + } + for (int i = 0; i < kSize; ++i) { + EXPECT_LE(winners[i], kLoops / 10); + } +} + +TEST(RandomSearchTest, CheckSeed) { + const int kSeeds = 10; + for (int seed = 0; seed < kSeeds; ++seed) { + const int kSize = 20; + std::vector winners(kSize, 0); + const int kLoops = 50; + for (int l = 0; l < kLoops; ++l) { + const CpModelProto model_proto = + CreateExactlyOneTrueBooleanCpModel(kSize); + + SatParameters params; + params.set_randomize_search(true); + params.set_cp_model_presolve(false); + params.set_search_branching(SatParameters::FIXED_SEARCH); + params.set_use_absl_random(false); // Otherwise, each solve changes. + params.set_random_seed(0); + const CpSolverResponse response = + SolveWithParameters(model_proto, params); + for (int i = 0; i < kSize; ++i) { + if (response.solution(i)) { + winners[i]++; + } + } + } + for (int i = 0; i < kSize; ++i) { + EXPECT_TRUE(winners[i] == 0 || winners[i] == kLoops) << winners[i]; + } + } +} + +TEST(BasicFixedSearchBehaviorTest, Default) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 4, 50 ] } + variables { domain: [ 3, 7 ] } + variables { domain: [ 0, 7 ] } + variables { domain: [ 4, 5 ] } + variables { domain: [ 3, 9 ] } + constraints { + all_diff { + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + exprs { vars: 3 coeffs: 1 } + exprs { vars: 4 coeffs: 1 } + } + } + )pb"); + Model model; + model.Add(NewSatParameters( + "cp_model_presolve:false,search_branching:FIXED_SEARCH")); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_THAT(response.solution(), testing::ElementsAre(4, 3, 0, 5, 6)); +} + +TEST(BasicFixedSearchBehaviorTest, ReverseOrder) { + // Note that SELECT_LOWER_HALF or SELECT_MIN_VALUE result in the same + // solution. + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 4, 50 ] } + variables { domain: [ 3, 7 ] } + variables { domain: [ 0, 7 ] } + variables { domain: [ 4, 5 ] } + variables { domain: [ 3, 9 ] } + constraints { + all_diff { + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + exprs { vars: 3 coeffs: 1 } + exprs { vars: 4 coeffs: 1 } + } + } + search_strategy { + variables: [ 4, 3, 2, 1, 0 ] + variable_selection_strategy: CHOOSE_FIRST + domain_reduction_strategy: SELECT_LOWER_HALF + } + )pb"); + Model model; + model.Add(NewSatParameters( + "cp_model_presolve:false,search_branching:FIXED_SEARCH")); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_THAT(response.solution(), testing::ElementsAre(6, 5, 0, 4, 3)); +} + +// The strategies that sort variables according to their domain do not have +// a fixed solution depending on the propagation strength... +TEST(BasicFixedSearchBehaviorTest, MinDomainSize) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 4, 10 ] } + variables { domain: [ 3, 7 ] } + variables { domain: [ 0, 7 ] } + variables { domain: [ 4, 5 ] } + variables { domain: [ 3, 9 ] } + constraints { + all_diff { + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + exprs { vars: 2 coeffs: 1 } + exprs { vars: 3 coeffs: 1 } + exprs { vars: 4 coeffs: 1 } + } + } + search_strategy { + variables: [ 0, 1, 2, 3, 4 ] + variable_selection_strategy: CHOOSE_MIN_DOMAIN_SIZE + domain_reduction_strategy: SELECT_MAX_VALUE + } + )pb"); + Model model; + model.Add(NewSatParameters( + "cp_model_presolve:false,search_branching:FIXED_SEARCH")); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_THAT(response.solution(), testing::ElementsAre(10, 7, 6, 5, 9)); +} + +TEST(BasicFixedSearchBehaviorTest, WithTransformation1) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 3, 10 ] } + variables { domain: [ 3, 7 ] } + constraints { + all_diff { + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + search_strategy { + exprs { vars: 0 coeffs: 1 offset: 4 } + exprs { vars: 1 coeffs: 4 } + variable_selection_strategy: CHOOSE_LOWEST_MIN + domain_reduction_strategy: SELECT_MIN_VALUE + } + )pb"); + Model model; + model.Add(NewSatParameters( + "cp_model_presolve:false,search_branching:FIXED_SEARCH")); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_THAT(response.solution(), testing::ElementsAre(3, 4)); +} + +TEST(BasicFixedSearchBehaviorTest, WithTransformation2) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 3, 7 ] } + variables { domain: [ 3, 7 ] } + constraints { + all_diff { + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + search_strategy { + exprs { vars: 0 coeffs: -1 offset: 4 } + exprs { vars: 1 coeffs: -4 } + variable_selection_strategy: CHOOSE_LOWEST_MIN + domain_reduction_strategy: SELECT_MIN_VALUE + } + )pb"); + Model model; + model.Add(NewSatParameters( + "cp_model_presolve:false,search_branching:FIXED_SEARCH")); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_THAT(response.solution(), testing::ElementsAre(6, 7)); +} + +TEST(BasicFixedSearchBehaviorTest, MedianTest) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 8 ] } + variables { domain: [ 0, 8 ] } + constraints { + linear { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + domain: [ 8, 100 ] + } + } + search_strategy { + variables: [ 0, 1 ] + variable_selection_strategy: CHOOSE_FIRST + domain_reduction_strategy: SELECT_MEDIAN_VALUE + } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + params.set_search_branching(SatParameters::FIXED_SEARCH); + const CpSolverResponse response = SolveWithParameters(model_proto, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_THAT(response.solution(), testing::ElementsAre(4, 6)); +} + +TEST(BasicFixedSearchBehaviorTest, MedianTest2) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 20 ] } + variables { domain: [ 6, 12 ] } + constraints { + all_diff { + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + search_strategy { + variables: [ 0, 1 ] + variable_selection_strategy: CHOOSE_MAX_DOMAIN_SIZE + domain_reduction_strategy: SELECT_MEDIAN_VALUE + } + )pb"); + SatParameters params; + params.set_keep_all_feasible_solutions_in_presolve(true); + params.set_search_branching(SatParameters::FIXED_SEARCH); + const CpSolverResponse response = SolveWithParameters(model_proto, params); + + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_THAT(response.solution(), testing::ElementsAre(10, 8)); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/cp_model_solver.cc b/ortools/sat/cp_model_solver.cc index f47ec0aa4da..728524ed369 100644 --- a/ortools/sat/cp_model_solver.cc +++ b/ortools/sat/cp_model_solver.cc @@ -1172,7 +1172,7 @@ class LnsSolver : public SubSolver { static_cast(generator_->num_fully_solved_calls()) / static_cast(num_calls); const std::string lns_info = absl::StrFormat( - "%s (d=%0.2f s=%i t=%0.2f p=%0.2f stall=%d h=%s)", source_info, + "%s (d=%0.3f s=%i t=%0.2f p=%0.2f stall=%d h=%s)", source_info, data.difficulty, task_id, data.deterministic_limit, fully_solved_proportion, stall, search_info); diff --git a/ortools/sat/cuts.cc b/ortools/sat/cuts.cc index e65af77517f..af4aec65ded 100644 --- a/ortools/sat/cuts.cc +++ b/ortools/sat/cuts.cc @@ -1085,6 +1085,17 @@ struct LargeContribFirst { } }; +struct LargeLpValueFirst { + bool operator()(const CutTerm& a, const CutTerm& b) const { + if (a.lp_value == b.lp_value) { + // Prefer high coefficients if the distance is the same. + // We have more chance to get a cover this way. + return a.coeff > b.coeff; + } + return a.lp_value > b.lp_value; + } +}; + // When minimizing a cover we want to remove bad score (large dist) divided by // item size. Note that here we assume item are "boolean" fully taken or not. // for general int we use (lp_dist / bound_diff) / (coeff * bound_diff) which @@ -1117,14 +1128,15 @@ struct KnapsackRemove { template int CoverCutHelper::MinimizeCover(int cover_size, absl::int128 slack) { CHECK_GT(slack, 0); - std::sort(cut_.terms.begin(), cut_.terms.begin() + cover_size, Compare()); + absl::Span terms = absl::MakeSpan(cut_.terms); + std::sort(terms.begin(), terms.begin() + cover_size, Compare()); for (int i = 0; i < cover_size;) { - const CutTerm& t = cut_.terms[i]; + const CutTerm& t = terms[i]; const absl::int128 contrib = absl::int128(t.bound_diff.value()) * absl::int128(t.coeff.value()); if (contrib < slack) { slack -= contrib; - std::swap(cut_.terms[i], cut_.terms[--cover_size]); + std::swap(terms[i], terms[--cover_size]); } else { ++i; } @@ -1136,16 +1148,17 @@ int CoverCutHelper::MinimizeCover(int cover_size, absl::int128 slack) { template int CoverCutHelper::GetCoverSize(int relevant_size) { if (relevant_size == 0) return 0; + absl::Span terms = absl::MakeSpan(cut_.terms); // Take first all at variable at upper bound, and ignore the one at lower // bound. int part1 = 0; for (int i = 0; i < relevant_size;) { - CutTerm& term = cut_.terms[i]; + CutTerm& term = terms[i]; const double dist = term.LpDistToMaxValue(); if (dist < 1e-6) { // Move to part 1. - std::swap(term, cut_.terms[part1]); + std::swap(term, terms[part1]); ++i; ++part1; } else if (term.lp_value > 1e-6) { @@ -1154,30 +1167,27 @@ int CoverCutHelper::GetCoverSize(int relevant_size) { } else { // Exclude entirely (part 3). --relevant_size; - std::swap(term, cut_.terms[relevant_size]); + std::swap(term, terms[relevant_size]); } } - std::sort(cut_.terms.begin() + part1, cut_.terms.begin() + relevant_size, - CompareAdd()); + std::sort(terms.begin() + part1, terms.begin() + relevant_size, CompareAdd()); // We substract the initial rhs to avoid overflow. - CHECK_GE(cut_.rhs, 0); + DCHECK_GE(cut_.rhs, 0); absl::int128 max_shifted_activity = -cut_.rhs; absl::int128 shifted_round_up = -cut_.rhs; int cover_size = 0; - double dist = 0.0; for (; cover_size < relevant_size; ++cover_size) { if (max_shifted_activity > 0) break; - const CutTerm& term = cut_.terms[cover_size]; + const CutTerm& term = terms[cover_size]; max_shifted_activity += absl::int128(term.coeff.value()) * absl::int128(term.bound_diff.value()); shifted_round_up += absl::int128(term.coeff.value()) * std::min(absl::int128(term.bound_diff.value()), absl::int128(std::ceil(term.lp_value - 1e-6))); - dist += term.LpDistToMaxValue(); } - CHECK_GE(cover_size, 0); + DCHECK_GE(cover_size, 0); if (shifted_round_up <= 0) { return 0; } @@ -1187,51 +1197,60 @@ int CoverCutHelper::GetCoverSize(int relevant_size) { // Try a simple cover heuristic. // Look for violated CUT of the form: sum (UB - X) or (X - LB) >= 1. int CoverCutHelper::GetCoverSizeForBooleans() { + absl::Span terms = absl::MakeSpan(cut_.terms); + // Sorting can be slow, so we start by splitting the vector in 3 parts - // [can always be in cover, candidates, can never be in cover]. + // - Can always be in cover + // - Candidates that needs sorting + // - At most one can be in cover (we keep the max). int part1 = 0; - int relevant_size = cut_.terms.size(); - const double threshold = 1.0 - 1.0 / static_cast(relevant_size); + int relevant_size = terms.size(); + int best_in_part3 = -1; + const double threshold = 1.0 - 1.0 / static_cast(terms.size()); for (int i = 0; i < relevant_size;) { - const double lp_value = cut_.terms[i].lp_value; + const double lp_value = terms[i].lp_value; // Exclude non-Boolean. - if (cut_.terms[i].bound_diff > 1) { + if (terms[i].bound_diff > 1) { --relevant_size; - std::swap(cut_.terms[i], cut_.terms[relevant_size]); + std::swap(terms[i], terms[relevant_size]); continue; } if (lp_value >= threshold) { // Move to part 1. - std::swap(cut_.terms[i], cut_.terms[part1]); + std::swap(terms[i], terms[part1]); ++i; ++part1; - } else if (lp_value >= 0.001) { + } else if (lp_value > 0.5) { // Keep in part 2. ++i; } else { - // Exclude entirely (part 3). + // Only keep the max (part 3). --relevant_size; - std::swap(cut_.terms[i], cut_.terms[relevant_size]); + std::swap(terms[i], terms[relevant_size]); + + if (best_in_part3 == -1 || + LargeLpValueFirst()(terms[relevant_size], terms[best_in_part3])) { + best_in_part3 = relevant_size; + } } } + if (best_in_part3 != -1) { + std::swap(terms[relevant_size], terms[best_in_part3]); + ++relevant_size; + } + // Sort by decreasing Lp value. - std::sort(cut_.terms.begin() + part1, cut_.terms.begin() + relevant_size, - [](const CutTerm& a, const CutTerm& b) { - if (a.lp_value == b.lp_value) { - // Prefer low coefficients if the distance is the same. - return a.coeff < b.coeff; - } - return a.lp_value > b.lp_value; - }); + std::sort(terms.begin() + part1, terms.begin() + relevant_size, + LargeLpValueFirst()); double activity = 0.0; int cover_size = relevant_size; absl::int128 slack = -cut_.rhs; for (int i = 0; i < relevant_size; ++i) { - const CutTerm& term = cut_.terms[i]; + const CutTerm& term = terms[i]; activity += term.LpDistToMaxValue(); // As an heuristic we select all the term so that the sum of distance @@ -1259,7 +1278,9 @@ int CoverCutHelper::GetCoverSizeForBooleans() { // possible violation. Note also that we lift as much as possible, so we don't // necessarily optimize for the cut efficacity though. But we do get a // stronger cut. - if (slack <= 0) return 0; + if (slack <= 0) { + return 0; + } if (cover_size == 0) return 0; return MinimizeCover(cover_size, slack); } @@ -1315,11 +1336,12 @@ bool CoverCutHelper::TrySimpleKnapsack(const CutData& input_ct, : GetCoverSizeForBooleans(); if (!has_relevant_int && ib_processor == nullptr) { // If some implied bound substitution are possible, we do not cache anything - // currently because the logic is currently sighlty different betweent the + // currently because the logic is currently sighlty different between the // two code. Fix? has_bool_base_ct_ = true; - bool_base_ct_ = cut_; bool_cover_size_ = cover_size; + if (cover_size == 0) return false; + bool_base_ct_ = cut_; } if (cover_size == 0) return false; @@ -1502,8 +1524,9 @@ bool CoverCutHelper::TryWithLetchfordSouliLifting( // We already called GetCoverSizeForBooleans() and ib_processor was nullptr, // so reuse that info. CHECK(ib_processor == nullptr); - InitializeCut(bool_base_ct_); cover_size = bool_cover_size_; + if (cover_size == 0) return false; + InitializeCut(bool_base_ct_); } else { InitializeCut(input_ct); diff --git a/ortools/sat/flaky_models_test.cc b/ortools/sat/flaky_models_test.cc new file mode 100644 index 00000000000..ce388d8e377 --- /dev/null +++ b/ortools/sat/flaky_models_test.cc @@ -0,0 +1,101 @@ +// Copyright 2010-2024 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "gtest/gtest.h" +#include "ortools/base/parse_test_proto.h" +#include "ortools/sat/cp_model.pb.h" +#include "ortools/sat/cp_model_solver.h" +#include "ortools/sat/sat_parameters.pb.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::google::protobuf::contrib::parse_proto::ParseTestProto; + +TEST(FlakyTest, Issue3108) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + constraints { + enforcement_literal: 2 + interval { + start { vars: 0 coeffs: 1 } + end { vars: 0 coeffs: 1 offset: 1 } + size { offset: 1 } + } + } + constraints { + enforcement_literal: 3 + interval { + start { vars: 1 coeffs: 1 } + end { vars: 1 coeffs: 1 offset: 1 } + size { offset: 1 } + } + } + constraints { + cumulative { + capacity { vars: 4 coeffs: 1 } + intervals: 0 + intervals: 1 + demands { offset: 1 } + demands { offset: 1 } + } + } + constraints { + enforcement_literal: 2 + linear { vars: 0 coeffs: 1 domain: 0 domain: 1 } + } + constraints { + enforcement_literal: -3 + linear { + vars: 0 + coeffs: 1 + domain: -9223372036854775808 + domain: -1 + domain: 2 + domain: 9223372036854775807 + } + } + constraints { + enforcement_literal: 3 + linear { vars: 1 coeffs: 1 domain: 0 domain: 1 } + } + constraints { + enforcement_literal: -4 + linear { + vars: 1 + coeffs: 1 + domain: -9223372036854775808 + domain: -1 + domain: 2 + domain: 9223372036854775807 + } + } + objective { vars: 4 coeffs: 1 } + )pb"); + SatParameters parameters; + parameters.set_log_search_progress(true); + parameters.set_cp_model_probing_level(0); + parameters.set_num_search_workers(1); + const CpSolverResponse response = + SolveWithParameters(model_proto, parameters); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/integer_expr_test.cc b/ortools/sat/integer_expr_test.cc new file mode 100644 index 00000000000..93c02494e5b --- /dev/null +++ b/ortools/sat/integer_expr_test.cc @@ -0,0 +1,1644 @@ +// Copyright 2010-2024 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/sat/integer_expr.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/container/btree_set.h" +#include "absl/log/check.h" +#include "absl/random/distributions.h" +#include "absl/random/random.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/base/logging.h" +#include "ortools/base/parse_test_proto.h" +#include "ortools/port/proto_utils.h" +#include "ortools/sat/cp_model.pb.h" +#include "ortools/sat/cp_model_checker.h" +#include "ortools/sat/cp_model_solver.h" +#include "ortools/sat/cp_model_utils.h" +#include "ortools/sat/integer.h" +#include "ortools/sat/linear_constraint.h" +#include "ortools/sat/model.h" +#include "ortools/sat/sat_base.h" +#include "ortools/sat/sat_parameters.pb.h" +#include "ortools/sat/sat_solver.h" +#include "ortools/util/saturated_arithmetic.h" +#include "ortools/util/sorted_interval_list.h" +#include "ortools/util/strong_integers.h" + +namespace operations_research { +namespace sat { +namespace { + +// Weighted sum <= constant reified. +void AddWeightedSumLowerOrEqualReif(Literal is_le, + absl::Span vars, + absl::Span coefficients, + int64_t upper_bound, Model* model) { + AddWeightedSumLowerOrEqual({is_le}, vars, coefficients, upper_bound, model); + AddWeightedSumGreaterOrEqual({is_le.Negated()}, vars, coefficients, + upper_bound + 1, model); +} + +// Weighted sum >= constant reified. +void AddWeightedSumGreaterOrEqualReif(Literal is_ge, + absl::Span vars, + absl::Span coefficients, + int64_t lower_bound, Model* model) { + AddWeightedSumGreaterOrEqual({is_ge}, vars, coefficients, lower_bound, model); + AddWeightedSumLowerOrEqual({is_ge.Negated()}, vars, coefficients, + lower_bound - 1, model); +} + +// Weighted sum == constant reified. +// TODO(user): Simplify if the constant is at the edge of the possible values. +void AddFixedWeightedSumReif(Literal is_eq, + const std::vector& vars, + const std::vector& coefficients, + int64_t value, Model* model) { + // We creates two extra Boolean variables in this case. The alternative is + // to code a custom propagator for the direction equality => reified. + const Literal is_le = Literal(model->Add(NewBooleanVariable()), true); + const Literal is_ge = Literal(model->Add(NewBooleanVariable()), true); + model->Add(ReifiedBoolAnd({is_le, is_ge}, is_eq)); + AddWeightedSumLowerOrEqualReif(is_le, vars, coefficients, value, model); + AddWeightedSumGreaterOrEqualReif(is_ge, vars, coefficients, value, model); +} + +using ::google::protobuf::contrib::parse_proto::ParseTestProto; + +CpSolverResponse SolveAndCheck( + const CpModelProto& initial_model, absl::string_view extra_parameters = "", + absl::btree_set>* solutions = nullptr) { + SatParameters params; + params.set_enumerate_all_solutions(true); + if (!extra_parameters.empty()) { + params.MergeFromString(extra_parameters); + } + auto observer = [&](const CpSolverResponse& response) { + VLOG(2) << response; + EXPECT_TRUE(SolutionIsFeasible( + initial_model, std::vector(response.solution().begin(), + response.solution().end()))); + if (solutions != nullptr) { + std::vector solution; + for (int var = 0; var < initial_model.variables_size(); ++var) { + solution.push_back(response.solution(var)); + } + solutions->insert(solution); + } + }; + Model model; + model.Add(NewSatParameters(params)); + model.Add(NewFeasibleSolutionObserver(observer)); + return SolveCpModel(initial_model, &model); +} + +// A simple macro to make the code more readable. +#define EXPECT_BOUNDS_EQ(var, lb, ub) \ + EXPECT_TRUE((model.Get(LowerBound(var)) == lb) && \ + (model.Get(UpperBound(var)) == ub)) + +TEST(WeightedSumTest, LevelZeroPropagation) { + Model model; + std::vector vars{model.Add(NewIntegerVariable(4, 9)), + model.Add(NewIntegerVariable(-7, -2)), + model.Add(NewIntegerVariable(3, 8))}; + + const IntegerVariable sum = + model.Add(NewWeightedSum(std::vector{1, -2, 3}, vars)); + EXPECT_EQ(SatSolver::FEASIBLE, model.GetOrCreate()->Solve()); + EXPECT_EQ(model.Get(LowerBound(sum)), 4 + 2 * 2 + 3 * 3); + EXPECT_EQ(model.Get(UpperBound(sum)), 9 + 2 * 7 + 3 * 8); + + // Setting this leave only a slack of 2. + model.Add(LowerOrEqual(sum, 19)); + EXPECT_EQ(SatSolver::FEASIBLE, model.GetOrCreate()->Solve()); + EXPECT_BOUNDS_EQ(vars[0], 4, 6); // coeff = 1, slack = 2 + EXPECT_BOUNDS_EQ(vars[1], -3, -2); // coeff = 2, slack = 1 + EXPECT_BOUNDS_EQ(vars[2], 3, 3); // coeff = 3, slack = 0 +} + +TEST(WeightedSumLowerOrEqualTest, UnaryRounding) { + Model model; + IntegerVariable var = model.Add(NewIntegerVariable(0, 10)); + const std::vector coeffs = {-100}; + model.Add(WeightedSumLowerOrEqual({var}, coeffs, -320)); + EXPECT_EQ(SatSolver::FEASIBLE, model.GetOrCreate()->Solve()); + EXPECT_EQ(model.Get(LowerBound(var)), 4); +} + +// This one used to fail before CL 139204507. +TEST(WeightedSumTest, LevelZeroPropagationWithNegativeNumbers) { + Model model; + std::vector vars{model.Add(NewIntegerVariable(-5, 0)), + model.Add(NewIntegerVariable(-6, 0)), + model.Add(NewIntegerVariable(-4, 0))}; + + const IntegerVariable sum = + model.Add(NewWeightedSum(std::vector{3, 3, 3}, vars)); + EXPECT_EQ(SatSolver::FEASIBLE, model.GetOrCreate()->Solve()); + EXPECT_EQ(model.Get(LowerBound(sum)), -15 * 3); + EXPECT_EQ(model.Get(UpperBound(sum)), 0); + + // Setting this leave only a slack of 5 which is not an exact multiple of 3. + model.Add(LowerOrEqual(sum, -40)); + EXPECT_EQ(SatSolver::FEASIBLE, model.GetOrCreate()->Solve()); + EXPECT_BOUNDS_EQ(vars[0], -5, -4); + EXPECT_BOUNDS_EQ(vars[1], -6, -5); + EXPECT_BOUNDS_EQ(vars[2], -4, -3); +} + +TEST(ReifiedWeightedSumLeTest, ReifToBoundPropagation) { + Model model; + const Literal r = Literal(model.Add(NewBooleanVariable()), true); + const IntegerVariable var = model.Add(NewIntegerVariable(4, 9)); + AddWeightedSumLowerOrEqualReif(r, {var}, std::vector{1}, 6, &model); + EXPECT_EQ( + SatSolver::FEASIBLE, + model.GetOrCreate()->ResetAndSolveWithGivenAssumptions({r})); + EXPECT_BOUNDS_EQ(var, 4, 6); + EXPECT_EQ(SatSolver::FEASIBLE, + model.GetOrCreate()->ResetAndSolveWithGivenAssumptions( + {r.Negated()})); + EXPECT_BOUNDS_EQ(var, 7, 9); // The associated literal (x <= 6) is false. +} + +TEST(ReifiedWeightedSumLeTest, ReifToBoundPropagationWithNegatedCoeff) { + Model model; + const Literal r = Literal(model.Add(NewBooleanVariable()), true); + const IntegerVariable var = model.Add(NewIntegerVariable(-9, 9)); + AddWeightedSumLowerOrEqualReif(r, {var}, std::vector{-3}, 7, &model); + EXPECT_EQ( + SatSolver::FEASIBLE, + model.GetOrCreate()->ResetAndSolveWithGivenAssumptions({r})); + EXPECT_BOUNDS_EQ(var, -2, 9); + EXPECT_EQ(SatSolver::FEASIBLE, + model.GetOrCreate()->ResetAndSolveWithGivenAssumptions( + {r.Negated()})); + EXPECT_BOUNDS_EQ(var, -9, -3); // The associated literal (x >= -2) is false. +} + +TEST(ReifiedWeightedSumGeTest, ReifToBoundPropagation) { + Model model; + const Literal r = Literal(model.Add(NewBooleanVariable()), true); + const IntegerVariable var = model.Add(NewIntegerVariable(4, 9)); + AddWeightedSumGreaterOrEqualReif(r, {var}, std::vector{1}, 6, + &model); + EXPECT_EQ( + SatSolver::FEASIBLE, + model.GetOrCreate()->ResetAndSolveWithGivenAssumptions({r})); + EXPECT_BOUNDS_EQ(var, 6, 9); + EXPECT_EQ(SatSolver::FEASIBLE, + model.GetOrCreate()->ResetAndSolveWithGivenAssumptions( + {r.Negated()})); + EXPECT_BOUNDS_EQ(var, 4, 5); +} + +TEST(ReifiedFixedWeightedSumTest, ReifToBoundPropagation) { + Model model; + const Literal r = Literal(model.Add(NewBooleanVariable()), true); + const IntegerVariable var = model.Add(NewIntegerVariable(4, 9)); + AddFixedWeightedSumReif(r, {var}, std::vector{1}, 6, &model); + EXPECT_EQ( + SatSolver::FEASIBLE, + model.GetOrCreate()->ResetAndSolveWithGivenAssumptions({r})); + EXPECT_BOUNDS_EQ(var, 6, 6); + + // Because we introduced intermediate Boolean, we decide if var is < 6 or > 6. + EXPECT_EQ(SatSolver::FEASIBLE, + model.GetOrCreate()->ResetAndSolveWithGivenAssumptions( + {r.Negated()})); + if (model.Get(LowerBound(var)) == 4) { + EXPECT_BOUNDS_EQ(var, 4, 5); + } else { + EXPECT_BOUNDS_EQ(var, 7, 9); + } +} + +TEST(ReifiedWeightedSumTest, BoundToReifTrueLe) { + Model model; + const Literal r = Literal(model.Add(NewBooleanVariable()), true); + const IntegerVariable var = model.Add(NewIntegerVariable(4, 9)); + AddWeightedSumLowerOrEqualReif(r, {var}, std::vector{1}, 9, &model); + EXPECT_TRUE(model.GetOrCreate()->Propagate()); + EXPECT_TRUE(model.Get(Value(r))); +} + +TEST(ReifiedWeightedSumTest, BoundToReifFalseLe) { + Model model; + const Literal r = Literal(model.Add(NewBooleanVariable()), true); + const IntegerVariable var = model.Add(NewIntegerVariable(4, 9)); + AddWeightedSumLowerOrEqualReif(r, {var}, std::vector{1}, 3, &model); + EXPECT_TRUE(model.GetOrCreate()->Propagate()); + EXPECT_FALSE(model.Get(Value(r))); +} + +TEST(ReifiedWeightedSumTest, BoundToReifTrueEq) { + Model model; + const Literal r = Literal(model.Add(NewBooleanVariable()), true); + const IntegerVariable var = model.Add(NewIntegerVariable(4, 4)); + AddFixedWeightedSumReif(r, {var}, std::vector{1}, 4, &model); + EXPECT_TRUE(model.GetOrCreate()->Propagate()); + EXPECT_TRUE(model.Get(Value(r))); +} + +TEST(ReifiedWeightedSumTest, BoundToReifFalseEq1) { + Model model; + const Literal r = Literal(model.Add(NewBooleanVariable()), true); + const IntegerVariable var = model.Add(NewIntegerVariable(4, 6)); + AddFixedWeightedSumReif(r, {var}, std::vector{1}, 8, &model); + EXPECT_TRUE(model.GetOrCreate()->Propagate()); + EXPECT_FALSE(model.Get(Value(r))); +} + +TEST(ReifiedWeightedSumTest, BoundToReifFalseEq2) { + Model model; + const Literal r = Literal(model.Add(NewBooleanVariable()), true); + const IntegerVariable var = model.Add(NewIntegerVariable(4, 6)); + AddFixedWeightedSumReif(r, {var}, std::vector{1}, 3, &model); + EXPECT_TRUE(model.GetOrCreate()->Propagate()); + EXPECT_FALSE(model.Get(Value(r))); +} + +TEST(ConditionalLbTest, BasicPositiveCase) { + Model model; + const IntegerVariable var = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable obj = model.Add(NewIntegerVariable(-10, 10)); + + std::vector vars{var, obj}; + std::vector coeffs{6, -2}; + const IntegerValue rhs = 4; + IntegerSumLE constraint({}, vars, coeffs, rhs, &model); + + // We have 2 * obj >= 6 * var - 4. + const auto result = + constraint.ConditionalLb(IntegerLiteral::GreaterOrEqual(var, 1), obj); + EXPECT_EQ(result.first, -2); // When false. + EXPECT_EQ(result.second, 1); // When true. + + // We have 2 * obj >= 6 * var - 4. + const auto result2 = + constraint.ConditionalLb(IntegerLiteral::GreaterOrEqual(var, 3), obj); + EXPECT_EQ(result2.first, -2); // When false. + EXPECT_EQ(result2.second, 7); // When true. +} + +TEST(ConditionalLbTest, CornerCase) { + Model model; + const IntegerVariable var = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable obj = model.Add(NewIntegerVariable(-10, 10)); + + std::vector vars{var, obj}; + std::vector coeffs{6, -2}; + const IntegerValue rhs = 4; + IntegerSumLE constraint({}, vars, coeffs, rhs, &model); + + // Here we don't even look at the equation. + const auto result = + constraint.ConditionalLb(IntegerLiteral::GreaterOrEqual(obj, 2), obj); + EXPECT_EQ(result.first, kMinIntegerValue); // When false. + EXPECT_EQ(result.second, 2); // When true. + + const auto result2 = + constraint.ConditionalLb(IntegerLiteral::LowerOrEqual(obj, 3), obj); + EXPECT_EQ(result2.first, 4); // When false. + EXPECT_EQ(result2.second, kMinIntegerValue); // When true. +} + +TEST(ConditionalLbTest, BasicNegativeCase) { + Model model; + const IntegerVariable var = model.Add(NewIntegerVariable(0, 1)); + const IntegerVariable obj = model.Add(NewIntegerVariable(-10, 10)); + + std::vector vars{var, obj}; + std::vector coeffs{-6, -1}; + const IntegerValue rhs = -4; + IntegerSumLE constraint({}, vars, coeffs, rhs, &model); + + // We have obj >= 4 - 6 * var. + const auto result = + constraint.ConditionalLb(IntegerLiteral::LowerOrEqual(var, 0), obj); + EXPECT_EQ(result.first, -2); // false, var <= 1 + EXPECT_EQ(result.second, 4); // true, var <= 0. +} + +TEST(MinMaxTest, LevelZeroPropagation) { + Model model; + std::vector vars{model.Add(NewIntegerVariable(4, 9)), + model.Add(NewIntegerVariable(2, 7)), + model.Add(NewIntegerVariable(3, 8))}; + const IntegerVariable min = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable max = model.Add(NewIntegerVariable(0, 10)); + model.Add(IsEqualToMinOf(min, vars)); + model.Add(IsEqualToMaxOf(max, vars)); + + EXPECT_EQ(SatSolver::FEASIBLE, model.GetOrCreate()->Solve()); + EXPECT_BOUNDS_EQ(min, 2, 7); + EXPECT_BOUNDS_EQ(max, 4, 9); + + model.Add(LowerOrEqual(min, 5)); + EXPECT_EQ(SatSolver::FEASIBLE, model.GetOrCreate()->Solve()); + EXPECT_BOUNDS_EQ(min, 2, 5); + + model.Add(GreaterOrEqual(max, 7)); + EXPECT_EQ(SatSolver::FEASIBLE, model.GetOrCreate()->Solve()); + EXPECT_BOUNDS_EQ(max, 7, 9); + + // Test the propagation in the other direction (PrecedencesPropagator). + model.Add(GreaterOrEqual(min, 5)); + EXPECT_EQ(SatSolver::FEASIBLE, model.GetOrCreate()->Solve()); + EXPECT_BOUNDS_EQ(vars[0], 5, 9); + EXPECT_BOUNDS_EQ(vars[1], 5, 7); + EXPECT_BOUNDS_EQ(vars[2], 5, 8); + + model.Add(LowerOrEqual(max, 8)); + EXPECT_EQ(SatSolver::FEASIBLE, model.GetOrCreate()->Solve()); + EXPECT_BOUNDS_EQ(vars[0], 5, 8); + EXPECT_BOUNDS_EQ(vars[1], 5, 7); + EXPECT_BOUNDS_EQ(vars[2], 5, 8); +} + +TEST(LinMinMaxTest, LevelZeroPropagation) { + Model model; + std::vector vars{model.Add(NewIntegerVariable(4, 9)), + model.Add(NewIntegerVariable(2, 7)), + model.Add(NewIntegerVariable(3, 8))}; + std::vector exprs; + for (const IntegerVariable var : vars) { + LinearExpression expr; + expr.vars.push_back(var); + expr.coeffs.push_back(1); + exprs.push_back(expr); + } + const IntegerVariable min = model.Add(NewIntegerVariable(-100, 100)); + LinearExpression min_expr; + min_expr.vars.push_back(min); + min_expr.coeffs.push_back(1); + model.Add(IsEqualToMinOf(min_expr, exprs)); + + EXPECT_EQ(SatSolver::FEASIBLE, model.GetOrCreate()->Solve()); + EXPECT_BOUNDS_EQ(min, 2, 7); + + model.Add(LowerOrEqual(min, 5)); + EXPECT_EQ(SatSolver::FEASIBLE, model.GetOrCreate()->Solve()); + EXPECT_BOUNDS_EQ(min, 2, 5); + + // Test the propagation in the other direction (PrecedencesPropagator). + model.Add(GreaterOrEqual(min, 5)); + EXPECT_EQ(SatSolver::FEASIBLE, model.GetOrCreate()->Solve()); + EXPECT_BOUNDS_EQ(vars[0], 5, 9); + EXPECT_BOUNDS_EQ(vars[1], 5, 7); + EXPECT_BOUNDS_EQ(vars[2], 5, 8); +} + +TEST(MinTest, OnlyOnePossibleCandidate) { + Model model; + std::vector vars{model.Add(NewIntegerVariable(4, 7)), + model.Add(NewIntegerVariable(2, 9)), + model.Add(NewIntegerVariable(5, 8))}; + const IntegerVariable min = model.Add(NewIntegerVariable(0, 10)); + model.Add(IsEqualToMinOf(min, vars)); + + // So far everything is normal. + EXPECT_EQ(SatSolver::FEASIBLE, model.GetOrCreate()->Solve()); + EXPECT_BOUNDS_EQ(min, 2, 7); + + // But now, if the min is known to be <= 3, the minimum variable is known! it + // has to be variable #1, so we can propagate its upper bound. + model.Add(LowerOrEqual(min, 3)); + EXPECT_EQ(SatSolver::FEASIBLE, model.GetOrCreate()->Solve()); + EXPECT_BOUNDS_EQ(min, 2, 3); + EXPECT_BOUNDS_EQ(vars[1], 2, 3); + + // Test infeasibility. + model.Add(LowerOrEqual(min, 1)); + EXPECT_EQ(SatSolver::INFEASIBLE, model.GetOrCreate()->Solve()); +} + +TEST(LinMinTest, OnlyOnePossibleCandidate) { + Model model; + std::vector vars{model.Add(NewIntegerVariable(4, 7)), + model.Add(NewIntegerVariable(2, 9)), + model.Add(NewIntegerVariable(5, 8))}; + std::vector exprs; + for (const IntegerVariable var : vars) { + LinearExpression expr; + expr.vars.push_back(var); + expr.coeffs.push_back(1); + exprs.push_back(expr); + } + const IntegerVariable min = model.Add(NewIntegerVariable(-100, 100)); + LinearExpression min_expr; + min_expr.vars.push_back(min); + min_expr.coeffs.push_back(1); + model.Add(IsEqualToMinOf(min_expr, exprs)); + + // So far everything is normal. + EXPECT_EQ(SatSolver::FEASIBLE, model.GetOrCreate()->Solve()); + EXPECT_BOUNDS_EQ(min, 2, 7); + + // But now, if the min is known to be <= 3, the minimum variable is known! it + // has to be variable #1, so we can propagate its upper bound. + model.Add(LowerOrEqual(min, 3)); + EXPECT_EQ(SatSolver::FEASIBLE, model.GetOrCreate()->Solve()); + EXPECT_BOUNDS_EQ(min, 2, 3); + EXPECT_BOUNDS_EQ(vars[1], 2, 3); + + // Test infeasibility. + model.Add(LowerOrEqual(min, 1)); + EXPECT_EQ(SatSolver::INFEASIBLE, model.GetOrCreate()->Solve()); +} + +TEST(LinMinTest, OnlyOnePossibleExpr) { + Model model; + std::vector vars{model.Add(NewIntegerVariable(1, 2)), + model.Add(NewIntegerVariable(0, 3)), + model.Add(NewIntegerVariable(-2, 4))}; + std::vector exprs; + IntegerTrail* integer_trail = model.GetOrCreate(); + LinearExpression expr1; // 2x0 + 3x1 - 5 + expr1.vars = {vars[0], vars[1]}; + expr1.coeffs = {2, 3}; + expr1.offset = -5; + expr1 = CanonicalizeExpr(expr1); + EXPECT_EQ(-3, expr1.Min(*integer_trail)); + EXPECT_EQ(8, expr1.Max(*integer_trail)); + + LinearExpression expr2; // 2x1 - 5x2 + 6 + expr2.vars = {vars[1], vars[2]}; + expr2.coeffs = {2, -5}; + expr2.offset = 6; + expr2 = CanonicalizeExpr(expr2); + EXPECT_EQ(-14, expr2.Min(*integer_trail)); + EXPECT_EQ(22, expr2.Max(*integer_trail)); + + LinearExpression expr3; // 2x0 + 3x2 + expr3.vars = {vars[0], vars[2]}; + expr3.coeffs = {2, 3}; + expr3 = CanonicalizeExpr(expr3); + EXPECT_EQ(-4, expr3.Min(*integer_trail)); + EXPECT_EQ(16, expr3.Max(*integer_trail)); + + exprs.push_back(expr1); + exprs.push_back(expr2); + exprs.push_back(expr3); + IntegerVariable min = model.Add(NewIntegerVariable(-100, 100)); + LinearExpression min_expr; + min_expr.vars.push_back(min); + min_expr.coeffs.push_back(1); + model.Add(IsEqualToMinOf(min_expr, exprs)); + + // So far everything is normal. + EXPECT_EQ(SatSolver::FEASIBLE, model.GetOrCreate()->Solve()); + EXPECT_BOUNDS_EQ(min, -14, 8); + + // But now, if the min is known to be <= -5, the minimum expression has to be + // expr 2, so we can propagate its upper bound. + model.Add(LowerOrEqual(min, -5)); + EXPECT_EQ(SatSolver::FEASIBLE, model.GetOrCreate()->Solve()); + EXPECT_BOUNDS_EQ(min, -14, -5); + EXPECT_BOUNDS_EQ(vars[0], 1, 2); + EXPECT_BOUNDS_EQ(vars[1], 0, 3); + EXPECT_BOUNDS_EQ(vars[2], 3, 4); + // NOTE: The expression bound is not as tight because the underlying variable + // bounds can't be propagated enough without throwing away valid solutions. + EXPECT_LE(expr2.Max(*integer_trail), -3); + + // Test infeasibility. + model.Add(LowerOrEqual(min, -15)); + EXPECT_EQ(SatSolver::INFEASIBLE, model.GetOrCreate()->Solve()); +} + +TEST(OneOfTest, BasicPropagation) { + Model model; + + IntegerVariable var = model.Add(NewIntegerVariable(0, 10)); + std::vector selectors; + for (int i = 0; i < 5; ++i) { + selectors.push_back(Literal(model.Add(NewBooleanVariable()), true)); + } + std::vector values{5, 0, 3, 3, 9}; + + model.Add(IsOneOf(var, selectors, values)); + + // We start with nothing fixed and then start fixing variables. + SatSolver* solver = model.GetOrCreate(); + EXPECT_TRUE(solver->Propagate()); + EXPECT_BOUNDS_EQ(var, 0, 9); + EXPECT_TRUE(solver->EnqueueDecisionIfNotConflicting(selectors[1].Negated())); + EXPECT_BOUNDS_EQ(var, 3, 9); + EXPECT_TRUE(solver->EnqueueDecisionIfNotConflicting(selectors[4].Negated())); + EXPECT_BOUNDS_EQ(var, 3, 5); + EXPECT_TRUE(solver->EnqueueDecisionIfNotConflicting(selectors[2].Negated())); + EXPECT_BOUNDS_EQ(var, 3, 5); + EXPECT_TRUE(solver->EnqueueDecisionIfNotConflicting(selectors[3].Negated())); + EXPECT_BOUNDS_EQ(var, 5, 5); + + // Now we restrict the possible values by changing the bound. + solver->Backtrack(0); + model.Add(LowerOrEqual(var, 3)); + EXPECT_EQ( + SatSolver::FEASIBLE, + model.GetOrCreate()->ResetAndSolveWithGivenAssumptions({})); + EXPECT_FALSE(model.Get(Value(selectors[0]))); + EXPECT_FALSE(model.Get(Value(selectors[4]))); +} + +// Propagates a * b = p by hand. Return false if the domains are empty, +// otherwise returns true and the expected domains value. This is slow and +// work in O(product of domain(a).size() * domain(b).size())!. +bool TestProductPropagation(const IntegerTrail& trail, + std::vector vars, + std::vector* expected_mins, + std::vector* expected_maxs) { + const IntegerValue min_a = trail.LowerBound(vars[0]); + const IntegerValue max_a = trail.UpperBound(vars[0]); + const IntegerValue min_b = trail.LowerBound(vars[1]); + const IntegerValue max_b = trail.UpperBound(vars[1]); + const IntegerValue min_p = trail.LowerBound(vars[2]); + const IntegerValue max_p = trail.UpperBound(vars[2]); + + std::vector> new_values(3); + for (IntegerValue va(min_a); va <= max_a; ++va) { + for (IntegerValue vb(min_b); vb <= max_b; ++vb) { + const IntegerValue vp = va * vb; + if (vp >= min_p && vp <= max_p) { + new_values[0].insert(va); + new_values[1].insert(vb); + new_values[2].insert(vp); + } + } + } + if (new_values[0].empty() || new_values[1].empty() || new_values[2].empty()) { + return false; + } + + expected_mins->clear(); + expected_maxs->clear(); + for (int i = 0; i < 3; ++i) { + std::vector sorted(new_values[i].begin(), + new_values[i].end()); + expected_mins->push_back(sorted.front()); + expected_maxs->push_back(sorted.back()); + } + return true; +} + +TEST(ProductConstraintTest, RandomCases) { + absl::BitGen random; + + int num_non_perfect = 0; + const int num_tests = 1000; + for (int i = 0; i < num_tests; ++i) { + Model model; + IntegerTrail* integer_trail = model.GetOrCreate(); + std::vector vars; + std::string input_string; + for (int v = 0; v < 3; ++v) { + const int limit = v < 2 ? 20 : 200; + int64_t min = absl::Uniform(random, -limit, limit); + int64_t max = absl::Uniform(random, -limit, limit); + if (min > max) std::swap(min, max); + absl::StrAppend(&input_string, + (v == 1 ? " * " + : v == 2 ? " = " + : ""), + "[", min, ", ", max, "]"); + vars.push_back(model.Add(NewIntegerVariable(min, max))); + } + + // Start by computing the expected result. + std::vector expected_mins; + std::vector expected_maxs; + const bool expected_result = TestProductPropagation( + *integer_trail, vars, &expected_mins, &expected_maxs); + + bool perfect_propagation = true; + bool ok_propagation = true; + model.Add(ProductConstraint(vars[0], vars[1], vars[2])); + const bool result = model.GetOrCreate()->Propagate(); + if (expected_result != result) { + if (expected_result) { + ok_propagation = false; + } else { + // If the exact result is UNSAT, we might not have seen that. + perfect_propagation = false; + } + } + std::string expected_string; + std::string result_string; + for (int i = 0; i < 3; ++i) { + const int64_t lb = integer_trail->LowerBound(vars[i]).value(); + const int64_t ub = integer_trail->UpperBound(vars[i]).value(); + if (expected_result) { + if (expected_mins[i] != lb) perfect_propagation = false; + if (expected_mins[i] < lb) ok_propagation = false; + if (expected_maxs[i] != ub) perfect_propagation = false; + if (expected_maxs[i] > ub) ok_propagation = false; + + // We should always be exact on the domain of a and b. + if (i < 2 && !perfect_propagation) { + ok_propagation = false; + } + absl::StrAppend(&expected_string, "[", expected_mins[i].value(), ", ", + expected_maxs[i].value(), "] "); + } + + if (result) { + absl::StrAppend(&result_string, "[", lb, ", ", ub, "] "); + } + } + if (!perfect_propagation || !ok_propagation) { + VLOG(1) << "Imperfect on input: " << input_string; + if (expected_result) { + VLOG(1) << "Expected: " << expected_string; + if (result) { + VLOG(1) << "Result: " << result_string; + } else { + VLOG(1) << "UNSAT was received."; + } + } else { + VLOG(1) << "Result: " << result_string; + VLOG(1) << "UNSAT was expected."; + } + ++num_non_perfect; + } + ASSERT_TRUE(ok_propagation); + } + + // Unfortunately our TestProductPropagation() is too good and in some corner + // cases like when the product is [18, 19] it can detect stuff like the + // product 19 (which is prime) can't be reached by any product a * b, + // whereas our propagator doesn't see that! + LOG(INFO) << "Num imperfect: " << num_non_perfect << " / " << num_tests; + EXPECT_LT(num_non_perfect, num_tests / 2); +} + +TEST(ProductConstraintTest, RestrictedProductDomainPosPos) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { name: 'y' domain: 0 domain: 3 } + variables { name: 'x' domain: 0 domain: 2 } + variables { name: 'p' domain: 0 domain: 4 } + constraints { + int_prod { + target { vars: 2 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + absl::btree_set> expected{ + {0, 0, 0}, {0, 1, 0}, {0, 2, 0}, {1, 0, 0}, {1, 1, 1}, {1, 2, 2}, + {2, 0, 0}, {2, 1, 2}, {2, 2, 4}, {3, 0, 0}, {3, 1, 3}, + }; + EXPECT_EQ(solutions, expected); +} + +TEST(ProductConstraintTest, RestrictedProductDomainPosNeg) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { name: 'y' domain: 0 domain: 3 } + variables { name: 'x' domain: -2 domain: 0 } + variables { name: 'p' domain: -4 domain: 0 } + constraints { + int_prod { + target { vars: 2 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + absl::btree_set> expected{ + {0, 0, 0}, {0, -1, 0}, {0, -2, 0}, {1, 0, 0}, {1, -1, -1}, {1, -2, -2}, + {2, 0, 0}, {2, -1, -2}, {2, -2, -4}, {3, 0, 0}, {3, -1, -3}, + }; + EXPECT_EQ(solutions, expected); +} + +TEST(ProductConstraintTest, RestrictedProductDomainNegPos) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { name: 'y' domain: -3 domain: 0 } + variables { name: 'x' domain: 0 domain: 2 } + variables { name: 'p' domain: -4 domain: 0 } + constraints { + int_prod { + target { vars: 2 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + absl::btree_set> expected{ + {0, 0, 0}, {0, 1, 0}, {0, 2, 0}, {-1, 0, 0}, + {-1, 1, -1}, {-1, 2, -2}, {-2, 0, 0}, {-2, 1, -2}, + {-2, 2, -4}, {-3, 0, 0}, {-3, 1, -3}, + }; + EXPECT_EQ(solutions, expected); +} + +TEST(ProductConstraintTest, RestrictedProductDomainNegNeg) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { name: 'y' domain: -3 domain: 0 } + variables { name: 'x' domain: -2 domain: 0 } + variables { name: 'p' domain: 0 domain: 4 } + constraints { + int_prod { + target { vars: 2 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + absl::btree_set> expected{ + {0, 0, 0}, {0, -1, 0}, {0, -2, 0}, {-1, 0, 0}, + {-1, -1, 1}, {-1, -2, 2}, {-2, 0, 0}, {-2, -1, 2}, + {-2, -2, 4}, {-3, 0, 0}, {-3, -1, 3}, + }; + EXPECT_EQ(solutions, expected); +} + +TEST(ProductConstraintTest, ProductIsNull) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { name: 'y' domain: 0 domain: 3 } + variables { name: 'x' domain: 0 domain: 2 } + variables { name: 'p' domain: 0 domain: 6 } + constraints { + int_prod { + target { vars: 2 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + constraints { linear { vars: 2 coeffs: 1 domain: 0 domain: 0 } } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + absl::btree_set> expected{{0, 0, 0}, {0, 1, 0}, {0, 2, 0}, + {1, 0, 0}, {2, 0, 0}, {3, 0, 0}}; + EXPECT_EQ(solutions, expected); +} + +TEST(ProductConstraintTest, CheckAllSolutionsRandomProblem) { + absl::BitGen random; + const int kMaxValue = 50; + const int kNumLoops = DEBUG_MODE ? 50 : 100; + + for (int loop = 0; loop < kNumLoops; ++loop) { + CpModelProto cp_model; + int x_min = absl::Uniform(random, -kMaxValue, kMaxValue); + int x_max = absl::Uniform(random, -kMaxValue, kMaxValue); + if (x_min > x_max) std::swap(x_min, x_max); + IntegerVariableProto* x = cp_model.add_variables(); + x->add_domain(x_min); + x->add_domain(x_max); + + int y_min = absl::Uniform(random, -kMaxValue, kMaxValue); + int y_max = absl::Uniform(random, -kMaxValue, kMaxValue); + if (y_min > y_max) std::swap(y_min, y_max); + IntegerVariableProto* y = cp_model.add_variables(); + y->add_domain(y_min); + y->add_domain(y_max); + + int z_min = absl::Uniform(random, -kMaxValue, kMaxValue); + int z_max = absl::Uniform(random, -kMaxValue, kMaxValue); + if (z_min > z_max) std::swap(z_min, z_max); + IntegerVariableProto* z = cp_model.add_variables(); + z->add_domain(z_min); + z->add_domain(z_max); + + // z == x * y. + LinearArgumentProto* prod = cp_model.add_constraints()->mutable_int_prod(); + prod->add_exprs()->add_vars(0); // x. + prod->mutable_exprs(0)->add_coeffs(1); + prod->add_exprs()->add_vars(1); // y + prod->mutable_exprs(1)->add_coeffs(1); + prod->mutable_target()->add_vars(2); // z + prod->mutable_target()->add_coeffs(1); + + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(cp_model, "linearization_level:0", &solutions); + + // Loop through the domains of x and y, and collect valid solutions. + absl::btree_set> expected; + for (int i = x_min; i <= x_max; ++i) { + for (int j = y_min; j <= y_max; ++j) { + const int k = i * j; + if (k < z_min || k > z_max) continue; + expected.insert({i, j, k}); + } + } + + // Checks that we get the same solution set through the two methods. + EXPECT_EQ(solutions, expected); + } +} + +TEST(ProductPropagationTest, RightAcrossZero) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { name: 'y' domain: 2 domain: 4 } + variables { name: 'x' domain: -6 domain: 6 } + variables { name: 'p' domain: -30 domain: 30 } + constraints { + int_prod { + target { vars: 2 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + absl::btree_set> expected{ + {2, -6, -12}, {3, -6, -18}, {4, -6, -24}, {2, -5, -10}, {3, -5, -15}, + {4, -5, -20}, {2, -4, -8}, {3, -4, -12}, {4, -4, -16}, {2, -3, -6}, + {3, -3, -9}, {4, -3, -12}, {2, -2, -4}, {3, -2, -6}, {4, -2, -8}, + {2, -1, -2}, {3, -1, -3}, {4, -1, -4}, {2, 0, 0}, {3, 0, 0}, + {4, 0, 0}, {2, 1, 2}, {3, 1, 3}, {4, 1, 4}, {2, 2, 4}, + {3, 2, 6}, {4, 2, 8}, {2, 3, 6}, {3, 3, 9}, {4, 3, 12}, + {2, 4, 8}, {3, 4, 12}, {4, 4, 16}, {2, 5, 10}, {3, 5, 15}, + {4, 5, 20}, {2, 6, 12}, {3, 6, 18}, {4, 6, 24}, + }; + EXPECT_EQ(solutions.size(), 3 * 13); + EXPECT_EQ(solutions, expected); +} + +TEST(ProductPropagationTest, BothAcrossZero) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { name: 'y' domain: -2 domain: 3 } + variables { name: 'x' domain: -3 domain: 2 } + variables { name: 'p' domain: -10 domain: 10 } + constraints { + int_prod { + target { vars: 2 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + absl::btree_set> expected{ + {-2, -3, 6}, {-2, -2, 4}, {-2, -1, 2}, {-2, 0, 0}, {-2, 1, -2}, + {-2, 2, -4}, {-1, -3, 3}, {-1, -2, 2}, {-1, -1, 1}, {-1, 0, 0}, + {-1, 1, -1}, {-1, 2, -2}, {0, -3, 0}, {0, -2, 0}, {0, -1, 0}, + {0, 0, 0}, {0, 1, 0}, {0, 2, 0}, {1, -3, -3}, {1, -2, -2}, + {1, -1, -1}, {1, 0, 0}, {1, 1, 1}, {1, 2, 2}, {2, -3, -6}, + {2, -2, -4}, {2, -1, -2}, {2, 0, 0}, {2, 1, 2}, {2, 2, 4}, + {3, -3, -9}, {3, -2, -6}, {3, -1, -3}, {3, 0, 0}, {3, 1, 3}, + {3, 2, 6}}; + EXPECT_EQ(solutions.size(), 6 * 6); + EXPECT_EQ(solutions, expected); +} + +TEST(ProductPropagationTest, BothAcrossZeroWithRangeRestriction) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { name: 'y' domain: -2 domain: 3 } + variables { name: 'x' domain: -3 domain: 2 } + variables { name: 'p' domain: -3 domain: 4 } + constraints { + int_prod { + target { vars: 2 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + absl::btree_set> expected{ + {-2, -2, 4}, {-2, -1, 2}, {-2, 0, 0}, {-2, 1, -2}, {-1, -3, 3}, + {-1, -2, 2}, {-1, -1, 1}, {-1, 0, 0}, {-1, 1, -1}, {-1, 2, -2}, + {0, -3, 0}, {0, -2, 0}, {0, -1, 0}, {0, 0, 0}, {0, 1, 0}, + {0, 2, 0}, {1, -3, -3}, {1, -2, -2}, {1, -1, -1}, {1, 0, 0}, + {1, 1, 1}, {1, 2, 2}, {2, -1, -2}, {2, 0, 0}, {2, 1, 2}, + {2, 2, 4}, {3, -1, -3}, {3, 0, 0}, {3, 1, 3}, + }; + EXPECT_EQ(solutions, expected); +} + +TEST(ProductPropagationTest, BothAcrossZeroWithPositiveTarget) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -2, 6 ] } + variables { domain: [ -2, 6 ] } + variables { domain: [ 12, 12 ] } + constraints { + int_prod { + target { vars: 2 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + absl::btree_set> expected{ + {2, 6, 12}, {3, 4, 12}, {4, 3, 12}, {6, 2, 12}}; + EXPECT_EQ(solutions, expected); +} + +TEST(ProductPropagationTest, BothAcrossZeroWithFarPositiveTarget) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -2, 6 ] } + variables { domain: [ -2, 6 ] } + variables { domain: [ 15, 15 ] } + constraints { + int_prod { + target { vars: 2 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + absl::btree_set> expected{{3, 5, 15}, {5, 3, 15}}; + EXPECT_EQ(solutions, expected); +} + +TEST(ProductPropagationTest, BothAcrossZeroWithNegativeTarget) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -2, 6 ] } + variables { domain: [ -2, 6 ] } + variables { domain: [ -12, -12 ] } + constraints { + int_prod { + target { vars: 2 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + absl::btree_set> expected{{-2, 6, -12}, {6, -2, -12}}; + EXPECT_EQ(solutions, expected); +} + +TEST(ProductPropagationTest, LargePositiveDomain) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: 0 domain: 3000000000 } + variables { domain: 0 domain: 3000000000 } + variables { domain: [ -30, -15, 15, 30 ] } + constraints { + int_prod { + target { vars: 2 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + const Domain dp = ReadDomainFromProto(initial_model.variables(2)); + absl::btree_set> expected; + for (int vx = 0; vx <= 30; ++vx) { + for (int vy = 0; vy <= 30; ++vy) { + if (dp.Contains(vx * vy)) { + expected.insert(std::vector{vx, vy, vx * vy}); + } + } + } + EXPECT_EQ(solutions, expected); +} + +TEST(ProductPropagationTest, LargeDomain) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: -30 domain: 3000000000 } + variables { domain: -30 domain: 3000000000 } + variables { domain: [ -30, -15, 15, 30 ] } + constraints { + int_prod { + target { vars: 2 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + const Domain dp = ReadDomainFromProto(initial_model.variables(2)); + absl::btree_set> expected; + for (int vx = -30; vx <= 30; ++vx) { + for (int vy = -30; vy <= 30; ++vy) { + if (dp.Contains(vx * vy)) { + expected.insert(std::vector{vx, vy, vx * vy}); + } + } + } + EXPECT_EQ(solutions, expected); +} + +TEST(DivisionConstraintTest, CheckAllSolutions) { + absl::BitGen random; + const int kMaxValue = 100; + const int kShift = 10; + const int kNumLoops = DEBUG_MODE ? 100 : 1000; + + for (int loop = 0; loop < kNumLoops; ++loop) { + // Generate domains for x, y, and z. + // z is meant to be roughly compatible with x / y. There can still be no + // feasible solutions. + CpModelProto cp_model; + const int x_min = absl::Uniform(random, -kMaxValue, kMaxValue); + const int x_max = absl::Uniform(random, x_min, kMaxValue); + IntegerVariableProto* x = cp_model.add_variables(); + x->add_domain(x_min); + x->add_domain(x_max); + + const int y_min = absl::Uniform(random, 1, kMaxValue); + const int y_max = absl::Uniform(random, y_min, kMaxValue); + IntegerVariableProto* y = cp_model.add_variables(); + y->add_domain(y_min); + y->add_domain(y_max); + + const int z_min = std::max( + x_min / y_max + absl::Uniform(random, -kShift, kShift), 0); + const int z_max = std::max( + z_min, x_max / y_min + absl::Uniform(random, -kShift, kShift)); + IntegerVariableProto* z = cp_model.add_variables(); + z->add_domain(z_min); + z->add_domain(z_max); + + // z == x / y. + LinearArgumentProto* div = cp_model.add_constraints()->mutable_int_div(); + div->add_exprs()->add_vars(0); // x. + div->mutable_exprs(0)->add_coeffs(1); + div->add_exprs()->add_vars(1); // y + div->mutable_exprs(1)->add_coeffs(1); + div->mutable_target()->add_vars(2); // z + div->mutable_target()->add_coeffs(1); + + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(cp_model, "linearization_level:0", &solutions); + + // Loop through the domains of x and y, and collect valid solutions. + absl::btree_set> expected; + for (int i = x_min; i <= x_max; ++i) { + for (int j = y_min; j <= y_max; ++j) { + const int k = i / j; + if (k < z_min || k > z_max) continue; + expected.insert({i, j, k}); + } + } + + // Checks that we get the same solution set through the two methods. + EXPECT_EQ(solutions, expected) + << "x = [" << x_min << ".." << x_max << "], y = [" << y_min << ".." + << y_max << "], z = [" << z_min << ".." << z_max << "]\n---------\n" + << ProtobufDebugString(cp_model) << "---------\n"; + } +} + +TEST(DivisionConstraintTest, NumeratorAcrossZeroPositiveDenom) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -2, 6 ] } + variables { domain: [ 2, 4 ] } + variables { domain: [ -1, 3 ] } + constraints { + int_div { + target { vars: 2 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "linearization_level:0", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + absl::btree_set> expected{ + {-2, 2, -1}, {-2, 3, 0}, {-2, 4, 0}, {-1, 2, 0}, {-1, 3, 0}, {-1, 4, 0}, + {0, 2, 0}, {0, 3, 0}, {0, 4, 0}, {1, 2, 0}, {1, 3, 0}, {1, 4, 0}, + {2, 2, 1}, {2, 3, 0}, {2, 4, 0}, {3, 2, 1}, {3, 3, 1}, {3, 4, 0}, + {4, 2, 2}, {4, 3, 1}, {4, 4, 1}, {5, 2, 2}, {5, 3, 1}, {5, 4, 1}, + {6, 2, 3}, {6, 3, 2}, {6, 4, 1}}; + EXPECT_EQ(solutions, expected); +} + +TEST(DivisionConstraintTest, NumeratorAcrossZeroNegativeDenom) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ -2, 6 ] } + variables { domain: [ -4, -2 ] } + variables { domain: [ -3, 1 ] } + constraints { + int_div { + target { vars: 2 coeffs: 1 } + exprs { vars: 0 coeffs: 1 } + exprs { vars: 1 coeffs: 1 } + } + } + )pb"); + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "linearization_level:0", &solutions); + EXPECT_EQ(OPTIMAL, response.status()); + absl::btree_set> expected{ + {-2, -4, 0}, {-2, -3, 0}, {-2, -2, 1}, {-1, -4, 0}, {-1, -3, 0}, + {-1, -2, 0}, {0, -4, 0}, {0, -3, 0}, {0, -2, 0}, {1, -4, 0}, + {1, -3, 0}, {1, -2, 0}, {2, -4, 0}, {2, -3, 0}, {2, -2, -1}, + {3, -4, 0}, {3, -3, -1}, {3, -2, -1}, {4, -4, -1}, {4, -3, -1}, + {4, -2, -2}, {5, -4, -1}, {5, -3, -1}, {5, -2, -2}, {6, -4, -1}, + {6, -3, -2}, {6, -2, -3}}; + EXPECT_EQ(solutions, expected); +} + +TEST(DivisionConstraintTest, CheckAllPropagationsRandomProblem) { + absl::BitGen random; + const int kMaxValue = 50; + const int kMaxDenom = 10; + const int kNumLoops = DEBUG_MODE ? 5000 : 100000; + + for (int loop = 0; loop < kNumLoops; ++loop) { + // Generate domains for x, y, and z. + int x_min = absl::Uniform(random, -kMaxValue, kMaxValue); + int x_max = absl::Uniform(random, -kMaxValue, kMaxValue); + if (x_min > x_max) std::swap(x_min, x_max); + int y_min = absl::Uniform(random, 1, kMaxDenom); + int y_max = absl::Uniform(random, 1, kMaxDenom); + if (y_min > y_max) std::swap(y_min, y_max); + int z_min = absl::Uniform(random, -kMaxValue, kMaxValue); + int z_max = absl::Uniform(random, -kMaxValue, kMaxValue); + if (z_min > z_max) std::swap(z_min, z_max); + + // Loop through the domains of x and y, and collect valid bounds. + int expected_x_min = std::numeric_limits::max(); + int expected_x_max = std::numeric_limits::min(); + int expected_y_min = std::numeric_limits::max(); + int expected_y_max = std::numeric_limits::min(); + int expected_z_min = std::numeric_limits::max(); + int expected_z_max = std::numeric_limits::min(); + for (int i = x_min; i <= x_max; ++i) { + for (int j = y_min; j <= y_max; ++j) { + const int k = i / j; + if (k < z_min || k > z_max) continue; + expected_x_min = std::min(expected_x_min, i); + expected_x_max = std::max(expected_x_max, i); + expected_y_min = std::min(expected_y_min, j); + expected_y_max = std::max(expected_y_max, j); + expected_z_min = std::min(expected_z_min, k); + expected_z_max = std::max(expected_z_max, k); + } + } + + Model model; + const IntegerVariable var_x = model.Add(NewIntegerVariable(x_min, x_max)); + const IntegerVariable var_y = model.Add(NewIntegerVariable(y_min, y_max)); + const IntegerVariable var_z = model.Add(NewIntegerVariable(z_min, z_max)); + model.Add(DivisionConstraint(var_x, var_y, var_z)); + const bool result = model.GetOrCreate()->Propagate(); + if (result) { + EXPECT_BOUNDS_EQ(var_x, expected_x_min, expected_x_max); + EXPECT_BOUNDS_EQ(var_y, expected_y_min, expected_y_max); + EXPECT_BOUNDS_EQ(var_z, expected_z_min, expected_z_max); + } else { + EXPECT_EQ(expected_x_max, std::numeric_limits::min()); + } + } +} + +TEST(DivisionConstraintTest, CheckAllSolutionsOnExprs) { + absl::BitGen random; + const int kMaxValue = 30; + const int kMaxCoeff = 5; + const int kMaxOffset = 10; + const int kNumLoops = DEBUG_MODE ? 100 : 10000; + + for (int loop = 0; loop < kNumLoops; ++loop) { + CpModelProto initial_model; + + // Create the numerator. + int num_var_min = absl::Uniform(random, -kMaxValue, kMaxValue); + int num_var_max = absl::Uniform(random, -kMaxValue, kMaxValue); + if (num_var_min > num_var_max) std::swap(num_var_min, num_var_max); + IntegerVariableProto* num_var_proto = initial_model.add_variables(); + num_var_proto->add_domain(num_var_min); + num_var_proto->add_domain(num_var_max); + const int64_t num_coeff = absl::Uniform(random, 1, kMaxCoeff) * + (absl::Bernoulli(random, 0.5) ? 1 : -1); + const int64_t num_offset = absl::Uniform(random, -kMaxOffset, kMaxOffset); + + // Create the denominator. Make sure 0 is not accessible. + int denom_var_min = absl::Uniform(random, -kMaxValue, kMaxValue); + int denom_var_max = absl::Uniform(random, -kMaxValue, kMaxValue); + if (denom_var_min > denom_var_max) std::swap(denom_var_min, denom_var_max); + const int64_t denom_coeff = absl::Uniform(random, 1, kMaxCoeff) * + (absl::Bernoulli(random, 0.5) ? 1 : -1); + const int64_t denom_offset = absl::Uniform(random, -kMaxOffset, kMaxOffset); + if (denom_coeff == 0) continue; + Domain denom_var_domain = {denom_var_min, denom_var_max}; + const int64_t bad_value = -denom_offset / denom_coeff; + if (denom_var_domain.Contains(bad_value) && + bad_value * denom_coeff == -denom_offset) { + denom_var_domain = + denom_var_domain.IntersectionWith(Domain(bad_value).Complement()); + } + IntegerVariableProto* denom_var_proto = initial_model.add_variables(); + FillDomainInProto(denom_var_domain, denom_var_proto); + + int target_var_min = absl::Uniform(random, -kMaxValue, kMaxValue); + int target_var_max = absl::Uniform(random, -kMaxValue, kMaxValue); + if (target_var_min > target_var_max) + std::swap(target_var_min, target_var_max); + IntegerVariableProto* target_var_proto = initial_model.add_variables(); + target_var_proto->add_domain(target_var_min); + target_var_proto->add_domain(target_var_max); + const int64_t target_coeff = absl::Uniform(random, 1, kMaxCoeff) * + (absl::Bernoulli(random, 0.5) ? 1 : -1); + const int64_t target_offset = + absl::Uniform(random, -kMaxOffset, kMaxOffset); + + // target = num / denom. + LinearArgumentProto* div = + initial_model.add_constraints()->mutable_int_div(); + div->add_exprs()->add_vars(0); // num + div->mutable_exprs(0)->add_coeffs(num_coeff); + div->mutable_exprs(0)->set_offset(num_offset); + div->add_exprs()->add_vars(1); // denom + div->mutable_exprs(1)->add_coeffs(denom_coeff); + div->mutable_exprs(1)->set_offset(denom_offset); + div->mutable_target()->add_vars(2); // target + div->mutable_target()->add_coeffs(target_coeff); + div->mutable_target()->set_offset(target_offset); + + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "linearization_level:0", &solutions); + + // Loop through the domains of var and target, and collect valid solutions. + absl::btree_set> expected; + for (int i = num_var_min; i <= num_var_max; ++i) { + const int num_value = num_coeff * i + num_offset; + for (const int j : denom_var_domain.Values()) { + const int denom_value = denom_coeff * j + denom_offset; + if (denom_value == 0) continue; + const int target_expr_value = num_value / denom_value; + const int target_var_value = + (target_expr_value - target_offset) / target_coeff; + if (target_var_value >= target_var_min && + target_var_value <= target_var_max && + target_var_value * target_coeff + target_offset == + target_expr_value) { + expected.insert({i, j, target_var_value}); + } + } + } + + // Checks that we get we get the same solution set through the two methods. + EXPECT_EQ(solutions, expected) + << "\n---------\n" + << ProtobufDebugString(initial_model) << "---------\n"; + } +} + +void TestAllDivisionValues(int64_t min_a, int64_t max_a, int64_t b, + int64_t min_c, int64_t max_c) { + int64_t true_min_a = std::numeric_limits::max(); + int64_t true_max_a = std::numeric_limits::min(); + int64_t true_min_c = std::numeric_limits::max(); + int64_t true_max_c = std::numeric_limits::min(); + for (int64_t a = min_a; a <= max_a; ++a) { + for (int64_t c = min_c; c <= max_c; ++c) { + if (a / b == c) { + true_min_a = std::min(true_min_a, a); + true_max_a = std::max(true_max_a, a); + true_min_c = std::min(true_min_c, c); + true_max_c = std::max(true_max_c, c); + } + } + } + Model model; + const AffineExpression var_a = + min_a == max_a + ? AffineExpression(IntegerValue(min_a)) + : AffineExpression(model.Add(NewIntegerVariable(min_a, max_a))); + const AffineExpression var_c = + min_c == max_c + ? AffineExpression(IntegerValue(min_c)) + : AffineExpression(model.Add(NewIntegerVariable(min_c, max_c))); + model.Add(FixedDivisionConstraint(var_a, IntegerValue(b), var_c)); + const bool result = model.GetOrCreate()->Propagate(); + IntegerTrail* integer_trail = model.GetOrCreate(); + if (result) { + EXPECT_EQ(integer_trail->LowerBound(var_a), true_min_a); + EXPECT_EQ(integer_trail->UpperBound(var_a), true_max_a); + EXPECT_EQ(integer_trail->LowerBound(var_c), true_min_c); + EXPECT_EQ(integer_trail->UpperBound(var_c), true_max_c); + } else { + EXPECT_EQ(true_min_a, std::numeric_limits::max()); // No solution. + } +} + +TEST(FixedDivisionConstraintTest, AllSmallValues) { + for (int b = 1; b < 7; ++b) { + for (int min_a = -10; min_a <= 10; ++min_a) { + for (int max_a = min_a; max_a <= 10; ++max_a) { + TestAllDivisionValues(min_a, max_a, b, -20, 20); + } + } + for (int min_c = -10; min_c <= 10; ++min_c) { + for (int max_c = min_c; max_c <= 10; ++max_c) { + TestAllDivisionValues(-100, 100, b, min_c, max_c); + } + } + } +} + +bool PropagateFixedDivision(int64_t a, int64_t max_a, int64_t b, int64_t c, + int64_t max_c, int64_t new_a, int64_t new_max_a, + int64_t new_c, int64_t new_max_c) { + Model model; + const IntegerVariable var_a = model.Add(NewIntegerVariable(a, max_a)); + const IntegerVariable var_c = model.Add(NewIntegerVariable(c, max_c)); + model.Add(FixedDivisionConstraint(var_a, IntegerValue(b), var_c)); + const bool result = model.GetOrCreate()->Propagate(); + if (result) { + EXPECT_BOUNDS_EQ(var_a, new_a, new_max_a); + EXPECT_BOUNDS_EQ(var_c, new_c, new_max_c); + } + return result; +} + +TEST(FixedDivisionConstraintTest, ExpectedPropagation) { + // Propagate from a to c. + EXPECT_TRUE(PropagateFixedDivision(/*a=*/2, 21, /*b=*/3, /*c=*/-5, 10, + /*new_a=*/2, 21, /*new_c=*/0, 7)); + EXPECT_TRUE(PropagateFixedDivision(/*a=*/4, 20, /*b=*/3, /*c=*/0, 10, + /*new_a=*/4, 20, /*new_c=*/1, 6)); + EXPECT_TRUE(PropagateFixedDivision(/*a=*/-4, 20, /*b=*/3, /*c=*/-5, 10, + /*new_a=*/-4, 20, /*new_c=*/-1, 6)); + EXPECT_TRUE(PropagateFixedDivision(/*a=*/-15, -5, /*b=*/3, /*c=*/-10, 10, + /*new_a=*/-15, -5, /*new_c=*/-5, -1)); + // Propagate from c to a. + EXPECT_TRUE(PropagateFixedDivision(/*a=*/-10, 10, /*b=*/3, /*c=*/-2, 2, + /*new_a=*/-8, 8, /*new_c=*/-2, 2)); + EXPECT_TRUE(PropagateFixedDivision(/*a=*/-10, 10, /*b=*/3, /*c=*/1, 2, + /*new_a=*/3, 8, /*new_c=*/1, 2)); + EXPECT_TRUE(PropagateFixedDivision(/*a=*/-10, 10, /*b=*/3, /*c=*/0, 2, + /*new_a=*/-2, 8, /*new_c=*/0, 2)); + EXPECT_TRUE(PropagateFixedDivision(/*a=*/-10, 10, /*b=*/3, /*c=*/-2, -1, + /*new_a=*/-8, -3, /*new_c=*/-2, -1)); + EXPECT_TRUE(PropagateFixedDivision(/*a=*/-10, 10, /*b=*/3, /*c=*/-2, 0, + /*new_a=*/-8, 2, /*new_c=*/-2, 0)); + // Check large domains. + EXPECT_TRUE(PropagateFixedDivision( + /*a=*/0, std::numeric_limits::max() / 2, + /*b=*/5, /*c=*/3, std::numeric_limits::max() - 3, + /*new_a=*/15, std::numeric_limits::max() / 2, + /*new_c=*/3, std::numeric_limits::max() / 10)); + EXPECT_TRUE(PropagateFixedDivision( + /*a=*/0, std::numeric_limits::max() / 2, + /*b=*/5, /*c=*/3, std::numeric_limits::max() - 3, + /*new_a=*/15, std::numeric_limits::max() / 2, + /*new_c=*/3, std::numeric_limits::max() / 10)); +} + +TEST(ModuloConstraintTest, CheckAllSolutions) { + absl::BitGen random; + const int kMaxValue = 50; + const int kMaxModulo = 10; + const int kNumLoops = DEBUG_MODE ? 200 : 2000; + + for (int loop = 0; loop < kNumLoops; ++loop) { + CpModelProto initial_model; + int var_min = absl::Uniform(random, -kMaxValue, kMaxValue); + int var_max = absl::Uniform(random, -kMaxValue, kMaxValue); + if (var_min > var_max) std::swap(var_min, var_max); + IntegerVariableProto* var = initial_model.add_variables(); + var->add_domain(var_min); + var->add_domain(var_max); + + const int mod = absl::Uniform(random, 1, kMaxModulo); + IntegerVariableProto* mod_var = initial_model.add_variables(); + mod_var->add_domain(mod); + mod_var->add_domain(mod); + + IntegerVariableProto* target = initial_model.add_variables(); + int target_min = + absl::Uniform(random, -2 * kMaxModulo, 2 * kMaxModulo); + int target_max = + absl::Uniform(random, -2 * kMaxModulo, 2 * kMaxModulo); + if (target_min > target_max) std::swap(target_min, target_max); + target->add_domain(target_min); + target->add_domain(target_max); + + // target = var % mod. + LinearArgumentProto* modulo = + initial_model.add_constraints()->mutable_int_mod(); + modulo->add_exprs()->add_vars(0); // var. + modulo->mutable_exprs(0)->add_coeffs(1); + modulo->add_exprs()->add_vars(1); // mod + modulo->mutable_exprs(1)->add_coeffs(1); + modulo->mutable_target()->add_vars(2); // target + modulo->mutable_target()->add_coeffs(1); + + absl::btree_set> solutions; + const CpSolverResponse response = + SolveAndCheck(initial_model, "linearization_level:0", &solutions); + + // Loop through the domains of var and target, and collect valid solutions. + absl::btree_set> expected; + for (int i = var_min; i <= var_max; ++i) { + const int k = i % mod; + if (k < target_min || k > target_max) continue; + expected.insert({i, mod, k}); + } + + // Checks that we get we get the same solution set through the two methods. + EXPECT_EQ(solutions, expected) + << "\n---------\n" + << ProtobufDebugString(initial_model) << "---------\n"; + } +} + +TEST(ModuloConstraintTest, CheckAllPropagationsRandomProblem) { + absl::BitGen random; + const int kMaxValue = 50; + const int kMaxModulo = 10; + const int kNumLoops = DEBUG_MODE ? 5000 : 20000; + + for (int loop = 0; loop < kNumLoops; ++loop) { + // Generate domains for var and target. + int var_min = absl::Uniform(random, -kMaxValue, kMaxValue); + int var_max = absl::Uniform(random, -kMaxValue, kMaxValue); + if (var_min > var_max) std::swap(var_min, var_max); + int mod = absl::Uniform(random, 2, kMaxModulo); + int target_min = + absl::Uniform(random, -2 * kMaxModulo, 2 * kMaxModulo); + int target_max = + absl::Uniform(random, -2 * kMaxModulo, 2 * kMaxModulo); + if (target_min > target_max) std::swap(target_min, target_max); + + // Loop through the domains of var and target, and collect valid bounds. + int expected_var_min = std::numeric_limits::max(); + int expected_var_max = std::numeric_limits::min(); + int expected_target_min = std::numeric_limits::max(); + int expected_target_max = std::numeric_limits::min(); + for (int i = var_min; i <= var_max; ++i) { + const int k = i % mod; + if (k < target_min || k > target_max) continue; + expected_var_min = std::min(expected_var_min, i); + expected_var_max = std::max(expected_var_max, i); + expected_target_min = std::min(expected_target_min, k); + expected_target_max = std::max(expected_target_max, k); + } + + Model model; + const IntegerVariable var = model.Add(NewIntegerVariable(var_min, var_max)); + const IntegerVariable target = + model.Add(NewIntegerVariable(target_min, target_max)); + model.Add(FixedModuloConstraint(var, IntegerValue(mod), target)); + const bool result = model.GetOrCreate()->Propagate(); + if (result) { + EXPECT_BOUNDS_EQ(var, expected_var_min, expected_var_max); + EXPECT_BOUNDS_EQ(target, expected_target_min, expected_target_max) + << "var = [" << var_min << ".." << var_max << "], mod = " << mod + << ", target = [" << target_min << ".." << target_max + << "], expected_target = [" << expected_target_min << ".." + << expected_target_max << "], propagated target = [" + << model.Get(LowerBound(target)) << ".." + << model.Get(UpperBound(target)) << "]"; + } else { + EXPECT_EQ(expected_var_max, std::numeric_limits::min()); + } + } +} + +bool TestSquarePropagation(std::pair initial_domain_x, + std::pair initial_domain_s, + std::pair expected_domain_x, + std::pair expected_domain_s) { + Model model; + IntegerVariable x = model.Add( + NewIntegerVariable(initial_domain_x.first, initial_domain_x.second)); + IntegerVariable s = model.Add( + NewIntegerVariable(initial_domain_s.first, initial_domain_s.second)); + model.Add(ProductConstraint(x, x, s)); + const bool result = model.GetOrCreate()->Propagate(); + if (result) { + EXPECT_BOUNDS_EQ(x, expected_domain_x.first, expected_domain_x.second); + EXPECT_BOUNDS_EQ(s, expected_domain_s.first, expected_domain_s.second); + } + return result; +} + +bool TestSquarePropagation(std::pair initial_domain_x, + std::pair initial_domain_s) { + return TestSquarePropagation(initial_domain_x, initial_domain_s, + initial_domain_x, initial_domain_s); +} + +TEST(SquareConstraintTest, SquareExpectedPropagation) { + // Propagate s -> x, then x -> s. + EXPECT_TRUE(TestSquarePropagation({0, 3}, {1, 7}, {1, 2}, {1, 4})); + // Same but negative. + EXPECT_TRUE(TestSquarePropagation({-3, 0}, {1, 7}, {-2, -1}, {1, 4})); + // No propagation. + EXPECT_TRUE(TestSquarePropagation({2, 5}, {4, 25})); + // Propagate x -> s. + EXPECT_TRUE(TestSquarePropagation({2, 3}, {1, 12}, {2, 3}, {4, 9})); + // Infeasible, s has no square in its domain. + EXPECT_FALSE(TestSquarePropagation({0, 5}, {17, 20})); + // Infeasible, s cannot be the square of x. + EXPECT_FALSE(TestSquarePropagation({3, 7}, {50, 100})); + // Propagate s -> x. + EXPECT_TRUE(TestSquarePropagation({0, 10}, {16, 25}, {4, 5}, {16, 25})); +} + +TEST(SquareConstraintTest, LargestSquare) { + const int64_t max = kMaxIntegerValue.value(); + const int64_t square = + static_cast(std::floor(std::sqrt(static_cast(max)))); + CHECK_GE(CapProd(square + 1, square + 1), max); + EXPECT_TRUE(TestSquarePropagation({0, max}, {0, max}, {0, square}, + {0, square * square})); +} + +TEST(LevelZeroEqualityTest, BasicExample) { + Model model; + + const IntegerVariable obj = model.Add(NewIntegerVariable(1, 14)); + std::vector vars{model.Add(NewIntegerVariable(0, 1)), + model.Add(NewIntegerVariable(0, 1)), + model.Add(NewIntegerVariable(0, 1))}; + std::vector coeff{3, 4, 3}; + model.TakeOwnership(new LevelZeroEquality(obj, vars, coeff, &model)); + + // No propagations. + EXPECT_TRUE(model.GetOrCreate()->Propagate()); + EXPECT_EQ(model.Get(LowerBound(obj)), 1); + EXPECT_EQ(model.Get(UpperBound(obj)), 14); + + // Fix vars[1], obj is detected to be 3*X + 4. + // + // Note that the LB is not 4 because we have just the LevelZeroEquality + // propagator which doesn't propagate bounds. + model.Add(GreaterOrEqual(vars[1], 1)); + EXPECT_TRUE(model.GetOrCreate()->Propagate()); + EXPECT_EQ(model.Get(LowerBound(obj)), 1); + EXPECT_EQ(model.Get(UpperBound(obj)), 13); + + // Still propagate when new bound changes. + model.Add(GreaterOrEqual(obj, 5)); + EXPECT_TRUE(model.GetOrCreate()->Propagate()); + EXPECT_EQ(model.Get(LowerBound(obj)), 7); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/linear_model_test.cc b/ortools/sat/linear_model_test.cc new file mode 100644 index 00000000000..143cbe8bc58 --- /dev/null +++ b/ortools/sat/linear_model_test.cc @@ -0,0 +1,144 @@ +// Copyright 2010-2024 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/sat/linear_model.h" + +#include + +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/parse_test_proto.h" +#include "ortools/sat/cp_model.pb.h" +#include "ortools/sat/cp_model_presolve.h" +#include "ortools/sat/model.h" +#include "ortools/sat/presolve_context.h" +#include "ortools/sat/sat_parameters.pb.h" +#include "ortools/util/logging.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::google::protobuf::contrib::parse_proto::ParseTestProto; +using ::testing::EqualsProto; + +TEST(LinearModelTest, DetectFullEncoding) { + const CpModelProto cp_model = ParseTestProto(R"pb( + variables { domain: [ 0, 3 ] } + variables { domain: [ 0, 0, 5, 5, 10, 10 ] } + variables { domain: [ 0, 0 ] } + variables { domain: [ 5, 5 ] } + variables { domain: [ 10, 10 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: 6 + linear { vars: 5 coeffs: 1 domain: 0 domain: 0 } + } + constraints { + enforcement_literal: -7 + linear { vars: 5 coeffs: 1 domain: 1 domain: 2 } + } + constraints { + exactly_one { literals: 7 literals: 8 literals: 9 literals: 10 } + } + constraints { + enforcement_literal: 7 + linear { vars: 0 coeffs: 1 domain: 0 domain: 0 } + } + constraints { + enforcement_literal: -8 + linear { vars: 0 coeffs: 1 domain: 1 domain: 3 } + } + constraints { + enforcement_literal: 7 + linear { vars: 5 coeffs: 1 domain: 1 domain: 1 } + } + constraints { + enforcement_literal: -8 + linear { vars: 5 coeffs: 1 domain: 0 domain: 0 domain: 2 domain: 2 } + } + constraints { + enforcement_literal: 8 + linear { vars: 0 coeffs: 1 domain: 1 domain: 1 } + } + constraints { + enforcement_literal: -9 + linear { vars: 0 coeffs: 1 domain: 0 domain: 0 domain: 2 domain: 3 } + } + constraints { + enforcement_literal: 9 + linear { vars: 0 coeffs: 1 domain: 2 domain: 2 } + } + constraints { + enforcement_literal: -10 + linear { vars: 0 coeffs: 1 domain: 0 domain: 1 domain: 3 domain: 3 } + } + constraints { + enforcement_literal: 9 + linear { vars: 5 coeffs: 1 domain: 2 domain: 2 } + } + constraints { + enforcement_literal: -10 + linear { vars: 5 coeffs: 1 domain: 0 domain: 1 } + } + constraints { + enforcement_literal: 10 + linear { vars: 0 coeffs: 1 domain: 3 domain: 3 } + } + constraints { + enforcement_literal: -11 + linear { vars: 0 coeffs: 1 domain: 0 domain: 2 } + } + constraints { exactly_one { literals: -7 literals: 8 literals: 10 } } + constraints { + linear { vars: 1 vars: 5 coeffs: 1 coeffs: -5 domain: 0 domain: 0 } + } + )pb"); + + LinearModel linear_model(cp_model); + int num_ignored = 0; + for (const bool is_ignored : linear_model.ignored_constraints()) { + if (is_ignored) num_ignored++; + } + EXPECT_EQ(num_ignored, 14); + ASSERT_EQ(linear_model.additional_constraints().size(), 3); + const ConstraintProto ct0 = ParseTestProto(R"pb( + linear { + vars: [ 0, 8, 9, 10 ] + coeffs: [ -1, 1, 2, 3 ] + domain: [ 0, 0 ] + } + )pb"); + const ConstraintProto ct1 = ParseTestProto(R"pb( + linear { + vars: [ 5, 7, 9 ] + coeffs: [ -1, 1, 2 ] + domain: [ 0, 0 ] + } + )pb"); + const ConstraintProto ct2 = ParseTestProto(R"pb( + exactly_one { literals: [ 6, 7, 9 ] } + )pb"); + EXPECT_THAT(ct0, EqualsProto(linear_model.additional_constraints()[0])); + EXPECT_THAT(ct1, EqualsProto(linear_model.additional_constraints()[1])); + EXPECT_THAT(ct2, EqualsProto(linear_model.additional_constraints()[2])); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/linear_relaxation_test.cc b/ortools/sat/linear_relaxation_test.cc new file mode 100644 index 00000000000..14c0682f28e --- /dev/null +++ b/ortools/sat/linear_relaxation_test.cc @@ -0,0 +1,1200 @@ +// Copyright 2010-2024 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/sat/linear_relaxation.h" + +#include +#include +#include + +#include "absl/base/attributes.h" +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/base/parse_test_proto.h" +#include "ortools/sat/cp_model.pb.h" +#include "ortools/sat/cp_model_loader.h" +#include "ortools/sat/cp_model_mapping.h" +#include "ortools/sat/integer.h" +#include "ortools/sat/intervals.h" +#include "ortools/sat/linear_constraint.h" +#include "ortools/sat/model.h" +#include "ortools/sat/sat_base.h" +#include "ortools/util/sorted_interval_list.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::google::protobuf::contrib::parse_proto::ParseTestProto; + +TEST(AppendRelaxationForEqualityEncodingTest, DomainOfSize2) { + Model model; + IntegerEncoder* encoder = model.GetOrCreate(); + const IntegerVariable var = + model.Add(NewIntegerVariable(Domain::FromValues({4, 8}))); + encoder->FullyEncodeVariable(var); + + // Initially we don't have a view, so this should return false. + LinearRelaxation relaxation; + int num_tight = 0; + int num_loose = 0; + AppendRelaxationForEqualityEncoding(var, model, &relaxation, &num_tight, + &num_loose); + EXPECT_EQ(num_tight, 0); + EXPECT_EQ(num_loose, 0); + + // Make sure all literals have a view. + for (const auto literal_value : encoder->FullDomainEncoding(var)) { + model.Add(NewIntegerVariableFromLiteral(literal_value.literal)); + } + AppendRelaxationForEqualityEncoding(var, model, &relaxation, &num_tight, + &num_loose); + EXPECT_EQ(num_tight, 1); + + // In this case, because there is just two value, we should get a literal + // and its negation, so just one constraint (the first one is empty). + EXPECT_EQ(relaxation.linear_constraints.size(), 2); + EXPECT_EQ(relaxation.linear_constraints[0].num_terms, 0); + + // The variable (0) is equal to 8 - 4 * [var == 4]. + EXPECT_EQ(relaxation.linear_constraints[1].DebugString(), + "8 <= 1*X0 4*X1 <= 8"); +} + +// Convert the at_most_one to a linear constraint and call DebugString(). +std::string AtMostOneAsString(absl::Span at_most_one, + Model* model) { + LinearConstraintBuilder lc(model, kMinIntegerValue, IntegerValue(1)); + for (const Literal literal : at_most_one) { + const bool unused ABSL_ATTRIBUTE_UNUSED = + lc.AddLiteralTerm(literal, IntegerValue(1)); + } + return lc.Build().DebugString(); +} + +TEST(AppendRelaxationForEqualityEncodingTest, DomainOfSize4) { + Model model; + IntegerEncoder* encoder = model.GetOrCreate(); + const IntegerVariable var = + model.Add(NewIntegerVariable(Domain::FromValues({1, 5, 8, 9}))); + encoder->FullyEncodeVariable(var); + + // Make sure all relevant literals have a view. + for (const auto literal_value : encoder->FullDomainEncoding(var)) { + model.Add(NewIntegerVariableFromLiteral(literal_value.literal)); + } + + LinearRelaxation relaxation; + int num_tight = 0; + int num_loose = 0; + AppendRelaxationForEqualityEncoding(var, model, &relaxation, &num_tight, + &num_loose); + + EXPECT_EQ(relaxation.linear_constraints.size(), 2); + EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), + "1 <= 1*X1 1*X2 1*X3 1*X4"); + EXPECT_EQ(relaxation.linear_constraints[1].DebugString(), + "1 <= 1*X0 -4*X2 -7*X3 -8*X4 <= 1"); + + EXPECT_EQ(relaxation.at_most_ones.size(), 1); + EXPECT_EQ(AtMostOneAsString(relaxation.at_most_ones[0], &model), + "1*X1 1*X2 1*X3 1*X4 <= 1"); +} + +TEST(AppendRelaxationForEqualityEncodingTest, PartialEncoding) { + Model model; + IntegerEncoder* encoder = model.GetOrCreate(); + const IntegerVariable var = model.Add(NewIntegerVariable(0, 10)); + for (const int value : {1, 5}) { + encoder->AssociateToIntegerEqualValue( + Literal(model.Add(NewBooleanVariable()), true), var, + IntegerValue(value)); + } + + // Make sure all relevant literals have a view. + for (const auto literal_value : encoder->PartialDomainEncoding(var)) { + model.Add(NewIntegerVariableFromLiteral(literal_value.literal)); + } + + // The encoded values should be 0, 1 and 5, so the min/max not encoded should + // be 2 and 10. + LinearRelaxation relaxation; + int num_tight = 0; + int num_loose = 0; + AppendRelaxationForEqualityEncoding(var, model, &relaxation, &num_tight, + &num_loose); + EXPECT_EQ(num_tight, 0); + EXPECT_EQ(num_loose, 2); + + EXPECT_EQ(relaxation.linear_constraints.size(), 2); + EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), + "2 <= 1*X0 2*X1 1*X2 -3*X3"); + EXPECT_EQ(relaxation.linear_constraints[1].DebugString(), + "1*X0 10*X1 9*X2 5*X3 <= 10"); + + EXPECT_EQ(relaxation.at_most_ones.size(), 1); + EXPECT_EQ(AtMostOneAsString(relaxation.at_most_ones[0], &model), + "1*X1 1*X2 1*X3 <= 1"); +} + +TEST(AppendPartialGreaterThanEncodingRelaxationTest, FullEncoding) { + Model model; + IntegerEncoder* encoder = model.GetOrCreate(); + const IntegerVariable var = + model.Add(NewIntegerVariable(Domain::FromValues({1, 5, 8, 9}))); + encoder->FullyEncodeVariable(var); + + // Make sure all >= literal have a view. + for (const auto value_literal : encoder->PartialGreaterThanEncoding(var)) { + model.Add(NewIntegerVariableFromLiteral(value_literal.literal)); + } + + LinearRelaxation relaxation; + AppendPartialGreaterThanEncodingRelaxation(var, model, &relaxation); + + // The implications. + EXPECT_EQ(relaxation.at_most_ones.size(), 2); + EXPECT_EQ(AtMostOneAsString(relaxation.at_most_ones[0], &model), + "-1*X1 1*X2 <= 0"); + EXPECT_EQ(AtMostOneAsString(relaxation.at_most_ones[1], &model), + "-1*X2 1*X3 <= 0"); + + // The "diffs" are 4,3,1. + // Because here we have a full encoding, we actually have == 1. + EXPECT_EQ(relaxation.linear_constraints.size(), 2); + EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), + "1 <= 1*X0 -4*X1 -3*X2 -1*X3"); + EXPECT_EQ(relaxation.linear_constraints[1].DebugString(), + "-1 <= -1*X0 4*X1 3*X2 1*X3"); +} + +TEST(AppendPartialGreaterThanEncodingRelaxationTest, PartialEncoding) { + Model model; + IntegerEncoder* encoder = model.GetOrCreate(); + const IntegerVariable var = model.Add(NewIntegerVariable(0, 10)); + + // Create a literal for var >= 1, var >= 2 and var >= 6 + for (const int value : {1, 2, 6}) { + encoder->AssociateToIntegerLiteral( + Literal(model.Add(NewBooleanVariable()), true), + IntegerLiteral::GreaterOrEqual(var, IntegerValue(value))); + } + + // Make sure all >= literal have a view. + for (const auto value_literal : encoder->PartialGreaterThanEncoding(var)) { + model.Add(NewIntegerVariableFromLiteral(value_literal.literal)); + } + + LinearRelaxation relaxation; + AppendPartialGreaterThanEncodingRelaxation(var, model, &relaxation); + + // The implications. + EXPECT_EQ(relaxation.at_most_ones.size(), 2); + EXPECT_EQ(AtMostOneAsString(relaxation.at_most_ones[0], &model), + "-1*X1 1*X2 <= 0"); + EXPECT_EQ(AtMostOneAsString(relaxation.at_most_ones[1], &model), + "-1*X2 1*X3 <= 0"); + + // The first constraint is var >= 0 + (>=1) + (>=2) + 4*(>=6) + EXPECT_EQ(relaxation.linear_constraints.size(), 2); + EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), + "0 <= 1*X0 -1*X1 -1*X2 -4*X3"); + + // The second is var <= (>=1) + 4*(>=2) + 5*(>=6) which gives the bounds + // <=0,<=1,<=5 and <=10. + EXPECT_EQ(relaxation.linear_constraints[1].DebugString(), + "0 <= -1*X0 1*X1 4*X2 5*X3"); +} + +TEST(TryToLinearizeConstraint, BoolOr) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: 0 + bool_or { literals: [ -2, 2 ] } + } + )pb"); + + Model model; + LoadVariables(initial_model, true, &model); + + LinearRelaxation relaxation; + TryToLinearizeConstraint(initial_model, initial_model.constraints(0), + /*linearization_level=*/2, &model, &relaxation); + + EXPECT_EQ(relaxation.linear_constraints.size(), 1); + EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), + "-1 <= -1*X0 -1*X1 1*X2"); +} + +TEST(TryToLinearizeConstraint, BoolOrLevel1) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: 0 + bool_or { literals: [ -2, 2 ] } + } + )pb"); + + Model model; + LoadVariables(initial_model, true, &model); + + LinearRelaxation relaxation; + TryToLinearizeConstraint(initial_model, initial_model.constraints(0), + /*linearization_level=*/1, &model, &relaxation); + + EXPECT_EQ(relaxation.linear_constraints.size(), 0); + EXPECT_EQ(relaxation.at_most_ones.size(), 0); +} + +TEST(TryToLinearizeConstraint, BoolAndSingleEnforcement) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: 0 + bool_and { literals: [ -2, 2 ] } + } + )pb"); + + Model model; + LoadVariables(initial_model, true, &model); + + LinearRelaxation relaxation; + TryToLinearizeConstraint(initial_model, initial_model.constraints(0), + /*linearization_level=*/2, &model, &relaxation); + + EXPECT_EQ(relaxation.at_most_ones.size(), 2); + EXPECT_EQ(AtMostOneAsString(relaxation.at_most_ones[0], &model), + "1*X0 1*X1 <= 1"); + EXPECT_EQ(AtMostOneAsString(relaxation.at_most_ones[1], &model), + "1*X0 -1*X2 <= 0"); +} + +TEST(TryToLinearizeConstraint, BoolAndMultipleEnforcement) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: [ 0, 3 ] + bool_and { literals: [ -2, 2 ] } + } + )pb"); + + Model model; + LoadVariables(initial_model, true, &model); + + LinearRelaxation relaxation; + TryToLinearizeConstraint(initial_model, initial_model.constraints(0), + /*linearization_level=*/2, &model, &relaxation); + + // X0 & X3 => X2 ==1 & not(X1) == 1; + EXPECT_EQ(relaxation.linear_constraints.size(), 2); + EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), + "1*X0 1*X1 1*X3 <= 2"); + EXPECT_EQ(relaxation.linear_constraints[1].DebugString(), + "1*X0 -1*X2 1*X3 <= 1"); +} + +TEST(TryToLinearizeConstraint, BoolAndNoEnforcement) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { bool_and { literals: [ -2, 2 ] } } + )pb"); + + Model model; + LoadVariables(initial_model, true, &model); + + LinearRelaxation relaxation; + TryToLinearizeConstraint(initial_model, initial_model.constraints(0), + /*linearization_level=*/2, &model, &relaxation); + + EXPECT_EQ(relaxation.linear_constraints.size(), 0); + EXPECT_EQ(relaxation.at_most_ones.size(), 0); +} + +TEST(TryToLinearizeConstraint, BoolAndLevel1) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: [ 0, 3 ] + bool_and { literals: [ -2, 2 ] } + } + )pb"); + + Model model; + LoadVariables(initial_model, true, &model); + + LinearRelaxation relaxation; + TryToLinearizeConstraint(initial_model, initial_model.constraints(0), + /*linearization_level=*/1, &model, &relaxation); + + EXPECT_EQ(relaxation.linear_constraints.size(), 0); + EXPECT_EQ(relaxation.at_most_ones.size(), 0); +} + +TEST(TryToLinearizeConstraint, LinMaxLevel1Bis) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 5 ] } + variables { domain: [ -1, 7 ] } + variables { domain: [ -2, 9 ] } + variables { domain: [ -5, 10 ] } + constraints { + lin_max { + target: { vars: 3 coeffs: 1 } + exprs: { vars: 0 coeffs: 1 } + exprs: { vars: 1 coeffs: 1 } + exprs: { vars: 2 coeffs: -1 } + } + } + )pb"); + + Model model; + LoadVariables(initial_model, true, &model); + + LinearRelaxation relaxation; + TryToLinearizeConstraint(initial_model, initial_model.constraints(0), + /*linearization_level=*/1, &model, &relaxation); + + EXPECT_EQ(relaxation.linear_constraints.size(), 3); + EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), "1*X0 -1*X3 <= 0"); + EXPECT_EQ(relaxation.linear_constraints[1].DebugString(), "1*X1 -1*X3 <= 0"); + EXPECT_EQ(relaxation.linear_constraints[2].DebugString(), "-1*X2 -1*X3 <= 0"); +} + +TEST(TryToLinearizeConstraint, LinMaxSmall) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 5 ] } + variables { domain: [ -1, 7 ] } + variables { domain: [ -5, 10 ] } + constraints { + lin_max { + target: { vars: 2 coeffs: 1 } + exprs: { vars: 0 coeffs: 1 } + exprs: { vars: 1 coeffs: 1 } + } + } + )pb"); + + Model model; + LoadVariables(initial_model, true, &model); + + LinearRelaxation relaxation; + TryToLinearizeConstraint(initial_model, initial_model.constraints(0), + /*linearization_level=*/2, &model, &relaxation); + + // Take into account the constraints added by the cut generator. + EXPECT_GE(relaxation.linear_constraints.size(), 2); + EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), "1*X0 -1*X2 <= 0"); + EXPECT_EQ(relaxation.linear_constraints[1].DebugString(), "1*X1 -1*X2 <= 0"); +} + +TEST(TryToLinearizeConstraint, IntSquare) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 1, 10 ] } + variables { domain: [ 1, 100 ] } + constraints { + int_prod { + target: { vars: 1 coeffs: 1 } + exprs: { vars: 0 coeffs: 1 } + exprs: { vars: 0 coeffs: 1 } + } + } + )pb"); + + Model model; + LoadVariables(initial_model, true, &model); + + LinearRelaxation relaxation; + TryToLinearizeConstraint(initial_model, initial_model.constraints(0), + /*linearization_level=*/1, &model, &relaxation); + + EXPECT_EQ(relaxation.linear_constraints.size(), 3); + EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), + "-11*X0 1*X1 <= -10"); + EXPECT_EQ(relaxation.linear_constraints[1].DebugString(), "-2 <= -3*X0 1*X1"); + EXPECT_EQ(relaxation.linear_constraints[2].DebugString(), + "-90 <= -19*X0 1*X1"); +} + +TEST(TryToLinearizeConstraint, IntAbs) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 100 ] } + variables { domain: [ -20, 30 ] } + constraints { + lin_max { + target: { vars: 0 coeffs: 1 } + exprs: { vars: 1 coeffs: 1 } + exprs: { vars: 1 coeffs: -1 } + } + } + )pb"); + + Model model; + LoadVariables(initial_model, true, &model); + + LinearRelaxation relaxation; + TryToLinearizeConstraint(initial_model, initial_model.constraints(0), + /*linearization_level=*/1, &model, &relaxation); + + EXPECT_EQ(relaxation.linear_constraints.size(), 3); + EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), "-1*X0 1*X1 <= 0"); + EXPECT_EQ(relaxation.linear_constraints[1].DebugString(), "-1*X0 -1*X1 <= 0"); + EXPECT_EQ(relaxation.linear_constraints[2].DebugString(), + "50*X0 -10*X1 <= 1200"); +} + +TEST(TryToLinearizeConstraint, LinMaxLevel1) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 5 ] } + variables { domain: [ -1, 7 ] } + variables { domain: [ -2, 9 ] } + variables { domain: [ -5, 10 ] } + constraints { + lin_max { + target: { + vars: [ 0 ] + coeffs: [ 1 ] + offset: 3 + } + exprs: { + vars: [ 1 ] + coeffs: [ 2 ] + offset: 1 + } + exprs: { + vars: [ 2 ] + coeffs: [ -1 ] + offset: 2 + } + exprs: { + vars: [ 3 ] + coeffs: [ 3 ] + offset: 3 + } + } + } + )pb"); + + Model model; + LoadVariables(initial_model, true, &model); + + LinearRelaxation relaxation; + TryToLinearizeConstraint(initial_model, initial_model.constraints(0), + /*linearization_level=*/1, &model, &relaxation); + + EXPECT_EQ(relaxation.linear_constraints.size(), 3); + EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), "-1*X0 2*X1 <= 2"); + EXPECT_EQ(relaxation.linear_constraints[1].DebugString(), "-1*X0 -1*X2 <= 1"); + EXPECT_EQ(relaxation.linear_constraints[2].DebugString(), "-1*X0 3*X3 <= 0"); +} + +TEST(AppendLinMaxRelaxation, BasicBehavior) { + Model model; + IntegerVariable x0 = model.Add(NewIntegerVariable(0, 5)); + IntegerVariable x1 = model.Add(NewIntegerVariable(-1, 7)); + IntegerVariable x2 = model.Add(NewIntegerVariable(-2, 9)); + IntegerVariable target = model.Add(NewIntegerVariable(-5, 10)); + LinearExpression e0; + e0.vars = {x0}; + e0.coeffs = {IntegerValue(1)}; + LinearExpression e1; + e1.vars = {x1}; + e1.coeffs = {IntegerValue(1)}; + LinearExpression e2; + e2.vars = {x2}; + e2.coeffs = {IntegerValue(-1)}; + + const std::vector exprs = {e0, e1, e2}; + + LinearRelaxation relaxation; + const std::vector literals = + CreateAlternativeLiteralsWithView(exprs.size(), &model, &relaxation); + AppendLinMaxRelaxationPart2(target, literals, exprs, &model, &relaxation); + + EXPECT_EQ(literals.size(), 3); + ASSERT_EQ(relaxation.linear_constraints.size(), 4); + EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), + "1 <= 1*X4 1*X5 1*X6 <= 1"); + EXPECT_EQ(relaxation.linear_constraints[1].DebugString(), + "-1*X0 1*X3 -7*X5 -2*X6 <= 0"); + EXPECT_EQ(relaxation.linear_constraints[2].DebugString(), + "-1*X1 1*X3 -6*X4 -3*X6 <= 0"); + EXPECT_EQ(relaxation.linear_constraints[3].DebugString(), + "1*X2 1*X3 -14*X4 -16*X5 <= 0"); +} + +TEST(AppendLinMaxRelaxation, BasicBehaviorExprs) { + Model model; + IntegerVariable x0 = model.Add(NewIntegerVariable(-1, 1)); + IntegerVariable x1 = model.Add(NewIntegerVariable(-1, 1)); + IntegerVariable target = model.Add(NewIntegerVariable(-100, 100)); + LinearExpression e0; + e0.offset = IntegerValue(1); + LinearExpression e1; + e1.vars = {x0, x1}; + e1.coeffs = {IntegerValue(-1), IntegerValue(-2)}; + LinearExpression e2; + e2.vars = {x0, x1}; + e2.coeffs = {IntegerValue(-1), IntegerValue(1)}; + + const std::vector exprs = {e0, e1, e2}; + + LinearRelaxation relaxation; + const std::vector literals = + CreateAlternativeLiteralsWithView(exprs.size(), &model, &relaxation); + AppendLinMaxRelaxationPart2(target, literals, exprs, &model, &relaxation); + + EXPECT_EQ(literals.size(), 3); + ASSERT_EQ(relaxation.linear_constraints.size(), 4); + EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), + "1 <= 1*X3 1*X4 1*X5 <= 1"); + EXPECT_EQ(relaxation.linear_constraints[1].DebugString(), + "1*X2 -1*X3 -3*X4 -2*X5 <= 0"); + EXPECT_EQ(relaxation.linear_constraints[2].DebugString(), + "1*X0 2*X1 1*X2 -4*X3 -3*X5 <= 0"); + EXPECT_EQ(relaxation.linear_constraints[3].DebugString(), + "1*X0 -1*X1 1*X2 -3*X3 -3*X4 <= 0"); +} + +TEST(AppendLinMaxRelaxation, BasicBehaviorExprs2) { + Model model; + IntegerVariable x0 = model.Add(NewIntegerVariable(1, 2)); + IntegerVariable x1 = model.Add(NewIntegerVariable(0, 1)); + IntegerVariable x2 = model.Add(NewIntegerVariable(-2, -1)); + IntegerVariable target = model.Add(NewIntegerVariable(-3, 0)); + LinearExpression e0; + e0.vars = {x0, x1}; + e0.coeffs = {IntegerValue(-2), IntegerValue(-3)}; + e0.offset = IntegerValue(5); + LinearExpression e1; + e1.vars = {x1, x2}; + e1.coeffs = {IntegerValue(-2), IntegerValue(-5)}; + e1.offset = IntegerValue(-6); + LinearExpression e2; + e2.vars = {x0, x2}; + e2.coeffs = {IntegerValue(-2), IntegerValue(-3)}; + + const std::vector exprs = {e0, e1, e2}; + + LinearRelaxation relaxation; + const std::vector literals = + CreateAlternativeLiteralsWithView(exprs.size(), &model, &relaxation); + AppendLinMaxRelaxationPart2(NegationOf(target), literals, exprs, &model, + &relaxation); + + EXPECT_EQ(literals.size(), 3); + ASSERT_EQ(relaxation.linear_constraints.size(), 4); + EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), + "1 <= 1*X4 1*X5 1*X6 <= 1"); + EXPECT_EQ(relaxation.linear_constraints[1].DebugString(), + "2*X0 3*X1 -1*X3 -5*X4 -9*X5 -9*X6 <= 0"); + EXPECT_EQ(relaxation.linear_constraints[2].DebugString(), + "2*X1 5*X2 -1*X3 2*X4 6*X5 2*X6 <= 0"); + EXPECT_EQ(relaxation.linear_constraints[3].DebugString(), + "2*X0 3*X2 -1*X3 -2*X4 -2*X5 <= 0"); +} + +void AppendNoOverlapRelaxation(const ConstraintProto& ct, Model* model, + LinearRelaxation* relaxation) { + auto* mapping = model->GetOrCreate(); + std::vector intervals = + mapping->Intervals(ct.no_overlap().intervals()); + const IntegerValue one(1); + std::vector demands(intervals.size(), one); + IntervalsRepository* repository = model->GetOrCreate(); + SchedulingConstraintHelper* helper = repository->GetOrCreateHelper(intervals); + SchedulingDemandHelper* demands_helper = + new SchedulingDemandHelper(demands, helper, model); + model->TakeOwnership(demands_helper); + + AddCumulativeRelaxation(/*capacity=*/one, helper, demands_helper, + /*makespan=*/std::nullopt, model, relaxation); +} + +TEST(AppendNoOverlapRelaxation, IntersectingIntervals) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 5 ] } + variables { domain: [ 1, 7 ] } + variables { domain: [ 1, 12 ] } + variables { domain: [ 0, 5 ] } + variables { domain: [ 1, 7 ] } + variables { domain: [ 1, 12 ] } + constraints { no_overlap { intervals: [ 1, 2 ] } } + constraints { + interval { + start { vars: 0 coeffs: 1 } + size { vars: 1 coeffs: 1 } + end { vars: 2 coeffs: 1 } + } + } + constraints { + interval { + start { vars: 3 coeffs: 1 } + size { vars: 4 coeffs: 1 } + end { vars: 5 coeffs: 1 } + } + } + )pb"); + + Model model; + LoadVariables(initial_model, true, &model); + + LinearRelaxation relaxation; + AppendNoOverlapRelaxation(initial_model.constraints(0), &model, &relaxation); + + EXPECT_EQ(relaxation.linear_constraints.size(), 1); + EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), "1*X1 1*X4 <= 12"); +} + +TEST(AppendNoOverlapRelaxation, NoIntersection) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 1, 1 ] } + variables { domain: [ 1, 2 ] } + variables { domain: [ 2, 5 ] } + variables { domain: [ 1, 7 ] } + variables { domain: [ 1, 12 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + size { vars: 1 coeffs: 1 } + end { vars: 2 coeffs: 1 } + } + } + constraints { + interval { + start { vars: 3 coeffs: 1 } + size { vars: 4 coeffs: 1 } + end { vars: 5 coeffs: 1 } + } + } + constraints { no_overlap { intervals: [ 0, 1 ] } } + )pb"); + + Model model; + LoadVariables(initial_model, true, &model); + + LinearRelaxation relaxation; + AppendNoOverlapRelaxation(initial_model.constraints(2), &model, &relaxation); + + EXPECT_EQ(relaxation.linear_constraints.size(), 1); + EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), "1*X4 <= 11"); +} + +TEST(AppendNoOverlapRelaxation, IntervalWithEnforcement) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 2, 5 ] } + variables { domain: [ 1, 7 ] } + variables { domain: [ 1, 12 ] } + variables { domain: [ 2, 5 ] } + variables { domain: [ 1, 7 ] } + variables { domain: [ 1, 12 ] } + variables { domain: [ 0, 1 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + size { vars: 1 coeffs: 1 } + end { vars: 2 coeffs: 1 } + } + } + constraints { + enforcement_literal: 6 + interval { + start { vars: 3 coeffs: 1 } + size { vars: 4 coeffs: 1 } + end { vars: 5 coeffs: 1 } + } + } + constraints { no_overlap { intervals: [ 0, 1 ] } } + )pb"); + + Model model; + LoadVariables(initial_model, true, &model); + + LinearRelaxation relaxation; + AppendNoOverlapRelaxation(initial_model.constraints(2), &model, &relaxation); + + EXPECT_EQ(relaxation.linear_constraints.size(), 1); + EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), "1*X1 1*X6 <= 10"); +} + +TEST(AppendNoOverlapRelaxation, ZeroMinEnergy) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 1, 5 ] } + variables { domain: [ 0, 7 ] } + variables { domain: [ 1, 12 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 1, 5 ] } + variables { domain: [ 0, 7 ] } + variables { domain: [ 1, 12 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: 3 + interval { + start { vars: 0 coeffs: 1 } + size { vars: 1 coeffs: 1 } + end { vars: 2 coeffs: 1 } + } + } + constraints { + enforcement_literal: 7 + interval { + start { vars: 4 coeffs: 1 } + size { vars: 5 coeffs: 1 } + end { vars: 6 coeffs: 1 } + } + } + constraints { no_overlap { intervals: [ 0, 1 ] } } + )pb"); + + Model model; + LoadVariables(initial_model, true, &model); + + LinearRelaxation relaxation; + AppendNoOverlapRelaxation(initial_model.constraints(2), &model, &relaxation); + + EXPECT_EQ(relaxation.linear_constraints.size(), 0); +} + +TEST(AppendNoOverlapRelaxation, OneInterval) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 1, 1 ] } + variables { domain: [ 1, 2 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + size { vars: 1 coeffs: 1 } + end { vars: 2 coeffs: 1 } + } + } + constraints { no_overlap { intervals: 0 } } + )pb"); + + Model model; + LoadVariables(initial_model, true, &model); + + LinearRelaxation relaxation; + AppendNoOverlapRelaxation(initial_model.constraints(1), &model, &relaxation); + + EXPECT_EQ(relaxation.linear_constraints.size(), 0); +} + +void AppendCumulativeRelaxation(const ConstraintProto& ct, Model* model, + LinearRelaxation* relaxation) { + auto* mapping = model->GetOrCreate(); + std::vector intervals = + mapping->Intervals(ct.cumulative().intervals()); + const std::vector demands = + mapping->Affines(ct.cumulative().demands()); + const AffineExpression capacity = mapping->Affine(ct.cumulative().capacity()); + IntervalsRepository* repository = model->GetOrCreate(); + SchedulingConstraintHelper* helper = repository->GetOrCreateHelper(intervals); + SchedulingDemandHelper* demands_helper = + new SchedulingDemandHelper(demands, helper, model); + model->TakeOwnership(demands_helper); + + AddCumulativeRelaxation(capacity, helper, demands_helper, + /*makespan=*/std::nullopt, model, relaxation); +} + +TEST(AppendCumulativeRelaxation, GcdOnFixedDemandsSizesAndCapacity) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 5 ] } + variables { domain: [ 1, 4 ] } + variables { domain: [ 0, 7 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + size { offset: 4 } + end { vars: 0 coeffs: 1 offset: 4 } + } + } + constraints { + enforcement_literal: 3 + interval { + start { vars: 1 coeffs: 1 } + size { offset: 4 } + end { vars: 1 coeffs: 1 offset: 4 } + } + } + constraints { + enforcement_literal: 4 + interval { + start { vars: 2 coeffs: 1 } + size { offset: 2 } + end { vars: 2 coeffs: 1 offset: 2 } + } + } + constraints { + cumulative { + intervals: [ 0, 1, 2 ] + demands { offset: 3 } + demands { offset: 6 } + demands { offset: 3 } + capacity { offset: 7 } + } + } + )pb"); + + Model model; + LoadVariables(initial_model, true, &model); + + LinearRelaxation relaxation; + AppendCumulativeRelaxation(initial_model.constraints(3), &model, &relaxation); + + EXPECT_EQ(relaxation.linear_constraints.size(), 1); + EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), "4*X3 1*X4 <= 6"); +} + +TEST(AppendCumulativeRelaxation, IgnoreZeroDemandOrSize) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 5 ] } + variables { domain: [ 1, 4 ] } + variables { domain: [ 0, 7 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + interval { + start { vars: 0 coeffs: 1 } + size { offset: 4 } + end { vars: 0 coeffs: 1 offset: 4 } + } + } + constraints { + enforcement_literal: 3 + interval { + start { vars: 1 coeffs: 1 } + size { offset: 4 } + end { vars: 1 coeffs: 1 offset: 4 } + } + } + constraints { + enforcement_literal: 4 + interval { + start { vars: 2 coeffs: 1 } + size { offset: 2 } + end { vars: 2 coeffs: 1 offset: 2 } + } + } + constraints { + enforcement_literal: 5 + interval { + start { vars: 2 coeffs: 1 } + size { offset: 0 } + end { vars: 2 coeffs: 1 } + } + } + constraints { + enforcement_literal: 6 + interval { + start { vars: 2 coeffs: 1 offset: 5 } + size { offset: 3 } + end { vars: 2 coeffs: 1 offset: 8 } + } + } + constraints { + cumulative { + intervals: [ 0, 1, 2, 3, 4 ] + demands { offset: 3 } + demands { offset: 6 } + demands { offset: 3 } + demands { offset: 3 } + demands { offset: 0 } + capacity { offset: 7 } + } + } + )pb"); + + Model model; + LoadVariables(initial_model, true, &model); + + LinearRelaxation relaxation; + AppendCumulativeRelaxation(initial_model.constraints(5), &model, &relaxation); + + EXPECT_EQ(relaxation.linear_constraints.size(), 1); + EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), "4*X3 1*X4 <= 6"); +} + +TEST(AppendLinearConstraintRelaxation, NoEnforcementLiteral) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 2 ] } + constraints { + linear { + vars: [ 0, 2 ] + coeffs: [ 2, 1 ] + domain: [ 3, 4 ] + } + } + )pb"); + + Model model; + LoadVariables(initial_model, true, &model); + + LinearRelaxation relaxation; + AppendLinearConstraintRelaxation(initial_model.constraints(0), + /*linearize_enforced_constraints=*/true, + &model, &relaxation); + + EXPECT_EQ(relaxation.linear_constraints.size(), 1); + EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), + "3 <= 2*X0 1*X2 <= 4"); +} + +TEST(AppendLinearConstraintRelaxation, SmallLinearizationLevel) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 2 ] } + constraints { + enforcement_literal: 1 + linear { + vars: [ 0, 2 ] + coeffs: [ 2, 1 ] + domain: [ 3, 5 ] + } + } + )pb"); + + Model model; + LoadVariables(initial_model, true, &model); + + LinearRelaxation relaxation; + AppendLinearConstraintRelaxation(initial_model.constraints(0), + /*linearize_enforced_constraints=*/false, + &model, &relaxation); + EXPECT_EQ(relaxation.linear_constraints.size(), 0); +} + +TEST(AppendLinearConstraintRelaxation, PbConstraint) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 2, 1, 3 ] + domain: [ 3, 5 ] + } + } + )pb"); + + Model model; + LoadVariables(initial_model, false, &model); + + LinearRelaxation relaxation; + AppendLinearConstraintRelaxation(initial_model.constraints(0), + /*linearize_enforced_constraints=*/false, + &model, &relaxation); + EXPECT_EQ(relaxation.linear_constraints.size(), 1); + EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), + "3 <= 2*X0 1*X1 3*X2 <= 5"); +} + +TEST(AppendLinearConstraintRelaxation, SmallConstraint) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: 1 + linear { + vars: 0 + coeffs: 2 + domain: [ 3, 5 ] + } + } + )pb"); + + Model model; + LoadVariables(initial_model, true, &model); + + LinearRelaxation relaxation; + AppendLinearConstraintRelaxation(initial_model.constraints(0), + /*linearize_enforced_constraints=*/true, + &model, &relaxation); + + EXPECT_EQ(relaxation.linear_constraints.size(), 0); +} + +TEST(AppendLinearConstraintRelaxation, SingleEnforcementLiteralLowerBound) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 2 ] } + constraints { + enforcement_literal: 1 + linear { + vars: [ 0, 2 ] + coeffs: [ 2, 1 ] + domain: [ 3, 9223372036854775807 ] + } + } + )pb"); + + Model model; + LoadVariables(initial_model, true, &model); + + LinearRelaxation relaxation; + AppendLinearConstraintRelaxation(initial_model.constraints(0), + /*linearize_enforced_constraints=*/true, + &model, &relaxation); + + EXPECT_EQ(relaxation.linear_constraints.size(), 1); + EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), + "0 <= 2*X0 -3*X1 1*X2"); +} + +TEST(AppendLinearConstraintRelaxation, SingleEnforcementLiteralUpperBound) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 2 ] } + constraints { + enforcement_literal: 1 + linear { + vars: [ 0, 2 ] + coeffs: [ 2, 1 ] + domain: [ -9223372036854775808, 3 ] + } + } + )pb"); + + Model model; + LoadVariables(initial_model, true, &model); + + LinearRelaxation relaxation; + AppendLinearConstraintRelaxation(initial_model.constraints(0), + /*linearize_enforced_constraints=*/true, + &model, &relaxation); + + EXPECT_EQ(relaxation.linear_constraints.size(), 1); + EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), + "2*X0 1*X1 1*X2 <= 4"); +} + +TEST(AppendLinearConstraintRelaxation, SingleEnforcementLiteralBothBounds) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 2 ] } + constraints { + enforcement_literal: 1 + linear { + vars: [ 0, 2 ] + coeffs: [ 2, 1 ] + domain: [ 2, 3 ] + } + } + )pb"); + + Model model; + LoadVariables(initial_model, true, &model); + + LinearRelaxation relaxation; + AppendLinearConstraintRelaxation(initial_model.constraints(0), + /*linearize_enforced_constraints=*/true, + &model, &relaxation); + + EXPECT_EQ(relaxation.linear_constraints.size(), 2); + EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), + "0 <= 2*X0 -2*X1 1*X2"); + EXPECT_EQ(relaxation.linear_constraints[1].DebugString(), + "2*X0 1*X1 1*X2 <= 4"); +} + +TEST(AppendLinearConstraintRelaxation, MultipleEnforcementLiteral) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: [ 1, 3, 4 ] + linear { + vars: [ 0, 2 ] + coeffs: [ 2, 1 ] + domain: [ 2, 3 ] + } + } + )pb"); + + Model model; + LoadVariables(initial_model, true, &model); + + LinearRelaxation relaxation; + AppendLinearConstraintRelaxation(initial_model.constraints(0), + /*linearize_enforced_constraints=*/true, + &model, &relaxation); + + EXPECT_EQ(relaxation.linear_constraints.size(), 2); + EXPECT_EQ(relaxation.linear_constraints[0].DebugString(), + "-4 <= 2*X0 -2*X1 1*X2 -2*X3 -2*X4"); + EXPECT_EQ(relaxation.linear_constraints[1].DebugString(), + "2*X0 1*X1 1*X2 1*X3 1*X4 <= 6"); +} + +// This used to generate the completely wrong constraint: +// 1*X0 -8*X1 1*X2 -8*X3 <= -6 before. +TEST(AppendLinearConstraintRelaxation, BoundsNotTight) { + const CpModelProto initial_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + enforcement_literal: 1 + enforcement_literal: 3 + linear { + vars: [ 0, 2 ] + coeffs: [ 1, 1 ] + domain: [ 0, 10 ] # 10 > implied ub of 2. + } + } + )pb"); + + Model model; + LoadVariables(initial_model, true, &model); + + LinearRelaxation relaxation; + AppendLinearConstraintRelaxation(initial_model.constraints(0), + /*linearize_enforced_constraints=*/true, + &model, &relaxation); + + EXPECT_EQ(relaxation.linear_constraints.size(), 0); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/presolve_context_test.cc b/ortools/sat/presolve_context_test.cc new file mode 100644 index 00000000000..89c58a29720 --- /dev/null +++ b/ortools/sat/presolve_context_test.cc @@ -0,0 +1,1038 @@ +// Copyright 2010-2024 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/sat/presolve_context.h" + +#include +#include + +#include "absl/container/flat_hash_set.h" +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/parse_test_proto.h" +#include "ortools/base/types.h" +#include "ortools/sat/cp_model.pb.h" +#include "ortools/sat/cp_model_utils.h" +#include "ortools/sat/model.h" +#include "ortools/util/affine_relation.h" +#include "ortools/util/sorted_interval_list.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::google::protobuf::contrib::parse_proto::ParseTestProto; + +TEST(PresolveContextTest, GetOrCreateEncodingOnIntVar) { + Model model; + CpModelProto working_model; + PresolveContext context(&model, &working_model, nullptr); + context.NewIntVar({1, 5}); + + EXPECT_EQ(1, context.GetOrCreateVarValueEncoding(0, 2)); + EXPECT_EQ(2, context.GetOrCreateVarValueEncoding(0, 4)); + EXPECT_EQ(1, context.GetOrCreateVarValueEncoding(0, 2)); + EXPECT_EQ(1, context.GetOrCreateVarValueEncoding(-1, -2)); +} + +TEST(PresolveContextTest, GetOrCreateEncodingOnBoolVar) { + Model model; + CpModelProto working_model; + PresolveContext context(&model, &working_model, nullptr); + context.NewBoolVar("test"); + + EXPECT_EQ(0, context.GetOrCreateVarValueEncoding(0, 1)); + EXPECT_EQ(-1, context.GetOrCreateVarValueEncoding(0, 0)); +} + +TEST(PresolveContextTest, GetOrCreateEncodingOnSize2Var) { + Model model; + CpModelProto working_model; + PresolveContext context(&model, &working_model, nullptr); + context.NewIntVar(Domain::FromValues({1, 4})); + + EXPECT_EQ(-2, context.GetOrCreateVarValueEncoding(0, 1)); + EXPECT_EQ(1, context.GetOrCreateVarValueEncoding(0, 4)); +} + +TEST(PresolveContextTest, GetOrCreateEncodingOnSize2VarBis) { + Model model; + CpModelProto working_model; + PresolveContext context(&model, &working_model, nullptr); + context.NewIntVar(Domain::FromValues({1, 4})); + + EXPECT_EQ(1, context.GetOrCreateVarValueEncoding(0, 4)); + EXPECT_EQ(-2, context.GetOrCreateVarValueEncoding(0, 1)); +} + +TEST(PresolveContextTest, InsertVarValueEncodingOnIntVar) { + Model model; + CpModelProto working_model; + PresolveContext context(&model, &working_model, nullptr); + context.NewIntVar({1, 5}); + context.NewBoolVar("test"); + + context.InsertVarValueEncoding(1, 0, 2); + EXPECT_EQ(1, context.GetOrCreateVarValueEncoding(0, 2)); + EXPECT_EQ(1, context.GetOrCreateVarValueEncoding(-1, -2)); +} + +TEST(PresolveContextTest, InsertVarValueEncodingOnSize2Var) { + Model model; + CpModelProto working_model; + PresolveContext context(&model, &working_model, nullptr); + context.NewIntVar(Domain::FromValues({1, 4})); + context.NewBoolVar("test"); + + context.InsertVarValueEncoding(1, 0, 1); + EXPECT_EQ(1, context.GetOrCreateVarValueEncoding(0, 1)); + EXPECT_EQ(-2, context.GetOrCreateVarValueEncoding(0, 4)); +} + +TEST(PresolveContextTest, InsertVarValueEncodingOnSize2VarBis) { + Model model; + CpModelProto working_model; + PresolveContext context(&model, &working_model, nullptr); + context.NewIntVar(Domain::FromValues({1, 4})); + context.NewBoolVar("test"); + + context.InsertVarValueEncoding(1, 0, 4); + EXPECT_EQ(1, context.GetOrCreateVarValueEncoding(0, 4)); + EXPECT_EQ(-2, context.GetOrCreateVarValueEncoding(0, 1)); +} + +TEST(PresolveContextTest, InsertVarValueEncodingOnPosLitMinLit) { + Model model; + CpModelProto working_model; + PresolveContext context(&model, &working_model, nullptr); + const int a = context.NewBoolVar("test"); + const int b = context.NewBoolVar("test"); + context.InsertVarValueEncoding(a, b, 0); + EXPECT_EQ(context.GetLiteralRepresentative(b), NegatedRef(a)); + EXPECT_TRUE(context.VarToConstraints(a).contains(kAffineRelationConstraint)); + EXPECT_TRUE(context.VarToConstraints(b).contains(kAffineRelationConstraint)); +} + +TEST(PresolveContextTest, InsertVarValueEncodingOnPosLitPosMaxLit) { + Model model; + CpModelProto working_model; + PresolveContext context(&model, &working_model, nullptr); + const int a = context.NewBoolVar("test"); + const int b = context.NewBoolVar("test"); + context.InsertVarValueEncoding(a, b, 1); + EXPECT_EQ(context.GetLiteralRepresentative(b), a); + EXPECT_TRUE(context.VarToConstraints(a).contains(kAffineRelationConstraint)); + EXPECT_TRUE(context.VarToConstraints(b).contains(kAffineRelationConstraint)); +} + +TEST(PresolveContextTest, InsertVarValueEncodingOnNegLitMinLit) { + Model model; + CpModelProto working_model; + PresolveContext context(&model, &working_model, nullptr); + const int a = context.NewBoolVar("test"); + const int b = context.NewBoolVar("test"); + context.InsertVarValueEncoding(NegatedRef(a), b, 0); + EXPECT_EQ(context.GetLiteralRepresentative(b), a); + EXPECT_TRUE(context.VarToConstraints(a).contains(kAffineRelationConstraint)); + EXPECT_TRUE(context.VarToConstraints(b).contains(kAffineRelationConstraint)); +} + +TEST(PresolveContextTest, InsertVarValueEncodingOnNegLitMaxLit) { + Model model; + CpModelProto working_model; + PresolveContext context(&model, &working_model, nullptr); + const int a = context.NewBoolVar("test"); + const int b = context.NewBoolVar("test"); + context.InsertVarValueEncoding(NegatedRef(a), b, 1); + EXPECT_EQ(context.GetLiteralRepresentative(b), NegatedRef(a)); + EXPECT_TRUE(context.VarToConstraints(a).contains(kAffineRelationConstraint)); + EXPECT_TRUE(context.VarToConstraints(b).contains(kAffineRelationConstraint)); +} + +TEST(PresolveContextTest, InsertVarValueEncodingOnPosLitMinVar) { + Model model; + CpModelProto working_model; + PresolveContext context(&model, &working_model, nullptr); + const int a = context.NewBoolVar("test"); + const int b = context.NewIntVar(Domain::FromValues({2, 5})); + context.InsertVarValueEncoding(a, b, 2); + + EXPECT_EQ(context.GetAffineRelation(b).representative, a); + EXPECT_EQ(context.GetAffineRelation(b).coeff, -3); + EXPECT_EQ(context.GetAffineRelation(b).offset, 5); + + EXPECT_TRUE(context.VarToConstraints(a).contains(kAffineRelationConstraint)); + EXPECT_TRUE(context.VarToConstraints(b).contains(kAffineRelationConstraint)); +} + +TEST(PresolveContextTest, InsertVarValueEncodingOnPosLitMaxVar) { + Model model; + CpModelProto working_model; + PresolveContext context(&model, &working_model, nullptr); + const int a = context.NewBoolVar("test"); + const int b = context.NewIntVar(Domain::FromValues({2, 5})); + context.InsertVarValueEncoding(a, b, 5); + + EXPECT_EQ(context.GetAffineRelation(b).representative, a); + EXPECT_EQ(context.GetAffineRelation(b).coeff, 3); + EXPECT_EQ(context.GetAffineRelation(b).offset, 2); + + EXPECT_TRUE(context.VarToConstraints(a).contains(kAffineRelationConstraint)); + EXPECT_TRUE(context.VarToConstraints(b).contains(kAffineRelationConstraint)); +} + +TEST(PresolveContextTest, InsertVarValueEncodingOnNegLitMinVar) { + Model model; + CpModelProto working_model; + PresolveContext context(&model, &working_model, nullptr); + const int a = context.NewBoolVar("test"); + const int b = context.NewIntVar(Domain::FromValues({2, 5})); + context.InsertVarValueEncoding(NegatedRef(a), b, 2); + + EXPECT_EQ(context.GetAffineRelation(b).representative, a); + EXPECT_EQ(context.GetAffineRelation(b).coeff, 3); + EXPECT_EQ(context.GetAffineRelation(b).offset, 2); + + EXPECT_TRUE(context.VarToConstraints(a).contains(kAffineRelationConstraint)); + EXPECT_TRUE(context.VarToConstraints(b).contains(kAffineRelationConstraint)); +} + +TEST(PresolveContextTest, InsertVarValueEncodingOnNegLitMaxVar) { + Model model; + CpModelProto working_model; + PresolveContext context(&model, &working_model, nullptr); + const int a = context.NewBoolVar("test"); + const int b = context.NewIntVar(Domain::FromValues({2, 5})); + context.InsertVarValueEncoding(NegatedRef(a), b, 5); + + EXPECT_EQ(context.GetAffineRelation(b).representative, a); + EXPECT_EQ(context.GetAffineRelation(b).coeff, -3); + EXPECT_EQ(context.GetAffineRelation(b).offset, 5); + + EXPECT_TRUE(context.VarToConstraints(a).contains(kAffineRelationConstraint)); + EXPECT_TRUE(context.VarToConstraints(b).contains(kAffineRelationConstraint)); +} + +TEST(PresolveContextTest, DomainContainsExpr) { + Model model; + CpModelProto working_model; + PresolveContext context(&model, &working_model, nullptr); + const int var = context.NewIntVar({1, 5}); + + LinearExpressionProto expr; + expr.add_vars(var); + expr.add_coeffs(3); + expr.set_offset(2); + + EXPECT_FALSE(context.DomainContains(expr, 2)); + EXPECT_FALSE(context.DomainContains(expr, 7)); + EXPECT_TRUE(context.DomainContains(expr, 11)); + + LinearExpressionProto fixed; + fixed.set_offset(-1); + EXPECT_FALSE(context.DomainContains(fixed, 2)); + EXPECT_TRUE(context.DomainContains(fixed, -1)); + + LinearExpressionProto coeff0; + coeff0.add_vars(var); + coeff0.add_coeffs(0); + coeff0.set_offset(5); + EXPECT_FALSE(context.DomainContains(coeff0, 2)); + EXPECT_TRUE(context.DomainContains(coeff0, 5)); +} + +TEST(PresolveContextTest, GetOrCreateEncodingOnAffine) { + Model model; + CpModelProto working_model; + PresolveContext context(&model, &working_model, nullptr); + const int var = context.NewIntVar({1, 5}); + + LinearExpressionProto expr; + expr.add_vars(var); + expr.add_coeffs(3); + expr.set_offset(2); + + const int zero = context.GetFalseLiteral(); + const int one = context.GetTrueLiteral(); + + EXPECT_EQ(zero, context.GetOrCreateAffineValueEncoding(expr, 2)); + EXPECT_EQ(zero, context.GetOrCreateAffineValueEncoding(expr, 7)); + EXPECT_EQ(context.GetOrCreateAffineValueEncoding(expr, 11), + context.GetOrCreateVarValueEncoding(var, 3)); + + LinearExpressionProto fixed; + fixed.set_offset(-1); + EXPECT_EQ(zero, context.GetOrCreateAffineValueEncoding(fixed, 2)); + EXPECT_EQ(one, context.GetOrCreateAffineValueEncoding(fixed, -1)); +} + +TEST(PresolveContextTest, LinearExpressionMinMax) { + Model model; + CpModelProto working_model; + PresolveContext context(&model, &working_model, nullptr); + context.NewIntVar(Domain(0, 1)); + context.NewIntVar(Domain(0, 1)); + const LinearExpressionProto expr = ParseTestProto(R"pb( + vars: [ 0, 1 ] + coeffs: [ 2, -3 ] + offset: 5 + )pb"); + + EXPECT_EQ(2, context.MinOf(expr)); + EXPECT_EQ(7, context.MaxOf(expr)); +} + +TEST(PresolveContextTest, ObjectiveReadCanonicalizeWrite) { + Model model; + CpModelProto working_model = ParseTestProto(R"pb( + variables { domain: [ 0, 8 ] } + variables { domain: [ 3, 3 ] } + variables { domain: [ -2, 7 ] } + variables { domain: [ -2, -2 ] } + variables { domain: [ -4, 11 ] } + objective { + vars: [ 0, 4, 2, 3, 1 ] + coeffs: [ 2, 4, -2, -4, -2 ] + domain: [ 0, 1000 ] + offset: 3 + } + )pb"); + + PresolveContext context(&model, &working_model, nullptr); + context.InitializeNewDomains(); + context.ReadObjectiveFromProto(); + EXPECT_TRUE(context.CanonicalizeObjective()); + context.WriteObjectiveToProto(); + + const CpModelProto expected = ParseTestProto(R"pb( + variables { domain: [ 0, 8 ] } + variables { domain: [ 3, 3 ] } + variables { domain: [ -2, 7 ] } + variables { domain: [ -2, -2 ] } + variables { domain: [ -4, 11 ] } + objective { + vars: [ 0, 2, 4 ] + coeffs: [ 1, -1, 2 ] + domain: [ -1, 32 ] + offset: 2.5 + scaling_factor: 2 + integer_before_offset: 1 + integer_scaling_factor: 2 + } + )pb"); + EXPECT_THAT(working_model, testing::EqualsProto(expected)); +} + +TEST(PresolveContextTest, ExploitAtMostOneInObjective) { + Model model; + CpModelProto working_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + objective { + vars: [ 0, 1, 2, 3 ] + coeffs: [ 2, 3, 7, 4 ] + } + constraints { bool_or { literals: [ 0, 1, 2, 3 ] } } + )pb"); + + PresolveContext context(&model, &working_model, nullptr); + context.InitializeNewDomains(); + context.ReadObjectiveFromProto(); + EXPECT_TRUE(context.CanonicalizeObjective()); + + // Do not crash if called with empty exactly one. The problem should be UNSAT + // in this case, but we might call this before reporting it. + EXPECT_FALSE(context.ExploitExactlyOneInObjective({})); + + EXPECT_TRUE(context.ExploitExactlyOneInObjective({0, 1, 2})); + EXPECT_TRUE(context.CanonicalizeObjective()); + context.WriteObjectiveToProto(); + + const CpModelProto expected = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + objective { + vars: [ 1, 2, 3 ] + coeffs: [ 1, 5, 4 ] + domain: [ 0, 10 ] + offset: 2 + scaling_factor: 1 + integer_before_offset: 2 + } + constraints { bool_or { literals: [ 0, 1, 2, 3 ] } } + )pb"); + EXPECT_THAT(working_model, testing::EqualsProto(expected)); +} + +TEST(PresolveContextTest, ExploitAtMostOneInObjectiveNegatedRef) { + Model model; + CpModelProto working_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { bool_or { literals: [ 0, 1, 2, 3 ] } } + objective { + vars: [ 0, 1, 2, 3 ] + coeffs: [ 2, 3, 7, 4 ] + } + )pb"); + + PresolveContext context(&model, &working_model, nullptr); + context.InitializeNewDomains(); + context.ReadObjectiveFromProto(); + EXPECT_TRUE(context.CanonicalizeObjective()); + EXPECT_TRUE(context.ExploitExactlyOneInObjective({0, NegatedRef(1), 2})); + EXPECT_TRUE(context.CanonicalizeObjective()); + context.WriteObjectiveToProto(); + + // The objective is 2X + 3(1 - Y) + 7Z with X + Y + Z = 1 + // So we get 3 + 2 X - 3 Y + 7 Z and when shifted by -3, we get 5X + 10Z. + const CpModelProto expected = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { bool_or { literals: [ 0, 1, 2, 3 ] } } + objective { + vars: [ 0, 2, 3 ] + coeffs: [ 5, 10, 4 ] + domain: [ 0, 15 ] # We get 15 because 16 is not reachable. + scaling_factor: 1 + } + )pb"); + EXPECT_THAT(working_model, testing::EqualsProto(expected)); +} + +TEST(PresolveContextTest, ObjectiveSubstitution) { + Model model; + CpModelProto working_model = ParseTestProto(R"pb( + variables { domain: [ 0, 9 ] } + variables { domain: [ 0, 9 ] } + variables { domain: [ 0, 9 ] } + variables { domain: [ 0, 9 ] } + variables { domain: [ 0, 9 ] } + objective { + vars: [ 0 ] + coeffs: [ 1 ] + domain: [ 0, 1000 ] + offset: 3 + } + )pb"); + + PresolveContext context(&model, &working_model, nullptr); + context.InitializeNewDomains(); + context.ReadObjectiveFromProto(); + EXPECT_TRUE(context.CanonicalizeObjective()); + + const ConstraintProto constraint = ParseTestProto(R"pb( + linear { + vars: [ 0, 1, 2 ] + coeffs: [ -1, 1, 1 ] + domain: [ 6, 6 ] + } + )pb"); + EXPECT_TRUE(context.SubstituteVariableInObjective(0, -1, constraint)); + + context.WriteObjectiveToProto(); + const CpModelProto expected = ParseTestProto(R"pb( + variables { domain: [ 0, 9 ] } + variables { domain: [ 0, 9 ] } + variables { domain: [ 0, 9 ] } + variables { domain: [ 0, 9 ] } + variables { domain: [ 0, 9 ] } + objective { + vars: [ 1, 2 ] + coeffs: [ 1, 1 ] + domain: [ 6, 15 ] # [0, 9] initially, + 6 offset. + offset: -3 + integer_before_offset: -6 + scaling_factor: 1 + } + )pb"); + EXPECT_THAT(working_model, testing::EqualsProto(expected)); +} + +TEST(PresolveContextTest, ObjectiveSubstitutionWithLargeCoeff) { + Model model; + CpModelProto working_model = ParseTestProto(R"pb( + variables { domain: [ 0, 9 ] } + variables { domain: [ 0, 9 ] } + variables { domain: [ 0, 9 ] } + variables { domain: [ 0, 9 ] } + variables { domain: [ 0, 9 ] } + objective { + vars: [ 0 ] + coeffs: [ 4 ] + domain: [ 0, 1000 ] + offset: 3 + } + )pb"); + + PresolveContext context(&model, &working_model, nullptr); + context.InitializeNewDomains(); + context.ReadObjectiveFromProto(); + + const ConstraintProto constraint = ParseTestProto(R"pb( + linear { + vars: [ 0, 1, 2 ] + coeffs: [ -2, 1, 1 ] + domain: [ 6, 6 ] + } + )pb"); + EXPECT_TRUE(context.SubstituteVariableInObjective(0, -2, constraint)); + + context.WriteObjectiveToProto(); + const CpModelProto expected = ParseTestProto(R"pb( + variables { domain: [ 0, 9 ] } + variables { domain: [ 0, 9 ] } + variables { domain: [ 0, 9 ] } + variables { domain: [ 0, 9 ] } + variables { domain: [ 0, 9 ] } + objective { + vars: [ 1, 2 ] + coeffs: [ 2, 2 ] + domain: [ 12, 1012 ] # [0, 1000] initially, + 2*6 offset. + offset: -9 + integer_before_offset: -12 + scaling_factor: 1 + } + )pb"); + EXPECT_THAT(working_model, testing::EqualsProto(expected)); +} + +TEST(PresolveContextTest, VarValueEncoding) { + Model model; + CpModelProto working_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 9 ] } + variables { domain: [ 0, 9 ] } + )pb"); + + PresolveContext context(&model, &working_model, nullptr); + context.InitializeNewDomains(); + EXPECT_TRUE(context.StoreLiteralImpliesVarEqValue(0, 2, 4)); + EXPECT_FALSE(context.StoreLiteralImpliesVarEqValue(0, 2, 4)); + EXPECT_FALSE(context.HasVarValueEncoding(2, 4)); + + EXPECT_TRUE(context.StoreLiteralImpliesVarNEqValue(-1, 2, 4)); + EXPECT_FALSE(context.StoreLiteralImpliesVarNEqValue(-1, 2, 4)); + EXPECT_TRUE(context.HasVarValueEncoding(2, 4)); + + EXPECT_TRUE(context.StoreLiteralImpliesVarNEqValue(0, 1, 4)); + EXPECT_FALSE(context.StoreLiteralImpliesVarNEqValue(0, 1, 4)); + EXPECT_FALSE(context.HasVarValueEncoding(1, 4)); + + EXPECT_TRUE(context.StoreLiteralImpliesVarEqValue(-1, 1, 4)); + EXPECT_FALSE(context.StoreLiteralImpliesVarEqValue(-1, 1, 4)); + EXPECT_TRUE(context.HasVarValueEncoding(1, 4)); +} + +TEST(PresolveContextTest, DetectVarEqValueHalfEncoding) { + Model model; + CpModelProto working_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 9 ] } + constraints { + enforcement_literal: 1 + linear { + vars: [ 2 ] + coeffs: [ 1 ] + domain: [ 6, 6 ] + } + } + constraints { + enforcement_literal: -2 + linear { + vars: [ 2 ] + coeffs: [ 1 ] + domain: [ 0, 5, 7, 9 ] + } + } + )pb"); + + const int kLiteral = 1; + const int kVar = 2; + const int64_t kValue = 6; + + PresolveContext context(&model, &working_model, nullptr); + context.InitializeNewDomains(); + + context.StoreLiteralImpliesVarEqValue(kLiteral, kVar, kValue); + context.StoreLiteralImpliesVarNEqValue(NegatedRef(kLiteral), kVar, kValue); + int encoding_literal = 0; + EXPECT_TRUE(context.HasVarValueEncoding(kVar, kValue, &encoding_literal)); + EXPECT_EQ(encoding_literal, kLiteral); +} + +TEST(PresolveContextTest, GetLiteralRepresentative) { + Model model; + CpModelProto working_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + )pb"); + + PresolveContext context(&model, &working_model, nullptr); + context.InitializeNewDomains(); + + EXPECT_NE(context.GetLiteralRepresentative(0), + context.GetLiteralRepresentative(1)); + EXPECT_NE(context.GetLiteralRepresentative(0), + context.GetLiteralRepresentative(2)); + EXPECT_NE(context.GetLiteralRepresentative(1), + context.GetLiteralRepresentative(2)); + EXPECT_NE(context.GetLiteralRepresentative(-1), + context.GetLiteralRepresentative(-2)); + EXPECT_NE(context.GetLiteralRepresentative(-1), + context.GetLiteralRepresentative(-3)); + EXPECT_NE(context.GetLiteralRepresentative(-2), + context.GetLiteralRepresentative(-3)); + + context.StoreBooleanEqualityRelation(0, 1); + EXPECT_EQ(context.GetLiteralRepresentative(0), + context.GetLiteralRepresentative(1)); + EXPECT_NE(context.GetLiteralRepresentative(0), + context.GetLiteralRepresentative(2)); + EXPECT_NE(context.GetLiteralRepresentative(1), + context.GetLiteralRepresentative(2)); + EXPECT_EQ(context.GetLiteralRepresentative(-1), + context.GetLiteralRepresentative(-2)); + EXPECT_NE(context.GetLiteralRepresentative(-1), + context.GetLiteralRepresentative(-3)); + EXPECT_NE(context.GetLiteralRepresentative(-2), + context.GetLiteralRepresentative(-3)); + + context.StoreBooleanEqualityRelation(0, -3); + EXPECT_EQ(context.GetLiteralRepresentative(0), + context.GetLiteralRepresentative(1)); + EXPECT_EQ(context.GetLiteralRepresentative(0), + context.GetLiteralRepresentative(-3)); + EXPECT_EQ(context.GetLiteralRepresentative(1), + context.GetLiteralRepresentative(-3)); + EXPECT_EQ(context.GetLiteralRepresentative(-1), + context.GetLiteralRepresentative(-2)); + EXPECT_EQ(context.GetLiteralRepresentative(-1), + context.GetLiteralRepresentative(2)); + EXPECT_EQ(context.GetLiteralRepresentative(-2), + context.GetLiteralRepresentative(2)); +} + +TEST(PresolveContextTest, VarIsOnlyUsedInEncoding) { + Model model; + CpModelProto working_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + constraints { + enforcement_literal: 0 + linear { + vars: [ 2 ] + coeffs: [ 1 ] + domain: [ 3, 15 ] + } + } + constraints { + enforcement_literal: 1 + linear { + vars: [ 2 ] + coeffs: [ 1 ] + domain: [ 7, 8 ] + } + } + constraints { + linear { + vars: [ 3, 4 ] + coeffs: [ 1, 1 ] + domain: [ 5, 5 ] + } + } + )pb"); + PresolveContext context(&model, &working_model, nullptr); + context.InitializeNewDomains(); + context.UpdateNewConstraintsVariableUsage(); + EXPECT_FALSE(context.VariableIsOnlyUsedInEncodingAndMaybeInObjective(0)); + EXPECT_FALSE(context.VariableIsOnlyUsedInEncodingAndMaybeInObjective(1)); + EXPECT_TRUE(context.VariableIsOnlyUsedInEncodingAndMaybeInObjective(2)); + EXPECT_FALSE(context.VariableIsOnlyUsedInEncodingAndMaybeInObjective(3)); + EXPECT_FALSE(context.VariableIsOnlyUsedInEncodingAndMaybeInObjective(4)); +} + +TEST(PresolveContextTest, ReifiedConstraintCache) { + Model model; + CpModelProto working_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 10 ] } + variables { domain: [ 0, 10 ] } + )pb"); + PresolveContext context(&model, &working_model, nullptr); + context.InitializeNewDomains(); + context.UpdateNewConstraintsVariableUsage(); + LinearExpressionProto expr1; + expr1.add_vars(2); + expr1.add_coeffs(1); + LinearExpressionProto expr2; + expr2.add_vars(3); + expr2.add_coeffs(1); + + const int var2_before_var3 = + context.GetOrCreateReifiedPrecedenceLiteral(expr1, expr2, 0, 1); + EXPECT_EQ(var2_before_var3, + context.GetOrCreateReifiedPrecedenceLiteral(expr1, expr2, 0, 1)); + EXPECT_EQ(var2_before_var3, + context.GetOrCreateReifiedPrecedenceLiteral(expr1, expr2, 1, 0)); + EXPECT_NE(var2_before_var3, + context.GetOrCreateReifiedPrecedenceLiteral(expr2, expr1, 1, 0)); + ConstraintProto bool_or = ParseTestProto(R"pb( + bool_or { literals: [ 5, 4, -2, -1 ] })pb"); + // 2 x (2 implications , 2 enforced linear) + bool_or. + ASSERT_EQ(9, working_model.constraints_size()); + EXPECT_THAT(working_model.constraints(8), ::testing::EqualsProto(bool_or)); +} + +TEST(PresolveContextTest, ExploitFixedDomainOverflow) { + Model model; + CpModelProto working_model = ParseTestProto(R"pb( + variables { domain: 0 domain: 0 } + variables { domain: 34359738368 domain: 34359738368 } + constraints { dummy_constraint { vars: 0 vars: 1 } } + )pb"); + PresolveContext context(&model, &working_model, nullptr); + context.InitializeNewDomains(); + context.UpdateNewConstraintsVariableUsage(); +} + +TEST(PresolveContextTest, IntersectDomainWithConstant) { + Model model; + CpModelProto working_model; + PresolveContext context(&model, &working_model, nullptr); + + LinearExpressionProto constant; + constant.set_offset(3); + EXPECT_TRUE(context.IntersectDomainWith(constant, Domain(2, 3))); + EXPECT_FALSE(context.IntersectDomainWith(constant, Domain(2, 2))); +} + +// Most of the logic is already tested by the Domain() manipulation function, +// we just test a simpel case here. +TEST(PresolveContextTest, IntersectDomainWithAffineExpression) { + Model model; + CpModelProto working_model = ParseTestProto(R"pb( + variables { domain: 0 domain: 5 } + )pb"); + PresolveContext context(&model, &working_model, nullptr); + context.InitializeNewDomains(); + + // -2 x + 3 in [2, 3] so -2x in [-1, 0] and x must be in [0, 1]. + LinearExpressionProto expr; + expr.add_vars(0); + expr.add_coeffs(-1); + expr.set_offset(3); + EXPECT_TRUE(context.IntersectDomainWith(expr, Domain(2, 3))); + EXPECT_EQ(context.DomainOf(0), Domain(0, 1)); +} + +TEST(PresolveContextTest, DomainSuperSetOf) { + Model model; + CpModelProto working_model = ParseTestProto(R"pb( + variables { domain: 0 domain: 1000 } + )pb"); + PresolveContext context(&model, &working_model, nullptr); + context.InitializeNewDomains(); + + const LinearExpressionProto expr1 = + ParseTestProto(R"pb(vars: 0 coeffs: 1 offset: 4)pb"); + EXPECT_EQ(context.DomainSuperSetOf(expr1), Domain(4, 1004)); + + const LinearExpressionProto expr2 = + ParseTestProto(R"pb(vars: 0 coeffs: 2 offset: 4)pb"); + EXPECT_EQ(context.DomainSuperSetOf(expr2), Domain(4, 2004)); +} + +TEST(PresolveContextTest, DomainSuperSetOfDiscrete) { + Model model; + CpModelProto working_model = ParseTestProto(R"pb( + variables { domain: 0 domain: 1 } + )pb"); + PresolveContext context(&model, &working_model, nullptr); + context.InitializeNewDomains(); + + const LinearExpressionProto expr1 = + ParseTestProto(R"pb(vars: 0 coeffs: -2 offset: 4)pb"); + EXPECT_EQ(context.DomainSuperSetOf(expr1), Domain::FromValues({2, 4})); +} + +TEST(PresolveContextTest, AddAffineRelation) { + Model model; + CpModelProto working_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1000 ] } + variables { domain: [ 0, 1000 ] } + variables { domain: [ 0, 1000 ] } + variables { domain: [ 0, 1000 ] } + )pb"); + PresolveContext context(&model, &working_model, nullptr); + context.InitializeNewDomains(); + context.UpdateNewConstraintsVariableUsage(); + + EXPECT_TRUE(context.StoreAffineRelation(0, 1, 3, 0)); // x0 = 3x1 + EXPECT_TRUE(context.StoreAffineRelation(2, 3, 5, 0)); // x2 = 5x3 + EXPECT_TRUE(context.StoreAffineRelation(0, 2, 2, 0)); // x0 = 2x2 ! + + // A new variable is created: x4 ! + // x0 = 2x2 get expanded into 3x1 = 10 x3, so x1 is a multiple of 10. + EXPECT_EQ(context.GetAffineRelation(1).representative, 4); + EXPECT_EQ(context.GetAffineRelation(1).coeff, 10); + EXPECT_EQ(context.DomainOf(4).ToString(), "[0,33]"); + + // x0 = 3x1 multiple of 30. + EXPECT_EQ(context.GetAffineRelation(0).representative, 4); + EXPECT_EQ(context.GetAffineRelation(0).coeff, 30); + + // x3 is a multiple of 3. + EXPECT_EQ(context.GetAffineRelation(3).representative, 4); + EXPECT_EQ(context.GetAffineRelation(3).coeff, 3); + + // x2 = 5x3 is a multiple of 15. + EXPECT_EQ(context.GetAffineRelation(2).representative, 4); + EXPECT_EQ(context.GetAffineRelation(2).coeff, 15); +} + +TEST(PresolveContextTest, AddAffineRelationWithOffset) { + Model model; + CpModelProto working_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1000 ] } + variables { domain: [ 0, 1000 ] } + variables { domain: [ 0, 1000 ] } + variables { domain: [ 0, 1000 ] } + )pb"); + PresolveContext context(&model, &working_model, nullptr); + context.InitializeNewDomains(); + context.UpdateNewConstraintsVariableUsage(); + + EXPECT_TRUE(context.StoreAffineRelation(0, 1, 3, 10)); // x0 = 3x1 + 10 + EXPECT_TRUE(context.StoreAffineRelation(2, 3, 1, 30)); // x2 = x3 + 30 + EXPECT_TRUE(context.StoreAffineRelation(0, 2, 1, 0)); // x0 = x2 ! + + // x0 = 3x1 + 10 + EXPECT_EQ(context.GetAffineRelation(0).representative, 1); + EXPECT_EQ(context.GetAffineRelation(0).coeff, 3); + EXPECT_EQ(context.GetAffineRelation(0).offset, 10); + + // x3 = x2 - 30 = 3x1 - 20 + EXPECT_EQ(context.GetAffineRelation(3).representative, 1); + EXPECT_EQ(context.GetAffineRelation(3).coeff, 3); + EXPECT_EQ(context.GetAffineRelation(3).offset, -20); + + // x2 same as x0 + EXPECT_EQ(context.GetAffineRelation(2).representative, 1); + EXPECT_EQ(context.GetAffineRelation(2).coeff, 3); + EXPECT_EQ(context.GetAffineRelation(2).offset, 10); +} + +TEST(PresolveContextTest, AddAffineRelationPreventOverflow) { + Model model; + CpModelProto working_model = ParseTestProto(R"pb( + variables { domain: [ 0, 1000000 ] } + variables { domain: [ 100000001, 100000004 ] } + )pb"); + PresolveContext context(&model, &working_model, nullptr); + context.InitializeNewDomains(); + context.UpdateNewConstraintsVariableUsage(); + + // x0 = 10 x2 - 1e9. + EXPECT_TRUE(context.StoreAffineRelation(0, 1, 10, -1000000000)); + + // To avoid "future" overflow a new variable is created. + // And everything is expressed using that one. + EXPECT_EQ(context.GetAffineRelation(1).representative, 2); + EXPECT_EQ(context.GetAffineRelation(1).coeff, 1); + EXPECT_EQ(context.GetAffineRelation(1).offset, 100000001); + EXPECT_EQ(context.DomainOf(2).ToString(), "[0,3]"); + + // And x0 is in term of that one. + EXPECT_EQ(context.GetAffineRelation(0).representative, 2); + EXPECT_EQ(context.GetAffineRelation(0).coeff, 10); + EXPECT_EQ(context.DomainOf(0).ToString(), "[10][20][30][40]"); +} + +TEST(PresolveContextTest, ObjectiveScalingMinimize) { + Model model; + CpModelProto working_model = ParseTestProto(R"pb( + variables { domain: [ 0, 20 ] } + variables { domain: [ 10, 30 ] } + floating_point_objective { + vars: [ 0, 1 ], + coeffs: [ 3.5, -1.3333333333 ], + maximize: false, + offset: 1.0 + } + )pb"); + PresolveContext context(&model, &working_model, nullptr); + context.InitializeNewDomains(); + ASSERT_TRUE(context.ScaleFloatingPointObjective()); + ASSERT_TRUE(working_model.has_objective()); + ASSERT_FALSE(working_model.has_floating_point_objective()); + const CpObjectiveProto& obj = working_model.objective(); + EXPECT_EQ(2, obj.vars_size()); + EXPECT_FLOAT_EQ(obj.scaling_factor() * obj.coeffs(0), 3.5); + EXPECT_NEAR(obj.scaling_factor() * obj.coeffs(1), -4.0 / 3.0, 1e-5); + EXPECT_FLOAT_EQ(obj.scaling_factor() * obj.offset(), 1.0); +} + +TEST(PresolveContextTest, ObjectiveScalingMaximize) { + Model model; + CpModelProto working_model = ParseTestProto(R"pb( + variables { domain: [ 0, 20 ] } + variables { domain: [ 10, 30 ] } + floating_point_objective { + vars: [ 0, 1 ], + coeffs: [ 3.5, -1.3333333333 ], + maximize: true, + offset: 1.0 + } + )pb"); + PresolveContext context(&model, &working_model, nullptr); + context.InitializeNewDomains(); + ASSERT_TRUE(context.ScaleFloatingPointObjective()); + ASSERT_TRUE(working_model.has_objective()); + ASSERT_FALSE(working_model.has_floating_point_objective()); + const CpObjectiveProto& obj = working_model.objective(); + EXPECT_EQ(2, obj.vars_size()); + EXPECT_FLOAT_EQ(obj.scaling_factor() * obj.coeffs(0), 3.5); + EXPECT_NEAR(obj.scaling_factor() * obj.coeffs(1), -4.0 / 3.0, 1e-5); + EXPECT_FLOAT_EQ(obj.scaling_factor() * obj.offset(), 1.0); +} + +TEST(ExpressionIsALiteralTest, BasicApi) { + Model model; + CpModelProto working_model = ParseTestProto(R"pb( + variables { domain: [ 0, 5 ] } + variables { domain: [ 0, 1 ] } + )pb"); + PresolveContext context(&model, &working_model, nullptr); + context.InitializeNewDomains(); + context.UpdateNewConstraintsVariableUsage(); + + int ref; + const LinearExpressionProto expr1 = ParseTestProto(R"pb( + vars: 0 coeffs: 1 + )pb"); + EXPECT_FALSE(context.ExpressionIsALiteral(expr1)); + + const LinearExpressionProto expr2 = ParseTestProto(R"pb( + vars: 1 coeffs: 1 + )pb"); + EXPECT_TRUE(context.ExpressionIsALiteral(expr2, &ref)); + EXPECT_EQ(1, ref); + + const LinearExpressionProto expr3 = + ParseTestProto(R"pb( + vars: 1 coeffs: -1 offset: 1 + )pb"); + EXPECT_TRUE(context.ExpressionIsALiteral(expr3, &ref)); + EXPECT_EQ(-2, ref); + + const LinearExpressionProto expr4 = + ParseTestProto(R"pb( + vars: 1 coeffs: -1 offset: 2 + )pb"); + EXPECT_FALSE(context.ExpressionIsALiteral(expr4)); + + const LinearExpressionProto expr5 = + ParseTestProto(R"pb( + vars: -2 coeffs: 1 offset: 1 + )pb"); + EXPECT_TRUE(context.ExpressionIsALiteral(expr5, &ref)); + EXPECT_EQ(-2, ref); +} + +TEST(PresolveContextTest, CanonicalizeAffineVariable) { + Model model; + CpModelProto working_model; + PresolveContext context(&model, &working_model, nullptr); + const int x = context.NewIntVar(Domain(0, 15)); + + // 3 * x + 9 is a multiple of 6. + // This is the same as x + 3 is a multiple of 2. + EXPECT_TRUE(context.CanonicalizeAffineVariable(x, 3, 6, 9)); + + const AffineRelation::Relation r = context.GetAffineRelation(x); + EXPECT_EQ(r.coeff, 2); +} + +TEST(PresolveContextTest, ComputeMinMaxActivity) { + Model model; + CpModelProto working_model = ParseTestProto(R"pb( + variables { domain: [ 0, 8 ] } + variables { domain: [ 3, 3 ] } + variables { domain: [ -2, 7 ] } + variables { domain: [ -2, -2 ] } + variables { domain: [ -4, 11 ] } + objective { + vars: [ 0, 1, 2, 3, 4 ] + coeffs: [ 2, 4, -2, -4, -2 ] + domain: [ 0, 1000 ] + offset: 3 + } + )pb"); + + PresolveContext context(&model, &working_model, nullptr); + context.InitializeNewDomains(); + const auto [min_activity, max_activity] = + context.ComputeMinMaxActivity(working_model.objective()); + EXPECT_EQ(min_activity, 2 * 0 + 4 * 3 - 2 * 7 - 4 * -2 - 2 * 11); + EXPECT_EQ(max_activity, 2 * 8 + 4 * 3 - 2 * -2 - 4 * -2 - 2 * -4); +} + +TEST(PresolveContextTest, CanonicalizeLinearConstraint) { + Model model; + CpModelProto working_model = ParseTestProto(R"pb( + variables { domain: [ 0, 8 ] } + variables { domain: [ 0, 8 ] } + variables { domain: [ 0, 8 ] } + variables { domain: [ 0, 8 ] } + constraints { + linear { + vars: [ 0, 1, 2, 0, 1 ] + coeffs: [ 2, 4, -2, -4, -2 ] + domain: [ 0, 1000 ] + } + } + )pb"); + PresolveContext context(&model, &working_model, nullptr); + context.InitializeNewDomains(); + + context.CanonicalizeLinearConstraint(working_model.mutable_constraints(0)); + + const ConstraintProto expected = ParseTestProto(R"pb( + linear { + vars: [ 0, 1, 2 ] + coeffs: [ -2, 2, -2 ] + domain: [ 0, 1000 ] + } + )pb"); + EXPECT_THAT(working_model.constraints(0), testing::EqualsProto(expected)); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/presolve_util_test.cc b/ortools/sat/presolve_util_test.cc new file mode 100644 index 00000000000..61cf0f2de79 --- /dev/null +++ b/ortools/sat/presolve_util_test.cc @@ -0,0 +1,513 @@ +// Copyright 2010-2024 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/sat/presolve_util.h" + +#include + +#include +#include +#include + +#include "absl/container/flat_hash_set.h" +#include "absl/random/random.h" +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/logging.h" +#include "ortools/base/parse_test_proto.h" +#include "ortools/sat/cp_model.h" +#include "ortools/sat/cp_model.pb.h" +#include "ortools/sat/cp_model_solver.h" +#include "ortools/sat/cp_model_utils.h" +#include "ortools/sat/sat_parameters.pb.h" +#include "ortools/util/sorted_interval_list.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::google::protobuf::contrib::parse_proto::ParseTestProto; +using ::testing::ElementsAre; + +TEST(DomainDeductionsTest, BasicTest) { + DomainDeductions deductions; + + deductions.AddDeduction(0, 3, Domain(0, 4)); + deductions.AddDeduction(1, 3, Domain(1, 8)); + + EXPECT_TRUE(deductions.ProcessClause({0, 1, 2}).empty()); + EXPECT_THAT(deductions.ProcessClause({0, 1}), + ElementsAre(std::make_pair(3, Domain(0, 8)))); + EXPECT_THAT(deductions.ProcessClause({0}), + ElementsAre(std::make_pair(3, Domain(0, 4)))); + EXPECT_THAT(deductions.ProcessClause({1}), + ElementsAre(std::make_pair(3, Domain(1, 8)))); + + deductions.MarkProcessingAsDoneForNow(); + EXPECT_TRUE(deductions.ProcessClause({0}).empty()); + + deductions.AddDeduction(0, 3, Domain(4, 4)); + EXPECT_EQ(deductions.ImpliedDomain(0, 3), Domain(4, 4)); + EXPECT_EQ(deductions.ImpliedDomain(7, 3), Domain::AllValues()); + EXPECT_TRUE(deductions.ProcessClause({1}).empty()); + EXPECT_THAT(deductions.ProcessClause({0}), + ElementsAre(std::make_pair(3, Domain(4, 4)))); + EXPECT_THAT(deductions.ProcessClause({0, 1}), + ElementsAre(std::make_pair(3, Domain(1, 8)))); +} + +TEST(AddLinearConstraintMultiple, BasicTestWithPositiveCoeff) { + ConstraintProto to_modify = ParseTestProto(R"pb( + linear { + vars: [ 0, 1, 2, 3 ] + coeffs: [ 2, 3, 4, 5 ] + domain: [ 0, 10 ] + } + )pb"); + const ConstraintProto to_add = ParseTestProto(R"pb( + linear { + vars: [ 0, 1, 2, 3 ] + coeffs: [ 2, 1, 4, 5 ] + domain: [ 3, 3 ] + } + )pb"); + + EXPECT_TRUE(AddLinearConstraintMultiple(3, to_add, &to_modify)); + const ConstraintProto expected = ParseTestProto(R"pb( + linear { + vars: [ 0, 1, 2, 3 ] + coeffs: [ 8, 6, 16, 20 ] + domain: [ 9, 19 ] + } + )pb"); + EXPECT_THAT(to_modify, testing::EqualsProto(expected)); +} + +TEST(SubstituteVariableTest, BasicTestWithPositiveCoeff) { + ConstraintProto constraint = ParseTestProto(R"pb( + linear { + vars: [ 0, 1, 2, 3 ] + coeffs: [ 2, 3, 4, 5 ] + domain: [ 0, 10 ] + } + )pb"); + const ConstraintProto definition = ParseTestProto(R"pb( + linear { + vars: [ 0, 1, 2, 3 ] + coeffs: [ 2, 1, 4, 5 ] + domain: [ 3, 3 ] + } + )pb"); + + EXPECT_TRUE(SubstituteVariable(1, 1, definition, &constraint)); + + // We have X1 = 3 - 2X0 - 4X2 -5X3 and the coeff of X1 in constraint is 3. + const ConstraintProto expected = ParseTestProto(R"pb( + linear { + vars: [ 0, 2, 3 ] + coeffs: [ -4, -8, -10 ] + domain: [ -9, 1 ] + } + )pb"); + EXPECT_THAT(constraint, testing::EqualsProto(expected)); +} + +TEST(SubstituteVariableTest, BasicTestWithNegativeCoeff) { + ConstraintProto constraint = ParseTestProto(R"pb( + linear { + vars: [ 0, 1, 2, 3 ] + coeffs: [ 2, 3, 4, 5 ] + domain: [ 0, 10 ] + } + )pb"); + const ConstraintProto definition = ParseTestProto(R"pb( + linear { + vars: [ 0, 1, 2, 3 ] + coeffs: [ 2, -1, 4, 5 ] + domain: [ 3, 3 ] + } + )pb"); + + EXPECT_TRUE(SubstituteVariable(1, -1, definition, &constraint)); + + // We have X1 = 2X0 + 4X2 + 5X3 - 3 and the coeff of X1 in constraint is 3. + const ConstraintProto expected = ParseTestProto(R"pb( + linear { + vars: [ 0, 2, 3 ] + coeffs: [ 8, 16, 20 ] + domain: [ 9, 19 ] + } + )pb"); + EXPECT_THAT(constraint, testing::EqualsProto(expected)); +} + +TEST(SubstituteVariableTest, WorkWithDuplicate) { + ConstraintProto constraint = ParseTestProto(R"pb( + linear { + vars: [ 0, 1, 2, 3, 1, 3 ] + coeffs: [ 2, 3, 4, 5, 5, 5 ] + domain: [ 0, 10 ] + } + )pb"); + const ConstraintProto definition = ParseTestProto(R"pb( + linear { + vars: [ 0, 1, 2, 3 ] + coeffs: [ 2, 1, 4, 5 ] + domain: [ 3, 3 ] + } + )pb"); + + EXPECT_TRUE(SubstituteVariable(1, 1, definition, &constraint)); + + // Constraint is actually 2X0 + 7X1 + 4X2 + 10X3 + // Which gives 2X0 + 8(3 - 2X0 - 4X2 -5X3) + 4X2 + 10X3 + const ConstraintProto expected = ParseTestProto(R"pb( + linear { + vars: [ 0, 2, 3 ] + coeffs: [ -14, -28, -30 ] + domain: [ -24, -14 ] + } + )pb"); + EXPECT_THAT(constraint, testing::EqualsProto(expected)); +} + +TEST(SubstituteVariableTest, FalseIfVariableNotThere) { + ConstraintProto constraint = ParseTestProto(R"pb( + linear { + vars: [ 0, 1, 1 ] + coeffs: [ 2, 3, -3 ] + domain: [ 0, 10 ] + } + )pb"); + const ConstraintProto definition = ParseTestProto(R"pb( + linear { + vars: [ 0, 1, 2, 3 ] + coeffs: [ 2, 1, 4, 5 ] + domain: [ 3, 3 ] + } + )pb"); + + EXPECT_FALSE(SubstituteVariable(1, 1, definition, &constraint)); +} + +TEST(ActivityBoundHelperTest, TrivialMaxBound) { + ActivityBoundHelper helper; + + // If there are no amo, we get trivial values + std::vector> conditional; + const int64_t result = + helper.ComputeMaxActivity({{+3, 4}, {-1, -7}, {-3, 5}}, &conditional); + EXPECT_EQ(result, 9); + ASSERT_EQ(conditional.size(), 3); + EXPECT_EQ(conditional[0][0], 5); + EXPECT_EQ(conditional[0][1], 9); + EXPECT_EQ(conditional[1][0], 9); + EXPECT_EQ(conditional[1][1], 2); + EXPECT_EQ(conditional[2][0], 4); + EXPECT_EQ(conditional[2][1], 9); +} + +TEST(ActivityBoundHelperTest, TrivialMinBound) { + ActivityBoundHelper helper; + + // If there are no amo, we get trivial values + std::vector> conditional; + const int64_t result = + helper.ComputeMinActivity({{+3, 4}, {-1, -7}, {-3, 5}}, &conditional); + EXPECT_EQ(result, -7); + ASSERT_EQ(conditional.size(), 3); + EXPECT_EQ(conditional[0][0], -7); + EXPECT_EQ(conditional[0][1], -3); + EXPECT_EQ(conditional[1][0], 0); + EXPECT_EQ(conditional[1][1], -7); + EXPECT_EQ(conditional[2][0], -7); + EXPECT_EQ(conditional[2][1], -2); +} + +TEST(ActivityBoundHelperTest, DisjointAmo) { + ActivityBoundHelper helper; + helper.AddAtMostOne({+1, +2, -3}); + helper.AddAtMostOne({-5, -6, -7}); + + std::vector> conditional; + const int64_t result = helper.ComputeMaxActivity( + {{+1, 4}, {+2, 7}, {-5, 5}, {-6, 6}, {10, 3}}, &conditional); + + // We have a partition [+1, +2] [-5, -6] [10]. + EXPECT_EQ(result, 16); + ASSERT_EQ(conditional.size(), 5); + EXPECT_EQ(conditional[0][0], 16); + EXPECT_EQ(conditional[0][1], 13); + EXPECT_EQ(conditional[1][0], 13); + EXPECT_EQ(conditional[1][1], 16); + + EXPECT_EQ(conditional[2][0], 16); + EXPECT_EQ(conditional[2][1], 15); + EXPECT_EQ(conditional[3][0], 15); + EXPECT_EQ(conditional[3][1], 16); + + EXPECT_EQ(conditional[4][0], 13); + EXPECT_EQ(conditional[4][1], 16); +} + +TEST(ActivityBoundHelperTest, PartitionLiteralsIntoAmo) { + ActivityBoundHelper helper; + helper.AddAtMostOne({+1, +2, -3}); + helper.AddAtMostOne({-5, -6, -7}); + + // The order is not documented, but it actually follow the original order. + std::vector literals({+1, -6, +2, 10, -5}); + EXPECT_THAT( + helper.PartitionLiteralsIntoAmo(literals), + ElementsAre(ElementsAre(+1, +2), ElementsAre(-6, -5), ElementsAre(10))); +} + +TEST(ActivityBoundHelperTest, IsAmo) { + ActivityBoundHelper helper; + helper.AddAtMostOne({+1, +2, -3}); + helper.AddAtMostOne({-5, -6, -7}); + + EXPECT_FALSE(helper.IsAmo({+1, +2, +3})); + EXPECT_FALSE(helper.IsAmo({+1, -5, -6})); + EXPECT_TRUE(helper.IsAmo({+1, -3})); + EXPECT_TRUE(helper.IsAmo({-5, -7})); +} + +// We will compare with CP-SAT on small instances, and make sure bounds are +// correct. +TEST(ActivityBoundHelperTest, RandomTest) { + for (int num_test = 0; num_test < 10; ++num_test) { + absl::BitGen random; + const int num_vars = 10; + const int num_amos = 5; + + // Generate random sat instances. + // These are always feasible. + CpModelBuilder model; + std::vector vars; + for (int i = 0; i < num_vars; ++i) vars.push_back(model.NewBoolVar()); + for (int c = 0; c < num_amos; ++c) { + std::vector amo; + for (int i = 0; i < num_vars; ++i) { + if (absl::Bernoulli(random, 0.5)) { + amo.push_back(vars[i]); + } + } + if (!amo.empty()) model.AddAtMostOne(amo); + } + LinearExpr obj; + std::vector> terms; + for (int i = 0; i < num_vars; ++i) { + const int coeff = absl::Uniform(random, -100, 100); + obj += coeff * vars[i]; + terms.push_back({i, coeff}); + } + model.Maximize(obj); + + // Get Maximum bound. + SatParameters params; + params.set_log_search_progress(false); + params.set_cp_model_presolve(false); + const CpModelProto proto = model.Build(); + const CpSolverResponse response = SolveWithParameters(proto, params); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + + // Same with helper + ActivityBoundHelper helper; + helper.AddAllAtMostOnes(proto); + std::vector> conditional_max; + const int64_t max_activity = + helper.ComputeMaxActivity(terms, &conditional_max); + EXPECT_GE(max_activity, response.objective_value()); + LOG(INFO) << response.objective_value() << " " << max_activity; + for (int i = 0; i < conditional_max.size(); ++i) { + // We also know the exact bound for the returned optimal solution. + EXPECT_GE(conditional_max[i][response.solution(i)], + response.objective_value()); + } + } +} + +TEST(ActivityBoundHelperTest, PresolveEnforcement) { + ActivityBoundHelper helper; + helper.AddAtMostOne({+1, +2, +3}); + helper.AddAtMostOne({+4, +5, +6, +7}); + + ConstraintProto ct; + ct.add_enforcement_literal(+1); + ct.add_enforcement_literal(NegatedRef(+2)); + ct.add_enforcement_literal(+6); + + absl::flat_hash_set at_true; + EXPECT_TRUE(helper.PresolveEnforcement({1, 2, 3, 4, 5}, &ct, &at_true)); + + // NegatedRef(+2) is a consequence of +1 (we process in order), so removed. + EXPECT_THAT(ct.enforcement_literal(), ElementsAre(+1, +6)); + EXPECT_TRUE(at_true.contains(+1)); + EXPECT_TRUE(at_true.contains(NegatedRef(+2))); + EXPECT_TRUE(at_true.contains(NegatedRef(+3))); + EXPECT_TRUE(at_true.contains(NegatedRef(+4))); + EXPECT_TRUE(at_true.contains(NegatedRef(+5))); + + // Not in the list, so not contained. + EXPECT_FALSE(at_true.contains(+7)); + EXPECT_FALSE(at_true.contains(NegatedRef(+7))); +} + +// This used to fail because of the degenerate AMO with x and not(x). +TEST(ActivityBoundHelperTest, PresolveEnforcementCornerCase) { + ActivityBoundHelper helper; + helper.AddAtMostOne({+1, -2}); + + ConstraintProto ct; + ct.add_enforcement_literal(+1); + + absl::flat_hash_set at_true; + EXPECT_TRUE(helper.PresolveEnforcement({}, &ct, &at_true)); + EXPECT_THAT(ct.enforcement_literal(), ElementsAre(+1)); +} + +TEST(ClauseWithOneMissingHasherTest, BasicTest) { + absl::BitGen random; + ClauseWithOneMissingHasher hasher(random); + + hasher.RegisterClause(0, {+1, -5, +6, +7}); + hasher.RegisterClause(2, {+1, +7, +6, -4}); + EXPECT_EQ(hasher.HashWithout(0, -5), hasher.HashWithout(2, -4)); + EXPECT_NE(hasher.HashWithout(0, +6), hasher.HashWithout(2, +6)); +} + +// !X1 => X2 + X3 <= 1 +// X1 + X2 <= 1 +// +// when X1 is true, we can see that X2 + X3 <= 1 still stand, so we don't need +// the enforcement. +TEST(ActivityBoundHelper, RemoveEnforcementThatCouldBeLifted) { + ActivityBoundHelper helper; + helper.AddAtMostOne({+1, +2}); + + ConstraintProto ct; + ct.add_enforcement_literal(NegatedRef(1)); + std::vector> terms{{+2, 1}, {+3, 1}}; + + const int num_removed = helper.RemoveEnforcementThatMakesConstraintTrivial( + terms, Domain(0), Domain(0, 1), &ct); + EXPECT_EQ(num_removed, 1); + EXPECT_TRUE(ct.enforcement_literal().empty()); +} + +// !X1 => 2 * X2 + X3 + X4 <= 2 and X1 + X2 + X3 <= 1 +// Note that in this case, if X1 is 1, we have some slack, so we could lift it +// into X1 + 2 * X2 + X3 + X4 <= 2. +// +// But here, we could just extract X2 as an enforcement too, and just have +// X2 => X4 <= 0. This should just be a stronger relaxation. +TEST(ActivityBoundHelper, RemoveEnforcementThatCouldBeLiftedCase2) { + ActivityBoundHelper helper; + helper.AddAtMostOne({+1, +2, +3}); + + ConstraintProto ct; + ct.add_enforcement_literal(NegatedRef(1)); + std::vector> terms{{+2, 2}, {+3, 1}, {+4, 1}}; + + const int num_removed = helper.RemoveEnforcementThatMakesConstraintTrivial( + terms, Domain(0), Domain(0, 2), &ct); + EXPECT_EQ(num_removed, 1); + EXPECT_TRUE(ct.enforcement_literal().empty()); +} + +TEST(ClauseIsEnforcementImpliesLiteralTest, BasicTest) { + EXPECT_TRUE(ClauseIsEnforcementImpliesLiteral( + {+1, -5, +7, -9}, {NegatedRef(+1), NegatedRef(-5), NegatedRef(-9)}, +7)); +} + +LinearConstraintProto GetLinear(std::vector> terms) { + LinearConstraintProto result; + for (const auto [var, coeff] : terms) { + result.add_vars(var); + result.add_coeffs(coeff); + } + return result; +} + +TEST(FindSingleLinearDifferenceTest, TwoDiff1) { + LinearConstraintProto lin1 = GetLinear({{0, 1}, {1, 1}, {2, 1}}); + LinearConstraintProto lin2 = GetLinear({{0, 2}, {1, 1}, {2, 2}}); + int var1, var2; + int64_t coeff1, coeff2; + EXPECT_FALSE( + FindSingleLinearDifference(lin1, lin2, &var1, &coeff1, &var2, &coeff2)); + EXPECT_FALSE( + FindSingleLinearDifference(lin2, lin1, &var1, &coeff1, &var2, &coeff2)); +} + +TEST(FindSingleLinearDifferenceTest, TwoDiff2) { + LinearConstraintProto lin1 = GetLinear({{0, 1}, {1, 1}, {3, 1}}); + LinearConstraintProto lin2 = GetLinear({{0, 2}, {1, 1}, {2, 1}}); + int var1, var2; + int64_t coeff1, coeff2; + EXPECT_FALSE( + FindSingleLinearDifference(lin1, lin2, &var1, &coeff1, &var2, &coeff2)); + EXPECT_FALSE( + FindSingleLinearDifference(lin2, lin1, &var1, &coeff1, &var2, &coeff2)); +} + +TEST(FindSingleLinearDifferenceTest, OkNotSameVariable) { + LinearConstraintProto lin1 = GetLinear({{0, 1}, {1, 1}, {3, 1}}); + LinearConstraintProto lin2 = GetLinear({{0, 1}, {2, 1}, {3, 1}}); + int var1, var2; + int64_t coeff1, coeff2; + EXPECT_TRUE( + FindSingleLinearDifference(lin2, lin1, &var1, &coeff1, &var2, &coeff2)); + EXPECT_TRUE( + FindSingleLinearDifference(lin1, lin2, &var1, &coeff1, &var2, &coeff2)); + EXPECT_EQ(var1, 1); + EXPECT_EQ(coeff1, 1); + EXPECT_EQ(var2, 2); + EXPECT_EQ(coeff2, 1); +} + +TEST(FindSingleLinearDifferenceTest, OkNotSameCoeff) { + LinearConstraintProto lin1 = GetLinear({{0, 1}, {1, 1}, {3, 1}}); + LinearConstraintProto lin2 = GetLinear({{0, 1}, {1, 3}, {3, 1}}); + int var1, var2; + int64_t coeff1, coeff2; + EXPECT_TRUE( + FindSingleLinearDifference(lin2, lin1, &var1, &coeff1, &var2, &coeff2)); + EXPECT_TRUE( + FindSingleLinearDifference(lin1, lin2, &var1, &coeff1, &var2, &coeff2)); + EXPECT_EQ(var1, 1); + EXPECT_EQ(coeff1, 1); + EXPECT_EQ(var2, 1); + EXPECT_EQ(coeff2, 3); +} + +TEST(FindSingleLinearDifferenceTest, OkNotSamePosition) { + LinearConstraintProto lin1 = GetLinear({{0, 1}, {3, 1}, {5, 1}}); + LinearConstraintProto lin2 = GetLinear({{0, 1}, {1, 3}, {3, 1}}); + int var1, var2; + int64_t coeff1, coeff2; + EXPECT_TRUE( + FindSingleLinearDifference(lin2, lin1, &var1, &coeff1, &var2, &coeff2)); + EXPECT_TRUE( + FindSingleLinearDifference(lin1, lin2, &var1, &coeff1, &var2, &coeff2)); + EXPECT_EQ(var1, 5); + EXPECT_EQ(coeff1, 1); + EXPECT_EQ(var2, 1); + EXPECT_EQ(coeff2, 3); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/rins_test.cc b/ortools/sat/rins_test.cc new file mode 100644 index 00000000000..e08921c1b19 --- /dev/null +++ b/ortools/sat/rins_test.cc @@ -0,0 +1,173 @@ +// Copyright 2010-2024 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/sat/rins.h" + +#include +#include + +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/base/parse_test_proto.h" +#include "ortools/sat/cp_model.pb.h" +#include "ortools/sat/cp_model_loader.h" +#include "ortools/sat/model.h" +#include "ortools/sat/synchronization.h" +#include "ortools/util/random_engine.h" + +namespace operations_research { +namespace sat { + +namespace { + +using ::google::protobuf::contrib::parse_proto::ParseTestProto; + +TEST(GetRinsRensNeighborhoodTest, GetRENSNeighborhood) { + CpModelProto proto = ParseTestProto(R"pb( + variables { name: 'x' domain: 0 domain: 10 } + variables { name: 'y' domain: 0 domain: 10 } + objective { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + } + )pb"); + + Model model; + LoadVariables(proto, /*view_all_booleans_as_integers=*/true, &model); + + SharedLPSolutionRepository lp_solutions(/*num_solutions_to_keep=*/1); + SharedIncompleteSolutionManager incomplete_solutions; + + // No solutions are recorded. + random_engine_t random; + const ReducedDomainNeighborhood empty_rins_neighborhood = + GetRinsRensNeighborhood( + /*response_manager=*/nullptr, &lp_solutions, &incomplete_solutions, + /*difficulty=*/1.0, random); + + EXPECT_EQ(empty_rins_neighborhood.reduced_domain_vars.size(), 0); + EXPECT_EQ(empty_rins_neighborhood.fixed_vars.size(), 0); + + // Add a lp solution. + lp_solutions.NewLPSolution({3.5, 5.0}); + lp_solutions.Synchronize(); + + const ReducedDomainNeighborhood rins_neighborhood = GetRinsRensNeighborhood( + /*response_manager=*/nullptr, &lp_solutions, &incomplete_solutions, + /*difficulty=*/0.5, random); + + EXPECT_EQ(rins_neighborhood.reduced_domain_vars.size(), 1); + EXPECT_EQ(rins_neighborhood.reduced_domain_vars[0].first, 0); + EXPECT_EQ(rins_neighborhood.reduced_domain_vars[0].second.first, 3); + EXPECT_EQ(rins_neighborhood.reduced_domain_vars[0].second.second, 4); + + EXPECT_EQ(rins_neighborhood.fixed_vars.size(), 1); + EXPECT_EQ(rins_neighborhood.fixed_vars[0].first, 1); + EXPECT_EQ(rins_neighborhood.fixed_vars[0].second, 5); +} + +TEST(GetRinsRensNeighborhoodTest, GetRENSNeighborhoodIncomplete) { + CpModelProto proto = ParseTestProto(R"pb( + variables { name: 'x' domain: 0 domain: 10 } + variables { name: 'y' domain: 0 domain: 10 } + objective { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + } + )pb"); + + Model model; + LoadVariables(proto, /*view_all_booleans_as_integers=*/true, &model); + + SharedLPSolutionRepository lp_solutions(/*num_solutions_to_keep=*/1); + SharedIncompleteSolutionManager incomplete_solutions; + + // No solutions are recorded. + random_engine_t random; + const ReducedDomainNeighborhood empty_rins_neighborhood = + GetRinsRensNeighborhood( + /*response_manager=*/nullptr, &lp_solutions, &incomplete_solutions, + /*difficulty=*/1.0, random); + + EXPECT_EQ(empty_rins_neighborhood.reduced_domain_vars.size(), 0); + EXPECT_EQ(empty_rins_neighborhood.fixed_vars.size(), 0); + + // Add a incomplete solution. + incomplete_solutions.AddSolution({4.0, 5.0}); + + const ReducedDomainNeighborhood rins_neighborhood = GetRinsRensNeighborhood( + /*response_manager=*/nullptr, &lp_solutions, &incomplete_solutions, + /*difficulty=*/0.0, random); + + EXPECT_EQ(rins_neighborhood.fixed_vars.size(), 2); + const int pos_0 = rins_neighborhood.fixed_vars[0].first == 0 ? 0 : 1; + const int pos_1 = 1 - pos_0; + EXPECT_EQ(rins_neighborhood.fixed_vars[pos_0].first, 0); + EXPECT_EQ(rins_neighborhood.fixed_vars[pos_0].second, 4); + + EXPECT_EQ(rins_neighborhood.fixed_vars[pos_1].first, 1); + EXPECT_EQ(rins_neighborhood.fixed_vars[pos_1].second, 5); +} + +TEST(GetRinsRensNeighborhoodTest, GetRinsRensNeighborhoodLP) { + const CpModelProto proto = ParseTestProto(R"pb( + variables { name: 'x' domain: 0 domain: 10 } + variables { name: 'y' domain: 0 domain: 10 } + objective { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + } + )pb"); + + Model model; + LoadVariables(proto, /*view_all_booleans_as_integers=*/true, &model); + + auto* shared_response_manager = model.GetOrCreate(); + shared_response_manager->InitializeObjective(proto); + SharedLPSolutionRepository lp_solutions(/*num_solutions_to_keep=*/1); + SharedIncompleteSolutionManager incomplete_solutions; + + // No solutions are recorded. + random_engine_t random; + const ReducedDomainNeighborhood empty_rins_neighborhood = + GetRinsRensNeighborhood(shared_response_manager, &lp_solutions, + &incomplete_solutions, + /*difficulty=*/0.5, random); + + EXPECT_EQ(empty_rins_neighborhood.reduced_domain_vars.size(), 0); + EXPECT_EQ(empty_rins_neighborhood.fixed_vars.size(), 0); + + // Add a lp solution. + lp_solutions.NewLPSolution({3.5, 5}); + lp_solutions.Synchronize(); + // Add a solution. + CpSolverResponse solution; + solution.add_solution(4); + solution.add_solution(5); + shared_response_manager->NewSolution(solution.solution(), + solution.solution_info(), &model); + shared_response_manager->MutableSolutionsRepository()->Synchronize(); + + const ReducedDomainNeighborhood rins_neighborhood = GetRinsRensNeighborhood( + shared_response_manager, &lp_solutions, &incomplete_solutions, + /*difficulty=*/0.5, random); + + EXPECT_EQ(rins_neighborhood.reduced_domain_vars.size(), 0); + EXPECT_EQ(rins_neighborhood.fixed_vars.size(), 1); + EXPECT_EQ(rins_neighborhood.fixed_vars[0].first, 1); + EXPECT_EQ(rins_neighborhood.fixed_vars[0].second, 5); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/table_test.cc b/ortools/sat/table_test.cc new file mode 100644 index 00000000000..04555f6524b --- /dev/null +++ b/ortools/sat/table_test.cc @@ -0,0 +1,603 @@ +// Copyright 2010-2024 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/sat/table.h" + +#include +#include +#include + +#include "absl/container/btree_set.h" +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/base/container_logging.h" +#include "ortools/base/gmock.h" +#include "ortools/base/logging.h" +#include "ortools/base/parse_test_proto.h" +#include "ortools/sat/cp_model.h" +#include "ortools/sat/cp_model.pb.h" +#include "ortools/sat/cp_model_solver.h" +#include "ortools/sat/integer.h" +#include "ortools/sat/model.h" +#include "ortools/sat/sat_base.h" +#include "ortools/sat/sat_parameters.pb.h" +#include "ortools/sat/sat_solver.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::google::protobuf::contrib::parse_proto::ParseTestProto; + +TEST(TableConstraintTest, EnumerationAndEncoding) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { + name: "X1" + domain: [ 0, 4 ] + } + variables { + name: "X3" + domain: [ 0, 4 ] + } + variables { + name: "X2" + domain: [ 0, 4 ] + } + variables { + name: "X4" + domain: [ 0, 4 ] + } + constraints { table { vars: 0 vars: 2 values: 0 values: 1 } } + constraints { table { vars: 1 vars: 3 values: 4 values: 0 } } + constraints { table { vars: 2 vars: 1 values: 1 values: 4 } } + )pb"); + + Model model; + model.Add(NewSatParameters("enumerate_all_solutions:true")); + int count = 0; + model.Add( + NewFeasibleSolutionObserver([&count](const CpSolverResponse& response) { + LOG(INFO) << gtl::LogContainer(response.solution()); + ++count; + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + + // There should be just one solution [0, 4, 1, 0], but the solver used to + // report more because of extra "free" variable used in the encoding. + EXPECT_EQ(count, 1); +} + +TEST(TableConstraintTest, EnumerationAndEncodingTwoVars) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { + name: "X1" + domain: [ 0, 4 ] + } + variables { + name: "X3" + domain: [ 0, 4 ] + } + constraints { + table { + vars: [ 0, 1 ] + values: [ 0, 0, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4 ] + } + } + )pb"); + + Model model; + model.Add(NewSatParameters("enumerate_all_solutions:true")); + int count = 0; + model.Add( + NewFeasibleSolutionObserver([&count](const CpSolverResponse& response) { + LOG(INFO) << gtl::LogContainer(response.solution()); + ++count; + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(count, 7); +} + +TEST(TableConstraintTest, EnumerationAndEncodingFullPrefix) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + constraints { + table { + vars: [ 0, 1, 2 ] + values: [ + 0, 0, 0, 0, 1, 1, 0, 2, 2, 1, 0, 1, 1, 1, + 2, 1, 2, 0, 2, 0, 2, 2, 1, 0, 2, 2, 1 + ] + } + } + )pb"); + + Model model; + model.Add(NewSatParameters("enumerate_all_solutions:true")); + int count = 0; + model.Add( + NewFeasibleSolutionObserver([&count](const CpSolverResponse& response) { + LOG(INFO) << gtl::LogContainer(response.solution()); + ++count; + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + + EXPECT_EQ(count, 9); +} + +TEST(TableConstraintTest, EnumerationAndEncodingPartialPrefix) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + constraints { + table { + vars: [ 0, 1, 2 ] + values: [ + 0, 0, 0, 0, 2, 2, 1, 0, 1, 1, 1, 2, 1, 2, 0, 2, 0, 2, 2, 1, 0 + ] + } + } + )pb"); + + Model model; + model.Add(NewSatParameters("enumerate_all_solutions:true")); + int count = 0; + model.Add( + NewFeasibleSolutionObserver([&count](const CpSolverResponse& response) { + LOG(INFO) << gtl::LogContainer(response.solution()); + ++count; + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + + EXPECT_EQ(count, 7); +} + +TEST(TableConstraintTest, EnumerationAndEncodingInvalidTuples) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + variables { domain: [ 0, 2 ] } + constraints { + table { + vars: [ 0, 1, 2 ] + values: [ + 0, 0, 4, 0, 2, 2, 1, 0, 1, 1, 1, 2, 1, 2, 0, 2, 0, 2, 2, 1, 4 + ] + } + } + )pb"); + + Model model; + model.Add(NewSatParameters("enumerate_all_solutions:true")); + int count = 0; + model.Add( + NewFeasibleSolutionObserver([&count](const CpSolverResponse& response) { + LOG(INFO) << gtl::LogContainer(response.solution()); + ++count; + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + + // There should be exactly one solution per valid tuple. + EXPECT_EQ(count, 5); +} + +TEST(TableConstraintTest, EnumerationAndEncodingOneTupleWithAny) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 3 ] } + variables { domain: [ 0, 3 ] } + variables { domain: [ 0, 3 ] } + constraints { + table { + vars: [ 0, 1, 2 ] + values: [ 1, 0, 2, 1, 1, 2, 1, 2, 2 ] + } + } + )pb"); + + Model model; + model.Add(NewSatParameters("enumerate_all_solutions:true")); + int count = 0; + model.Add( + NewFeasibleSolutionObserver([&count](const CpSolverResponse& response) { + LOG(INFO) << gtl::LogContainer(response.solution()); + ++count; + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + + EXPECT_EQ(count, 3); +} + +TEST(TableConstraintTest, EnumerationAndEncodingPrefixWithLargeNegatedPart) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 5 ] } + variables { domain: [ 0, 5 ] } + variables { domain: [ 0, 5 ] } + constraints { + table { + vars: [ 0, 1, 2 ] + values: [ 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5 ] + } + } + )pb"); + + Model model; + model.Add(NewSatParameters("enumerate_all_solutions:true")); + int count = 0; + model.Add( + NewFeasibleSolutionObserver([&count](const CpSolverResponse& response) { + LOG(INFO) << gtl::LogContainer(response.solution()); + ++count; + })); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + + EXPECT_EQ(count, 6); +} + +TEST(TableConstraintTest, UnsatTable) { + const CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 4 ] } + variables { domain: [ 5, 9 ] } + constraints { table { vars: 0 vars: 1 values: 3 values: 3 } } + )pb"); + + Model model; + model.Add(NewSatParameters("cp_model_presolve:false")); + const CpSolverResponse response = SolveCpModel(model_proto, &model); + EXPECT_EQ(response.status(), CpSolverStatus::INFEASIBLE); +} + +TEST(NegatedTableConstraintTest, BasicTest) { + CpModelBuilder cp_model; + std::vector vars; + vars.push_back(cp_model.NewIntVar({1, 2})); + vars.push_back(cp_model.NewIntVar({1, 3})); + vars.push_back(cp_model.NewIntVar({1, 3})); + + TableConstraint table = cp_model.AddForbiddenAssignments(vars); + table.AddTuple({1, 2, 1}); + table.AddTuple({1, 2, 3}); + table.AddTuple({2, 2, 1}); + + Model model; + absl::btree_set> solutions; + model.Add(NewFeasibleSolutionObserver([&](const CpSolverResponse& r) { + std::vector solution; + for (const IntVar var : vars) { + solution.push_back(SolutionIntegerValue(r, var)); + } + solutions.insert(solution); + })); + + // Tell the solver to enumerate all solutions. + SatParameters parameters; + parameters.set_enumerate_all_solutions(true); + model.Add(NewSatParameters(parameters)); + const CpSolverResponse response = SolveCpModel(cp_model.Build(), &model); + + absl::btree_set> expected{{1, 1, 1}, + {1, 1, 2}, + {1, 1, 3}, + // {1, 2, 1}, + {1, 2, 2}, + // {1, 2, 3}, + {1, 3, 1}, + {1, 3, 2}, + {1, 3, 3}, + {2, 1, 1}, + {2, 1, 2}, + {2, 1, 3}, + // {2, 2, 1}, + {2, 2, 2}, + {2, 2, 3}, + {2, 3, 1}, + {2, 3, 2}, + {2, 3, 3}}; + EXPECT_EQ(solutions, expected); +} + +TEST(AutomatonTest, TestAutomaton) { + const int kNumVars = 4; + CpModelBuilder cp_model; + std::vector variables; + for (int i = 0; i < kNumVars; ++i) { + variables.push_back(IntVar(cp_model.NewBoolVar())); + } + + AutomatonConstraint automaton = cp_model.AddAutomaton(variables, 0, {3}); + automaton.AddTransition(0, 1, 0L); + automaton.AddTransition(0, 2, 1L); + automaton.AddTransition(1, 1, 0L); + automaton.AddTransition(2, 2, 1L); + automaton.AddTransition(1, 3, 1L); + automaton.AddTransition(2, 3, 0L); + const CpModelProto expected_model = ParseTestProto(R"pb( + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + variables { domain: 0 domain: 1 } + constraints { + automaton { + final_states: 3 + transition_tail: 0 + transition_tail: 0 + transition_tail: 1 + transition_tail: 2 + transition_tail: 1 + transition_tail: 2 + transition_head: 1 + transition_head: 2 + transition_head: 1 + transition_head: 2 + transition_head: 3 + transition_head: 3 + transition_label: 0 + transition_label: 1 + transition_label: 0 + transition_label: 1 + transition_label: 1 + transition_label: 0 + vars: 0 + vars: 1 + vars: 2 + vars: 3 + } + } + )pb"); + EXPECT_THAT(cp_model.Proto(), testing::EqualsProto(expected_model)); + + Model model; + int num_solutions = 0; + model.Add(NewFeasibleSolutionObserver([&](const CpSolverResponse& r) { + num_solutions++; + EXPECT_EQ(r.solution(0), r.solution(1)); + EXPECT_EQ(r.solution(0), r.solution(2)); + EXPECT_NE(r.solution(0), r.solution(3)); + })); + + // Tell the solver to enumerate all solutions. + SatParameters parameters; + parameters.set_enumerate_all_solutions(true); + model.Add(NewSatParameters(parameters)); + + SolveCpModel(cp_model.Build(), &model); + EXPECT_EQ(num_solutions, 2); +} + +TEST(AutomatonTest, LoopingAutomatonMultipleFinalStates) { + CpModelBuilder cp_model; + std::vector variables; + for (int i = 0; i < 10; ++i) { + variables.push_back(cp_model.NewIntVar({0, 10})); + } + + // These tuples accept "0*(12)+0*". + AutomatonConstraint automaton = cp_model.AddAutomaton(variables, 1, {3, 4}); + automaton.AddTransition(1, 1, 0); + automaton.AddTransition(1, 2, 1); + automaton.AddTransition(2, 3, 2); + automaton.AddTransition(3, 2, 1); + automaton.AddTransition(3, 4, 0); + automaton.AddTransition(4, 4, 0); + + Model model; + absl::btree_set> solutions; + model.Add(NewFeasibleSolutionObserver([&](const CpSolverResponse& r) { + std::vector solution; + for (const IntVar var : variables) { + solution.push_back(SolutionIntegerValue(r, var)); + } + solutions.insert(solution); + })); + + // Tell the solver to enumerate all solutions. + SatParameters parameters; + parameters.set_enumerate_all_solutions(true); + model.Add(NewSatParameters(parameters)); + const CpSolverResponse response = SolveCpModel(cp_model.Build(), &model); + + absl::btree_set> expected{ + {0, 0, 0, 0, 0, 0, 0, 0, 1, 2}, {0, 0, 0, 0, 0, 0, 0, 1, 2, 0}, + {0, 0, 0, 0, 0, 0, 1, 2, 0, 0}, {0, 0, 0, 0, 0, 0, 1, 2, 1, 2}, + {0, 0, 0, 0, 0, 1, 2, 0, 0, 0}, {0, 0, 0, 0, 0, 1, 2, 1, 2, 0}, + {0, 0, 0, 0, 1, 2, 0, 0, 0, 0}, {0, 0, 0, 0, 1, 2, 1, 2, 0, 0}, + {0, 0, 0, 0, 1, 2, 1, 2, 1, 2}, {0, 0, 0, 1, 2, 0, 0, 0, 0, 0}, + {0, 0, 0, 1, 2, 1, 2, 0, 0, 0}, {0, 0, 0, 1, 2, 1, 2, 1, 2, 0}, + {0, 0, 1, 2, 0, 0, 0, 0, 0, 0}, {0, 0, 1, 2, 1, 2, 0, 0, 0, 0}, + {0, 0, 1, 2, 1, 2, 1, 2, 0, 0}, {0, 0, 1, 2, 1, 2, 1, 2, 1, 2}, + {0, 1, 2, 0, 0, 0, 0, 0, 0, 0}, {0, 1, 2, 1, 2, 0, 0, 0, 0, 0}, + {0, 1, 2, 1, 2, 1, 2, 0, 0, 0}, {0, 1, 2, 1, 2, 1, 2, 1, 2, 0}, + {1, 2, 0, 0, 0, 0, 0, 0, 0, 0}, {1, 2, 1, 2, 0, 0, 0, 0, 0, 0}, + {1, 2, 1, 2, 1, 2, 0, 0, 0, 0}, {1, 2, 1, 2, 1, 2, 1, 2, 0, 0}, + {1, 2, 1, 2, 1, 2, 1, 2, 1, 2}}; + EXPECT_EQ(solutions, expected); +} + +TEST(AutomatonTest, NonogramRule) { + CpModelBuilder cp_model; + std::vector variables; + for (int i = 0; i < 10; ++i) { + variables.push_back(cp_model.NewIntVar({0, 10})); + } + + // Accept sequences with 3 '1', then 2 '1', then 1 '1', separated by at least + // one '0'. + AutomatonConstraint automaton = cp_model.AddAutomaton(variables, 1, {9}); + automaton.AddTransition(1, 1, 0); + automaton.AddTransition(1, 2, 1); + automaton.AddTransition(2, 3, 1); + automaton.AddTransition(3, 4, 1); + automaton.AddTransition(4, 5, 0); + automaton.AddTransition(5, 5, 0); + automaton.AddTransition(5, 6, 1); + automaton.AddTransition(6, 7, 1); + automaton.AddTransition(7, 8, 0); + automaton.AddTransition(8, 8, 0); + automaton.AddTransition(8, 9, 1); + automaton.AddTransition(9, 9, 0); + + Model model; + absl::btree_set> solutions; + model.Add(NewFeasibleSolutionObserver([&](const CpSolverResponse& r) { + std::vector solution; + for (const IntVar var : variables) { + solution.push_back(SolutionIntegerValue(r, var)); + } + solutions.insert(solution); + })); + + // Tell the solver to enumerate all solutions. + SatParameters parameters; + parameters.set_enumerate_all_solutions(true); + model.Add(NewSatParameters(parameters)); + const CpSolverResponse response = SolveCpModel(cp_model.Build(), &model); + + absl::btree_set> expected{ + {0, 0, 1, 1, 1, 0, 1, 1, 0, 1}, {0, 1, 1, 1, 0, 0, 1, 1, 0, 1}, + {0, 1, 1, 1, 0, 1, 1, 0, 0, 1}, {0, 1, 1, 1, 0, 1, 1, 0, 1, 0}, + {1, 1, 1, 0, 0, 0, 1, 1, 0, 1}, {1, 1, 1, 0, 0, 1, 1, 0, 0, 1}, + {1, 1, 1, 0, 0, 1, 1, 0, 1, 0}, {1, 1, 1, 0, 1, 1, 0, 0, 0, 1}, + {1, 1, 1, 0, 1, 1, 0, 0, 1, 0}, {1, 1, 1, 0, 1, 1, 0, 1, 0, 0}}; + EXPECT_EQ(solutions, expected); +} + +TEST(AutomatonTest, AnotherAutomaton) { + CpModelBuilder cp_model; + std::vector variables; + for (int i = 0; i < 7; ++i) { + variables.push_back(cp_model.NewIntVar({0, 10})); + } + + AutomatonConstraint automaton = + cp_model.AddAutomaton(variables, 1, {1, 2, 3, 4, 5, 6, 7}); + automaton.AddTransition(1, 2, 1); + automaton.AddTransition(1, 5, 2); + automaton.AddTransition(2, 3, 1); + automaton.AddTransition(2, 5, 2); + automaton.AddTransition(3, 4, 1); + automaton.AddTransition(3, 5, 2); + automaton.AddTransition(4, 0, 1); + automaton.AddTransition(4, 5, 2); + automaton.AddTransition(5, 2, 1); + automaton.AddTransition(5, 6, 2); + automaton.AddTransition(6, 2, 1); + automaton.AddTransition(6, 7, 2); + automaton.AddTransition(7, 2, 1); + automaton.AddTransition(7, 0, 2); + + Model model; + absl::btree_set> solutions; + model.Add(NewFeasibleSolutionObserver([&](const CpSolverResponse& r) { + std::vector solution; + for (const IntVar var : variables) { + solution.push_back(SolutionIntegerValue(r, var)); + } + solutions.insert(solution); + })); + + // Tell the solver to enumerate all solutions. + SatParameters parameters; + parameters.set_enumerate_all_solutions(true); + parameters.set_log_search_progress(true); + model.Add(NewSatParameters(parameters)); + const CpSolverResponse response = SolveCpModel(cp_model.Build(), &model); + + // Out of the 2**7 tuples, the one that contains 4 consecutive 1 are: + // - 1111??? (8) + // - 21111?? (4) + // - ?21111? (4) + // - ??21111 (4) + EXPECT_EQ(solutions.size(), 128 - 2 * 20); +} + +TEST(LiteralTableConstraint, PropagationFromLiterals) { + Model model; + std::vector selected; + for (int i = 0; i < 4; ++i) { + selected.push_back(Literal(model.Add(NewBooleanVariable()), true)); + } + std::vector> literals(3); + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + literals[i].push_back(Literal(model.Add(NewBooleanVariable()), true)); + } + model.Add(ExactlyOneConstraint(literals[i])); + } + + // Tuples (0, 0, 0), (1, 1, 1), (2, 2, 2), (0, 1, 2). + std::vector> tuples = { + {literals[0][0], literals[1][0], literals[2][0]}, + {literals[0][1], literals[1][1], literals[2][1]}, + {literals[0][2], literals[1][2], literals[2][2]}, + {literals[0][0], literals[1][1], literals[2][2]}}; + + model.Add(LiteralTableConstraint(tuples, selected)); + SatSolver* sat_solver = model.GetOrCreate(); + + EXPECT_TRUE(sat_solver->EnqueueDecisionIfNotConflicting(literals[0][0])); + EXPECT_TRUE(sat_solver->Propagate()); + EXPECT_TRUE(sat_solver->Assignment().LiteralIsFalse(selected[1])); + EXPECT_TRUE(sat_solver->Assignment().LiteralIsFalse(selected[2])); + + EXPECT_TRUE(sat_solver->EnqueueDecisionIfNotConflicting(literals[1][1])); + EXPECT_TRUE(sat_solver->Propagate()); + EXPECT_TRUE(sat_solver->Assignment().LiteralIsFalse(selected[0])); + EXPECT_TRUE(sat_solver->Assignment().LiteralIsTrue(selected[3])); +} + +TEST(LiteralTableConstraint, PropagationFromSelected) { + Model model; + std::vector selected; + for (int i = 0; i < 4; ++i) { + selected.push_back(Literal(model.Add(NewBooleanVariable()), true)); + } + std::vector> literals(3); + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + literals[i].push_back(Literal(model.Add(NewBooleanVariable()), true)); + } + model.Add(ExactlyOneConstraint(literals[i])); + } + + // Tuples (0, 0, 0), (1, 1, 1), (2, 2, 2), (0, 1, 2). + std::vector> tuples = { + {literals[0][0], literals[1][0], literals[2][0]}, + {literals[0][1], literals[1][1], literals[2][1]}, + {literals[0][2], literals[1][2], literals[2][2]}, + {literals[0][0], literals[1][1], literals[2][2]}}; + + model.Add(LiteralTableConstraint(tuples, selected)); + Trail* trail = model.GetOrCreate(); + SatSolver* sat_solver = model.GetOrCreate(); + + trail->EnqueueSearchDecision(selected[1].Negated()); + EXPECT_TRUE(sat_solver->Propagate()); + EXPECT_TRUE(sat_solver->Assignment().LiteralIsFalse(literals[0][1])); + EXPECT_TRUE(sat_solver->Assignment().LiteralIsFalse(literals[2][1])); + + trail->EnqueueSearchDecision(selected[3].Negated()); + EXPECT_TRUE(sat_solver->Propagate()); + EXPECT_TRUE(sat_solver->Assignment().LiteralIsFalse(literals[1][1])); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/var_domination_test.cc b/ortools/sat/var_domination_test.cc new file mode 100644 index 00000000000..b1c63997823 --- /dev/null +++ b/ortools/sat/var_domination_test.cc @@ -0,0 +1,614 @@ +// Copyright 2010-2024 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/sat/var_domination.h" + +#include + +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/parse_test_proto.h" +#include "ortools/sat/cp_model.pb.h" +#include "ortools/sat/integer.h" +#include "ortools/sat/model.h" +#include "ortools/sat/presolve_context.h" +#include "ortools/util/sorted_interval_list.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::google::protobuf::contrib::parse_proto::ParseTestProto; +using ::testing::ElementsAre; +using ::testing::IsEmpty; +using ::testing::UnorderedElementsAre; + +// X + 2Y + Z = 0 +// X + 2Z >= 2 +// +// Doing (X--, Z++) is always beneficial if possible. +TEST(VarDominationTest, BasicExample1) { + CpModelProto model_proto = ParseTestProto(R"pb( + variables { + name: "X" + domain: [ -10, 10 ] + } + variables { + name: "Y" + domain: [ -10, 10 ] + } + variables { + name: "Z" + domain: [ -10, 10 ] + } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 2, 1 ] + domain: [ 0, 0 ] + } + } + constraints { + linear { + vars: [ 0, 2 ] + coeffs: [ 1, 2 ] + domain: [ 2, 9223372036854775807 ] + } + } + )pb"); + VarDomination var_dom; + Model model; + PresolveContext context(&model, &model_proto, nullptr); + context.InitializeNewDomains(); + context.ReadObjectiveFromProto(); + ScanModelForDominanceDetection(context, &var_dom); + + const IntegerVariable X = VarDomination::RefToIntegerVariable(0); + const IntegerVariable Y = VarDomination::RefToIntegerVariable(1); + const IntegerVariable Z = VarDomination::RefToIntegerVariable(2); + EXPECT_THAT(var_dom.DominatingVariables(X), ElementsAre(Z)); + EXPECT_THAT(var_dom.DominatingVariables(Y), IsEmpty()); + EXPECT_THAT(var_dom.DominatingVariables(Z), IsEmpty()); + EXPECT_THAT(var_dom.DominatingVariables(NegationOf(X)), IsEmpty()); + EXPECT_THAT(var_dom.DominatingVariables(NegationOf(Y)), IsEmpty()); + EXPECT_THAT(var_dom.DominatingVariables(NegationOf(Z)), + ElementsAre(NegationOf(X))); +} + +// X + 2Y + Z = 0 +// X + 2Z >= 2 +// +// Doing (X--, Z++) is always beneficial if possible. +TEST(VarDominationTest, ExploitDominanceRelation) { + CpModelProto model_proto = ParseTestProto(R"pb( + variables { + name: "X" + domain: [ -10, 10 ] + } + variables { + name: "Y" + domain: [ -10, 10 ] + } + variables { + name: "Z" + domain: [ -10, 10 ] + } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 2, 1 ] + domain: [ 0, 0 ] + } + } + constraints { + linear { + vars: [ 0, 2 ] + coeffs: [ 1, 2 ] + domain: [ 2, 9223372036854775807 ] + } + } + )pb"); + VarDomination var_dom; + Model model; + PresolveContext context(&model, &model_proto, nullptr); + context.InitializeNewDomains(); + context.ReadObjectiveFromProto(); + context.UpdateNewConstraintsVariableUsage(); + ScanModelForDominanceDetection(context, &var_dom); + EXPECT_TRUE(ExploitDominanceRelations(var_dom, &context)); + + // Because X--, Z++ is always ok, we can exclude some value from Z using + // equation X + 2Z >=2 we see that if Z=5, X >= -8, so we can decrease it, + // but for Z = 6, X might be -10, so we are not sure. + // + // Also not that X can be 10 with Z at 10 too, so we cannot reduced the domain + // of X. + EXPECT_EQ(context.DomainOf(0).ToString(), "[-10,10]"); + EXPECT_EQ(context.DomainOf(1).ToString(), "[-10,10]"); + EXPECT_EQ(context.DomainOf(2).ToString(), "[6,10]"); +} + +// Same example as before but now Z has holes, which complicate a bit the +// final result. +TEST(VarDominationTest, ExploitDominanceRelationWithHoles) { + CpModelProto model_proto = ParseTestProto(R"pb( + variables { + name: "X" + domain: [ -10, 10 ] + } + variables { + name: "Y" + domain: [ -10, 10 ] + } + variables { + name: "Z" + domain: [ -10, 0, 7, 10 ] + } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 2, 1 ] + domain: [ 0, 0 ] + } + } + constraints { + linear { + vars: [ 0, 2 ] + coeffs: [ 1, 2 ] + domain: [ 2, 9223372036854775807 ] + } + } + )pb"); + VarDomination var_dom; + Model model; + PresolveContext context(&model, &model_proto, nullptr); + context.InitializeNewDomains(); + context.ReadObjectiveFromProto(); + context.UpdateNewConstraintsVariableUsage(); + ScanModelForDominanceDetection(context, &var_dom); + EXPECT_TRUE(ExploitDominanceRelations(var_dom, &context)); + + // With hole, if Z is 0, we will not be able to increase it up to 6, so we + // can't remove 0. If it is lower, we can safely increase it to zero though. + EXPECT_EQ(context.DomainOf(0).ToString(), "[-10,10]"); + EXPECT_EQ(context.DomainOf(1).ToString(), "[-10,10]"); + EXPECT_EQ(context.DomainOf(2).ToString(), "[0][7,10]"); +} + +// X + Y + Z = 0 +// X + 2 Z >= 2 +TEST(VarDominationTest, BasicExample1Variation) { + CpModelProto model_proto = ParseTestProto(R"pb( + variables { + name: "X" + domain: [ -10, 10 ] + } + variables { + name: "Y" + domain: [ -10, 10 ] + } + variables { + name: "Z" + domain: [ -10, 10 ] + } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 1, 1 ] + domain: [ 0, 0 ] + } + } + constraints { + linear { + vars: [ 0, 2 ] + coeffs: [ 1, 2 ] + domain: [ 2, 9223372036854775807 ] + } + } + )pb"); + VarDomination var_dom; + Model model; + PresolveContext context(&model, &model_proto, nullptr); + context.InitializeNewDomains(); + context.ReadObjectiveFromProto(); + ScanModelForDominanceDetection(context, &var_dom); + + const IntegerVariable X = VarDomination::RefToIntegerVariable(0); + const IntegerVariable Y = VarDomination::RefToIntegerVariable(1); + const IntegerVariable Z = VarDomination::RefToIntegerVariable(2); + EXPECT_THAT(var_dom.DominatingVariables(X), ElementsAre(Z)); + EXPECT_THAT(var_dom.DominatingVariables(Y), UnorderedElementsAre(X, Z)); + EXPECT_THAT(var_dom.DominatingVariables(Z), IsEmpty()); + + // TODO(user): Transpose is broken. + EXPECT_THAT(var_dom.DominatingVariables(NegationOf(X)), + ElementsAre(NegationOf(Y))); + EXPECT_THAT(var_dom.DominatingVariables(NegationOf(Y)), IsEmpty()); + EXPECT_THAT(var_dom.DominatingVariables(NegationOf(Z)), + UnorderedElementsAre(NegationOf(X), NegationOf(Y))); +} + +// X + Y + Z >= 0 +// Y + Z <= 0 +TEST(VarDominationTest, BasicExample2) { + CpModelProto model_proto = ParseTestProto(R"pb( + variables { + name: "X" + domain: [ -10, 10 ] + } + variables { + name: "Y" + domain: [ -10, 10 ] + } + variables { + name: "Z" + domain: [ -10, 10 ] + } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 1, 1 ] + domain: [ 0, 9223372036854775807 ] + } + } + constraints { + linear { + vars: [ 1, 2 ] + coeffs: [ 1, 1 ] + domain: [ -9223372036854775808, 0 ] + } + } + )pb"); + VarDomination var_dom; + Model model; + PresolveContext context(&model, &model_proto, nullptr); + context.InitializeNewDomains(); + context.ReadObjectiveFromProto(); + ScanModelForDominanceDetection(context, &var_dom); + + const IntegerVariable X = VarDomination::RefToIntegerVariable(0); + const IntegerVariable Y = VarDomination::RefToIntegerVariable(1); + const IntegerVariable Z = VarDomination::RefToIntegerVariable(2); + + EXPECT_FALSE(var_dom.CanFreelyDecrease(X)); + EXPECT_THAT(var_dom.DominatingVariables(X), IsEmpty()); + EXPECT_TRUE(var_dom.CanFreelyDecrease(NegationOf(X))); + + // We do not include X in these lists, because X++ can always happen. + EXPECT_THAT(var_dom.DominatingVariables(Y), UnorderedElementsAre(Z)); + EXPECT_THAT(var_dom.DominatingVariables(Z), UnorderedElementsAre(Y)); + EXPECT_THAT(var_dom.DominatingVariables(NegationOf(Y)), + ElementsAre(NegationOf(Z))); + EXPECT_THAT(var_dom.DominatingVariables(NegationOf(Z)), + ElementsAre(NegationOf(Y))); +} + +// X + Y <= 0 +// Y + Z <= 0 +TEST(VarDominationTest, BasicExample3) { + CpModelProto model_proto = ParseTestProto(R"pb( + variables { + name: "X" + domain: [ -10, 10 ] + } + variables { + name: "Y" + domain: [ -10, 10 ] + } + variables { + name: "Z" + domain: [ -10, 10 ] + } + constraints { + linear { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + domain: [ -9223372036854775808, 0 ] + } + } + constraints { + linear { + vars: [ 1, 2 ] + coeffs: [ 1, 1 ] + domain: [ -9223372036854775808, 0 ] + } + } + )pb"); + VarDomination var_dom; + Model model; + PresolveContext context(&model, &model_proto, nullptr); + context.InitializeNewDomains(); + context.ReadObjectiveFromProto(); + ScanModelForDominanceDetection(context, &var_dom); + + const IntegerVariable X = VarDomination::RefToIntegerVariable(0); + const IntegerVariable Y = VarDomination::RefToIntegerVariable(1); + const IntegerVariable Z = VarDomination::RefToIntegerVariable(2); + + EXPECT_TRUE(var_dom.CanFreelyDecrease(X)); + EXPECT_TRUE(var_dom.CanFreelyDecrease(Y)); + EXPECT_TRUE(var_dom.CanFreelyDecrease(Z)); + EXPECT_FALSE(var_dom.CanFreelyDecrease(NegationOf(X))); + EXPECT_FALSE(var_dom.CanFreelyDecrease(NegationOf(Y))); + EXPECT_FALSE(var_dom.CanFreelyDecrease(NegationOf(Z))); + + // No domination here, because all the dominator can just freely move in + // one direction. + EXPECT_THAT(var_dom.DominatingVariables(NegationOf(X)), IsEmpty()); + EXPECT_THAT(var_dom.DominatingVariables(NegationOf(Y)), IsEmpty()); + EXPECT_THAT(var_dom.DominatingVariables(NegationOf(Z)), IsEmpty()); +} + +// X + Y >= 0 +// Y + Z >= 0 +TEST(VarDominationTest, BasicExample4) { + CpModelProto model_proto = ParseTestProto(R"pb( + variables { + name: "X" + domain: [ -10, 10 ] + } + variables { + name: "Y" + domain: [ -10, 10 ] + } + variables { + name: "Z" + domain: [ -10, 10 ] + } + constraints { + linear { + vars: [ 0, 1 ] + coeffs: [ 1, 1 ] + domain: [ 0, 9223372036854775807 ] + } + } + constraints { + linear { + vars: [ 1, 2 ] + coeffs: [ 1, 1 ] + domain: [ 0, 9223372036854775807 ] + } + } + )pb"); + VarDomination var_dom; + Model model; + PresolveContext context(&model, &model_proto, nullptr); + context.InitializeNewDomains(); + context.ReadObjectiveFromProto(); + ScanModelForDominanceDetection(context, &var_dom); + + const IntegerVariable X = VarDomination::RefToIntegerVariable(0); + const IntegerVariable Y = VarDomination::RefToIntegerVariable(1); + const IntegerVariable Z = VarDomination::RefToIntegerVariable(2); + + EXPECT_FALSE(var_dom.CanFreelyDecrease(X)); + EXPECT_FALSE(var_dom.CanFreelyDecrease(Y)); + EXPECT_FALSE(var_dom.CanFreelyDecrease(Z)); + EXPECT_TRUE(var_dom.CanFreelyDecrease(NegationOf(X))); + EXPECT_TRUE(var_dom.CanFreelyDecrease(NegationOf(Y))); + EXPECT_TRUE(var_dom.CanFreelyDecrease(NegationOf(Z))); + + EXPECT_THAT(var_dom.DominatingVariables(X), IsEmpty()); + EXPECT_THAT(var_dom.DominatingVariables(Y), IsEmpty()); + EXPECT_THAT(var_dom.DominatingVariables(Z), IsEmpty()); +} + +// X + Y + Z = 0 +TEST(VarDominationTest, AllEquivalent) { + CpModelProto model_proto = ParseTestProto(R"pb( + variables { + name: "X" + domain: [ -10, 10 ] + } + variables { + name: "Y" + domain: [ -10, 10 ] + } + variables { + name: "Z" + domain: [ -10, 10 ] + } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 1, 1 ] + domain: [ 0, 0 ] + } + } + )pb"); + VarDomination var_dom; + Model model; + PresolveContext context(&model, &model_proto, nullptr); + context.InitializeNewDomains(); + context.ReadObjectiveFromProto(); + ScanModelForDominanceDetection(context, &var_dom); + + // Domination is slightly related to symmetry and duplicate columns. + const IntegerVariable X = VarDomination::RefToIntegerVariable(0); + const IntegerVariable Y = VarDomination::RefToIntegerVariable(1); + const IntegerVariable Z = VarDomination::RefToIntegerVariable(2); + EXPECT_THAT(var_dom.DominatingVariables(X), UnorderedElementsAre(Y, Z)); + EXPECT_THAT(var_dom.DominatingVariables(Y), UnorderedElementsAre(X, Z)); + EXPECT_THAT(var_dom.DominatingVariables(Z), UnorderedElementsAre(X, Y)); +} + +// X + Y + Z <= 0 (to prevent freely moving variables). +// -X + -2Y + -3Z <= 0 +TEST(VarDominationTest, NegativeCoefficients) { + CpModelProto model_proto = ParseTestProto(R"pb( + variables { + name: "X" + domain: [ -10, 10 ] + } + variables { + name: "Y" + domain: [ -10, 10 ] + } + variables { + name: "Z" + domain: [ -10, 10 ] + } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 1, 1 ] + domain: [ -9223372036854775808, 0 ] + } + } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ -1, -2, -3 ] + domain: [ -9223372036854775808, 0 ] + } + } + )pb"); + VarDomination var_dom; + Model model; + PresolveContext context(&model, &model_proto, nullptr); + context.InitializeNewDomains(); + context.ReadObjectiveFromProto(); + ScanModelForDominanceDetection(context, &var_dom); + + const IntegerVariable X = VarDomination::RefToIntegerVariable(0); + const IntegerVariable Y = VarDomination::RefToIntegerVariable(1); + const IntegerVariable Z = VarDomination::RefToIntegerVariable(2); + EXPECT_THAT(var_dom.DominatingVariables(X), UnorderedElementsAre(Y, Z)); + EXPECT_THAT(var_dom.DominatingVariables(Y), UnorderedElementsAre(Z)); + EXPECT_THAT(var_dom.DominatingVariables(Z), IsEmpty()); +} + +// Bound propagation see nothing, but if we can remove feasible solution, from +// this constraint point of view, all variables can freely increase or decrease +// until zero (because the constraint is trivial above/below). +// +// -20 <= X + Y + Z <= 20 +TEST(DualBoundReductionTest, BasicTest) { + CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ -10, 10 ] } + variables { domain: [ -10, 10 ] } + variables { domain: [ -10, 10 ] } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 1, 1 ] + domain: [ -20, 20 ] + } + } + )pb"); + DualBoundStrengthening dual_bound_strengthening; + Model model; + PresolveContext context(&model, &model_proto, nullptr); + context.InitializeNewDomains(); + context.ReadObjectiveFromProto(); + ScanModelForDualBoundStrengthening(context, &dual_bound_strengthening); + EXPECT_TRUE(dual_bound_strengthening.Strengthen(&context)); + EXPECT_EQ(context.DomainOf(0).ToString(), "[0]"); + EXPECT_EQ(context.DomainOf(1).ToString(), "[0]"); + EXPECT_EQ(context.DomainOf(2).ToString(), "[0]"); +} + +TEST(DualBoundReductionTest, CarefulWithHoles) { + CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ -10, 10 ] } + variables { domain: [ -10, 0, 7, 10 ] } + variables { domain: [ -10, -6, 3, 10 ] } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 1, 1 ] + domain: [ -15, 15 ] + } + } + )pb"); + DualBoundStrengthening dual_bound_strengthening; + Model model; + PresolveContext context(&model, &model_proto, nullptr); + context.InitializeNewDomains(); + context.ReadObjectiveFromProto(); + ScanModelForDualBoundStrengthening(context, &dual_bound_strengthening); + EXPECT_TRUE(dual_bound_strengthening.Strengthen(&context)); + EXPECT_EQ(context.DomainOf(0).ToString(), "[-5,5]"); + EXPECT_EQ(context.DomainOf(1).ToString(), "[-5,0][7]"); + EXPECT_EQ(context.DomainOf(2).ToString(), "[-6][3,5]"); +} + +// Here the infered bounds crosses, so we have multiple choices, we will fix +// to the lowest magnitude. +TEST(DualBoundReductionTest, Choices) { + CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ -10, 10 ] } + variables { domain: [ -10, -2, 3, 10 ] } + variables { domain: [ -10, -3, 2, 10 ] } + constraints { + linear { + vars: [ 0, 1, 2 ] + coeffs: [ 1, 1, 1 ] + domain: [ -25, 25 ] + } + } + )pb"); + DualBoundStrengthening dual_bound_strengthening; + Model model; + PresolveContext context(&model, &model_proto, nullptr); + context.InitializeNewDomains(); + context.ReadObjectiveFromProto(); + ScanModelForDualBoundStrengthening(context, &dual_bound_strengthening); + EXPECT_TRUE(dual_bound_strengthening.Strengthen(&context)); + EXPECT_EQ(context.DomainOf(0).ToString(), "[0]"); + EXPECT_EQ(context.DomainOf(1).ToString(), "[-2]"); + EXPECT_EQ(context.DomainOf(2).ToString(), "[2]"); +} + +TEST(DualBoundReductionTest, EquivalenceDetection) { + CpModelProto model_proto = ParseTestProto(R"pb( + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + variables { domain: [ 0, 1 ] } + constraints { + # a => b + enforcement_literal: 0 + bool_and { literals: [ 1 ] } + } + constraints { + linear { + # b == c (we just want b and c not to freely vary) + vars: [ 1, 2 ] + coeffs: [ 1, -1 ] + domain: [ 0, 0 ] + } + } + constraints { + bool_or { + literals: [ 0, 2 ] # a + c >= 0 + } + } + )pb"); + DualBoundStrengthening dual_bound_strengthening; + Model model; + PresolveContext context(&model, &model_proto, nullptr); + context.InitializeNewDomains(); + context.ReadObjectiveFromProto(); + ScanModelForDualBoundStrengthening(context, &dual_bound_strengthening); + EXPECT_TRUE(dual_bound_strengthening.Strengthen(&context)); + EXPECT_EQ(context.DomainOf(0).ToString(), "[0,1]"); + EXPECT_EQ(context.DomainOf(1).ToString(), "[0,1]"); + EXPECT_EQ(context.DomainOf(2).ToString(), "[0,1]"); + + // Equivalence between a and b. + EXPECT_EQ(context.GetLiteralRepresentative(1), 0); +} + +} // namespace +} // namespace sat +} // namespace operations_research From bcbe132cb637fb68c418de1d54719053aa84a301 Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Thu, 3 Oct 2024 11:00:02 +0200 Subject: [PATCH 05/11] graph: export from google3 --- ortools/graph/BUILD.bazel | 9 +++++++-- ortools/graph/assignment_test.cc | 1 + ortools/graph/christofides_test.cc | 14 ++++++-------- ortools/graph/cliques_test.cc | 9 +++++---- ortools/graph/min_cost_flow_test.cc | 8 ++++++-- ortools/graph/one_tree_lower_bound_test.cc | 14 +++++++------- ortools/graph/samples/assignment_min_flow.py | 15 +++++++-------- .../graph/samples/simple_min_cost_flow_program.py | 3 ++- 8 files changed, 41 insertions(+), 32 deletions(-) diff --git a/ortools/graph/BUILD.bazel b/ortools/graph/BUILD.bazel index 0d1be0e5a53..45811e3633b 100644 --- a/ortools/graph/BUILD.bazel +++ b/ortools/graph/BUILD.bazel @@ -178,10 +178,11 @@ cc_test( deps = [ ":cliques", "//ortools/base:gmock_main", - "//ortools/base:mathutil", "//ortools/util:time_limit", "@com_google_absl//absl/container:flat_hash_set", + "@com_google_absl//absl/flags:flag", "@com_google_absl//absl/functional:bind_front", + "@com_google_absl//absl/log", "@com_google_absl//absl/log:check", "@com_google_absl//absl/random:distributions", "@com_google_absl//absl/strings", @@ -317,8 +318,8 @@ cc_test( ":one_tree_lower_bound", "//ortools/base:gmock_main", "//ortools/base:path", - "//ortools/base:types", "//ortools/routing/parsers:tsplib_parser", + "@com_google_absl//absl/log", "@com_google_absl//absl/types:span", ], ) @@ -512,9 +513,12 @@ cc_test( size = "medium", srcs = ["min_cost_flow_test.cc"], deps = [ + ":ebert_graph", ":graphs", ":min_cost_flow", "//ortools/base:gmock_main", + "@com_google_absl//absl/log", + "@com_google_absl//absl/log:check", "@com_google_absl//absl/random:distributions", "@com_google_absl//absl/strings:str_format", "@com_google_absl//absl/types:span", @@ -567,6 +571,7 @@ cc_test( srcs = ["assignment_test.cc"], deps = [ ":assignment", + ":ebert_graph", "//ortools/base:gmock_main", ], ) diff --git a/ortools/graph/assignment_test.cc b/ortools/graph/assignment_test.cc index c36a7956991..4c169f9d450 100644 --- a/ortools/graph/assignment_test.cc +++ b/ortools/graph/assignment_test.cc @@ -16,6 +16,7 @@ #include #include "gtest/gtest.h" +#include "ortools/graph/ebert_graph.h" namespace operations_research { diff --git a/ortools/graph/christofides_test.cc b/ortools/graph/christofides_test.cc index c092b06722f..5058f9a4899 100644 --- a/ortools/graph/christofides_test.cc +++ b/ortools/graph/christofides_test.cc @@ -13,8 +13,8 @@ #include "ortools/graph/christofides.h" +#include #include -#include #include #include #include @@ -26,7 +26,6 @@ #include "benchmark/benchmark.h" #include "gtest/gtest.h" #include "ortools/base/logging.h" -#include "ortools/base/macros.h" namespace operations_research { @@ -210,28 +209,27 @@ TEST(HamiltonianPathTest, Ulysses) { } TEST(ChristofidesTest, EmptyModel) { - ChristofidesPathSolver chris_solver(0, [](int i, int j) { return 0; }); + ChristofidesPathSolver chris_solver(0, [](int, int) { return 0; }); EXPECT_EQ(0, chris_solver.TravelingSalesmanCost()); EXPECT_TRUE(chris_solver.TravelingSalesmanPath().empty()); } TEST(ChristofidesTest, SingleNodeModel) { - ChristofidesPathSolver chris_solver(1, [](int i, int j) { return 0; }); + ChristofidesPathSolver chris_solver(1, [](int, int) { return 0; }); EXPECT_EQ(0, chris_solver.TravelingSalesmanCost()); EXPECT_EQ("0 0 ", PathToString(chris_solver.TravelingSalesmanPath())); } TEST(ChristofidesTest, Int64Overflow) { ChristofidesPathSolver chris_solver( - 10, [](int i, int j) { return std::numeric_limits::max() / 2; }); + 10, [](int, int) { return std::numeric_limits::max() / 2; }); EXPECT_EQ(std::numeric_limits::max(), chris_solver.TravelingSalesmanCost()); } TEST(ChristofidesTest, SaturatedDouble) { - ChristofidesPathSolver chris_solver(10, [](int i, int j) { - return std::numeric_limits::max() / 2.0; - }); + ChristofidesPathSolver chris_solver( + 10, [](int, int) { return std::numeric_limits::max() / 2.0; }); EXPECT_EQ(std::numeric_limits::infinity(), chris_solver.TravelingSalesmanCost()); } diff --git a/ortools/graph/cliques_test.cc b/ortools/graph/cliques_test.cc index b28bbda188b..ddb68665ac1 100644 --- a/ortools/graph/cliques_test.cc +++ b/ortools/graph/cliques_test.cc @@ -24,6 +24,7 @@ #include #include "absl/container/flat_hash_set.h" +#include "absl/flags/flag.h" #include "absl/functional/bind_front.h" #include "absl/log/check.h" #include "absl/random/distributions.h" @@ -31,7 +32,7 @@ #include "absl/types/span.h" #include "benchmark/benchmark.h" #include "gtest/gtest.h" -#include "ortools/base/mathutil.h" +#include "ortools/base/logging.h" #include "ortools/util/time_limit.h" namespace operations_research { @@ -98,7 +99,7 @@ class CliqueSizeVerifier { int64_t num_cliques_; }; -inline bool FullGraph(int index1, int index2) { return true; } +inline bool FullGraph(int /*index1*/, int /*index2*/) { return true; } inline bool EmptyGraph(int index1, int index2) { return (index1 == index2); } @@ -562,7 +563,7 @@ TEST(BronKerboschAlgorithmTest, WallTimeLimit) { absl::SetFlag(&FLAGS_time_limit_use_usertime, true); TimeLimit time_limit(kTimeLimitSeconds); - const auto graph = [kNumPartitions](int index1, int index2) { + const auto graph = [](int index1, int index2) { return FullKPartiteGraph(kNumPartitions, index1, index2); }; CliqueSizeVerifier verifier(kExpectedCliqueSize, kExpectedCliqueSize); @@ -583,7 +584,7 @@ TEST(BronKerboschAlgorithmTest, DeterministicTimeLimit) { std::unique_ptr time_limit = TimeLimit::FromDeterministicTime(kDeterministicLimit); - const auto graph = [kNumPartitions](int index1, int index2) { + const auto graph = [](int index1, int index2) { return FullKPartiteGraph(kNumPartitions, index1, index2); }; CliqueSizeVerifier verifier(kExpectedCliqueSize, kExpectedCliqueSize); diff --git a/ortools/graph/min_cost_flow_test.cc b/ortools/graph/min_cost_flow_test.cc index 8ad26727b2e..3391167d489 100644 --- a/ortools/graph/min_cost_flow_test.cc +++ b/ortools/graph/min_cost_flow_test.cc @@ -20,12 +20,15 @@ #include #include +#include "absl/log/check.h" #include "absl/random/distributions.h" #include "absl/strings/str_format.h" #include "absl/types/span.h" #include "benchmark/benchmark.h" #include "gtest/gtest.h" #include "ortools/algorithms/binary_search.h" +#include "ortools/base/logging.h" +#include "ortools/graph/ebert_graph.h" #include "ortools/graph/graph.h" #include "ortools/graph/graphs.h" #include "ortools/linear_solver/linear_solver.h" @@ -631,7 +634,8 @@ struct MinCostFlowSolver { template void FullRandomAssignment(typename MinCostFlowSolver::Solver f, NodeIndex num_sources, NodeIndex num_targets, - CostValue expected_cost1, CostValue expected_cost2) { + CostValue expected_cost1, + CostValue /*expected_cost2*/) { const CostValue kCostRange = 1000; Graph graph; GenerateCompleteGraph(num_sources, num_targets, &graph); @@ -656,7 +660,7 @@ template void PartialRandomAssignment(typename MinCostFlowSolver::Solver f, NodeIndex num_sources, NodeIndex num_targets, CostValue expected_cost1, - CostValue expected_cost2) { + CostValue /*expected_cost2*/) { const NodeIndex kDegree = 10; const CostValue kCostRange = 1000; Graph graph; diff --git a/ortools/graph/one_tree_lower_bound_test.cc b/ortools/graph/one_tree_lower_bound_test.cc index 977b0c52364..0554d56e69a 100644 --- a/ortools/graph/one_tree_lower_bound_test.cc +++ b/ortools/graph/one_tree_lower_bound_test.cc @@ -21,15 +21,15 @@ #include "absl/types/span.h" #include "gtest/gtest.h" +#include "ortools/base/logging.h" #include "ortools/base/path.h" -#include "ortools/base/types.h" #include "ortools/routing/parsers/tsplib_parser.h" namespace operations_research { namespace { TEST(OneTreeLBTest, VolgenantJonkerEmpty) { - const double cost = ComputeOneTreeLowerBound(0, [](int from, int to) { + const double cost = ComputeOneTreeLowerBound(0, [](int /*from*/, int /*to*/) { ADD_FAILURE(); // Making sure the function is not being called. return 0; }); @@ -42,7 +42,7 @@ TEST(OneTreeLBTest, HeldWolfeCrowderEmpty) { TravelingSalesmanLowerBoundParameters::HeldWolfeCrowder; const double cost = ComputeOneTreeLowerBoundWithParameters( 0, - [](int from, int to) { + [](int /*from*/, int /*to*/) { ADD_FAILURE(); // Making sure the function is not being called. return 0; }, @@ -52,7 +52,7 @@ TEST(OneTreeLBTest, HeldWolfeCrowderEmpty) { TEST(OneTreeLBTest, VolgenantJonkerOneNode) { const double cost = - ComputeOneTreeLowerBound(1, [](int from, int to) { return 0; }); + ComputeOneTreeLowerBound(1, [](int /*from*/, int /*to*/) { return 0; }); EXPECT_EQ(0, cost); } @@ -61,13 +61,13 @@ TEST(OneTreeLBTest, HeldWolfeCrowderOneNode) { parameters.algorithm = TravelingSalesmanLowerBoundParameters::HeldWolfeCrowder; const double cost = ComputeOneTreeLowerBoundWithParameters( - 1, [](int from, int to) { return 0; }, parameters); + 1, [](int /*from*/, int /*to*/) { return 0; }, parameters); EXPECT_EQ(0, cost); } TEST(OneTreeLBTest, VolgenantJonkerTwoNodes) { const double cost = - ComputeOneTreeLowerBound(2, [](int from, int to) { return 1; }); + ComputeOneTreeLowerBound(2, [](int /*from*/, int /*to*/) { return 1; }); EXPECT_EQ(2, cost); } @@ -76,7 +76,7 @@ TEST(OneTreeLBTest, HeldWolfeCrowderTwoNodes) { parameters.algorithm = TravelingSalesmanLowerBoundParameters::HeldWolfeCrowder; const double cost = ComputeOneTreeLowerBoundWithParameters( - 2, [](int from, int to) { return 1; }, parameters); + 2, [](int /*from*/, int /*to*/) { return 1; }, parameters); EXPECT_EQ(2, cost); } diff --git a/ortools/graph/samples/assignment_min_flow.py b/ortools/graph/samples/assignment_min_flow.py index 22f9cd8e186..381a75da5fb 100755 --- a/ortools/graph/samples/assignment_min_flow.py +++ b/ortools/graph/samples/assignment_min_flow.py @@ -51,13 +51,13 @@ def main(): # [START constraints] # Add each arc. - for i in range(len(start_nodes)): + for idx, _ in enumerate(start_nodes): smcf.add_arc_with_capacity_and_unit_cost( - start_nodes[i], end_nodes[i], capacities[i], costs[i] + start_nodes[idx], end_nodes[idx], capacities[idx], costs[idx] ) # Add node supplies. - for i in range(len(supplies)): - smcf.set_node_supply(i, supplies[i]) + for idx, supply in enumerate(supplies): + smcf.set_node_supply(idx, supply) # [END constraints] # [START solve] @@ -67,8 +67,7 @@ def main(): # [START print_solution] if status == smcf.OPTIMAL: - print("Total cost = ", smcf.optimal_cost()) - print() + print(f"Total cost = {smcf.optimal_cost()}") for arc in range(smcf.num_arcs()): # Can ignore arcs leading out of source or into sink. if smcf.tail(arc) != source and smcf.head(arc) != sink: @@ -77,8 +76,8 @@ def main(): # give an assignment of worker to task. if smcf.flow(arc) > 0: print( - "Worker %d assigned to task %d. Cost = %d" - % (smcf.tail(arc), smcf.head(arc), smcf.unit_cost(arc)) + f"Worker {smcf.tail(arc)} assigned to task {smcf.head(arc)}. " + f"Cost = {smcf.unit_cost(arc)}" ) else: print("There was an issue with the min cost flow input.") diff --git a/ortools/graph/samples/simple_min_cost_flow_program.py b/ortools/graph/samples/simple_min_cost_flow_program.py index b3b06856bfc..569058e0373 100755 --- a/ortools/graph/samples/simple_min_cost_flow_program.py +++ b/ortools/graph/samples/simple_min_cost_flow_program.py @@ -68,7 +68,8 @@ def main(): costs = solution_flows * unit_costs for arc, flow, cost in zip(all_arcs, solution_flows, costs): print( - f"{smcf.tail(arc):1} -> {smcf.head(arc)} {flow:3} / {smcf.capacity(arc):3} {cost}" + f"{smcf.tail(arc):1} -> " + f"{smcf.head(arc)} {flow:3} / {smcf.capacity(arc):3} {cost}" ) # [END print_solution] From 66d16a668704e0e9fd7c0ba331a6ea8cf9e7aae8 Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Mon, 7 Oct 2024 08:25:17 +0200 Subject: [PATCH 06/11] base: add proto_enum_utils --- ortools/base/BUILD.bazel | 9 ++ ortools/base/proto_enum_utils.h | 209 ++++++++++++++++++++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 ortools/base/proto_enum_utils.h diff --git a/ortools/base/BUILD.bazel b/ortools/base/BUILD.bazel index 1d632f6df77..f67053959ec 100644 --- a/ortools/base/BUILD.bazel +++ b/ortools/base/BUILD.bazel @@ -460,6 +460,15 @@ cc_library( ], ) +cc_library( + name = "proto_enum_utils", + hdrs = ["proto_enum_utils.h"], + deps = [ + "@com_google_absl//absl/types:span", + "@com_google_protobuf//:protobuf", + ], +) + cc_library( name = "ptr_util", hdrs = ["ptr_util.h"], diff --git a/ortools/base/proto_enum_utils.h b/ortools/base/proto_enum_utils.h new file mode 100644 index 00000000000..301e9988690 --- /dev/null +++ b/ortools/base/proto_enum_utils.h @@ -0,0 +1,209 @@ +// Copyright 2010-2024 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OR_TOOLS_BASE_PROTO_ENUM_UTILS_H_ +#define OR_TOOLS_BASE_PROTO_ENUM_UTILS_H_ + +// Provides utility functions that help with handling Protocol Buffer enums. +// +// Examples: +// +// A function to easily iterate over all defined values of an enum known at +// compile-time: +// +// for (Proto::Enum e : EnumerateEnumValues()) { +// ... +// } +// + +#include +#include + +#include "absl/types/span.h" +#include "google/protobuf/descriptor.pb.h" + +namespace google::protobuf::contrib::utils { + +using google::protobuf::GetEnumDescriptor; +using google::protobuf::RepeatedField; + +template +class ProtoEnumIterator; + +template +class EnumeratedProtoEnumView; + +template +bool operator==(const ProtoEnumIterator& a, const ProtoEnumIterator& b); + +template +bool operator!=(const ProtoEnumIterator& a, const ProtoEnumIterator& b); + +// Generic Proto enum iterator. +template +class ProtoEnumIterator { + public: + typedef E value_type; + typedef std::forward_iterator_tag iterator_category; + typedef int difference_type; + typedef E* pointer; + typedef E& reference; + + ProtoEnumIterator() : current_(0) {} + + ProtoEnumIterator(const ProtoEnumIterator& other) + : current_(other.current_) {} + + ProtoEnumIterator& operator=(const ProtoEnumIterator& other) { + current_ = other.current_; + return *this; + } + + ProtoEnumIterator operator++(int) { + ProtoEnumIterator other(*this); + ++(*this); + return other; + } + + ProtoEnumIterator& operator++() { + ++current_; + return *this; + } + + E operator*() const { + return static_cast(GetEnumDescriptor()->value(current_)->number()); + } + + private: + explicit ProtoEnumIterator(int current) : current_(current) {} + + int current_; + + // Only EnumeratedProtoEnumView can instantiate ProtoEnumIterator. + friend class EnumeratedProtoEnumView; + friend bool operator== + <>(const ProtoEnumIterator& a, const ProtoEnumIterator& b); + friend bool operator!= + <>(const ProtoEnumIterator& a, const ProtoEnumIterator& b); +}; + +template +bool operator==(const ProtoEnumIterator& a, const ProtoEnumIterator& b) { + return a.current_ == b.current_; +} + +template +bool operator!=(const ProtoEnumIterator& a, const ProtoEnumIterator& b) { + return a.current_ != b.current_; +} + +template +class EnumeratedProtoEnumView { + public: + typedef E value_type; + typedef ProtoEnumIterator iterator; + iterator begin() const { return iterator(0); } + iterator end() const { + return iterator(GetEnumDescriptor()->value_count()); + } +}; + +// Returns an EnumeratedProtoEnumView that can be iterated over: +// for (Proto::Enum e : EnumerateEnumValues()) { +// ... +// } +template +EnumeratedProtoEnumView EnumerateEnumValues() { + return EnumeratedProtoEnumView(); +} + +// Returns a view that allows to iterate directly over the enum values +// in an enum repeated field, wrapping the repeated field with a type-safe +// iterator that provides access to the enum values. +// +// for (Enum enum : +// REPEATED_ENUM_ADAPTER(message, repeated_enum_field)) { +// ... +// } +// +// It provides greater safety than iterating over the enum directly, as the +// following will fail to type-check: +// +// .proto +// RightEnum enum = 5; +// +// client .cc +// for (WrongEnum e : REPEATED_ENUM_ADAPTER(proto, enum)) { <- Error: Cannot +// cast from +// RightEnum to +// WrongEnum +// } +// +// NOTE: As per http://shortn/_CYfjpruK6N, unrecognized enum values are treated +// differently between proto2 and proto3. +// +// For proto2, they are stripped out from the message when read, so all +// unrecognized enum values from the wire format will be skipped when iterating +// over the wrapper (this is the same behavior as iterating over the +// RepeatedField directly). +// +// For proto3, they are left as-is, so unrecognized enum values from the wire +// format will still be returned when iterating over the wrapper (this is the +// same behavior as iterating over the RepeatedField directly). +// +#define REPEATED_ENUM_ADAPTER(var, field) \ + google::protobuf::contrib::utils::internal::RepeatedEnumView< \ + decltype(var.field(0))>(var.field()) + +// ==== WARNING TO USERS ==== +// Below are internal implementations, not public API, and may change without +// notice. Do NOT use directly. + +namespace internal { + +// Implementation for REPEATED_ENUM_ADAPTER. This does not provide type safety +// thus should be used through REPEATED_ENUM_ADAPTER only. See cr/246914845 for +// context. +template +class RepeatedEnumView { + public: + class Iterator : public std::iterator { + public: + explicit Iterator(RepeatedField::const_iterator ptr) : ptr_(ptr) {} + bool operator==(const Iterator& it) const { return ptr_ == it.ptr_; } + bool operator!=(const Iterator& it) const { return ptr_ != it.ptr_; } + Iterator& operator++() { + ++ptr_; + return *this; + } + E operator*() const { return static_cast(*ptr_); } + + private: + RepeatedField::const_iterator ptr_; + }; + + explicit RepeatedEnumView(const RepeatedField& repeated_field) + : repeated_field_(repeated_field) {} + + Iterator begin() const { return Iterator(repeated_field_.begin()); } + Iterator end() const { return Iterator(repeated_field_.end()); } + + private: + const RepeatedField& repeated_field_; +}; + +} // namespace internal + +} // namespace google::protobuf::contrib::utils + +#endif // OR_TOOLS_BASE_PROTO_ENUM_UTILS_H_ From ed1adfb11ab71bca1b5ec3692e5d14cef19ccf41 Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Mon, 7 Oct 2024 08:28:37 +0200 Subject: [PATCH 07/11] graph: cleanup assignment_min_cost_flow --- ortools/graph/samples/assignment_min_flow.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ortools/graph/samples/assignment_min_flow.py b/ortools/graph/samples/assignment_min_flow.py index 381a75da5fb..e4183679973 100755 --- a/ortools/graph/samples/assignment_min_flow.py +++ b/ortools/graph/samples/assignment_min_flow.py @@ -51,10 +51,10 @@ def main(): # [START constraints] # Add each arc. - for idx, _ in enumerate(start_nodes): - smcf.add_arc_with_capacity_and_unit_cost( - start_nodes[idx], end_nodes[idx], capacities[idx], costs[idx] - ) + for start_node, end_node, capacity, cost in zip( + start_nodes, end_nodes, capacities, costs + ): + smcf.add_arc_with_capacity_and_unit_cost(start_node, end_node, capacity, cost) # Add node supplies. for idx, supply in enumerate(supplies): smcf.set_node_supply(idx, supply) From bda54b7e390d8728780781dcc98012cb3c796e2e Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Mon, 7 Oct 2024 08:40:04 +0200 Subject: [PATCH 08/11] sat: Export from google3 --- ortools/sat/2d_rectangle_presolve_test.cc | 5 +- ortools/sat/BUILD.bazel | 22 + ortools/sat/cp_model_checker.cc | 6 +- ortools/sat/cp_model_lns.cc | 154 +++++- ortools/sat/cp_model_lns.h | 26 +- ortools/sat/cp_model_presolve.cc | 75 +++ ortools/sat/cp_model_solver.cc | 6 + ortools/sat/feasibility_pump.cc | 4 +- ortools/sat/linear_constraint_manager.cc | 11 + ortools/sat/linear_programming_constraint.cc | 321 ++++++++--- ortools/sat/linear_programming_constraint.h | 14 +- ortools/sat/work_assignment_test.cc | 545 +++++++++++++++++++ 12 files changed, 1082 insertions(+), 107 deletions(-) create mode 100644 ortools/sat/work_assignment_test.cc diff --git a/ortools/sat/2d_rectangle_presolve_test.cc b/ortools/sat/2d_rectangle_presolve_test.cc index 899dc9cd376..20700a826db 100644 --- a/ortools/sat/2d_rectangle_presolve_test.cc +++ b/ortools/sat/2d_rectangle_presolve_test.cc @@ -48,13 +48,14 @@ using ::testing::IsEmpty; std::vector BuildFromAsciiArt(std::string_view input) { std::vector rectangles; std::vector lines = absl::StrSplit(input, '\n'); + const int num_lines = lines.size(); for (int i = 0; i < lines.size(); i++) { for (int j = 0; j < lines[i].size(); j++) { if (lines[i][j] != ' ') { rectangles.push_back({.x_min = j, .x_max = j + 1, - .y_min = 2 * lines.size() - 2 * i, - .y_max = 2 * lines.size() - 2 * i + 2}); + .y_min = 2 * num_lines - 2 * i, + .y_max = 2 * num_lines - 2 * i + 2}); } } } diff --git a/ortools/sat/BUILD.bazel b/ortools/sat/BUILD.bazel index 5e1b5ba3d75..e4d9c5c1f9a 100644 --- a/ortools/sat/BUILD.bazel +++ b/ortools/sat/BUILD.bazel @@ -2146,6 +2146,7 @@ cc_library( "//ortools/lp_data:base", "//ortools/lp_data:lp_data_utils", "//ortools/lp_data:scattered_vector", + "//ortools/lp_data:sparse", "//ortools/lp_data:sparse_column", "//ortools/util:bitset", "//ortools/util:rev", @@ -2156,6 +2157,7 @@ cc_library( "@com_google_absl//absl/log:check", "@com_google_absl//absl/numeric:int128", "@com_google_absl//absl/strings", + "@com_google_absl//absl/strings:str_format", "@com_google_absl//absl/types:span", ], ) @@ -3404,6 +3406,26 @@ cc_library( ], ) +cc_test( + name = "work_assignment_test", + srcs = ["work_assignment_test.cc"], + deps = [ + ":cp_model", + ":cp_model_cc_proto", + ":cp_model_checker", + ":cp_model_loader", + ":cp_model_solver", + ":integer", + ":model", + ":sat_parameters_cc_proto", + ":synchronization", + ":work_assignment", + "//ortools/base:gmock_main", + "//ortools/base:parse_text_proto", + "@com_google_absl//absl/strings:string_view", + ], +) + cc_test( name = "inclusion_test", size = "small", diff --git a/ortools/sat/cp_model_checker.cc b/ortools/sat/cp_model_checker.cc index 926d58d04ac..8645013cd02 100644 --- a/ortools/sat/cp_model_checker.cc +++ b/ortools/sat/cp_model_checker.cc @@ -854,9 +854,9 @@ std::string ValidateSolutionHint(const CpModelProto& model) { if (hint.vars().size() != hint.values().size()) { return "Invalid solution hint: vars and values do not have the same size."; } - for (const int ref : hint.vars()) { - if (!VariableReferenceIsValid(model, ref)) { - return absl::StrCat("Invalid variable reference in solution hint: ", ref); + for (const int var : hint.vars()) { + if (!VariableIndexIsValid(model, var)) { + return absl::StrCat("Invalid variable in solution hint: ", var); } } diff --git a/ortools/sat/cp_model_lns.cc b/ortools/sat/cp_model_lns.cc index a3ccabe003c..436d65d68d0 100644 --- a/ortools/sat/cp_model_lns.cc +++ b/ortools/sat/cp_model_lns.cc @@ -448,7 +448,7 @@ std::vector NeighborhoodGeneratorHelper::GetActiveIntervals( initial_solution); } -std::vector> +std::vector NeighborhoodGeneratorHelper::GetActiveRectangles( const CpSolverResponse& initial_solution) const { const std::vector active_intervals = @@ -456,7 +456,7 @@ NeighborhoodGeneratorHelper::GetActiveRectangles( const absl::flat_hash_set active_intervals_set(active_intervals.begin(), active_intervals.end()); - std::vector> active_rectangles; + absl::flat_hash_map, std::vector> active_rectangles; for (const int ct_index : TypeToConstraints(ConstraintProto::kNoOverlap2D)) { const NoOverlap2DConstraintProto& ct = model_proto_.constraints(ct_index).no_overlap_2d(); @@ -465,12 +465,20 @@ NeighborhoodGeneratorHelper::GetActiveRectangles( const int y_i = ct.y_intervals(i); if (active_intervals_set.contains(x_i) || active_intervals_set.contains(y_i)) { - active_rectangles.push_back({x_i, y_i}); + active_rectangles[{x_i, y_i}].push_back(ct_index); } } } - return active_rectangles; + std::vector results; + for (const auto& [rectangle, no_overlap_2d_constraints] : active_rectangles) { + ActiveRectangle& result = results.emplace_back(); + result.x_interval = rectangle.first; + result.y_interval = rectangle.second; + result.no_overlap_2d_constraints = {no_overlap_2d_constraints.begin(), + no_overlap_2d_constraints.end()}; + } + return results; } std::vector> @@ -2256,14 +2264,125 @@ Neighborhood SchedulingResourceWindowsNeighborhoodGenerator::Generate( Neighborhood RandomRectanglesPackingNeighborhoodGenerator::Generate( const CpSolverResponse& initial_solution, SolveData& data, absl::BitGenRef random) { - std::vector> rectangles_to_freeze = + std::vector rectangles_to_freeze = helper_.GetActiveRectangles(initial_solution); GetRandomSubset(1.0 - data.difficulty, &rectangles_to_freeze, random); absl::flat_hash_set variables_to_freeze; - for (const auto& [x, y] : rectangles_to_freeze) { - InsertVariablesFromConstraint(helper_.ModelProto(), x, variables_to_freeze); - InsertVariablesFromConstraint(helper_.ModelProto(), y, variables_to_freeze); + for (const ActiveRectangle& rectangle : rectangles_to_freeze) { + InsertVariablesFromConstraint(helper_.ModelProto(), rectangle.x_interval, + variables_to_freeze); + InsertVariablesFromConstraint(helper_.ModelProto(), rectangle.y_interval, + variables_to_freeze); + } + + return helper_.FixGivenVariables(initial_solution, variables_to_freeze); +} + +Neighborhood RectanglesPackingRelaxTwoNeighborhoodsGenerator::Generate( + const CpSolverResponse& initial_solution, SolveData& data, + absl::BitGenRef random) { + // First pick a pair of rectangles. + std::vector all_active_rectangles = + helper_.GetActiveRectangles(initial_solution); + if (all_active_rectangles.size() <= 2) return helper_.FullNeighborhood(); + + const int first_idx = + absl::Uniform(random, 0, all_active_rectangles.size()); + int second_idx = + absl::Uniform(random, 0, all_active_rectangles.size() - 1); + if (second_idx >= first_idx) { + second_idx++; + } + + const ActiveRectangle& chosen_rectangle_1 = all_active_rectangles[first_idx]; + const ActiveRectangle& chosen_rectangle_2 = all_active_rectangles[second_idx]; + + const auto get_rectangle = [&initial_solution, helper = &helper_]( + const ActiveRectangle& rectangle) { + const int x_interval_idx = rectangle.x_interval; + const int y_interval_idx = rectangle.y_interval; + const ConstraintProto& x_interval_ct = + helper->ModelProto().constraints(x_interval_idx); + const ConstraintProto& y_interval_ct = + helper->ModelProto().constraints(y_interval_idx); + return Rectangle{.x_min = GetLinearExpressionValue( + x_interval_ct.interval().start(), initial_solution), + .x_max = GetLinearExpressionValue( + x_interval_ct.interval().end(), initial_solution), + .y_min = GetLinearExpressionValue( + y_interval_ct.interval().start(), initial_solution), + .y_max = GetLinearExpressionValue( + y_interval_ct.interval().end(), initial_solution)}; + }; + + // TODO(user): This computes the distance between the center of the + // rectangles. We could use the real distance between the closest points, but + // not sure it is worth the extra complexity. + const auto compute_rectangle_distance = [](const Rectangle& rect1, + const Rectangle& rect2) { + return (static_cast(rect1.x_min.value()) + rect1.x_max.value() - + rect2.x_min.value() - rect2.x_max.value()) * + (static_cast(rect1.y_min.value()) + rect1.y_max.value() - + rect2.y_min.value() - rect2.y_max.value()); + }; + const Rectangle rect1 = get_rectangle(chosen_rectangle_1); + const Rectangle rect2 = get_rectangle(chosen_rectangle_2); + + // Now compute a neighborhood around each rectangle. Note that we only + // consider two rectangles as potential neighbors if they are part of the same + // no_overlap_2d constraint. + absl::flat_hash_set variables_to_freeze; + std::vector> distances1; + std::vector> distances2; + distances1.reserve(all_active_rectangles.size()); + distances2.reserve(all_active_rectangles.size()); + for (int i = 0; i < all_active_rectangles.size(); ++i) { + const ActiveRectangle& rectangle = all_active_rectangles[i]; + InsertVariablesFromConstraint(helper_.ModelProto(), rectangle.x_interval, + variables_to_freeze); + InsertVariablesFromConstraint(helper_.ModelProto(), rectangle.y_interval, + variables_to_freeze); + + const Rectangle rect = get_rectangle(rectangle); + const bool same_no_overlap_as_rect1 = + absl::c_any_of(chosen_rectangle_1.no_overlap_2d_constraints, + [&rectangle](const int c) { + return rectangle.no_overlap_2d_constraints.contains(c); + }); + const bool same_no_overlap_as_rect2 = + absl::c_any_of(chosen_rectangle_2.no_overlap_2d_constraints, + [&rectangle](const int c) { + return rectangle.no_overlap_2d_constraints.contains(c); + }); + if (same_no_overlap_as_rect1) { + distances1.push_back({i, compute_rectangle_distance(rect1, rect)}); + } + if (same_no_overlap_as_rect2) { + distances2.push_back({i, compute_rectangle_distance(rect2, rect)}); + } + } + const int num_to_sample_each = + data.difficulty * all_active_rectangles.size() / 2; + std::sort(distances1.begin(), distances1.end(), + [](const auto& a, const auto& b) { return a.second < b.second; }); + std::sort(distances2.begin(), distances2.end(), + [](const auto& a, const auto& b) { return a.second < b.second; }); + absl::flat_hash_set variables_to_relax; + for (auto& samples : {distances1, distances2}) { + const int num_potential_samples = samples.size(); + for (int i = 0; i < std::min(num_potential_samples, num_to_sample_each); + ++i) { + const int rectangle_idx = samples[i].first; + const ActiveRectangle& rectangle = all_active_rectangles[rectangle_idx]; + InsertVariablesFromConstraint(helper_.ModelProto(), rectangle.x_interval, + variables_to_relax); + InsertVariablesFromConstraint(helper_.ModelProto(), rectangle.y_interval, + variables_to_relax); + } + } + for (const int v : variables_to_relax) { + variables_to_freeze.erase(v); } return helper_.FixGivenVariables(initial_solution, variables_to_freeze); @@ -2272,13 +2391,13 @@ Neighborhood RandomRectanglesPackingNeighborhoodGenerator::Generate( Neighborhood RandomPrecedencesPackingNeighborhoodGenerator::Generate( const CpSolverResponse& initial_solution, SolveData& data, absl::BitGenRef random) { - std::vector> rectangles_to_relax = + std::vector rectangles_to_relax = helper_.GetActiveRectangles(initial_solution); GetRandomSubset(data.difficulty, &rectangles_to_relax, random); std::vector intervals_to_relax; - for (const auto& [x, y] : rectangles_to_relax) { - intervals_to_relax.push_back(x); - intervals_to_relax.push_back(y); + for (const ActiveRectangle& rect : rectangles_to_relax) { + intervals_to_relax.push_back(rect.x_interval); + intervals_to_relax.push_back(rect.y_interval); } gtl::STLSortAndRemoveDuplicates(&intervals_to_relax); @@ -2289,13 +2408,14 @@ Neighborhood RandomPrecedencesPackingNeighborhoodGenerator::Generate( Neighborhood SlicePackingNeighborhoodGenerator::Generate( const CpSolverResponse& initial_solution, SolveData& data, absl::BitGenRef random) { - const std::vector> active_rectangles = + const std::vector active_rectangles = helper_.GetActiveRectangles(initial_solution); const bool use_first_dimension = absl::Bernoulli(random, 0.5); std::vector projected_intervals; projected_intervals.reserve(active_rectangles.size()); - for (const auto& [x, y] : active_rectangles) { - projected_intervals.push_back(use_first_dimension ? x : y); + for (const ActiveRectangle& rect : active_rectangles) { + projected_intervals.push_back(use_first_dimension ? rect.x_interval + : rect.y_interval); } const TimePartition partition = PartitionIndicesAroundRandomTimeWindow( @@ -2310,10 +2430,10 @@ Neighborhood SlicePackingNeighborhoodGenerator::Generate( for (int index = 0; index < active_rectangles.size(); ++index) { if (indices_to_fix[index]) { InsertVariablesFromConstraint(helper_.ModelProto(), - active_rectangles[index].first, + active_rectangles[index].x_interval, variables_to_freeze); InsertVariablesFromConstraint(helper_.ModelProto(), - active_rectangles[index].second, + active_rectangles[index].y_interval, variables_to_freeze); } } diff --git a/ortools/sat/cp_model_lns.h b/ortools/sat/cp_model_lns.h index dd92ff04d33..00856cbe1c4 100644 --- a/ortools/sat/cp_model_lns.h +++ b/ortools/sat/cp_model_lns.h @@ -219,7 +219,14 @@ class NeighborhoodGeneratorHelper : public SubSolver { // lns_focus_on_performed_intervals. If true, this method returns the list of // performed rectangles in the solution. If false, it returns all rectangles // of the model. - std::vector> GetActiveRectangles( + struct ActiveRectangle { + int x_interval; + int y_interval; + // The set of no_overlap_2d constraints that both x_interval and y_interval + // are participating in. + absl::flat_hash_set no_overlap_2d_constraints; + }; + std::vector GetActiveRectangles( const CpSolverResponse& initial_solution) const; // Returns the set of unique intervals list appearing in a no_overlap, @@ -356,6 +363,8 @@ class NeighborhoodGenerator { : name_(name), helper_(*helper), difficulty_(0.5) {} virtual ~NeighborhoodGenerator() = default; + using ActiveRectangle = NeighborhoodGeneratorHelper::ActiveRectangle; + // Adds solve data about one "solved" neighborhood. struct SolveData { // The status of the sub-solve. @@ -706,6 +715,21 @@ class RandomRectanglesPackingNeighborhoodGenerator SolveData& data, absl::BitGenRef random) final; }; +// Only make sense for problems with no_overlap_2d constraints. This selects two +// random rectangles and relax them alongside the closest rectangles to each one +// of them. The idea is that this will find a better solution when there is a +// cost function that would be improved by swapping the two rectangles. +class RectanglesPackingRelaxTwoNeighborhoodsGenerator + : public NeighborhoodGenerator { + public: + explicit RectanglesPackingRelaxTwoNeighborhoodsGenerator( + NeighborhoodGeneratorHelper const* helper, absl::string_view name) + : NeighborhoodGenerator(name, helper) {} + + Neighborhood Generate(const CpSolverResponse& initial_solution, + SolveData& data, absl::BitGenRef random) final; +}; + // Only make sense for problems with no_overlap_2d constraints. This select a // random set of rectangles (i.e. a pair of intervals) of the problem according // to the difficulty. Then add all implied precedences from the current diff --git a/ortools/sat/cp_model_presolve.cc b/ortools/sat/cp_model_presolve.cc index 4b6720e1064..1a1eeb358d0 100644 --- a/ortools/sat/cp_model_presolve.cc +++ b/ortools/sat/cp_model_presolve.cc @@ -1704,6 +1704,60 @@ bool CpModelPresolver::PresolveIntProd(ConstraintProto* ct) { } } + if (ct->int_prod().exprs().size() == 2) { + const auto is_boolean_affine = + [context = context_](const LinearExpressionProto& expr) { + return expr.vars().size() == 1 && context->MinOf(expr.vars(0)) == 0 && + context->MaxOf(expr.vars(0)) == 1; + }; + const LinearExpressionProto* boolean_linear = nullptr; + const LinearExpressionProto* other_linear = nullptr; + if (is_boolean_affine(ct->int_prod().exprs(0))) { + boolean_linear = &ct->int_prod().exprs(0); + other_linear = &ct->int_prod().exprs(1); + } else if (is_boolean_affine(ct->int_prod().exprs(1))) { + boolean_linear = &ct->int_prod().exprs(1); + other_linear = &ct->int_prod().exprs(0); + } + if (boolean_linear) { + // We have: + // (u + b * v) * other_expr = B, where `b` is a boolean variable. + // + // We can rewrite this as: + // u * other_expr = B, if b = false; + // (u + v) * other_expr = B, if b = true + ConstraintProto* constraint_for_false = + context_->working_model->add_constraints(); + ConstraintProto* constraint_for_true = + context_->working_model->add_constraints(); + constraint_for_true->add_enforcement_literal(boolean_linear->vars(0)); + constraint_for_false->add_enforcement_literal( + NegatedRef(boolean_linear->vars(0))); + LinearConstraintProto* linear_for_false = + constraint_for_false->mutable_linear(); + LinearConstraintProto* linear_for_true = + constraint_for_true->mutable_linear(); + + linear_for_false->add_domain(0); + linear_for_false->add_domain(0); + AddLinearExpressionToLinearConstraint( + *other_linear, boolean_linear->offset(), linear_for_false); + AddLinearExpressionToLinearConstraint(ct->int_prod().target(), -1, + linear_for_false); + + linear_for_true->add_domain(0); + linear_for_true->add_domain(0); + AddLinearExpressionToLinearConstraint( + *other_linear, boolean_linear->offset() + boolean_linear->coeffs(0), + linear_for_true); + AddLinearExpressionToLinearConstraint(ct->int_prod().target(), -1, + linear_for_true); + context_->UpdateRuleStats("int_prod: boolean affine term"); + context_->UpdateNewConstraintsVariableUsage(); + return RemoveConstraint(ct); + } + } + // For now, we only presolve the case where all variables are Booleans. const LinearExpressionProto target_expr = ct->int_prod().target(); int target; @@ -12472,6 +12526,27 @@ void CopyEverythingExceptVariablesAndConstraintsFieldsIntoContext( } if (in_model.has_solution_hint()) { *context->working_model->mutable_solution_hint() = in_model.solution_hint(); + + // We make sure the hint is within the variables domain. + // + // This allows to avoid overflow because we know evaluating constraints on + // the variables domains should be safe thanks to the initial validation. + const int num_terms = in_model.solution_hint().vars().size(); + for (int i = 0; i < num_terms; ++i) { + const int var = in_model.solution_hint().vars(i); + const int64_t value = in_model.solution_hint().values(i); + const auto& domain = in_model.variables(var).domain(); + if (domain.empty()) continue; // UNSAT. + const int64_t min = domain[0]; + const int64_t max = domain[domain.size() - 1]; + if (value < min) { + context->UpdateRuleStats("hint: moved var hint within its domain."); + context->working_model->mutable_solution_hint()->set_values(i, min); + } else if (value > max) { + context->working_model->mutable_solution_hint()->set_values(i, max); + context->UpdateRuleStats("hint: moved var hint within its domain."); + } + } } } diff --git a/ortools/sat/cp_model_solver.cc b/ortools/sat/cp_model_solver.cc index 728524ed369..dd60f67fa36 100644 --- a/ortools/sat/cp_model_solver.cc +++ b/ortools/sat/cp_model_solver.cc @@ -1663,6 +1663,12 @@ void SolveCpModelParallel(SharedClasses* shared, Model* global_model) { helper, name_filter.LastName()), lns_params, helper, shared)); } + if (name_filter.Keep("packing_swap_lns")) { + reentrant_interleaved_subsolvers.push_back(std::make_unique( + std::make_unique( + helper, name_filter.LastName()), + lns_params, helper, shared)); + } if (name_filter.Keep("packing_precedences_lns")) { reentrant_interleaved_subsolvers.push_back(std::make_unique( std::make_unique( diff --git a/ortools/sat/feasibility_pump.cc b/ortools/sat/feasibility_pump.cc index 5ff2c7dd856..9a0cb4a761b 100644 --- a/ortools/sat/feasibility_pump.cc +++ b/ortools/sat/feasibility_pump.cc @@ -360,14 +360,14 @@ void FeasibilityPump::L1DistanceMinimize() { const ColIndex norm_lhs_slack_variable = lp_data_.GetSlackVariable(norm_lhs_constraints_[col]); const double lhs_scaling_factor = - scaler_.VariableScalingFactor(norm_lhs_slack_variable); + scaler_.VariableScalingFactorWithSlack(norm_lhs_slack_variable); lp_data_.SetVariableBounds( norm_lhs_slack_variable, -glop::kInfinity, lhs_scaling_factor * integer_solution_[col.value()]); const ColIndex norm_rhs_slack_variable = lp_data_.GetSlackVariable(norm_rhs_constraints_[col]); const double rhs_scaling_factor = - scaler_.VariableScalingFactor(norm_rhs_slack_variable); + scaler_.VariableScalingFactorWithSlack(norm_rhs_slack_variable); lp_data_.SetVariableBounds( norm_rhs_slack_variable, -glop::kInfinity, -rhs_scaling_factor * integer_solution_[col.value()]); diff --git a/ortools/sat/linear_constraint_manager.cc b/ortools/sat/linear_constraint_manager.cc index 1f5288284ec..c0524551ac0 100644 --- a/ortools/sat/linear_constraint_manager.cc +++ b/ortools/sat/linear_constraint_manager.cc @@ -92,6 +92,15 @@ bool LinearConstraintManager::MaybeRemoveSomeInactiveConstraints( int new_size = 0; for (int i = 0; i < num_rows; ++i) { const ConstraintIndex constraint_index = lp_constraints_[i]; + if (constraint_infos_[constraint_index].constraint.num_terms == 0) { + // Remove empty constraint. + // + // TODO(user): If the constraint is infeasible we could detect unsat + // right away, but hopefully this is a case where the propagation part + // of the solver can detect that too. + constraint_infos_[constraint_index].is_in_lp = false; + continue; + } // Constraints that are not tight in the current solution have a basic // status. We remove the ones that have been inactive in the last recent @@ -764,6 +773,8 @@ bool LinearConstraintManager::ChangeLp(glop::BasisState* solution_state, void LinearConstraintManager::AddAllConstraintsToLp() { for (ConstraintIndex i(0); i < constraint_infos_.size(); ++i) { if (constraint_infos_[i].is_in_lp) continue; + if (constraint_infos_[i].constraint.num_terms == 0) continue; + constraint_infos_[i].is_in_lp = true; lp_constraints_.push_back(i); } diff --git a/ortools/sat/linear_programming_constraint.cc b/ortools/sat/linear_programming_constraint.cc index 63237ee2f70..d308b1ae9d1 100644 --- a/ortools/sat/linear_programming_constraint.cc +++ b/ortools/sat/linear_programming_constraint.cc @@ -31,6 +31,7 @@ #include "absl/log/check.h" #include "absl/numeric/int128.h" #include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" #include "absl/strings/string_view.h" #include "absl/types/span.h" #include "ortools/algorithms/binary_search.h" @@ -45,6 +46,7 @@ #include "ortools/lp_data/lp_data_utils.h" #include "ortools/lp_data/lp_types.h" #include "ortools/lp_data/scattered_vector.h" +#include "ortools/lp_data/sparse.h" #include "ortools/lp_data/sparse_column.h" #include "ortools/sat/cp_model_mapping.h" #include "ortools/sat/cuts.h" @@ -373,8 +375,6 @@ void LinearProgrammingConstraint::SetObjectiveCoefficient(IntegerVariable ivar, // for TSP for instance where the number of edges is large, but only a small // fraction will be used in the optimal solution. bool LinearProgrammingConstraint::CreateLpFromConstraintManager() { - simplex_.NotifyThatMatrixIsChangedForNextSolve(); - // Fill integer_lp_. integer_lp_.clear(); integer_lp_cols_.clear(); @@ -384,6 +384,10 @@ bool LinearProgrammingConstraint::CreateLpFromConstraintManager() { const auto& all_constraints = constraint_manager_.AllConstraints(); for (const auto index : constraint_manager_.LpConstraints()) { const LinearConstraint& ct = all_constraints[index].constraint; + if (ct.lb > ct.ub) { + VLOG(1) << "Trivial infeasible bound in an LP constraint"; + return false; + } integer_lp_.push_back(LinearConstraintInternal()); LinearConstraintInternal& new_ct = integer_lp_.back(); @@ -391,16 +395,15 @@ bool LinearProgrammingConstraint::CreateLpFromConstraintManager() { new_ct.ub = ct.ub; new_ct.lb_is_trivial = all_constraints[index].lb_is_trivial; new_ct.ub_is_trivial = all_constraints[index].ub_is_trivial; - const int size = ct.num_terms; - if (ct.lb > ct.ub) { - VLOG(1) << "Trivial infeasible bound in an LP constraint"; - return false; - } IntegerValue infinity_norm = 0; infinity_norm = std::max(infinity_norm, IntTypeAbs(ct.lb)); infinity_norm = std::max(infinity_norm, IntTypeAbs(ct.ub)); new_ct.start_in_buffer = integer_lp_cols_.size(); + + // TODO(user): Make sure we don't have empty constraint! + // this currently can happen in some corner cases. + const int size = ct.num_terms; new_ct.num_terms = size; for (int i = 0; i < size; ++i) { // We only use positive variable inside this class. @@ -418,12 +421,6 @@ bool LinearProgrammingConstraint::CreateLpFromConstraintManager() { integer_lp_cols_.data() + new_ct.start_in_buffer + new_ct.num_terms)); } - // Copy the integer_lp_ into lp_data_. - lp_data_.Clear(); - for (int i = 0; i < integer_variables_.size(); ++i) { - CHECK_EQ(glop::ColIndex(i), lp_data_.CreateNewVariable()); - } - // We remove fixed variables from the objective. This should help the LP // scaling, but also our integer reason computation. int new_size = 0; @@ -438,49 +435,26 @@ bool LinearProgrammingConstraint::CreateLpFromConstraintManager() { objective_infinity_norm_ = std::max(objective_infinity_norm_, IntTypeAbs(entry.second)); integer_objective_[new_size++] = entry; - lp_data_.SetObjectiveCoefficient(entry.first, ToDouble(entry.second)); } + integer_objective_.resize(new_size); objective_infinity_norm_ = std::max(objective_infinity_norm_, IntTypeAbs(integer_objective_offset_)); - integer_objective_.resize(new_size); - lp_data_.SetObjectiveOffset(ToDouble(integer_objective_offset_)); - for (const LinearConstraintInternal& ct : integer_lp_) { - const ConstraintIndex row = lp_data_.CreateNewConstraint(); + // Scale everything. + // TODO(user): As we have an idea of the LP optimal after the first solves, + // maybe we can adapt the scaling accordingly. + ComputeIntegerLpScalingFactors(); - // TODO(user): Using trivial bound might be good for things like - // sum bool <= 1 since setting the slack in [0, 1] can lead to bound flip in - // the simplex. However if the bound is large, maybe it make more sense to - // use +/- infinity. - const double infinity = std::numeric_limits::infinity(); - lp_data_.SetConstraintBounds( - row, ct.lb_is_trivial ? -infinity : ToDouble(ct.lb), - ct.ub_is_trivial ? +infinity : ToDouble(ct.ub)); - for (int i = 0; i < ct.num_terms; ++i) { - const int index = ct.start_in_buffer + i; - lp_data_.SetCoefficient(row, integer_lp_cols_[index], - ToDouble(integer_lp_coeffs_[index])); - } - } - lp_data_.NotifyThatColumnsAreClean(); + // Tricky: we use level zero bounds here for the second scaling step below. + FillLpData(); - // We scale the LP using the level zero bounds that we later override - // with the current ones. - // - // TODO(user): As part of the scaling, we may also want to shift the initial - // variable bounds so that each variable contain the value zero in their - // domain. Maybe just once and for all at the beginning. - const int num_vars = integer_variables_.size(); - for (int i = 0; i < num_vars; i++) { - const IntegerVariable cp_var = integer_variables_[i]; - const double lb = ToDouble(integer_trail_->LevelZeroLowerBound(cp_var)); - const double ub = ToDouble(integer_trail_->LevelZeroUpperBound(cp_var)); - lp_data_.SetVariableBounds(glop::ColIndex(i), lb, ub); - } + // Fills the helper. + scaler_.ConfigureFromFactors(row_factors_, col_factors_); + scaler_.AverageCostScaling(&obj_with_slack_); + scaler_.ContainOneBoundScaling(simplex_.MutableLowerBounds(), + simplex_.MutableUpperBounds()); - // TODO(user): As we have an idea of the LP optimal after the first solves, - // maybe we can adapt the scaling accordingly. - scaler_.Scale(simplex_params_, &lp_data_); + // Since we used level zero bounds above, fix them. UpdateBoundsOfLpVariables(); // Set the information for the step to polish the LP basis. All our variables @@ -488,6 +462,7 @@ bool LinearProgrammingConstraint::CreateLpFromConstraintManager() { // binary variables. if (parameters_.polish_lp_solution()) { simplex_.ClearIntegralityScales(); + const int num_vars = integer_variables_.size(); for (int i = 0; i < num_vars; ++i) { const IntegerVariable cp_var = integer_variables_[i]; const IntegerValue lb = integer_trail_->LevelZeroLowerBound(cp_var); @@ -499,13 +474,184 @@ bool LinearProgrammingConstraint::CreateLpFromConstraintManager() { } } - lp_data_.NotifyThatColumnsAreClean(); - VLOG(3) << "LP relaxation: " << lp_data_.GetDimensionString() << ". " + VLOG(3) << "LP relaxation: " << integer_lp_.size() << " x " + << integer_variables_.size() << ". " << constraint_manager_.AllConstraints().size() << " Managed constraints."; return true; } +// TODO(user): This is a duplicate of glop scaling code, but it allows to +// work directly on our representation... +void LinearProgrammingConstraint::ComputeIntegerLpScalingFactors() { + const int num_rows = integer_lp_.size(); + const int num_cols = integer_variables_.size(); + + // Assign vectors. + const double infinity = std::numeric_limits::infinity(); + row_factors_.assign(num_rows, 1.0); + col_factors_.assign(num_cols, 1.0); + + // Cache pointers to avoid refetching them. + IntegerValue* coeffs = integer_lp_coeffs_.data(); + glop::ColIndex* cols = integer_lp_cols_.data(); + double* row_factors = row_factors_.data(); + double* col_factors = col_factors_.data(); + + col_min_.assign(num_cols, infinity); + col_max_.assign(num_cols, 0.0); + double* col_min = col_min_.data(); + double* col_max = col_max_.data(); + + for (int i = 0; i < 4; ++i) { + // Scale row geometrically. + for (int row = 0; row < num_rows; ++row) { + double min_scaled = +infinity; + double max_scaled = 0.0; + const LinearConstraintInternal& ct = integer_lp_[RowIndex(row)]; + for (int i = 0; i < ct.num_terms; ++i) { + const int index = ct.start_in_buffer + i; + const int col = cols[index].value(); + const double coeff = static_cast(coeffs[index].value()); + const double scaled_magnitude = col_factors[col] * std::abs(coeff); + min_scaled = std::min(min_scaled, scaled_magnitude); + max_scaled = std::max(max_scaled, scaled_magnitude); + } + + if (ct.num_terms == 0) continue; + const Fractional factor(std::sqrt(max_scaled * min_scaled)); + row_factors[row] = 1.0 / factor; + } + + // Scale columns geometrically. + for (int row = 0; row < num_rows; ++row) { + const double row_factor = row_factors[row]; + const LinearConstraintInternal& ct = integer_lp_[RowIndex(row)]; + for (int i = 0; i < ct.num_terms; ++i) { + const int index = ct.start_in_buffer + i; + const int col = cols[index].value(); + const double coeff = static_cast(coeffs[index].value()); + const double scaled_magnitude = row_factor * std::abs(coeff); + col_min[col] = std::min(col_min[col], scaled_magnitude); + col_max[col] = std::max(col_max[col], scaled_magnitude); + } + } + for (int col = 0; col < num_cols; ++col) { + if (col_min[col] == infinity) continue; // Empty. + col_factors[col] = 1.0 / std::sqrt(col_min[col] * col_max[col]); + + // Reset, in case we have many fixed variable, faster than assign again. + col_min[col] = infinity; + col_max[col] = 0; + } + } + + // Now we equilibrate (i.e. just divide by the max) the row + for (int row = 0; row < num_rows; ++row) { + double max_scaled = 0.0; + const LinearConstraintInternal& ct = integer_lp_[RowIndex(row)]; + for (int i = 0; i < ct.num_terms; ++i) { + const int index = ct.start_in_buffer + i; + const int col = cols[index].value(); + const double coeff = static_cast(coeffs[index].value()); + const double scaled_magnitude = col_factors[col] * std::abs(coeff); + max_scaled = std::max(max_scaled, scaled_magnitude); + } + if (ct.num_terms == 0) continue; + row_factors[row] = 1.0 / max_scaled; + } + + // And finally the columns. + for (int row = 0; row < num_rows; ++row) { + const double row_factor = row_factors[row]; + const LinearConstraintInternal& ct = integer_lp_[RowIndex(row)]; + for (int i = 0; i < ct.num_terms; ++i) { + const int index = ct.start_in_buffer + i; + const int col = cols[index].value(); + const double coeff = static_cast(coeffs[index].value()); + const double scaled_magnitude = row_factor * std::abs(coeff); + col_max[col] = std::max(col_max[col], scaled_magnitude); + } + } + for (int col = 0; col < num_cols; ++col) { + if (col_max[col] == 0) continue; // Empty. + col_factors[col] = 1.0 / col_max[col]; + } +} + +void LinearProgrammingConstraint::FillLpData() { + const int num_rows = integer_lp_.size(); + const int num_cols = integer_variables_.size(); + IntegerValue* coeffs = integer_lp_coeffs_.data(); + glop::ColIndex* cols = integer_lp_cols_.data(); + double* row_factors = row_factors_.data(); + double* col_factors = col_factors_.data(); + + // Now fill the tranposed matrix + glop::CompactSparseMatrix* data = simplex_.MutableTransposedMatrixWithSlack(); + data->Reset(glop::RowIndex(num_cols + num_rows)); + for (int row = 0; row < num_rows; ++row) { + const LinearConstraintInternal& ct = integer_lp_[RowIndex(row)]; + const double row_factor = row_factors[row]; + for (int i = 0; i < ct.num_terms; ++i) { + const int index = ct.start_in_buffer + i; + const int col = cols[index].value(); + const double coeff = static_cast(coeffs[index].value()); + const double scaled_coeff = row_factor * col_factors[col] * coeff; + data->AddEntryToCurrentColumn(RowIndex(col), scaled_coeff); + } + + // Add slack. + data->AddEntryToCurrentColumn(RowIndex(num_cols + row), 1.0); + + // Close column. + data->CloseCurrentColumn(); + } + + // Fill and scale the objective. + const glop::ColIndex num_cols_with_slacks(num_rows + num_cols); + obj_with_slack_.assign(num_cols_with_slacks, 0.0); + for (const auto [col, value] : integer_objective_) { + obj_with_slack_[col] = ToDouble(value) * col_factors[col.value()]; + } + + // Fill and scales the bound. + simplex_.MutableLowerBounds()->resize(num_cols_with_slacks); + simplex_.MutableUpperBounds()->resize(num_cols_with_slacks); + Fractional* lb_with_slack = simplex_.MutableLowerBounds()->data(); + Fractional* ub_with_slack = simplex_.MutableUpperBounds()->data(); + const double infinity = std::numeric_limits::infinity(); + for (int row = 0; row < integer_lp_.size(); ++row) { + const LinearConstraintInternal& ct = integer_lp_[glop::RowIndex(row)]; + + // TODO(user): Using trivial bound might be good for things like + // sum bool <= 1 since setting the slack in [0, 1] can lead to bound flip in + // the simplex. However if the bound is large, maybe it make more sense to + // use +/- infinity. + const double factor = row_factors[row]; + lb_with_slack[num_cols + row] = + ct.ub_is_trivial ? -infinity : ToDouble(-ct.ub) * factor; + ub_with_slack[num_cols + row] = + ct.lb_is_trivial ? +infinity : ToDouble(-ct.lb) * factor; + } + + // We scale the LP using the level zero bounds that we later override + // with the current ones. + // + // TODO(user): As part of the scaling, we may also want to shift the initial + // variable bounds so that each variable contain the value zero in their + // domain. Maybe just once and for all at the beginning. + const int num_vars = integer_variables_.size(); + for (int i = 0; i < num_vars; i++) { + const IntegerVariable cp_var = integer_variables_[i]; + const double factor = col_factors[i]; + lb_with_slack[i] = + ToDouble(integer_trail_->LevelZeroLowerBound(cp_var)) * factor; + ub_with_slack[i] = + ToDouble(integer_trail_->LevelZeroUpperBound(cp_var)) * factor; + } +} + void LinearProgrammingConstraint::FillReducedCostReasonIn( const glop::DenseRow& reduced_costs, std::vector* integer_reason) { @@ -682,12 +828,17 @@ double LinearProgrammingConstraint::GetSolutionReducedCost( void LinearProgrammingConstraint::UpdateBoundsOfLpVariables() { const int num_vars = integer_variables_.size(); + Fractional* lb_with_slack = simplex_.MutableLowerBounds()->data(); + Fractional* ub_with_slack = simplex_.MutableUpperBounds()->data(); for (int i = 0; i < num_vars; i++) { const IntegerVariable cp_var = integer_variables_[i]; - const double lb = ToDouble(integer_trail_->LowerBound(cp_var)); - const double ub = ToDouble(integer_trail_->UpperBound(cp_var)); + const double lb = + static_cast(integer_trail_->LowerBound(cp_var).value()); + const double ub = + static_cast(integer_trail_->UpperBound(cp_var).value()); const double factor = scaler_.VariableScalingFactor(glop::ColIndex(i)); - lp_data_.SetVariableBounds(glop::ColIndex(i), lb * factor, ub * factor); + lb_with_slack[i] = lb * factor; + ub_with_slack[i] = ub * factor; } } @@ -697,7 +848,12 @@ bool LinearProgrammingConstraint::SolveLp() { lp_at_level_zero_is_final_ = false; } - const auto status = simplex_.Solve(lp_data_, time_limit_); + const double unscaling_factor = 1.0 / scaler_.ObjectiveScalingFactor(); + const double offset_before_unscaling = + ToDouble(integer_objective_offset_) * scaler_.ObjectiveScalingFactor(); + const auto status = simplex_.MinimizeFromTransposedMatrixWithSlack( + obj_with_slack_, unscaling_factor, offset_before_unscaling, time_limit_); + state_ = simplex_.GetState(); total_num_simplex_iterations_ += simplex_.GetNumberOfIterations(); if (!status.ok()) { @@ -711,19 +867,14 @@ bool LinearProgrammingConstraint::SolveLp() { << average_degeneracy_.CurrentAverage(); } - // By default we assume the matrix is unchanged. - // This will be reset by CreateLpFromConstraintManager(). - simplex_.NotifyThatMatrixIsUnchangedForNextSolve(); - const int status_as_int = static_cast(simplex_.GetProblemStatus()); if (status_as_int >= num_solves_by_status_.size()) { num_solves_by_status_.resize(status_as_int + 1); } num_solves_++; num_solves_by_status_[status_as_int]++; - VLOG(2) << lp_data_.GetDimensionString() - << " lvl:" << trail_->CurrentDecisionLevel() << " " - << simplex_.GetProblemStatus() + VLOG(2) << DimensionString() << " lvl:" << trail_->CurrentDecisionLevel() + << " " << simplex_.GetProblemStatus() << " iter:" << simplex_.GetNumberOfIterations() << " obj:" << simplex_.GetObjectiveValue() << " scaled:" << objective_definition_->ScaleObjective( @@ -1285,11 +1436,17 @@ void LinearProgrammingConstraint::AddCGCuts() { const bool old_gomory = true; // Note that the index is permuted and do not correspond to a row. - const RowIndex num_rows = lp_data_.num_constraints(); + const RowIndex num_rows(integer_lp_.size()); for (RowIndex index(0); index < num_rows; ++index) { if (time_limit_->LimitReached()) break; const ColIndex basis_col = simplex_.GetBasis(index); + + // If this variable is a slack, we ignore it. This is because the + // corresponding row is not tight under the given lp values. + if (old_gomory && basis_col >= integer_variables_.size()) continue; + + // TODO(user): If the variable is a slack, the unscaling is wrong! const Fractional lp_value = GetVariableValueAtCpScale(basis_col); // Only consider fractional basis element. We ignore element that are close @@ -1300,10 +1457,6 @@ void LinearProgrammingConstraint::AddCGCuts() { // also be just under it. if (std::abs(lp_value - std::round(lp_value)) < 0.01) continue; - // If this variable is a slack, we ignore it. This is because the - // corresponding row is not tight under the given lp values. - if (old_gomory && basis_col >= integer_variables_.size()) continue; - // TODO(user): Avoid code duplication between the sparse/dense path. tmp_lp_multipliers_.clear(); const glop::ScatteredRow& lambda = simplex_.GetUnitRowLeftInverse(index); @@ -1464,9 +1617,12 @@ void LinearProgrammingConstraint::AddMirCuts() { // We compute all the rows that are tight, these will be used as the base row // for the MIR_n procedure below. - const int num_rows = lp_data_.num_constraints().value(); + const int num_cols = integer_variables_.size(); + const int num_rows = integer_lp_.size(); std::vector> base_rows; util_intops::StrongVector row_weights(num_rows, 0.0); + Fractional* lb_with_slack = simplex_.MutableLowerBounds()->data(); + Fractional* ub_with_slack = simplex_.MutableUpperBounds()->data(); for (RowIndex row(0); row < num_rows; ++row) { // We only consider tight rows. // We use both the status and activity to have as much options as possible. @@ -1476,12 +1632,14 @@ void LinearProgrammingConstraint::AddMirCuts() { // cannot be good. const auto status = simplex_.GetConstraintStatus(row); const double activity = simplex_.GetConstraintActivity(row); - if (activity > lp_data_.constraint_upper_bounds()[row] - 1e-4 || + const double ct_lb = -ub_with_slack[num_cols + row.value()]; + const double ct_ub = -lb_with_slack[num_cols + row.value()]; + if (activity > ct_ub - 1e-4 || status == glop::ConstraintStatus::AT_UPPER_BOUND || status == glop::ConstraintStatus::FIXED_VALUE) { base_rows.push_back({row, IntegerValue(1)}); } - if (activity < lp_data_.constraint_lower_bounds()[row] + 1e-4 || + if (activity < ct_lb + 1e-4 || status == glop::ConstraintStatus::AT_LOWER_BOUND || status == glop::ConstraintStatus::FIXED_VALUE) { base_rows.push_back({row, IntegerValue(-1)}); @@ -1514,6 +1672,7 @@ void LinearProgrammingConstraint::AddMirCuts() { std::vector weights; util_intops::StrongVector used_rows; std::vector> integer_multipliers; + const auto matrix = simplex_.MatrixWithSlack().view(); for (const std::pair& entry : base_rows) { if (time_limit_->LimitReached()) break; if (dtime_num_entries > 1e7) break; @@ -1567,8 +1726,7 @@ void LinearProgrammingConstraint::AddMirCuts() { if (dense_cut[col] == 0) continue; max_magnitude = std::max(max_magnitude, IntTypeAbs(dense_cut[col])); - const int col_degree = - lp_data_.GetSparseColumn(col).num_entries().value(); + const int col_degree = matrix.ColumnNumEntries(col).value(); if (col_degree <= 1) continue; if (simplex_.GetVariableStatus(col) != glop::VariableStatus::BASIC) { continue; @@ -1592,8 +1750,9 @@ void LinearProgrammingConstraint::AddMirCuts() { // What rows can we add to eliminate var_to_eliminate? std::vector possible_rows; weights.clear(); - for (const auto entry : lp_data_.GetSparseColumn(var_to_eliminate)) { - const RowIndex row = entry.row(); + for (const auto entry_index : matrix.Column(var_to_eliminate)) { + const RowIndex row = matrix.EntryRow(entry_index); + const glop::Fractional coeff = matrix.EntryCoefficient(entry_index); // We disallow all the rows that contain a variable that we already // eliminated (or are about to). This mean that we choose rows that @@ -1608,14 +1767,14 @@ void LinearProgrammingConstraint::AddMirCuts() { // still be chosen after the tight-one in most situation. bool add_row = false; if (!integer_lp_[row].ub_is_trivial) { - if (entry.coefficient() > 0.0) { + if (coeff > 0.0) { if (dense_cut[var_to_eliminate] < 0) add_row = true; } else { if (dense_cut[var_to_eliminate] > 0) add_row = true; } } if (!integer_lp_[row].lb_is_trivial) { - if (entry.coefficient() > 0.0) { + if (coeff > 0.0) { if (dense_cut[var_to_eliminate] > 0) add_row = true; } else { if (dense_cut[var_to_eliminate] < 0) add_row = true; @@ -2409,11 +2568,8 @@ void LinearProgrammingConstraint::ReducedCostStrengtheningDeductions( double cp_objective_delta) { deductions_.clear(); - // TRICKY: while simplex_.GetObjectiveValue() use the objective scaling factor - // stored in the lp_data_, all the other functions like GetReducedCost() or - // GetVariableValue() do not. const double lp_objective_delta = - cp_objective_delta / lp_data_.objective_scaling_factor(); + cp_objective_delta / scaler_.ObjectiveScalingFactor(); const int num_vars = integer_variables_.size(); for (int i = 0; i < num_vars; i++) { const IntegerVariable cp_var = integer_variables_[i]; @@ -2596,5 +2752,10 @@ absl::Span LinearProgrammingConstraint::IntegerLpRowCoeffs( return {integer_lp_coeffs_.data() + start, num_terms}; } +std::string LinearProgrammingConstraint::DimensionString() const { + return absl::StrFormat("%d rows, %d columns, %d entries", integer_lp_.size(), + integer_variables_.size(), integer_lp_coeffs_.size()); +} + } // namespace sat } // namespace operations_research diff --git a/ortools/sat/linear_programming_constraint.h b/ortools/sat/linear_programming_constraint.h index ae988777085..bdf34fc6869 100644 --- a/ortools/sat/linear_programming_constraint.h +++ b/ortools/sat/linear_programming_constraint.h @@ -190,7 +190,7 @@ class LinearProgrammingConstraint : public PropagatorInterface, const std::vector& integer_variables() const { return integer_variables_; } - std::string DimensionString() const { return lp_data_.GetDimensionString(); } + std::string DimensionString() const; // Returns a IntegerLiteral guided by the underlying LP constraints. // @@ -408,6 +408,15 @@ class LinearProgrammingConstraint : public PropagatorInterface, absl::Span IntegerLpRowCols(glop::RowIndex row) const; absl::Span IntegerLpRowCoeffs(glop::RowIndex row) const; + void ComputeIntegerLpScalingFactors(); + void FillLpData(); + + // For ComputeIntegerLpScalingFactors(). + std::vector row_factors_; + std::vector col_factors_; + std::vector col_max_; + std::vector col_min_; + // This epsilon is related to the precision of the value/reduced_cost returned // by the LP once they have been scaled back into the CP domain. So for large // domain or cost coefficient, we may have some issues. @@ -456,8 +465,9 @@ class LinearProgrammingConstraint : public PropagatorInterface, // Underlying LP solver API. glop::GlopParameters simplex_params_; glop::BasisState state_; - glop::LinearProgram lp_data_; + glop::DenseRow obj_with_slack_; glop::RevisedSimplex simplex_; + int64_t next_simplex_iter_ = 500; // For the scaling. diff --git a/ortools/sat/work_assignment_test.cc b/ortools/sat/work_assignment_test.cc new file mode 100644 index 00000000000..2236c3d0124 --- /dev/null +++ b/ortools/sat/work_assignment_test.cc @@ -0,0 +1,545 @@ +// Copyright 2010-2024 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/sat/work_assignment.h" + +#include + +#include "absl/strings/string_view.h" +#include "gtest/gtest.h" +#include "net/proto2/contrib/parse_proto/parse_text_proto.h" +#include "ortools/base/gmock.h" +#include "ortools/sat/cp_model.h" +#include "ortools/sat/cp_model.pb.h" +#include "ortools/sat/cp_model_checker.h" +#include "ortools/sat/cp_model_loader.h" +#include "ortools/sat/cp_model_solver.h" +#include "ortools/sat/integer.h" +#include "ortools/sat/model.h" +#include "ortools/sat/sat_parameters.pb.h" +#include "ortools/sat/synchronization.h" + +namespace operations_research { +namespace sat { +namespace { + +TEST(ProtoTrailTest, PushLevel) { + ProtoTrail p; + p.PushLevel({0, 0}, 0, 1); + + EXPECT_EQ(p.MaxLevel(), 1); + EXPECT_EQ(p.Decision(1), ProtoLiteral(0, 0)); + EXPECT_EQ(p.ObjectiveLb(1), 0); +} + +TEST(ProtoTrailTest, AddImplications) { + ProtoTrail p; + p.PushLevel({0, 0}, 0, 1); + p.PushLevel({1, 0}, 1, 2); + p.PushLevel({2, 0}, 2, 3); + p.PushLevel({3, 0}, 2, 4); + + p.AddImplication(2, {5, 0}); + p.AddImplication(3, {6, 0}); + + EXPECT_THAT(p.Implications(2), testing::ElementsAre(ProtoLiteral(5, 0))); + EXPECT_THAT(p.Implications(3), testing::ElementsAre(ProtoLiteral(6, 0))); + p.SetLevelImplied(3); + EXPECT_THAT(p.Implications(2), + testing::UnorderedElementsAre( + ProtoLiteral(5, 0), ProtoLiteral(2, 0), ProtoLiteral(6, 0))); +} + +TEST(ProtoTrailTest, SetLevel1Implied) { + ProtoTrail p; + p.PushLevel({0, 0}, 0, 1); + p.PushLevel({1, 0}, 1, 2); + p.PushLevel({2, 0}, 2, 3); + + p.SetLevelImplied(1); + + EXPECT_THAT(p.NodeIds(0), testing::ElementsAre(1)); + EXPECT_THAT(p.NodeIds(1), testing::ElementsAre(2)); + EXPECT_THAT(p.NodeIds(2), testing::ElementsAre(3)); + EXPECT_EQ(p.MaxLevel(), 2); + EXPECT_EQ(p.Decision(1), ProtoLiteral(1, 0)); + EXPECT_EQ(p.Decision(2), ProtoLiteral(2, 0)); + EXPECT_EQ(p.ObjectiveLb(1), 1); + EXPECT_EQ(p.ObjectiveLb(2), 2); +} + +TEST(ProtoTrailTest, SetMidLevelImplied) { + ProtoTrail p; + p.PushLevel({0, 0}, 0, 1); + p.PushLevel({1, 0}, 1, 2); + p.PushLevel({2, 0}, 2, 3); + + p.SetLevelImplied(2); + + EXPECT_THAT(p.NodeIds(0), testing::IsEmpty()); + EXPECT_THAT(p.NodeIds(1), testing::ElementsAre(1, 2)); + EXPECT_THAT(p.NodeIds(2), testing::ElementsAre(3)); + EXPECT_EQ(p.MaxLevel(), 2); + EXPECT_EQ(p.Decision(1), ProtoLiteral(0, 0)); + EXPECT_EQ(p.Decision(2), ProtoLiteral(2, 0)); + EXPECT_EQ(p.ObjectiveLb(1), 1); + EXPECT_EQ(p.ObjectiveLb(2), 2); +} + +TEST(ProtoTrailTest, SetFinalLevelImplied) { + ProtoTrail p; + p.PushLevel({0, 0}, 0, 1); + p.PushLevel({1, 0}, 1, 2); + p.PushLevel({2, 0}, 2, 3); + + p.SetLevelImplied(3); + + EXPECT_THAT(p.NodeIds(0), testing::IsEmpty()); + EXPECT_THAT(p.NodeIds(1), testing::ElementsAre(1)); + EXPECT_THAT(p.NodeIds(2), testing::ElementsAre(2, 3)); + EXPECT_EQ(p.MaxLevel(), 2); + EXPECT_EQ(p.Decision(1), ProtoLiteral(0, 0)); + EXPECT_EQ(p.Decision(2), ProtoLiteral(1, 0)); + EXPECT_EQ(p.ObjectiveLb(1), 0); + EXPECT_EQ(p.ObjectiveLb(2), 2); +} + +TEST(ProtoTrailTest, SetMultiLevelImplied) { + ProtoTrail p; + p.PushLevel({0, 0}, 0, 1); + p.PushLevel({1, 0}, 1, 2); + p.PushLevel({2, 0}, 2, 3); + + p.SetLevelImplied(3); + p.SetLevelImplied(1); + + EXPECT_EQ(p.MaxLevel(), 1); + EXPECT_EQ(p.Decision(1), ProtoLiteral(1, 0)); + EXPECT_EQ(p.ObjectiveLb(1), 2); +} + +TEST(ProtoTrailTest, Clear) { + ProtoTrail p; + p.PushLevel({0, 0}, 0, 1); + p.PushLevel({1, 0}, 1, 2); + p.PushLevel({2, 0}, 2, 3); + + p.Clear(); + + EXPECT_EQ(p.MaxLevel(), 0); +} + +class SharedTreeSolveTest : public testing::TestWithParam { + public: + SatParameters GetParams() { + SatParameters params; + params.set_num_workers(4); + params.set_shared_tree_num_workers(4); + params.set_cp_model_presolve(false); + params.MergeFrom( + google::protobuf::contrib::parse_proto::ParseTextProtoOrDie( + GetParam())); + return params; + } +}; +INSTANTIATE_TEST_SUITE_P( + SharedTreeParams, SharedTreeSolveTest, + testing::Values("shared_tree_worker_enable_trail_sharing:false", + "shared_tree_worker_enable_trail_sharing:true")); + +TEST_P(SharedTreeSolveTest, SmokeTest) { + CpModelBuilder model_builder; + auto bool_var = model_builder.NewBoolVar(); + auto int_var = model_builder.NewIntVar({0, 7}); + model_builder.AddLessOrEqual(int_var, 3).OnlyEnforceIf(bool_var); + model_builder.Maximize(int_var + 5 * bool_var); + Model model; + SatParameters params = GetParams(); + model.Add(NewSatParameters(params)); + + CpSolverResponse response = SolveCpModel(model_builder.Build(), &model); + + EXPECT_EQ(model.GetOrCreate()->NumWorkers(), + params.shared_tree_num_workers()); + ASSERT_EQ(response.status(), OPTIMAL) + << "Validation: " << ValidateCpModel(model_builder.Build()); + EXPECT_EQ(response.objective_value(), 5 + 3); + EXPECT_EQ(SolutionBooleanValue(response, bool_var), true); + EXPECT_EQ(SolutionIntegerValue(response, int_var), 3); +} + +TEST_P(SharedTreeSolveTest, FeasiblePidgeonHoleSmokeTest) { + CpModelBuilder model_builder; + const int pidgeons = 10; + const int holes = 10; + std::vector count_per_hole(holes); + IntVar max_pidgeon_hole_product = + model_builder.NewIntVar({0, pidgeons * holes}); + for (int i = 0; i < pidgeons; ++i) { + LinearExpr count_per_pidgeon; + for (int j = 0; j < holes; ++j) { + auto var = model_builder.NewBoolVar(); + count_per_hole[j] += LinearExpr(var); + count_per_pidgeon += LinearExpr(var); + model_builder + .AddGreaterOrEqual(max_pidgeon_hole_product, (i + 1) * (j + 1)) + .OnlyEnforceIf(var); + } + model_builder.AddEquality(count_per_pidgeon, 1); + } + for (const auto& count : count_per_hole) { + model_builder.AddLessOrEqual(count, 1); + } + Model model; + SatParameters params = GetParams(); + model.Add(NewSatParameters(params)); + + CpSolverResponse response = SolveCpModel(model_builder.Build(), &model); + + EXPECT_EQ(model.GetOrCreate()->NumWorkers(), 4); + EXPECT_EQ(response.status(), OPTIMAL); +} + +TEST_P(SharedTreeSolveTest, InfeasiblePidgeonHoleSmokeTest) { + CpModelBuilder model_builder; + const int pidgeons = 10; + const int holes = 9; + std::vector count_per_hole(holes); + IntVar max_pidgeon_hole_product = + model_builder.NewIntVar({0, pidgeons * holes}); + for (int i = 0; i < pidgeons; ++i) { + LinearExpr count_per_pidgeon; + for (int j = 0; j < holes; ++j) { + auto var = model_builder.NewBoolVar(); + count_per_hole[j] += LinearExpr(var); + count_per_pidgeon += LinearExpr(var); + model_builder + .AddGreaterOrEqual(max_pidgeon_hole_product, (i + 1) * (j + 1)) + .OnlyEnforceIf(var); + } + model_builder.AddEquality(count_per_pidgeon, 1); + } + for (const auto& count : count_per_hole) { + model_builder.AddLessOrEqual(count, 1); + } + Model model; + SatParameters params = GetParams(); + model.Add(NewSatParameters(params)); + + CpSolverResponse response = SolveCpModel(model_builder.Build(), &model); + + EXPECT_EQ(model.GetOrCreate()->NumWorkers(), 4); + EXPECT_EQ(response.status(), INFEASIBLE); +} + +TEST(SharedTreeManagerTest, SplitTest) { + CpModelBuilder model_builder; + auto bool_var = model_builder.NewBoolVar(); + auto int_var = model_builder.NewIntVar({0, 7}); + model_builder.AddLessOrEqual(int_var, 3).OnlyEnforceIf(bool_var); + model_builder.Maximize(int_var); + Model model; + SatParameters params; + params.set_num_workers(4); + params.set_shared_tree_num_workers(4); + params.set_cp_model_presolve(false); + model.Add(NewSatParameters(params)); + LoadVariables(model_builder.Build(), false, &model); + auto* shared_tree_manager = model.GetOrCreate(); + ProtoTrail shared_trail; + + shared_tree_manager->ProposeSplit(shared_trail, {-1, 0}); + + EXPECT_EQ(shared_trail.MaxLevel(), 1); +} + +TEST(SharedTreeManagerTest, RestartTest) { + CpModelBuilder model_builder; + auto bool_var = model_builder.NewBoolVar(); + auto int_var = model_builder.NewIntVar({0, 7}); + model_builder.AddLessOrEqual(int_var, 3).OnlyEnforceIf(bool_var); + model_builder.Maximize(int_var); + Model model; + SatParameters params; + params.set_num_workers(4); + params.set_shared_tree_num_workers(4); + params.set_cp_model_presolve(false); + model.Add(NewSatParameters(params)); + LoadVariables(model_builder.Build(), false, &model); + auto* shared_tree_manager = model.GetOrCreate(); + ProtoTrail shared_trail; + + shared_tree_manager->ProposeSplit(shared_trail, {-1, 0}); + shared_tree_manager->Restart(); + shared_tree_manager->SyncTree(shared_trail); + + EXPECT_EQ(shared_trail.MaxLevel(), 0); +} + +TEST(SharedTreeManagerTest, RestartTestWithLevelZeroImplications) { + CpModelBuilder model_builder; + auto bool_var = model_builder.NewBoolVar(); + auto int_var = model_builder.NewIntVar({0, 7}); + model_builder.AddLessOrEqual(int_var, 3).OnlyEnforceIf(bool_var); + model_builder.Maximize(int_var); + Model model; + SatParameters params; + params.set_num_workers(4); + params.set_shared_tree_num_workers(4); + params.set_cp_model_presolve(false); + model.Add(NewSatParameters(params)); + LoadVariables(model_builder.Build(), false, &model); + auto* shared_tree_manager = model.GetOrCreate(); + ProtoTrail shared_trail; + + shared_tree_manager->ProposeSplit(shared_trail, {-1, 0}); + shared_tree_manager->CloseTree(shared_trail, 1); + shared_tree_manager->SyncTree(shared_trail); + shared_tree_manager->ReplaceTree(shared_trail); + shared_tree_manager->Restart(); + shared_tree_manager->SyncTree(shared_trail); + + EXPECT_EQ(shared_trail.NodeIds(0).size(), 0); + EXPECT_EQ(shared_trail.MaxLevel(), 0); +} + +TEST(SharedTreeManagerTest, SharedBranchingTest) { + CpModelBuilder model_builder; + auto bool_var = model_builder.NewBoolVar(); + auto int_var = model_builder.NewIntVar({0, 7}); + model_builder.AddLessOrEqual(int_var, 3).OnlyEnforceIf(bool_var); + model_builder.Maximize(int_var); + Model model; + SatParameters params; + params.set_num_workers(2); + params.set_shared_tree_num_workers(2); + params.set_cp_model_presolve(false); + model.Add(NewSatParameters(params)); + LoadVariables(model_builder.Build(), false, &model); + auto* shared_tree_manager = model.GetOrCreate(); + ProtoTrail trail1, trail2; + + shared_tree_manager->ProposeSplit(trail1, {-1, 0}); + shared_tree_manager->ReplaceTree(trail2); + + EXPECT_EQ(trail1.MaxLevel(), 1); + EXPECT_EQ(trail2.MaxLevel(), 1); + EXPECT_EQ(trail1.Decision(1), trail2.Decision(1).Negated()); +} + +TEST(SharedTreeManagerTest, ObjectiveLbSplitTest) { + CpModelBuilder model_builder; + auto bool_var = model_builder.NewBoolVar(); + auto int_var = model_builder.NewIntVar({0, 7}); + model_builder.AddLessOrEqual(int_var, 3).OnlyEnforceIf(bool_var); + model_builder.Maximize(int_var); + Model model; + SatParameters params; + params.set_num_workers(4); + params.set_shared_tree_num_workers(4); + params.set_cp_model_presolve(false); + params.set_shared_tree_split_strategy( + SatParameters::SPLIT_STRATEGY_OBJECTIVE_LB); + model.Add(NewSatParameters(params)); + LoadVariables(model_builder.Build(), false, &model); + auto* response_manager = model.GetOrCreate(); + response_manager->InitializeObjective(model_builder.Build()); + auto* shared_tree_manager = model.GetOrCreate(); + ProtoTrail trail1, trail2; + + shared_tree_manager->ProposeSplit(trail1, {-1, 0}); + ASSERT_EQ(trail1.MaxLevel(), 1); + trail1.SetObjectiveLb(1, 2); + shared_tree_manager->SyncTree(trail1); + shared_tree_manager->ReplaceTree(trail2); + ASSERT_EQ(trail2.MaxLevel(), 1); + trail2.SetObjectiveLb(1, 1); + shared_tree_manager->SyncTree(trail2); + // Reject this split because it is not at the global lower bound. + shared_tree_manager->ProposeSplit(trail1, {int_var.index(), 3}); + + EXPECT_EQ(response_manager->GetInnerObjectiveLowerBound(), 1); + EXPECT_EQ(shared_tree_manager->NumNodes(), 3); +} + +TEST(SharedTreeManagerTest, DiscrepancySplitTestOneLeafPerWorker) { + CpModelBuilder model_builder; + auto bool_var = model_builder.NewBoolVar(); + auto int_var = model_builder.NewIntVar({0, 7}); + model_builder.AddLessOrEqual(int_var, 3).OnlyEnforceIf(bool_var); + model_builder.Maximize(int_var); + Model model; + SatParameters params; + params.set_num_workers(4); + params.set_shared_tree_num_workers(4); + params.set_shared_tree_open_leaves_per_worker(1); + params.set_cp_model_presolve(false); + params.set_shared_tree_split_strategy( + SatParameters::SPLIT_STRATEGY_DISCREPANCY); + model.Add(NewSatParameters(params)); + LoadVariables(model_builder.Build(), false, &model); + auto* response_manager = model.GetOrCreate(); + response_manager->InitializeObjective(model_builder.Build()); + auto* shared_tree_manager = model.GetOrCreate(); + ProtoTrail trail1, trail2; + + shared_tree_manager->ProposeSplit(trail1, {-1, 0}); + shared_tree_manager->SyncTree(trail1); + shared_tree_manager->ReplaceTree(trail2); + shared_tree_manager->ProposeSplit(trail2, {int_var.index(), 3}); + shared_tree_manager->ProposeSplit(trail1, {int_var.index(), 3}); + // Reject this split: 2 depth + 1 discrepancy is not minimal. + shared_tree_manager->ProposeSplit(trail2, {int_var.index(), 5}); + // Reject this split: 2 depth + 0 discrepancy is not minimal. + shared_tree_manager->ProposeSplit(trail1, {int_var.index(), 5}); + + EXPECT_EQ(trail1.MaxLevel(), 2); + EXPECT_EQ(trail2.MaxLevel(), 2); + EXPECT_EQ(shared_tree_manager->NumNodes(), 7); +} + +TEST(SharedTreeManagerTest, DiscrepancySplitTest) { + CpModelBuilder model_builder; + auto bool_var = model_builder.NewBoolVar(); + auto int_var = model_builder.NewIntVar({0, 7}); + model_builder.AddLessOrEqual(int_var, 3).OnlyEnforceIf(bool_var); + model_builder.Maximize(int_var); + Model model; + SatParameters params; + params.set_num_workers(2); + params.set_shared_tree_num_workers(2); + params.set_shared_tree_open_leaves_per_worker(2); + params.set_cp_model_presolve(false); + params.set_shared_tree_split_strategy( + SatParameters::SPLIT_STRATEGY_DISCREPANCY); + model.Add(NewSatParameters(params)); + LoadVariables(model_builder.Build(), false, &model); + auto* response_manager = model.GetOrCreate(); + response_manager->InitializeObjective(model_builder.Build()); + auto* shared_tree_manager = model.GetOrCreate(); + ProtoTrail trail1, trail2; + + shared_tree_manager->ProposeSplit(trail1, {-1, 0}); + shared_tree_manager->SyncTree(trail1); + shared_tree_manager->ReplaceTree(trail2); + shared_tree_manager->ProposeSplit(trail2, {int_var.index(), 3}); + shared_tree_manager->ProposeSplit(trail1, {int_var.index(), 3}); + // Reject this split: 2 depth + 1 discrepancy is not minimal. + shared_tree_manager->ProposeSplit(trail2, {int_var.index(), 5}); + // Reject this split: 2 depth + 0 discrepancy is not minimal. + shared_tree_manager->ProposeSplit(trail1, {int_var.index(), 5}); + + EXPECT_EQ(trail1.MaxLevel(), 2); + EXPECT_EQ(trail2.MaxLevel(), 2); + EXPECT_EQ(shared_tree_manager->NumNodes(), 7); +} + +TEST(SharedTreeManagerTest, BalancedSplitTestOneLeafPerWorker) { + CpModelBuilder model_builder; + auto bool_var = model_builder.NewBoolVar(); + auto int_var = model_builder.NewIntVar({0, 7}); + model_builder.AddLessOrEqual(int_var, 3).OnlyEnforceIf(bool_var); + model_builder.Maximize(int_var); + Model model; + SatParameters params; + params.set_num_workers(5); + params.set_shared_tree_num_workers(5); + params.set_shared_tree_open_leaves_per_worker(1); + params.set_cp_model_presolve(false); + params.set_shared_tree_split_strategy( + SatParameters::SPLIT_STRATEGY_BALANCED_TREE); + model.Add(NewSatParameters(params)); + LoadVariables(model_builder.Build(), false, &model); + auto* response_manager = model.GetOrCreate(); + response_manager->InitializeObjective(model_builder.Build()); + auto* shared_tree_manager = model.GetOrCreate(); + ProtoTrail trail1, trail2; + + shared_tree_manager->ProposeSplit(trail1, {-1, 0}); + shared_tree_manager->SyncTree(trail1); + shared_tree_manager->ReplaceTree(trail2); + shared_tree_manager->SyncTree(trail2); + shared_tree_manager->ProposeSplit(trail1, {int_var.index(), 3}); + // Reject this split because it creates an unbalanced tree + shared_tree_manager->ProposeSplit(trail1, {int_var.index(), 5}); + shared_tree_manager->ProposeSplit(trail2, {int_var.index(), 3}); + + EXPECT_EQ(shared_tree_manager->NumNodes(), 7); + EXPECT_EQ(trail1.MaxLevel(), 2); + EXPECT_EQ(trail2.MaxLevel(), 2); +} + +TEST(SharedTreeManagerTest, BalancedSplitTest) { + CpModelBuilder model_builder; + auto bool_var = model_builder.NewBoolVar(); + auto int_var = model_builder.NewIntVar({0, 7}); + model_builder.AddLessOrEqual(int_var, 3).OnlyEnforceIf(bool_var); + model_builder.Maximize(int_var); + Model model; + SatParameters params; + params.set_num_workers(3); + params.set_shared_tree_num_workers(3); + params.set_shared_tree_open_leaves_per_worker(2); + params.set_cp_model_presolve(false); + params.set_shared_tree_split_strategy( + SatParameters::SPLIT_STRATEGY_BALANCED_TREE); + model.Add(NewSatParameters(params)); + LoadVariables(model_builder.Build(), false, &model); + auto* response_manager = model.GetOrCreate(); + response_manager->InitializeObjective(model_builder.Build()); + auto* shared_tree_manager = model.GetOrCreate(); + ProtoTrail trail1, trail2; + + shared_tree_manager->ProposeSplit(trail1, {-1, 0}); + shared_tree_manager->SyncTree(trail1); + shared_tree_manager->ReplaceTree(trail2); + shared_tree_manager->SyncTree(trail2); + shared_tree_manager->ProposeSplit(trail1, {int_var.index(), 3}); + // Reject this split because it creates an unbalanced tree + shared_tree_manager->ProposeSplit(trail1, {int_var.index(), 5}); + shared_tree_manager->ProposeSplit(trail2, {int_var.index(), 3}); + + EXPECT_EQ(shared_tree_manager->NumNodes(), 7); + EXPECT_EQ(trail1.MaxLevel(), 2); + EXPECT_EQ(trail2.MaxLevel(), 2); +} + +TEST(SharedTreeManagerTest, CloseTreeTest) { + CpModelBuilder model_builder; + auto bool_var = model_builder.NewBoolVar(); + auto int_var = model_builder.NewIntVar({0, 7}); + model_builder.AddLessOrEqual(int_var, 3).OnlyEnforceIf(bool_var); + model_builder.Maximize(int_var); + Model model; + SatParameters params; + params.set_num_workers(4); + params.set_shared_tree_num_workers(4); + params.set_cp_model_presolve(false); + model.Add(NewSatParameters(params)); + LoadVariables(model_builder.Build(), false, &model); + auto* shared_tree_manager = model.GetOrCreate(); + ProtoTrail trail1, trail2, trail3; + shared_tree_manager->ProposeSplit(trail1, {-1, 0}); + shared_tree_manager->ReplaceTree(trail2); + shared_tree_manager->ProposeSplit(trail1, {1, 0}); + shared_tree_manager->CloseTree(trail1, 1); + shared_tree_manager->ReplaceTree(trail1); + + EXPECT_EQ(trail1.MaxLevel(), 0); + EXPECT_EQ(trail2.MaxLevel(), 1); + EXPECT_EQ(trail2.Decision(1), ProtoLiteral(0, 1)); +} +// TODO(user): Test objective propagation. +} // namespace +} // namespace sat +} // namespace operations_research From 0b2ecb11d325bb5eb8c7042adbce6bd34176152c Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Mon, 7 Oct 2024 10:02:36 +0200 Subject: [PATCH 09/11] cmake: fix examples/tests/ configure --- examples/tests/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/tests/CMakeLists.txt b/examples/tests/CMakeLists.txt index 622c576fecd..5d49f347e90 100644 --- a/examples/tests/CMakeLists.txt +++ b/examples/tests/CMakeLists.txt @@ -18,7 +18,7 @@ endif() if(BUILD_CXX_EXAMPLES) file(GLOB CXX_SRCS "*.cc") foreach(FILE_NAME IN LISTS CXX_SRCS) - add_cxx_test(FILE_NAME ${FILE_NAME}) + ortools_cxx_test(FILE_NAME ${FILE_NAME}) endforeach() endif() From 205e2d91293c51654f69e2574f8c0ac2e2a69e49 Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Mon, 7 Oct 2024 10:40:57 +0200 Subject: [PATCH 10/11] Export lp_data and glop from google3 --- ortools/glop/revised_simplex.cc | 109 ++++++++++++++++++------- ortools/glop/revised_simplex.h | 36 ++++++--- ortools/glop/variables_info.cc | 10 +++ ortools/glop/variables_info.h | 6 ++ ortools/lp_data/lp_data_utils.cc | 134 ++++++++++++++++++++++++++----- ortools/lp_data/lp_data_utils.h | 35 ++++++-- ortools/lp_data/sparse.cc | 16 +++- ortools/lp_data/sparse.h | 4 + 8 files changed, 281 insertions(+), 69 deletions(-) diff --git a/ortools/glop/revised_simplex.cc b/ortools/glop/revised_simplex.cc index 88549afa300..2a7e20dc8ac 100644 --- a/ortools/glop/revised_simplex.cc +++ b/ortools/glop/revised_simplex.cc @@ -154,29 +154,72 @@ void RevisedSimplex::SetStartingVariableValuesForNextSolve( variable_starting_values_ = values; } -void RevisedSimplex::NotifyThatMatrixIsUnchangedForNextSolve() { - notify_that_matrix_is_unchanged_ = true; -} +Status RevisedSimplex::MinimizeFromTransposedMatrixWithSlack( + const DenseRow& objective, Fractional objective_scaling_factor, + Fractional objective_offset, TimeLimit* time_limit) { + const double start_time = time_limit->GetElapsedTime(); + default_logger_.EnableLogging(parameters_.log_search_progress()); + default_logger_.SetLogToStdOut(parameters_.log_to_stdout()); + parameters_ = initial_parameters_; + PropagateParameters(); + + // The source of truth is the transposed matrix. + if (transpose_was_changed_) { + compact_matrix_.PopulateFromTranspose(transposed_matrix_); + num_rows_ = compact_matrix_.num_rows(); + num_cols_ = compact_matrix_.num_cols(); + first_slack_col_ = num_cols_ - RowToColIndex(num_rows_); + } + + DCHECK_EQ(num_cols_, objective.size()); + + // Copy objective + objective_scaling_factor_ = objective_scaling_factor; + objective_offset_ = objective_offset; + const bool objective_is_unchanged = objective_ == objective; + objective_ = objective; + InitializeObjectiveLimit(); + + // Initialize variable infos from the mutated bounds. + variables_info_.InitializeFromMutatedState(); + + if (objective_is_unchanged && parameters_.use_dual_simplex() && + !transpose_was_changed_ && !solution_state_has_been_set_externally_ && + !solution_state_.IsEmpty()) { + // Fast track if we just changed variable bounds. + primal_edge_norms_.Clear(); + variables_info_.InitializeFromBasisState(first_slack_col_, ColIndex(0), + solution_state_); + variable_values_.ResetAllNonBasicVariableValues(variable_starting_values_); + variable_values_.RecomputeBasicVariableValues(); + return SolveInternal(start_time, false, objective, time_limit); + } else { + GLOP_RETURN_IF_ERROR(FinishInitialization(true)); + } -void RevisedSimplex::NotifyThatMatrixIsChangedForNextSolve() { - notify_that_matrix_is_unchanged_ = false; + return SolveInternal(start_time, false, objective, time_limit); } Status RevisedSimplex::Solve(const LinearProgram& lp, TimeLimit* time_limit) { - SCOPED_TIME_STAT(&function_stats_); + const double start_time = time_limit->GetElapsedTime(); + default_logger_.EnableLogging(parameters_.log_search_progress()); + default_logger_.SetLogToStdOut(parameters_.log_to_stdout()); + DCHECK(lp.IsCleanedUp()); + GLOP_RETURN_IF_ERROR(Initialize(lp)); + return SolveInternal(start_time, lp.IsMaximizationProblem(), + lp.objective_coefficients(), time_limit); +} + +ABSL_MUST_USE_RESULT Status RevisedSimplex::SolveInternal( + double start_time, bool is_maximization_problem, + const DenseRow& objective_coefficients, TimeLimit* time_limit) { + SCOPED_TIME_STAT(&function_stats_); GLOP_RETURN_ERROR_IF_NULL(time_limit); Cleanup update_deterministic_time_on_return( [this, time_limit]() { AdvanceDeterministicTime(time_limit); }); - default_logger_.EnableLogging(parameters_.log_search_progress()); - default_logger_.SetLogToStdOut(parameters_.log_to_stdout()); SOLVER_LOG(logger_, ""); - - // Initialization. Note That Initialize() must be called first since it - // analyzes the current solver state. - const double start_time = time_limit->GetElapsedTime(); - GLOP_RETURN_IF_ERROR(Initialize(lp)); if (logger_->LoggingIsEnabled()) { DisplayBasicVariableStatistics(); } @@ -310,7 +353,13 @@ Status RevisedSimplex::Solve(const LinearProgram& lp, TimeLimit* time_limit) { // After the primal phase I, we need to restore the objective. if (problem_status_ != ProblemStatus::PRIMAL_INFEASIBLE) { - InitializeObjectiveAndTestIfUnchanged(lp); + objective_ = objective_coefficients; + if (is_maximization_problem) { + for (Fractional& value : objective_) { + value = -value; + } + } + objective_.resize(num_cols_, 0.0); // For the slack. reduced_costs_.ResetForNewObjective(); } } @@ -639,7 +688,7 @@ Status RevisedSimplex::Solve(const LinearProgram& lp, TimeLimit* time_limit) { solution_reduced_costs_ = reduced_costs_.GetReducedCosts(); SaveState(); - if (lp.IsMaximizationProblem()) { + if (is_maximization_problem) { ChangeSign(&solution_dual_values_); ChangeSign(&solution_reduced_costs_); } @@ -650,7 +699,7 @@ Status RevisedSimplex::Solve(const LinearProgram& lp, TimeLimit* time_limit) { solution_objective_value_ = (problem_status_ == ProblemStatus::DUAL_UNBOUNDED) ? kInfinity : -kInfinity; - if (lp.IsMaximizationProblem()) { + if (is_maximization_problem) { solution_objective_value_ = -solution_objective_value_; } } @@ -1379,21 +1428,13 @@ Status RevisedSimplex::Initialize(const LinearProgram& lp) { ColIndex num_new_cols(0); bool only_change_is_new_rows = false; bool only_change_is_new_cols = false; - bool matrix_is_unchanged = true; - bool only_new_bounds = false; - if (solution_state_.IsEmpty() || !notify_that_matrix_is_unchanged_) { - matrix_is_unchanged = InitializeMatrixAndTestIfUnchanged( - lp, lp_is_in_equation_form, &only_change_is_new_rows, - &only_change_is_new_cols, &num_new_cols); - only_new_bounds = only_change_is_new_cols && num_new_cols > 0 && - OldBoundsAreUnchangedAndNewVariablesHaveOneBoundAtZero( - lp, lp_is_in_equation_form, num_new_cols); - } else if (DEBUG_MODE) { - CHECK(InitializeMatrixAndTestIfUnchanged( - lp, lp_is_in_equation_form, &only_change_is_new_rows, - &only_change_is_new_cols, &num_new_cols)); - } - notify_that_matrix_is_unchanged_ = false; + const bool matrix_is_unchanged = InitializeMatrixAndTestIfUnchanged( + lp, lp_is_in_equation_form, &only_change_is_new_rows, + &only_change_is_new_cols, &num_new_cols); + const bool only_new_bounds = + only_change_is_new_cols && num_new_cols > 0 && + OldBoundsAreUnchangedAndNewVariablesHaveOneBoundAtZero( + lp, lp_is_in_equation_form, num_new_cols); // TODO(user): move objective with ReducedCosts class. const bool objective_is_unchanged = InitializeObjectiveAndTestIfUnchanged(lp); @@ -1509,6 +1550,10 @@ Status RevisedSimplex::Initialize(const LinearProgram& lp) { } } + return FinishInitialization(solve_from_scratch); +} + +Status RevisedSimplex::FinishInitialization(bool solve_from_scratch) { // If we couldn't perform a "quick" warm start above, we can at least try to // reuse the variable statuses. if (solve_from_scratch && !solution_state_.IsEmpty()) { @@ -1589,6 +1634,8 @@ Status RevisedSimplex::Initialize(const LinearProgram& lp) { SOLVER_LOG(logger_, "Starting basis: incremental solve."); } DCHECK(BasisIsConsistent()); + + transpose_was_changed_ = false; return Status::OK(); } diff --git a/ortools/glop/revised_simplex.h b/ortools/glop/revised_simplex.h index a4fa4b51adb..90e55698e4a 100644 --- a/ortools/glop/revised_simplex.h +++ b/ortools/glop/revised_simplex.h @@ -170,14 +170,6 @@ class RevisedSimplex { // variables. void SetStartingVariableValuesForNextSolve(const DenseRow& values); - // Advanced usage. Tells the next Solve() that the matrix inside the linear - // program will not change compared to the one used the last time Solve() was - // called. This allows to bypass the somewhat costly check of comparing both - // matrices. Note that this call will be ignored if Solve() was never called - // or if ClearStateForNextSolve() was called. - void NotifyThatMatrixIsUnchangedForNextSolve(); - void NotifyThatMatrixIsChangedForNextSolve(); - // Getters to retrieve all the information computed by the last Solve(). RowIndex GetProblemNumRows() const; ColIndex GetProblemNumCols() const; @@ -252,6 +244,24 @@ class RevisedSimplex { void SetLogger(SolverLogger* logger) { logger_ = logger; } + // Advanced usage. For fast incremental call to the solver, it is better not + // to use LinearProgram at all. This api allows to directly modify the + // internal data of glop and then call solve. + const CompactSparseMatrix& MatrixWithSlack() const { return compact_matrix_; } + CompactSparseMatrix* MutableTransposedMatrixWithSlack() { + transpose_was_changed_ = true; + return &transposed_matrix_; + } + DenseRow* MutableLowerBounds() { + return variables_info_.MutableLowerBounds(); + } + DenseRow* MutableUpperBounds() { + return variables_info_.MutableUpperBounds(); + } + ABSL_MUST_USE_RESULT Status MinimizeFromTransposedMatrixWithSlack( + const DenseRow& objective, Fractional objective_scaling_factor, + Fractional objective_offset, TimeLimit* time_limit); + private: struct IterationStats : public StatsGroup { IterationStats() @@ -303,6 +313,10 @@ class RevisedSimplex { FINAL_CHECK }; + ABSL_MUST_USE_RESULT Status SolveInternal(double start_time, bool maximize, + const DenseRow& objective, + TimeLimit* time_limit); + // Propagates parameters_ to all the other classes that need it. // // TODO(user): Maybe a better design is for them to have a reference to a @@ -427,6 +441,7 @@ class RevisedSimplex { // Entry point for the solver initialization. ABSL_MUST_USE_RESULT Status Initialize(const LinearProgram& lp); + ABSL_MUST_USE_RESULT Status FinishInitialization(bool solve_from_scratch); // Saves the current variable statuses in solution_state_. void SaveState(); @@ -715,9 +730,8 @@ class RevisedSimplex { // If this is cleared, we assume they are none. DenseRow variable_starting_values_; - // Flag used by NotifyThatMatrixIsUnchangedForNextSolve() and changing - // the behavior of Initialize(). - bool notify_that_matrix_is_unchanged_ = false; + // See MutableTransposedMatrixWithSlack(). + bool transpose_was_changed_ = false; // This is known as 'd' in the literature and is set during each pivot to the // right inverse of the basic entering column of A by ComputeDirection(). diff --git a/ortools/glop/variables_info.cc b/ortools/glop/variables_info.cc index d100f15eea1..dd5b0d8d716 100644 --- a/ortools/glop/variables_info.cc +++ b/ortools/glop/variables_info.cc @@ -46,6 +46,16 @@ bool VariablesInfo::LoadBoundsAndReturnTrueIfUnchanged( return false; } +void VariablesInfo::InitializeFromMutatedState() { + const ColIndex num_cols = matrix_.num_cols(); + DCHECK_EQ(num_cols, lower_bounds_.size()); + DCHECK_EQ(num_cols, upper_bounds_.size()); + variable_type_.resize(num_cols, VariableType::UNCONSTRAINED); + for (ColIndex col(0); col < num_cols; ++col) { + variable_type_[col] = ComputeVariableType(col); + } +} + bool VariablesInfo::LoadBoundsAndReturnTrueIfUnchanged( const DenseRow& variable_lower_bounds, const DenseRow& variable_upper_bounds, diff --git a/ortools/glop/variables_info.h b/ortools/glop/variables_info.h index 2a8fa4f7e6f..944d39cfc42 100644 --- a/ortools/glop/variables_info.h +++ b/ortools/glop/variables_info.h @@ -174,6 +174,12 @@ class VariablesInfo { void EndDualPhaseI(Fractional dual_feasibility_tolerance, DenseRow::ConstView reduced_costs); + // Advanced incremental API to reuse directly the internal storage. + // This saves two copy per solves, and only matter on large easy problems. + void InitializeFromMutatedState(); + DenseRow* MutableLowerBounds() { return &lower_bounds_; } + DenseRow* MutableUpperBounds() { return &upper_bounds_; } + private: // Computes the initial/default variable status from its type. A constrained // variable is set to the lowest of its 2 bounds in absolute value. diff --git a/ortools/lp_data/lp_data_utils.cc b/ortools/lp_data/lp_data_utils.cc index 25d03a176ac..126121e65b6 100644 --- a/ortools/lp_data/lp_data_utils.cc +++ b/ortools/lp_data/lp_data_utils.cc @@ -13,7 +13,12 @@ #include "ortools/lp_data/lp_data_utils.h" +#include +#include +#include + #include "absl/log/check.h" +#include "absl/types/span.h" #include "ortools/glop/parameters.pb.h" #include "ortools/lp_data/lp_data.h" #include "ortools/lp_data/lp_types.h" @@ -84,14 +89,40 @@ void Scale(LinearProgram* lp, SparseMatrixScaler* scaler, void LpScalingHelper::Scale(LinearProgram* lp) { Scale(GlopParameters(), lp); } void LpScalingHelper::Scale(const GlopParameters& params, LinearProgram* lp) { - scaler_.Clear(); - ::operations_research::glop::Scale(lp, &scaler_, params.scaling_method()); + SparseMatrixScaler scaler; + ::operations_research::glop::Scale(lp, &scaler, params.scaling_method()); bound_scaling_factor_ = 1.0 / lp->ScaleBounds(); objective_scaling_factor_ = 1.0 / lp->ScaleObjective(params.cost_scaling()); + + matrix_is_scaled_ = true; + row_unscaling_factors_ = scaler.row_scales(); + col_unscaling_factors_ = scaler.col_scales(); + + // It is possible the scaler didn't do anything. + // we still allocate the vector though since we don't test that below. + row_unscaling_factors_.resize(lp->num_constraints(), 1.0); + col_unscaling_factors_.resize(lp->num_variables(), 1.0); +} + +void LpScalingHelper::ConfigureFromFactors( + absl::Span row_factors, + absl::Span col_factors) { + matrix_is_scaled_ = true; + const RowIndex num_rows(row_factors.size()); + row_unscaling_factors_.resize(num_rows, 1.0); + for (RowIndex row(0); row < num_rows; ++row) { + row_unscaling_factors_[row] = 1.0 / row_factors[row.value()]; + } + + const ColIndex num_cols(col_factors.size()); + col_unscaling_factors_.resize(num_cols, 1.0); + for (ColIndex col(0); col < num_cols; ++col) { + col_unscaling_factors_[col] = 1.0 / col_factors[col.value()]; + } } void LpScalingHelper::Clear() { - scaler_.Clear(); + matrix_is_scaled_ = false; bound_scaling_factor_ = 1.0; objective_scaling_factor_ = 1.0; } @@ -99,71 +130,81 @@ void LpScalingHelper::Clear() { Fractional LpScalingHelper::VariableScalingFactor(ColIndex col) const { // During scaling a col was multiplied by ColScalingFactor() and the variable // bounds divided by it. - return scaler_.ColUnscalingFactor(col) * bound_scaling_factor_; + return ColUnscalingFactor(col) * bound_scaling_factor_; +} + +Fractional LpScalingHelper::VariableScalingFactorWithSlack(ColIndex col) const { + if (!matrix_is_scaled_) return bound_scaling_factor_; + const ColIndex num_cols = col_unscaling_factors_.size(); + if (col < num_cols) { + return col_unscaling_factors_[col] * bound_scaling_factor_; + } + return row_unscaling_factors_[ColToRowIndex(col - num_cols)] * + bound_scaling_factor_; } Fractional LpScalingHelper::ScaleVariableValue(ColIndex col, Fractional value) const { - return value * scaler_.ColUnscalingFactor(col) * bound_scaling_factor_; + return value * ColUnscalingFactor(col) * bound_scaling_factor_; } Fractional LpScalingHelper::ScaleReducedCost(ColIndex col, Fractional value) const { // The reduced cost move like the objective and the col scale. - return value / scaler_.ColUnscalingFactor(col) * objective_scaling_factor_; + return value / ColUnscalingFactor(col) * objective_scaling_factor_; } Fractional LpScalingHelper::ScaleDualValue(RowIndex row, Fractional value) const { // The dual value move like the objective and the inverse of the row scale. - return value * (scaler_.RowUnscalingFactor(row) * objective_scaling_factor_); + return value * (RowUnscalingFactor(row) * objective_scaling_factor_); } Fractional LpScalingHelper::ScaleConstraintActivity(RowIndex row, Fractional value) const { // The activity move with the row_scale and the bound_scaling_factor. - return value / scaler_.RowUnscalingFactor(row) * bound_scaling_factor_; + return value / RowUnscalingFactor(row) * bound_scaling_factor_; } Fractional LpScalingHelper::UnscaleVariableValue(ColIndex col, Fractional value) const { // Just the opposite of ScaleVariableValue(). - return value / (scaler_.ColUnscalingFactor(col) * bound_scaling_factor_); + return value / (ColUnscalingFactor(col) * bound_scaling_factor_); } Fractional LpScalingHelper::UnscaleReducedCost(ColIndex col, Fractional value) const { // The reduced cost move like the objective and the col scale. - return value * scaler_.ColUnscalingFactor(col) / objective_scaling_factor_; + return value * ColUnscalingFactor(col) / objective_scaling_factor_; } Fractional LpScalingHelper::UnscaleDualValue(RowIndex row, Fractional value) const { // The dual value move like the objective and the inverse of the row scale. - return value / (scaler_.RowUnscalingFactor(row) * objective_scaling_factor_); + return value / (RowUnscalingFactor(row) * objective_scaling_factor_); } Fractional LpScalingHelper::UnscaleConstraintActivity(RowIndex row, Fractional value) const { // The activity move with the row_scale and the bound_scaling_factor. - return value * scaler_.RowUnscalingFactor(row) / bound_scaling_factor_; + return value * RowUnscalingFactor(row) / bound_scaling_factor_; } void LpScalingHelper::UnscaleUnitRowLeftSolve( ColIndex basis_col, ScatteredRow* left_inverse) const { - const Fractional global_factor = scaler_.ColUnscalingFactor(basis_col); + const Fractional global_factor = ColUnscalingFactor(basis_col); // We have left_inverse * [RowScale * B * ColScale] = unit_row. if (left_inverse->non_zeros.empty()) { const ColIndex num_rows = left_inverse->values.size(); for (ColIndex col(0); col < num_rows; ++col) { left_inverse->values[col] /= - scaler_.RowUnscalingFactor(ColToRowIndex(col)) * global_factor; + RowUnscalingFactor(ColToRowIndex(col)) * global_factor; } } else { for (const ColIndex col : left_inverse->non_zeros) { left_inverse->values[col] /= - scaler_.RowUnscalingFactor(ColToRowIndex(col)) * global_factor; + RowUnscalingFactor(ColToRowIndex(col)) * global_factor; } } } @@ -171,7 +212,7 @@ void LpScalingHelper::UnscaleUnitRowLeftSolve( void LpScalingHelper::UnscaleColumnRightSolve( const RowToColMapping& basis, ColIndex col, ScatteredColumn* right_inverse) const { - const Fractional global_factor = scaler_.ColScalingFactor(col); + const Fractional global_factor = 1.0 / ColUnscalingFactor(col); // [RowScale * B * BColScale] * inverse = RowScale * column * ColScale. // That is B * (BColScale * inverse) = column * ColScale[col]. @@ -179,14 +220,71 @@ void LpScalingHelper::UnscaleColumnRightSolve( const RowIndex num_rows = right_inverse->values.size(); for (RowIndex row(0); row < num_rows; ++row) { right_inverse->values[row] /= - scaler_.ColUnscalingFactor(basis[row]) * global_factor; + ColUnscalingFactor(basis[row]) * global_factor; } } else { for (const RowIndex row : right_inverse->non_zeros) { right_inverse->values[row] /= - scaler_.ColUnscalingFactor(basis[row]) * global_factor; + ColUnscalingFactor(basis[row]) * global_factor; + } + } +} + +void LpScalingHelper::AverageCostScaling(DenseRow* objective) { + Fractional sum = 0.0; + int num_terms = 0; + for (const Fractional f : *objective) { + if (f == 0) continue; + ++num_terms; + sum += std::abs(f); + } + if (num_terms == 0) { + objective_scaling_factor_ = 1.0; + return; + } + + const Fractional average = sum / static_cast(num_terms); + objective_scaling_factor_ = 1.0 / average; + for (Fractional& f : *objective) { + f *= objective_scaling_factor_; + } +} + +void LpScalingHelper::ContainOneBoundScaling(DenseRow* upper_bounds, + DenseRow* lower_bounds) { + const double infinity = std::numeric_limits::infinity(); + Fractional min_magnitude = infinity; + Fractional max_magnitude = 0.0; + for (const Fractional f : *lower_bounds) { + const Fractional m = std::abs(f); + if (m == 0 || m == infinity) continue; + min_magnitude = std::min(min_magnitude, m); + max_magnitude = std::max(max_magnitude, m); + } + for (const Fractional f : *upper_bounds) { + const Fractional m = std::abs(f); + if (m == 0 || m == infinity) continue; + min_magnitude = std::min(min_magnitude, m); + max_magnitude = std::max(max_magnitude, m); + } + + bound_scaling_factor_ = 1.0; + if (min_magnitude != infinity) { + CHECK_LE(min_magnitude, max_magnitude); + if (min_magnitude > 1.0) { + bound_scaling_factor_ = 1.0 / min_magnitude; + } else if (max_magnitude < 1.0) { + bound_scaling_factor_ = 1.0 / max_magnitude; } } + + if (bound_scaling_factor_ == 1.0) return; + for (Fractional& f : *lower_bounds) { + f *= bound_scaling_factor_; + } + for (Fractional& f : *upper_bounds) { + f *= bound_scaling_factor_; + } } } // namespace glop diff --git a/ortools/lp_data/lp_data_utils.h b/ortools/lp_data/lp_data_utils.h index d63eb52a3ec..37e94249ce0 100644 --- a/ortools/lp_data/lp_data_utils.h +++ b/ortools/lp_data/lp_data_utils.h @@ -51,12 +51,14 @@ void Scale(LinearProgram* lp, SparseMatrixScaler* scaler); // sense to have a single place where all the scaling formulas are kept. class LpScalingHelper { public: + // Clear all scaling coefficients. + void Clear(); + // Scale the given LP. void Scale(LinearProgram* lp); void Scale(const GlopParameters& params, LinearProgram* lp); - - // Clear all scaling coefficients. - void Clear(); + void ConfigureFromFactors(absl::Span row_factors, + absl::Span col_factors); // Transforms value from unscaled domain to the scaled one. Fractional ScaleVariableValue(ColIndex col, Fractional value) const; @@ -83,18 +85,37 @@ class LpScalingHelper { // to be in the scaled domain. Fractional VariableScalingFactor(ColIndex col) const; - // Visible for testing. All objective coefficients of the original LP where - // multiplied by this factor. Nothing else changed. - Fractional BoundsScalingFactor() const { return bound_scaling_factor_; } + // Same as VariableScalingFactor() except that ColIndex greater than the + // number of columns will be interpreted as "slack" variable whose scaling + // factor depends on the row. + Fractional VariableScalingFactorWithSlack(ColIndex col) const; + + // Extra scaling function, to scale objective/bounds. + void AverageCostScaling(DenseRow* objective); + void ContainOneBoundScaling(DenseRow* upper_bounds, DenseRow* lower_bounds); // Visible for testing. All variable/constraint bounds of the original LP // where multiplied by this factor. Nothing else changed. + Fractional BoundsScalingFactor() const { return bound_scaling_factor_; } + + // Visible for testing. All objective coefficients of the original LP where + // multiplied by this factor. Nothing else changed. Fractional ObjectiveScalingFactor() const { return objective_scaling_factor_; } private: - SparseMatrixScaler scaler_; + Fractional RowUnscalingFactor(RowIndex row) const { + return matrix_is_scaled_ ? row_unscaling_factors_[row] : 1.0; + } + Fractional ColUnscalingFactor(ColIndex col) const { + return matrix_is_scaled_ ? col_unscaling_factors_[col] : 1.0; + } + + bool matrix_is_scaled_ = false; + DenseColumn row_unscaling_factors_; + DenseRow col_unscaling_factors_; + Fractional bound_scaling_factor_ = 1.0; Fractional objective_scaling_factor_ = 1.0; }; diff --git a/ortools/lp_data/sparse.cc b/ortools/lp_data/sparse.cc index 1928aa7d55a..585744ecd96 100644 --- a/ortools/lp_data/sparse.cc +++ b/ortools/lp_data/sparse.cc @@ -577,6 +577,17 @@ void TriangularMatrix::Reset(RowIndex num_rows, ColIndex col_capacity) { starts_[ColIndex(0)] = 0; } +void CompactSparseMatrix::AddEntryToCurrentColumn(RowIndex row, + Fractional coeff) { + rows_.push_back(row); + coefficients_.push_back(coeff); +} + +void CompactSparseMatrix::CloseCurrentColumn() { + starts_.push_back(rows_.size()); + ++num_cols_; +} + ColIndex CompactSparseMatrix::AddDenseColumn(const DenseColumn& dense_column) { return AddDenseColumnPrefix(dense_column.const_view(), RowIndex(0)); } @@ -832,6 +843,7 @@ void TriangularMatrix::UpperSolveInternal(DenseColumn::View rhs) const { const auto entry_rows = rows_.view(); const auto entry_coefficients = coefficients_.view(); const auto diagonal_coefficients = diagonal_coefficients_.view(); + const auto starts = starts_.view(); for (ColIndex col(diagonal_coefficients.size() - 1); col >= end; --col) { const Fractional value = rhs[ColToRowIndex(col)]; if (value == 0.0) continue; @@ -844,8 +856,8 @@ void TriangularMatrix::UpperSolveInternal(DenseColumn::View rhs) const { // It is faster to iterate this way (instead of i : Column(col)) because of // cache locality. Note that the floating-point computations are exactly the // same in both cases. - const EntryIndex i_end = starts_[col]; - for (EntryIndex i(starts_[col + 1] - 1); i >= i_end; --i) { + const EntryIndex i_end = starts[col]; + for (EntryIndex i(starts[col + 1] - 1); i >= i_end; --i) { rhs[entry_rows[i]] -= coeff * entry_coefficients[i]; } } diff --git a/ortools/lp_data/sparse.h b/ortools/lp_data/sparse.h index 96c45714449..19053dba320 100644 --- a/ortools/lp_data/sparse.h +++ b/ortools/lp_data/sparse.h @@ -369,6 +369,10 @@ class CompactSparseMatrix { // Add*() functions below. void Reset(RowIndex num_rows); + // Api to add columns one at the time. + void AddEntryToCurrentColumn(RowIndex row, Fractional coeff); + void CloseCurrentColumn(); + // Adds a dense column to the CompactSparseMatrix (only the non-zero will be // actually stored). This work in O(input.size()) and returns the index of the // added column. From c41dbb8e43b4b0b6245911293302b7e4de75cc66 Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Mon, 7 Oct 2024 11:08:38 +0200 Subject: [PATCH 11/11] st: Fixup export from google3 --- ortools/base/parse_text_proto.h | 27 ++++++++++++++++++++++++ ortools/sat/cp_model_expand.cc | 3 ++- ortools/sat/cp_model_lns.cc | 4 ++-- ortools/sat/feasibility_jump.cc | 3 ++- ortools/sat/linear_constraint_manager.cc | 3 ++- ortools/sat/lp_utils.cc | 4 ++-- ortools/sat/precedences.cc | 5 +++-- ortools/sat/probing.cc | 5 +++-- ortools/sat/sat_base.h | 5 +++-- ortools/sat/sat_inprocessing.cc | 7 +++--- ortools/sat/var_domination.cc | 4 ++-- ortools/sat/work_assignment_test.cc | 2 +- 12 files changed, 53 insertions(+), 19 deletions(-) diff --git a/ortools/base/parse_text_proto.h b/ortools/base/parse_text_proto.h index 625ad29ad16..5e78d3dfc37 100644 --- a/ortools/base/parse_text_proto.h +++ b/ortools/base/parse_text_proto.h @@ -14,6 +14,8 @@ #ifndef OR_TOOLS_BASE_PARSE_TEXT_PROTO_H_ #define OR_TOOLS_BASE_PARSE_TEXT_PROTO_H_ +#include + #include "absl/log/absl_check.h" #include "google/protobuf/message.h" #include "google/protobuf/text_format.h" @@ -32,6 +34,31 @@ T ParseTextOrDie(const std::string& input) { return result; } +namespace text_proto_internal { + +class ParseProtoHelper { + public: + explicit ParseProtoHelper(std::string_view asciipb) : asciipb_(asciipb) {} + template + operator T() { // NOLINT(runtime/explicit) + T result; + const bool ok = ::google::protobuf::TextFormat::TextFormat::ParseFromString( + asciipb_, &result); + CHECK(ok) << "Failed to parse text proto: " << asciipb_; + return result; + } + + private: + const std::string asciipb_; +}; + +} // namespace text_proto_internal + +text_proto_internal::ParseProtoHelper ParseTextProtoOrDie( + std::string_view input) { + return text_proto_internal::ParseProtoHelper(input); +} + } // namespace google::protobuf::contrib::parse_proto #endif // OR_TOOLS_BASE_PARSE_TEXT_PROTO_H_ diff --git a/ortools/sat/cp_model_expand.cc b/ortools/sat/cp_model_expand.cc index e9cceb9d4dd..eaa97cdc807 100644 --- a/ortools/sat/cp_model_expand.cc +++ b/ortools/sat/cp_model_expand.cc @@ -1768,7 +1768,8 @@ void CompressAndExpandPositiveTable(ConstraintProto* ct, } } - VLOG(2) << "Table compression" << " var=" << vars.size() + VLOG(2) << "Table compression" + << " var=" << vars.size() << " cost=" << domain_sizes.size() - vars.size() << " tuples= " << num_tuples_before_compression << " -> " << num_tuples_after_first_compression << " -> " diff --git a/ortools/sat/cp_model_lns.cc b/ortools/sat/cp_model_lns.cc index 436d65d68d0..48404abdfdd 100644 --- a/ortools/sat/cp_model_lns.cc +++ b/ortools/sat/cp_model_lns.cc @@ -1726,8 +1726,8 @@ Neighborhood DecompositionGraphNeighborhoodGenerator::Generate( VLOG(2) << "#relaxed " << relaxed_variables.size() << " #zero_score " << num_zero_score << " max_width " << max_width << " (size,min_width)_after_100 (" << size_at_min_width_after_100 - << "," << min_width_after_100 << ") " << " final_width " - << pq.Size(); + << "," << min_width_after_100 << ") " + << " final_width " << pq.Size(); } return helper_.RelaxGivenVariables(initial_solution, relaxed_variables); diff --git a/ortools/sat/feasibility_jump.cc b/ortools/sat/feasibility_jump.cc index 8219baf393d..1083b3e6e01 100644 --- a/ortools/sat/feasibility_jump.cc +++ b/ortools/sat/feasibility_jump.cc @@ -86,7 +86,8 @@ bool JumpTable::JumpIsUpToDate(int var) const { if (abs(score - scores_[var]) / std::max(abs(score), 1.0) > 1e-2) { score_ok = false; LOG(ERROR) << "Incorrect score for var " << var << ": " << scores_[var] - << " (should be " << score << ") " << " delta = " << delta; + << " (should be " << score << ") " + << " delta = " << delta; } return delta == deltas_[var] && score_ok; } diff --git a/ortools/sat/linear_constraint_manager.cc b/ortools/sat/linear_constraint_manager.cc index c0524551ac0..ec2d67e595c 100644 --- a/ortools/sat/linear_constraint_manager.cc +++ b/ortools/sat/linear_constraint_manager.cc @@ -254,7 +254,8 @@ bool LinearConstraintManager::AddCut(LinearConstraint ct, std::string type_name, // Only add cut with sufficient efficacy. if (violation / l2_norm < 1e-4) { - VLOG(3) << "BAD Cut '" << type_name << "'" << " size=" << ct.num_terms + VLOG(3) << "BAD Cut '" << type_name << "'" + << " size=" << ct.num_terms << " max_magnitude=" << ComputeInfinityNorm(ct) << " norm=" << l2_norm << " violation=" << violation << " eff=" << violation / l2_norm << " " << extra_info; diff --git a/ortools/sat/lp_utils.cc b/ortools/sat/lp_utils.cc index 847d64cf082..a95b8b70b16 100644 --- a/ortools/sat/lp_utils.cc +++ b/ortools/sat/lp_utils.cc @@ -1507,8 +1507,8 @@ bool ConvertBinaryMPModelProtoToBooleanProblem(const MPModelProto& mp_model, // Abort if the variable is not binary. if (!is_binary) { LOG(WARNING) << "The variable #" << var_id << " with name " - << mp_var.name() << " is not binary. " << "lb: " << lb - << " ub: " << ub; + << mp_var.name() << " is not binary. " + << "lb: " << lb << " ub: " << ub; return false; } } diff --git a/ortools/sat/precedences.cc b/ortools/sat/precedences.cc index 34b1559a0df..18b2a363d03 100644 --- a/ortools/sat/precedences.cc +++ b/ortools/sat/precedences.cc @@ -647,8 +647,9 @@ void PrecedencesPropagator::AddArc( // A self-arc is either plain SAT or plain UNSAT or it forces something on // the given offset_var or presence_literal_index. In any case it could be // presolved in something more efficient. - VLOG(1) << "Self arc! This could be presolved. " << "var:" << tail - << " offset:" << offset << " offset_var:" << offset_var + VLOG(1) << "Self arc! This could be presolved. " + << "var:" << tail << " offset:" << offset + << " offset_var:" << offset_var << " conditioned_by:" << presence_literals; } diff --git a/ortools/sat/probing.cc b/ortools/sat/probing.cc index 941c78c8309..37d9b7d9d3d 100644 --- a/ortools/sat/probing.cc +++ b/ortools/sat/probing.cc @@ -890,8 +890,9 @@ bool FailedLiteralProbingRound(ProbingOptions options, Model* model) { const bool limit_reached = time_limit->LimitReached() || time_limit->GetElapsedDeterministicTime() > limit; LOG_IF(INFO, options.log_info) - << "Probing. " << " num_probed: " << num_probed << " num_fixed: +" - << num_newly_fixed << " (" << num_fixed << "/" << num_variables << ")" + << "Probing. " + << " num_probed: " << num_probed << " num_fixed: +" << num_newly_fixed + << " (" << num_fixed << "/" << num_variables << ")" << " explicit_fix:" << num_explicit_fix << " num_conflicts:" << num_conflicts << " new_binary_clauses: " << num_new_binary diff --git a/ortools/sat/sat_base.h b/ortools/sat/sat_base.h index c342af7105f..ced510384bd 100644 --- a/ortools/sat/sat_base.h +++ b/ortools/sat/sat_base.h @@ -625,8 +625,9 @@ inline bool SatPropagator::PropagatePreconditionsAreSatisfied( if (propagation_trail_index_ < trail.Index() && trail.Info(trail[propagation_trail_index_].Variable()).level != trail.CurrentDecisionLevel()) { - LOG(INFO) << "Issue in '" << name_ << "':" << " propagation_trail_index_=" - << propagation_trail_index_ << " trail_.Index()=" << trail.Index() + LOG(INFO) << "Issue in '" << name_ << "':" + << " propagation_trail_index_=" << propagation_trail_index_ + << " trail_.Index()=" << trail.Index() << " level_at_propagation_index=" << trail.Info(trail[propagation_trail_index_].Variable()).level << " current_decision_level=" << trail.CurrentDecisionLevel(); diff --git a/ortools/sat/sat_inprocessing.cc b/ortools/sat/sat_inprocessing.cc index 1d6dd85cd28..24a7de25b86 100644 --- a/ortools/sat/sat_inprocessing.cc +++ b/ortools/sat/sat_inprocessing.cc @@ -704,8 +704,8 @@ bool StampingSimplifier::ComputeStampsForNextRound(bool log_info) { // TODO(user): compute some dtime, it is always zero currently. time_limit_->AdvanceDeterministicTime(dtime_); - LOG_IF(INFO, log_info) << "Prestamping." << " num_fixed: " << num_fixed_ - << " dtime: " << dtime_ + LOG_IF(INFO, log_info) << "Prestamping." + << " num_fixed: " << num_fixed_ << " dtime: " << dtime_ << " wtime: " << wall_timer.Get(); return true; } @@ -1259,7 +1259,8 @@ bool BoundedVariableElimination::DoOneRound(bool log_info) { dtime_ += 1e-8 * num_inspected_literals_; time_limit_->AdvanceDeterministicTime(dtime_); log_info |= VLOG_IS_ON(1); - LOG_IF(INFO, log_info) << "BVE." << " num_fixed: " + LOG_IF(INFO, log_info) << "BVE." + << " num_fixed: " << trail_->Index() - saved_trail_index << " num_simplified_literals: " << num_simplifications_ << " num_blocked_clauses_: " << num_blocked_clauses_ diff --git a/ortools/sat/var_domination.cc b/ortools/sat/var_domination.cc index f4368db2d57..ae589ac3e7c 100644 --- a/ortools/sat/var_domination.cc +++ b/ortools/sat/var_domination.cc @@ -1304,8 +1304,8 @@ void ScanModelForDominanceDetection(PresolveContext& context, } } if (num_unconstrained_refs == 0 && num_dominated_refs == 0) return; - VLOG(1) << "Dominance:" << " num_unconstrained_refs=" - << num_unconstrained_refs + VLOG(1) << "Dominance:" + << " num_unconstrained_refs=" << num_unconstrained_refs << " num_dominated_refs=" << num_dominated_refs << " num_dominance_relations=" << num_dominance_relations; } diff --git a/ortools/sat/work_assignment_test.cc b/ortools/sat/work_assignment_test.cc index 2236c3d0124..ba4a6d95334 100644 --- a/ortools/sat/work_assignment_test.cc +++ b/ortools/sat/work_assignment_test.cc @@ -17,8 +17,8 @@ #include "absl/strings/string_view.h" #include "gtest/gtest.h" -#include "net/proto2/contrib/parse_proto/parse_text_proto.h" #include "ortools/base/gmock.h" +#include "ortools/base/parse_text_proto.h" #include "ortools/sat/cp_model.h" #include "ortools/sat/cp_model.pb.h" #include "ortools/sat/cp_model_checker.h"