From 6a793340aaac262ed73f7b7a98c99acdae65312e Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Wed, 18 Sep 2024 17:34:33 +0200 Subject: [PATCH] sat: add some _test.cc --- ortools/sat/2d_rectangle_presolve_test.cc | 596 ++++++++ ortools/sat/all_different_test.cc | 159 ++ ortools/sat/circuit_test.cc | 334 +++++ ortools/sat/cp_constraints_test.cc | 120 ++ ortools/sat/cumulative_energy_test.cc | 562 +++++++ ortools/sat/cumulative_test.cc | 421 ++++++ ortools/sat/cuts_test.cc | 1163 ++++++++++++++ ortools/sat/diffn_test.cc | 176 +++ ortools/sat/diffn_util_test.cc | 960 ++++++++++++ ortools/sat/disjunctive_test.cc | 527 +++++++ ortools/sat/encoding_test.cc | 106 ++ ortools/sat/feasibility_jump_test.cc | 92 ++ ortools/sat/implied_bounds_test.cc | 706 +++++++++ ortools/sat/inclusion_test.cc | 177 +++ ortools/sat/integer_test.cc | 1333 +++++++++++++++++ ortools/sat/intervals_test.cc | 278 ++++ ortools/sat/linear_constraint_manager_test.cc | 421 ++++++ ortools/sat/linear_constraint_test.cc | 480 ++++++ ortools/sat/linear_propagation_test.cc | 321 ++++ ortools/sat/model_test.cc | 92 ++ ortools/sat/optimization_test.cc | 172 +++ ortools/sat/parameters_validation_test.cc | 125 ++ ortools/sat/pb_constraint_test.cc | 673 +++++++++ ortools/sat/precedences_test.cc | 592 ++++++++ ortools/sat/probing_test.cc | 80 + ortools/sat/pseudo_costs_test.cc | 263 ++++ ortools/sat/restart_test.cc | 86 ++ ortools/sat/routing_cuts_test.cc | 422 ++++++ ortools/sat/sat_base_test.cc | 74 + ortools/sat/sat_inprocessing_test.cc | 287 ++++ ortools/sat/scheduling_cuts_test.cc | 576 +++++++ ortools/sat/subsolver_test.cc | 105 ++ ortools/sat/symmetry_test.cc | 151 ++ ortools/sat/theta_tree_test.cc | 291 ++++ ortools/sat/timetable_test.cc | 555 +++++++ ortools/sat/zero_half_cuts_test.cc | 114 ++ 36 files changed, 13590 insertions(+) create mode 100644 ortools/sat/2d_rectangle_presolve_test.cc create mode 100644 ortools/sat/all_different_test.cc create mode 100644 ortools/sat/circuit_test.cc create mode 100644 ortools/sat/cp_constraints_test.cc create mode 100644 ortools/sat/cumulative_energy_test.cc create mode 100644 ortools/sat/cumulative_test.cc create mode 100644 ortools/sat/cuts_test.cc create mode 100644 ortools/sat/diffn_test.cc create mode 100644 ortools/sat/diffn_util_test.cc create mode 100644 ortools/sat/disjunctive_test.cc create mode 100644 ortools/sat/encoding_test.cc create mode 100644 ortools/sat/feasibility_jump_test.cc create mode 100644 ortools/sat/implied_bounds_test.cc create mode 100644 ortools/sat/inclusion_test.cc create mode 100644 ortools/sat/integer_test.cc create mode 100644 ortools/sat/intervals_test.cc create mode 100644 ortools/sat/linear_constraint_manager_test.cc create mode 100644 ortools/sat/linear_constraint_test.cc create mode 100644 ortools/sat/linear_propagation_test.cc create mode 100644 ortools/sat/model_test.cc create mode 100644 ortools/sat/optimization_test.cc create mode 100644 ortools/sat/parameters_validation_test.cc create mode 100644 ortools/sat/pb_constraint_test.cc create mode 100644 ortools/sat/precedences_test.cc create mode 100644 ortools/sat/probing_test.cc create mode 100644 ortools/sat/pseudo_costs_test.cc create mode 100644 ortools/sat/restart_test.cc create mode 100644 ortools/sat/routing_cuts_test.cc create mode 100644 ortools/sat/sat_base_test.cc create mode 100644 ortools/sat/sat_inprocessing_test.cc create mode 100644 ortools/sat/scheduling_cuts_test.cc create mode 100644 ortools/sat/subsolver_test.cc create mode 100644 ortools/sat/symmetry_test.cc create mode 100644 ortools/sat/theta_tree_test.cc create mode 100644 ortools/sat/timetable_test.cc create mode 100644 ortools/sat/zero_half_cuts_test.cc diff --git a/ortools/sat/2d_rectangle_presolve_test.cc b/ortools/sat/2d_rectangle_presolve_test.cc new file mode 100644 index 0000000000..bbe5a5bee6 --- /dev/null +++ b/ortools/sat/2d_rectangle_presolve_test.cc @@ -0,0 +1,596 @@ +// 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_rectangle_presolve.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include "absl/log/check.h" +#include "absl/random/bit_gen_ref.h" +#include "absl/random/random.h" +#include "absl/strings/str_split.h" +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/logging.h" +#include "ortools/sat/2d_orthogonal_packing_testing.h" +#include "ortools/sat/diffn_util.h" +#include "ortools/sat/integer.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::testing::ElementsAre; +using ::testing::IsEmpty; + +std::vector BuildFromAsciiArt(std::string_view input) { + std::vector rectangles; + std::vector lines = absl::StrSplit(input, '\n'); + 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 = i, .y_max = i + 1}); + } + } + } + std::vector empty; + ReduceNumberofBoxes(&rectangles, &empty); + return rectangles; +} + +TEST(RectanglePresolve, Basic) { + std::vector input = BuildFromAsciiArt(R"( + *********** *********** + *********** *********** + *********** *********** + + + *********** *********** + *********** *********** + *********** *********** + )"); + // Note that a single naive pass over the fixed rectangles' gaps would not + // fill the middle region. + std::vector input_in_range; + // Add a single object that is too large to fit between the fixed boxes. + input_in_range.push_back( + {.box_index = 0, + .bounding_area = {.x_min = 0, .x_max = 80, .y_min = 0, .y_max = 80}, + .x_size = 5, + .y_size = 5}); + + EXPECT_TRUE(PresolveFixed2dRectangles(input_in_range, &input)); + EXPECT_EQ(input.size(), 1); +} + +TEST(RectanglePresolve, Trim) { + std::vector input = { + {.x_min = 0, .x_max = 5, .y_min = 0, .y_max = 5}}; + std::vector input_in_range; + input_in_range.push_back( + {.box_index = 0, + .bounding_area = {.x_min = 1, .x_max = 80, .y_min = 1, .y_max = 80}, + .x_size = 5, + .y_size = 5}); + + EXPECT_TRUE(PresolveFixed2dRectangles(input_in_range, &input)); + EXPECT_THAT(input, ElementsAre(Rectangle{ + .x_min = 1, .x_max = 5, .y_min = 1, .y_max = 5})); +} + +TEST(RectanglePresolve, FillBoundingBoxEdge) { + std::vector input = { + {.x_min = 1, .x_max = 5, .y_min = 1, .y_max = 5}}; + std::vector input_in_range; + input_in_range.push_back( + {.box_index = 0, + .bounding_area = {.x_min = 0, .x_max = 80, .y_min = 0, .y_max = 80}, + .x_size = 5, + .y_size = 5}); + + EXPECT_TRUE(PresolveFixed2dRectangles(input_in_range, &input)); + EXPECT_THAT(input, ElementsAre(Rectangle{ + .x_min = 0, .x_max = 5, .y_min = 0, .y_max = 5})); +} + +TEST(RectanglePresolve, UseAreaNotOccupiable) { + std::vector input = { + {.x_min = 20, .x_max = 25, .y_min = 0, .y_max = 5}}; + std::vector input_in_range; + input_in_range.push_back( + {.box_index = 0, + .bounding_area = {.x_min = 0, .x_max = 10, .y_min = 0, .y_max = 10}, + .x_size = 5, + .y_size = 5}); + input_in_range.push_back( + {.box_index = 1, + .bounding_area = {.x_min = 0, .x_max = 15, .y_min = 0, .y_max = 10}, + .x_size = 5, + .y_size = 5}); + input_in_range.push_back( + {.box_index = 1, + .bounding_area = {.x_min = 25, .x_max = 100, .y_min = 0, .y_max = 10}, + .x_size = 5, + .y_size = 5}); + + EXPECT_TRUE(PresolveFixed2dRectangles(input_in_range, &input)); + EXPECT_THAT(input, ElementsAre(Rectangle{ + .x_min = 15, .x_max = 25, .y_min = 0, .y_max = 10})); +} + +TEST(RectanglePresolve, RemoveOutsideBB) { + std::vector input = { + {.x_min = 0, .x_max = 5, .y_min = 0, .y_max = 5}}; + std::vector input_in_range; + input_in_range.push_back( + {.box_index = 0, + .bounding_area = {.x_min = 5, .x_max = 80, .y_min = 5, .y_max = 80}, + .x_size = 5, + .y_size = 5}); + + EXPECT_TRUE(PresolveFixed2dRectangles(input_in_range, &input)); + EXPECT_THAT(input, IsEmpty()); +} + +TEST(RectanglePresolve, RandomTest) { + constexpr int kTotalRectangles = 100; + constexpr int kFixedRectangleSize = 60; + constexpr int kNumRuns = 1000; + absl::BitGen bit_gen; + + for (int run = 0; run < kNumRuns; ++run) { + // Start by generating a feasible problem that we know the solution with + // some items fixed. + std::vector input = + GenerateNonConflictingRectangles(kTotalRectangles, bit_gen); + std::shuffle(input.begin(), input.end(), bit_gen); + CHECK_EQ(input.size(), kTotalRectangles); + absl::Span fixed_rectangles = + absl::MakeConstSpan(input).subspan(0, kFixedRectangleSize); + absl::Span other_rectangles = + absl::MakeSpan(input).subspan(kFixedRectangleSize); + std::vector new_fixed_rectangles(fixed_rectangles.begin(), + fixed_rectangles.end()); + const std::vector input_in_range = + MakeItemsFromRectangles(other_rectangles, 0.6, bit_gen); + + // Presolve the fixed items. + PresolveFixed2dRectangles(input_in_range, &new_fixed_rectangles); + LOG(INFO) << "Presolved:\n" + << RenderDot(std::nullopt, fixed_rectangles) << "To:\n" + << RenderDot(std::nullopt, new_fixed_rectangles); + + CHECK_LE(new_fixed_rectangles.size(), kFixedRectangleSize); + + // Check if the original solution is still a solution. + std::vector all_rectangles(new_fixed_rectangles.begin(), + new_fixed_rectangles.end()); + all_rectangles.insert(all_rectangles.end(), other_rectangles.begin(), + other_rectangles.end()); + for (int i = 0; i < all_rectangles.size(); ++i) { + for (int j = i + 1; j < all_rectangles.size(); ++j) { + CHECK(all_rectangles[i].IsDisjoint(all_rectangles[j])) + << RenderDot(std::nullopt, {all_rectangles[i], all_rectangles[j]}); + } + } + } +} + +Neighbours NaiveBuildNeighboursGraph(const std::vector& rectangles) { + auto interval_intersect = [](IntegerValue begin1, IntegerValue end1, + IntegerValue begin2, IntegerValue end2) { + return std::max(begin1, begin2) < std::min(end1, end2); + }; + std::vector> neighbors; + for (int i = 0; i < rectangles.size(); ++i) { + for (int j = 0; j < rectangles.size(); ++j) { + if (i == j) continue; + const Rectangle& r1 = rectangles[i]; + const Rectangle& r2 = rectangles[j]; + if (r1.x_min == r2.x_max && + interval_intersect(r1.y_min, r1.y_max, r2.y_min, r2.y_max)) { + neighbors.push_back({i, EdgePosition::LEFT, j}); + neighbors.push_back({j, EdgePosition::RIGHT, i}); + } + if (r1.y_min == r2.y_max && + interval_intersect(r1.x_min, r1.x_max, r2.x_min, r2.x_max)) { + neighbors.push_back({i, EdgePosition::BOTTOM, j}); + neighbors.push_back({j, EdgePosition::TOP, i}); + } + } + } + return Neighbours(rectangles, neighbors); +} + +std::string RenderNeighborsGraph(std::optional bb, + absl::Span rectangles, + const Neighbours& neighbours) { + const absl::flat_hash_map edge_colors = { + {EdgePosition::TOP, "red"}, + {EdgePosition::BOTTOM, "green"}, + {EdgePosition::LEFT, "blue"}, + {EdgePosition::RIGHT, "cyan"}}; + std::stringstream ss; + ss << " edge[headclip=false, tailclip=false, penwidth=30];\n"; + for (int box_index = 0; box_index < neighbours.NumRectangles(); ++box_index) { + for (int edge_int = 0; edge_int < 4; ++edge_int) { + const EdgePosition edge = static_cast(edge_int); + const auto edge_neighbors = + neighbours.GetSortedNeighbors(box_index, edge); + for (int neighbor : edge_neighbors) { + ss << " " << box_index << "->" << neighbor << " [color=\"" + << edge_colors.find(edge)->second << "\"];\n"; + } + } + } + return RenderDot(bb, rectangles, ss.str()); +} + +std::string RenderContour(std::optional bb, + absl::Span rectangles, + const ShapePath& path) { + const std::vector colors = {"red", "green", "blue", + "cyan", "yellow", "purple"}; + std::stringstream ss; + ss << " edge[headclip=false, tailclip=false, penwidth=30];\n"; + for (int i = 0; i < path.step_points.size(); ++i) { + std::pair p = path.step_points[i]; + ss << " p" << i << "[pos=\"" << 2 * p.first << "," << 2 * p.second + << "!\" shape=point]\n"; + if (i != path.step_points.size() - 1) { + ss << " p" << i << "->p" << i + 1 << "\n"; + } + } + return RenderDot(bb, rectangles, ss.str()); +} + +TEST(BuildNeighboursGraphTest, Simple) { + std::vector rectangles = { + {.x_min = 0, .x_max = 10, .y_min = 0, .y_max = 10}, + {.x_min = 10, .x_max = 20, .y_min = 0, .y_max = 10}, + {.x_min = 0, .x_max = 10, .y_min = 10, .y_max = 20}}; + const Neighbours neighbours = BuildNeighboursGraph(rectangles); + EXPECT_THAT(neighbours.GetSortedNeighbors(0, EdgePosition::RIGHT), + ElementsAre(1)); + EXPECT_THAT(neighbours.GetSortedNeighbors(0, EdgePosition::TOP), + ElementsAre(2)); + EXPECT_THAT(neighbours.GetSortedNeighbors(1, EdgePosition::LEFT), + ElementsAre(0)); + EXPECT_THAT(neighbours.GetSortedNeighbors(2, EdgePosition::BOTTOM), + ElementsAre(0)); +} + +TEST(BuildNeighboursGraphTest, NeighborsAroundCorner) { + std::vector rectangles = { + {.x_min = 0, .x_max = 10, .y_min = 0, .y_max = 10}, + {.x_min = 10, .x_max = 20, .y_min = 10, .y_max = 20}}; + const Neighbours neighbours = BuildNeighboursGraph(rectangles); + for (int i = 0; i < 4; ++i) { + const EdgePosition edge = static_cast(i); + EXPECT_THAT(neighbours.GetSortedNeighbors(0, edge), IsEmpty()); + EXPECT_THAT(neighbours.GetSortedNeighbors(1, edge), IsEmpty()); + } +} + +TEST(BuildNeighboursGraphTest, RandomTest) { + constexpr int kNumRuns = 100; + absl::BitGen bit_gen; + + for (int run = 0; run < kNumRuns; ++run) { + // Start by generating a feasible problem that we know the solution with + // some items fixed. + std::vector input = + GenerateNonConflictingRectanglesWithPacking({100, 100}, 60, bit_gen); + std::shuffle(input.begin(), input.end(), bit_gen); + auto neighbours = BuildNeighboursGraph(input); + auto expected_neighbours = NaiveBuildNeighboursGraph(input); + for (int box_index = 0; box_index < neighbours.NumRectangles(); + ++box_index) { + for (int edge_int = 0; edge_int < 4; ++edge_int) { + const EdgePosition edge = static_cast(edge_int); + if (neighbours.GetSortedNeighbors(box_index, edge) != + expected_neighbours.GetSortedNeighbors(box_index, edge)) { + LOG(FATAL) << "Got:\n" + << RenderNeighborsGraph(std::nullopt, input, neighbours) + << "Expected:\n" + << RenderNeighborsGraph(std::nullopt, input, + expected_neighbours); + } + } + } + } +} + +ShapePath TraceBoundaryNaive( + std::pair starting_corner, + absl::Span rectangles) { + // First build a grid that tells by which box each 1x1 rectangle is occupied + // or -1 if empty. + constexpr int kBoundingBoxSize = 100; + std::vector> grid( + kBoundingBoxSize + 1, std::vector(kBoundingBoxSize + 1, -1)); + + for (int n = 0; n < rectangles.size(); n++) { + const Rectangle& r = rectangles[n]; + CHECK_GE(r.x_min, 0); + CHECK_LE(r.x_max, kBoundingBoxSize); + CHECK_GE(r.y_min, 0); + CHECK_LE(r.y_max, kBoundingBoxSize); + for (IntegerValue i = r.x_min; i < r.x_max; i++) { + for (IntegerValue j = r.y_min; j < r.y_max; j++) { + grid[i.value()][j.value()] = n; + } + } + } + + // Now collect all the boundary edges: an occupied cell that touches an + // unoccupied one. + absl::flat_hash_map, int> x_edges; + absl::flat_hash_map, int> y_edges; + for (int i = -1; i < kBoundingBoxSize; i++) { + for (int j = -1; j < kBoundingBoxSize; j++) { + if (i != -1) { + if ((j == -1 || grid[i][j] == -1) && grid[i][j + 1] != -1) { + x_edges[{i, j + 1}] = grid[i][j + 1]; + } + if (j != -1 && grid[i][j + 1] == -1 && grid[i][j] != -1) { + x_edges[{i, j + 1}] = grid[i][j]; + } + } + if (j != -1) { + if ((i == -1 || grid[i][j] == -1) && grid[i + 1][j] != -1) { + y_edges[{i + 1, j}] = grid[i + 1][j]; + } + if (i != -1 && grid[i + 1][j] == -1 && grid[i][j] != -1) { + y_edges[{i + 1, j}] = grid[i][j]; + } + } + } + } + + ShapePath path; + std::pair cur = starting_corner; + int cur_index; + if (x_edges.contains(starting_corner)) { + cur_index = x_edges.at(starting_corner); + } else if (y_edges.contains(starting_corner)) { + cur_index = y_edges.at(starting_corner); + } else { + LOG(FATAL) << "Should not happen: {" << starting_corner.first << "," + << starting_corner.second << "} " + << RenderDot(std::nullopt, rectangles); + } + const int first_index = cur_index; + + auto is_aligned = [](const std::pair& p1, + const std::pair& p2, + const std::pair& p3) { + return ((p1.first == p2.first) == (p2.first == p3.first)) && + ((p1.second == p2.second) == (p2.second == p3.second)); + }; + + // Grow the path by a segment of size one. + const auto add_segment = + [&path, &is_aligned](const std::pair& segment, + int index) { + if (path.step_points.size() > 1 && + is_aligned(path.step_points[path.step_points.size() - 1], + path.step_points[path.step_points.size() - 2], + segment) && + path.touching_box_index.back() == index) { + path.step_points.back() = segment; + } else { + if (!path.step_points.empty()) { + path.touching_box_index.push_back(index); + } + path.step_points.push_back(segment); + } + }; + + // Now we navigate from one edge to the next. To avoid going back, we remove + // used edges from the hash map. + do { + add_segment(cur, cur_index); + + // Find the next segment. + if (x_edges.contains({cur.first, cur.second}) && + x_edges.contains({cur.first - 1, cur.second}) && + !path.touching_box_index.empty()) { + // Corner case (literally): + // ******** + // ******** + // ******** + // ******** + // +++++++++ + // +++++++++ + // +++++++++ + // +++++++++ + // + // In this case we keep following the same box. + auto it_x = x_edges.find({cur.first, cur.second}); + if (cur_index == it_x->second) { + auto extract = x_edges.extract({cur.first, cur.second}); + cur = {cur.first + 1, cur.second}; + cur_index = extract.mapped(); + } else { + auto extract = x_edges.extract({cur.first - 1, cur.second}); + cur = extract.key(); + cur_index = extract.mapped(); + } + } else if (y_edges.contains({cur.first, cur.second}) && + y_edges.contains({cur.first, cur.second - 1}) && + !path.touching_box_index.empty()) { + auto it_y = y_edges.find({cur.first, cur.second}); + if (cur_index == it_y->second) { + auto extract = y_edges.extract({cur.first, cur.second}); + cur = {cur.first, cur.second + 1}; + cur_index = extract.mapped(); + } else { + auto extract = y_edges.extract({cur.first, cur.second - 1}); + cur = extract.key(); + cur_index = extract.mapped(); + } + } else if (auto extract = y_edges.extract({cur.first, cur.second}); + !extract.empty()) { + cur = {cur.first, cur.second + 1}; + cur_index = extract.mapped(); + } else if (auto extract = x_edges.extract({cur.first - 1, cur.second}); + !extract.empty()) { + cur = extract.key(); + cur_index = extract.mapped(); + } else if (auto extract = x_edges.extract({cur.first, cur.second}); + !extract.empty()) { + cur = {cur.first + 1, cur.second}; + cur_index = extract.mapped(); + } else if (auto extract = y_edges.extract({cur.first, cur.second - 1}); + !extract.empty()) { + cur = extract.key(); + cur_index = extract.mapped(); + } else { + LOG(FATAL) << "Should not happen: {" << cur.first << "," << cur.second + << "} " << RenderContour(std::nullopt, rectangles, path); + } + } while (cur != starting_corner); + + add_segment(cur, cur_index); + path.touching_box_index.push_back(first_index); + return path; +} + +TEST(ContourTest, Random) { + constexpr int kNumRuns = 100; + absl::BitGen bit_gen; + + for (int run = 0; run < kNumRuns; ++run) { + // Start by generating a feasible problem that we know the solution with + // some items fixed. + std::vector input = + GenerateNonConflictingRectanglesWithPacking({100, 100}, 60, bit_gen); + std::shuffle(input.begin(), input.end(), bit_gen); + const int num_fixed_rectangles = input.size() * 2 / 3; + absl::Span fixed_rectangles = + absl::MakeConstSpan(input).subspan(0, num_fixed_rectangles); + absl::Span other_rectangles = + absl::MakeSpan(input).subspan(num_fixed_rectangles); + std::vector new_fixed_rectangles(fixed_rectangles.begin(), + fixed_rectangles.end()); + const std::vector input_in_range = + MakeItemsFromRectangles(other_rectangles, 0.6, bit_gen); + + auto neighbours = BuildNeighboursGraph(fixed_rectangles); + const auto components = SplitInConnectedComponents(neighbours); + const Rectangle bb = {.x_min = 0, .x_max = 100, .y_min = 0, .y_max = 100}; + int min_index = -1; + std::pair min_coord = { + std::numeric_limits::max(), + std::numeric_limits::max()}; + for (const int box_index : components[0]) { + const Rectangle& rectangle = fixed_rectangles[box_index]; + if (std::make_pair(rectangle.x_min, rectangle.y_min) < min_coord) { + min_coord = {rectangle.x_min, rectangle.y_min}; + min_index = box_index; + } + } + + const ShapePath shape = + TraceBoundary(min_coord, min_index, fixed_rectangles, neighbours); + absl::flat_hash_set seen; + std::vector component; + std::vector index_map(input.size()); + for (const int box_index : components[0]) { + component.push_back(fixed_rectangles[box_index]); + index_map[box_index] = component.size() - 1; + } + + const ShapePath expected_shape = + TraceBoundaryNaive(shape.step_points[0], component); + if (shape.step_points != expected_shape.step_points) { + LOG(ERROR) << "Fast algo:\n" + << RenderContour(bb, fixed_rectangles, shape); + LOG(ERROR) << "Naive algo:\n" + << RenderContour(bb, component, expected_shape); + LOG(FATAL) << "Found different solutions between naive and fast algo!"; + } + EXPECT_EQ(shape.step_points, expected_shape.step_points); + for (int i = 0; i < shape.step_points.size(); ++i) { + EXPECT_EQ(index_map[shape.touching_box_index[i]], + expected_shape.touching_box_index[i]); + } + } +} + +TEST(ContourTest, SimpleShapes) { + std::vector rectangles = { + {.x_min = 0, .x_max = 10, .y_min = 10, .y_max = 20}, + {.x_min = 3, .x_max = 8, .y_min = 0, .y_max = 10}}; + ShapePath shape = + TraceBoundary({0, 20}, 0, rectangles, BuildNeighboursGraph(rectangles)); + EXPECT_THAT(shape.touching_box_index, ElementsAre(0, 0, 0, 1, 1, 1, 0, 0, 0)); + EXPECT_THAT(shape.step_points, + ElementsAre(std::make_pair(0, 20), std::make_pair(10, 20), + std::make_pair(10, 10), std::make_pair(8, 10), + std::make_pair(8, 0), std::make_pair(3, 0), + std::make_pair(3, 10), std::make_pair(0, 10), + std::make_pair(0, 20))); + + rectangles = {{.x_min = 0, .x_max = 10, .y_min = 10, .y_max = 20}, + {.x_min = 0, .x_max = 10, .y_min = 0, .y_max = 10}}; + shape = + TraceBoundary({0, 20}, 0, rectangles, BuildNeighboursGraph(rectangles)); + EXPECT_THAT(shape.touching_box_index, ElementsAre(0, 0, 1, 1, 1, 0, 0)); + EXPECT_THAT(shape.step_points, + ElementsAre(std::make_pair(0, 20), std::make_pair(10, 20), + std::make_pair(10, 10), std::make_pair(10, 0), + std::make_pair(0, 0), std::make_pair(0, 10), + std::make_pair(0, 20))); + + rectangles = {{.x_min = 0, .x_max = 10, .y_min = 10, .y_max = 20}, + {.x_min = 0, .x_max = 15, .y_min = 0, .y_max = 10}}; + shape = + TraceBoundary({0, 20}, 0, rectangles, BuildNeighboursGraph(rectangles)); + EXPECT_THAT(shape.touching_box_index, ElementsAre(0, 0, 1, 1, 1, 1, 0, 0)); + EXPECT_THAT(shape.step_points, + ElementsAre(std::make_pair(0, 20), std::make_pair(10, 20), + std::make_pair(10, 10), std::make_pair(15, 10), + std::make_pair(15, 0), std::make_pair(0, 0), + std::make_pair(0, 10), std::make_pair(0, 20))); + + rectangles = {{.x_min = 0, .x_max = 10, .y_min = 10, .y_max = 20}, + {.x_min = 0, .x_max = 10, .y_min = 0, .y_max = 10}, + {.x_min = 10, .x_max = 20, .y_min = 0, .y_max = 10}}; + shape = + TraceBoundary({0, 20}, 0, rectangles, BuildNeighboursGraph(rectangles)); + EXPECT_THAT(shape.touching_box_index, ElementsAre(0, 0, 2, 2, 2, 1, 1, 0, 0)); + EXPECT_THAT(shape.step_points, + ElementsAre(std::make_pair(0, 20), std::make_pair(10, 20), + std::make_pair(10, 10), std::make_pair(20, 10), + std::make_pair(20, 0), std::make_pair(10, 0), + std::make_pair(0, 0), std::make_pair(0, 10), + std::make_pair(0, 20))); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/all_different_test.cc b/ortools/sat/all_different_test.cc new file mode 100644 index 0000000000..bdc91b6090 --- /dev/null +++ b/ortools/sat/all_different_test.cc @@ -0,0 +1,159 @@ +// 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/all_different.h" + +#include +#include +#include +#include +#include +#include + +#include "gtest/gtest.h" +#include "ortools/base/logging.h" +#include "ortools/base/types.h" +#include "ortools/sat/integer.h" +#include "ortools/sat/integer_search.h" +#include "ortools/sat/model.h" +#include "ortools/sat/sat_solver.h" +#include "ortools/util/sorted_interval_list.h" + +namespace operations_research { +namespace sat { +namespace { + +class AllDifferentTest : public ::testing::TestWithParam { + public: + std::function AllDifferent( + const std::vector& vars) { + return [=](Model* model) { + if (GetParam() == "binary") { + model->Add(AllDifferentBinary(vars)); + } else if (GetParam() == "ac") { + model->Add(AllDifferentBinary(vars)); + model->Add(AllDifferentAC(vars)); + } else if (GetParam() == "bounds") { + model->Add(AllDifferentOnBounds(vars)); + } else { + LOG(FATAL) << "Unknown implementation " << GetParam(); + } + }; + } +}; + +INSTANTIATE_TEST_SUITE_P(All, AllDifferentTest, + ::testing::Values("binary", "ac", "bounds")); + +TEST_P(AllDifferentTest, BasicBehavior) { + Model model; + std::vector vars; + vars.push_back(model.Add(NewIntegerVariable(1, 3))); + vars.push_back(model.Add(NewIntegerVariable(0, 2))); + vars.push_back(model.Add(NewIntegerVariable(1, 3))); + vars.push_back(model.Add(NewIntegerVariable(0, 2))); + model.Add(AllDifferent(vars)); + EXPECT_EQ(SatSolver::FEASIBLE, SolveIntegerProblemWithLazyEncoding(&model)); + + std::vector value_seen(5, false); + for (const IntegerVariable var : vars) { + const int64_t value = model.Get(Value(var)); + EXPECT_FALSE(value_seen[value]); + value_seen[value] = true; + } +} + +TEST_P(AllDifferentTest, PerfectMatching) { + Model model; + std::vector vars; + for (int i = 0; i < 4; ++i) { + vars.push_back(model.Add(NewIntegerVariable(0, 10))); + } + IntegerTrail* integer_trail = model.GetOrCreate(); + integer_trail->UpdateInitialDomain(vars[0], Domain::FromValues({3, 9})); + integer_trail->UpdateInitialDomain(vars[1], Domain::FromValues({3, 8})); + integer_trail->UpdateInitialDomain(vars[2], Domain::FromValues({1, 8})); + integer_trail->UpdateInitialDomain(vars[3], Domain(1)); + model.Add(AllDifferent(vars)); + EXPECT_EQ(SatSolver::FEASIBLE, SolveIntegerProblemWithLazyEncoding(&model)); + EXPECT_EQ(1, model.Get(Value(vars[3]))); + EXPECT_EQ(8, model.Get(Value(vars[2]))); + EXPECT_EQ(3, model.Get(Value(vars[1]))); + EXPECT_EQ(9, model.Get(Value(vars[0]))); +} + +TEST_P(AllDifferentTest, EnumerateAllPermutations) { + const int n = 6; + Model model; + std::vector vars; + for (int i = 0; i < n; ++i) { + vars.push_back(model.Add(NewIntegerVariable(0, n - 1))); + } + model.Add(AllDifferent(vars)); + + std::vector> solutions; + while (true) { + const auto status = SolveIntegerProblemWithLazyEncoding(&model); + if (status != SatSolver::Status::FEASIBLE) break; + solutions.emplace_back(n); + for (int i = 0; i < n; ++i) solutions.back()[i] = model.Get(Value(vars[i])); + model.Add(ExcludeCurrentSolutionAndBacktrack()); + } + + // Test that we do have all the permutations (but in a random order). + std::sort(solutions.begin(), solutions.end()); + std::vector expected(n); + std::iota(expected.begin(), expected.end(), 0); + for (int i = 0; i < solutions.size(); ++i) { + EXPECT_EQ(expected, solutions[i]); + if (i + 1 < solutions.size()) { + EXPECT_TRUE(std::next_permutation(expected.begin(), expected.end())); + } else { + // We enumerated all the permutations. + EXPECT_FALSE(std::next_permutation(expected.begin(), expected.end())); + } + } +} + +int Factorial(int n) { return n ? n * Factorial(n - 1) : 1; } + +TEST_P(AllDifferentTest, EnumerateAllInjections) { + const int n = 5; + const int m = n + 2; + Model model; + std::vector vars; + for (int i = 0; i < n; ++i) { + vars.push_back(model.Add(NewIntegerVariable(0, m - 1))); + } + model.Add(AllDifferent(vars)); + + std::vector solution(n); + int num_solutions = 0; + while (true) { + const auto status = SolveIntegerProblemWithLazyEncoding(&model); + if (status != SatSolver::Status::FEASIBLE) break; + for (int i = 0; i < n; i++) solution[i] = model.Get(Value(vars[i])); + std::sort(solution.begin(), solution.end()); + for (int i = 1; i < n; i++) { + EXPECT_LT(solution[i - 1], solution[i]); + } + num_solutions++; + model.Add(ExcludeCurrentSolutionAndBacktrack()); + } + // Count the number of solutions, it should be m!/(m-n)!. + EXPECT_EQ(num_solutions, Factorial(m) / Factorial(m - n)); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/circuit_test.cc b/ortools/sat/circuit_test.cc new file mode 100644 index 0000000000..d6d3cf98f4 --- /dev/null +++ b/ortools/sat/circuit_test.cc @@ -0,0 +1,334 @@ +// 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/circuit.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/graph/strongly_connected_components.h" +#include "ortools/sat/integer.h" +#include "ortools/sat/integer_search.h" +#include "ortools/sat/model.h" +#include "ortools/sat/sat_base.h" +#include "ortools/sat/sat_solver.h" + +namespace operations_research { +namespace sat { +namespace { + +std::function DenseCircuitConstraint( + int num_nodes, bool allow_subcircuit, + bool allow_multiple_subcircuit_through_zero) { + return [=](Model* model) { + std::vector tails; + std::vector heads; + std::vector literals; + for (int tail = 0; tail < num_nodes; ++tail) { + for (int head = 0; head < num_nodes; ++head) { + if (!allow_subcircuit && tail == head) continue; + tails.push_back(tail); + heads.push_back(head); + literals.push_back(Literal(model->Add(NewBooleanVariable()), true)); + } + } + LoadSubcircuitConstraint(num_nodes, tails, heads, literals, model, + allow_multiple_subcircuit_through_zero); + }; +} + +int CountSolutions(Model* model) { + int num_solutions = 0; + while (true) { + const SatSolver::Status status = SolveIntegerProblemWithLazyEncoding(model); + if (status != SatSolver::Status::FEASIBLE) break; + + // Add the solution. + ++num_solutions; + + // Loop to the next solution. + model->Add(ExcludeCurrentSolutionAndBacktrack()); + } + return num_solutions; +} + +int Factorial(int n) { return n ? n * Factorial(n - 1) : 1; } + +TEST(ReindexArcTest, BasicCase) { + const int num_nodes = 1000; + std::vector tails(num_nodes); + std::vector heads(num_nodes); + for (int i = 0; i < num_nodes; ++i) { + tails[i] = 100 * i; + heads[i] = 100 * i; + } + ReindexArcs(&tails, &heads); + for (int i = 0; i < num_nodes; ++i) { + EXPECT_EQ(i, tails[i]); + EXPECT_EQ(i, heads[i]); + } +} + +TEST(ReindexArcTest, NegativeNumbering) { + const int num_nodes = 1000; + std::vector tails(num_nodes); + std::vector heads(num_nodes); + for (int i = 0; i < num_nodes; ++i) { + tails[i] = -100 * i; + heads[i] = -100 * i; + } + ReindexArcs(&tails, &heads); + for (int i = 0; i < num_nodes; ++i) { + EXPECT_EQ(i, tails[num_nodes - 1 - i]); + EXPECT_EQ(i, heads[num_nodes - 1 - i]); + } +} + +TEST(CircuitConstraintTest, NodeWithNoArcsIsUnsat) { + static const int kNumNodes = 2; + Model model; + std::vector tails; + std::vector heads; + std::vector literals; + tails.push_back(0); + heads.push_back(1); + literals.push_back(Literal(model.Add(NewBooleanVariable()), true)); + LoadSubcircuitConstraint(kNumNodes, tails, heads, literals, &model); + EXPECT_TRUE(model.GetOrCreate()->ModelIsUnsat()); +} + +TEST(CircuitConstraintTest, AllCircuits) { + static const int kNumNodes = 4; + Model model; + model.Add( + DenseCircuitConstraint(kNumNodes, /*allow_subcircuit=*/false, + /*allow_multiple_subcircuit_through_zero=*/false)); + + const int num_solutions = CountSolutions(&model); + EXPECT_EQ(num_solutions, Factorial(kNumNodes - 1)); +} + +TEST(CircuitConstraintTest, AllSubCircuits) { + static const int kNumNodes = 4; + + Model model; + model.Add( + DenseCircuitConstraint(kNumNodes, /*allow_subcircuit=*/true, + /*allow_multiple_subcircuit_through_zero=*/false)); + + const int num_solutions = CountSolutions(&model); + int expected = 1; // No circuit at all. + for (int circuit_size = 2; circuit_size <= kNumNodes; ++circuit_size) { + // The number of circuit of a given size is: + // - n for the first element + // - times (n-1) for the second + // - ... + // - times (n - (circuit_size - 1)) for the last. + // That is n! / (n - circuit_size)!, and like this we count circuit_size + // times the same circuit, so we have to divide by circuit_size in the end. + expected += Factorial(kNumNodes) / + (circuit_size * Factorial(kNumNodes - circuit_size)); + } + EXPECT_EQ(num_solutions, expected); +} + +TEST(CircuitConstraintTest, AllVehiculeRoutes) { + static const int kNumNodes = 4; + Model model; + + model.Add( + DenseCircuitConstraint(kNumNodes, /*allow_subcircuit=*/false, + /*allow_multiple_subcircuit_through_zero=*/true)); + + const int num_solutions = CountSolutions(&model); + int expected = 1; // 3 outgoing arcs from zero. + expected += 2 * 3; // 2 outgoing arcs from zero. 3 pairs, 2 direction. + expected += 6; // full circuit. + EXPECT_EQ(num_solutions, expected); +} + +TEST(CircuitConstraintTest, AllCircuitCoverings) { + // This test counts the number of circuit coverings of the clique on + // num_nodes with num_distinguished distinguished nodes, i.e. graphs that are + // vertex-disjoint circuits where every circuit must contain exactly one + // distinguished node. + // + // When writing n the number of nodes and k the number of distinguished nodes, + // and the number of such coverings T(n, k), we have: + // T(n,1) = (n-1)!, T(k,k) = 1, T(n,k) = (n-1)!/(k-1)! for n >= k >= 1. + // Indeed, we can enumerate canonical representations, e.g. [1]64[2]35, + // by starting with [1][2]...[k], and place every node in turn at its final + // place w.r.t. existing neighbours. To generate the above example, we go + // though [1][2], [1][2]3, [1]4[2]3, [1]4[2]35, [1]64[2]35. + // At the first iteration, there are k choices, then k+1 ... n-1. + for (int num_nodes = 1; num_nodes <= 6; num_nodes++) { + for (int num_distinguished = 1; num_distinguished <= num_nodes; + num_distinguished++) { + Model model; + std::vector distinguished(num_distinguished); + std::iota(distinguished.begin(), distinguished.end(), 0); + std::vector> graph(num_nodes); + std::vector arcs; + for (int i = 0; i < num_nodes; i++) { + graph[i].resize(num_nodes); + for (int j = 0; j < num_nodes; j++) { + const auto var = model.Add(NewBooleanVariable()); + graph[i][j] = Literal(var, true); + arcs.emplace_back(graph[i][j]); + } + if (i >= num_distinguished) { + model.Add(ClauseConstraint({graph[i][i].Negated()})); + } + } + model.Add(ExactlyOnePerRowAndPerColumn(graph)); + model.Add(CircuitCovering(graph, distinguished)); + const int64_t num_solutions = CountSolutions(&model); + EXPECT_EQ(num_solutions * Factorial(num_distinguished - 1), + Factorial(num_nodes - 1)); + } + } +} + +TEST(CircuitConstraintTest, InfeasibleBecauseOfMissingArcs) { + Model model; + std::vector tails; + std::vector heads; + std::vector literals; + for (const auto arcs : + std::vector>{{0, 1}, {1, 1}, {0, 2}, {2, 2}}) { + tails.push_back(arcs.first); + heads.push_back(arcs.second); + literals.push_back(Literal(model.Add(NewBooleanVariable()), true)); + } + LoadSubcircuitConstraint(3, tails, heads, literals, &model, false); + const SatSolver::Status status = SolveIntegerProblemWithLazyEncoding(&model); + EXPECT_EQ(status, SatSolver::Status::INFEASIBLE); +} + +// The graph look like this with a self-loop at 2. If 2 is not selected +// (self-loop) then there is one solution (0,1,3,0) and (0,3,5,0). Otherwise, +// there is 2 more solutions with 2 inserteed in one of the two routes. +// +// 0 ---> 1 ---> 4 ------------- +// | | ^ | +// | -----> 2* --> 5 ---> 0 +// | ^ ^ +// | | | +// -------------> 3 ------ +// +TEST(CircuitConstraintTest, RouteConstraint) { + Model model; + std::vector tails; + std::vector heads; + std::vector literals; + for (const auto arcs : std::vector>{{0, 1}, + {0, 3}, + {1, 2}, + {1, 4}, + {2, 2}, + {2, 4}, + {2, 5}, + {3, 2}, + {3, 5}, + {4, 0}, + {5, 0}}) { + tails.push_back(arcs.first); + heads.push_back(arcs.second); + literals.push_back(Literal(model.Add(NewBooleanVariable()), true)); + } + LoadSubcircuitConstraint(6, tails, heads, literals, &model, true); + const int64_t num_solutions = CountSolutions(&model); + EXPECT_EQ(num_solutions, 3); +} + +TEST(NoCyclePropagatorTest, CountAllSolutions) { + // We create a 2 * 2 grid with diagonal arcs. + Model model; + int num_nodes = 0; + const int num_x = 2; + const int num_y = 2; + const auto get_index = [&num_nodes](int x, int y) { + const int index = x * num_y + y; + num_nodes = std::max(num_nodes, index + 1); + return index; + }; + + std::vector tails; + std::vector heads; + std::vector literals; + for (int x = 0; x < num_x; ++x) { + for (int y = 0; y < num_y; ++y) { + for (const int x_dir : {-1, 0, 1}) { + for (const int y_dir : {-1, 0, 1}) { + const int head_x = x + x_dir; + const int head_y = y + y_dir; + if (x_dir == 0 && y_dir == 0) continue; + if (head_x < 0 || head_x >= num_x) continue; + if (head_y < 0 || head_y >= num_y) continue; + tails.push_back(get_index(x, y)); + heads.push_back(get_index(head_x, head_y)); + literals.push_back(Literal(model.Add(NewBooleanVariable()), true)); + } + } + } + } + model.TakeOwnership( + new NoCyclePropagator(num_nodes, tails, heads, literals, &model)); + + // Graph is small enough. + CHECK_EQ(num_nodes, 4); + CHECK_EQ(tails.size(), 12); + + // Counts solution with brute-force algo. + int num_expected_solutions = 0; + std::vector> subgraph(num_nodes); + std::vector> components; + const int num_cases = 1 << tails.size(); + for (int mask = 0; mask < num_cases; ++mask) { + for (int n = 0; n < num_nodes; ++n) { + subgraph[n].clear(); + } + for (int a = 0; a < tails.size(); ++a) { + if ((1 << a) & mask) { + subgraph[tails[a]].push_back(heads[a]); + } + } + components.clear(); + FindStronglyConnectedComponents(num_nodes, subgraph, &components); + bool has_cycle = false; + for (const std::vector compo : components) { + if (compo.size() > 1) { + has_cycle = true; + break; + } + } + if (!has_cycle) ++num_expected_solutions; + } + EXPECT_EQ(num_expected_solutions, 543); + + // There is 12 arcs. + // So out of 2^12 solution, we have to exclude all the one with cycles. + EXPECT_EQ(CountSolutions(&model), 543); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/cp_constraints_test.cc b/ortools/sat/cp_constraints_test.cc new file mode 100644 index 0000000000..bceee075ad --- /dev/null +++ b/ortools/sat/cp_constraints_test.cc @@ -0,0 +1,120 @@ +// 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_constraints.h" + +#include + +#include +#include + +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/base/logging.h" +#include "ortools/sat/integer.h" +#include "ortools/sat/integer_search.h" +#include "ortools/sat/model.h" +#include "ortools/sat/precedences.h" +#include "ortools/sat/sat_base.h" +#include "ortools/sat/sat_solver.h" + +namespace operations_research { +namespace sat { +namespace { + +TEST(LiteralXorIsTest, OneVariable) { + Model model; + const BooleanVariable a = model.Add(NewBooleanVariable()); + const BooleanVariable b = model.Add(NewBooleanVariable()); + model.Add(LiteralXorIs({Literal(a, true)}, true)); + model.Add(LiteralXorIs({Literal(b, true)}, false)); + SatSolver* solver = model.GetOrCreate(); + EXPECT_TRUE(solver->Propagate()); + EXPECT_TRUE(solver->Assignment().LiteralIsTrue(Literal(a, true))); + EXPECT_TRUE(solver->Assignment().LiteralIsFalse(Literal(b, true))); +} + +// A simple macro to make the code more readable. +#define EXPECT_BOUNDS_EQ(var, lb, ub) \ + EXPECT_EQ(model.Get(LowerBound(var)), lb); \ + EXPECT_EQ(model.Get(UpperBound(var)), ub) + +TEST(PartialIsOneOfVarTest, MinMaxPropagation) { + Model model; + const IntegerVariable target_var = model.Add(NewIntegerVariable(-10, 20)); + std::vector vars; + std::vector selectors; + for (int i = 0; i < 10; ++i) { + vars.push_back(model.Add(ConstantIntegerVariable(i))); + selectors.push_back(Literal(model.Add(NewBooleanVariable()), true)); + } + model.Add(PartialIsOneOfVar(target_var, vars, selectors)); + + EXPECT_TRUE(model.GetOrCreate()->Propagate()); + EXPECT_BOUNDS_EQ(target_var, 0, 9); + + model.Add(ClauseConstraint({selectors[0].Negated()})); + EXPECT_TRUE(model.GetOrCreate()->Propagate()); + EXPECT_BOUNDS_EQ(target_var, 1, 9); + + model.Add(ClauseConstraint({selectors[8].Negated()})); + EXPECT_TRUE(model.GetOrCreate()->Propagate()); + EXPECT_BOUNDS_EQ(target_var, 1, 9); + + model.Add(ClauseConstraint({selectors[9].Negated()})); + EXPECT_TRUE(model.GetOrCreate()->Propagate()); + EXPECT_BOUNDS_EQ(target_var, 1, 7); +} + +TEST(GreaterThanAtLeastOneOfPropagatorTest, BasicTest) { + for (int i = 0; i < 2; ++i) { + Model model; + + // We create a simple model with 3 variables and 2 conditional precedences. + // We only add the GreaterThanAtLeastOneOfPropagator() for i == 1. + const IntegerVariable a = model.Add(NewIntegerVariable(0, 3)); + const IntegerVariable b = model.Add(NewIntegerVariable(0, 3)); + const IntegerVariable c = model.Add(NewIntegerVariable(0, 3)); + const Literal ac = Literal(model.Add(NewBooleanVariable()), true); + const Literal bc = Literal(model.Add(NewBooleanVariable()), true); + model.Add(ConditionalLowerOrEqualWithOffset(a, c, 3, ac)); + model.Add(ConditionalLowerOrEqualWithOffset(b, c, 2, bc)); + model.Add(ClauseConstraint({ac, bc})); + if (i == 1) { + model.Add(GreaterThanAtLeastOneOf( + c, {a, b}, {IntegerValue(3), IntegerValue(2)}, {ac, bc}, {})); + } + + // Test that we do propagate more with the extra propagator. + EXPECT_TRUE(model.GetOrCreate()->Propagate()); + EXPECT_EQ(model.Get(LowerBound(c)), i == 0 ? 0 : 2); + + // Test that we find all solutions. + int num_solutions = 0; + while (true) { + const auto status = SolveIntegerProblemWithLazyEncoding(&model); + if (status != SatSolver::Status::FEASIBLE) break; + ++num_solutions; + VLOG(1) << model.Get(Value(a)) << " " << model.Get(Value(b)) << " " + << model.Get(Value(c)); + model.Add(ExcludeCurrentSolutionAndBacktrack()); + } + EXPECT_EQ(num_solutions, 18); + } +} + +#undef EXPECT_BOUNDS_EQ + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/cumulative_energy_test.cc b/ortools/sat/cumulative_energy_test.cc new file mode 100644 index 0000000000..8e58b53a28 --- /dev/null +++ b/ortools/sat/cumulative_energy_test.cc @@ -0,0 +1,562 @@ +// 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/cumulative_energy.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/strings/str_cat.h" +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/base/logging.h" +#include "ortools/sat/2d_orthogonal_packing_testing.h" +#include "ortools/sat/cp_model_solver.h" +#include "ortools/sat/cumulative.h" +#include "ortools/sat/diffn_util.h" +#include "ortools/sat/integer.h" +#include "ortools/sat/integer_search.h" +#include "ortools/sat/intervals.h" +#include "ortools/sat/linear_constraint.h" +#include "ortools/sat/model.h" +#include "ortools/sat/precedences.h" +#include "ortools/sat/sat_base.h" +#include "ortools/sat/sat_parameters.pb.h" +#include "ortools/sat/sat_solver.h" +#include "ortools/util/strong_integers.h" + +namespace operations_research { +namespace sat { +namespace { + +// An instance is a set of energy tasks and a capacity. +struct EnergyTask { + int start_min; + int end_max; + int energy_min; + int energy_max; + int duration_min; + int duration_max; + bool is_optional; +}; + +struct EnergyInstance { + std::vector tasks; + int capacity; +}; + +std::string InstanceDebugString(const EnergyInstance& instance) { + std::string result; + absl::StrAppend(&result, "Instance capacity:", instance.capacity, "\n"); + for (const EnergyTask& task : instance.tasks) { + absl::StrAppend(&result, "[", task.start_min, ", ", task.end_max, + "] duration:", task.duration_min, "..", task.duration_max, + " energy:", task.energy_min, "..", task.energy_max, + " is_optional:", task.is_optional, "\n"); + } + return result; +} + +// Satisfiability using the constraint. +bool SolveUsingConstraint(const EnergyInstance& instance) { + Model model; + std::vector intervals; + std::vector energies; + for (const auto& task : instance.tasks) { + LinearExpression energy; + energy.vars.push_back( + model.Add(NewIntegerVariable(task.energy_min, task.energy_max))); + energy.coeffs.push_back(IntegerValue(1)); + energies.push_back(energy); + if (task.is_optional) { + const Literal is_present = Literal(model.Add(NewBooleanVariable()), true); + const IntegerVariable start = + model.Add(NewIntegerVariable(task.start_min, task.end_max)); + const IntegerVariable end = + model.Add(NewIntegerVariable(task.start_min, task.end_max)); + const IntegerVariable duration = + model.Add(NewIntegerVariable(task.duration_min, task.duration_max)); + intervals.push_back( + model.Add(NewOptionalInterval(start, end, duration, is_present))); + } else { + intervals.push_back(model.Add(NewIntervalWithVariableSize( + task.start_min, task.end_max, task.duration_min, task.duration_max))); + } + } + + const AffineExpression capacity( + model.Add(ConstantIntegerVariable(instance.capacity))); + + SchedulingConstraintHelper* helper = + new SchedulingConstraintHelper(intervals, &model); + model.TakeOwnership(helper); + SchedulingDemandHelper* demands_helper = + new SchedulingDemandHelper({}, helper, &model); + demands_helper->OverrideLinearizedEnergies(energies); + model.TakeOwnership(demands_helper); + + AddCumulativeOverloadChecker(capacity, helper, demands_helper, &model); + + return SolveIntegerProblemWithLazyEncoding(&model) == + SatSolver::Status::FEASIBLE; +} + +// One task by itself is infeasible. +TEST(CumulativeEnergyTest, UnfeasibleFixedCharacteristics) { + EnergyInstance instance = {{{0, 100, 11, 11, 2, 2, false}}, 5}; + EXPECT_FALSE(SolveUsingConstraint(instance)) << InstanceDebugString(instance); +} + +// Tasks are feasible iff all are at energy min. +TEST(CumulativeEnergyTest, FeasibleEnergyMin) { + EnergyInstance instance = {{ + {-10, 10, 10, 15, 0, 20, false}, + {-10, 10, 15, 20, 0, 20, false}, + {-10, 10, 5, 10, 0, 20, false}, + }, + 3}; + EXPECT_TRUE(SolveUsingConstraint(instance)) << InstanceDebugString(instance); +} + +// Tasks are feasible iff optionals tasks are removed. +TEST(CumulativeEnergyTest, FeasibleRemoveOptionals) { + EnergyInstance instance = {{ + {-10, 10, 1, 1, 1, 1, true}, + {-10, 10, 5, 10, 7, 7, true}, + {-10, 10, 10, 15, 0, 20, false}, + {-10, 10, 15, 20, 0, 20, false}, + {-10, 10, 5, 10, 0, 20, false}, + }, + 3}; + EXPECT_TRUE(SolveUsingConstraint(instance)) << InstanceDebugString(instance); +} + +// This instance was problematic. +TEST(CumulativeEnergyTest, Problematic1) { + EnergyInstance instance = {{ + {2, 18, 6, 7, 5, 10, false}, + {2, 25, 6, 9, 14, 17, false}, + {-4, 19, 6, 9, 10, 20, false}, + {-9, 7, 6, 15, 9, 16, false}, + {-1, 19, 6, 12, 6, 14, false}, + }, + 1}; + EXPECT_TRUE(SolveUsingConstraint(instance)) << InstanceDebugString(instance); +} + +// Satisfiability using a naive model: one task per unit of energy. +// Force energy-based reasoning in Cumulative() and add symmetry breaking, +// or the solver has a much harder time. +bool SolveUsingNaiveModel(const EnergyInstance& instance) { + Model model; + std::vector intervals; + std::vector consumptions; + IntegerVariable one = model.Add(ConstantIntegerVariable(1)); + IntervalsRepository* intervals_repository = + model.GetOrCreate(); + + for (const auto& task : instance.tasks) { + if (task.is_optional) { + const Literal is_present = Literal(model.Add(NewBooleanVariable()), true); + for (int i = 0; i < task.energy_min; i++) { + const IntegerVariable start = + model.Add(NewIntegerVariable(task.start_min, task.end_max)); + const IntegerVariable end = + model.Add(NewIntegerVariable(task.start_min, task.end_max)); + + intervals.push_back( + model.Add(NewOptionalInterval(start, end, one, is_present))); + consumptions.push_back(AffineExpression(IntegerValue(1))); + } + } else { + IntegerVariable first_start = kNoIntegerVariable; + IntegerVariable previous_start = kNoIntegerVariable; + for (int i = 0; i < task.energy_min; i++) { + IntervalVariable interval = + model.Add(NewInterval(task.start_min, task.end_max, 1)); + intervals.push_back(interval); + consumptions.push_back(AffineExpression(IntegerValue(1))); + const AffineExpression start_expr = + intervals_repository->Start(interval); + CHECK_EQ(start_expr.coeff, 1); + CHECK_EQ(start_expr.constant, 0); + CHECK_NE(start_expr.var, kNoIntegerVariable); + const IntegerVariable start = start_expr.var; + if (previous_start != kNoIntegerVariable) { + model.Add(LowerOrEqual(previous_start, start)); + } else { + first_start = start; + } + previous_start = start; + } + // start[last] <= start[0] + duration_max - 1 + if (previous_start != kNoIntegerVariable) { + model.Add(LowerOrEqualWithOffset(previous_start, first_start, + -task.duration_max + 1)); + } + } + } + + SatParameters params = + model.Add(NewSatParameters("use_overload_checker_in_cumulative:true")); + model.Add(Cumulative(intervals, consumptions, + AffineExpression(IntegerValue(instance.capacity)))); + + return SolveIntegerProblemWithLazyEncoding(&model) == + SatSolver::Status::FEASIBLE; +} + +// Generates random instances, fill the schedule to try and make a tricky case. +EnergyInstance GenerateRandomInstance(int num_tasks, + absl::BitGenRef randomizer) { + const int capacity = absl::Uniform(randomizer, 1, 12); + std::vector tasks; + for (int i = 0; i < num_tasks; i++) { + int start_min = absl::Uniform(randomizer, -10, 10); + int duration_min = absl::Uniform(randomizer, 1, 21); + int duration_max = absl::Uniform(randomizer, 1, 21); + if (duration_min > duration_max) std::swap(duration_min, duration_max); + int end_max = start_min + duration_max + absl::Uniform(randomizer, 0, 10); + int energy_min = (capacity * 30) / num_tasks; + int energy_max = energy_min + absl::Uniform(randomizer, 1, 10); + tasks.push_back({start_min, end_max, energy_min, energy_max, duration_min, + duration_max, false}); + } + + return {tasks, capacity}; +} + +// Compare constraint to naive model. +TEST(CumulativeEnergyTest, CompareToNaiveModel) { + const int num_tests = 10; + std::mt19937 randomizer(12345); + for (int test = 0; test < num_tests; test++) { + EnergyInstance instance = + GenerateRandomInstance(absl::Uniform(randomizer, 4, 7), randomizer); + bool result_constraint = SolveUsingConstraint(instance); + bool result_naive = SolveUsingNaiveModel(instance); + EXPECT_EQ(result_naive, result_constraint) << InstanceDebugString(instance); + LOG(INFO) << result_constraint; + } +} + +struct CumulativeTasks { + int64_t duration; + int64_t demand; + int64_t min_start; + int64_t max_end; +}; + +enum class PropagatorChoice { + OVERLOAD, + OVERLOAD_DFF, +}; +bool TestOverloadCheckerPropagation( + absl::Span tasks, int capacity_min_before, + int capacity_min_after, int capacity_max, + PropagatorChoice propagator_choice = PropagatorChoice::OVERLOAD) { + Model model; + IntegerTrail* integer_trail = model.GetOrCreate(); + PrecedencesPropagator* precedences = + model.GetOrCreate(); + + const int num_tasks = tasks.size(); + std::vector interval_vars(num_tasks); + std::vector demands(num_tasks); + const AffineExpression capacity = + AffineExpression(integer_trail->AddIntegerVariable( + IntegerValue(capacity_min_before), IntegerValue(capacity_max))); + + // Build the task variables. + for (int t = 0; t < num_tasks; ++t) { + interval_vars[t] = model.Add( + NewInterval(tasks[t].min_start, tasks[t].max_end, tasks[t].duration)); + demands[t] = AffineExpression(IntegerValue(tasks[t].demand)); + } + + // Propagate properly the other bounds of the intervals. + EXPECT_TRUE(precedences->Propagate()); + + // Propagator responsible for filtering the capacity variable. + SchedulingConstraintHelper* helper = + new SchedulingConstraintHelper(interval_vars, &model); + model.TakeOwnership(helper); + SchedulingDemandHelper* demands_helper = + new SchedulingDemandHelper(demands, helper, &model); + model.TakeOwnership(demands_helper); + + if (propagator_choice == PropagatorChoice::OVERLOAD) { + AddCumulativeOverloadChecker(capacity, helper, demands_helper, &model); + } else if (propagator_choice == PropagatorChoice::OVERLOAD_DFF) { + AddCumulativeOverloadCheckerDff(capacity, helper, demands_helper, &model); + } else { + LOG(FATAL) << "Unknown propagator choice!"; + } + + // Check initial satisfiability. + auto* sat_solver = model.GetOrCreate(); + if (!sat_solver->Propagate()) return false; + + // Check capacity. + EXPECT_EQ(capacity_min_after, integer_trail->LowerBound(capacity)); + return true; +} + +// This is a trivially infeasible instance. +TEST(OverloadCheckerTest, UNSAT1) { + EXPECT_FALSE( + TestOverloadCheckerPropagation({{4, 2, 0, 7}, {4, 2, 0, 7}}, 2, 2, 2)); +} + +// This is an infeasible instance on which timetabling finds nothing. The +// overload checker finds the contradiction. +TEST(OverloadCheckerTest, UNSAT2) { + EXPECT_FALSE(TestOverloadCheckerPropagation( + {{4, 2, 0, 8}, {4, 2, 0, 8}, {4, 2, 0, 8}}, 2, 2, 2)); +} + +// This is the same instance as in UNSAT1 but here the capacity can increase. +TEST(OverloadCheckerTest, IncreaseCapa1) { + EXPECT_TRUE( + TestOverloadCheckerPropagation({{4, 2, 2, 9}, {4, 2, 2, 9}}, 2, 3, 10)); +} + +// This is an instance in which tasks can be perfectly packed in a rectangle of +// size 5 to 6. OverloadChecker increases the capacity from 3 to 5. +TEST(OverloadCheckerTest, IncreaseCapa2) { + EXPECT_TRUE(TestOverloadCheckerPropagation({{5, 2, 2, 8}, + {2, 3, 2, 8}, + {2, 1, 2, 8}, + {1, 3, 2, 8}, + {1, 3, 2, 8}, + {3, 2, 2, 8}}, + 3, 5, 10)); +} + +// This is an instance in which OverloadChecker increases the capacity. +TEST(OverloadCheckerTest, IncreaseCapa3) { + EXPECT_TRUE(TestOverloadCheckerPropagation( + {{1, 3, 3, 6}, {1, 3, 3, 6}, {1, 1, 3, 8}}, 0, 2, 10)); +} + +// This is a trivially infeasible instance with negative times. +TEST(OverloadCheckerTest, UNSATNeg1) { + EXPECT_FALSE( + TestOverloadCheckerPropagation({{4, 2, -7, 0}, {4, 2, -7, 0}}, 2, 2, 2)); +} + +// This is an infeasible instance with negative times on which timetabling finds +// nothing. The overload checker finds the contradiction. +TEST(OverloadCheckerTest, UNSATNeg2) { + EXPECT_FALSE(TestOverloadCheckerPropagation( + {{4, 2, -4, 4}, {4, 2, -4, 4}, {4, 2, -4, 4}}, 2, 2, 2)); +} + +// This is the same instance as in UNSATNeg1 but here the capacity can increase. +TEST(OverloadCheckerTest, IncreaseCapaNeg1) { + EXPECT_TRUE(TestOverloadCheckerPropagation({{4, 2, -10, -3}, {4, 2, -10, -3}}, + 2, 3, 10)); +} + +// This is an instance with negative times in which tasks can be perfectly +// packed in a rectangle of size 5 to 6. OverloadChecker increases the capacity +// from 3 to 5. +TEST(OverloadCheckerTest, IncreaseCapaNeg2) { + EXPECT_TRUE(TestOverloadCheckerPropagation({{5, 2, -2, 4}, + {2, 3, -2, 4}, + {2, 1, -2, 4}, + {1, 3, -2, 4}, + {1, 3, -2, 4}, + {3, 2, -2, 4}}, + 3, 5, 10)); +} + +// This is an instance with negative times in which OverloadChecker increases +// the capacity. +TEST(OverloadCheckerTest, IncreaseCapaNeg3) { + EXPECT_TRUE(TestOverloadCheckerPropagation( + {{1, 3, -3, 0}, {1, 3, -3, 0}, {1, 1, -3, 2}}, 0, 2, 10)); +} + +TEST(OverloadCheckerTest, OptionalTaskPropagatedToAbsent) { + Model model; + const Literal is_present = Literal(model.Add(NewBooleanVariable()), true); + + // TODO(user): Fix the code! the propagation is dependent on the order of + // tasks. If we use the proper theta-lambda tree, this will be fixed. + const IntervalVariable i2 = model.Add(NewInterval(0, 10, /*size=*/8)); + const IntervalVariable i1 = + model.Add(NewOptionalInterval(0, 10, /*size=*/8, is_present)); + + SchedulingConstraintHelper* helper = + new SchedulingConstraintHelper({i1, i2}, &model); + model.TakeOwnership(helper); + const AffineExpression cte(IntegerValue(2)); + SchedulingDemandHelper* demands_helper = + new SchedulingDemandHelper({cte, cte}, helper, &model); + model.TakeOwnership(demands_helper); + + AddCumulativeOverloadChecker(cte, helper, demands_helper, &model); + EXPECT_TRUE(model.GetOrCreate()->Propagate()); + EXPECT_FALSE(model.Get(Value(is_present))); +} + +TEST(OverloadCheckerTest, OptionalTaskMissedPropagationCase) { + Model model; + const Literal is_present = Literal(model.Add(NewBooleanVariable()), true); + const IntervalVariable i1 = + model.Add(NewOptionalInterval(0, 10, /*size=*/8, is_present)); + const IntervalVariable i2 = + model.Add(NewOptionalInterval(0, 10, /*size=*/8, is_present)); + + SchedulingConstraintHelper* helper = + new SchedulingConstraintHelper({i1, i2}, &model); + model.TakeOwnership(helper); + const AffineExpression cte(IntegerValue(2)); + SchedulingDemandHelper* demands_helper = + new SchedulingDemandHelper({cte, cte}, helper, &model); + model.TakeOwnership(demands_helper); + + AddCumulativeOverloadChecker(cte, helper, demands_helper, &model); + EXPECT_TRUE(model.GetOrCreate()->Propagate()); + EXPECT_FALSE(model.GetOrCreate()->Assignment().VariableIsAssigned( + is_present.Variable())); +} + +TEST(OverloadCheckerDffTest, DffIsNeeded) { + const std::vector tasks = { + {.duration = 10, .demand = 5, .min_start = 0, .max_end = 22}, + {.duration = 10, .demand = 5, .min_start = 0, .max_end = 22}, + {.duration = 10, .demand = 5, .min_start = 0, .max_end = 22}, + {.duration = 10, .demand = 5, .min_start = 0, .max_end = 22}, + }; + EXPECT_FALSE(TestOverloadCheckerPropagation(tasks, /*capacity_min_before=*/9, + /*capacity_min_after=*/9, + /*capacity_max=*/9, + PropagatorChoice::OVERLOAD_DFF)); +} + +TEST(OverloadCheckerDffTest, NoConflictRandomFeasibleProblem) { + absl::BitGen random; + for (int i = 0; i < 100; ++i) { + const std::vector rectangles = GenerateNonConflictingRectangles( + absl::Uniform(random, 6, 20), random); + Rectangle bounding_box; + for (const auto& item : rectangles) { + bounding_box.x_min = std::min(bounding_box.x_min, item.x_min); + bounding_box.x_max = std::max(bounding_box.x_max, item.x_max); + bounding_box.y_min = std::min(bounding_box.y_min, item.y_min); + bounding_box.y_max = std::max(bounding_box.y_max, item.y_max); + } + const std::vector range_items = + MakeItemsFromRectangles(rectangles, 0.3, random); + std::vector tasks(range_items.size()); + + for (int i = 0; i < range_items.size(); ++i) { + tasks[i] = {.duration = range_items[i].x_size.value(), + .demand = range_items[i].y_size.value(), + .min_start = range_items[i].bounding_area.x_min.value(), + .max_end = range_items[i].bounding_area.x_max.value()}; + } + EXPECT_TRUE(TestOverloadCheckerPropagation( + tasks, /*capacity_min_before=*/bounding_box.SizeY().value(), + /*capacity_min_after=*/bounding_box.SizeY().value(), + /*capacity_max=*/bounding_box.SizeY().value(), + PropagatorChoice::OVERLOAD_DFF)); + } +} + +bool TestIsAfterCumulative(absl::Span tasks, + int capacity_max, int expected_end_min) { + Model model; + IntegerTrail* integer_trail = model.GetOrCreate(); + PrecedencesPropagator* precedences = + model.GetOrCreate(); + + const int num_tasks = tasks.size(); + std::vector interval_vars(num_tasks); + std::vector demands(num_tasks); + const AffineExpression capacity = + AffineExpression(integer_trail->AddIntegerVariable( + IntegerValue(capacity_max), IntegerValue(capacity_max))); + + // Build the task variables. + std::vector subtasks; + for (int t = 0; t < num_tasks; ++t) { + interval_vars[t] = model.Add( + NewInterval(tasks[t].min_start, tasks[t].max_end, tasks[t].duration)); + demands[t] = AffineExpression(IntegerValue(tasks[t].demand)); + subtasks.push_back(t); + } + + // Propagate properly the other bounds of the intervals. + EXPECT_TRUE(precedences->Propagate()); + + // Propagator responsible for filtering the capacity variable. + SchedulingConstraintHelper* helper = + new SchedulingConstraintHelper(interval_vars, &model); + model.TakeOwnership(helper); + SchedulingDemandHelper* demands_helper = + new SchedulingDemandHelper(demands, helper, &model); + model.TakeOwnership(demands_helper); + + const IntegerVariable var = + integer_trail->AddIntegerVariable(IntegerValue(0), IntegerValue(100)); + + std::vector offsets(subtasks.size(), IntegerValue(0)); + CumulativeIsAfterSubsetConstraint* propag = + new CumulativeIsAfterSubsetConstraint(var, capacity, subtasks, offsets, + helper, demands_helper, &model); + propag->RegisterWith(model.GetOrCreate()); + model.TakeOwnership(propag); + + // Check initial satisfiability. + auto* sat_solver = model.GetOrCreate(); + if (!sat_solver->Propagate()) return false; + + // Check bound + EXPECT_EQ(expected_end_min, integer_trail->LowerBound(var)); + return true; +} + +// We detect that the interval cannot overlap. +TEST(IsAfterCumulativeTest, BasicCase1) { + // duration, demand, start_min, end_max + EXPECT_TRUE(TestIsAfterCumulative({{4, 2, 0, 8}, {4, 2, 0, 10}}, + /*capacity_max=*/3, + /*expected_end_min=*/8)); +} + +// Now, one interval can overlap. It is also after the other, so the best bound +// we get is not that great: energy = 2 + 8 + 8 = 18, with capa = 3, we get 6. +// +// TODO(user): Maybe we can do more advanced reasoning to recover the 8 here. +TEST(IsAfterCumulativeTest, BasicCase2) { + // duration, demand, start_min, end_max. + EXPECT_TRUE(TestIsAfterCumulative({{2, 1, 3, 8}, {4, 2, 0, 8}, {4, 2, 0, 10}}, + /*capacity_max=*/3, + /*expected_end_min=*/6)); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/cumulative_test.cc b/ortools/sat/cumulative_test.cc new file mode 100644 index 0000000000..e14f2c63ce --- /dev/null +++ b/ortools/sat/cumulative_test.cc @@ -0,0 +1,421 @@ +// 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/cumulative.h" + +#include +#include +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/random/random.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_join.h" +#include "gtest/gtest.h" +#include "ortools/base/logging.h" +#include "ortools/sat/integer.h" +#include "ortools/sat/integer_search.h" +#include "ortools/sat/intervals.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/strong_integers.h" + +namespace operations_research { +namespace sat { +namespace { + +// RcpspInstance contains the data to define an instance of the Resource +// Constrained Project Scheduling Problem (RCPSP). We only consider a restricted +// variant of the RCPSP which is the problem of scheduling a set of +// non-premptive tasks that consume a given quantity of a resource without +// exceeding the resource's capacity. We assume that the duration of a task, its +// demand, and the resource capacity are fixed. +struct RcpspInstance { + RcpspInstance() : capacity(0), min_start(0), max_end(0) {} + std::vector durations; + std::vector optional; + std::vector demands; + int64_t capacity; + int64_t min_start; + int64_t max_end; + std::string DebugString() const { + std::string result = "RcpspInstance {\n"; + result += " demands: {" + absl::StrJoin(demands, ", ") + "}\n"; + result += " durations: {" + absl::StrJoin(durations, ", ") + "}\n"; + result += " optional: {" + absl::StrJoin(optional, ", ") + "}\n"; + result += " min_start: " + absl::StrCat(min_start) + "\n"; + result += " max_end: " + absl::StrCat(max_end) + "\n"; + result += " capacity: " + absl::StrCat(capacity) + "\n}"; + return result; + } +}; + +// Generates a random RcpspInstance with num_tasks tasks such that: +// - the duration of a task is a fixed random number in +// [min_duration, max_durations]; +// - tasks can be optional if enable_optional is true; +// - the demand of a task is a fixed random number in [min_demand, max_demand]; +// - the resource capacity is a fixed random number in +// [max_demand - 1, max_capacity]. This allows the capacity to be lower than +// the highest demand to generate trivially unfeasible instances. +// - the energy (i.e. surface) of the resource is 120% of the total energy of +// the tasks. This allows the generation of infeasible instances. +RcpspInstance GenerateRandomInstance(int num_tasks, int min_duration, + int max_duration, int min_demand, + int max_demand, int max_capacity, + int min_start, bool enable_optional) { + absl::BitGen random; + RcpspInstance instance; + int energy = 0; + + // Generate task demands and durations. + int max_of_all_durations = 0; + for (int t = 0; t < num_tasks; ++t) { + const int duration = absl::Uniform(random, min_duration, max_duration + 1); + const int demand = absl::Uniform(random, min_demand, max_demand + 1); + energy += duration * demand; + max_of_all_durations = std::max(max_of_all_durations, duration); + instance.durations.push_back(duration); + instance.demands.push_back(demand); + instance.optional.push_back(enable_optional && + absl::Bernoulli(random, 0.5)); + } + + // Generate the resource capacity. + instance.capacity = absl::Uniform(random, max_demand, max_capacity + 1); + + // Generate the time window. + instance.min_start = min_start; + instance.max_end = + min_start + + std::max(static_cast(std::round(energy * 1.2 / instance.capacity)), + max_of_all_durations); + return instance; +} + +template +int CountAllSolutions(const RcpspInstance& instance, SatParameters parameters, + const Cumulative& cumulative) { + Model model; + parameters.set_use_disjunctive_constraint_in_cumulative(false); + model.GetOrCreate()->SetParameters(parameters); + + DCHECK_EQ(instance.demands.size(), instance.durations.size()); + DCHECK_LE(instance.min_start, instance.max_end); + + const int num_tasks = instance.demands.size(); + std::vector intervals(num_tasks); + std::vector demands(num_tasks); + const AffineExpression capacity = IntegerValue(instance.capacity); + + for (int t = 0; t < num_tasks; ++t) { + if (instance.optional[t]) { + const Literal is_present = Literal(model.Add(NewBooleanVariable()), true); + intervals[t] = + model.Add(NewOptionalInterval(instance.min_start, instance.max_end, + instance.durations[t], is_present)); + } else { + intervals[t] = model.Add(NewInterval(instance.min_start, instance.max_end, + instance.durations[t])); + } + demands[t] = IntegerValue(instance.demands[t]); + } + + model.Add(cumulative(intervals, demands, capacity, nullptr)); + + // Make sure that every Boolean variable is considered as a decision variable + // to be fixed. + if (parameters.search_branching() == SatParameters::FIXED_SEARCH) { + SatSolver* sat_solver = model.GetOrCreate(); + for (int i = 0; i < sat_solver->NumVariables(); ++i) { + model.Add( + NewIntegerVariableFromLiteral(Literal(BooleanVariable(i), true))); + } + } + + int num_solutions_found = 0; + // Loop until there is no remaining solution to find. + while (true) { + // Try to find a solution. + const SatSolver::Status status = + SolveIntegerProblemWithLazyEncoding(&model); + // Leave the loop if there is no solution left. + if (status != SatSolver::Status::FEASIBLE) break; + num_solutions_found++; + model.Add(ExcludeCurrentSolutionAndBacktrack()); + } + + return num_solutions_found; +} + +TEST(CumulativeTimeDecompositionTest, AllPermutations) { + RcpspInstance instance; + instance.demands = {1, 1, 1, 1, 1}; + instance.durations = {1, 1, 1, 1, 1}; + instance.optional = {false, false, false, false, false}; + instance.capacity = 1; + instance.min_start = 0; + instance.max_end = 5; + ASSERT_EQ(120, CountAllSolutions(instance, {}, CumulativeTimeDecomposition)); +} + +TEST(CumulativeTimeDecompositionTest, FindAll) { + RcpspInstance instance; + instance.demands = {1, 1, 1, 1, 4, 4}; + instance.durations = {1, 2, 3, 3, 3, 3}; + instance.optional = {false, false, false, false, false, false}; + instance.capacity = 4; + instance.min_start = 0; + instance.max_end = 11; + ASSERT_EQ(2040, CountAllSolutions(instance, {}, CumulativeTimeDecomposition)); + ASSERT_EQ(2040, CountAllSolutions(instance, {}, CumulativeUsingReservoir)); +} + +TEST(CumulativeTimeDecompositionTest, OptionalTasks1) { + RcpspInstance instance; + instance.demands = {3, 3, 3}; + instance.durations = {1, 1, 1}; + instance.optional = {true, true, true}; + instance.capacity = 7; + instance.min_start = 0; + instance.max_end = 2; + ASSERT_EQ(25, CountAllSolutions(instance, {}, Cumulative)); + ASSERT_EQ(25, CountAllSolutions(instance, {}, CumulativeUsingReservoir)); +} + +// Up to two tasks can be scheduled at the same time. +TEST(CumulativeTimeDecompositionTest, OptionalTasks2) { + RcpspInstance instance; + instance.demands = {3, 3, 3}; + instance.durations = {3, 3, 3}; + instance.optional = {true, true, true}; + instance.capacity = 7; + instance.min_start = 0; + instance.max_end = 3; + ASSERT_EQ(7, CountAllSolutions(instance, {}, CumulativeTimeDecomposition)); + ASSERT_EQ(7, CountAllSolutions(instance, {}, CumulativeUsingReservoir)); +} + +TEST(CumulativeTimeDecompositionTest, RegressionTest1) { + RcpspInstance instance; + instance.demands = {5, 4, 1}; + instance.durations = {1, 1, 2}; + instance.optional = {false, false, false}; + instance.capacity = 5; + instance.min_start = 0; + instance.max_end = 2; + ASSERT_EQ(0, CountAllSolutions(instance, {}, CumulativeTimeDecomposition)); +} + +// Cumulative was pruning too many solutions on that instance. +TEST(CumulativeTimeDecompositionTest, RegressionTest2) { + SatParameters parameters; + parameters.set_use_overload_checker_in_cumulative(false); + parameters.set_use_timetable_edge_finding_in_cumulative(false); + RcpspInstance instance; + instance.demands = {4, 4, 3}; + instance.durations = {2, 2, 3}; + instance.optional = {true, true, true}; + instance.capacity = 6; + instance.min_start = 0; + instance.max_end = 5; + ASSERT_EQ( + 22, CountAllSolutions(instance, parameters, CumulativeTimeDecomposition)); +} + +bool CheckCumulative(const SatParameters& parameters, + const RcpspInstance& instance) { + const int64_t num_solutions_ref = + CountAllSolutions(instance, parameters, CumulativeTimeDecomposition); + const int64_t num_solutions_test = + CountAllSolutions(instance, parameters, Cumulative); + if (num_solutions_ref != num_solutions_test) { + LOG(INFO) << "Want: " << num_solutions_ref + << " solutions, got: " << num_solutions_test << " solutions."; + LOG(INFO) << instance.DebugString(); + return false; + } + const int64_t num_solutions_reservoir = + CountAllSolutions(instance, parameters, CumulativeUsingReservoir); + if (num_solutions_ref != num_solutions_reservoir) { + LOG(INFO) << "Want: " << num_solutions_ref + << " solutions, got: " << num_solutions_reservoir + << " solutions."; + LOG(INFO) << instance.DebugString(); + return false; + } + return true; +} + +// Checks that the cumulative constraint performs trivial propagation by +// updating the capacity and demand variables. +TEST(CumulativeTest, CapacityAndDemand) { + Model model; + SatSolver* sat_solver = model.GetOrCreate(); + const IntervalVariable interval = model.Add(NewInterval(-1000, 1000, 1)); + const IntegerVariable demand = model.Add(NewIntegerVariable(5, 15)); + const IntegerVariable capacity = model.Add(NewIntegerVariable(0, 10)); + const IntegerTrail* integer_trail = model.GetOrCreate(); + model.Add(Cumulative({interval}, {AffineExpression(demand)}, + AffineExpression(capacity))); + ASSERT_TRUE(sat_solver->Propagate()); + ASSERT_EQ(integer_trail->LowerBound(capacity), 5); + ASSERT_EQ(integer_trail->UpperBound(capacity), 10); + ASSERT_EQ(integer_trail->LowerBound(demand), 5); + ASSERT_EQ(integer_trail->UpperBound(demand), 10); +} + +// Checks that the cumulative constraint adpats the demand of the task to +// prevent the capacity overload. +TEST(CumulativeTest, CapacityAndZeroDemand) { + Model model; + SatSolver* sat_solver = model.GetOrCreate(); + const IntegerVariable start = model.Add(NewIntegerVariable(-1000, 1000)); + const IntegerVariable size = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable end = model.Add(NewIntegerVariable(-1000, 1000)); + const IntervalVariable interval = model.Add(NewInterval(start, end, size)); + const IntegerVariable demand = model.Add(NewIntegerVariable(11, 15)); + const IntegerVariable capacity = model.Add(NewIntegerVariable(0, 10)); + const IntegerTrail* integer_trail = model.GetOrCreate(); + model.Add(Cumulative({interval}, {AffineExpression(demand)}, + AffineExpression(capacity))); + ASSERT_TRUE(sat_solver->Propagate()); + ASSERT_EQ(integer_trail->LowerBound(capacity), 0); + ASSERT_EQ(integer_trail->UpperBound(capacity), 10); + ASSERT_EQ(integer_trail->LowerBound(demand), 11); + ASSERT_EQ(integer_trail->UpperBound(demand), 15); + ASSERT_EQ(integer_trail->UpperBound(size), 0); +} + +// Checks that the cumulative constraint removes the task to prevent the +// capacity overload. +TEST(CumulativeTest, CapacityAndOptionalTask) { + Model model; + SatSolver* sat_solver = model.GetOrCreate(); + const Literal l = Literal(model.Add(NewBooleanVariable()), true); + const IntervalVariable interval = + model.Add(NewOptionalInterval(-1000, 1000, 1, l)); + const IntegerVariable demand = model.Add(ConstantIntegerVariable(15)); + const IntegerVariable capacity = model.Add(ConstantIntegerVariable(10)); + model.Add(Cumulative({interval}, {AffineExpression(demand)}, + AffineExpression(capacity))); + ASSERT_TRUE(sat_solver->Propagate()); + ASSERT_FALSE(model.Get(Value(l))); +} + +// Cumulative was pruning too many solutions on that instance. +TEST(CumulativeTest, RegressionTest1) { + SatParameters parameters; + parameters.set_use_overload_checker_in_cumulative(false); + parameters.set_use_timetable_edge_finding_in_cumulative(false); + RcpspInstance instance; + instance.demands = {4, 4, 3}; + instance.durations = {2, 2, 3}; + instance.optional = {true, true, true}; + instance.capacity = 6; + instance.min_start = 0; + instance.max_end = 5; + ASSERT_EQ(22, CountAllSolutions(instance, parameters, Cumulative)); +} + +// Cumulative was pruning too many solutions on that instance. +TEST(CumulativeTest, RegressionTest2) { + SatParameters parameters; + parameters.set_use_overload_checker_in_cumulative(false); + parameters.set_use_timetable_edge_finding_in_cumulative(false); + RcpspInstance instance; + instance.demands = {5, 4}; + instance.durations = {4, 4}; + instance.optional = {true, true}; + instance.capacity = 6; + instance.min_start = 0; + instance.max_end = 7; + ASSERT_EQ(9, CountAllSolutions(instance, parameters, Cumulative)); +} + +// ======================================================================== +// All the test belows check that the cumulative propagator finds the exact +// same number of solutions than its time point decomposition. +// ======================================================================== + +// Param1: Number of tasks. +// Param3: Enable overload checking. +// Param4: Enable timetable edge finding. +typedef ::testing::tuple CumulativeTestParams; + +class RandomCumulativeTest + : public ::testing::TestWithParam { + protected: + int GetNumTasks() { return ::testing::get<0>(GetParam()); } + + SatParameters GetSatParameters() { + SatParameters parameters; + parameters.set_use_disjunctive_constraint_in_cumulative(false); + parameters.set_use_overload_checker_in_cumulative( + ::testing::get<1>(GetParam())); + parameters.set_use_timetable_edge_finding_in_cumulative( + ::testing::get<2>(GetParam())); + return parameters; + } +}; + +class FastRandomCumulativeTest : public RandomCumulativeTest {}; +class SlowRandomCumulativeTest : public RandomCumulativeTest {}; + +TEST_P(FastRandomCumulativeTest, FindAll) { + ASSERT_TRUE(CheckCumulative( + GetSatParameters(), + GenerateRandomInstance(GetNumTasks(), 1, 4, 1, 5, 7, 0, false))); +} + +TEST_P(FastRandomCumulativeTest, FindAllNegativeTime) { + ASSERT_TRUE(CheckCumulative( + GetSatParameters(), + GenerateRandomInstance(GetNumTasks(), 1, 4, 1, 5, 7, -100, false))); +} + +TEST_P(SlowRandomCumulativeTest, FindAllZeroDuration) { + ASSERT_TRUE(CheckCumulative( + GetSatParameters(), + GenerateRandomInstance(GetNumTasks(), 0, 4, 1, 5, 7, 0, false))); +} + +TEST_P(SlowRandomCumulativeTest, FindAllZeroDemand) { + ASSERT_TRUE(CheckCumulative( + GetSatParameters(), + GenerateRandomInstance(GetNumTasks(), 1, 4, 0, 5, 7, 0, false))); +} + +TEST_P(SlowRandomCumulativeTest, FindAllOptionalTasks) { + ASSERT_TRUE(CheckCumulative( + GetSatParameters(), + GenerateRandomInstance(GetNumTasks(), 1, 4, 0, 5, 7, 0, true))); +} + +INSTANTIATE_TEST_SUITE_P( + All, FastRandomCumulativeTest, + ::testing::Combine(::testing::Range(3, DEBUG_MODE ? 4 : 6), + ::testing::Bool(), ::testing::Bool())); + +INSTANTIATE_TEST_SUITE_P( + All, SlowRandomCumulativeTest, + ::testing::Combine(::testing::Range(3, DEBUG_MODE ? 4 : 5), + ::testing::Bool(), ::testing::Bool())); + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/cuts_test.cc b/ortools/sat/cuts_test.cc new file mode 100644 index 0000000000..130db4669f --- /dev/null +++ b/ortools/sat/cuts_test.cc @@ -0,0 +1,1163 @@ +// 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/cuts.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/numeric/int128.h" +#include "absl/strings/str_cat.h" +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/strong_vector.h" +#include "ortools/sat/implied_bounds.h" +#include "ortools/sat/integer.h" +#include "ortools/sat/linear_constraint.h" +#include "ortools/sat/linear_constraint_manager.h" +#include "ortools/sat/model.h" +#include "ortools/sat/sat_base.h" +#include "ortools/sat/sat_parameters.pb.h" +#include "ortools/util/fp_utils.h" +#include "ortools/util/sorted_interval_list.h" +#include "ortools/util/strong_integers.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::testing::EndsWith; +using ::testing::StartsWith; + +std::vector IntegerValueVector(absl::Span values) { + std::vector result; + for (const int v : values) result.push_back(IntegerValue(v)); + return result; +} + +TEST(GetSuperAdditiveRoundingFunctionTest, AllSmallValues) { + const int max_divisor = 25; + for (IntegerValue max_t(1); max_t <= 9; ++max_t) { + for (IntegerValue max_scaling(1); max_scaling <= 9; max_scaling++) { + for (IntegerValue divisor(1); divisor <= max_divisor; ++divisor) { + for (IntegerValue rhs_remainder(1); rhs_remainder < divisor; + ++rhs_remainder) { + const std::string info = absl::StrCat( + " rhs_remainder = ", rhs_remainder.value(), + " divisor = ", divisor.value(), " max_t = ", max_t.value(), + " max_scaling = ", max_scaling.value()); + const auto f = GetSuperAdditiveRoundingFunction( + rhs_remainder, divisor, + std::min(max_t, + GetFactorT(rhs_remainder, divisor, IntegerValue(100))), + max_scaling); + ASSERT_EQ(f(IntegerValue(0)), 0) << info; + ASSERT_GE(f(divisor), 1) << info; + ASSERT_LE(f(divisor), max_scaling * max_t) << info; + for (IntegerValue a(0); a < divisor; ++a) { + IntegerValue min_diff = kMaxIntegerValue; + for (IntegerValue b(1); b < divisor; ++b) { + min_diff = std::min(min_diff, f(a + b) - f(a) - f(b)); + ASSERT_GE(min_diff, 0) + << info << ", f(" << a << ")=" << f(a) << " + f(" << b + << ")=" << f(b) << " <= f(" << a + b << ")=" << f(a + b); + } + + // TODO(user): Our discretized "mir" function is not always + // maximal. Try to fix it? + if (a <= rhs_remainder || max_scaling != 2) continue; + if (rhs_remainder * max_t < divisor / 2) continue; + + // min_diff > 0 shows that our function is dominated (i.e. not + // maximal) since f(a) could be increased by 1/2. + ASSERT_EQ(min_diff, 0) + << "Not maximal at " << info << " f(" << a << ") = " << f(a) + << " min_diff:" << min_diff; + } + } + } + } + } +} + +TEST(GetSuperAdditiveStrengtheningFunction, AllSmallValues) { + for (const int64_t rhs : {13, 14}) { // test odd/even + for (int64_t min_magnitude = 1; min_magnitude <= rhs; ++min_magnitude) { + const auto f = GetSuperAdditiveStrengtheningFunction(rhs, min_magnitude); + + // Check super additivity in -[50, 50] + for (int a = -50; a <= 50; ++a) { + for (int b = -50; b <= 50; ++b) { + ASSERT_LE(f(a) + f(b), f(a + b)) + << " a=" << a << " b=" << b << " min=" << min_magnitude + << " rhs=" << rhs; + } + } + } + } +} + +TEST(GetSuperAdditiveStrengtheningMirFunction, AllSmallValues) { + for (const int64_t rhs : {13, 14}) { // test odd/even + for (int64_t scaling = 1; scaling <= rhs; ++scaling) { + const auto f = GetSuperAdditiveStrengtheningMirFunction(rhs, scaling); + + // Check super additivity in -[50, 50] + for (int a = -50; a <= 50; ++a) { + for (int b = -50; b <= 50; ++b) { + ASSERT_LE(f(a) + f(b), f(a + b)) + << " a=" << a << " b=" << b << " scaling=" << scaling + << " rhs=" << rhs; + } + } + } + } +} + +TEST(CutDataTest, ComputeViolation) { + CutData cut; + cut.rhs = 2; + cut.terms.push_back({.lp_value = 1.2, .coeff = 1}); + cut.terms.push_back({.lp_value = 0.5, .coeff = 2}); + EXPECT_COMPARABLE(cut.ComputeViolation(), 0.2, 1e-10); +} + +template +std::string GetCutString(const Helper& helper) { + LinearConstraint ct; + CutDataBuilder builder; + EXPECT_TRUE(builder.ConvertToLinearConstraint(helper.cut(), &ct)); + return ct.DebugString(); +} + +TEST(CoverCutHelperTest, SimpleExample) { + // 6x0 + 4x1 + 10x2 <= 9. + std::vector vars = {IntegerVariable(0), IntegerVariable(2), + IntegerVariable(4)}; + std::vector coeffs = IntegerValueVector({6, 4, 10}); + std::vector lbs = IntegerValueVector({0, 0, 0}); + std::vector lp_values{1.0, 0.5, 0.1}; // Tight. + + // Note(user): the ub of the last variable is not used. But the first two + // are even though only the second one is required for the validity of the + // cut. + std::vector ubs = IntegerValueVector({1, 1, 10}); + + CutData data; + data.FillFromParallelVectors(IntegerValue(9), vars, coeffs, lp_values, lbs, + ubs); + data.ComplementForPositiveCoefficients(); + CoverCutHelper helper; + EXPECT_TRUE(helper.TrySimpleKnapsack(data)); + EXPECT_EQ(GetCutString(helper), "1*X0 1*X1 1*X2 <= 1"); + EXPECT_EQ(helper.Info(), "lift=1"); +} + +// I tried to reproduce bug 169094958, but if the base constraint is tight, +// the bug was triggered only due to numerical imprecision. A simple way to +// trigger it is like with this test if the given LP value just violate the +// initial constraint. +TEST(CoverCutHelperTest, WeirdExampleWithViolatedConstraint) { + // x0 + x1 <= 9. + std::vector vars = {IntegerVariable(0), IntegerVariable(2)}; + std::vector coeffs = IntegerValueVector({1, 1}); + std::vector lbs = IntegerValueVector({ + 0, + 0, + }); + std::vector ubs = IntegerValueVector({10, 13}); + std::vector lp_values{0.0, 12.6}; // violated. + + CutData data; + data.FillFromParallelVectors(IntegerValue(9), vars, coeffs, lp_values, lbs, + ubs); + data.ComplementForPositiveCoefficients(); + CoverCutHelper helper; + EXPECT_TRUE(helper.TrySimpleKnapsack(data)); + EXPECT_EQ(GetCutString(helper), "1*X0 1*X1 <= 9"); + EXPECT_EQ(helper.Info(), "lift=1"); +} + +TEST(CoverCutHelperTest, LetchfordSouliLifting) { + const int n = 10; + const IntegerValue rhs = IntegerValue(16); + std::vector vars; + std::vector coeffs = + IntegerValueVector({5, 5, 5, 5, 15, 13, 9, 8, 8, 8}); + for (int i = 0; i < n; ++i) { + vars.push_back(IntegerVariable(2 * i)); + } + std::vector lbs(n, IntegerValue(0)); + std::vector ubs(n, IntegerValue(1)); + std::vector lps(n, 0.0); + for (int i = 0; i < 4; ++i) { + lps[i] = 0.9; + } + + CutData data; + data.FillFromParallelVectors(rhs, vars, coeffs, lps, lbs, ubs); + data.ComplementForPositiveCoefficients(); + + CoverCutHelper helper; + EXPECT_TRUE(helper.TryWithLetchfordSouliLifting(data)); + EXPECT_EQ(GetCutString(helper), + "1*X0 1*X1 1*X2 1*X3 3*X4 3*X5 2*X6 1*X7 1*X8 1*X9 <= 3"); + + // For now, we only support Booleans in the cover. + // Note that we don't care for variable not in the cover though. + data.terms[3].bound_diff = IntegerValue(2); + EXPECT_FALSE(helper.TryWithLetchfordSouliLifting(data)); +} + +LinearConstraint IntegerRoundingCutWithBoundsFromTrail( + const RoundingOptions& options, IntegerValue rhs, + absl::Span vars, + absl::Span coeffs, absl::Span lp_values, + const Model& model) { + std::vector lbs; + std::vector ubs; + auto* integer_trail = model.Get(); + for (int i = 0; i < vars.size(); ++i) { + lbs.push_back(integer_trail->LowerBound(vars[i])); + ubs.push_back(integer_trail->UpperBound(vars[i])); + } + + CutData data; + data.FillFromParallelVectors(rhs, vars, coeffs, lp_values, lbs, ubs); + data.ComplementForSmallerLpValues(); + + IntegerRoundingCutHelper helper; + EXPECT_TRUE(helper.ComputeCut(options, data, nullptr)); + + CutDataBuilder builder; + LinearConstraint constraint; + EXPECT_TRUE(builder.ConvertToLinearConstraint(helper.cut(), &constraint)); + return constraint; +} + +TEST(IntegerRoundingCutTest, LetchfordLodiExample1) { + Model model; + const IntegerVariable x0 = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable x1 = model.Add(NewIntegerVariable(0, 10)); + + // 6x0 + 4x1 <= 9. + const IntegerValue rhs = IntegerValue(9); + std::vector vars = {x0, x1}; + std::vector coeffs = {IntegerValue(6), IntegerValue(4)}; + + std::vector lp_values{1.5, 0.0}; + RoundingOptions options; + options.max_scaling = 2; + LinearConstraint constraint = IntegerRoundingCutWithBoundsFromTrail( + options, rhs, vars, coeffs, lp_values, model); + EXPECT_EQ(constraint.DebugString(), "2*X0 1*X1 <= 2"); +} + +TEST(IntegerRoundingCutTest, LetchfordLodiExample1Modified) { + Model model; + const IntegerVariable x0 = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable x1 = model.Add(NewIntegerVariable(0, 1)); + + // 6x0 + 4x1 <= 9. + const IntegerValue rhs = IntegerValue(9); + + std::vector vars = {x0, x1}; + std::vector coeffs = {IntegerValue(6), IntegerValue(4)}; + + // x1 is at its upper bound here. + std::vector lp_values{5.0 / 6.0, 1.0}; + + // Note that the cut is only valid because the bound of x1 is one here. + LinearConstraint constraint = IntegerRoundingCutWithBoundsFromTrail( + RoundingOptions(), rhs, vars, coeffs, lp_values, model); + EXPECT_EQ(constraint.DebugString(), "1*X0 1*X1 <= 1"); +} + +TEST(IntegerRoundingCutTest, LetchfordLodiExample2) { + Model model; + const IntegerVariable x0 = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable x1 = model.Add(NewIntegerVariable(0, 10)); + + // 6x0 + 4x1 <= 9. + const IntegerValue rhs = IntegerValue(9); + std::vector vars = {x0, x1}; + std::vector coeffs = {IntegerValue(6), IntegerValue(4)}; + + std::vector lp_values{0.0, 2.25}; + LinearConstraint constraint = IntegerRoundingCutWithBoundsFromTrail( + RoundingOptions(), rhs, vars, coeffs, lp_values, model); + EXPECT_EQ(constraint.DebugString(), "3*X0 2*X1 <= 4"); +} + +TEST(IntegerRoundingCutTest, LetchfordLodiExample2WithNegatedCoeff) { + Model model; + const IntegerVariable x0 = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable x1 = model.Add(NewIntegerVariable(-3, 0)); + + // 6x0 - 4x1 <= 9. + const IntegerValue rhs = IntegerValue(9); + std::vector vars = {x0, x1}; + std::vector coeffs = {IntegerValue(6), IntegerValue(-4)}; + + std::vector lp_values{0.0, -2.25}; + LinearConstraint constraint = IntegerRoundingCutWithBoundsFromTrail( + RoundingOptions(), rhs, vars, coeffs, lp_values, model); + + // We actually do not return like in the example "3*X0 -2*X1 <= 4" + // But the simpler X0 - X1 <= 2 which has the same violation (0.25) but a + // better norm. + EXPECT_EQ(constraint.DebugString(), "1*X0 -1*X1 <= 2"); +} + +// This used to trigger a failure with a wrong implied bound code path. +TEST(IntegerRoundingCutTest, TestCaseUsedForDebugging) { + Model model; + // Variable values are in comment. + const IntegerVariable x0 = model.Add(NewIntegerVariable(0, 3)); // 1 + const IntegerVariable x1 = model.Add(NewIntegerVariable(0, 4)); // 0 + const IntegerVariable x2 = model.Add(NewIntegerVariable(0, 2)); // 1 + const IntegerVariable x3 = model.Add(NewIntegerVariable(0, 1)); // 0 + const IntegerVariable x4 = model.Add(NewIntegerVariable(0, 3)); // 1 + + // The constraint is tight with value above (-5 - 4 + 7 == -2). + const IntegerValue rhs = IntegerValue(-2); + std::vector vars = {x0, x1, x2, x3, x4}; + std::vector coeffs = IntegerValueVector({-5, -1, -4, -7, 7}); + + // The constraint is tight under LP (-5 * 0.4 == -2). + std::vector lp_values{0.4, 0.0, -1e-16, 0.0, 0.0}; + LinearConstraint constraint = IntegerRoundingCutWithBoundsFromTrail( + RoundingOptions(), rhs, vars, coeffs, lp_values, model); + + EXPECT_EQ(constraint.DebugString(), "-2*X0 -1*X1 -2*X2 -2*X3 2*X4 <= -2"); +} + +// The algo should find a "divisor" 2 when it lead to a good cut. +// +// TODO(user): Double check that such divisor will always be found? Of course, +// if the initial constraint coefficient are too high, then it will not, but +// that is okay since such cut efficacity will be bad anyway. +TEST(IntegerRoundingCutTest, ZeroHalfCut) { + Model model; + const IntegerVariable x0 = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable x1 = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable x2 = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable x3 = model.Add(NewIntegerVariable(0, 10)); + + // 6x0 + 4x1 + 8x2 + 7x3 <= 9. + const IntegerValue rhs = IntegerValue(9); + std::vector vars = {x0, x1, x2, x3}; + std::vector coeffs = {IntegerValue(6), IntegerValue(4), + IntegerValue(8), IntegerValue(7)}; + + std::vector lp_values{0.25, 1.25, 0.3125, 0.0}; + LinearConstraint constraint = IntegerRoundingCutWithBoundsFromTrail( + RoundingOptions(), rhs, vars, coeffs, lp_values, model); + EXPECT_EQ(constraint.DebugString(), "3*X0 2*X1 4*X2 3*X3 <= 4"); +} + +TEST(IntegerRoundingCutTest, LargeCoeffWithSmallImprecision) { + Model model; + const IntegerVariable x0 = model.Add(NewIntegerVariable(0, 5)); + const IntegerVariable x1 = model.Add(NewIntegerVariable(0, 5)); + + // 1e6 x0 - x1 <= 1.5e6. + const IntegerValue rhs = IntegerValue(1.5e6); + std::vector vars = {x0, x1}; + std::vector coeffs = {IntegerValue(1e6), IntegerValue(-1)}; + + // Note thate without adjustement, this returns 2 * X0 - X1 <= 2. + // TODO(user): expose parameters so this can be verified other than manually? + std::vector lp_values{1.5, 0.1}; + LinearConstraint constraint = IntegerRoundingCutWithBoundsFromTrail( + RoundingOptions(), rhs, vars, coeffs, lp_values, model); + EXPECT_EQ(constraint.DebugString(), "1*X0 <= 1"); +} + +TEST(IntegerRoundingCutTest, LargeCoeffWithSmallImprecision2) { + Model model; + const IntegerVariable x0 = model.Add(NewIntegerVariable(0, 5)); + const IntegerVariable x1 = model.Add(NewIntegerVariable(0, 5)); + + // 1e6 x0 + 999999 * x1 <= 1.5e6. + const IntegerValue rhs = IntegerValue(1.5e6); + std::vector vars = {x0, x1}; + std::vector coeffs = {IntegerValue(1e6), IntegerValue(999999)}; + + // Note thate without adjustement, this returns 2 * X0 + X1 <= 2. + // TODO(user): expose parameters so this can be verified other than manually? + std::vector lp_values{1.49, 0.1}; + LinearConstraint constraint = IntegerRoundingCutWithBoundsFromTrail( + RoundingOptions(), rhs, vars, coeffs, lp_values, model); + EXPECT_EQ(constraint.DebugString(), "1*X0 1*X1 <= 1"); +} + +TEST(IntegerRoundingCutTest, MirOnLargerConstraint) { + Model model; + std::vector vars(10); + for (int i = 0; i < 10; ++i) { + vars[i] = model.Add(NewIntegerVariable(0, 5)); + } + + // sum (i + 1) x_i <= 16. + const IntegerValue rhs = IntegerValue(16); + std::vector coeffs; + for (int i = 0; i < vars.size(); ++i) { + coeffs.push_back(IntegerValue(i + 1)); + } + + std::vector lp_values(vars.size(), 0.0); + lp_values[9] = 1.6; // 10 * 1.6 == 16 + + RoundingOptions options; + options.max_scaling = 4; + LinearConstraint constraint = IntegerRoundingCutWithBoundsFromTrail( + options, rhs, vars, coeffs, lp_values, model); + EXPECT_EQ(constraint.DebugString(), "1*X6 2*X7 3*X8 4*X9 <= 4"); +} + +TEST(IntegerRoundingCutTest, MirOnLargerConstraint2) { + Model model; + std::vector vars(10); + for (int i = 0; i < 10; ++i) vars[i] = model.Add(NewIntegerVariable(0, 5)); + + // sum (i + 1) x_i <= 16. + const IntegerValue rhs = IntegerValue(16); + std::vector coeffs; + for (int i = 0; i < vars.size(); ++i) { + coeffs.push_back(IntegerValue(i + 1)); + } + + std::vector lp_values(vars.size(), 0.0); + lp_values[4] = 5.5 / 5.0; + lp_values[9] = 1.05; + + RoundingOptions options; + options.max_scaling = 4; + LinearConstraint constraint = IntegerRoundingCutWithBoundsFromTrail( + options, rhs, vars, coeffs, lp_values, model); + EXPECT_EQ(constraint.DebugString(), + "2*X1 3*X2 4*X3 6*X4 6*X5 8*X6 9*X7 10*X8 12*X9 <= 18"); +} + +std::vector ToIntegerValues(const std::vector input) { + std::vector output; + for (const int64_t v : input) output.push_back(IntegerValue(v)); + return output; +} + +std::vector ToIntegerVariables( + const std::vector input) { + std::vector output; + for (const int64_t v : input) output.push_back(IntegerVariable(v)); + return output; +} + +// This used to fail as I was coding the CL when I was trying to force t==1 +// in the GetSuperAdditiveRoundingFunction() code. +TEST(IntegerRoundingCutTest, RegressionTest) { + RoundingOptions options; + options.max_scaling = 4; + + const IntegerValue rhs = int64_t{7469520585651099083}; + std::vector vars = ToIntegerVariables( + {0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, + 28, 30, 32, 34, 36, 38, 42, 44, 46, 48, 50, 52, 54, 56}); + std::vector coeffs = ToIntegerValues( + {22242929208935956LL, 128795791007031270LL, 64522773588815932LL, + 106805487542181976LL, 136903984044996548LL, 177476314670499137LL, + 364043443034395LL, 28002509947960647LL, 310965596097558939LL, + 103949088324014599LL, 41400520193055115LL, 50111468002532494LL, + 53821870865384327LL, 68690238549704032LL, 75189534851923882LL, + 136250652059774801LL, 169776580612315087LL, 172493907306536826LL, + 13772608007357656LL, 74052819842959090LL, 134400722410234077LL, + 5625133860678171LL, 299572729577293761LL, 81099235700461109LL, + 178989907222373586LL, 16642124499479353LL, 110378717916671350LL, + 41703587448036910LL}); + std::vector lp_values = { + 0, 0, 2.51046, 0.0741114, 0.380072, 5.17238, 0, + 0, 13.2214, 0, 0.635977, 0, 0, 3.39859, + 1.15936, 0.165207, 2.29673, 2.19505, 0, 0, 2.31191, + 0, 0.785149, 0.258119, 2.26978, 0, 0.970046, 0}; + std::vector lbs(28, IntegerValue(0)); + std::vector ubs(28, IntegerValue(99)); + ubs[8] = 17; + std::vector solution = + ToIntegerValues({0, 3, 0, 2, 2, 2, 0, 1, 5, 1, 1, 1, 1, 2, + 0, 2, 1, 3, 1, 1, 4, 1, 6, 2, 3, 0, 1, 1}); + + EXPECT_EQ(coeffs.size(), vars.size()); + EXPECT_EQ(lp_values.size(), vars.size()); + EXPECT_EQ(lbs.size(), vars.size()); + EXPECT_EQ(ubs.size(), vars.size()); + EXPECT_EQ(solution.size(), vars.size()); + + // The solution is a valid integer solution of the inequality. + { + IntegerValue activity(0); + for (int i = 0; i < vars.size(); ++i) { + activity += solution[i] * coeffs[i]; + } + EXPECT_LE(activity, rhs); + } + + CutData data; + data.FillFromParallelVectors(rhs, vars, coeffs, lp_values, lbs, ubs); + IntegerRoundingCutHelper helper; + + // TODO(user): Actually this fail, so we don't compute a cut here. + EXPECT_FALSE(helper.ComputeCut(options, data, nullptr)); +} + +void InitializeLpValues(absl::Span values, Model* model) { + auto* lp_values = model->GetOrCreate(); + lp_values->resize(2 * values.size()); + for (int i = 0; i < values.size(); ++i) { + (*lp_values)[IntegerVariable(2 * i)] = values[i]; + (*lp_values)[IntegerVariable(2 * i + 1)] = -values[i]; + } +} + +TEST(SquareCutGeneratorTest, TestBelowCut) { + Model model; + IntegerVariable x = model.Add(NewIntegerVariable(0, 5)); + IntegerVariable y = model.Add(NewIntegerVariable(0, 25)); + InitializeLpValues({2.0, 12.0}, &model); + + CutGenerator square = CreateSquareCutGenerator(y, x, 1, &model); + auto* manager = model.GetOrCreate(); + square.generate_cuts(manager); + EXPECT_EQ(1, manager->num_cuts()); + EXPECT_THAT(manager->AllConstraints().front().constraint.DebugString(), + EndsWith("-5*X0 1*X1 <= 0")); +} + +TEST(SquareCutGeneratorTest, TestBelowCutWithOffset) { + Model model; + IntegerVariable x = model.Add(NewIntegerVariable(1, 5)); + IntegerVariable y = model.Add(NewIntegerVariable(1, 25)); + InitializeLpValues({2.0, 12.0}, &model); + + CutGenerator square = CreateSquareCutGenerator(y, x, 1, &model); + auto* manager = model.GetOrCreate(); + square.generate_cuts(manager); + ASSERT_EQ(1, manager->num_cuts()); + EXPECT_THAT(manager->AllConstraints().front().constraint.DebugString(), + EndsWith("-6*X0 1*X1 <= -5")); +} + +TEST(SquareCutGeneratorTest, TestNoBelowCut) { + Model model; + IntegerVariable x = model.Add(NewIntegerVariable(1, 5)); + IntegerVariable y = model.Add(NewIntegerVariable(1, 25)); + InitializeLpValues({2.0, 6.0}, &model); + + CutGenerator square = CreateSquareCutGenerator(y, x, 1, &model); + auto* manager = model.GetOrCreate(); + square.generate_cuts(manager); + ASSERT_EQ(0, manager->num_cuts()); +} + +TEST(SquareCutGeneratorTest, TestAboveCut) { + Model model; + IntegerVariable x = model.Add(NewIntegerVariable(1, 5)); + IntegerVariable y = model.Add(NewIntegerVariable(1, 25)); + InitializeLpValues({2.5, 6.25}, &model); + + CutGenerator square = CreateSquareCutGenerator(y, x, 1, &model); + auto* manager = model.GetOrCreate(); + square.generate_cuts(manager); + ASSERT_EQ(1, manager->num_cuts()); + EXPECT_THAT(manager->AllConstraints().front().constraint.DebugString(), + StartsWith("-6 <= -5*X0 1*X1")); +} + +TEST(SquareCutGeneratorTest, TestNearlyAboveCut) { + Model model; + IntegerVariable x = model.Add(NewIntegerVariable(1, 5)); + IntegerVariable y = model.Add(NewIntegerVariable(1, 25)); + InitializeLpValues({2.4, 5.99999}, &model); + + CutGenerator square = CreateSquareCutGenerator(y, x, 1, &model); + auto* manager = model.GetOrCreate(); + square.generate_cuts(manager); + ASSERT_EQ(0, manager->num_cuts()); +} + +TEST(MultiplicationCutGeneratorTest, TestCut1) { + Model model; + IntegerVariable x = model.Add(NewIntegerVariable(1, 5)); + IntegerVariable y = model.Add(NewIntegerVariable(2, 3)); + IntegerVariable z = model.Add(NewIntegerVariable(1, 15)); + InitializeLpValues({1.2, 2.1, 2.1}, &model); + + CutGenerator mult = + CreatePositiveMultiplicationCutGenerator(z, x, y, 1, &model); + auto* manager = model.GetOrCreate(); + mult.generate_cuts(manager); + ASSERT_EQ(1, manager->num_cuts()); + EXPECT_THAT(manager->AllConstraints().front().constraint.DebugString(), + EndsWith("2*X0 1*X1 -1*X2 <= 2")); +} + +TEST(MultiplicationCutGeneratorTest, TestCut2) { + Model model; + IntegerVariable x = model.Add(NewIntegerVariable(1, 5)); + IntegerVariable y = model.Add(NewIntegerVariable(2, 3)); + IntegerVariable z = model.Add(NewIntegerVariable(1, 15)); + InitializeLpValues({4.9, 2.8, 12.0}, &model); + + CutGenerator mult = + CreatePositiveMultiplicationCutGenerator(z, x, y, 1, &model); + auto* manager = model.GetOrCreate(); + mult.generate_cuts(manager); + ASSERT_EQ(1, manager->num_cuts()); + EXPECT_THAT(manager->AllConstraints().front().constraint.DebugString(), + EndsWith("3*X0 5*X1 -1*X2 <= 15")); +} + +TEST(MultiplicationCutGeneratorTest, TestCut3) { + Model model; + IntegerVariable x = model.Add(NewIntegerVariable(1, 5)); + IntegerVariable y = model.Add(NewIntegerVariable(2, 3)); + IntegerVariable z = model.Add(NewIntegerVariable(1, 15)); + InitializeLpValues({1.2, 2.1, 4.4}, &model); + + CutGenerator mult = + CreatePositiveMultiplicationCutGenerator(z, x, y, 1, &model); + auto* manager = model.GetOrCreate(); + mult.generate_cuts(manager); + ASSERT_EQ(2, manager->num_cuts()); + EXPECT_THAT(manager->AllConstraints().front().constraint.DebugString(), + StartsWith("3 <= 3*X0 1*X1 -1*X2")); + EXPECT_THAT(manager->AllConstraints().back().constraint.DebugString(), + StartsWith("10 <= 2*X0 5*X1 -1*X2")); +} + +TEST(MultiplicationCutGeneratorTest, TestNoCut1) { + Model model; + IntegerVariable x = model.Add(NewIntegerVariable(1, 50)); + IntegerVariable y = model.Add(NewIntegerVariable(2, 30)); + IntegerVariable z = model.Add(NewIntegerVariable(1, 1500)); + InitializeLpValues({40.0, 20.0, 799.0}, &model); + + CutGenerator mult = + CreatePositiveMultiplicationCutGenerator(z, x, y, 1, &model); + auto* manager = model.GetOrCreate(); + mult.generate_cuts(manager); + ASSERT_EQ(0, manager->num_cuts()); +} + +TEST(MultiplicationCutGeneratorTest, TestNoCut2) { + Model model; + IntegerVariable x = model.Add(NewIntegerVariable(1, 50)); + IntegerVariable y = model.Add(NewIntegerVariable(2, 30)); + IntegerVariable z = model.Add(NewIntegerVariable(1, 1500)); + InitializeLpValues({40.0, 20.0, 801.0}, &model); + + CutGenerator mult = + CreatePositiveMultiplicationCutGenerator(z, x, y, 1, &model); + auto* manager = model.GetOrCreate(); + mult.generate_cuts(manager); + ASSERT_EQ(0, manager->num_cuts()); +} + +TEST(AllDiffCutGeneratorTest, TestCut) { + Model model; + Domain domain(10); + domain = domain.UnionWith(Domain(15)); + domain = domain.UnionWith(Domain(25)); + IntegerVariable x = model.Add(NewIntegerVariable(domain)); + IntegerVariable y = model.Add(NewIntegerVariable(domain)); + IntegerVariable z = model.Add(NewIntegerVariable(domain)); + InitializeLpValues({15.0, 15.0, 15.0}, &model); + + CutGenerator all_diff = CreateAllDifferentCutGenerator({x, y, z}, &model); + auto* manager = model.GetOrCreate(); + all_diff.generate_cuts(manager); + ASSERT_EQ(1, manager->num_cuts()); + EXPECT_EQ(manager->AllConstraints().front().constraint.DebugString(), + "50 <= 1*X0 1*X1 1*X2 <= 50"); +} + +TEST(AllDiffCutGeneratorTest, TestCut2) { + Model model; + Domain domain(10); + domain = domain.UnionWith(Domain(15)); + domain = domain.UnionWith(Domain(25)); + IntegerVariable x = model.Add(NewIntegerVariable(domain)); + IntegerVariable y = model.Add(NewIntegerVariable(domain)); + IntegerVariable z = model.Add(NewIntegerVariable(domain)); + InitializeLpValues({13.0, 10.0, 12.0}, &model); + + CutGenerator all_diff = CreateAllDifferentCutGenerator({x, y, z}, &model); + auto* manager = model.GetOrCreate(); + all_diff.generate_cuts(manager); + ASSERT_EQ(2, manager->num_cuts()); + EXPECT_EQ(manager->AllConstraints().front().constraint.DebugString(), + "25 <= 1*X1 1*X2 <= 40"); + EXPECT_EQ(manager->AllConstraints().back().constraint.DebugString(), + "50 <= 1*X0 1*X1 1*X2 <= 50"); +} + +// We model the maximum of 3 affine functions: +// f0(x) = 1 +// f1(x) = -x0 - 2x1 +// f2(x) = -x0 + x1 +// over the box domain -1 <= x0, x1 <= 1. For this data, there are 9 possible +// maximum corner cuts. I denote each by noting which function f^i each input +// variable x_j gets assigned: +// (1) x0 -> f0, x1 -> f0: y <= 0x0 + 0x1 + 1z_0 + 3z_1 + 2z_2 +// (2) x0 -> f0, x1 -> f1: y <= 0x0 - 2x1 + 3z_0 + 1z_1 + 4z_2 +// (3) x0 -> f0, x1 -> f2: y <= 0x0 + x1 + 2z_0 + 4z_1 + 1z_2 +// (4) x0 -> f1, x1 -> f0: y <= -x0 + 0x1 + 2z_0 + 2z_1 + 1z_2 +// (5) x0 -> f1, x1 -> f1: y <= -x0 - 2x1 + 4z_0 + 0z_1 + 3z_2 +// (6) x0 -> f1, x1 -> f2: y <= -x0 + x1 + 3z_0 + 3z_1 + 0z_2 +// (7) x0 -> f2, x1 -> f0: y <= -x0 + 0x1 + 2z_0 + 2z_1 + 1z_2 +// (8) x0 -> f2, x1 -> f1: y <= -x0 - 2x1 + 4z_0 + 0z_1 + 3z_2 +// (9) x0 -> f2, x1 -> f2: y <= -x0 + x1 + 3z_0 + 3z_1 + 0z_2 +TEST(LinMaxCutsTest, BasicCuts1) { + Model model; + IntegerVariable x0 = model.Add(NewIntegerVariable(-1, 1)); + IntegerVariable x1 = model.Add(NewIntegerVariable(-1, 1)); + IntegerVariable target = model.Add(NewIntegerVariable(-100, 100)); + LinearExpression f0; + f0.offset = IntegerValue(1); + LinearExpression f1; + f1.vars = {x0, x1}; + f1.coeffs = {IntegerValue(-1), IntegerValue(-2)}; + LinearExpression f2; + f2.vars = {x0, x1}; + f2.coeffs = {IntegerValue(-1), IntegerValue(1)}; + + std::vector exprs = {f0, f1, f2}; + std::vector z_vars; + for (int i = 0; i < exprs.size(); ++i) { + IntegerVariable z = model.Add(NewIntegerVariable(0, 1)); + z_vars.push_back(z); + } + + CutGenerator max_cuts = + CreateLinMaxCutGenerator(target, exprs, z_vars, &model); + + auto* manager = model.GetOrCreate(); + InitializeLpValues({-1.0, 1.0, 2.0, 1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0}, &model); + + max_cuts.generate_cuts(manager); + ASSERT_EQ(1, manager->num_cuts()); + + // x vars are X0,X1 respectively, target is X2, z_vars are X3,X4,X5 + // respectively. + // Most violated inequality is 2. + EXPECT_THAT(manager->AllConstraints().front().constraint.DebugString(), + StartsWith("0 <= -2*X1 -1*X2 3*X3 1*X4 4*X5")); + + InitializeLpValues({-1.0, -1.0, 2.0, 1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0}, + &model); + max_cuts.generate_cuts(manager); + ASSERT_EQ(2, manager->num_cuts()); + // Most violated inequality is 3. + EXPECT_THAT(manager->AllConstraints().back().constraint.DebugString(), + StartsWith("0 <= 1*X1 -1*X2 2*X3 4*X4 1*X5")); +} + +// We model the maximum of 3 affine functions: +// f0(x) = 1 +// f1(x) = x +// f2(x) = -x +// target = max(f0, f1, f2) +// x in [-10, 10] +TEST(LinMaxCutsTest, AffineCuts1) { + Model model; + const IntegerValue zero(0); + const IntegerValue one(1); + IntegerVariable x = model.Add(NewIntegerVariable(-10, 10)); + IntegerVariable target = model.Add(NewIntegerVariable(1, 100)); + LinearExpression target_expr; + target_expr.vars.push_back(target); + target_expr.coeffs.push_back(one); + + std::vector> affines = { + {zero, one}, {one, zero}, {-one, zero}}; + + LinearConstraintBuilder builder(&model); + ASSERT_TRUE( + BuildMaxAffineUpConstraint(target_expr, x, affines, &model, &builder)); + + // Note, the cut is not normalized. + EXPECT_EQ(builder.Build().DebugString(), "20*X1 <= 200"); +} + +// We model the maximum of 3 affine functions: +// f0(x) = 1 +// f1(x) = x +// f2(x) = -x +// target = max(f0, f1, f2) +// x in [-1, 10] +TEST(LinMaxCutsTest, AffineCuts2) { + Model model; + const IntegerValue zero(0); + const IntegerValue one(1); + IntegerVariable x = model.Add(NewIntegerVariable(-1, 10)); + IntegerVariable target = model.Add(NewIntegerVariable(1, 100)); + LinearExpression target_expr; + target_expr.vars.push_back(target); + target_expr.coeffs.push_back(one); + + std::vector> affines = { + {zero, one}, {one, zero}, {-one, zero}}; + + LinearConstraintBuilder builder(&model); + ASSERT_TRUE( + BuildMaxAffineUpConstraint(target_expr, x, affines, &model, &builder)); + + EXPECT_EQ(builder.Build().DebugString(), "-9*X0 11*X1 <= 20"); +} + +// We model the maximum of 3 affine functions: +// f0(x) = 1 +// f1(x) = x +// f2(x) = -x +// target = max(f0, f1, f2) +// x fixed +TEST(LinMaxCutsTest, AffineCutsFixedVar) { + Model model; + const IntegerValue zero(0); + const IntegerValue one(1); + IntegerVariable x = model.Add(NewIntegerVariable(2, 2)); + IntegerVariable target = model.Add(NewIntegerVariable(0, 100)); + LinearExpression target_expr; + target_expr.vars.push_back(target); + target_expr.coeffs.push_back(one); + + std::vector> affines = { + {zero, one}, {one, zero}, {-one, zero}}; + + CutGenerator max_cuts = + CreateMaxAffineCutGenerator(target_expr, x, affines, "test", &model); + + auto* manager = model.GetOrCreate(); + InitializeLpValues({2.0, 8.0}, &model); + max_cuts.generate_cuts(manager); + EXPECT_EQ(0, manager->num_cuts()); +} + +TEST(ImpliedBoundsProcessorTest, PositiveBasicTest) { + Model model; + model.GetOrCreate()->set_use_implied_bounds(true); + + const BooleanVariable b = model.Add(NewBooleanVariable()); + const IntegerVariable b_view = model.Add(NewIntegerVariable(0, 1)); + const IntegerVariable x = model.Add(NewIntegerVariable(2, 9)); + + auto* integer_encoder = model.GetOrCreate(); + auto* integer_trail = model.GetOrCreate(); + auto* implied_bounds = model.GetOrCreate(); + + integer_encoder->AssociateToIntegerEqualValue(Literal(b, true), b_view, + IntegerValue(1)); + implied_bounds->Add(Literal(b, true), + IntegerLiteral::GreaterOrEqual(x, IntegerValue(5))); + + // Lp solution. + ImpliedBoundsProcessor processor({x, b_view}, integer_trail, implied_bounds); + + util_intops::StrongVector lp_values(1000); + lp_values[x] = 4.0; + lp_values[b_view] = 2.0 / 3.0; // 2.0 + b_view_value * (5-2) == 4.0 + processor.RecomputeCacheAndSeparateSomeImpliedBoundCuts(lp_values); + + // Lets look at the term X. + CutData data; + CutDataBuilder builder; + + CutTerm X; + X.coeff = 1; + X.lp_value = 2.0; + X.bound_diff = 7; + X.expr_vars[0] = x; + X.expr_coeffs[0] = 1; + X.expr_coeffs[1] = 0; + X.expr_offset = -2; + data.terms.push_back(X); + + processor.CacheDataForCut(IntegerVariable(100), &data); + EXPECT_TRUE(processor.TryToExpandWithLowerImpliedbound(IntegerValue(1), 0, + /*complement=*/false, + &data, &builder)); + EXPECT_EQ(data.terms.size(), 2); + EXPECT_THAT(data.terms[0].DebugString(), + ::testing::StartsWith("coeff=1 lp=0 range=7")); + EXPECT_THAT(data.terms[1].DebugString(), + ::testing::StartsWith("coeff=3 lp=0.666667 range=1")); + EXPECT_EQ(data.terms[1].expr_offset, 0); +} + +// Same as above but with b.Negated() +TEST(ImpliedBoundsProcessorTest, NegativeBasicTest) { + Model model; + model.GetOrCreate()->set_use_implied_bounds(true); + + const BooleanVariable b = model.Add(NewBooleanVariable()); + const IntegerVariable b_view = model.Add(NewIntegerVariable(0, 1)); + const IntegerVariable x = model.Add(NewIntegerVariable(2, 9)); + + auto* integer_encoder = model.GetOrCreate(); + auto* integer_trail = model.GetOrCreate(); + auto* implied_bounds = model.GetOrCreate(); + + integer_encoder->AssociateToIntegerEqualValue(Literal(b, true), b_view, + IntegerValue(1)); + implied_bounds->Add(Literal(b, false), // False here. + IntegerLiteral::GreaterOrEqual(x, IntegerValue(5))); + + // Lp solution. + ImpliedBoundsProcessor processor({x, b_view}, integer_trail, implied_bounds); + + util_intops::StrongVector lp_values(1000); + lp_values[x] = 4.0; + lp_values[b_view] = 1.0 - 2.0 / 3.0; // 1 - value above. + processor.RecomputeCacheAndSeparateSomeImpliedBoundCuts(lp_values); + + // Lets look at the term X. + CutData data; + CutDataBuilder builder; + + CutTerm X; + X.coeff = 1; + X.lp_value = 2.0; + X.bound_diff = 7; + X.expr_vars[0] = x; + X.expr_coeffs[0] = 1; + X.expr_coeffs[1] = 0; + X.expr_offset = -2; + data.terms.push_back(X); + + processor.CacheDataForCut(IntegerVariable(100), &data); + EXPECT_TRUE(processor.TryToExpandWithLowerImpliedbound(IntegerValue(1), 0, + /*complement=*/false, + &data, &builder)); + EXPECT_EQ(data.terms.size(), 2); + EXPECT_THAT(data.terms[0].DebugString(), + ::testing::StartsWith("coeff=1 lp=0 range=7")); + EXPECT_THAT(data.terms[1].DebugString(), + ::testing::StartsWith("coeff=3 lp=0.666667 range=1")); + + // This is the only change, we have 1 - bool there actually. + EXPECT_EQ(data.terms[1].expr_offset, 1); + EXPECT_EQ(data.terms[1].expr_coeffs[0], -1); + EXPECT_EQ(data.terms[1].expr_vars[0], b_view); +} + +TEST(ImpliedBoundsProcessorTest, DecompositionTest) { + Model model; + model.GetOrCreate()->set_use_implied_bounds(true); + + const BooleanVariable b = model.Add(NewBooleanVariable()); + const IntegerVariable b_view = model.Add(NewIntegerVariable(0, 1)); + const BooleanVariable c = model.Add(NewBooleanVariable()); + const IntegerVariable c_view = model.Add(NewIntegerVariable(0, 1)); + const IntegerVariable x = model.Add(NewIntegerVariable(2, 9)); + + auto* integer_encoder = model.GetOrCreate(); + auto* integer_trail = model.GetOrCreate(); + auto* implied_bounds = model.GetOrCreate(); + + integer_encoder->AssociateToIntegerEqualValue(Literal(b, true), b_view, + IntegerValue(1)); + integer_encoder->AssociateToIntegerEqualValue(Literal(c, true), c_view, + IntegerValue(1)); + implied_bounds->Add(Literal(b, true), + IntegerLiteral::GreaterOrEqual(x, IntegerValue(5))); + implied_bounds->Add(Literal(c, true), + IntegerLiteral::LowerOrEqual(x, IntegerValue(2))); + + // Lp solution. + ImpliedBoundsProcessor processor({x, b_view, c_view}, integer_trail, + implied_bounds); + + util_intops::StrongVector lp_values(1000); + lp_values[x] = 4.0; + lp_values[NegationOf(x)] = -4.0; + lp_values[b_view] = 2.0 / 3.0; // 2.0 + b_view_value * (5-2) == 4.0 + lp_values[c_view] = 0.5; + processor.RecomputeCacheAndSeparateSomeImpliedBoundCuts(lp_values); + + // Lets look at the term X. + CutTerm X; + X.coeff = 1; + X.lp_value = 2.0; + X.bound_diff = 7; + X.expr_vars[0] = x; + X.expr_coeffs[0] = 1; + X.expr_coeffs[1] = 0; + X.expr_offset = -2; + + CutData data; + data.terms.push_back(X); + processor.CacheDataForCut(IntegerVariable(100), &data); + X = data.terms[0]; + + // X - 2 = 3 * B + slack; + CutTerm bool_term; + CutTerm slack_term; + EXPECT_TRUE(processor.DecomposeWithImpliedLowerBound(X, IntegerValue(1), + bool_term, slack_term)); + EXPECT_THAT(bool_term.DebugString(), + ::testing::StartsWith("coeff=3 lp=0.666667 range=1")); + EXPECT_THAT(slack_term.DebugString(), + ::testing::StartsWith("coeff=1 lp=0 range=7")); + + // (9 - X) = 7 * C + slack; + CutTerm Y = X; + absl::int128 unused; + Y.Complement(&unused); + Y.coeff = -Y.coeff; + EXPECT_TRUE(processor.DecomposeWithImpliedLowerBound(Y, IntegerValue(1), + bool_term, slack_term)); + EXPECT_THAT(bool_term.DebugString(), + ::testing::StartsWith("coeff=7 lp=0.5 range=1")); + EXPECT_THAT(slack_term.DebugString(), + ::testing::StartsWith("coeff=1 lp=1.5 range=7")); + + // X - 2 = 7 * (1 - C) - slack; + EXPECT_TRUE(processor.DecomposeWithImpliedUpperBound(X, IntegerValue(1), + bool_term, slack_term)); + EXPECT_THAT(bool_term.DebugString(), + ::testing::StartsWith("coeff=7 lp=0.5 range=1")); + EXPECT_THAT(slack_term.DebugString(), + ::testing::StartsWith("coeff=-1 lp=1.5 range=7")); +} + +TEST(CutDataTest, SimpleExample) { + Model model; + const IntegerVariable x0 = model.Add(NewIntegerVariable(7, 10)); + const IntegerVariable x1 = model.Add(NewIntegerVariable(-3, 20)); + + // 6x0 - 4x1 <= 9. + const IntegerValue rhs = IntegerValue(9); + std::vector vars = {x0, x1}; + std::vector coeffs = {IntegerValue(6), IntegerValue(-4)}; + std::vector lp_values = {7.5, 4.5}; + + CutData cut; + std::vector lbs; + std::vector ubs; + auto* integer_trail = model.Get(); + for (int i = 0; i < vars.size(); ++i) { + lbs.push_back(integer_trail->LowerBound(vars[i])); + ubs.push_back(integer_trail->UpperBound(vars[i])); + } + cut.FillFromParallelVectors(rhs, vars, coeffs, lp_values, lbs, ubs); + cut.ComplementForSmallerLpValues(); + + // 6 (X0' + 7) - 4 (X1' - 3) <= 9 + ASSERT_EQ(cut.terms.size(), 2); + EXPECT_EQ(cut.rhs, 9 - 4 * 3 - 6 * 7); + EXPECT_EQ(cut.terms[0].coeff, 6); + EXPECT_EQ(cut.terms[0].lp_value, 0.5); + EXPECT_EQ(cut.terms[0].bound_diff, 3); + EXPECT_EQ(cut.terms[1].coeff, -4); + EXPECT_EQ(cut.terms[1].lp_value, 7.5); + EXPECT_EQ(cut.terms[1].bound_diff, 23); + + // Lets complement. + const absl::int128 old_rhs = cut.rhs; + cut.terms[0].Complement(&cut.rhs); + EXPECT_EQ(cut.rhs, old_rhs - 3 * 6); + EXPECT_EQ(cut.terms[0].coeff, -6); + EXPECT_EQ(cut.terms[0].lp_value, 3 - 0.5); + EXPECT_EQ(cut.terms[0].bound_diff, 3); + + // Encode back. + LinearConstraint new_constraint; + CutDataBuilder builder; + EXPECT_TRUE(builder.ConvertToLinearConstraint(cut, &new_constraint)); + + // We have a division by GCD in there. + const IntegerValue gcd = 2; + EXPECT_EQ(vars.size(), new_constraint.num_terms); + for (int i = 0; i < new_constraint.num_terms; ++i) { + EXPECT_EQ(vars[i], new_constraint.vars[i]); + EXPECT_EQ(coeffs[i] / gcd, new_constraint.coeffs[i]); + } +} + +TEST(SumOfAllDiffLowerBounderTest, ContinuousVariables) { + Model model; + IntegerTrail* integer_trail = model.GetOrCreate(); + IntegerVariable x1 = model.Add(NewIntegerVariable(1, 10)); + IntegerVariable x2 = model.Add(NewIntegerVariable(1, 10)); + IntegerVariable x3 = model.Add(NewIntegerVariable(1, 10)); + + SumOfAllDiffLowerBounder helper; + helper.Add(x1, 3, *integer_trail); + helper.Add(x2, 3, *integer_trail); + helper.Add(x3, 3, *integer_trail); + EXPECT_EQ(3, helper.size()); + EXPECT_EQ(6, helper.SumOfMinDomainValues()); + EXPECT_EQ(6, helper.SumOfDifferentMins()); + std::string suffix; + EXPECT_EQ(6, helper.GetBestLowerBound(suffix)); + EXPECT_EQ("e", suffix); + helper.Clear(); + EXPECT_EQ(0, helper.size()); +} + +TEST(SumOfAllDiffLowerBounderTest, DisjointVariables) { + Model model; + IntegerTrail* integer_trail = model.GetOrCreate(); + IntegerVariable x1 = model.Add(NewIntegerVariable(1, 10)); + IntegerVariable x2 = model.Add(NewIntegerVariable(1, 10)); + IntegerVariable x3 = model.Add(NewIntegerVariable(1, 10)); + + SumOfAllDiffLowerBounder helper; + helper.Add(x1, 3, *integer_trail); + helper.Add(x2, 3, *integer_trail); + helper.Add(AffineExpression(x3, 1, 10), 3, *integer_trail); + EXPECT_EQ(3, helper.size()); + EXPECT_EQ(6, helper.SumOfMinDomainValues()); + EXPECT_EQ(14, helper.SumOfDifferentMins()); + std::string suffix; + EXPECT_EQ(14, helper.GetBestLowerBound(suffix)); + EXPECT_EQ("a", suffix); +} + +TEST(SumOfAllDiffLowerBounderTest, DiscreteDomains) { + Model model; + IntegerTrail* integer_trail = model.GetOrCreate(); + IntegerVariable x1 = model.Add(NewIntegerVariable(1, 10)); + IntegerVariable x2 = model.Add(NewIntegerVariable(1, 10)); + IntegerVariable x3 = model.Add(NewIntegerVariable(1, 10)); + + SumOfAllDiffLowerBounder helper; + helper.Add(AffineExpression(x1, 3, 0), 3, *integer_trail); + helper.Add(AffineExpression(x2, 3, 0), 3, *integer_trail); + helper.Add(AffineExpression(x3, 3, 0), 3, *integer_trail); + EXPECT_EQ(3, helper.size()); + EXPECT_EQ(18, helper.SumOfMinDomainValues()); + EXPECT_EQ(12, helper.SumOfDifferentMins()); + std::string suffix; + EXPECT_EQ(18, helper.GetBestLowerBound(suffix)); + EXPECT_EQ("d", suffix); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/diffn_test.cc b/ortools/sat/diffn_test.cc new file mode 100644 index 0000000000..a46d78d29f --- /dev/null +++ b/ortools/sat/diffn_test.cc @@ -0,0 +1,176 @@ +// 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/diffn.h" + +#include + +#include + +#include "absl/strings/str_join.h" +#include "gtest/gtest.h" +#include "ortools/base/logging.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/integer_search.h" +#include "ortools/sat/intervals.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 { + +// Counts how many ways we can put two square of minimal size 1 in an n x n +// square. +// +// For n = 1, infeasible. +// For n = 2, should be 4 * 3. +// For n = 3: +// - 9 * 8 for two size 1. +// - 4 * 5 for size 2 + size 1. Times 2 for the permutation. +int CountAllTwoBoxesSolutions(int n) { + Model model; + std::vector x; + std::vector y; + for (int i = 0; i < 2; ++i) { + // Create a square shaped box of minimum size 1. + const IntegerVariable size = model.Add(NewIntegerVariable(1, n)); + x.push_back( + model.Add(NewInterval(model.Add(NewIntegerVariable(0, n)), + model.Add(NewIntegerVariable(0, n)), size))); + y.push_back( + model.Add(NewInterval(model.Add(NewIntegerVariable(0, n)), + model.Add(NewIntegerVariable(0, n)), size))); + } + + // The cumulative relaxation adds extra variables that are not complextly + // fixed. So to not count too many solution with our code here, we disable + // that. Note that alternativelly, we could have used the cp_model.proto API + // to do the same, and that should works even with this on. + AddNonOverlappingRectangles(x, y, &model); + + int num_solutions_found = 0; + auto* integer_trail = model.GetOrCreate(); + auto* repository = model.GetOrCreate(); + auto start_value = [repository, integer_trail](IntervalVariable i) { + return integer_trail->LowerBound(repository->Start(i)).value(); + }; + auto end_value = [repository, integer_trail](IntervalVariable i) { + return integer_trail->LowerBound(repository->End(i)).value(); + }; + while (true) { + const SatSolver::Status status = + SolveIntegerProblemWithLazyEncoding(&model); + if (status != SatSolver::Status::FEASIBLE) break; + + // Display the first few solutions. + if (num_solutions_found < 30) { + LOG(INFO) << "R1: " << start_value(x[0]) << "," << start_value(y[0]) + << " " << end_value(x[0]) << "," << end_value(y[0]) + << " R2: " << start_value(x[1]) << "," << start_value(y[1]) + << " " << end_value(x[1]) << "," << end_value(y[1]); + } + + num_solutions_found++; + model.Add(ExcludeCurrentSolutionAndBacktrack()); + } + return num_solutions_found; +} + +TEST(NonOverlappingRectanglesTest, SimpleCounting) { + EXPECT_EQ(CountAllTwoBoxesSolutions(1), 0); + EXPECT_EQ(CountAllTwoBoxesSolutions(2), 3 * 4); + EXPECT_EQ(CountAllTwoBoxesSolutions(3), 9 * 8 + 4 * 5 * 2); + EXPECT_EQ(CountAllTwoBoxesSolutions(4), + /*2 1x1 square*/ 16 * 15 + + /*2 2x2 square*/ 2 * (5 + 3 + 4 + 4) + + /*3x3 and 1x1*/ 2 * 4 * 7 + + /*2x2 amd 1x1*/ 2 * 9 * 12); +} + +TEST(NonOverlappingRectanglesTest, SimpleCountingWithOptional) { + Model model; + IntervalsRepository* interval_repository = + model.GetOrCreate(); + std::vector x; + std::vector y; + const Literal l1(model.Add(NewBooleanVariable()), true); + x.push_back(interval_repository->CreateInterval( + IntegerValue(0), IntegerValue(5), IntegerValue(5), l1.Index(), false)); + y.push_back(interval_repository->CreateInterval( + IntegerValue(0), IntegerValue(2), IntegerValue(2), l1.Index(), false)); + + const Literal l2(model.Add(NewBooleanVariable()), true); + x.push_back(interval_repository->CreateInterval( + IntegerValue(4), IntegerValue(6), IntegerValue(2), l2.Index(), false)); + y.push_back(interval_repository->CreateInterval( + IntegerValue(3), IntegerValue(4), IntegerValue(1), l2.Index(), false)); + + // The cumulative relaxation adds extra variables that are not completely + // fixed. So to not count too many solution with our code here, we disable + // that. Note that alternatively, we could have used the cp_model.proto API + // to do the same, and that should works even with this on. + // TODO(user): Fix and run with add_cumulative_relaxation = true. + AddNonOverlappingRectangles(x, y, &model); + + int num_solutions_found = 0; + while (true) { + const SatSolver::Status status = + SolveIntegerProblemWithLazyEncoding(&model); + if (status != SatSolver::Status::FEASIBLE) break; + + // Display the first few solutions. + if (num_solutions_found < 30) { + LOG(INFO) << "R1: " << interval_repository->IsPresent(x[0]) << " " + << " R2: " << interval_repository->IsPresent(x[1]) << " "; + } + + num_solutions_found++; + model.Add(ExcludeCurrentSolutionAndBacktrack()); + } + EXPECT_EQ(4, num_solutions_found); +} + +TEST(NonOverlappingRectanglesTest, CountSolutionsWithZeroAreaBoxes) { + CpModelBuilder cp_model; + IntVar v1 = cp_model.NewIntVar({1, 2}); + IntVar v2 = cp_model.NewIntVar({0, 1}); + IntervalVar x1 = cp_model.NewIntervalVar(2, v2, 2 + v2); + IntervalVar x2 = cp_model.NewFixedSizeIntervalVar(1, 2); + IntervalVar y1 = cp_model.NewIntervalVar(1, v1, v1 + 1); + IntervalVar y2 = cp_model.NewFixedSizeIntervalVar(2, 0); + NoOverlap2DConstraint diffn = cp_model.AddNoOverlap2D(); + diffn.AddRectangle(x1, y1); + diffn.AddRectangle(x2, y2); + + Model model; + model.Add(NewSatParameters("enumerate_all_solutions:true")); + int count = 0; + model.Add( + NewFeasibleSolutionObserver([&count](const CpSolverResponse& response) { + LOG(INFO) << absl::StrJoin(response.solution(), " "); + ++count; + })); + const CpSolverResponse response = SolveCpModel(cp_model.Build(), &model); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + EXPECT_EQ(count, 2); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/diffn_util_test.cc b/ortools/sat/diffn_util_test.cc new file mode 100644 index 0000000000..508ac031be --- /dev/null +++ b/ortools/sat/diffn_util_test.cc @@ -0,0 +1,960 @@ +// 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/diffn_util.h" + +#include +#include +#include +#include +#include + +#include "absl/algorithm/container.h" +#include "absl/log/check.h" +#include "absl/random/bit_gen_ref.h" +#include "absl/random/distributions.h" +#include "absl/random/random.h" +#include "absl/strings/str_join.h" +#include "absl/types/span.h" +#include "benchmark/benchmark.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/logging.h" +#include "ortools/graph/connected_components.h" +#include "ortools/sat/2d_orthogonal_packing_testing.h" +#include "ortools/sat/integer.h" +#include "ortools/util/strong_integers.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::testing::ElementsAre; +using ::testing::ElementsAreArray; +using ::testing::UnorderedElementsAre; +using ::testing::UnorderedElementsAreArray; + +TEST(GetOverlappingRectangleComponentsTest, NoComponents) { + EXPECT_TRUE(GetOverlappingRectangleComponents({}, {}).empty()); + IntegerValue zero(0); + IntegerValue two(2); + IntegerValue four(4); + EXPECT_TRUE(GetOverlappingRectangleComponents( + {{zero, two, zero, two}, {two, four, two, four}}, {}) + .empty()); + std::vector first = {0}; + EXPECT_TRUE(GetOverlappingRectangleComponents( + {{zero, two, zero, two}, {two, four, two, four}}, + absl::MakeSpan(first)) + .empty()); + std::vector both = {0, 1}; + EXPECT_TRUE(GetOverlappingRectangleComponents( + {{zero, two, zero, two}, {two, four, two, four}}, + absl::MakeSpan(both)) + .empty()); + EXPECT_TRUE(GetOverlappingRectangleComponents( + {{zero, two, zero, two}, {two, four, zero, two}}, + absl::MakeSpan(both)) + .empty()); + EXPECT_TRUE(GetOverlappingRectangleComponents( + {{zero, two, zero, two}, {zero, two, two, four}}, + absl::MakeSpan(both)) + .empty()); +} + +TEST(GetOverlappingRectangleComponentsTest, ComponentAndActive) { + EXPECT_TRUE(GetOverlappingRectangleComponents({}, {}).empty()); + IntegerValue zero(0); + IntegerValue one(1); + IntegerValue two(2); + IntegerValue three(3); + IntegerValue four(4); + + std::vector all = {0, 1, 2}; + const auto& components = GetOverlappingRectangleComponents( + {{zero, two, zero, two}, {zero, two, one, three}, {zero, two, two, four}}, + absl::MakeSpan(all)); + ASSERT_EQ(1, components.size()); + EXPECT_EQ(3, components[0].size()); + + std::vector only_two = {0, 2}; + EXPECT_TRUE(GetOverlappingRectangleComponents({{zero, two, zero, two}, + {zero, two, one, three}, + {zero, two, two, four}}, + absl::MakeSpan(only_two)) + .empty()); +} + +TEST(AnalyzeIntervalsTest, Random) { + // Generate a random set of intervals until the first conflict. We are in n^5! + absl::BitGen random; + const int64_t size = 20; + std::vector rectangles; + std::vector energies; + std::vector boxes; + for (int i = 0; i < 40; ++i) { + Rectangle box; + box.x_min = IntegerValue(absl::Uniform(random, 0, size)); + box.x_max = + IntegerValue(absl::Uniform(random, box.x_min.value() + 1, size + 1)); + box.y_min = IntegerValue(absl::Uniform(random, 0, size)); + box.y_max = + IntegerValue(absl::Uniform(random, box.y_min.value() + 1, size + 1)); + rectangles.push_back(box); + boxes.push_back(i); + energies.push_back(IntegerValue(absl::Uniform( + random, 1, (box.x_max - box.x_min + 1).value())) * + IntegerValue(absl::Uniform( + random, 1, (box.y_max - box.y_min + 1).value()))); + + LOG(INFO) << i << " " << box << " energy:" << energies.back(); + Rectangle conflict; + if (!BoxesAreInEnergyConflict(rectangles, energies, boxes, &conflict)) { + continue; + } + + LOG(INFO) << "Conflict! " << conflict; + + // Make sure whatever filter we do, we do not remove the conflict. + absl::Span s = absl::MakeSpan(boxes); + IntegerValue threshold_x = kMaxIntegerValue; + IntegerValue threshold_y = kMaxIntegerValue; + for (int i = 0; i < 4; ++i) { + if (!AnalyzeIntervals(/*transpose=*/i % 2 == 1, s, rectangles, energies, + &threshold_x, &threshold_y)) { + LOG(INFO) << "Detected by analyse."; + return; + } + s = FilterBoxesAndRandomize(rectangles, s, threshold_x, threshold_y, + random); + LOG(INFO) << "Filtered size: " << s.size() << " x<=" << threshold_x + << " y<=" << threshold_y; + ASSERT_TRUE(BoxesAreInEnergyConflict(rectangles, energies, s)); + } + + break; + } +} + +TEST(FilterBoxesThatAreTooLargeTest, Empty) { + std::vector r; + std::vector energies; + std::vector boxes; + EXPECT_TRUE( + FilterBoxesThatAreTooLarge(r, energies, absl::MakeSpan(boxes)).empty()); +} + +TEST(FilterBoxesThatAreTooLargeTest, BasicTest) { + int num_boxes(3); + std::vector r(num_boxes); + std::vector energies(num_boxes, IntegerValue(25)); + std::vector boxes{0, 1, 2}; + + r[0] = {IntegerValue(0), IntegerValue(5), IntegerValue(0), IntegerValue(5)}; + r[1] = {IntegerValue(0), IntegerValue(10), IntegerValue(0), IntegerValue(10)}; + r[2] = {IntegerValue(0), IntegerValue(6), IntegerValue(0), IntegerValue(6)}; + + EXPECT_THAT(FilterBoxesThatAreTooLarge(r, energies, absl::MakeSpan(boxes)), + ElementsAre(0, 2)); +} + +TEST(ConstructOverlappingSetsTest, BasicTest) { + std::vector> result{{3}}; // To be sure we clear. + + // --------------------0 + // --------1 --------2 + // ------------3 + // ------4 + std::vector intervals{{0, IntegerValue(0), IntegerValue(10)}, + {1, IntegerValue(0), IntegerValue(4)}, + {2, IntegerValue(6), IntegerValue(10)}, + {3, IntegerValue(2), IntegerValue(8)}, + {4, IntegerValue(3), IntegerValue(6)}}; + + // Note that the order is deterministic, but not sorted. + ConstructOverlappingSets(/*already_sorted=*/false, &intervals, &result); + EXPECT_THAT(result, ElementsAre(UnorderedElementsAre(0, 1, 3, 4), + UnorderedElementsAre(3, 0, 2))); +} + +TEST(ConstructOverlappingSetsTest, OneSet) { + std::vector> result{{3}}; // To be sure we clear. + + std::vector intervals{ + {0, IntegerValue(0), IntegerValue(10)}, + {1, IntegerValue(1), IntegerValue(10)}, + {2, IntegerValue(2), IntegerValue(10)}, + {3, IntegerValue(3), IntegerValue(10)}, + {4, IntegerValue(4), IntegerValue(10)}}; + + ConstructOverlappingSets(/*already_sorted=*/false, &intervals, &result); + EXPECT_THAT(result, ElementsAre(ElementsAre(0, 1, 2, 3, 4))); +} + +TEST(GetOverlappingIntervalComponentsTest, BasicTest) { + std::vector> components{{3}}; // To be sure we clear. + + std::vector intervals{{0, IntegerValue(0), IntegerValue(3)}, + {1, IntegerValue(2), IntegerValue(4)}, + {2, IntegerValue(4), IntegerValue(7)}, + {3, IntegerValue(8), IntegerValue(10)}, + {4, IntegerValue(5), IntegerValue(9)}}; + + GetOverlappingIntervalComponents(&intervals, &components); + EXPECT_THAT(components, ElementsAre(ElementsAre(0, 1), ElementsAre(2, 4, 3))); +} + +TEST(GetOverlappingIntervalComponentsAndArticulationPointsTest, + WithWeirdIndicesAndSomeCornerCases) { + // Here are our intervals: 2======5 7====9 + // They are indexed from top to 0===2 4=====7 8======11 + // bottom, from left to right, 1===3 5=6 7=8 + // starting at 10. + std::vector intervals{ + {10, IntegerValue(2), IntegerValue(5)}, + {11, IntegerValue(7), IntegerValue(9)}, + {12, IntegerValue(0), IntegerValue(2)}, + {13, IntegerValue(4), IntegerValue(7)}, + {14, IntegerValue(8), IntegerValue(11)}, + {15, IntegerValue(1), IntegerValue(3)}, + {16, IntegerValue(5), IntegerValue(6)}, + {17, IntegerValue(7), IntegerValue(8)}, + }; + + std::vector> components; + GetOverlappingIntervalComponents(&intervals, &components); + EXPECT_THAT(components, ElementsAre(ElementsAre(12, 15, 10, 13, 16), + ElementsAre(17, 11, 14))); + + EXPECT_THAT(GetIntervalArticulationPoints(&intervals), + ElementsAre(15, 10, 13, 11)); +} + +std::vector GenerateRandomIntervalVector( + absl::BitGenRef random, int num_intervals) { + std::vector intervals; + intervals.reserve(num_intervals); + const int64_t interval_domain = + absl::LogUniform(random, 1, std::numeric_limits::max()); + const int64_t max_interval_length = absl::Uniform( + random, std::max(1, interval_domain / (2 * num_intervals + 1)), + interval_domain); + for (int i = 0; i < num_intervals; ++i) { + const int64_t start = absl::Uniform(random, 0, interval_domain); + const int64_t max_length = + std::min(interval_domain - start, max_interval_length); + const int64_t end = + start + absl::Uniform(absl::IntervalClosed, random, 1, max_length); + intervals.push_back( + IndexedInterval{i, IntegerValue(start), IntegerValue(end)}); + } + return intervals; +} + +std::vector> GetOverlappingIntervalComponentsBruteForce( + const std::vector& intervals) { + // Build the adjacency list. + std::vector> adj(intervals.size()); + for (int i = 1; i < intervals.size(); ++i) { + for (int j = 0; j < i; ++j) { + if (std::max(intervals[i].start, intervals[j].start) < + std::min(intervals[i].end, intervals[j].end)) { + adj[i].push_back(j); + adj[j].push_back(i); + } + } + } + std::vector component_indices = + util::GetConnectedComponents(intervals.size(), adj); + if (component_indices.empty()) return {}; + // Transform that into the expected output: a vector of components. + std::vector> components( + *absl::c_max_element(component_indices) + 1); + for (int i = 0; i < intervals.size(); ++i) { + components[component_indices[i]].push_back(i); + } + // Sort the components by start, like GetOverlappingIntervalComponents(). + absl::c_sort(components, [&intervals](const std::vector& c1, + const std::vector& c2) { + CHECK(!c1.empty() && !c2.empty()); + return intervals[c1[0]].start < intervals[c2[0]].start; + }); + // Inside each component, the intervals should be sorted, too. + // Moreover, we need to convert our indices to IntervalIndex.index. + for (std::vector& component : components) { + absl::c_sort(component, [&intervals](int i, int j) { + return IndexedInterval::ComparatorByStartThenEndThenIndex()(intervals[i], + intervals[j]); + }); + for (int& index : component) index = intervals[index].index; + } + return components; +} + +TEST(GetOverlappingIntervalComponentsTest, RandomizedStressTest) { + // Test duration as of 2021-06: .6s in fastbuild, .3s in opt. + constexpr int kNumTests = 10000; + absl::BitGen random; + for (int test = 0; test < kNumTests; ++test) { + const int num_intervals = absl::Uniform(random, 0, 16); + std::vector intervals = + GenerateRandomIntervalVector(random, num_intervals); + const std::vector intervals_copy = intervals; + std::vector> components; + GetOverlappingIntervalComponents(&intervals, &components); + ASSERT_THAT( + components, + ElementsAreArray(GetOverlappingIntervalComponentsBruteForce(intervals))) + << test << " " << absl::StrJoin(intervals_copy, ","); + // Also verify that the function only altered the order of "intervals". + EXPECT_THAT(intervals, UnorderedElementsAreArray(intervals_copy)); + ASSERT_FALSE(HasFailure()) + << test << " " << absl::StrJoin(intervals_copy, ","); + } +} + +TEST(GetIntervalArticulationPointsTest, RandomizedStressTest) { + // THIS TEST ASSUMES THAT GetOverlappingIntervalComponents() IS CORRECT. + // -> don't look at it if GetOverlappingIntervalComponentsTest.StressTest + // fails, and rather investigate that other test first. + + auto get_num_components = [](const std::vector& intervals) { + std::vector mutable_intervals = intervals; + std::vector> components; + GetOverlappingIntervalComponents(&mutable_intervals, &components); + return components.size(); + }; + // Test duration as of 2021-06: 1s in fastbuild, .4s in opt. + constexpr int kNumTests = 10000; + absl::BitGen random; + for (int test = 0; test < kNumTests; ++test) { + const int num_intervals = absl::Uniform(random, 0, 16); + const std::vector intervals = + GenerateRandomIntervalVector(random, num_intervals); + const int baseline_num_components = get_num_components(intervals); + + // Compute the expected articulation points: try removing each interval + // individually and check whether there are more components if we do. + std::vector expected_articulation_points; + for (int i = 0; i < num_intervals; ++i) { + std::vector tmp_intervals = intervals; + tmp_intervals.erase(tmp_intervals.begin() + i); + if (get_num_components(tmp_intervals) > baseline_num_components) { + expected_articulation_points.push_back(i); + } + } + // Sort the articulation points by start, and replace them by their + // corresponding IndexedInterval.index. + absl::c_sort(expected_articulation_points, [&intervals](int i, int j) { + return intervals[i].start < intervals[j].start; + }); + for (int& idx : expected_articulation_points) idx = intervals[idx].index; + + // Compare our function with the expected values. + std::vector mutable_intervals = intervals; + EXPECT_THAT(GetIntervalArticulationPoints(&mutable_intervals), + ElementsAreArray(expected_articulation_points)); + + // Also verify that the function only altered the order of "intervals". + EXPECT_THAT(mutable_intervals, UnorderedElementsAreArray(intervals)); + ASSERT_FALSE(HasFailure()) << test << " " << absl::StrJoin(intervals, ","); + } +} + +TEST(CapacityProfileTest, BasicApi) { + CapacityProfile profile; + profile.AddRectangle(IntegerValue(2), IntegerValue(6), IntegerValue(0), + IntegerValue(2)); + profile.AddRectangle(IntegerValue(4), IntegerValue(12), IntegerValue(0), + IntegerValue(1)); + profile.AddRectangle(IntegerValue(4), IntegerValue(8), IntegerValue(0), + IntegerValue(5)); + std::vector result; + profile.BuildResidualCapacityProfile(&result); + EXPECT_THAT( + result, + ElementsAre( + CapacityProfile::Rectangle(kMinIntegerValue, IntegerValue(0)), + CapacityProfile::Rectangle(IntegerValue(2), IntegerValue(2)), + CapacityProfile::Rectangle(IntegerValue(4), IntegerValue(5)), + CapacityProfile::Rectangle(IntegerValue(8), IntegerValue(1)), + CapacityProfile::Rectangle(IntegerValue(12), IntegerValue(0)))); + + // We query it twice to test that it can be done and that the result is not + // messed up. + profile.BuildResidualCapacityProfile(&result); + EXPECT_THAT( + result, + ElementsAre( + CapacityProfile::Rectangle(kMinIntegerValue, IntegerValue(0)), + CapacityProfile::Rectangle(IntegerValue(2), IntegerValue(2)), + CapacityProfile::Rectangle(IntegerValue(4), IntegerValue(5)), + CapacityProfile::Rectangle(IntegerValue(8), IntegerValue(1)), + CapacityProfile::Rectangle(IntegerValue(12), IntegerValue(0)))); + EXPECT_EQ(IntegerValue(2 * 2 + 4 * 5 + 4 * 1), profile.GetBoundingArea()); +} + +TEST(CapacityProfileTest, ProfileWithMandatoryPart) { + CapacityProfile profile; + profile.AddRectangle(IntegerValue(2), IntegerValue(6), IntegerValue(0), + IntegerValue(2)); + profile.AddRectangle(IntegerValue(4), IntegerValue(12), IntegerValue(0), + IntegerValue(1)); + profile.AddRectangle(IntegerValue(4), IntegerValue(8), IntegerValue(0), + IntegerValue(5)); + profile.AddMandatoryConsumption(IntegerValue(5), IntegerValue(10), + IntegerValue(1)); + std::vector result; + + // Add a dummy rectangle to test the result is cleared. result.push_bask(..); + result.push_back( + CapacityProfile::Rectangle(IntegerValue(2), IntegerValue(3))); + + profile.BuildResidualCapacityProfile(&result); + EXPECT_THAT( + result, + ElementsAre( + CapacityProfile::Rectangle(kMinIntegerValue, IntegerValue(0)), + CapacityProfile::Rectangle(IntegerValue(2), IntegerValue(2)), + CapacityProfile::Rectangle(IntegerValue(4), IntegerValue(5)), + CapacityProfile::Rectangle(IntegerValue(5), IntegerValue(4)), + CapacityProfile::Rectangle(IntegerValue(8), IntegerValue(0)), + CapacityProfile::Rectangle(IntegerValue(10), IntegerValue(1)), + CapacityProfile::Rectangle(IntegerValue(12), IntegerValue(0)))); + + // The bounding area should not be impacted by the mandatory consumption. + EXPECT_EQ(IntegerValue(2 * 2 + 4 * 5 + 4 * 1), profile.GetBoundingArea()); +} + +IntegerValue NaiveSmallest1DIntersection(IntegerValue range_min, + IntegerValue range_max, + IntegerValue size, + IntegerValue interval_min, + IntegerValue interval_max) { + IntegerValue min_intersection = std::numeric_limits::max(); + for (IntegerValue start = range_min; start + size <= range_max; ++start) { + // Interval is [start, start + size] + const IntegerValue intersection_start = std::max(start, interval_min); + const IntegerValue intersection_end = std::min(start + size, interval_max); + const IntegerValue intersection_length = + std::max(IntegerValue(0), intersection_end - intersection_start); + min_intersection = std::min(min_intersection, intersection_length); + } + return min_intersection; +} + +TEST(Smallest1DIntersectionTest, BasicTest) { + absl::BitGen random; + const int64_t max_size = 20; + constexpr int num_runs = 400; + for (int k = 0; k < num_runs; k++) { + const IntegerValue range_min = + IntegerValue(absl::Uniform(random, 0, max_size - 1)); + const IntegerValue range_max = + IntegerValue(absl::Uniform(random, range_min.value() + 1, max_size)); + const IntegerValue size = + absl::Uniform(random, 1, range_max.value() - range_min.value()); + + const IntegerValue interval_min = + IntegerValue(absl::Uniform(random, 0, max_size - 1)); + const IntegerValue interval_max = + IntegerValue(absl::Uniform(random, interval_min.value() + 1, max_size)); + EXPECT_EQ(NaiveSmallest1DIntersection(range_min, range_max, size, + interval_min, interval_max), + Smallest1DIntersection(range_min, range_max, size, interval_min, + interval_max)); + } +} + +TEST(RectangleTest, BasicTest) { + Rectangle r1 = {.x_min = 0, .x_max = 2, .y_min = 0, .y_max = 2}; + Rectangle r2 = {.x_min = 1, .x_max = 3, .y_min = 1, .y_max = 3}; + EXPECT_EQ(r1.Intersect(r2), + Rectangle({.x_min = 1, .x_max = 2, .y_min = 1, .y_max = 2})); +} + +TEST(RectangleTest, RandomSetDifferenceTest) { + absl::BitGen random; + const int64_t size = 20; + constexpr int num_runs = 400; + for (int k = 0; k < num_runs; k++) { + Rectangle ret[2]; + for (int i = 0; i < 2; ++i) { + ret[i].x_min = IntegerValue(absl::Uniform(random, 0, size - 1)); + ret[i].x_max = + ret[i].x_min + IntegerValue(absl::Uniform(random, 1, size - 1)); + ret[i].y_min = IntegerValue(absl::Uniform(random, 0, size - 1)); + ret[i].y_max = + ret[i].y_min + IntegerValue(absl::Uniform(random, 1, size - 1)); + } + auto set_diff = ret[0].SetDifference(ret[1]); + EXPECT_EQ(set_diff.empty(), ret[0].Intersect(ret[1]) == ret[0]); + IntegerValue diff_area = 0; + for (int i = 0; i < set_diff.size(); ++i) { + for (int j = i + 1; j < set_diff.size(); ++j) { + EXPECT_TRUE(set_diff[i].IsDisjoint(set_diff[j])); + } + EXPECT_NE(set_diff[i].Intersect(ret[0]), Rectangle::GetEmpty()); + EXPECT_EQ(set_diff[i].Intersect(ret[1]), Rectangle::GetEmpty()); + IntegerValue area = set_diff[i].Area(); + EXPECT_GT(area, 0); + diff_area += area; + } + EXPECT_EQ(ret[0].IntersectArea(ret[1]) + diff_area, ret[0].Area()); + } +} + +TEST(GetMinimumOverlapTest, BasicTest) { + RectangleInRange range_ret = { + .bounding_area = {.x_min = 0, .x_max = 15, .y_min = 0, .y_max = 15}, + .x_size = 10, + .y_size = 10}; + + // Minimum intersection is when the item is in the bottom-left corner of the + // allowed space. + Rectangle r = {.x_min = 3, .x_max = 30, .y_min = 3, .y_max = 30}; + EXPECT_EQ(range_ret.GetMinimumIntersection(r).Area(), 7 * 7); + EXPECT_EQ(range_ret.GetAtCorner(RectangleInRange::Corner::BOTTOM_LEFT), + Rectangle({.x_min = 0, .x_max = 10, .y_min = 0, .y_max = 10})); + EXPECT_EQ(range_ret.GetAtCorner(RectangleInRange::Corner::BOTTOM_LEFT) + .Intersect(r) + .Area(), + 7 * 7); + EXPECT_EQ(r.Intersect( + Rectangle({.x_min = 0, .x_max = 10, .y_min = 0, .y_max = 10})), + Rectangle({.x_min = 3, .x_max = 10, .y_min = 3, .y_max = 10})); + + RectangleInRange bigger = + RectangleInRange::BiggestWithMinIntersection(r, range_ret, 7, 7); + // This should be a broader range but don't increase the minimum intersection. + EXPECT_EQ(bigger.GetMinimumIntersection(r).Area(), 7 * 7); + for (const auto& pos : + {RectangleInRange::Corner::BOTTOM_LEFT, + RectangleInRange::Corner::TOP_LEFT, RectangleInRange::Corner::TOP_RIGHT, + RectangleInRange::Corner::BOTTOM_RIGHT}) { + EXPECT_EQ(bigger.GetAtCorner(pos).Intersect(r).Area(), 7 * 7); + } + EXPECT_EQ(bigger.bounding_area.x_min, 0); + EXPECT_EQ(bigger.bounding_area.x_max, 33); + EXPECT_EQ(bigger.bounding_area.y_min, 0); + EXPECT_EQ(bigger.bounding_area.y_max, 33); + EXPECT_EQ(r.Intersect(Rectangle( + {.x_min = 23, .x_max = 33, .y_min = 23, .y_max = 33})), + Rectangle({.x_min = 23, .x_max = 30, .y_min = 23, .y_max = 30})); + + RectangleInRange range_ret2 = { + .bounding_area = {.x_min = 0, .x_max = 105, .y_min = 0, .y_max = 120}, + .x_size = 100, + .y_size = 100}; + Rectangle r2 = {.x_min = 2, .x_max = 4, .y_min = 0, .y_max = 99}; + EXPECT_EQ(range_ret2.GetMinimumIntersection(r2), Rectangle::GetEmpty()); +} + +IntegerValue RecomputeEnergy(const Rectangle& rectangle, + const std::vector& intervals) { + IntegerValue ret = 0; + for (const RectangleInRange& range : intervals) { + const Rectangle min_intersect = range.GetMinimumIntersection(rectangle); + EXPECT_LE(min_intersect.SizeX(), range.x_size); + EXPECT_LE(min_intersect.SizeY(), range.y_size); + ret += min_intersect.Area(); + } + return ret; +} + +IntegerValue RecomputeEnergy(const ProbingRectangle& ranges) { + return RecomputeEnergy(ranges.GetCurrentRectangle(), ranges.Intervals()); +} + +void MoveAndCheck(ProbingRectangle& ranges, ProbingRectangle::Edge type) { + EXPECT_TRUE(ranges.CanShrink(type)); + const IntegerValue expected_area = + ranges.GetCurrentRectangle().Area() - ranges.GetShrinkDeltaArea(type); + const IntegerValue expected_min_energy = + ranges.GetMinimumEnergy() - ranges.GetShrinkDeltaEnergy(type); + ranges.Shrink(type); + EXPECT_EQ(ranges.GetMinimumEnergy(), RecomputeEnergy(ranges)); + EXPECT_EQ(ranges.GetMinimumEnergy(), expected_min_energy); + EXPECT_EQ(ranges.GetCurrentRectangle().Area(), expected_area); + ranges.ValidateInvariants(); +} + +TEST(ProbingRectangleTest, BasicTest) { + RectangleInRange range_ret = { + .bounding_area = {.x_min = 0, .x_max = 15, .y_min = 0, .y_max = 13}, + .x_size = 10, + .y_size = 8}; + RectangleInRange range_ret2 = { + .bounding_area = {.x_min = 1, .x_max = 8, .y_min = 7, .y_max = 14}, + .x_size = 5, + .y_size = 5}; + + std::vector ranges_vec = {range_ret, range_ret2}; + ProbingRectangle ranges(ranges_vec); + EXPECT_EQ(ranges.GetCurrentRectangle(), + Rectangle({.x_min = 0, .x_max = 15, .y_min = 0, .y_max = 14})); + + // Start with the full bounding box, thus both are fully inside. + EXPECT_EQ(ranges.GetMinimumEnergy(), 10 * 8 + 5 * 5); + + EXPECT_EQ(ranges.GetMinimumEnergy(), RecomputeEnergy(ranges)); + + MoveAndCheck(ranges, ProbingRectangle::Edge::LEFT); + EXPECT_EQ(ranges.GetCurrentRectangle(), + Rectangle({.x_min = 1, .x_max = 15, .y_min = 0, .y_max = 14})); + + MoveAndCheck(ranges, ProbingRectangle::Edge::LEFT); + EXPECT_EQ(ranges.GetCurrentRectangle(), + Rectangle({.x_min = 3, .x_max = 15, .y_min = 0, .y_max = 14})); + + MoveAndCheck(ranges, ProbingRectangle::Edge::LEFT); + EXPECT_EQ(ranges.GetCurrentRectangle(), + Rectangle({.x_min = 5, .x_max = 15, .y_min = 0, .y_max = 14})); + + MoveAndCheck(ranges, ProbingRectangle::Edge::LEFT); + EXPECT_EQ(ranges.GetCurrentRectangle(), + Rectangle({.x_min = 6, .x_max = 15, .y_min = 0, .y_max = 14})); + + MoveAndCheck(ranges, ProbingRectangle::Edge::TOP); + EXPECT_EQ(ranges.GetCurrentRectangle(), + Rectangle({.x_min = 6, .x_max = 15, .y_min = 0, .y_max = 13})); + + MoveAndCheck(ranges, ProbingRectangle::Edge::TOP); + EXPECT_EQ(ranges.GetCurrentRectangle(), + Rectangle({.x_min = 6, .x_max = 15, .y_min = 0, .y_max = 8})); + + MoveAndCheck(ranges, ProbingRectangle::Edge::TOP); + EXPECT_EQ(ranges.GetCurrentRectangle(), + Rectangle({.x_min = 6, .x_max = 15, .y_min = 0, .y_max = 5})); +} + +void ReduceUntilDone(ProbingRectangle& ranges, absl::BitGen& random) { + static constexpr ProbingRectangle::Edge kAllEdgesArr[] = { + ProbingRectangle::Edge::LEFT, + ProbingRectangle::Edge::TOP, + ProbingRectangle::Edge::RIGHT, + ProbingRectangle::Edge::BOTTOM, + }; + static constexpr absl::Span kAllMoveTypes( + kAllEdgesArr); + while (!ranges.IsMinimal()) { + ProbingRectangle::Edge type = + kAllMoveTypes.at(absl::Uniform(random, 0, (int)kAllMoveTypes.size())); + if (!ranges.CanShrink(type)) continue; + MoveAndCheck(ranges, type); + } +} + +// This function will find the conflicts for rectangles that have as coordinates +// for the edges one of {min, min + size, max - size, max} for every possible +// item that is at least partially inside the rectangle. Note that we might not +// detect a conflict even if there is one by looking only at those rectangles, +// see the ProbingRectangleTest.CounterExample unit test for a concrete example. +std::optional FindRectangleWithEnergyTooLargeExhaustive( + const std::vector& box_ranges) { + int num_boxes = box_ranges.size(); + std::vector x; + x.reserve(num_boxes * 4); + std::vector y; + y.reserve(num_boxes * 4); + for (const auto& box : box_ranges) { + x.push_back(box.bounding_area.x_min); + x.push_back(box.bounding_area.x_min + box.x_size); + x.push_back(box.bounding_area.x_max - box.x_size); + x.push_back(box.bounding_area.x_max); + y.push_back(box.bounding_area.y_min); + y.push_back(box.bounding_area.y_min + box.y_size); + y.push_back(box.bounding_area.y_max - box.y_size); + y.push_back(box.bounding_area.y_max); + } + std::sort(x.begin(), x.end()); + std::sort(y.begin(), y.end()); + x.erase(std::unique(x.begin(), x.end()), x.end()); + y.erase(std::unique(y.begin(), y.end()), y.end()); + for (int i = 0; i < x.size(); ++i) { + for (int j = i + 1; j < x.size(); ++j) { + for (int k = 0; k < y.size(); ++k) { + for (int l = k + 1; l < y.size(); ++l) { + IntegerValue used_energy = 0; + Rectangle rect = { + .x_min = x[i], .x_max = x[j], .y_min = y[k], .y_max = y[l]}; + for (const auto& box : box_ranges) { + auto intersection = box.GetMinimumIntersection(rect); + used_energy += intersection.Area(); + } + if (used_energy > rect.Area()) { + std::vector items_inside; + for (const auto& box : box_ranges) { + if (box.GetMinimumIntersectionArea(rect) > 0) { + items_inside.push_back(box); + } + } + if (items_inside.size() == num_boxes) { + return rect; + } else { + // Call it again after removing items that are outside. + auto try2 = + FindRectangleWithEnergyTooLargeExhaustive(items_inside); + if (try2.has_value()) { + return try2; + } + } + } + } + } + } + } + return std::nullopt; +} + +// This function should give exactly the same result as the +// `FindRectangleWithEnergyTooLargeExhaustive` above, but exercising the +// `ProbingRectangle` class. +std::optional FindRectangleWithEnergyTooLargeWithProbingRectangle( + std::vector& box_ranges) { + int left_shrinks = 0; + int right_shrinks = 0; + int top_shrinks = 0; + + ProbingRectangle ranges(box_ranges); + + while (true) { + // We want to do the equivalent of what + // `FindRectangleWithEnergyTooLargeExhaustive` does: for every + // left/right/top coordinates, try all possible bottom for conflicts. But + // since we cannot fix the coordinates with ProbingRectangle, we fix the + // number of shrinks instead. + ranges.Reset(); + for (int i = 0; i < left_shrinks; i++) { + CHECK(ranges.CanShrink(ProbingRectangle::Edge::LEFT)); + ranges.Shrink(ProbingRectangle::Edge::LEFT); + } + const bool left_end = !ranges.CanShrink(ProbingRectangle::Edge::LEFT); + for (int i = 0; i < top_shrinks; i++) { + CHECK(ranges.CanShrink(ProbingRectangle::Edge::TOP)); + ranges.Shrink(ProbingRectangle::Edge::TOP); + } + const bool top_end = !ranges.CanShrink(ProbingRectangle::Edge::TOP); + for (int i = 0; i < right_shrinks; i++) { + CHECK(ranges.CanShrink(ProbingRectangle::Edge::RIGHT)); + ranges.Shrink(ProbingRectangle::Edge::RIGHT); + } + const bool right_end = !ranges.CanShrink(ProbingRectangle::Edge::RIGHT); + if (ranges.GetMinimumEnergy() > ranges.GetCurrentRectangleArea()) { + return ranges.GetCurrentRectangle(); + } + while (ranges.CanShrink(ProbingRectangle::Edge::BOTTOM)) { + ranges.Shrink(ProbingRectangle::Edge::BOTTOM); + if (ranges.GetMinimumEnergy() > ranges.GetCurrentRectangleArea()) { + return ranges.GetCurrentRectangle(); + } + } + if (!right_end) { + right_shrinks++; + } else if (!top_end) { + top_shrinks++; + right_shrinks = 0; + } else if (!left_end) { + left_shrinks++; + top_shrinks = 0; + right_shrinks = 0; + } else { + break; + } + } + return std::nullopt; +} + +TEST(ProbingRectangleTest, Random) { + absl::BitGen random; + const int64_t size = 20; + std::vector rectangles; + int count = 0; + int comprehensive_count = 0; + constexpr int num_runs = 400; + for (int k = 0; k < num_runs; k++) { + const int num_intervals = absl::Uniform(random, 1, 20); + IntegerValue total_area = 0; + rectangles.clear(); + for (int i = 0; i < num_intervals; ++i) { + RectangleInRange& range = rectangles.emplace_back(); + range.bounding_area.x_min = IntegerValue(absl::Uniform(random, 0, size)); + range.bounding_area.x_max = IntegerValue( + absl::Uniform(random, range.bounding_area.x_min.value() + 1, size)); + range.x_size = absl::Uniform(random, 1, + range.bounding_area.x_max.value() - + range.bounding_area.x_min.value()); + + range.bounding_area.y_min = IntegerValue(absl::Uniform(random, 0, size)); + range.bounding_area.y_max = IntegerValue( + absl::Uniform(random, range.bounding_area.y_min.value() + 1, size)); + range.y_size = absl::Uniform(random, 1, + range.bounding_area.y_max.value() - + range.bounding_area.y_min.value()); + total_area += range.x_size * range.y_size; + } + auto ret = FindRectanglesWithEnergyConflictMC(rectangles, random, 1.0, 0.8); + count += !ret.conflicts.empty(); + ProbingRectangle ranges(rectangles); + EXPECT_EQ(total_area, ranges.GetMinimumEnergy()); + const bool has_possible_conflict = + FindRectangleWithEnergyTooLargeExhaustive(rectangles).has_value(); + if (has_possible_conflict) { + EXPECT_TRUE( + FindRectangleWithEnergyTooLargeWithProbingRectangle(rectangles) + .has_value()); + } + ReduceUntilDone(ranges, random); + comprehensive_count += has_possible_conflict; + } + LOG(INFO) << count << "/" << num_runs << " had an heuristic (out of " + << comprehensive_count << " possible)."; +} + +// Counterexample for proposition 5.4 of Clautiaux, François, et al. "A new +// constraint programming approach for the orthogonal packing problem." +// Computers & Operations Research 35.3 (2008): 944-959. +TEST(ProbingRectangleTest, CounterExample) { + const std::vector rectangles = { + {.bounding_area = {.x_min = 6, .x_max = 10, .y_min = 11, .y_max = 16}, + .x_size = 3, + .y_size = 2}, + {.bounding_area = {.x_min = 5, .x_max = 17, .y_min = 12, .y_max = 13}, + .x_size = 2, + .y_size = 1}, + {.bounding_area = {.x_min = 15, .x_max = 18, .y_min = 11, .y_max = 14}, + .x_size = 1, + .y_size = 1}, + {.bounding_area = {.x_min = 4, .x_max = 14, .y_min = 4, .y_max = 19}, + .x_size = 8, + .y_size = 7}, + {.bounding_area = {.x_min = 0, .x_max = 16, .y_min = 5, .y_max = 18}, + .x_size = 8, + .y_size = 9}, + {.bounding_area = {.x_min = 4, .x_max = 14, .y_min = 12, .y_max = 16}, + .x_size = 5, + .y_size = 1}, + {.bounding_area = {.x_min = 1, .x_max = 16, .y_min = 12, .y_max = 18}, + .x_size = 6, + .y_size = 1}, + {.bounding_area = {.x_min = 5, .x_max = 19, .y_min = 14, .y_max = 15}, + .x_size = 2, + .y_size = 1}}; + const Rectangle rect = {.x_min = 6, .x_max = 10, .y_min = 7, .y_max = 16}; + // The only other possible rectangle with a conflict is x(7..9), y(7..16), + // but none of {y_min, y_min + y_size, y_max - y_size, y_max} is equal to 7. + const IntegerValue energy = RecomputeEnergy(rect, rectangles); + EXPECT_GT(energy, rect.Area()); + EXPECT_FALSE( + FindRectangleWithEnergyTooLargeExhaustive(rectangles).has_value()); +} + +void BM_FindRectangles(benchmark::State& state) { + absl::BitGen random; + std::vector> problems; + static constexpr int kNumProblems = 20; + for (int i = 0; i < kNumProblems; i++) { + problems.push_back(MakeItemsFromRectangles( + GenerateNonConflictingRectangles(state.range(0), random), + state.range(1) / 100.0, random)); + } + int idx = 0; + for (auto s : state) { + CHECK(FindRectanglesWithEnergyConflictMC(problems[idx], random, 1.0, 0.8) + .conflicts.empty()); + ++idx; + if (idx == kNumProblems) idx = 0; + } +} + +BENCHMARK(BM_FindRectangles) + ->ArgPair(5, 1) + ->ArgPair(10, 1) + ->ArgPair(20, 1) + ->ArgPair(30, 1) + ->ArgPair(40, 1) + ->ArgPair(80, 1) + ->ArgPair(100, 1) + ->ArgPair(200, 1) + ->ArgPair(1000, 1) + ->ArgPair(10000, 1) + ->ArgPair(5, 100) + ->ArgPair(10, 100) + ->ArgPair(20, 100) + ->ArgPair(30, 100) + ->ArgPair(40, 100) + ->ArgPair(80, 100) + ->ArgPair(100, 100) + ->ArgPair(200, 100) + ->ArgPair(1000, 100) + ->ArgPair(10000, 100); + +TEST(FindPairwiseRestrictionsTest, Random) { + absl::BitGen random; + constexpr int num_runs = 400; + for (int k = 0; k < num_runs; k++) { + const int num_rectangles = absl::Uniform(random, 1, 20); + const std::vector rectangles = + GenerateNonConflictingRectangles(num_rectangles, random); + const std::vector items = + GenerateItemsRectanglesWithNoPairwiseConflict( + rectangles, absl::Uniform(random, 0, 1.0), random); + std::vector results; + AppendPairwiseRestrictions(items, &results); + for (const PairwiseRestriction& result : results) { + EXPECT_NE(result.type, + PairwiseRestriction::PairwiseRestrictionType::CONFLICT); + } + } +} + +void BM_FindPairwiseRestrictions(benchmark::State& state) { + absl::BitGen random; + // In the vast majority of the cases the propagator doesn't find any pairwise + // condition to propagate. Thus we choose to benchmark for this particular + // case. + const std::vector items = + GenerateItemsRectanglesWithNoPairwisePropagation( + state.range(0), state.range(1) / 100.0, random); + std::vector results; + for (auto s : state) { + AppendPairwiseRestrictions(items, &results); + CHECK(results.empty()); + } +} + +BENCHMARK(BM_FindPairwiseRestrictions) + ->ArgPair(5, 1) + ->ArgPair(10, 1) + ->ArgPair(20, 1) + ->ArgPair(30, 1) + ->ArgPair(40, 1) + ->ArgPair(80, 1) + ->ArgPair(100, 1) + ->ArgPair(200, 1) + ->ArgPair(1000, 1) + ->ArgPair(10000, 1) + ->ArgPair(5, 100) + ->ArgPair(10, 100) + ->ArgPair(20, 100) + ->ArgPair(30, 100) + ->ArgPair(40, 100) + ->ArgPair(80, 100) + ->ArgPair(100, 100) + ->ArgPair(200, 100) + ->ArgPair(1000, 100) + ->ArgPair(10000, 100); + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/disjunctive_test.cc b/ortools/sat/disjunctive_test.cc new file mode 100644 index 0000000000..bf58e2b915 --- /dev/null +++ b/ortools/sat/disjunctive_test.cc @@ -0,0 +1,527 @@ +// 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/disjunctive.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/random/bit_gen_ref.h" +#include "absl/random/random.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_join.h" +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/base/logging.h" +#include "ortools/sat/integer.h" +#include "ortools/sat/integer_search.h" +#include "ortools/sat/intervals.h" +#include "ortools/sat/model.h" +#include "ortools/sat/precedences.h" +#include "ortools/sat/sat_base.h" +#include "ortools/sat/sat_solver.h" +#include "ortools/util/strong_integers.h" + +namespace operations_research { +namespace sat { +namespace { + +// TODO(user): Add tests for variable duration intervals! The code is trickier +// to get right in this case. + +// Macros to improve the test readability below. +#define MIN_START(v) IntegerValue(v) +#define MIN_DURATION(v) IntegerValue(v) + +TEST(TaskSetTest, AddEntry) { + TaskSet tasks(1000); + std::mt19937 random(12345); + for (int i = 0; i < 1000; ++i) { + tasks.AddEntry({i, MIN_START(absl::Uniform(random, 0, 1000)), + MIN_DURATION(absl::Uniform(random, 0, 100))}); + } + EXPECT_TRUE( + std::is_sorted(tasks.SortedTasks().begin(), tasks.SortedTasks().end())); +} + +TEST(TaskSetTest, EndMinOnEmptySet) { + TaskSet tasks(0); + int critical_index; + EXPECT_EQ(kMinIntegerValue, + tasks.ComputeEndMin(/*task_to_ignore=*/-1, &critical_index)); + EXPECT_EQ(kMinIntegerValue, tasks.ComputeEndMin()); +} + +TEST(TaskSetTest, EndMinBasicTest) { + TaskSet tasks(3); + int critical_index; + tasks.AddEntry({0, MIN_START(2), MIN_DURATION(3)}); + tasks.AddEntry({1, MIN_START(2), MIN_DURATION(3)}); + tasks.AddEntry({2, MIN_START(2), MIN_DURATION(3)}); + EXPECT_EQ(11, tasks.ComputeEndMin(/*task_to_ignore=*/-1, &critical_index)); + EXPECT_EQ(11, tasks.ComputeEndMin()); + EXPECT_EQ(0, critical_index); +} + +TEST(TaskSetTest, EndMinWithNegativeValue) { + TaskSet tasks(3); + int critical_index; + tasks.AddEntry({0, MIN_START(-5), MIN_DURATION(1)}); + tasks.AddEntry({1, MIN_START(-6), MIN_DURATION(2)}); + tasks.AddEntry({2, MIN_START(-7), MIN_DURATION(3)}); + EXPECT_EQ(-1, tasks.ComputeEndMin(/*task_to_ignore=*/-1, &critical_index)); + EXPECT_EQ(-1, tasks.ComputeEndMin()); + EXPECT_EQ(0, critical_index); +} + +TEST(TaskSetTest, EndMinLimitCase) { + TaskSet tasks(3); + int critical_index; + tasks.AddEntry({0, MIN_START(2), MIN_DURATION(3)}); + tasks.AddEntry({1, MIN_START(2), MIN_DURATION(3)}); + tasks.AddEntry({2, MIN_START(8), MIN_DURATION(5)}); + EXPECT_EQ(8, tasks.ComputeEndMin(/*task_to_ignore=*/2, &critical_index)); + EXPECT_EQ(0, critical_index); + EXPECT_EQ(13, tasks.ComputeEndMin(/*task_to_ignore=*/-1, &critical_index)); + EXPECT_EQ(2, critical_index); +} + +TEST(TaskSetTest, IgnoringTheLastEntry) { + TaskSet tasks(3); + int critical_index; + tasks.AddEntry({0, MIN_START(2), MIN_DURATION(3)}); + tasks.AddEntry({1, MIN_START(7), MIN_DURATION(3)}); + EXPECT_EQ(10, tasks.ComputeEndMin(/*task_to_ignore=*/-1, &critical_index)); + EXPECT_EQ(5, tasks.ComputeEndMin(/*task_to_ignore=*/1, &critical_index)); +} + +#define MIN_START(v) IntegerValue(v) +#define MIN_DURATION(v) IntegerValue(v) + +// Tests that the DisjunctiveConstraint propagate how expected on the +// given input. Returns false if a conflict is detected (i.e. no feasible +// solution). +struct TaskWithDuration { + int min_start; + int max_end; + int min_duration; +}; +struct Task { + int min_start; + int max_end; +}; +bool TestDisjunctivePropagation(absl::Span input, + absl::Span expected, + int expected_num_enqueues) { + Model model; + IntegerTrail* integer_trail = model.GetOrCreate(); + IntervalsRepository* intervals = model.GetOrCreate(); + + const int kStart(0); + const int kHorizon(10000); + + std::vector ids; + for (const TaskWithDuration& task : input) { + const IntervalVariable i = + model.Add(NewInterval(kStart, kHorizon, task.min_duration)); + ids.push_back(i); + std::vector no_literal_reason; + std::vector no_integer_reason; + EXPECT_TRUE(integer_trail->Enqueue( + intervals->Start(i).GreaterOrEqual(IntegerValue(task.min_start)), + no_literal_reason, no_integer_reason)); + EXPECT_TRUE( + integer_trail->Enqueue(intervals->End(i).LowerOrEqual(task.max_end), + no_literal_reason, no_integer_reason)); + } + + // Propagate properly the other bounds of the intervals. + EXPECT_TRUE(model.GetOrCreate()->Propagate()); + + const int initial_num_enqueues = integer_trail->num_enqueues(); + AddDisjunctive(ids, &model); + if (!model.GetOrCreate()->Propagate()) return false; + CHECK_EQ(input.size(), expected.size()); + for (int i = 0; i < input.size(); ++i) { + EXPECT_EQ(expected[i].min_start, + integer_trail->LowerBound(intervals->Start(ids[i]))) + << "task #" << i; + EXPECT_EQ(expected[i].max_end, + integer_trail->UpperBound(intervals->End(ids[i]))) + << "task #" << i; + } + + // The *2 is because there is one Enqueue() for the start and end variable. + EXPECT_EQ(expected_num_enqueues + initial_num_enqueues, + integer_trail->num_enqueues()); + return true; +} + +// 01234567890 +// (---- ) +// ( ------) +TEST(DisjunctiveConstraintTest, NoPropagation) { + EXPECT_TRUE(TestDisjunctivePropagation({{0, 10, 4}, {0, 10, 6}}, + {{0, 10}, {0, 10}}, 0)); +} + +// 01234567890 +// (---- ) +// ( -------) +TEST(DisjunctiveConstraintTest, Overload) { + EXPECT_FALSE(TestDisjunctivePropagation({{0, 10, 4}, {0, 10, 7}}, {}, 0)); +} + +// 01234567890123456789 +// (----- ) +// ( -----) +// ( ------ ) +TEST(DisjunctiveConstraintTest, OverloadFromVilimPhd) { + EXPECT_FALSE( + TestDisjunctivePropagation({{0, 13, 5}, {1, 14, 5}, {2, 12, 6}}, {}, 0)); +} + +// 0123456789012345678901234567890123456789 +// ( [---- ) +// (--- ) +// ( ---) +// (-----) +// +// TODO(user): The problem with this test is that the other propagators do +// propagate the same bound, but in 2 steps, whereas the edge finding do that in +// one. To properly test this, we need to add options to deactivate some of +// the propagations. +TEST(DisjunctiveConstraintTest, EdgeFindingFromVilimPhd) { + EXPECT_TRUE(TestDisjunctivePropagation( + {{4, 30, 4}, {5, 13, 3}, {5, 13, 3}, {13, 18, 5}}, + {{18, 30}, {5, 13}, {5, 13}, {13, 18}}, /*expected_num_enqueues=*/2)); +} + +// 0123456789012345678901234567890123456789 +// (----------- ) +// ( ----------) +// ( -- ] ) +TEST(DisjunctiveConstraintTest, NotLastFromVilimPhd) { + EXPECT_TRUE(TestDisjunctivePropagation({{0, 25, 11}, {1, 27, 10}, {4, 20, 2}}, + {{0, 25}, {1, 27}, {4, 17}}, 1)); +} + +// 0123456789012345678901234567890123456789 +// (----- ) +// ( -----) +// (--- ) +// [ <- the new bound for the third task. +TEST(DisjunctiveConstraintTest, DetectablePrecedenceFromVilimPhd) { + EXPECT_TRUE(TestDisjunctivePropagation({{0, 13, 5}, {1, 14, 5}, {7, 17, 3}}, + {{0, 13}, {1, 14}, {10, 17}}, 1)); +} + +TEST(DisjunctiveConstraintTest, Precedences) { + Model model; + Trail* trail = model.GetOrCreate(); + IntegerTrail* integer_trail = model.GetOrCreate(); + auto* precedences = model.GetOrCreate(); + auto* relations = model.GetOrCreate(); + auto* intervals = model.GetOrCreate(); + + const auto add_affine_coeff_one_precedence = [&](const AffineExpression e1, + const AffineExpression& e2) { + CHECK_NE(e1.var, kNoIntegerVariable); + CHECK_EQ(e1.coeff, 1); + CHECK_NE(e2.var, kNoIntegerVariable); + CHECK_EQ(e2.coeff, 1); + precedences->AddPrecedenceWithOffset(e1.var, e2.var, + e1.constant - e2.constant); + relations->Add(e1.var, e2.var, e1.constant - e2.constant); + }; + + const int kStart(0); + const int kHorizon(10000); + + std::vector ids; + ids.push_back(model.Add(NewInterval(kStart, kHorizon, 10))); + ids.push_back(model.Add(NewInterval(kStart, kHorizon, 10))); + ids.push_back(model.Add(NewInterval(kStart, kHorizon, 10))); + AddDisjunctive(ids, &model); + + EXPECT_TRUE(model.GetOrCreate()->Propagate()); + for (const IntervalVariable i : ids) { + EXPECT_EQ(0, integer_trail->LowerBound(intervals->Start(i))); + } + + // Now with the precedences. + add_affine_coeff_one_precedence(intervals->End(ids[0]), + intervals->Start(ids[2])); + add_affine_coeff_one_precedence(intervals->End(ids[1]), + intervals->Start(ids[2])); + EXPECT_TRUE(precedences->Propagate(trail)); + EXPECT_EQ(10, integer_trail->LowerBound(intervals->Start(ids[2]))); + + EXPECT_TRUE(model.GetOrCreate()->Propagate()); + EXPECT_EQ(20, integer_trail->LowerBound(intervals->Start(ids[2]))); +} + +// This test should enumerate all the permutation of kNumIntervals elements. +// It used to fail before CL 134067105. +TEST(SchedulingTest, Permutations) { + static const int kNumIntervals = 4; + Model model; + std::vector intervals; + for (int i = 0; i < kNumIntervals; ++i) { + const IntervalVariable interval = + model.Add(NewInterval(0, kNumIntervals, 1)); + intervals.push_back(interval); + } + AddDisjunctive(intervals, &model); + + IntegerTrail* integer_trail = model.GetOrCreate(); + IntervalsRepository* repository = model.GetOrCreate(); + std::vector> solutions; + while (true) { + const SatSolver::Status status = + SolveIntegerProblemWithLazyEncoding(&model); + if (status != SatSolver::Status::FEASIBLE) break; + + // Add the solution. + std::vector solution(kNumIntervals, -1); + for (int i = 0; i < intervals.size(); ++i) { + const IntervalVariable interval = intervals[i]; + const int64_t start_time = + integer_trail->LowerBound(repository->Start(interval)).value(); + DCHECK_GE(start_time, 0); + DCHECK_LT(start_time, kNumIntervals); + solution[start_time] = i; + } + solutions.push_back(solution); + LOG(INFO) << "Found solution: {" << absl::StrJoin(solution, ", ") << "}."; + + // Loop to the next solution. + model.Add(ExcludeCurrentSolutionAndBacktrack()); + } + + // Test that we do have all the permutations (but in a random order). + std::sort(solutions.begin(), solutions.end()); + std::vector expected(kNumIntervals); + std::iota(expected.begin(), expected.end(), 0); + for (int i = 0; i < solutions.size(); ++i) { + EXPECT_EQ(expected, solutions[i]); + if (i + 1 < solutions.size()) { + EXPECT_TRUE(std::next_permutation(expected.begin(), expected.end())); + } else { + // We enumerated all the permutations. + EXPECT_FALSE(std::next_permutation(expected.begin(), expected.end())); + } + } +} + +// ============================================================================ +// Random tests with comparison with a simple time-decomposition encoding. +// ============================================================================ + +void AddDisjunctiveTimeDecomposition(absl::Span vars, + Model* model) { + const int num_tasks = vars.size(); + IntegerTrail* integer_trail = model->GetOrCreate(); + IntegerEncoder* encoder = model->GetOrCreate(); + IntervalsRepository* repository = model->GetOrCreate(); + + // Compute time range. + IntegerValue min_start = kMaxIntegerValue; + IntegerValue max_end = kMinIntegerValue; + for (int t = 0; t < num_tasks; ++t) { + const AffineExpression start = repository->Start(vars[t]); + const AffineExpression end = repository->End(vars[t]); + min_start = std::min(min_start, integer_trail->LowerBound(start)); + max_end = std::max(max_end, integer_trail->UpperBound(end)); + } + + // Add a constraint for each point of time. + for (IntegerValue time = min_start; time <= max_end; ++time) { + std::vector presence_at_time; + for (const IntervalVariable var : vars) { + const AffineExpression start = repository->Start(var); + const AffineExpression end = repository->End(var); + + const IntegerValue start_min = integer_trail->LowerBound(start); + const IntegerValue end_max = integer_trail->UpperBound(end); + if (end_max <= time || time < start_min) continue; + + // This will be true iff interval is present at time. + // TODO(user): we actually only need one direction of the equivalence. + presence_at_time.push_back( + Literal(model->Add(NewBooleanVariable()), true)); + + std::vector presence_condition; + presence_condition.push_back(encoder->GetOrCreateAssociatedLiteral( + start.LowerOrEqual(IntegerValue(time)))); + presence_condition.push_back(encoder->GetOrCreateAssociatedLiteral( + end.GreaterOrEqual(IntegerValue(time + 1)))); + if (repository->IsOptional(var)) { + presence_condition.push_back(repository->PresenceLiteral(var)); + } + model->Add(ReifiedBoolAnd(presence_condition, presence_at_time.back())); + } + model->Add(AtMostOneConstraint(presence_at_time)); + + // Abort if UNSAT. + if (model->GetOrCreate()->ModelIsUnsat()) return; + } +} + +struct OptionalTasksWithDuration { + int min_start; + int max_end; + int duration; + bool is_optional; +}; + +// TODO(user): we never generate zero duration for now. +std::vector GenerateRandomInstance( + int num_tasks, absl::BitGenRef randomizer) { + std::vector instance; + for (int i = 0; i < num_tasks; ++i) { + OptionalTasksWithDuration task; + task.min_start = absl::Uniform(randomizer, 0, 10); + task.max_end = absl::Uniform(randomizer, 0, 10); + if (task.min_start > task.max_end) std::swap(task.min_start, task.max_end); + if (task.min_start == task.max_end) ++task.max_end; + task.duration = + 1 + absl::Uniform(randomizer, 0, task.max_end - task.min_start - 1); + task.is_optional = absl::Bernoulli(randomizer, 1.0 / 2); + instance.push_back(task); + } + return instance; +} + +int CountAllSolutions( + absl::Span instance, + const std::function&, Model*)>& + add_disjunctive) { + Model model; + std::vector intervals; + for (const OptionalTasksWithDuration& task : instance) { + if (task.is_optional) { + const Literal is_present = Literal(model.Add(NewBooleanVariable()), true); + intervals.push_back(model.Add(NewOptionalInterval( + task.min_start, task.max_end, task.duration, is_present))); + } else { + intervals.push_back( + model.Add(NewInterval(task.min_start, task.max_end, task.duration))); + } + } + add_disjunctive(intervals, &model); + + int num_solutions_found = 0; + while (true) { + const SatSolver::Status status = + SolveIntegerProblemWithLazyEncoding(&model); + if (status != SatSolver::Status::FEASIBLE) break; + num_solutions_found++; + model.Add(ExcludeCurrentSolutionAndBacktrack()); + } + return num_solutions_found; +} + +std::string InstanceDebugString( + absl::Span instance) { + std::string result; + for (const OptionalTasksWithDuration& task : instance) { + absl::StrAppend(&result, "[", task.min_start, ", ", task.max_end, + "] duration:", task.duration, + " is_optional:", task.is_optional, "\n"); + } + return result; +} + +TEST(DisjunctiveTest, RandomComparisonWithSimpleEncoding) { + std::mt19937 randomizer(12345); + const int num_tests = DEBUG_MODE ? 100 : 1000; + for (int test = 0; test < num_tests; ++test) { + const int num_tasks = absl::Uniform(randomizer, 1, 6); + const std::vector instance = + GenerateRandomInstance(num_tasks, randomizer); + EXPECT_EQ(CountAllSolutions(instance, AddDisjunctiveTimeDecomposition), + CountAllSolutions(instance, AddDisjunctive)) + << InstanceDebugString(instance); + EXPECT_EQ( + CountAllSolutions(instance, AddDisjunctive), + CountAllSolutions(instance, AddDisjunctiveWithBooleanPrecedencesOnly)) + << InstanceDebugString(instance); + } +} + +TEST(DisjunctiveTest, TwoIntervalsTest) { + // All the way to put 2 intervals of size 4 and 3 in [0,9]. There is just + // two non-busy unit interval, so: + // - 2 possibilities with 1 hole of size 2 at beginning + // - 2 possibilities with 1 hole of size 2 at the end. + // - 2 possibilities with 1 hole of size 2 in the middle. + // - 2 possibilities with 2 holes around the interval of size 3. + // - 2 possibilities with 2 holes around the interval of size 4. + // - 2 possibilities with 2 holes on both extremities. + std::vector instance; + instance.push_back({0, 9, 4, false}); + instance.push_back({0, 9, 3, false}); + EXPECT_EQ(12, CountAllSolutions(instance, AddDisjunctive)); +} + +TEST(DisjunctiveTest, Precedences) { + Model model; + + std::vector ids; + ids.push_back(model.Add(NewInterval(0, 7, 3))); + ids.push_back(model.Add(NewInterval(0, 7, 2))); + AddDisjunctive(ids, &model); + + const IntegerVariable var = model.Add(NewIntegerVariable(0, 10)); + IntervalsRepository* intervals = model.GetOrCreate(); + model.Add( + AffineCoeffOneLowerOrEqualWithOffset(intervals->End(ids[0]), var, 5)); + model.Add( + AffineCoeffOneLowerOrEqualWithOffset(intervals->End(ids[1]), var, 4)); + + EXPECT_TRUE(model.GetOrCreate()->Propagate()); + EXPECT_EQ(model.Get(LowerBound(var)), (3 + 2) + std::min(4, 5)); +} + +TEST(DisjunctiveTest, OptionalIntervalsWithLinkedPresence) { + Model model; + const Literal alternative = Literal(model.Add(NewBooleanVariable()), true); + + std::vector intervals; + intervals.push_back(model.Add(NewOptionalInterval(0, 6, 3, alternative))); + intervals.push_back(model.Add(NewOptionalInterval(0, 6, 2, alternative))); + intervals.push_back( + model.Add(NewOptionalInterval(0, 6, 4, alternative.Negated()))); + AddDisjunctive(intervals, &model); + + int num_solutions_found = 0; + while (true) { + const SatSolver::Status status = + SolveIntegerProblemWithLazyEncoding(&model); + if (status != SatSolver::Status::FEASIBLE) break; + num_solutions_found++; + model.Add(ExcludeCurrentSolutionAndBacktrack()); + } + EXPECT_EQ(num_solutions_found, /*alternative*/ 6 + /*!alternative*/ 3); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/encoding_test.cc b/ortools/sat/encoding_test.cc new file mode 100644 index 0000000000..29608b3fbb --- /dev/null +++ b/ortools/sat/encoding_test.cc @@ -0,0 +1,106 @@ +// 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/encoding.h" + +#include +#include +#include +#include + +#include "absl/random/distributions.h" +#include "gtest/gtest.h" +#include "ortools/sat/pb_constraint.h" +#include "ortools/sat/sat_base.h" +#include "ortools/sat/sat_solver.h" + +namespace operations_research { +namespace sat { +namespace { + +TEST(MergeAllNodesWithDequeTest, BasicPropagation) { + // We start with a sat solver and n Boolean variables. + std::mt19937 random(12345); + const int n = 456; + SatSolver solver; + solver.SetNumVariables(n); + + // We encode the full cardinality constraint on the n variables. + std::deque repository; + std::vector nodes; + for (int i = 0; i < n; ++i) { + repository.push_back(EncodingNode::LiteralNode( + Literal(BooleanVariable(i), true), Coefficient(0))); + nodes.push_back(&repository.back()); + } + const Coefficient an_upper_bound(1000); + EncodingNode* root = + MergeAllNodesWithDeque(an_upper_bound, nodes, &solver, &repository); + EXPECT_EQ(root->lb(), 0); + EXPECT_EQ(root->ub(), n); + EXPECT_EQ(root->size(), n); + EXPECT_EQ(root->depth(), 9); // 2^9 = 512 which is the first value >= n. + + // We fix some of the n variables randomly, and check some property of the + // Encoding nodes. + for (int run = 0; run < 10; ++run) { + const float density = run / 10; + int exact_count = 0; + solver.Backtrack(0); + for (int i = 0; i < n; ++i) { + const bool value = absl::Bernoulli(random, density); + exact_count += value ? 1 : 0; + EXPECT_TRUE(solver.EnqueueDecisionIfNotConflicting( + Literal(BooleanVariable(i), value))); + } + EXPECT_EQ(solver.Solve(), SatSolver::FEASIBLE); + + // We use an exact encoding, so the number of affected variables at the root + // level of the encoding should be exactly exact_count. + if (exact_count > 0) { + EXPECT_TRUE(solver.Assignment().LiteralIsTrue( + root->GreaterThan(exact_count - 1))); + } + if (exact_count < n) { + EXPECT_FALSE( + solver.Assignment().LiteralIsTrue(root->GreaterThan(exact_count))); + } + } +} + +TEST(LazyMergeAllNodeWithPQAndIncreaseLbTest, CorrectDepth) { + // We start with a sat solver and n Boolean variables. + std::mt19937 random(12345); + const int n = 456; + SatSolver solver; + solver.SetNumVariables(n); + + // We encode the full cardinality constraint on the n variables. + std::deque repository; + std::vector nodes; + for (int i = 0; i < n; ++i) { + repository.push_back(EncodingNode::LiteralNode( + Literal(BooleanVariable(i), true), Coefficient(0))); + nodes.push_back(&repository.back()); + } + EncodingNode* root = + LazyMergeAllNodeWithPQAndIncreaseLb(1, nodes, &solver, &repository); + EXPECT_EQ(root->lb(), 1); + EXPECT_EQ(root->ub(), n); + EXPECT_EQ(root->size(), 0); + EXPECT_EQ(root->depth(), 9); // 2^9 = 512 which is the first value >= n. +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/feasibility_jump_test.cc b/ortools/sat/feasibility_jump_test.cc new file mode 100644 index 0000000000..e20cac1e0d --- /dev/null +++ b/ortools/sat/feasibility_jump_test.cc @@ -0,0 +1,92 @@ +// 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/feasibility_jump.h" + +#include + +#include "gtest/gtest.h" + +namespace operations_research::sat { +namespace { + +TEST(JumpTableTest, TestCachesCalls) { + int num_calls = 0; + JumpTable jumps; + jumps.SetComputeFunction( + [&](int) { return std::make_pair(++num_calls, -1.0); }); + jumps.RecomputeAll(1); + + EXPECT_EQ(jumps.GetJump(0), std::make_pair(1, -1.0)); + EXPECT_EQ(jumps.GetJump(0), std::make_pair(1, -1.0)); + EXPECT_EQ(num_calls, 1); +} + +TEST(JumpTableTest, TestNeedsRecomputationOneVar) { + int num_calls = 0; + JumpTable jumps; + jumps.SetComputeFunction( + [&](int) { return std::make_pair(++num_calls, -1.0); }); + jumps.RecomputeAll(1); + + jumps.GetJump(0); + jumps.Recompute(0); + + EXPECT_EQ(jumps.GetJump(0), std::make_pair(2, -1.0)); + EXPECT_EQ(num_calls, 2); +} + +TEST(JumpTableTest, TestNeedsRecomputationMultiVar) { + int num_calls = 0; + JumpTable jumps; + jumps.SetComputeFunction( + [&](int v) { return std::make_pair(++num_calls, v); }); + jumps.RecomputeAll(2); + + jumps.GetJump(0); + jumps.GetJump(1); + jumps.Recompute(0); + + EXPECT_EQ(jumps.GetJump(0), std::make_pair(3, 0)); + EXPECT_EQ(jumps.GetJump(1), std::make_pair(2, 1)); + EXPECT_EQ(num_calls, 3); +} + +TEST(JumpTableTest, TestVarsNeedingRecomputePossiblyGood) { + int num_calls = 0; + JumpTable jumps; + jumps.SetComputeFunction( + [&](int) { return std::make_pair(++num_calls, 1.0); }); + jumps.RecomputeAll(1); + + EXPECT_TRUE(jumps.NeedRecomputation(0)); + EXPECT_EQ(num_calls, 0); +} + +TEST(JumpTableTest, TestSetJump) { + int num_calls = 0; + JumpTable jumps; + jumps.SetComputeFunction( + [&](int) { return std::make_pair(++num_calls, -1.0); }); + jumps.RecomputeAll(1); + + jumps.SetJump(0, 1, 1.0); + + EXPECT_FALSE(jumps.NeedRecomputation(0)); + EXPECT_GE(jumps.Score(0), 0); + EXPECT_EQ(jumps.GetJump(0), std::make_pair(1, 1.0)); + EXPECT_EQ(num_calls, 0); +} + +} // namespace +} // namespace operations_research::sat diff --git a/ortools/sat/implied_bounds_test.cc b/ortools/sat/implied_bounds_test.cc new file mode 100644 index 0000000000..935c30b33d --- /dev/null +++ b/ortools/sat/implied_bounds_test.cc @@ -0,0 +1,706 @@ +// 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/implied_bounds.h" + +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/strong_vector.h" +#include "ortools/lp_data/lp_types.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/strong_integers.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::testing::Pair; +using ::testing::UnorderedElementsAre; + +TEST(ImpliedBoundsTest, BasicTest) { + Model model; + model.GetOrCreate()->set_use_implied_bounds(true); + auto* ib = model.GetOrCreate(); + auto* sat_solver = model.GetOrCreate(); + auto* integer_trail = model.GetOrCreate(); + + const Literal enforcement(model.Add(NewBooleanVariable()), true); + const IntegerVariable var(model.Add(NewIntegerVariable(0, 10))); + + EXPECT_TRUE(ib->Add(enforcement, + IntegerLiteral::GreaterOrEqual(var, IntegerValue(3)))); + EXPECT_TRUE(ib->Add(enforcement.Negated(), + IntegerLiteral::GreaterOrEqual(var, IntegerValue(7)))); + + // Here because we are at level-zero everything is propagated right away. + EXPECT_EQ(integer_trail->LowerBound(var), IntegerValue(3)); + EXPECT_EQ(integer_trail->LevelZeroLowerBound(var), IntegerValue(3)); + EXPECT_TRUE(sat_solver->Propagate()); + EXPECT_EQ(integer_trail->LowerBound(var), IntegerValue(3)); +} + +TEST(ImpliedBoundsTest, BasicTestPositiveLevel) { + Model model; + model.GetOrCreate()->set_use_implied_bounds(true); + auto* ib = model.GetOrCreate(); + auto* sat_solver = model.GetOrCreate(); + auto* integer_trail = model.GetOrCreate(); + + const Literal enforcement(model.Add(NewBooleanVariable()), true); + const IntegerVariable var(model.Add(NewIntegerVariable(0, 10))); + + // We can do the same at a positive level. + const Literal to_enqueue(model.Add(NewBooleanVariable()), true); + EXPECT_TRUE(sat_solver->ResetToLevelZero()); + EXPECT_TRUE(sat_solver->EnqueueDecisionIfNotConflicting(to_enqueue)); + EXPECT_GT(sat_solver->CurrentDecisionLevel(), 0); + + EXPECT_TRUE(ib->Add(enforcement, + IntegerLiteral::GreaterOrEqual(var, IntegerValue(3)))); + EXPECT_TRUE(ib->Add(enforcement.Negated(), + IntegerLiteral::GreaterOrEqual(var, IntegerValue(7)))); + + // Now, only the level zero bound is up to date. + EXPECT_EQ(integer_trail->LowerBound(var), IntegerValue(0)); + EXPECT_EQ(integer_trail->LevelZeroLowerBound(var), IntegerValue(3)); + + // But on the next restart, nothing is lost. + EXPECT_TRUE(sat_solver->ResetToLevelZero()); + EXPECT_EQ(integer_trail->LowerBound(var), IntegerValue(3)); +} + +// Same test as above but no deduction since parameter is false. +TEST(ImpliedBoundsTest, BasicTestWithFalseParameters) { + Model model; + model.GetOrCreate()->set_use_implied_bounds(false); + auto* ib = model.GetOrCreate(); + auto* sat_solver = model.GetOrCreate(); + auto* integer_trail = model.GetOrCreate(); + + const Literal enforcement(model.Add(NewBooleanVariable()), true); + const IntegerVariable var(model.Add(NewIntegerVariable(0, 10))); + + EXPECT_TRUE(ib->Add(enforcement, + IntegerLiteral::GreaterOrEqual(var, IntegerValue(3)))); + EXPECT_TRUE(ib->Add(enforcement.Negated(), + IntegerLiteral::GreaterOrEqual(var, IntegerValue(7)))); + + EXPECT_TRUE(sat_solver->Propagate()); + EXPECT_EQ(integer_trail->LowerBound(var), IntegerValue(0)); +} + +TEST(ImpliedBoundsTest, ReadBoundsFromTrail) { + Model model; + model.GetOrCreate()->set_use_implied_bounds(true); + + const Literal l(model.Add(NewBooleanVariable()), true); + const IntegerVariable var(model.Add(NewIntegerVariable(0, 100))); + + // Make sure l as a view. + const IntegerVariable view(model.Add(NewIntegerVariable(0, 1))); + model.GetOrCreate()->AssociateToIntegerEqualValue( + l, view, IntegerValue(1)); + + // So that there is a decision. + auto* sat_solver = model.GetOrCreate(); + EXPECT_TRUE(sat_solver->EnqueueDecisionIfNotConflicting(l)); + EXPECT_TRUE(sat_solver->Propagate()); + + // Enqueue a bunch of fact. + auto* integer_trail = model.GetOrCreate(); + EXPECT_TRUE(integer_trail->Enqueue( + IntegerLiteral::GreaterOrEqual(var, IntegerValue(2)), {l.Negated()}, {})); + EXPECT_TRUE(integer_trail->Enqueue( + IntegerLiteral::GreaterOrEqual(var, IntegerValue(4)), {l.Negated()}, {})); + EXPECT_TRUE(integer_trail->Enqueue( + IntegerLiteral::GreaterOrEqual(var, IntegerValue(8)), {l.Negated()}, {})); + EXPECT_TRUE(integer_trail->Enqueue( + IntegerLiteral::GreaterOrEqual(var, IntegerValue(9)), {l.Negated()}, {})); + + // Read from trail. + auto* ib = model.GetOrCreate(); + ib->ProcessIntegerTrail(l); + + std::vector result = ib->GetImpliedBounds(var); + EXPECT_EQ(result.size(), 1); + EXPECT_EQ(result[0].literal_view, view); + EXPECT_EQ(result[0].lower_bound, IntegerValue(9)); + EXPECT_TRUE(result[0].is_positive); +} + +TEST(ImpliedBoundsTest, DetectEqualityFromMin) { + Model model; + model.GetOrCreate()->set_use_implied_bounds(true); + + const Literal literal(model.Add(NewBooleanVariable()), true); + const IntegerVariable var(model.Add(NewIntegerVariable(0, 100))); + + auto* ib = model.GetOrCreate(); + ib->Add(literal, IntegerLiteral::LowerOrEqual(var, IntegerValue(0))); + + EXPECT_THAT( + ib->GetImpliedValues(literal), + testing::UnorderedElementsAre(testing::Pair(var, IntegerValue(0)))); +} + +TEST(ImpliedBoundsTest, DetectEqualityFromMax) { + Model model; + model.GetOrCreate()->set_use_implied_bounds(true); + + const Literal literal(model.Add(NewBooleanVariable()), true); + const IntegerVariable var(model.Add(NewIntegerVariable(0, 100))); + + auto* ib = model.GetOrCreate(); + ib->Add(literal, IntegerLiteral::GreaterOrEqual(var, IntegerValue(100))); + + EXPECT_THAT(ib->GetImpliedValues(literal), + UnorderedElementsAre(Pair(var, IntegerValue(100)))); +} + +TEST(ImpliedBoundsTest, DetectEqualityFromBothInequalities) { + Model model; + model.GetOrCreate()->set_use_implied_bounds(true); + + const Literal literal(model.Add(NewBooleanVariable()), true); + const IntegerVariable var(model.Add(NewIntegerVariable(0, 100))); + + auto* ib = model.GetOrCreate(); + ib->Add(literal, IntegerLiteral::LowerOrEqual(var, IntegerValue(7))); + ib->Add(literal, IntegerLiteral::GreaterOrEqual(var, IntegerValue(7))); + + EXPECT_THAT(ib->GetImpliedValues(literal), + UnorderedElementsAre(Pair(var, IntegerValue(7)))); +} + +TEST(ImpliedBoundsTest, NoEqualityDetection) { + Model model; + model.GetOrCreate()->set_use_implied_bounds(true); + + const Literal literal(model.Add(NewBooleanVariable()), true); + const IntegerVariable var(model.Add(NewIntegerVariable(0, 100))); + + auto* ib = model.GetOrCreate(); + ib->Add(literal, IntegerLiteral::LowerOrEqual(var, IntegerValue(7))); + ib->Add(literal, IntegerLiteral::GreaterOrEqual(var, IntegerValue(6))); + + EXPECT_TRUE(ib->GetImpliedValues(literal).empty()); +} + +TEST(DetectLinearEncodingOfProductsTest, MatchingElementEncodings) { + Model model; + const Literal l0(model.Add(NewBooleanVariable()), true); + const Literal l1(model.Add(NewBooleanVariable()), true); + const Literal l2(model.Add(NewBooleanVariable()), true); + const Literal l3(model.Add(NewBooleanVariable()), true); + + model.Add(NewIntegerVariableFromLiteral(l0)); + model.Add(NewIntegerVariableFromLiteral(l1)); + model.Add(NewIntegerVariableFromLiteral(l2)); + model.Add(NewIntegerVariableFromLiteral(l3)); + + const IntegerVariable x0(model.Add(NewIntegerVariable(0, 100))); + const IntegerVariable x1(model.Add(NewIntegerVariable(0, 100))); + auto* element_encodings = model.GetOrCreate(); + element_encodings->Add(x0, + {{IntegerValue(2), l0}, + {IntegerValue(4), l1}, + {IntegerValue(2), l2}, + {IntegerValue(10), l3}}, + 2); + element_encodings->Add(x1, + {{IntegerValue(3), l0}, + {IntegerValue(10), l1}, + {IntegerValue(20), l2}, + {IntegerValue(30), l3}}, + 2); + LinearConstraintBuilder builder(&model); + builder.AddConstant(IntegerValue(-1)); // To be cleared. + EXPECT_TRUE( + model.GetOrCreate()->TryToLinearize(x0, x1, &builder)); + EXPECT_EQ(builder.BuildExpression().DebugString(), "34*X1 34*X2 294*X3 + 6"); + + builder.Clear(); + EXPECT_TRUE( + model.GetOrCreate()->TryToLinearize(x1, x0, &builder)); + EXPECT_EQ(builder.BuildExpression().DebugString(), "34*X1 34*X2 294*X3 + 6"); +} + +TEST(DetectLinearEncodingOfProductsTest, MatchingEncodingAndSizeTwoEncoding) { + Model model; + const Literal l0(model.Add(NewBooleanVariable()), true); + const Literal l1(model.Add(NewBooleanVariable()), true); + const Literal l2(model.Add(NewBooleanVariable()), true); + const Literal l3(model.Add(NewBooleanVariable()), true); + const IntegerVariable x0(model.Add(NewIntegerVariable(0, 100))); + const IntegerVariable x1(model.Add(NewIntegerVariable(6, 7))); + auto* element_encodings = model.GetOrCreate(); + auto* integer_encoder = model.GetOrCreate(); + element_encodings->Add(x0, + {{IntegerValue(2), l0}, + {IntegerValue(4), l1}, + {IntegerValue(2), l2}, + {IntegerValue(10), l3}}, + 2); + integer_encoder->AssociateToIntegerEqualValue(l2, x1, IntegerValue(7)); + model.Add(NewIntegerVariableFromLiteral(l0)); + model.Add(NewIntegerVariableFromLiteral(l1)); + model.Add(NewIntegerVariableFromLiteral(l2)); + model.Add(NewIntegerVariableFromLiteral(l3)); + + LinearConstraintBuilder builder(&model); + builder.AddConstant(IntegerValue(-1)); // To be cleared. + EXPECT_TRUE( + model.GetOrCreate()->TryToLinearize(x0, x1, &builder)); + EXPECT_EQ(builder.BuildExpression().DebugString(), "12*X3 2*X4 48*X5 + 12"); + + EXPECT_TRUE( + model.GetOrCreate()->TryToLinearize(x1, x0, &builder)); + EXPECT_EQ(builder.BuildExpression().DebugString(), "12*X3 2*X4 48*X5 + 12"); +} + +TEST(DetectLinearEncodingOfProductsTest, BooleanAffinePosPosProduct) { + Model model; + const IntegerVariable var = model.Add(NewIntegerVariable(0, 1)); + const AffineExpression left(var, IntegerValue(2), IntegerValue(-1)); + const AffineExpression right(var, IntegerValue(3), IntegerValue(1)); + + LinearConstraintBuilder builder(&model); + util_intops::StrongVector lp_values(2, 0.0); + + EXPECT_TRUE(model.GetOrCreate()->TryToLinearize( + left, right, &builder)); + for (int value : {0, 1}) { + lp_values[var] = static_cast(value); + lp_values[NegationOf(var)] = static_cast(-value); + EXPECT_EQ(builder.BuildExpression().LpValue(lp_values), + left.LpValue(lp_values) * right.LpValue(lp_values)); + } + + builder.Clear(); + EXPECT_TRUE(model.GetOrCreate()->TryToLinearize( + right, left, &builder)); + for (int value : {0, 1}) { + lp_values[var] = static_cast(value); + lp_values[NegationOf(var)] = static_cast(-value); + EXPECT_EQ(builder.BuildExpression().LpValue(lp_values), + left.LpValue(lp_values) * right.LpValue(lp_values)); + } +} + +TEST(DetectLinearEncodingOfProductsTest, BooleanAffinePosNegProduct) { + Model model; + const IntegerVariable var = model.Add(NewIntegerVariable(0, 1)); + const AffineExpression left(var, IntegerValue(2), IntegerValue(-1)); + const AffineExpression right(NegationOf(var), IntegerValue(3), + IntegerValue(1)); + + LinearConstraintBuilder builder(&model); + util_intops::StrongVector lp_values(2, 0.0); + + EXPECT_TRUE(model.GetOrCreate()->TryToLinearize( + left, right, &builder)); + for (int value : {0, 1}) { + lp_values[var] = static_cast(value); + lp_values[NegationOf(var)] = static_cast(-value); + EXPECT_EQ(builder.BuildExpression().LpValue(lp_values), + left.LpValue(lp_values) * right.LpValue(lp_values)); + } + builder.Clear(); + EXPECT_TRUE(model.GetOrCreate()->TryToLinearize( + right, left, &builder)); + for (int value : {0, 1}) { + lp_values[var] = static_cast(value); + lp_values[NegationOf(var)] = static_cast(-value); + EXPECT_EQ(builder.BuildExpression().LpValue(lp_values), + left.LpValue(lp_values) * right.LpValue(lp_values)); + } +} + +TEST(DetectLinearEncodingOfProductsTest, BooleanAffineNegNegProduct) { + Model model; + const IntegerVariable var = model.Add(NewIntegerVariable(0, 1)); + const AffineExpression left(NegationOf(var), IntegerValue(2), + IntegerValue(-1)); + const AffineExpression right(NegationOf(var), IntegerValue(3), + IntegerValue(1)); + + LinearConstraintBuilder builder(&model); + util_intops::StrongVector lp_values(2, 0.0); + + EXPECT_TRUE(model.GetOrCreate()->TryToLinearize( + left, right, &builder)); + for (int value : {0, 1}) { + lp_values[var] = static_cast(value); + lp_values[NegationOf(var)] = static_cast(-value); + EXPECT_EQ(builder.BuildExpression().LpValue(lp_values), + left.LpValue(lp_values) * right.LpValue(lp_values)); + } + + builder.Clear(); + EXPECT_TRUE(model.GetOrCreate()->TryToLinearize( + right, left, &builder)); + for (int value : {0, 1}) { + lp_values[var] = static_cast(value); + lp_values[NegationOf(var)] = static_cast(-value); + EXPECT_EQ(builder.BuildExpression().LpValue(lp_values), + left.LpValue(lp_values) * right.LpValue(lp_values)); + } +} + +TEST(DetectLinearEncodingOfProductsTest, NoDetectionWhenNotBooleanA) { + Model model; + const IntegerVariable var = model.Add(NewIntegerVariable(0, 2)); + const AffineExpression left(var, IntegerValue(2), IntegerValue(-1)); + const AffineExpression right(var, IntegerValue(3), IntegerValue(1)); + + LinearConstraintBuilder builder(&model); + EXPECT_FALSE(model.GetOrCreate()->TryToLinearize( + left, right, &builder)); +} + +TEST(DetectLinearEncodingOfProductsTest, NoDetectionWhenNotBooleanB) { + Model model; + const IntegerVariable var = model.Add(NewIntegerVariable(-1, 1)); + const AffineExpression left(var, IntegerValue(2), IntegerValue(-1)); + const AffineExpression right(var, IntegerValue(3), IntegerValue(1)); + + LinearConstraintBuilder builder(&model); + EXPECT_FALSE(model.GetOrCreate()->TryToLinearize( + left, right, &builder)); +} + +TEST(DetectLinearEncodingOfProductsTest, AffineTimesConstant) { + Model model; + const IntegerVariable var = model.Add(NewIntegerVariable(0, 5)); + const AffineExpression left(var, IntegerValue(2), IntegerValue(-1)); + const AffineExpression right = IntegerValue(3); + + LinearConstraintBuilder builder(&model); + EXPECT_TRUE(model.GetOrCreate()->TryToLinearize( + left, right, &builder)); + EXPECT_EQ(builder.BuildExpression().DebugString(), "6*X0 + -3"); + + EXPECT_TRUE(model.GetOrCreate()->TryToLinearize( + right, left, &builder)); + EXPECT_EQ(builder.BuildExpression().DebugString(), "6*X0 + -3"); +} + +TEST(DecomposeProductTest, MatchingElementEncodings) { + Model model; + + const Literal l0(model.Add(NewBooleanVariable()), true); + const Literal l1(model.Add(NewBooleanVariable()), true); + const Literal l2(model.Add(NewBooleanVariable()), true); + const Literal l3(model.Add(NewBooleanVariable()), true); + + model.Add(NewIntegerVariableFromLiteral(l0)); + model.Add(NewIntegerVariableFromLiteral(l1)); + model.Add(NewIntegerVariableFromLiteral(l2)); + model.Add(NewIntegerVariableFromLiteral(l3)); + + const IntegerVariable x0(model.Add(NewIntegerVariable(0, 100))); + const IntegerVariable x1(model.Add(NewIntegerVariable(0, 100))); + + auto* element_encodings = model.GetOrCreate(); + element_encodings->Add(x0, + {{IntegerValue(2), l0}, + {IntegerValue(4), l1}, + {IntegerValue(2), l2}, + {IntegerValue(10), l3}}, + 2); + element_encodings->Add(x1, + {{IntegerValue(3), l0}, + {IntegerValue(10), l1}, + {IntegerValue(20), l2}, + {IntegerValue(30), l3}}, + 2); + + auto* decomposer = model.GetOrCreate(); + const std::vector terms_a = + decomposer->TryToDecompose(x0, x1); + const std::vector expected_terms_a = { + {l0, IntegerValue(2), IntegerValue(3)}, + {l1, IntegerValue(4), IntegerValue(10)}, + {l2, IntegerValue(2), IntegerValue(20)}, + {l3, IntegerValue(10), IntegerValue(30)}, + }; + ASSERT_FALSE(terms_a.empty()); + EXPECT_EQ(terms_a, expected_terms_a); + + const std::vector terms_b = + decomposer->TryToDecompose(x1, x0); + const std::vector expected_terms_b = { + {l0, IntegerValue(3), IntegerValue(2)}, + {l1, IntegerValue(10), IntegerValue(4)}, + {l2, IntegerValue(20), IntegerValue(2)}, + {l3, IntegerValue(30), IntegerValue(10)}, + }; + ASSERT_FALSE(terms_b.empty()); + EXPECT_EQ(terms_b, expected_terms_b); +} + +TEST(DecomposeProductTest, MatchingEncodingAndSizeTwoEncoding) { + Model model; + + const Literal l0(model.Add(NewBooleanVariable()), true); + const Literal l1(model.Add(NewBooleanVariable()), true); + const Literal l2(model.Add(NewBooleanVariable()), true); + const Literal l3(model.Add(NewBooleanVariable()), true); + const IntegerVariable x0(model.Add(NewIntegerVariable(0, 100))); + const IntegerVariable x1(model.Add(NewIntegerVariable(6, 7))); + + auto* element_encodings = model.GetOrCreate(); + element_encodings->Add(x0, + {{IntegerValue(2), l0}, + {IntegerValue(4), l1}, + {IntegerValue(2), l2}, + {IntegerValue(10), l3}}, + 2); + + auto* integer_encoder = model.GetOrCreate(); + integer_encoder->AssociateToIntegerEqualValue(l2, x1, IntegerValue(7)); + model.Add(NewIntegerVariableFromLiteral(l0)); + model.Add(NewIntegerVariableFromLiteral(l1)); + model.Add(NewIntegerVariableFromLiteral(l2)); + model.Add(NewIntegerVariableFromLiteral(l3)); + + auto* decomposer = model.GetOrCreate(); + const std::vector terms_a = + decomposer->TryToDecompose(x0, x1); + const std::vector expected_terms_a = { + {l0, IntegerValue(2), IntegerValue(6)}, + {l1, IntegerValue(4), IntegerValue(6)}, + {l2, IntegerValue(2), IntegerValue(7)}, + {l3, IntegerValue(10), IntegerValue(6)}, + }; + EXPECT_EQ(terms_a, expected_terms_a); + + const std::vector terms_b = + decomposer->TryToDecompose(x1, x0); + const std::vector expected_terms_b = { + {l0, IntegerValue(6), IntegerValue(2)}, + {l1, IntegerValue(6), IntegerValue(4)}, + {l2, IntegerValue(7), IntegerValue(2)}, + {l3, IntegerValue(6), IntegerValue(10)}, + }; + EXPECT_EQ(terms_b, expected_terms_b); +} + +TEST(DecomposeProductTest, MatchingSizeTwoEncodingsFirstFirst) { + Model model; + + const Literal l0(model.Add(NewBooleanVariable()), true); + const IntegerVariable x0(model.Add(NewIntegerVariable(5, 6))); + const IntegerVariable x1(model.Add(NewIntegerVariable(6, 7))); + + auto* integer_encoder = model.GetOrCreate(); + integer_encoder->AssociateToIntegerEqualValue(l0, x0, IntegerValue(5)); + integer_encoder->AssociateToIntegerEqualValue(l0, x1, IntegerValue(6)); + + auto* decomposer = model.GetOrCreate(); + const std::vector terms_a = + decomposer->TryToDecompose(x0, x1); + const std::vector expected_terms_a = { + {l0, IntegerValue(5), IntegerValue(6)}, + {l0.Negated(), IntegerValue(6), IntegerValue(7)}, + }; + EXPECT_EQ(terms_a, expected_terms_a); +} + +TEST(DecomposeProductTest, MatchingSizeTwoEncodingsFirstLast) { + Model model; + + const Literal l0(model.Add(NewBooleanVariable()), true); + const IntegerVariable x0(model.Add(NewIntegerVariable(5, 6))); + const IntegerVariable x1(model.Add(NewIntegerVariable(6, 7))); + + auto* integer_encoder = model.GetOrCreate(); + integer_encoder->AssociateToIntegerEqualValue(l0, x0, IntegerValue(5)); + integer_encoder->AssociateToIntegerEqualValue(l0, x1, IntegerValue(7)); + + auto* decomposer = model.GetOrCreate(); + const std::vector terms_a = + decomposer->TryToDecompose(x0, x1); + const std::vector expected_terms_a = { + {l0, IntegerValue(5), IntegerValue(7)}, + {l0.Negated(), IntegerValue(6), IntegerValue(6)}, + }; + EXPECT_EQ(terms_a, expected_terms_a); +} + +TEST(DecomposeProductTest, MatchingSizeTwoEncodingslastFirst) { + Model model; + + const Literal l0(model.Add(NewBooleanVariable()), true); + const IntegerVariable x0(model.Add(NewIntegerVariable(5, 6))); + const IntegerVariable x1(model.Add(NewIntegerVariable(6, 7))); + + auto* integer_encoder = model.GetOrCreate(); + integer_encoder->AssociateToIntegerEqualValue(l0, x0, IntegerValue(6)); + integer_encoder->AssociateToIntegerEqualValue(l0, x1, IntegerValue(6)); + + auto* decomposer = model.GetOrCreate(); + const std::vector terms_a = + decomposer->TryToDecompose(x0, x1); + const std::vector expected_terms_a = { + {l0.Negated(), IntegerValue(5), IntegerValue(7)}, + {l0, IntegerValue(6), IntegerValue(6)}, + }; + EXPECT_EQ(terms_a, expected_terms_a); +} + +TEST(DecomposeProductTest, MatchingSizeTwoEncodingsLastLast) { + Model model; + + const Literal l0(model.Add(NewBooleanVariable()), true); + const IntegerVariable x0(model.Add(NewIntegerVariable(5, 6))); + const IntegerVariable x1(model.Add(NewIntegerVariable(6, 7))); + + auto* integer_encoder = model.GetOrCreate(); + integer_encoder->AssociateToIntegerEqualValue(l0, x0, IntegerValue(6)); + integer_encoder->AssociateToIntegerEqualValue(l0, x1, IntegerValue(7)); + + auto* decomposer = model.GetOrCreate(); + const std::vector terms_a = + decomposer->TryToDecompose(x0, x1); + const std::vector expected_terms_a = { + {l0.Negated(), IntegerValue(5), IntegerValue(6)}, + {l0, IntegerValue(6), IntegerValue(7)}, + }; + EXPECT_EQ(terms_a, expected_terms_a); +} + +TEST(ProductDetectorTest, BasicCases) { + Model model; + model.GetOrCreate()->set_detect_linearized_product(true); + model.GetOrCreate()->set_linearization_level(2); + auto* detector = model.GetOrCreate(); + detector->ProcessTernaryClause(Literals({+1, +2, +3})); + detector->ProcessBinaryClause(Literals({-1, -2})); + detector->ProcessBinaryClause(Literals({-1, -3})); + EXPECT_EQ(kNoLiteralIndex, detector->GetProduct(Literal(-1), Literal(-2))); + EXPECT_EQ(kNoLiteralIndex, detector->GetProduct(Literal(-1), Literal(-3))); + EXPECT_EQ(Literal(+1).Index(), + detector->GetProduct(Literal(-2), Literal(-3))); +} + +TEST(ProductDetectorTest, BasicIntCase1) { + Model model; + model.GetOrCreate()->set_detect_linearized_product(true); + model.GetOrCreate()->set_linearization_level(2); + auto* detector = model.GetOrCreate(); + + IntegerVariable x(10); + IntegerVariable y(20); + detector->ProcessConditionalZero(Literal(+1), x); + detector->ProcessConditionalEquality(Literal(-1), x, y); + + EXPECT_EQ(x, detector->GetProduct(Literal(-1), y)); + EXPECT_EQ(kNoIntegerVariable, detector->GetProduct(Literal(-1), x)); + EXPECT_EQ(kNoIntegerVariable, detector->GetProduct(Literal(1), x)); + EXPECT_EQ(kNoIntegerVariable, detector->GetProduct(Literal(1), y)); +} + +TEST(ProductDetectorTest, BasicIntCase2) { + Model model; + model.GetOrCreate()->set_detect_linearized_product(true); + model.GetOrCreate()->set_linearization_level(2); + auto* detector = model.GetOrCreate(); + + IntegerVariable x(10); + IntegerVariable y(20); + detector->ProcessConditionalEquality(Literal(-1), x, y); + detector->ProcessConditionalZero(Literal(+1), x); + + EXPECT_EQ(x, detector->GetProduct(Literal(-1), y)); + EXPECT_EQ(kNoIntegerVariable, detector->GetProduct(Literal(-1), x)); + EXPECT_EQ(kNoIntegerVariable, detector->GetProduct(Literal(1), x)); + EXPECT_EQ(kNoIntegerVariable, detector->GetProduct(Literal(1), y)); +} + +TEST(ProductDetectorTest, RLT) { + Model model; + model.GetOrCreate()->set_add_rlt_cuts(true); + model.GetOrCreate()->set_linearization_level(2); + auto* detector = model.GetOrCreate(); + auto* integer_encoder = model.GetOrCreate(); + + const Literal l0(model.Add(NewBooleanVariable()), true); + const IntegerVariable x(model.Add(NewIntegerVariable(0, 1))); + integer_encoder->AssociateToIntegerEqualValue(l0, x, IntegerValue(1)); + + const Literal l1(model.Add(NewBooleanVariable()), true); + const IntegerVariable y(model.Add(NewIntegerVariable(0, 1))); + integer_encoder->AssociateToIntegerEqualValue(l1, y, IntegerValue(1)); + + const Literal l2(model.Add(NewBooleanVariable()), true); + const IntegerVariable z(model.Add(NewIntegerVariable(0, 1))); + integer_encoder->AssociateToIntegerEqualValue(l2, z, IntegerValue(1)); + + // X + (1 - Y) + Z >= 1 + detector->ProcessTernaryClause(Literals({+1, -2, +3})); + + // Lets choose value so that X + Z >= Y is tight. + util_intops::StrongVector lp_values(10, 0.0); + lp_values[x] = 0.7; + lp_values[y] = 0.9; + lp_values[z] = 0.2; + const absl::flat_hash_map lp_vars = { + {x, glop::ColIndex(0)}, {y, glop::ColIndex(1)}, {z, glop::ColIndex(2)}}; + detector->InitializeBooleanRLTCuts(lp_vars, lp_values); + + // (1 - X) * Y <= Z, 0.3 * 0.9 == 0.27 <= 0.2, interesting! + // (1 - X) * (1 - Z) <= (1 - Y), 0.3 * 0.8 == 0.24 <= 0.1, interesting ! + // Y * (1 - Z) <= X, 0.9 * 0.8 == 0.72 <= 0.7, interesting ! + EXPECT_EQ(detector->BoolRLTCandidates().size(), 3); + EXPECT_THAT(detector->BoolRLTCandidates().at(NegationOf(x)), + UnorderedElementsAre(y, NegationOf(z))); + EXPECT_THAT(detector->BoolRLTCandidates().at(y), + UnorderedElementsAre(NegationOf(x), NegationOf(z))); + EXPECT_THAT(detector->BoolRLTCandidates().at(NegationOf(z)), + UnorderedElementsAre(y, NegationOf(x))); + + // And we can recover the literal ub. + EXPECT_EQ(detector->LiteralProductUpperBound(NegationOf(x), y), z); + EXPECT_EQ(detector->LiteralProductUpperBound(NegationOf(x), NegationOf(z)), + NegationOf(y)); + EXPECT_EQ(detector->LiteralProductUpperBound(y, NegationOf(z)), x); + + // If we change values, we might get less candidates though + lp_values[x] = 0.0; + lp_values[y] = 0.2; + lp_values[z] = 0.2; + detector->InitializeBooleanRLTCuts(lp_vars, lp_values); + + // (1 - X) * Y <= Z, 1.0 * 0.2 <= 0.2, tight, but not interesting. + // (1 - X) * (1 - Z) <= (1 - Y), 1.0 * 0.8 <= 0.8 tight, but not interesting. + // Y * (1 - Z) <= X, 0.2 * 0.8 <= 0.0, interesting ! + EXPECT_EQ(detector->BoolRLTCandidates().size(), 2); + EXPECT_THAT(detector->BoolRLTCandidates().at(y), + UnorderedElementsAre(NegationOf(z))); + EXPECT_THAT(detector->BoolRLTCandidates().at(NegationOf(z)), + UnorderedElementsAre(y)); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/inclusion_test.cc b/ortools/sat/inclusion_test.cc new file mode 100644 index 0000000000..7f5276708f --- /dev/null +++ b/ortools/sat/inclusion_test.cc @@ -0,0 +1,177 @@ +// 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/inclusion.h" + +#include +#include + +#include "absl/random/random.h" +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/sat/util.h" + +namespace operations_research { +namespace sat { +namespace { + +TEST(InclusionDetectorTest, SymmetricExample) { + CompactVectorVector storage; + InclusionDetector detector(storage); + detector.AddPotentialSet(storage.Add({1, 2})); + detector.AddPotentialSet(storage.Add({1, 3})); + detector.AddPotentialSet(storage.Add({1, 2, 3})); + detector.AddPotentialSet(storage.Add({1, 4, 3, 2})); + + std::vector> included; + detector.DetectInclusions([&included](int subset, int superset) { + included.push_back({subset, superset}); + }); + EXPECT_THAT(included, + ::testing::ElementsAre(std::make_pair(0, 2), std::make_pair(1, 2), + std::make_pair(0, 3), std::make_pair(1, 3), + std::make_pair(2, 3))); +} + +// If sets are duplicates, we do not detect both inclusions, but just one. +TEST(InclusionDetectorTest, DuplicateBehavior) { + CompactVectorVector storage; + InclusionDetector detector(storage); + detector.AddPotentialSet(storage.Add({1, 2})); + detector.AddPotentialSet(storage.Add({1, 2})); + detector.AddPotentialSet(storage.Add({1, 2})); + detector.AddPotentialSet(storage.Add({1, 2})); + + std::vector> included; + detector.DetectInclusions([&included](int subset, int superset) { + included.push_back({subset, superset}); + }); + EXPECT_THAT(included, ::testing::ElementsAre( + std::make_pair(0, 1), std::make_pair(0, 2), + std::make_pair(1, 2), std::make_pair(0, 3), + std::make_pair(2, 3), std::make_pair(1, 3))); +} + +TEST(InclusionDetectorTest, NonSymmetricExample) { + CompactVectorVector storage; + InclusionDetector detector(storage); + + // Index 0, 1, 2 + detector.AddPotentialSubset(storage.Add({1, 2})); + detector.AddPotentialSubset(storage.Add({1, 3})); + detector.AddPotentialSubset(storage.Add({1, 2, 3})); + + // Index 3, 4, 5, 6 + detector.AddPotentialSuperset(storage.Add({1, 2})); + detector.AddPotentialSuperset(storage.Add({1, 4, 3})); + detector.AddPotentialSuperset(storage.Add({1, 4, 3})); + detector.AddPotentialSuperset(storage.Add({1, 5, 2, 3})); + + std::vector> included; + detector.DetectInclusions([&included](int subset, int superset) { + included.push_back({subset, superset}); + }); + EXPECT_THAT(included, ::testing::ElementsAre( + std::make_pair(0, 3), std::make_pair(1, 4), + std::make_pair(1, 5), std::make_pair(0, 6), + std::make_pair(2, 6), std::make_pair(1, 6))); + + // Class can be used multiple time. + // Here we test exclude a subset for appearing twice. + included.clear(); + detector.DetectInclusions([&detector, &included](int subset, int superset) { + included.push_back({subset, superset}); + detector.StopProcessingCurrentSubset(); + }); + EXPECT_THAT(included, + ::testing::ElementsAre(std::make_pair(0, 3), std::make_pair(1, 4), + std::make_pair(2, 6))); + + // Here we test exclude a superset for appearing twice. + included.clear(); + detector.DetectInclusions([&detector, &included](int subset, int superset) { + included.push_back({subset, superset}); + detector.StopProcessingCurrentSuperset(); + }); + EXPECT_THAT(included, ::testing::ElementsAre( + std::make_pair(0, 3), std::make_pair(1, 4), + std::make_pair(1, 5), std::make_pair(0, 6))); + + // Here we stop on first match. + included.clear(); + detector.DetectInclusions([&detector, &included](int subset, int superset) { + included.push_back({subset, superset}); + detector.Stop(); + }); + EXPECT_THAT(included, ::testing::ElementsAre(std::make_pair(0, 3))); +} + +TEST(InclusionDetectorTest, InclusionChain) { + CompactVectorVector storage; + InclusionDetector detector(storage); + detector.AddPotentialSet(storage.Add({1})); + detector.AddPotentialSet(storage.Add({1, 2})); + detector.AddPotentialSet(storage.Add({1, 2, 3})); + + std::vector> included; + detector.DetectInclusions([&included](int subset, int superset) { + included.push_back({subset, superset}); + }); + EXPECT_THAT(included, + ::testing::ElementsAre(std::make_pair(0, 1), std::make_pair(0, 2), + std::make_pair(1, 2))); + + // If we stop processing a superset that can also be a subset, it should + // not appear as such. + included.clear(); + detector.DetectInclusions([&](int subset, int superset) { + detector.StopProcessingCurrentSuperset(); + included.push_back({subset, superset}); + }); + EXPECT_THAT(included, ::testing::ElementsAre(std::make_pair(0, 1), + std::make_pair(0, 2))); +} + +// We just check that nothing crashes. +TEST(InclusionDetectorTest, RandomTest) { + absl::BitGen random; + CompactVectorVector storage; + InclusionDetector detector(storage); + + std::vector temp; + for (int i = 0; i < 1000; ++i) { + temp.clear(); + const int size = absl::Uniform(random, 0, 100); + for (int j = 0; j < size; ++j) { + temp.push_back(absl::Uniform(random, 0, 10000)); + } + if (absl::Bernoulli(random, 0.5)) { + detector.AddPotentialSet(storage.Add(temp)); + } else { + if (absl::Bernoulli(random, 0.5)) { + detector.AddPotentialSubset(storage.Add(temp)); + } else { + detector.AddPotentialSuperset(storage.Add(temp)); + } + } + } + + int num_inclusions = 0; + detector.DetectInclusions( + [&num_inclusions](int subset, int superset) { ++num_inclusions; }); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/integer_test.cc b/ortools/sat/integer_test.cc new file mode 100644 index 0000000000..48fe3902f1 --- /dev/null +++ b/ortools/sat/integer_test.cc @@ -0,0 +1,1333 @@ +// 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.h" + +#include +#include +#include +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/types/span.h" +#include "benchmark/benchmark.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/logging.h" +#include "ortools/base/types.h" +#include "ortools/sat/integer_search.h" +#include "ortools/sat/model.h" +#include "ortools/sat/sat_base.h" +#include "ortools/sat/sat_solver.h" +#include "ortools/util/sorted_interval_list.h" +#include "ortools/util/strong_integers.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::testing::ElementsAre; +using ::testing::UnorderedElementsAre; + +TEST(AffineExpressionTest, Inequalities) { + const IntegerVariable var(1); + EXPECT_EQ( + AffineExpression(var, IntegerValue(3)).LowerOrEqual(IntegerValue(8)), + IntegerLiteral::LowerOrEqual(var, IntegerValue(2))); + EXPECT_EQ( + AffineExpression(var, IntegerValue(-3)).LowerOrEqual(IntegerValue(-1)), + IntegerLiteral::GreaterOrEqual(var, IntegerValue(1))); + EXPECT_EQ( + AffineExpression(var, IntegerValue(2)).GreaterOrEqual(IntegerValue(3)), + IntegerLiteral::GreaterOrEqual(var, IntegerValue(2))); +} + +TEST(AffineExpressionTest, ValueAt) { + const IntegerVariable var(1); + EXPECT_EQ(AffineExpression(var, IntegerValue(3)).ValueAt(IntegerValue(8)), + IntegerValue(3 * 8)); + EXPECT_EQ(AffineExpression(var, IntegerValue(3), IntegerValue(-2)) + .ValueAt(IntegerValue(5)), + IntegerValue(3 * 5 - 2)); +} + +TEST(AffineExpressionTest, NegatedConstant) { + const AffineExpression negated = AffineExpression(IntegerValue(3)).Negated(); + EXPECT_EQ(negated.var, kNoIntegerVariable); + EXPECT_EQ(negated.coeff, 0); + EXPECT_EQ(negated.constant, -3); +} + +TEST(AffineExpressionTest, ApiWithoutVar) { + const AffineExpression three(IntegerValue(3)); + EXPECT_TRUE(three.GreaterOrEqual(IntegerValue(2)).IsAlwaysTrue()); + EXPECT_TRUE(three.LowerOrEqual(IntegerValue(2)).IsAlwaysFalse()); +} + +TEST(ToDoubleTest, Infinities) { + EXPECT_EQ(ToDouble(IntegerValue(100)), 100.0); + + const double kInfinity = std::numeric_limits::infinity(); + EXPECT_EQ(ToDouble(kMaxIntegerValue), kInfinity); + EXPECT_EQ(ToDouble(kMinIntegerValue), -kInfinity); + + EXPECT_LT(ToDouble(kMaxIntegerValue - IntegerValue(1)), kInfinity); + EXPECT_GT(ToDouble(kMinIntegerValue + IntegerValue(1)), -kInfinity); +} + +TEST(FloorRatioTest, AllSmallCases) { + // Dividend can take any value. + for (IntegerValue dividend(-100); dividend < 100; ++dividend) { + // Divisor must be positive. + for (IntegerValue divisor(1); divisor < 100; ++divisor) { + const IntegerValue floor = FloorRatio(dividend, divisor); + EXPECT_LE(floor * divisor, dividend); + EXPECT_GT((floor + 1) * divisor, dividend); + } + } +} + +TEST(PositiveRemainderTest, AllCasesForFixedDivisor) { + IntegerValue divisor(17); + for (IntegerValue dividend(-100); dividend < 100; ++dividend) { + EXPECT_EQ(PositiveRemainder(dividend, divisor), + dividend - divisor * FloorRatio(dividend, divisor)); + } +} + +TEST(CeilRatioTest, AllSmallCases) { + // Dividend can take any value. + for (IntegerValue dividend(-100); dividend < 100; ++dividend) { + // Divisor must be positive. + for (IntegerValue divisor(1); divisor < 100; ++divisor) { + const IntegerValue ceil = CeilRatio(dividend, divisor); + EXPECT_GE(ceil * divisor, dividend); + EXPECT_LT((ceil - 1) * divisor, dividend); + } + } +} + +TEST(NegationOfTest, IsIdempotent) { + for (int i = 0; i < 100; ++i) { + const IntegerVariable var(i); + EXPECT_EQ(NegationOf(NegationOf(var)), var); + } +} + +TEST(NegationOfTest, VectorArgument) { + std::vector vars{IntegerVariable(1), IntegerVariable(2)}; + std::vector negated_vars = NegationOf(vars); + EXPECT_EQ(negated_vars.size(), vars.size()); + for (int i = 0; i < vars.size(); ++i) { + EXPECT_EQ(negated_vars[i], NegationOf(vars[i])); + } +} + +TEST(IntegerValue, NegatedCannotOverflow) { + EXPECT_GT(kMinIntegerValue - 1, std::numeric_limits::min()); +} + +TEST(IntegerLiteral, OverflowValueAreCapped) { + const IntegerVariable var(0); + EXPECT_EQ(IntegerLiteral::GreaterOrEqual(var, kMaxIntegerValue + 1), + IntegerLiteral::GreaterOrEqual( + var, IntegerValue(std::numeric_limits::max()))); + EXPECT_EQ(IntegerLiteral::LowerOrEqual(var, kMinIntegerValue - 1), + IntegerLiteral::LowerOrEqual( + var, IntegerValue(std::numeric_limits::min()))); +} + +TEST(IntegerLiteral, NegatedIsIdempotent) { + for (const IntegerValue value : + {kMinIntegerValue, kMaxIntegerValue, kMaxIntegerValue + 1, + IntegerValue(0), IntegerValue(1), IntegerValue(2)}) { + const IntegerLiteral literal = + IntegerLiteral::GreaterOrEqual(IntegerVariable(0), value); + CHECK_EQ(literal, literal.Negated().Negated()); + } +} + +// A bound difference of exactly kint64max is ok. +TEST(IntegerTrailDeathTest, LargeVariableDomain) { + Model model; + model.Add(NewIntegerVariable(-3, std::numeric_limits::max() - 3)); + + if (DEBUG_MODE) { + // But one of kint64max + 1 cause a check fail in debug. + EXPECT_DEATH(model.Add(NewIntegerVariable( + -3, std::numeric_limits::max() - 2)), + ""); + } +} + +TEST(IntegerTrailTest, ConstantIntegerVariableSharing) { + Model model; + const IntegerVariable a = model.Add(ConstantIntegerVariable(0)); + const IntegerVariable b = model.Add(ConstantIntegerVariable(7)); + const IntegerVariable c = model.Add(ConstantIntegerVariable(-7)); + const IntegerVariable d = model.Add(ConstantIntegerVariable(0)); + const IntegerVariable e = model.Add(ConstantIntegerVariable(3)); + EXPECT_EQ(a, d); + EXPECT_EQ(b, NegationOf(c)); + EXPECT_NE(a, e); + EXPECT_EQ(0, model.Get(Value(a))); + EXPECT_EQ(7, model.Get(Value(b))); + EXPECT_EQ(-7, model.Get(Value(c))); + EXPECT_EQ(0, model.Get(Value(d))); + EXPECT_EQ(3, model.Get(Value(e))); +} + +TEST(IntegerTrailTest, VariableCreationAndBoundGetter) { + Model model; + IntegerTrail* p = model.GetOrCreate(); + IntegerVariable a = model.Add(NewIntegerVariable(0, 10)); + IntegerVariable b = model.Add(NewIntegerVariable(-10, 10)); + IntegerVariable c = model.Add(NewIntegerVariable(20, 30)); + + // Index are dense and contiguous, but two indices are created each time. + // They start at zero. + EXPECT_EQ(0, a.value()); + EXPECT_EQ(1, NegationOf(a).value()); + EXPECT_EQ(2, b.value()); + EXPECT_EQ(3, NegationOf(b).value()); + EXPECT_EQ(4, c.value()); + EXPECT_EQ(5, NegationOf(c).value()); + + // Bounds matches the one we passed at creation. + EXPECT_EQ(0, p->LowerBound(a)); + EXPECT_EQ(10, p->UpperBound(a)); + EXPECT_EQ(-10, p->LowerBound(b)); + EXPECT_EQ(10, p->UpperBound(b)); + EXPECT_EQ(20, p->LowerBound(c)); + EXPECT_EQ(30, p->UpperBound(c)); + + // Test level-zero enqueue. + EXPECT_TRUE( + p->Enqueue(IntegerLiteral::LowerOrEqual(a, IntegerValue(20)), {}, {})); + EXPECT_EQ(10, p->UpperBound(a)); + EXPECT_TRUE( + p->Enqueue(IntegerLiteral::LowerOrEqual(a, IntegerValue(7)), {}, {})); + EXPECT_EQ(7, p->UpperBound(a)); + EXPECT_TRUE( + p->Enqueue(IntegerLiteral::GreaterOrEqual(a, IntegerValue(5)), {}, {})); + EXPECT_EQ(5, p->LowerBound(a)); +} + +TEST(IntegerTrailTest, Untrail) { + Model model; + IntegerTrail* p = model.GetOrCreate(); + IntegerVariable a = p->AddIntegerVariable(IntegerValue(1), IntegerValue(10)); + IntegerVariable b = p->AddIntegerVariable(IntegerValue(2), IntegerValue(10)); + + Trail* trail = model.GetOrCreate(); + trail->Resize(10); + + // We need a reason for the Enqueue(): + const Literal r(model.Add(NewBooleanVariable()), true); + trail->EnqueueWithUnitReason(r.Negated()); + + // Enqueue. + trail->SetDecisionLevel(1); + EXPECT_TRUE(p->Propagate(trail)); + EXPECT_TRUE( + p->Enqueue(IntegerLiteral::GreaterOrEqual(a, IntegerValue(5)), {r}, {})); + EXPECT_EQ(5, p->LowerBound(a)); + EXPECT_TRUE( + p->Enqueue(IntegerLiteral::GreaterOrEqual(b, IntegerValue(7)), {r}, {})); + EXPECT_EQ(7, p->LowerBound(b)); + + trail->SetDecisionLevel(2); + EXPECT_TRUE(p->Propagate(trail)); + EXPECT_TRUE( + p->Enqueue(IntegerLiteral::GreaterOrEqual(b, IntegerValue(9)), {r}, {})); + EXPECT_EQ(9, p->LowerBound(b)); + + // Untrail. + trail->SetDecisionLevel(1); + p->Untrail(*trail, 0); + EXPECT_EQ(7, p->LowerBound(b)); + + trail->SetDecisionLevel(0); + p->Untrail(*trail, 0); + EXPECT_EQ(1, p->LowerBound(a)); + EXPECT_EQ(2, p->LowerBound(b)); +} + +TEST(IntegerTrailTest, BasicReason) { + Model model; + IntegerTrail* p = model.GetOrCreate(); + IntegerVariable a = p->AddIntegerVariable(IntegerValue(1), IntegerValue(10)); + + Trail* trail = model.GetOrCreate(); + trail->Resize(10); + trail->EnqueueWithUnitReason(Literal(-1)); + trail->EnqueueWithUnitReason(Literal(-2)); + trail->EnqueueWithUnitReason(Literal(+3)); + trail->EnqueueWithUnitReason(Literal(+4)); + trail->SetDecisionLevel(1); + EXPECT_TRUE(p->Propagate(trail)); + + // Enqueue. + EXPECT_TRUE(p->Enqueue(IntegerLiteral::GreaterOrEqual(a, IntegerValue(2)), + Literals({+1}), {})); + EXPECT_TRUE(p->Enqueue(IntegerLiteral::GreaterOrEqual(a, IntegerValue(3)), + Literals({+2}), {})); + EXPECT_TRUE(p->Enqueue(IntegerLiteral::GreaterOrEqual(a, IntegerValue(5)), + Literals({-3}), {})); + EXPECT_TRUE(p->Enqueue(IntegerLiteral::GreaterOrEqual(a, IntegerValue(6)), + Literals({-4}), {})); + + EXPECT_THAT(p->ReasonFor(IntegerLiteral::GreaterOrEqual(a, IntegerValue(6))), + ElementsAre(Literal(-4))); + EXPECT_THAT(p->ReasonFor(IntegerLiteral::GreaterOrEqual(a, IntegerValue(5))), + ElementsAre(Literal(-3))); + EXPECT_THAT(p->ReasonFor(IntegerLiteral::GreaterOrEqual(a, IntegerValue(4))), + ElementsAre(Literal(-3))); + EXPECT_THAT(p->ReasonFor(IntegerLiteral::GreaterOrEqual(a, IntegerValue(3))), + ElementsAre(Literal(+2))); + EXPECT_TRUE( + p->ReasonFor(IntegerLiteral::GreaterOrEqual(a, IntegerValue(0))).empty()); + EXPECT_TRUE(p->ReasonFor(IntegerLiteral::GreaterOrEqual(a, IntegerValue(-10))) + .empty()); +} + +struct LazyReasonForTest : public LazyReasonInterface { + bool called = false; + + void Explain(int /*id*/, IntegerValue /*propagation_slack*/, + IntegerVariable /*variable_to_explain*/, int /*trail_index*/, + std::vector* /*literals_reason*/, + std::vector* /*trail_indices_reason*/) final { + called = true; + } +}; + +TEST(IntegerTrailTest, LazyReason) { + Model model; + IntegerTrail* p = model.GetOrCreate(); + IntegerVariable a = p->AddIntegerVariable(IntegerValue(1), IntegerValue(10)); + + Trail* trail = model.GetOrCreate(); + trail->Resize(10); + trail->SetDecisionLevel(1); + EXPECT_TRUE(p->Propagate(trail)); + + LazyReasonForTest mock; + + // Enqueue. + EXPECT_TRUE(p->EnqueueWithLazyReason( + IntegerLiteral::GreaterOrEqual(a, IntegerValue(2)), 0, 0, &mock)); + EXPECT_TRUE(p->Propagate(trail)); + EXPECT_FALSE(mock.called); + + // Called if needed for the conflict. + EXPECT_FALSE( + p->Enqueue(IntegerLiteral::LowerOrEqual(a, IntegerValue(1)), {}, {})); + EXPECT_TRUE(mock.called); +} + +TEST(IntegerTrailTest, LiteralAndBoundReason) { + Model model; + IntegerTrail* p = model.GetOrCreate(); + IntegerVariable a = model.Add(NewIntegerVariable(0, 10)); + IntegerVariable b = model.Add(NewIntegerVariable(0, 10)); + IntegerVariable c = model.Add(NewIntegerVariable(0, 10)); + + Trail* trail = model.GetOrCreate(); + trail->Resize(10); + trail->EnqueueWithUnitReason(Literal(-1)); + trail->EnqueueWithUnitReason(Literal(-2)); + trail->EnqueueWithUnitReason(Literal(-3)); + trail->EnqueueWithUnitReason(Literal(-4)); + trail->SetDecisionLevel(1); + EXPECT_TRUE(p->Propagate(trail)); + + // Enqueue. + EXPECT_TRUE(p->Enqueue(IntegerLiteral::GreaterOrEqual(a, IntegerValue(1)), + Literals({+1}), {})); + EXPECT_TRUE(p->Enqueue(IntegerLiteral::GreaterOrEqual(a, IntegerValue(2)), + Literals({+2}), {})); + EXPECT_TRUE(p->Enqueue(IntegerLiteral::GreaterOrEqual(b, IntegerValue(3)), + Literals({+3}), + {IntegerLiteral::GreaterOrEqual(a, IntegerValue(1))})); + EXPECT_TRUE(p->Enqueue(IntegerLiteral::GreaterOrEqual(c, IntegerValue(5)), + Literals({+4, +3}), + {IntegerLiteral::GreaterOrEqual(a, IntegerValue(2)), + IntegerLiteral::GreaterOrEqual(b, IntegerValue(3))})); + + EXPECT_THAT(p->ReasonFor(IntegerLiteral::GreaterOrEqual(b, IntegerValue(2))), + UnorderedElementsAre(Literal(+1), Literal(+3))); + EXPECT_THAT(p->ReasonFor(IntegerLiteral::GreaterOrEqual(c, IntegerValue(3))), + UnorderedElementsAre(Literal(+2), Literal(+3), Literal(+4))); +} + +TEST(IntegerTrailTest, LevelZeroBounds) { + Model model; + auto* integer_trail = model.GetOrCreate(); + IntegerVariable x = model.Add(NewIntegerVariable(0, 10)); + + Trail* trail = model.GetOrCreate(); + trail->Resize(10); + trail->SetDecisionLevel(1); + trail->EnqueueWithUnitReason(Literal(-1)); + trail->EnqueueWithUnitReason(Literal(-2)); + EXPECT_TRUE(integer_trail->Propagate(trail)); + + // Enqueue. + EXPECT_TRUE(integer_trail->Enqueue( + IntegerLiteral::GreaterOrEqual(x, IntegerValue(1)), Literals({+1}), {})); + EXPECT_TRUE(integer_trail->Enqueue( + IntegerLiteral::LowerOrEqual(x, IntegerValue(2)), Literals({+2}), {})); + + // TEST. + EXPECT_EQ(integer_trail->LowerBound(x), IntegerValue(1)); + EXPECT_EQ(integer_trail->UpperBound(x), IntegerValue(2)); + EXPECT_EQ(integer_trail->LevelZeroLowerBound(x), IntegerValue(0)); + EXPECT_EQ(integer_trail->LevelZeroUpperBound(x), IntegerValue(10)); +} + +TEST(IntegerTrailTest, RelaxLinearReason) { + Model model; + IntegerTrail* integer_trail = model.GetOrCreate(); + const IntegerVariable a = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable b = model.Add(NewIntegerVariable(0, 10)); + + Trail* trail = model.GetOrCreate(); + trail->SetDecisionLevel(1); + EXPECT_TRUE(integer_trail->Propagate(trail)); + + EXPECT_TRUE(integer_trail->Enqueue( + IntegerLiteral::GreaterOrEqual(a, IntegerValue(1)), {}, {})); + EXPECT_TRUE(integer_trail->Enqueue( + IntegerLiteral::GreaterOrEqual(a, IntegerValue(2)), {}, {})); + EXPECT_TRUE(integer_trail->Enqueue( + IntegerLiteral::GreaterOrEqual(b, IntegerValue(1)), {}, {})); + EXPECT_TRUE(integer_trail->Enqueue( + IntegerLiteral::GreaterOrEqual(a, IntegerValue(3)), {}, {})); + EXPECT_TRUE(integer_trail->Enqueue( + IntegerLiteral::GreaterOrEqual(b, IntegerValue(3)), {}, {})); + + std::vector coeffs(2, IntegerValue(1)); + std::vector reasons{ + IntegerLiteral::GreaterOrEqual(a, IntegerValue(3)), + IntegerLiteral::GreaterOrEqual(b, IntegerValue(3))}; + + // No slack, nothing happens. + integer_trail->RelaxLinearReason(IntegerValue(0), coeffs, &reasons); + EXPECT_THAT(reasons, + ElementsAre(IntegerLiteral::GreaterOrEqual(a, IntegerValue(3)), + IntegerLiteral::GreaterOrEqual(b, IntegerValue(3)))); + + // Some slack, we find the "lowest" possible reason in term of trail index. + integer_trail->RelaxLinearReason(IntegerValue(3), coeffs, &reasons); + EXPECT_THAT(reasons, + ElementsAre(IntegerLiteral::GreaterOrEqual(a, IntegerValue(2)), + IntegerLiteral::GreaterOrEqual(b, IntegerValue(1)))); +} + +TEST(IntegerTrailTest, LiteralIsTrueOrFalse) { + Model model; + const IntegerVariable a = model.Add(NewIntegerVariable(1, 9)); + + auto* integer_trail = model.GetOrCreate(); + EXPECT_TRUE(integer_trail->IntegerLiteralIsTrue( + IntegerLiteral::GreaterOrEqual(a, IntegerValue(0)))); + EXPECT_TRUE(integer_trail->IntegerLiteralIsTrue( + IntegerLiteral::LowerOrEqual(a, IntegerValue(10)))); + + EXPECT_TRUE(integer_trail->IntegerLiteralIsTrue( + IntegerLiteral::GreaterOrEqual(a, IntegerValue(1)))); + EXPECT_FALSE(integer_trail->IntegerLiteralIsFalse( + IntegerLiteral::GreaterOrEqual(a, IntegerValue(1)))); + + EXPECT_FALSE(integer_trail->IntegerLiteralIsTrue( + IntegerLiteral::GreaterOrEqual(a, IntegerValue(2)))); + EXPECT_FALSE(integer_trail->IntegerLiteralIsFalse( + IntegerLiteral::GreaterOrEqual(a, IntegerValue(2)))); + + EXPECT_FALSE(integer_trail->IntegerLiteralIsTrue( + IntegerLiteral::GreaterOrEqual(a, IntegerValue(10)))); + EXPECT_TRUE(integer_trail->IntegerLiteralIsFalse( + IntegerLiteral::GreaterOrEqual(a, IntegerValue(10)))); +} + +TEST(IntegerTrailTest, VariableWithHole) { + Model model; + IntegerVariable a = + model.Add(NewIntegerVariable(Domain::FromIntervals({{1, 3}, {6, 7}}))); + model.Add(GreaterOrEqual(a, 4)); + EXPECT_EQ(model.Get(LowerBound(a)), 6); +} + +TEST(GenericLiteralWatcherTest, LevelZeroModifiedVariablesCallbackTest) { + Model model; + auto* integer_trail = model.GetOrCreate(); + auto* watcher = model.GetOrCreate(); + IntegerVariable a = model.Add(NewIntegerVariable(0, 10)); + IntegerVariable b = model.Add(NewIntegerVariable(-10, 10)); + IntegerVariable c = model.Add(NewIntegerVariable(20, 30)); + + std::vector collector; + watcher->RegisterLevelZeroModifiedVariablesCallback( + [&collector](const std::vector& modified_vars) { + collector = modified_vars; + }); + + // No propagation. + auto* sat_solver = model.GetOrCreate(); + EXPECT_TRUE(sat_solver->Propagate()); + EXPECT_EQ(0, collector.size()); + + // Modify 1 variable. + EXPECT_TRUE(integer_trail->Enqueue( + IntegerLiteral::LowerOrEqual(c, IntegerValue(27)), {}, {})); + EXPECT_TRUE(sat_solver->Propagate()); + EXPECT_EQ(1, collector.size()); + EXPECT_EQ(NegationOf(c), collector[0]); + + // Modify 2 variables. + EXPECT_TRUE(integer_trail->Enqueue( + IntegerLiteral::GreaterOrEqual(a, IntegerValue(10)), {}, {})); + EXPECT_TRUE(integer_trail->Enqueue( + IntegerLiteral::LowerOrEqual(b, IntegerValue(7)), {}, {})); + EXPECT_TRUE(sat_solver->Propagate()); + ASSERT_EQ(2, collector.size()); + EXPECT_EQ(a, collector[0]); + EXPECT_EQ(NegationOf(b), collector[1]); + + // Modify 1 variable at level 1. + model.GetOrCreate()->SetDecisionLevel(1); + EXPECT_TRUE(sat_solver->Propagate()); + collector.clear(); + EXPECT_TRUE(integer_trail->Enqueue( + IntegerLiteral::LowerOrEqual(b, IntegerValue(6)), {}, {})); + EXPECT_TRUE(sat_solver->Propagate()); + EXPECT_TRUE(collector.empty()); +} + +TEST(GenericLiteralWatcherTest, RevIsInDiveUpdate) { + Model model; + bool is_in_dive = false; + auto* sat_solver = model.GetOrCreate(); + auto* watcher = model.GetOrCreate(); + const Literal a(sat_solver->NewBooleanVariable(), true); + const Literal b(sat_solver->NewBooleanVariable(), true); + + // First decision. + EXPECT_TRUE(sat_solver->EnqueueDecisionIfNotConflicting(a)); + EXPECT_FALSE(is_in_dive); + watcher->SetUntilNextBacktrack(&is_in_dive); + + // Second decision. + EXPECT_TRUE(sat_solver->EnqueueDecisionIfNotConflicting(b)); + EXPECT_TRUE(is_in_dive); + watcher->SetUntilNextBacktrack(&is_in_dive); + + // If we backtrack, it should be set to false. + EXPECT_TRUE(sat_solver->ResetToLevelZero()); + EXPECT_FALSE(is_in_dive); + + // We can redo the same. + EXPECT_FALSE(is_in_dive); + watcher->SetUntilNextBacktrack(&is_in_dive); + + EXPECT_TRUE(sat_solver->EnqueueDecisionIfNotConflicting(a)); + EXPECT_TRUE(is_in_dive); +} + +TEST(IntegerEncoderTest, BasicInequalityEncoding) { + Model model; + IntegerEncoder* encoder = model.GetOrCreate(); + const IntegerVariable var = model.Add(NewIntegerVariable(0, 10)); + const Literal l3 = encoder->GetOrCreateAssociatedLiteral( + IntegerLiteral::GreaterOrEqual(var, IntegerValue(3))); + const Literal l7 = encoder->GetOrCreateAssociatedLiteral( + IntegerLiteral::GreaterOrEqual(var, IntegerValue(7))); + const Literal l5 = encoder->GetOrCreateAssociatedLiteral( + IntegerLiteral::GreaterOrEqual(var, IntegerValue(5))); + + // Test SearchForLiteralAtOrBefore(). + for (IntegerValue v(0); v < 10; ++v) { + IntegerValue unused; + const LiteralIndex lb_index = encoder->SearchForLiteralAtOrBefore( + IntegerLiteral::GreaterOrEqual(var, v), &unused); + const LiteralIndex ub_index = encoder->SearchForLiteralAtOrBefore( + IntegerLiteral::LowerOrEqual(var, v), &unused); + if (v < 3) { + EXPECT_EQ(lb_index, kNoLiteralIndex); + EXPECT_EQ(ub_index, l3.NegatedIndex()); + } else if (v < 5) { + EXPECT_EQ(lb_index, l3.Index()); + EXPECT_EQ(ub_index, l5.NegatedIndex()); + } else if (v < 7) { + EXPECT_EQ(lb_index, l5.Index()); + EXPECT_EQ(ub_index, l7.NegatedIndex()); + } else { + EXPECT_EQ(lb_index, l7.Index()); + EXPECT_EQ(ub_index, kNoLiteralIndex); + } + } + + // Test the propagation from the literal to the bounds. + // By default the polarity of the literal are false. + EXPECT_EQ(SatSolver::FEASIBLE, model.GetOrCreate()->Solve()); + EXPECT_FALSE(model.Get(Value(l3))); + EXPECT_FALSE(model.Get(Value(l5))); + EXPECT_FALSE(model.Get(Value(l7))); + EXPECT_EQ(0, model.Get(LowerBound(var))); + EXPECT_EQ(2, model.Get(UpperBound(var))); + + // Test the other way around. + model.GetOrCreate()->Backtrack(0); + model.Add(GreaterOrEqual(var, 4)); + EXPECT_EQ(SatSolver::FEASIBLE, model.GetOrCreate()->Solve()); + EXPECT_TRUE(model.Get(Value(l3))); + EXPECT_FALSE(model.Get(Value(l5))); + EXPECT_FALSE(model.Get(Value(l7))); + EXPECT_EQ(4, model.Get(LowerBound(var))); + EXPECT_EQ(4, model.Get(UpperBound(var))); +} + +TEST(IntegerEncoderTest, GetOrCreateTrivialAssociatedLiteral) { + Model model; + IntegerEncoder* encoder = model.GetOrCreate(); + const IntegerVariable var = model.Add(NewIntegerVariable(0, 10)); + EXPECT_EQ(encoder->GetTrueLiteral(), + encoder->GetOrCreateAssociatedLiteral( + IntegerLiteral::GreaterOrEqual(var, IntegerValue(0)))); + EXPECT_EQ(encoder->GetTrueLiteral(), + encoder->GetOrCreateAssociatedLiteral( + IntegerLiteral::GreaterOrEqual(var, IntegerValue(-1)))); + EXPECT_EQ(encoder->GetTrueLiteral(), + encoder->GetOrCreateAssociatedLiteral( + IntegerLiteral::LowerOrEqual(var, IntegerValue(10)))); + EXPECT_EQ(encoder->GetFalseLiteral(), + encoder->GetOrCreateAssociatedLiteral( + IntegerLiteral::GreaterOrEqual(var, IntegerValue(11)))); + EXPECT_EQ(encoder->GetFalseLiteral(), + encoder->GetOrCreateAssociatedLiteral( + IntegerLiteral::GreaterOrEqual(var, IntegerValue(12)))); + EXPECT_EQ(encoder->GetFalseLiteral(), + encoder->GetOrCreateAssociatedLiteral( + IntegerLiteral::LowerOrEqual(var, IntegerValue(-1)))); +} + +TEST(IntegerEncoderTest, ShiftedBinary) { + Model model; + IntegerEncoder* encoder = model.GetOrCreate(); + const IntegerVariable var = model.Add(NewIntegerVariable(1, 2)); + + encoder->FullyEncodeVariable(var); + EXPECT_EQ(encoder->FullDomainEncoding(var).size(), 2); + const std::vector var_encoding = + encoder->FullDomainEncoding(var); + + const Literal g2 = encoder->GetOrCreateAssociatedLiteral( + IntegerLiteral::GreaterOrEqual(var, IntegerValue(2))); + const Literal l1 = encoder->GetOrCreateAssociatedLiteral( + IntegerLiteral::LowerOrEqual(var, IntegerValue(1))); + + EXPECT_EQ(g2, var_encoding[1].literal); + EXPECT_EQ(l1, var_encoding[0].literal); + EXPECT_EQ(g2, l1.Negated()); +} + +TEST(IntegerEncoderTest, SizeTwoDomains) { + Model model; + IntegerEncoder* encoder = model.GetOrCreate(); + const IntegerVariable var = + model.Add(NewIntegerVariable(Domain::FromValues({1, 3}))); + + encoder->FullyEncodeVariable(var); + EXPECT_EQ(encoder->FullDomainEncoding(var).size(), 2); + const std::vector var_encoding = + encoder->FullDomainEncoding(var); + + const Literal g2 = encoder->GetOrCreateAssociatedLiteral( + IntegerLiteral::GreaterOrEqual(var, IntegerValue(2))); + const Literal g3 = encoder->GetOrCreateAssociatedLiteral( + IntegerLiteral::GreaterOrEqual(var, IntegerValue(3))); + const Literal l1 = encoder->GetOrCreateAssociatedLiteral( + IntegerLiteral::LowerOrEqual(var, IntegerValue(1))); + const Literal l2 = encoder->GetOrCreateAssociatedLiteral( + IntegerLiteral::LowerOrEqual(var, IntegerValue(2))); + + EXPECT_EQ(g3, var_encoding[1].literal); + EXPECT_EQ(l1, var_encoding[0].literal); + EXPECT_EQ(g3, l1.Negated()); + EXPECT_EQ(g2, g3); + EXPECT_EQ(l1, l2); +} + +TEST(IntegerEncoderDeathTest, NegatedIsNotCreatedTwice) { + Model model; + IntegerEncoder* encoder = model.GetOrCreate(); + const IntegerVariable var = model.Add(NewIntegerVariable(0, 10)); + const IntegerLiteral l = IntegerLiteral::GreaterOrEqual(var, IntegerValue(3)); + const Literal associated = encoder->GetOrCreateAssociatedLiteral(l); + EXPECT_EQ(associated.Negated(), + encoder->GetOrCreateAssociatedLiteral(l.Negated())); +} + +TEST(IntegerEncoderTest, AutomaticallyDetectFullEncoding) { + Model model; + IntegerEncoder* encoder = model.GetOrCreate(); + const IntegerVariable var = + model.Add(NewIntegerVariable(Domain::FromValues({3, -4, 0}))); + + // Adding <= min should automatically also add == min. + encoder->GetOrCreateAssociatedLiteral( + IntegerLiteral::LowerOrEqual(var, IntegerValue(-4))); + + // We still miss one value. + EXPECT_FALSE(encoder->VariableIsFullyEncoded(var)); + EXPECT_FALSE(encoder->VariableIsFullyEncoded(NegationOf(var))); + + // This is enough to fully encode, because not(<=0) is >=3 which is ==3, and + // we do have all values. + encoder->GetOrCreateLiteralAssociatedToEquality(var, IntegerValue(0)); + EXPECT_TRUE(encoder->VariableIsFullyEncoded(var)); + EXPECT_TRUE(encoder->VariableIsFullyEncoded(NegationOf(var))); + + std::vector values; + for (const auto pair : encoder->FullDomainEncoding(var)) { + values.push_back(pair.value.value()); + } + EXPECT_THAT(values, ElementsAre(-4, 0, 3)); +} + +TEST(IntegerEncoderTest, BasicFullEqualityEncoding) { + Model model; + IntegerEncoder* encoder = model.GetOrCreate(); + const IntegerVariable var = + model.Add(NewIntegerVariable(Domain::FromValues({3, -4, 0}))); + encoder->FullyEncodeVariable(var); + + // Normal var. + { + const auto& result = encoder->FullDomainEncoding(var); + EXPECT_EQ(result.size(), 3); + EXPECT_EQ(result[0], ValueLiteralPair({IntegerValue(-4), + Literal(BooleanVariable(0), true)})); + EXPECT_EQ(result[1], ValueLiteralPair({IntegerValue(0), + Literal(BooleanVariable(1), true)})); + EXPECT_EQ(result[2], + ValueLiteralPair( + {IntegerValue(3), Literal(BooleanVariable(2), false)})); + } + + // Its negation. + { + const auto& result = encoder->FullDomainEncoding(NegationOf(var)); + EXPECT_EQ(result.size(), 3); + EXPECT_EQ(result[0], + ValueLiteralPair( + {IntegerValue(-3), Literal(BooleanVariable(2), false)})); + EXPECT_EQ(result[1], ValueLiteralPair({IntegerValue(0), + Literal(BooleanVariable(1), true)})); + EXPECT_EQ(result[2], ValueLiteralPair({IntegerValue(4), + Literal(BooleanVariable(0), true)})); + } +} + +TEST(IntegerEncoderTest, PartialEncodingOfBinaryVarIsFull) { + Model model; + IntegerEncoder* encoder = model.GetOrCreate(); + const IntegerVariable var = + model.Add(NewIntegerVariable(Domain::FromValues({0, 5}))); + const Literal lit(model.Add(NewBooleanVariable()), true); + + // Initially empty. + EXPECT_TRUE(encoder->PartialDomainEncoding(var).empty()); + + // Normal var. + encoder->AssociateToIntegerEqualValue(lit, var, IntegerValue(0)); + { + const auto& result = encoder->PartialDomainEncoding(var); + EXPECT_EQ(result.size(), 2); + EXPECT_EQ(result[0], ValueLiteralPair({IntegerValue(0), lit})); + EXPECT_EQ(result[1], ValueLiteralPair({IntegerValue(5), lit.Negated()})); + } + + // Its negation. + { + const auto& result = encoder->PartialDomainEncoding(NegationOf(var)); + EXPECT_EQ(result.size(), 2); + EXPECT_EQ(result[0], ValueLiteralPair({IntegerValue(-5), lit.Negated()})); + EXPECT_EQ(result[1], ValueLiteralPair({IntegerValue(0), lit})); + } +} + +TEST(IntegerEncoderTest, PartialEncodingOfLargeVar) { + Model model; + IntegerEncoder* encoder = model.GetOrCreate(); + const IntegerVariable var = model.Add(NewIntegerVariable(0, 1e12)); + for (const int value : {50, 1000, 1}) { + const Literal lit(model.Add(NewBooleanVariable()), true); + encoder->AssociateToIntegerEqualValue(lit, var, IntegerValue(value)); + } + const auto& result = encoder->PartialDomainEncoding(var); + EXPECT_EQ(result.size(), 4); + // Zero is created because encoding (== 1) requires (>= 1 and <= 1), but the + // negation of (>= 1) is also (== 0). + EXPECT_EQ(result[0].value, IntegerValue(0)); + EXPECT_EQ(result[1].value, IntegerValue(1)); + EXPECT_EQ(result[2].value, IntegerValue(50)); + EXPECT_EQ(result[3].value, IntegerValue(1000)); +} + +TEST(IntegerEncoderTest, UpdateInitialDomain) { + Model model; + IntegerEncoder* encoder = model.GetOrCreate(); + const IntegerVariable var = + model.Add(NewIntegerVariable(Domain::FromValues({3, -4, 0}))); + encoder->FullyEncodeVariable(var); + EXPECT_TRUE(model.GetOrCreate()->UpdateInitialDomain( + var, Domain::FromIntervals({{-4, -4}, {0, 0}, {5, 5}}))); + + // Note that we return the filtered encoding. + { + const auto& result = encoder->FullDomainEncoding(var); + EXPECT_EQ(result.size(), 2); + EXPECT_EQ(result[0], ValueLiteralPair({IntegerValue(-4), + Literal(BooleanVariable(0), true)})); + EXPECT_EQ(result[1], ValueLiteralPair({IntegerValue(0), + Literal(BooleanVariable(1), true)})); + } +} + +TEST(IntegerEncoderTest, Canonicalize) { + Model model; + IntegerEncoder* encoder = model.GetOrCreate(); + const IntegerVariable var = + model.Add(NewIntegerVariable(Domain::FromIntervals({{1, 4}, {7, 9}}))); + + EXPECT_EQ(encoder->Canonicalize( + IntegerLiteral::GreaterOrEqual(var, IntegerValue(2))), + std::make_pair(IntegerLiteral::GreaterOrEqual(var, IntegerValue(2)), + IntegerLiteral::LowerOrEqual(var, IntegerValue(1)))); + EXPECT_EQ(encoder->Canonicalize( + IntegerLiteral::GreaterOrEqual(var, IntegerValue(4))), + std::make_pair(IntegerLiteral::GreaterOrEqual(var, IntegerValue(4)), + IntegerLiteral::LowerOrEqual(var, IntegerValue(3)))); + EXPECT_EQ( + encoder->Canonicalize(IntegerLiteral::LowerOrEqual(var, IntegerValue(4))), + std::make_pair(IntegerLiteral::LowerOrEqual(var, IntegerValue(4)), + IntegerLiteral::GreaterOrEqual(var, IntegerValue(7)))); + EXPECT_EQ( + encoder->Canonicalize(IntegerLiteral::LowerOrEqual(var, IntegerValue(6))), + std::make_pair(IntegerLiteral::LowerOrEqual(var, IntegerValue(4)), + IntegerLiteral::GreaterOrEqual(var, IntegerValue(7)))); +} + +TEST(IntegerEncoderDeathTest, CanonicalizeDoNotAcceptTrivialLiterals) { + if (!DEBUG_MODE) GTEST_SKIP() << "Moot in opt mode"; + + Model model; + IntegerEncoder* encoder = model.GetOrCreate(); + const IntegerVariable var = + model.Add(NewIntegerVariable(Domain::FromIntervals({{1, 4}, {7, 9}}))); + + EXPECT_DEATH(encoder->Canonicalize( + IntegerLiteral::GreaterOrEqual(var, IntegerValue(1))), + ""); + EXPECT_DEATH(encoder->Canonicalize( + IntegerLiteral::GreaterOrEqual(var, IntegerValue(0))), + ""); + EXPECT_DEATH( + encoder->Canonicalize(IntegerLiteral::LowerOrEqual(var, IntegerValue(0))), + ""); + EXPECT_DEATH(encoder->Canonicalize( + IntegerLiteral::GreaterOrEqual(var, IntegerValue(0))), + ""); + + EXPECT_DEATH( + encoder->Canonicalize(IntegerLiteral::LowerOrEqual(var, IntegerValue(9))), + ""); + EXPECT_DEATH(encoder->Canonicalize( + IntegerLiteral::LowerOrEqual(var, IntegerValue(15))), + ""); +} + +TEST(IntegerEncoderTest, TrivialAssociation) { + Model model; + IntegerEncoder* encoder = model.GetOrCreate(); + const IntegerVariable var = + model.Add(NewIntegerVariable(Domain::FromIntervals({{1, 1}, {5, 5}}))); + + { + const Literal l(model.Add(NewBooleanVariable()), true); + encoder->AssociateToIntegerLiteral( + l, IntegerLiteral::GreaterOrEqual(var, IntegerValue(1))); + EXPECT_EQ(model.Get(Value(l)), true); + } + { + const Literal l(model.Add(NewBooleanVariable()), true); + encoder->AssociateToIntegerLiteral( + l, IntegerLiteral::GreaterOrEqual(var, IntegerValue(6))); + EXPECT_EQ(model.Get(Value(l)), false); + } + { + const Literal l(model.Add(NewBooleanVariable()), true); + encoder->AssociateToIntegerEqualValue(l, var, IntegerValue(4)); + EXPECT_EQ(model.Get(Value(l)), false); + } +} + +TEST(IntegerEncoderTest, TrivialAssociationWithFixedVariable) { + Model model; + IntegerEncoder* encoder = model.GetOrCreate(); + const IntegerVariable var = model.Add(NewIntegerVariable(Domain(1))); + { + const Literal l(model.Add(NewBooleanVariable()), true); + encoder->AssociateToIntegerEqualValue(l, var, IntegerValue(1)); + EXPECT_EQ(model.Get(Value(l)), true); + } +} + +TEST(IntegerEncoderTest, FullEqualityEncodingForTwoValuesWithDuplicates) { + Model model; + IntegerEncoder* encoder = model.GetOrCreate(); + const IntegerVariable var = + model.Add(NewIntegerVariable(Domain::FromValues({3, 5, 3}))); + encoder->FullyEncodeVariable(var); + + // Normal var. + { + const auto& result = encoder->FullDomainEncoding(var); + EXPECT_EQ(result.size(), 2); + EXPECT_EQ(result[0], ValueLiteralPair({IntegerValue(3), + Literal(BooleanVariable(0), true)})); + EXPECT_EQ(result[1], + ValueLiteralPair( + {IntegerValue(5), Literal(BooleanVariable(0), false)})); + } + + // Its negation. + { + const auto& result = encoder->FullDomainEncoding(NegationOf(var)); + EXPECT_EQ(result.size(), 2); + EXPECT_EQ(result[0], + ValueLiteralPair( + {IntegerValue(-5), Literal(BooleanVariable(0), false)})); + EXPECT_EQ(result[1], ValueLiteralPair({IntegerValue(-3), + Literal(BooleanVariable(0), true)})); + } +} + +#define EXPECT_BOUNDS_EQ(var, lb, ub) \ + EXPECT_EQ(model.Get(LowerBound(var)), lb); \ + EXPECT_EQ(model.Get(UpperBound(var)), ub) + +TEST(IntegerEncoderTest, IntegerTrailToEncodingPropagation) { + Model model; + SatSolver* sat_solver = model.GetOrCreate(); + IntegerEncoder* encoder = model.GetOrCreate(); + Trail* trail = model.GetOrCreate(); + IntegerTrail* integer_trail = model.GetOrCreate(); + + const IntegerVariable var = model.Add( + NewIntegerVariable(Domain::FromIntervals({{3, 4}, {7, 7}, {9, 9}}))); + model.Add(FullyEncodeVariable(var)); + + // We copy this because Enqueue() might change it. + const auto encoding = encoder->FullDomainEncoding(var); + + // Initial propagation is correct. + EXPECT_TRUE(sat_solver->Propagate()); + EXPECT_BOUNDS_EQ(var, 3, 9); + + // Note that the bounds snap to the possible values. + const VariablesAssignment& assignment = trail->Assignment(); + EXPECT_TRUE(integer_trail->Enqueue( + IntegerLiteral::LowerOrEqual(var, IntegerValue(8)), {}, {})); + EXPECT_TRUE(sat_solver->Propagate()); + EXPECT_TRUE(assignment.LiteralIsFalse(encoding[3].literal)); + EXPECT_FALSE(assignment.VariableIsAssigned(encoding[0].literal.Variable())); + EXPECT_FALSE(assignment.VariableIsAssigned(encoding[1].literal.Variable())); + EXPECT_FALSE(assignment.VariableIsAssigned(encoding[2].literal.Variable())); + EXPECT_BOUNDS_EQ(var, 3, 7); + + EXPECT_TRUE(integer_trail->Enqueue( + IntegerLiteral::GreaterOrEqual(var, IntegerValue(5)), {}, {})); + EXPECT_TRUE(sat_solver->Propagate()); + EXPECT_TRUE(assignment.LiteralIsFalse(encoding[0].literal)); + EXPECT_TRUE(assignment.LiteralIsFalse(encoding[1].literal)); + EXPECT_TRUE(assignment.LiteralIsTrue(encoding[2].literal)); + EXPECT_BOUNDS_EQ(var, 7, 7); + + // Encoding[2] will become true on the sat solver propagation. + EXPECT_TRUE(sat_solver->Propagate()); + EXPECT_TRUE(assignment.LiteralIsTrue(encoding[2].literal)); +} + +TEST(IntegerEncoderTest, EncodingToIntegerTrailPropagation) { + Model model; + SatSolver* sat_solver = model.GetOrCreate(); + IntegerEncoder* encoder = model.GetOrCreate(); + Trail* trail = model.GetOrCreate(); + IntegerTrail* integer_trail = model.GetOrCreate(); + const IntegerVariable var = model.Add( + NewIntegerVariable(Domain::FromIntervals({{3, 4}, {7, 7}, {9, 9}}))); + model.Add(FullyEncodeVariable(var)); + const auto& encoding = encoder->FullDomainEncoding(var); + + // Initial propagation is correct. + EXPECT_TRUE(sat_solver->Propagate()); + EXPECT_BOUNDS_EQ(var, 3, 9); + + // We remove the value 4, nothing happen. + trail->SetDecisionLevel(1); + trail->EnqueueSearchDecision(encoding[1].literal.Negated()); + EXPECT_TRUE(sat_solver->Propagate()); + EXPECT_BOUNDS_EQ(var, 3, 9); + + // When we remove 3, the lower bound change though. + trail->SetDecisionLevel(2); + trail->EnqueueSearchDecision(encoding[0].literal.Negated()); + EXPECT_TRUE(sat_solver->Propagate()); + EXPECT_BOUNDS_EQ(var, 7, 9); + + // The reason for the lower bounds is that both encoding[0] and encoding[1] + // are false. But it is captured by the literal associated to x >= 7. + { + const IntegerLiteral l = integer_trail->LowerBoundAsLiteral(var); + EXPECT_EQ(integer_trail->ReasonFor(l), + std::vector{ + Literal(encoder->GetAssociatedLiteral(l)).Negated()}); + } + + // Test the other direction. + trail->SetDecisionLevel(3); + trail->EnqueueSearchDecision(encoding[3].literal.Negated()); + EXPECT_TRUE(sat_solver->Propagate()); + EXPECT_BOUNDS_EQ(var, 7, 7); + { + const IntegerLiteral l = integer_trail->UpperBoundAsLiteral(var); + EXPECT_EQ(integer_trail->ReasonFor(l), + std::vector{ + Literal(encoder->GetAssociatedLiteral(l)).Negated()}); + } +} + +TEST(IntegerEncoderTest, IsFixedOrHasAssociatedLiteral) { + Model model; + SatSolver* sat_solver = model.GetOrCreate(); + IntegerEncoder* encoder = model.GetOrCreate(); + const IntegerVariable var = model.Add( + NewIntegerVariable(Domain::FromIntervals({{3, 4}, {7, 7}, {9, 9}}))); + + // Initial propagation is correct. + EXPECT_TRUE(sat_solver->Propagate()); + EXPECT_BOUNDS_EQ(var, 3, 9); + + // These are trivially true/false. + EXPECT_TRUE(encoder->IsFixedOrHasAssociatedLiteral( + IntegerLiteral::GreaterOrEqual(var, 2))); + EXPECT_TRUE(encoder->IsFixedOrHasAssociatedLiteral( + IntegerLiteral::GreaterOrEqual(var, 3))); + EXPECT_TRUE(encoder->IsFixedOrHasAssociatedLiteral( + IntegerLiteral::GreaterOrEqual(var, 10))); + + // Not other encoding currently. + EXPECT_FALSE(encoder->IsFixedOrHasAssociatedLiteral( + IntegerLiteral::GreaterOrEqual(var, 4))); + EXPECT_FALSE(encoder->IsFixedOrHasAssociatedLiteral( + IntegerLiteral::GreaterOrEqual(var, 9))); + + // Add one encoding and test. + encoder->GetOrCreateAssociatedLiteral(IntegerLiteral::GreaterOrEqual(var, 7)); + EXPECT_TRUE(encoder->IsFixedOrHasAssociatedLiteral( + IntegerLiteral::GreaterOrEqual(var, 5))); + EXPECT_TRUE(encoder->IsFixedOrHasAssociatedLiteral( + IntegerLiteral::GreaterOrEqual(var, 7))); + EXPECT_TRUE(encoder->IsFixedOrHasAssociatedLiteral( + IntegerLiteral::LowerOrEqual(var, 6))); + EXPECT_TRUE(encoder->IsFixedOrHasAssociatedLiteral( + IntegerLiteral::LowerOrEqual(var, 4))); +} + +TEST(IntegerEncoderTest, EncodingOfConstantVariableHasSizeOne) { + Model model; + IntegerEncoder* encoder = model.GetOrCreate(); + const IntegerVariable var = model.Add(NewIntegerVariable(7, 7)); + model.Add(FullyEncodeVariable(var)); + const auto& encoding = encoder->FullDomainEncoding(var); + EXPECT_EQ(encoding.size(), 1); + EXPECT_TRUE(model.GetOrCreate()->Assignment().LiteralIsTrue( + encoding[0].literal)); +} + +TEST(IntegerEncoderTest, IntegerVariableOfAssignedLiteralIsFixed) { + Model model; + SatSolver* sat_solver = model.GetOrCreate(); + + { + Literal literal_false = Literal(sat_solver->NewBooleanVariable(), true); + CHECK(sat_solver->AddUnitClause(literal_false.Negated())); + const IntegerVariable zero = + model.Add(NewIntegerVariableFromLiteral(literal_false)); + EXPECT_EQ(model.Get(UpperBound(zero)), 0); + } + + { + Literal literal_true = Literal(sat_solver->NewBooleanVariable(), true); + CHECK(sat_solver->AddUnitClause(literal_true)); + const IntegerVariable one = + model.Add(NewIntegerVariableFromLiteral(literal_true)); + EXPECT_EQ(model.Get(LowerBound(one)), 1); + } +} + +TEST(IntegerEncoderTest, LiteralView1) { + Model model; + IntegerEncoder* encoder = model.GetOrCreate(); + const IntegerVariable var = model.Add(NewIntegerVariable(0, 1)); + const Literal literal(model.Add(NewBooleanVariable()), true); + encoder->AssociateToIntegerEqualValue(literal, var, IntegerValue(1)); + EXPECT_EQ(var, encoder->GetLiteralView(literal)); + EXPECT_EQ(kNoIntegerVariable, encoder->GetLiteralView(literal.Negated())); +} + +TEST(IntegerEncoderTest, LiteralView2) { + Model model; + IntegerEncoder* encoder = model.GetOrCreate(); + const IntegerVariable var = model.Add(NewIntegerVariable(0, 1)); + const Literal literal(model.Add(NewBooleanVariable()), true); + encoder->AssociateToIntegerEqualValue(literal, var, IntegerValue(0)); + EXPECT_EQ(kNoIntegerVariable, encoder->GetLiteralView(literal)); + EXPECT_EQ(var, encoder->GetLiteralView(literal.Negated())); +} + +TEST(IntegerEncoderTest, LiteralView3) { + Model model; + IntegerEncoder* encoder = model.GetOrCreate(); + const IntegerVariable var = model.Add(NewIntegerVariable(0, 1)); + const Literal literal(model.Add(NewBooleanVariable()), true); + encoder->AssociateToIntegerLiteral( + literal, IntegerLiteral::GreaterOrEqual(var, IntegerValue(1))); + EXPECT_EQ(var, encoder->GetLiteralView(literal)); + EXPECT_EQ(kNoIntegerVariable, encoder->GetLiteralView(literal.Negated())); +} + +TEST(IntegerEncoderTest, LiteralView4) { + Model model; + IntegerEncoder* encoder = model.GetOrCreate(); + const IntegerVariable var = model.Add(NewIntegerVariable(0, 1)); + const Literal literal(model.Add(NewBooleanVariable()), true); + encoder->AssociateToIntegerLiteral( + literal, IntegerLiteral::LowerOrEqual(var, IntegerValue(0))); + EXPECT_EQ(kNoIntegerVariable, encoder->GetLiteralView(literal)); + EXPECT_EQ(var, encoder->GetLiteralView(literal.Negated())); +} + +TEST(IntegerEncoderTest, IssueWhenNotFullyingPropagatingAtLoading) { + Model model; + auto* integer_trail = model.GetOrCreate(); + auto* integer_encoder = model.GetOrCreate(); + const IntegerVariable var = + integer_trail->AddIntegerVariable(Domain::FromValues({0, 3, 7, 9})); + const Literal false_literal = integer_encoder->GetFalseLiteral(); + integer_encoder->DisableImplicationBetweenLiteral(); + + // This currently doesn't propagate the domain. + integer_encoder->AssociateToIntegerLiteral( + false_literal, IntegerLiteral::GreaterOrEqual(var, IntegerValue(5))); + EXPECT_EQ(integer_trail->LowerBound(var), 0); + EXPECT_EQ(integer_trail->UpperBound(var), 9); + + // And that used to fail because it does some domain propagation when it + // detect that some value cannot be there and update the domains of var while + // iterating over it. + integer_encoder->FullyEncodeVariable(var); +} + +#undef EXPECT_BOUNDS_EQ + +TEST(SolveIntegerProblemWithLazyEncodingTest, Sat) { + static const int kNumVariables = 10; + Model model; + std::vector integer_vars; + for (int i = 0; i < kNumVariables; ++i) { + integer_vars.push_back(model.Add(NewIntegerVariable(0, 10))); + } + model.GetOrCreate()->fixed_search = + FirstUnassignedVarAtItsMinHeuristic(integer_vars, &model); + ConfigureSearchHeuristics(&model); + ASSERT_EQ(model.GetOrCreate()->SolveIntegerProblem(), + SatSolver::Status::FEASIBLE); + for (const IntegerVariable var : integer_vars) { + EXPECT_EQ(model.Get(LowerBound(var)), model.Get(UpperBound(var))); + } +} + +TEST(SolveIntegerProblemWithLazyEncodingTest, Unsat) { + Model model; + const IntegerVariable var = model.Add(NewIntegerVariable(-100, 100)); + model.Add(LowerOrEqual(var, -10)); + model.Add(GreaterOrEqual(var, 10)); + model.GetOrCreate()->fixed_search = + FirstUnassignedVarAtItsMinHeuristic({var}, &model); + ConfigureSearchHeuristics(&model); + EXPECT_EQ(model.GetOrCreate()->SolveIntegerProblem(), + SatSolver::Status::INFEASIBLE); +} + +TEST(IntegerTrailTest, InitialVariableDomainIsUpdated) { + Model model; + IntegerTrail* integer_trail = model.GetOrCreate(); + const IntegerVariable var = + integer_trail->AddIntegerVariable(IntegerValue(0), IntegerValue(1000)); + EXPECT_EQ(integer_trail->InitialVariableDomain(var), Domain(0, 1000)); + EXPECT_EQ(integer_trail->InitialVariableDomain(NegationOf(var)), + Domain(-1000, 0)); + + EXPECT_TRUE(integer_trail->Enqueue( + IntegerLiteral::GreaterOrEqual(var, IntegerValue(7)), {}, {})); + EXPECT_EQ(integer_trail->InitialVariableDomain(var), Domain(7, 1000)); + EXPECT_EQ(integer_trail->InitialVariableDomain(NegationOf(var)), + Domain(-1000, -7)); +} + +TEST(IntegerTrailTest, AppendNewBounds) { + Model model; + const Literal l(model.Add(NewBooleanVariable()), true); + const IntegerVariable var(model.Add(NewIntegerVariable(0, 100))); + + // So that there is a decision. + EXPECT_TRUE( + model.GetOrCreate()->EnqueueDecisionIfNotConflicting(l)); + + // Enqueue a bunch of fact. + IntegerTrail* integer_trail = model.GetOrCreate(); + EXPECT_TRUE(integer_trail->Enqueue( + IntegerLiteral::GreaterOrEqual(var, IntegerValue(2)), {l.Negated()}, {})); + EXPECT_TRUE(integer_trail->Enqueue( + IntegerLiteral::GreaterOrEqual(var, IntegerValue(4)), {l.Negated()}, {})); + EXPECT_TRUE(integer_trail->Enqueue( + IntegerLiteral::GreaterOrEqual(var, IntegerValue(8)), {l.Negated()}, {})); + EXPECT_TRUE(integer_trail->Enqueue( + IntegerLiteral::GreaterOrEqual(var, IntegerValue(9)), {l.Negated()}, {})); + + // Only the last bound should be present. + std::vector bounds; + integer_trail->AppendNewBounds(&bounds); + EXPECT_THAT(bounds, ElementsAre(IntegerLiteral::GreaterOrEqual( + var, IntegerValue(9)))); +} + +TEST(FastDivisionTest, AllPossibleValues) { + for (int i = 1; i <= std::numeric_limits::max(); ++i) { + const QuickSmallDivision div(i); + for (int j = 0; j <= std::numeric_limits::max(); ++j) { + const uint16_t result = div.DivideByDivisor(j); + const uint16_t j_rounded_to_lowest_multiple = result * i; + CHECK_LE(j_rounded_to_lowest_multiple, j); + CHECK_GT(j_rounded_to_lowest_multiple + i, j); + } + } +} + +static void BM_FloorRatio(benchmark::State& state) { + IntegerValue divisor(654676436498); + IntegerValue dividend(45454655155444); + IntegerValue test(0); + for (auto _ : state) { + dividend++; + divisor++; + benchmark::DoNotOptimize(test += FloorRatio(dividend, divisor)); + } + state.SetBytesProcessed(static_cast(state.iterations())); +} + +static void BM_PositiveRemainder(benchmark::State& state) { + IntegerValue divisor(654676436498); + IntegerValue dividend(45454655155444); + IntegerValue test(0); + for (auto _ : state) { + dividend++; + divisor++; + benchmark::DoNotOptimize(test += PositiveRemainder(dividend, divisor)); + } + state.SetBytesProcessed(static_cast(state.iterations())); +} + +static void BM_PositiveRemainderAlternative(benchmark::State& state) { + IntegerValue divisor(654676436498); + IntegerValue dividend(45454655155444); + IntegerValue test(0); + for (auto _ : state) { + dividend++; + divisor++; + benchmark::DoNotOptimize(test += dividend - + divisor * FloorRatio(dividend, divisor)); + } + state.SetBytesProcessed(static_cast(state.iterations())); +} + +// What we use in the code. This is safe of integer overflow. The compiler +// should also do a single integer division to get the quotient and remainder. +static void BM_DivisionAndRemainder(benchmark::State& state) { + IntegerValue divisor(654676436498); + IntegerValue dividend(45454655155444); + IntegerValue test(0); + for (auto _ : state) { + dividend++; + divisor++; + benchmark::DoNotOptimize(test += FloorRatio(dividend, divisor)); + benchmark::DoNotOptimize(test += PositiveRemainder(dividend, divisor)); + } + state.SetBytesProcessed(static_cast(state.iterations())); +} + +// An alternative version, note however that divisor * f might overflow! +static void BM_DivisionAndRemainderAlternative(benchmark::State& state) { + IntegerValue divisor(654676436498); + IntegerValue dividend(45454655155444); + IntegerValue test(0); + for (auto _ : state) { + dividend++; + divisor++; + const IntegerValue f = FloorRatio(dividend, divisor); + benchmark::DoNotOptimize(test += f); + benchmark::DoNotOptimize(test += dividend - divisor * f); + } + state.SetBytesProcessed(static_cast(state.iterations())); +} + +// The best we can hope for ? +static void BM_DivisionAndRemainderBaseline(benchmark::State& state) { + IntegerValue divisor(654676436498); + IntegerValue dividend(45454655155444); + IntegerValue test(0); + for (auto _ : state) { + dividend++; + divisor++; + benchmark::DoNotOptimize(test += dividend / divisor); + benchmark::DoNotOptimize(test += dividend % divisor); + } + state.SetBytesProcessed(static_cast(state.iterations())); +} + +BENCHMARK(BM_FloorRatio); +BENCHMARK(BM_PositiveRemainder); +BENCHMARK(BM_PositiveRemainderAlternative); +BENCHMARK(BM_DivisionAndRemainder); +BENCHMARK(BM_DivisionAndRemainderAlternative); +BENCHMARK(BM_DivisionAndRemainderBaseline); + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/intervals_test.cc b/ortools/sat/intervals_test.cc new file mode 100644 index 0000000000..ab2ead8f90 --- /dev/null +++ b/ortools/sat/intervals_test.cc @@ -0,0 +1,278 @@ +// 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/intervals.h" + +#include + +#include +#include + +#include "gtest/gtest.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_solver.h" +#include "ortools/util/strong_integers.h" + +namespace operations_research { +namespace sat { +namespace { + +TEST(IntervalsRepositoryTest, Precedences) { + Model model; + const AffineExpression start1(model.Add(NewIntegerVariable(0, 10))); + const AffineExpression size1(model.Add(NewIntegerVariable(2, 10))); + const AffineExpression end1(model.Add(NewIntegerVariable(0, 10))); + const AffineExpression start2(model.Add(NewIntegerVariable(0, 10))); + const AffineExpression size2(model.Add(NewIntegerVariable(2, 10))); + const AffineExpression end2(model.Add(NewIntegerVariable(0, 10))); + + auto* repo = model.GetOrCreate(); + const IntervalVariable a = repo->CreateInterval(start1, end1, size1); + const IntervalVariable b = repo->CreateInterval(start2, end2, size2); + + // Ok to call many times. + repo->CreateDisjunctivePrecedenceLiteral(a, b); + repo->CreateDisjunctivePrecedenceLiteral(a, b); + + EXPECT_NE(kNoLiteralIndex, repo->GetPrecedenceLiteral(a, b)); + EXPECT_EQ(Literal(repo->GetPrecedenceLiteral(a, b)), + Literal(repo->GetPrecedenceLiteral(b, a)).Negated()); +} + +TEST(SchedulingConstraintHelperTest, PushConstantBoundWithOptionalIntervals) { + Model model; + auto* repo = model.GetOrCreate(); + + const AffineExpression start(IntegerValue(0)); + const AffineExpression size(IntegerValue(10)); + const AffineExpression end(IntegerValue(10)); + + Literal presence2 = Literal(model.Add(NewBooleanVariable()), true); + IntervalVariable inter1 = + repo->CreateInterval(start, end, size, kNoLiteralIndex, false); + IntervalVariable inter2 = + repo->CreateInterval(start, end, size, presence2.Index(), false); + + SchedulingConstraintHelper helper({inter1, inter2}, &model); + + EXPECT_TRUE(helper.IncreaseStartMin(1, IntegerValue(20))); + EXPECT_FALSE(model.Get(Value(presence2))); +} + +TEST(SchedulingDemandHelperTest, EnergyInWindow) { + Model model; + + const AffineExpression start(model.Add(NewIntegerVariable(0, 10))); + const AffineExpression size(model.Add(NewIntegerVariable(2, 10))); + const AffineExpression end(model.Add(NewIntegerVariable(0, 10))); + const IntervalVariable inter = + model.GetOrCreate()->CreateInterval( + start, end, size, kNoLiteralIndex, false); + + const AffineExpression demand(model.Add(NewIntegerVariable(2, 10))); + + SchedulingConstraintHelper helper({inter}, &model); + SchedulingDemandHelper demands_helper({demand}, &helper, &model); + demands_helper.CacheAllEnergyValues(); + EXPECT_EQ(demands_helper.EnergyMin(0), IntegerValue(4)); + + const Literal alt1 = Literal(model.Add(NewBooleanVariable()), true); + const Literal alt2 = Literal(model.Add(NewBooleanVariable()), true); + demands_helper.OverrideDecomposedEnergies( + {{{alt1, IntegerValue(2), IntegerValue(4)}, + {alt2, IntegerValue(4), IntegerValue(2)}}}); + demands_helper.CacheAllEnergyValues(); + EXPECT_EQ(demands_helper.EnergyMin(0), IntegerValue(8)); + + EXPECT_EQ(0, demands_helper.EnergyMinInWindow(0, 8, 2)); + EXPECT_EQ(8, demands_helper.EnergyMinInWindow(0, 0, 10)); + EXPECT_EQ(0, demands_helper.EnergyMinInWindow(0, 2, 10)); + EXPECT_EQ(0, demands_helper.EnergyMinInWindow(0, 0, 8)); + EXPECT_EQ(4, demands_helper.EnergyMinInWindow(0, 0, 9)); +} + +TEST(SchedulingDemandHelperTest, EnergyInWindowTakeIntoAccountWindowSize) { + Model model; + + const AffineExpression start(model.Add(NewIntegerVariable(0, 4))); + const AffineExpression size(model.Add(NewIntegerVariable(6, 10))); + const AffineExpression end(model.Add(NewIntegerVariable(0, 10))); + const IntervalVariable inter = + model.GetOrCreate()->CreateInterval( + start, end, size, kNoLiteralIndex, false); + + const AffineExpression demand(model.Add(NewIntegerVariable(6, 10))); + + SchedulingConstraintHelper helper({inter}, &model); + SchedulingDemandHelper demands_helper({demand}, &helper, &model); + demands_helper.CacheAllEnergyValues(); + + const Literal alt1 = Literal(model.Add(NewBooleanVariable()), true); + const Literal alt2 = Literal(model.Add(NewBooleanVariable()), true); + demands_helper.OverrideDecomposedEnergies( + {{{alt1, IntegerValue(8), IntegerValue(6)}, + {alt2, IntegerValue(6), IntegerValue(8)}}}); + demands_helper.CacheAllEnergyValues(); + EXPECT_EQ(demands_helper.EnergyMin(0), IntegerValue(48)); + + EXPECT_EQ(6, demands_helper.EnergyMinInWindow(0, 5, 6)); +} + +TEST(SchedulingDemandHelperTest, LinearizedDemandWithAffineExpression) { + Model model; + + const AffineExpression start(model.Add(NewIntegerVariable(0, 10))); + const AffineExpression size(model.Add(NewIntegerVariable(2, 10))); + const AffineExpression end(model.Add(NewIntegerVariable(0, 10))); + const IntervalVariable inter = + model.GetOrCreate()->CreateInterval( + start, end, size, kNoLiteralIndex, false); + + const AffineExpression demand( + AffineExpression(model.Add(NewIntegerVariable(2, 10)), 2, 5)); + + SchedulingConstraintHelper helper({inter}, &model); + SchedulingDemandHelper demands_helper({demand}, &helper, &model); + demands_helper.CacheAllEnergyValues(); + + LinearConstraintBuilder builder(&model); + ASSERT_TRUE(demands_helper.AddLinearizedDemand(0, &builder)); + EXPECT_EQ(builder.BuildExpression().DebugString(), "2*X3 + 5"); +} + +TEST(SchedulingDemandHelperTest, LinearizedDemandWithDecomposedEnergy) { + Model model; + + const AffineExpression start(model.Add(NewIntegerVariable(0, 10))); + const AffineExpression size(model.Add(NewIntegerVariable(2, 10))); + const AffineExpression end(model.Add(NewIntegerVariable(0, 10))); + const IntervalVariable inter = + model.GetOrCreate()->CreateInterval( + start, end, size, kNoLiteralIndex, false); + + const AffineExpression demand(model.Add(NewIntegerVariable(2, 10))); + + SchedulingConstraintHelper helper({inter}, &model); + SchedulingDemandHelper demands_helper({demand}, &helper, &model); + demands_helper.CacheAllEnergyValues(); + EXPECT_EQ(demands_helper.EnergyMin(0), IntegerValue(4)); + + const Literal alt1 = Literal(model.Add(NewBooleanVariable()), true); + const IntegerVariable var1(model.Add(NewIntegerVariable(0, 1))); + model.GetOrCreate()->AssociateToIntegerEqualValue( + alt1, var1, IntegerValue(1)); + + const Literal alt2 = Literal(model.Add(NewBooleanVariable()), true); + const IntegerVariable var2(model.Add(NewIntegerVariable(0, 1))); + model.GetOrCreate()->AssociateToIntegerEqualValue( + alt2, var2, IntegerValue(1)); + demands_helper.OverrideDecomposedEnergies( + {{{alt1, IntegerValue(2), IntegerValue(4)}, + {alt2, IntegerValue(4), IntegerValue(2)}}}); + demands_helper.CacheAllEnergyValues(); + LinearConstraintBuilder builder(&model); + ASSERT_TRUE(demands_helper.AddLinearizedDemand(0, &builder)); + EXPECT_EQ(builder.BuildExpression().DebugString(), "4*X4 2*X5"); +} + +TEST(SchedulingDemandHelperTest, FilteredDecomposedEnergy) { + Model model; + SatSolver* sat_solver = model.GetOrCreate(); + IntegerEncoder* encoder = model.GetOrCreate(); + + const AffineExpression start(model.Add(NewIntegerVariable(0, 10))); + const AffineExpression size(model.Add(NewIntegerVariable(2, 10))); + const AffineExpression end(model.Add(NewIntegerVariable(0, 10))); + const IntervalVariable inter = + model.GetOrCreate()->CreateInterval( + start, end, size, kNoLiteralIndex, false); + + const AffineExpression demand(model.Add(NewIntegerVariable(2, 10))); + + SchedulingConstraintHelper helper({inter}, &model); + SchedulingDemandHelper demands_helper({demand}, &helper, &model); + demands_helper.CacheAllEnergyValues(); + EXPECT_EQ(demands_helper.EnergyMin(0), IntegerValue(4)); + + const std::vector no_energy; + EXPECT_EQ(demands_helper.FilteredDecomposedEnergy(0), no_energy); + + const Literal alt1 = Literal(model.Add(NewBooleanVariable()), true); + const IntegerVariable var1(model.Add(NewIntegerVariable(0, 1))); + encoder->AssociateToIntegerEqualValue(alt1, var1, IntegerValue(1)); + + const Literal alt2 = Literal(model.Add(NewBooleanVariable()), true); + const IntegerVariable var2(model.Add(NewIntegerVariable(0, 1))); + encoder->AssociateToIntegerEqualValue(alt2, var2, IntegerValue(1)); + const std::vector energy = { + {alt1, IntegerValue(2), IntegerValue(4)}, + {alt2, IntegerValue(4), IntegerValue(2)}}; + demands_helper.OverrideDecomposedEnergies({energy}); + demands_helper.CacheAllEnergyValues(); + EXPECT_EQ(demands_helper.FilteredDecomposedEnergy(0), energy); + + EXPECT_EQ(sat_solver->EnqueueDecisionAndBackjumpOnConflict(alt1.Negated()), + 0); + const std::vector filtered_energy = { + {alt2, IntegerValue(4), IntegerValue(2)}}; + EXPECT_EQ(demands_helper.FilteredDecomposedEnergy(0), filtered_energy); + EXPECT_EQ(demands_helper.DecomposedEnergies()[0], energy); +} + +TEST(SchedulingDemandHelperTest, FilteredDecomposedEnergyWithFalseLiteral) { + Model model; + IntegerEncoder* encoder = model.GetOrCreate(); + + const AffineExpression start(model.Add(NewIntegerVariable(0, 10))); + const AffineExpression size(model.Add(NewIntegerVariable(2, 10))); + const AffineExpression end(model.Add(NewIntegerVariable(0, 10))); + const IntervalVariable inter = + model.GetOrCreate()->CreateInterval( + start, end, size, kNoLiteralIndex, false); + + const AffineExpression demand(model.Add(NewIntegerVariable(2, 10))); + + SchedulingConstraintHelper helper({inter}, &model); + SchedulingDemandHelper demands_helper({demand}, &helper, &model); + demands_helper.CacheAllEnergyValues(); + EXPECT_EQ(demands_helper.EnergyMin(0), IntegerValue(4)); + + const std::vector no_energy; + EXPECT_EQ(demands_helper.FilteredDecomposedEnergy(0), no_energy); + + const Literal alt1 = encoder->GetFalseLiteral(); + const IntegerVariable var1(model.Add(NewIntegerVariable(0, 1))); + model.GetOrCreate()->AssociateToIntegerEqualValue( + alt1, var1, IntegerValue(1)); + + const Literal alt2 = Literal(model.Add(NewBooleanVariable()), true); + const IntegerVariable var2(model.Add(NewIntegerVariable(0, 1))); + encoder->AssociateToIntegerEqualValue(alt2, var2, IntegerValue(1)); + const std::vector energy = { + {alt1, IntegerValue(2), IntegerValue(4)}, + {alt2, IntegerValue(4), IntegerValue(2)}}; + demands_helper.OverrideDecomposedEnergies({energy}); + demands_helper.CacheAllEnergyValues(); + const std::vector filtered_energy = { + {alt2, IntegerValue(4), IntegerValue(2)}}; + EXPECT_EQ(demands_helper.DecomposedEnergies()[0], filtered_energy); + EXPECT_EQ(demands_helper.FilteredDecomposedEnergy(0), filtered_energy); + EXPECT_EQ(0, model.GetOrCreate()->CurrentDecisionLevel()); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/linear_constraint_manager_test.cc b/ortools/sat/linear_constraint_manager_test.cc new file mode 100644 index 0000000000..e842599ca5 --- /dev/null +++ b/ortools/sat/linear_constraint_manager_test.cc @@ -0,0 +1,421 @@ +// 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_constraint_manager.h" + +#include +#include +#include + +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/strong_vector.h" +#include "ortools/glop/variables_info.h" +#include "ortools/lp_data/lp_types.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/util/strong_integers.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::testing::ElementsAre; +using ::testing::EndsWith; +using ::testing::StartsWith; +using ::testing::UnorderedElementsAre; +using ConstraintIndex = LinearConstraintManager::ConstraintIndex; + +TEST(LinearConstraintManagerTest, DuplicateDetection) { + Model model; + LinearConstraintManager manager(&model); + const IntegerVariable x = model.Add(NewIntegerVariable(-10, 10)); + + LinearConstraintBuilder ct_one(IntegerValue(0), IntegerValue(10)); + ct_one.AddTerm(x, IntegerValue(2)); + manager.Add(ct_one.Build()); + + LinearConstraintBuilder ct_two(IntegerValue(-4), IntegerValue(6)); + ct_two.AddTerm(NegationOf(x), IntegerValue(-2)); + manager.Add(ct_two.Build()); + + EXPECT_EQ(manager.AllConstraints().size(), 1); + EXPECT_EQ(manager.AllConstraints().front().constraint.DebugString(), + "0 <= 1*X0 <= 3"); +} + +void SetLpValue(IntegerVariable v, double value, Model* model) { + auto& values = *model->GetOrCreate(); + const int needed_size = 1 + std::max(v.value(), NegationOf(v).value()); + if (needed_size > values.size()) values.resize(needed_size, 0.0); + values[v] = value; + values[NegationOf(v)] = -value; +} + +TEST(LinearConstraintManagerTest, DuplicateDetectionCuts) { + Model model; + LinearConstraintManager manager(&model); + const IntegerVariable x = model.Add(NewIntegerVariable(-10, 10)); + SetLpValue(x, -4.0, &model); + + LinearConstraintBuilder ct_one(IntegerValue(0), IntegerValue(10)); + ct_one.AddTerm(x, IntegerValue(2)); + manager.AddCut(ct_one.Build(), "Cut"); + + LinearConstraintBuilder ct_two(IntegerValue(-4), IntegerValue(6)); + ct_two.AddTerm(NegationOf(x), IntegerValue(-2)); + manager.AddCut(ct_two.Build(), "Cut"); + + // The second cut is more restrictive so it counts. + EXPECT_EQ(manager.num_cuts(), 2); + + EXPECT_EQ(manager.AllConstraints().size(), 1); + EXPECT_EQ(manager.AllConstraints().front().constraint.DebugString(), + "0 <= 1*X0 <= 3"); +} + +TEST(LinearConstraintManagerTest, DuplicateDetectionCauseLpChange) { + Model model; + LinearConstraintManager manager(&model); + const IntegerVariable x = model.Add(NewIntegerVariable(-10, 10)); + SetLpValue(x, 0.0, &model); + + LinearConstraintBuilder ct_one(IntegerValue(0), IntegerValue(10)); + ct_one.AddTerm(x, IntegerValue(2)); + manager.Add(ct_one.Build()); + + manager.AddAllConstraintsToLp(); + EXPECT_THAT(manager.LpConstraints(), + UnorderedElementsAre(ConstraintIndex(0))); + glop::BasisState state; + state.statuses.resize(glop::ColIndex(1)); + EXPECT_FALSE(manager.ChangeLp(&state)); + + // Adding the second constraint will cause a bound change, so ChangeLp() will + // returns true even if the constraint is satisfied. + LinearConstraintBuilder ct_two(IntegerValue(-4), IntegerValue(6)); + ct_two.AddTerm(x, IntegerValue(2)); + manager.Add(ct_two.Build()); + EXPECT_TRUE(manager.ChangeLp(&state)); + + EXPECT_EQ(manager.AllConstraints().size(), 1); + EXPECT_EQ(manager.AllConstraints().front().constraint.DebugString(), + "0 <= 1*X0 <= 3"); +} + +TEST(LinearConstraintManagerTest, OnlyAddInfeasibleConstraints) { + Model model; + LinearConstraintManager manager(&model); + const IntegerVariable x = model.Add(NewIntegerVariable(-10, 10)); + const IntegerVariable y = model.Add(NewIntegerVariable(-10, 10)); + SetLpValue(x, 0.0, &model); + SetLpValue(y, 0.0, &model); + + LinearConstraintBuilder ct_one(IntegerValue(0), IntegerValue(10)); + ct_one.AddTerm(x, IntegerValue(2)); + ct_one.AddTerm(y, IntegerValue(3)); + manager.Add(ct_one.Build()); + + LinearConstraintBuilder ct_two(IntegerValue(-4), IntegerValue(6)); + ct_two.AddTerm(x, IntegerValue(3)); + ct_one.AddTerm(y, IntegerValue(2)); + manager.Add(ct_two.Build()); + + EXPECT_TRUE(manager.LpConstraints().empty()); + EXPECT_EQ(manager.AllConstraints().size(), 2); + + // All constraints satisfy this, so no change. + glop::BasisState state; + state.statuses.resize(glop::ColIndex(2)); // Content is not relevant. + EXPECT_FALSE(manager.ChangeLp(&state)); + EXPECT_FALSE(manager.ChangeLp(&state)); + + SetLpValue(x, -1.0, &model); + EXPECT_TRUE(manager.ChangeLp(&state)); + EXPECT_THAT(manager.LpConstraints(), + UnorderedElementsAre(ConstraintIndex(0))); + EXPECT_EQ(state.statuses.size(), glop::ColIndex(3)); // State was resized. + EXPECT_EQ(state.statuses[glop::ColIndex(2)], glop::VariableStatus::BASIC); + + // Note that we keep the first constraint even if the value of 4.0 make it + // satisfied. + SetLpValue(x, 4.0, &model); + EXPECT_TRUE(manager.ChangeLp(&state)); + EXPECT_THAT(manager.LpConstraints(), + UnorderedElementsAre(ConstraintIndex(0), ConstraintIndex(1))); + EXPECT_EQ(state.statuses.size(), glop::ColIndex(4)); // State was resized. + EXPECT_EQ(state.statuses[glop::ColIndex(3)], glop::VariableStatus::BASIC); +} + +TEST(LinearConstraintManagerTest, OnlyAddOrthogonalConstraints) { + Model model; + model.GetOrCreate()->set_min_orthogonality_for_lp_constraints( + 0.8); + LinearConstraintManager manager(&model); + const IntegerVariable x = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable y = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable z = model.Add(NewIntegerVariable(0, 10)); + SetLpValue(x, 1.0, &model); + SetLpValue(y, 1.0, &model); + SetLpValue(z, 1.0, &model); + + LinearConstraintBuilder ct_one(IntegerValue(0), IntegerValue(11)); + ct_one.AddTerm(x, IntegerValue(3)); + ct_one.AddTerm(y, IntegerValue(-4)); + manager.Add(ct_one.Build()); + + LinearConstraintBuilder ct_two(IntegerValue(-4), IntegerValue(2)); + ct_two.AddTerm(z, IntegerValue(-5)); + manager.Add(ct_two.Build()); + + LinearConstraintBuilder ct_three(IntegerValue(0), IntegerValue(14)); + ct_three.AddTerm(x, IntegerValue(5)); + ct_three.AddTerm(y, IntegerValue(5)); + ct_three.AddTerm(z, IntegerValue(5)); + manager.Add(ct_three.Build()); + + EXPECT_TRUE(manager.LpConstraints().empty()); + EXPECT_EQ(manager.AllConstraints().size(), 3); + + // First Call. Last constraint does not satisfy the orthogonality criteria. + glop::BasisState state; + EXPECT_TRUE(manager.ChangeLp(&state)); + EXPECT_THAT(manager.LpConstraints(), + UnorderedElementsAre(ConstraintIndex(0), ConstraintIndex(1))); + + // Second Call. Only the last constraint is considered. The other two + // constraints are already added. + EXPECT_TRUE(manager.ChangeLp(&state)); + EXPECT_THAT(manager.LpConstraints(), + UnorderedElementsAre(ConstraintIndex(0), ConstraintIndex(1), + ConstraintIndex(2))); +} + +TEST(LinearConstraintManagerTest, RemoveIneffectiveCuts) { + Model model; + model.GetOrCreate()->set_max_consecutive_inactive_count(0); + + LinearConstraintManager manager(&model); + const IntegerVariable x = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable y = model.Add(NewIntegerVariable(0, 10)); + SetLpValue(x, 1.0, &model); + SetLpValue(y, 1.0, &model); + + LinearConstraintBuilder ct_one(IntegerValue(0), IntegerValue(11)); + ct_one.AddTerm(x, IntegerValue(3)); + ct_one.AddTerm(y, IntegerValue(-4)); + manager.AddCut(ct_one.Build(), "Cut"); + + EXPECT_TRUE(manager.LpConstraints().empty()); + EXPECT_EQ(manager.AllConstraints().size(), 1); + + // First Call. The constraint is added to LP. + glop::BasisState state; + EXPECT_TRUE(manager.ChangeLp(&state)); + EXPECT_THAT(manager.LpConstraints(), + UnorderedElementsAre(ConstraintIndex(0))); + + // Second Call. Constraint is inactive and hence removed. + state.statuses.resize(glop::ColIndex(2 + manager.LpConstraints().size())); + state.statuses[glop::ColIndex(2)] = glop::VariableStatus::BASIC; + EXPECT_TRUE(manager.ChangeLp(&state)); + EXPECT_TRUE(manager.LpConstraints().empty()); + EXPECT_EQ(state.statuses.size(), glop::ColIndex(2)); +} + +TEST(LinearConstraintManagerTest, ObjectiveParallelism) { + Model model; + LinearConstraintManager manager(&model); + const IntegerVariable x = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable y = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable z = model.Add(NewIntegerVariable(0, 10)); + SetLpValue(x, 1.0, &model); + SetLpValue(y, 1.0, &model); + SetLpValue(z, 1.0, &model); + + manager.SetObjectiveCoefficient(x, IntegerValue(1)); + manager.SetObjectiveCoefficient(y, IntegerValue(1)); + + LinearConstraintBuilder ct_one(IntegerValue(0), IntegerValue(0)); + ct_one.AddTerm(z, IntegerValue(-1)); + manager.Add(ct_one.Build()); + + LinearConstraintBuilder ct_two(IntegerValue(0), IntegerValue(2)); + ct_two.AddTerm(x, IntegerValue(1)); + ct_two.AddTerm(y, IntegerValue(1)); + ct_two.AddTerm(z, IntegerValue(1)); + manager.Add(ct_two.Build()); + + EXPECT_TRUE(manager.LpConstraints().empty()); + EXPECT_EQ(manager.AllConstraints().size(), 2); + + // Last constraint is more parallel to the objective. + glop::BasisState state; + EXPECT_TRUE(manager.ChangeLp(&state)); + // scores: efficacy, orthogonality, obj_para, total + // ct_one: 1, 1, 0, 2 + // ct_two: 0.5774, 1, 0.8165, 2.394 + + EXPECT_THAT(manager.LpConstraints(), + ElementsAre(ConstraintIndex(1), ConstraintIndex(0))); +} + +TEST(LinearConstraintManagerTest, SimplificationRemoveFixedVariable) { + Model model; + const IntegerVariable x = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable y = model.Add(NewIntegerVariable(0, 5)); + const IntegerVariable z = model.Add(NewIntegerVariable(0, 10)); + SetLpValue(x, 0.0, &model); + SetLpValue(y, 0.0, &model); + SetLpValue(z, 0.0, &model); + + LinearConstraintManager manager(&model); + + { + LinearConstraintBuilder ct(IntegerValue(0), IntegerValue(11)); + ct.AddTerm(x, IntegerValue(3)); + ct.AddTerm(y, IntegerValue(-4)); + ct.AddTerm(z, IntegerValue(7)); + manager.Add(ct.Build()); + } + + const LinearConstraintManager::ConstraintIndex index(0); + EXPECT_EQ("0 <= 3*X0 -4*X1 7*X2 <= 11", + manager.AllConstraints()[index].constraint.DebugString()); + + // ChangeLp will trigger the simplification. + EXPECT_TRUE(model.GetOrCreate()->Enqueue( + IntegerLiteral::GreaterOrEqual(y, IntegerValue(5)), {}, {})); + glop::BasisState state; + EXPECT_TRUE(manager.ChangeLp(&state)); + EXPECT_EQ(1, manager.num_shortened_constraints()); + EXPECT_EQ("20 <= 3*X0 7*X2 <= 31", + manager.AllConstraints()[index].constraint.DebugString()); + + // We also test that the constraint equivalence work with the change. + // Adding a constraint equiv to the new one is detected. + { + LinearConstraintBuilder ct(IntegerValue(0), IntegerValue(21)); + ct.AddTerm(x, IntegerValue(3)); + ct.AddTerm(z, IntegerValue(7)); + manager.Add(ct.Build()); + } + EXPECT_EQ(manager.AllConstraints().size(), 1); + EXPECT_EQ("20 <= 3*X0 7*X2 <= 21", + manager.AllConstraints()[index].constraint.DebugString()); +} + +TEST(LinearConstraintManagerTest, SimplificationStrenghtenUb) { + Model model; + const IntegerVariable x = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable y = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable z = model.Add(NewIntegerVariable(0, 10)); + LinearConstraintManager manager(&model); + + LinearConstraintBuilder ct(IntegerValue(-100), IntegerValue(30 + 70 - 5)); + ct.AddTerm(x, IntegerValue(3)); + ct.AddTerm(y, IntegerValue(-8)); + ct.AddTerm(z, IntegerValue(7)); + manager.Add(ct.Build()); + + const LinearConstraintManager::ConstraintIndex index(0); + EXPECT_EQ(2, manager.num_coeff_strenghtening()); + EXPECT_THAT(manager.AllConstraints()[index].constraint.DebugString(), + EndsWith("3*X0 -5*X1 5*X2 <= 75")); +} + +TEST(LinearConstraintManagerTest, SimplificationStrenghtenLb) { + Model model; + const IntegerVariable x = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable y = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable z = model.Add(NewIntegerVariable(0, 10)); + LinearConstraintManager manager(&model); + + LinearConstraintBuilder ct(IntegerValue(-75), IntegerValue(1000)); + ct.AddTerm(x, IntegerValue(3)); + ct.AddTerm(y, IntegerValue(-8)); + ct.AddTerm(z, IntegerValue(7)); + manager.Add(ct.Build()); + + const LinearConstraintManager::ConstraintIndex index(0); + EXPECT_EQ(2, manager.num_coeff_strenghtening()); + EXPECT_THAT(manager.AllConstraints()[index].constraint.DebugString(), + StartsWith("-45 <= 3*X0 -5*X1 5*X2")); +} + +TEST(LinearConstraintManagerTest, AdvancedStrenghtening1) { + Model model; + const IntegerVariable x = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable y = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable z = model.Add(NewIntegerVariable(0, 10)); + LinearConstraintManager manager(&model); + + LinearConstraintBuilder ct(IntegerValue(16), IntegerValue(1000)); + ct.AddTerm(x, IntegerValue(15)); + ct.AddTerm(y, IntegerValue(9)); + ct.AddTerm(z, IntegerValue(14)); + manager.Add(ct.Build()); + + const LinearConstraintManager::ConstraintIndex index(0); + EXPECT_EQ(3, manager.num_coeff_strenghtening()); + EXPECT_THAT(manager.AllConstraints()[index].constraint.DebugString(), + StartsWith("2 <= 1*X0 1*X1 1*X2")); +} + +TEST(LinearConstraintManagerTest, AdvancedStrenghtening2) { + Model model; + const IntegerVariable x = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable y = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable z = model.Add(NewIntegerVariable(0, 10)); + LinearConstraintManager manager(&model); + + LinearConstraintBuilder ct(IntegerValue(16), IntegerValue(1000)); + ct.AddTerm(x, IntegerValue(15)); + ct.AddTerm(y, IntegerValue(7)); + ct.AddTerm(z, IntegerValue(14)); + manager.Add(ct.Build()); + + const LinearConstraintManager::ConstraintIndex index(0); + EXPECT_EQ(2, manager.num_coeff_strenghtening()); + EXPECT_THAT(manager.AllConstraints()[index].constraint.DebugString(), + StartsWith("16 <= 9*X0 7*X1 9*X2")); +} + +TEST(LinearConstraintManagerTest, AdvancedStrenghtening3) { + Model model; + const IntegerVariable x = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable y = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable z = model.Add(NewIntegerVariable(0, 10)); + LinearConstraintManager manager(&model); + + LinearConstraintBuilder ct(IntegerValue(5), IntegerValue(1000)); + ct.AddTerm(x, IntegerValue(5)); + ct.AddTerm(y, IntegerValue(5)); + ct.AddTerm(z, IntegerValue(4)); + manager.Add(ct.Build()); + + // TODO(user): Technically, because the 5 are "enforcement" the inner + // constraint is 4*X2 >= 5 which can be rewriten and X2 >= 2, and we could + // instead have 2X0 + 2X1 + X2 >= 2 which should be tighter. + const LinearConstraintManager::ConstraintIndex index(0); + EXPECT_EQ(1, manager.num_coeff_strenghtening()); + EXPECT_THAT(manager.AllConstraints()[index].constraint.DebugString(), + StartsWith("5 <= 5*X0 5*X1 3*X2")); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/linear_constraint_test.cc b/ortools/sat/linear_constraint_test.cc new file mode 100644 index 0000000000..6c6dbc4ada --- /dev/null +++ b/ortools/sat/linear_constraint_test.cc @@ -0,0 +1,480 @@ +// 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_constraint.h" + +#include +#include +#include +#include +#include + +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/strong_vector.h" +#include "ortools/sat/integer.h" +#include "ortools/sat/model.h" +#include "ortools/sat/sat_base.h" +#include "ortools/util/strong_integers.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::testing::ElementsAre; + +TEST(ComputeActivityTest, BasicBehavior) { + // The bounds are not useful for this test. + LinearConstraintBuilder ct(IntegerValue(0), IntegerValue(0)); + + ct.AddTerm(IntegerVariable(0), IntegerValue(1)); + ct.AddTerm(IntegerVariable(2), IntegerValue(-2)); + ct.AddTerm(IntegerVariable(4), IntegerValue(3)); + + util_intops::StrongVector values = {0.5, 0.0, 1.4, + 0.0, -2.1, 0.0}; + EXPECT_EQ(ComputeActivity(ct.Build(), values), 1 * 0.5 - 2 * 1.4 - 3 * 2.1); +} + +TEST(ComputeActivityTest, EmptyConstraint) { + // The bounds are not useful for this test. + LinearConstraintBuilder ct(IntegerValue(-10), IntegerValue(10)); + util_intops::StrongVector values; + EXPECT_EQ(ComputeActivity(ct.Build(), values), 0.0); +} + +TEST(ComputeInfinityNormTest, BasicTest) { + IntegerVariable x(0); + IntegerVariable y(2); + IntegerVariable z(4); + { + LinearConstraint constraint; + EXPECT_EQ(IntegerValue(0), ComputeInfinityNorm(constraint)); + } + { + LinearConstraintBuilder constraint; + constraint.AddTerm(x, IntegerValue(3)); + constraint.AddTerm(y, IntegerValue(-4)); + constraint.AddTerm(z, IntegerValue(1)); + EXPECT_EQ(IntegerValue(4), ComputeInfinityNorm(constraint.Build())); + } + { + LinearConstraintBuilder constraint; + constraint.AddTerm(y, IntegerValue(std::numeric_limits::max())); + EXPECT_EQ(IntegerValue(std::numeric_limits::max()), + ComputeInfinityNorm(constraint.Build())); + } +} + +TEST(ComputeL2NormTest, BasicTest) { + IntegerVariable x(0); + IntegerVariable y(2); + IntegerVariable z(4); + { + LinearConstraint constraint; + EXPECT_EQ(0.0, ComputeL2Norm(constraint)); + } + { + LinearConstraintBuilder constraint; + constraint.AddTerm(x, IntegerValue(3)); + constraint.AddTerm(y, IntegerValue(-4)); + constraint.AddTerm(z, IntegerValue(12)); + EXPECT_EQ(13.0, ComputeL2Norm(constraint.Build())); + } + { + LinearConstraintBuilder constraint; + constraint.AddTerm(x, kMaxIntegerValue); + constraint.AddTerm(y, kMaxIntegerValue); + EXPECT_EQ(std::numeric_limits::infinity(), + ComputeL2Norm(constraint.Build())); + } + { + LinearConstraintBuilder constraint; + constraint.AddTerm(x, IntegerValue(1LL << 60)); + constraint.AddTerm(y, IntegerValue(1LL << 60)); + EXPECT_NEAR(1.6304772e+18, ComputeL2Norm(constraint.Build()), 1e+16); + } +} + +TEST(ScalarProductTest, BasicTest) { + IntegerVariable x(0); + IntegerVariable y(2); + IntegerVariable z(4); + + LinearConstraintBuilder ct_one(IntegerValue(0), IntegerValue(11)); + ct_one.AddTerm(x, IntegerValue(3)); + ct_one.AddTerm(y, IntegerValue(-4)); + + LinearConstraintBuilder ct_two(IntegerValue(1), IntegerValue(2)); + ct_two.AddTerm(z, IntegerValue(-1)); + + LinearConstraintBuilder ct_three(IntegerValue(0), IntegerValue(2)); + ct_three.AddTerm(x, IntegerValue(1)); + ct_three.AddTerm(y, IntegerValue(1)); + ct_three.AddTerm(z, IntegerValue(1)); + + EXPECT_EQ(0.0, ScalarProduct(ct_one.Build(), ct_two.Build())); + EXPECT_EQ(-1.0, ScalarProduct(ct_one.Build(), ct_three.Build())); + EXPECT_EQ(-1.0, ScalarProduct(ct_two.Build(), ct_three.Build())); +} + +namespace { + +// Creates an upper bounded LinearConstraintBuilder from a dense representation. +LinearConstraint CreateUbConstraintForTest( + absl::Span dense_coeffs, int64_t upper_bound) { + LinearConstraint result; + result.resize(dense_coeffs.size()); + int new_size = 0; + for (int i = 0; i < dense_coeffs.size(); ++i) { + if (dense_coeffs[i] != 0) { + result.vars[new_size] = IntegerVariable(i); + result.coeffs[new_size] = dense_coeffs[i]; + ++new_size; + } + } + result.resize(new_size); + result.lb = kMinIntegerValue; + result.ub = upper_bound; + return result; +} + +} // namespace + +TEST(DivideByGCDTest, BasicBehaviorWithoughLowerBound) { + LinearConstraint ct = CreateUbConstraintForTest({2, 4, -8}, 11); + DivideByGCD(&ct); + const LinearConstraint expected = CreateUbConstraintForTest({1, 2, -4}, 5); + EXPECT_EQ(ct, expected); +} + +TEST(DivideByGCDTest, BasicBehaviorWithLowerBound) { + LinearConstraint ct = CreateUbConstraintForTest({2, 4, -8}, 11); + ct.lb = IntegerValue(-3); + DivideByGCD(&ct); + LinearConstraint expected = CreateUbConstraintForTest({1, 2, -4}, 5); + expected.lb = IntegerValue(-1); + EXPECT_EQ(ct, expected); +} + +TEST(RemoveZeroTermsTest, BasicBehavior) { + LinearConstraint ct = CreateUbConstraintForTest({2, 4, -8}, 11); + ct.coeffs[1] = IntegerValue(0); + RemoveZeroTerms(&ct); + EXPECT_EQ(ct, CreateUbConstraintForTest({2, 0, -8}, 11)); +} + +TEST(MakeAllCoefficientsPositiveTest, BasicBehavior) { + // Note that this relies on the fact that the negation of an IntegerVariable + // var is is the one with IntegerVariable(var.value() ^ 1); + LinearConstraint ct = CreateUbConstraintForTest({-2, 0, -7, 0}, 10); + MakeAllCoefficientsPositive(&ct); + EXPECT_EQ(ct, CreateUbConstraintForTest({0, 2, 0, 7}, 10)); +} + +TEST(LinearConstraintBuilderTest, DuplicateCoefficient) { + Model model; + model.GetOrCreate(); + LinearConstraintBuilder builder(&model, kMinIntegerValue, IntegerValue(10)); + + // Note that internally, positive variable have an even index, so we only + // use those so that we don't remap a negated variable. + builder.AddTerm(IntegerVariable(0), IntegerValue(100)); + builder.AddTerm(IntegerVariable(2), IntegerValue(10)); + builder.AddTerm(IntegerVariable(4), IntegerValue(7)); + builder.AddTerm(IntegerVariable(0), IntegerValue(-10)); + builder.AddTerm(IntegerVariable(2), IntegerValue(1)); + builder.AddTerm(IntegerVariable(4), IntegerValue(-7)); + builder.AddTerm(IntegerVariable(2), IntegerValue(3)); + + EXPECT_EQ(builder.Build(), CreateUbConstraintForTest({90, 0, 14}, 10)); +} + +TEST(LinearConstraintBuilderTest, AffineExpression) { + Model model; + model.GetOrCreate(); + LinearConstraintBuilder builder(&model, kMinIntegerValue, IntegerValue(10)); + + // Note that internally, positive variable have an even index, so we only + // use those so that we don't remap a negated variable. + const IntegerVariable var(0); + builder.AddTerm(AffineExpression(var, IntegerValue(3), IntegerValue(2)), + IntegerValue(100)); + builder.AddTerm(AffineExpression(var, IntegerValue(-2), IntegerValue(1)), + IntegerValue(70)); + + // Coeff is 3*100 - 2 * 70, ub is 10 - 2*100 - 1*70 + EXPECT_EQ(builder.Build(), CreateUbConstraintForTest({160}, -260)) + << builder.Build().DebugString(); +} + +TEST(LinearConstraintBuilderTest, AddLiterals) { + Model model; + model.GetOrCreate(); + const BooleanVariable b = model.Add(NewBooleanVariable()); + const BooleanVariable c = model.Add(NewBooleanVariable()); + const BooleanVariable d = model.Add(NewBooleanVariable()); + + // Create integer views. + model.Add(NewIntegerVariableFromLiteral(Literal(b, true))); // X0 + model.Add(NewIntegerVariableFromLiteral(Literal(b, false))); // X1 + model.Add(NewIntegerVariableFromLiteral(Literal(c, false))); // X2 + model.Add(NewIntegerVariableFromLiteral(Literal(d, false))); // X3 + model.Add(NewIntegerVariableFromLiteral(Literal(d, true))); // X4 + + // When we have both view, we use the lowest IntegerVariable. + { + LinearConstraintBuilder builder(&model, kMinIntegerValue, IntegerValue(1)); + EXPECT_TRUE(builder.AddLiteralTerm(Literal(b, true), IntegerValue(1))); + EXPECT_EQ(builder.Build().DebugString(), "1*X0 <= 1"); + } + { + LinearConstraintBuilder builder(&model, kMinIntegerValue, IntegerValue(1)); + EXPECT_TRUE(builder.AddLiteralTerm(Literal(b, false), IntegerValue(1))); + EXPECT_EQ(builder.Build().DebugString(), "-1*X0 <= 0"); + } + { + LinearConstraintBuilder builder(&model, kMinIntegerValue, IntegerValue(1)); + EXPECT_TRUE(builder.AddLiteralTerm(Literal(d, true), IntegerValue(1))); + EXPECT_EQ(builder.Build().DebugString(), "-1*X3 <= 0"); + } + { + LinearConstraintBuilder builder(&model, kMinIntegerValue, IntegerValue(1)); + EXPECT_TRUE(builder.AddLiteralTerm(Literal(d, false), IntegerValue(1))); + EXPECT_EQ(builder.Build().DebugString(), "1*X3 <= 1"); + } + + // When we have just one view, we use the one we have. + { + LinearConstraintBuilder builder(&model, kMinIntegerValue, IntegerValue(1)); + EXPECT_TRUE(builder.AddLiteralTerm(Literal(c, true), IntegerValue(1))); + EXPECT_EQ(builder.Build().DebugString(), "-1*X2 <= 0"); + } + { + LinearConstraintBuilder builder(&model, kMinIntegerValue, IntegerValue(1)); + EXPECT_TRUE(builder.AddLiteralTerm(Literal(c, false), IntegerValue(1))); + EXPECT_EQ(builder.Build().DebugString(), "1*X2 <= 1"); + } +} + +TEST(LinearConstraintBuilderTest, AddConstant) { + Model model; + model.GetOrCreate(); + LinearConstraintBuilder builder1(&model, kMinIntegerValue, IntegerValue(10)); + builder1.AddTerm(IntegerVariable(0), IntegerValue(5)); + builder1.AddTerm(IntegerVariable(2), IntegerValue(10)); + builder1.AddConstant(IntegerValue(3)); + EXPECT_EQ(builder1.Build().DebugString(), "5*X0 10*X1 <= 7"); + + LinearConstraintBuilder builder2(&model, IntegerValue(4), kMaxIntegerValue); + builder2.AddTerm(IntegerVariable(0), IntegerValue(5)); + builder2.AddTerm(IntegerVariable(2), IntegerValue(10)); + builder2.AddConstant(IntegerValue(-3)); + EXPECT_EQ(builder2.Build().DebugString(), "7 <= 5*X0 10*X1"); + + LinearConstraintBuilder builder3(&model, kMinIntegerValue, IntegerValue(10)); + builder3.AddTerm(IntegerVariable(0), IntegerValue(5)); + builder3.AddTerm(IntegerVariable(2), IntegerValue(10)); + builder3.AddConstant(IntegerValue(-3)); + EXPECT_EQ(builder3.Build().DebugString(), "5*X0 10*X1 <= 13"); + + LinearConstraintBuilder builder4(&model, IntegerValue(4), kMaxIntegerValue); + builder4.AddTerm(IntegerVariable(0), IntegerValue(5)); + builder4.AddTerm(IntegerVariable(2), IntegerValue(10)); + builder4.AddConstant(IntegerValue(3)); + EXPECT_EQ(builder4.Build().DebugString(), "1 <= 5*X0 10*X1"); + + LinearConstraintBuilder builder5(&model, IntegerValue(4), IntegerValue(10)); + builder5.AddTerm(IntegerVariable(0), IntegerValue(5)); + builder5.AddTerm(IntegerVariable(2), IntegerValue(10)); + builder5.AddConstant(IntegerValue(3)); + EXPECT_EQ(builder5.Build().DebugString(), "1 <= 5*X0 10*X1 <= 7"); +} + +TEST(CleanTermsAndFillConstraintTest, VarAndItsNegation) { + std::vector> terms; + terms.push_back({IntegerVariable(4), IntegerValue(7)}); + terms.push_back({IntegerVariable(5), IntegerValue(4)}); + LinearConstraint constraint; + CleanTermsAndFillConstraint(&terms, &constraint); + EXPECT_EQ(constraint.DebugString(), "0 <= 3*X2 <= 0"); +} + +TEST(LinearConstraintBuilderTest, AddQuadraticLowerBound) { + Model model; + model.GetOrCreate(); + IntegerTrail* integer_trail = model.GetOrCreate(); + IntegerVariable x0 = model.Add(NewIntegerVariable(2, 5)); + IntegerVariable x1 = model.Add(NewIntegerVariable(3, 6)); + LinearConstraintBuilder builder1(&model, kMinIntegerValue, IntegerValue(10)); + AffineExpression a0(x0, IntegerValue(3), IntegerValue(2)); // 3 * x0 + 2. + builder1.AddQuadraticLowerBound(a0, x1, integer_trail); + EXPECT_EQ(builder1.Build().DebugString(), "9*X0 8*X1 <= 28"); +} + +TEST(LinearConstraintBuilderTest, AddQuadraticLowerBoundAffineIsVar) { + Model model; + model.GetOrCreate(); + IntegerTrail* integer_trail = model.GetOrCreate(); + IntegerVariable x0 = model.Add(NewIntegerVariable(2, 5)); + IntegerVariable x1 = model.Add(NewIntegerVariable(3, 6)); + LinearConstraintBuilder builder1(&model, kMinIntegerValue, IntegerValue(10)); + builder1.AddQuadraticLowerBound(x0, x1, integer_trail); + EXPECT_EQ(builder1.Build().DebugString(), "3*X0 2*X1 <= 16"); +} + +TEST(LinearConstraintBuilderTest, AddQuadraticLowerBoundAffineIsConstant) { + Model model; + model.GetOrCreate(); + IntegerTrail* integer_trail = model.GetOrCreate(); + IntegerVariable x0 = model.Add(NewIntegerVariable(2, 5)); + LinearConstraintBuilder builder1(&model, kMinIntegerValue, IntegerValue(10)); + builder1.AddQuadraticLowerBound(IntegerValue(4), x0, integer_trail); + EXPECT_EQ(builder1.Build().DebugString(), "4*X0 <= 10"); +} + +TEST(LinExprTest, Bounds) { + Model model; + std::vector vars{model.Add(NewIntegerVariable(1, 2)), + model.Add(NewIntegerVariable(0, 3)), + model.Add(NewIntegerVariable(-2, 4))}; + IntegerTrail* integer_trail = model.GetOrCreate(); + LinearExpression expr1; // 2x0 + 3x1 - 5 + expr1.vars = {vars[0], vars[1]}; + expr1.coeffs = {IntegerValue(2), IntegerValue(3)}; + expr1.offset = IntegerValue(-5); + expr1 = CanonicalizeExpr(expr1); + EXPECT_EQ(IntegerValue(-3), expr1.Min(*integer_trail)); + EXPECT_EQ(IntegerValue(8), expr1.Max(*integer_trail)); + + LinearExpression expr2; // 2x1 - 5x2 + 6 + expr2.vars = {vars[1], vars[2]}; + expr2.coeffs = {IntegerValue(2), IntegerValue(-5)}; + expr2.offset = IntegerValue(6); + expr2 = CanonicalizeExpr(expr2); + EXPECT_EQ(IntegerValue(-14), expr2.Min(*integer_trail)); + EXPECT_EQ(IntegerValue(22), expr2.Max(*integer_trail)); + + LinearExpression expr3; // 2x0 + 3x2 + expr3.vars = {vars[0], vars[2]}; + expr3.coeffs = {IntegerValue(2), IntegerValue(3)}; + expr3 = CanonicalizeExpr(expr3); + EXPECT_EQ(IntegerValue(-4), expr3.Min(*integer_trail)); + EXPECT_EQ(IntegerValue(16), expr3.Max(*integer_trail)); +} + +TEST(LinExprTest, Canonicalization) { + Model model; + std::vector vars{model.Add(NewIntegerVariable(1, 2)), + model.Add(NewIntegerVariable(0, 3))}; + LinearExpression expr; // 2x0 - 3x1 - 5 + expr.vars = vars; + expr.coeffs = {IntegerValue(2), IntegerValue(-3)}; + expr.offset = IntegerValue(-5); + + LinearExpression canonical_expr = CanonicalizeExpr(expr); + EXPECT_THAT(canonical_expr.vars, ElementsAre(vars[0], NegationOf(vars[1]))); + EXPECT_THAT(canonical_expr.coeffs, + ElementsAre(IntegerValue(2), IntegerValue(3))); + EXPECT_EQ(canonical_expr.offset, IntegerValue(-5)); +} + +TEST(NoDuplicateVariable, BasicBehavior) { + LinearConstraint ct; + ct.lb = kMinIntegerValue; + ct.ub = IntegerValue(10); + + ct.resize(3); + ct.num_terms = 1; + ct.vars[0] = IntegerVariable(4); + ct.coeffs[0] = IntegerValue(1); + EXPECT_TRUE(NoDuplicateVariable(ct)); + + ct.num_terms = 2; + ct.vars[1] = IntegerVariable(2); + ct.coeffs[1] = IntegerValue(5); + EXPECT_TRUE(NoDuplicateVariable(ct)); + + ct.num_terms = 3; + ct.vars[2] = IntegerVariable(4); + ct.coeffs[2] = IntegerValue(1); + EXPECT_FALSE(NoDuplicateVariable(ct)); +} + +TEST(NoDuplicateVariable, BasicBehaviorNegativeVar) { + LinearConstraint ct; + + ct.lb = kMinIntegerValue; + ct.ub = IntegerValue(10); + + ct.resize(3); + ct.num_terms = 1; + ct.vars[0] = IntegerVariable(4); + ct.coeffs[0] = IntegerValue(1); + EXPECT_TRUE(NoDuplicateVariable(ct)); + + ct.num_terms = 2; + ct.vars[1] = IntegerVariable(2); + ct.coeffs[1] = IntegerValue(5); + EXPECT_TRUE(NoDuplicateVariable(ct)); + + ct.num_terms = 3; + ct.vars[2] = IntegerVariable(5); + ct.coeffs[2] = IntegerValue(1); + EXPECT_FALSE(NoDuplicateVariable(ct)); +} + +TEST(PositiveVarExpr, BasicBehaviorNegativeVar) { + LinearExpression ct; + ct.offset = IntegerValue(10); + ct.vars.push_back(IntegerVariable(4)); + ct.coeffs.push_back(IntegerValue(1)); + + ct.vars.push_back(IntegerVariable(1)); + ct.coeffs.push_back(IntegerValue(5)); + + LinearExpression positive_var_expr = PositiveVarExpr(ct); + EXPECT_THAT(positive_var_expr.vars, + ElementsAre(ct.vars[0], NegationOf(ct.vars[1]))); + EXPECT_THAT(positive_var_expr.coeffs, + ElementsAre(ct.coeffs[0], -ct.coeffs[1])); + EXPECT_EQ(positive_var_expr.offset, ct.offset); +} + +TEST(GetCoefficient, BasicBehavior) { + LinearExpression ct; + ct.offset = IntegerValue(10); + ct.vars.push_back(IntegerVariable(4)); + ct.coeffs.push_back(IntegerValue(2)); + + EXPECT_EQ(IntegerValue(2), GetCoefficient(IntegerVariable(4), ct)); + EXPECT_EQ(IntegerValue(-2), GetCoefficient(IntegerVariable(5), ct)); + EXPECT_EQ(IntegerValue(0), GetCoefficient(IntegerVariable(2), ct)); +} + +TEST(GetCoefficientOfPositiveVar, BasicBehavior) { + LinearExpression ct; + ct.offset = IntegerValue(10); + ct.vars.push_back(IntegerVariable(4)); + ct.coeffs.push_back(IntegerValue(2)); + + EXPECT_EQ(IntegerValue(2), + GetCoefficientOfPositiveVar(IntegerVariable(4), ct)); + EXPECT_EQ(IntegerValue(0), + GetCoefficientOfPositiveVar(IntegerVariable(2), ct)); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/linear_propagation_test.cc b/ortools/sat/linear_propagation_test.cc new file mode 100644 index 0000000000..aeda248a41 --- /dev/null +++ b/ortools/sat/linear_propagation_test.cc @@ -0,0 +1,321 @@ +// 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_propagation.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/sat/integer.h" +#include "ortools/sat/model.h" +#include "ortools/sat/sat_base.h" +#include "ortools/sat/sat_solver.h" +#include "ortools/util/strong_integers.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::testing::ElementsAre; + +TEST(EnforcementPropagatorTest, BasicTest) { + Model model; + auto* sat_solver = model.GetOrCreate(); + auto* trail = model.GetOrCreate(); + auto* propag = model.GetOrCreate(); + sat_solver->SetNumVariables(10); + + const EnforcementId id1 = propag->Register(Literals({+1})); + const EnforcementId id2 = propag->Register(Literals({+1, +2})); + const EnforcementId id3 = propag->Register(Literals({-2})); + + EXPECT_TRUE(propag->Propagate(trail)); + EXPECT_EQ(propag->Status(id1), EnforcementStatus::CAN_PROPAGATE); + EXPECT_EQ(propag->Status(id2), EnforcementStatus::CANNOT_PROPAGATE); + EXPECT_EQ(propag->Status(id3), EnforcementStatus::CAN_PROPAGATE); + + sat_solver->EnqueueDecisionIfNotConflicting(Literal(+1)); + EXPECT_TRUE(propag->Propagate(trail)); + EXPECT_EQ(propag->Status(id1), EnforcementStatus::IS_ENFORCED); + EXPECT_EQ(propag->Status(id2), EnforcementStatus::CAN_PROPAGATE); + EXPECT_EQ(propag->Status(id3), EnforcementStatus::CAN_PROPAGATE); + + sat_solver->EnqueueDecisionIfNotConflicting(Literal(+2)); + EXPECT_EQ(propag->Status(id1), EnforcementStatus::IS_ENFORCED); + EXPECT_EQ(propag->Status(id2), EnforcementStatus::IS_ENFORCED); + EXPECT_EQ(propag->Status(id3), EnforcementStatus::IS_FALSE); + + CHECK(sat_solver->ResetToLevelZero()); + EXPECT_EQ(propag->Status(id1), EnforcementStatus::CAN_PROPAGATE); + EXPECT_EQ(propag->Status(id2), EnforcementStatus::CANNOT_PROPAGATE); + EXPECT_EQ(propag->Status(id3), EnforcementStatus::CAN_PROPAGATE); +} + +TEST(EnforcementPropagatorTest, UntrailWork) { + Model model; + auto* sat_solver = model.GetOrCreate(); + auto* trail = model.GetOrCreate(); + auto* propag = model.GetOrCreate(); + sat_solver->SetNumVariables(10); + + const EnforcementId id1 = propag->Register(Literals({+1})); + const EnforcementId id2 = propag->Register(Literals({+2})); + const EnforcementId id3 = propag->Register(Literals({+3})); + + EXPECT_TRUE(propag->Propagate(trail)); + EXPECT_EQ(propag->Status(id1), EnforcementStatus::CAN_PROPAGATE); + EXPECT_EQ(propag->Status(id2), EnforcementStatus::CAN_PROPAGATE); + EXPECT_EQ(propag->Status(id3), EnforcementStatus::CAN_PROPAGATE); + + sat_solver->EnqueueDecisionIfNotConflicting(Literal(+1)); + EXPECT_TRUE(propag->Propagate(trail)); + EXPECT_EQ(propag->Status(id1), EnforcementStatus::IS_ENFORCED); + EXPECT_EQ(propag->Status(id2), EnforcementStatus::CAN_PROPAGATE); + EXPECT_EQ(propag->Status(id3), EnforcementStatus::CAN_PROPAGATE); + + sat_solver->EnqueueDecisionIfNotConflicting(Literal(+2)); + EXPECT_TRUE(propag->Propagate(trail)); + EXPECT_EQ(propag->Status(id1), EnforcementStatus::IS_ENFORCED); + EXPECT_EQ(propag->Status(id2), EnforcementStatus::IS_ENFORCED); + EXPECT_EQ(propag->Status(id3), EnforcementStatus::CAN_PROPAGATE); + const int level = sat_solver->CurrentDecisionLevel(); + + sat_solver->EnqueueDecisionIfNotConflicting(Literal(+3)); + EXPECT_TRUE(propag->Propagate(trail)); + EXPECT_EQ(propag->Status(id1), EnforcementStatus::IS_ENFORCED); + EXPECT_EQ(propag->Status(id2), EnforcementStatus::IS_ENFORCED); + EXPECT_EQ(propag->Status(id3), EnforcementStatus::IS_ENFORCED); + + sat_solver->Backtrack(level); + EXPECT_EQ(propag->Status(id1), EnforcementStatus::IS_ENFORCED); + EXPECT_EQ(propag->Status(id2), EnforcementStatus::IS_ENFORCED); + EXPECT_EQ(propag->Status(id3), EnforcementStatus::CAN_PROPAGATE); +} + +TEST(EnforcementPropagatorTest, AddingAtPositiveLevelTrue) { + Model model; + auto* sat_solver = model.GetOrCreate(); + auto* trail = model.GetOrCreate(); + auto* propag = model.GetOrCreate(); + sat_solver->SetNumVariables(10); + + EXPECT_TRUE(propag->Propagate(trail)); + sat_solver->EnqueueDecisionIfNotConflicting(Literal(+1)); + EXPECT_TRUE(propag->Propagate(trail)); + + const EnforcementId id = propag->Register(std::vector{+1}); + EXPECT_EQ(propag->Status(id), EnforcementStatus::IS_ENFORCED); + + sat_solver->Backtrack(0); + EXPECT_TRUE(propag->Propagate(trail)); + EXPECT_EQ(propag->Status(id), EnforcementStatus::CAN_PROPAGATE); +} + +TEST(EnforcementPropagatorTest, AddingAtPositiveLevelFalse) { + Model model; + auto* sat_solver = model.GetOrCreate(); + auto* trail = model.GetOrCreate(); + auto* propag = model.GetOrCreate(); + sat_solver->SetNumVariables(10); + + EXPECT_TRUE(propag->Propagate(trail)); + sat_solver->EnqueueDecisionIfNotConflicting(Literal(-1)); + EXPECT_TRUE(propag->Propagate(trail)); + + const EnforcementId id = propag->Register(std::vector{+1}); + EXPECT_EQ(propag->Status(id), EnforcementStatus::IS_FALSE); + + sat_solver->Backtrack(0); + EXPECT_TRUE(propag->Propagate(trail)); + EXPECT_EQ(propag->Status(id), EnforcementStatus::CAN_PROPAGATE); +} + +// TEST copied from integer_expr test with little modif to use the new propag. +IntegerVariable AddWeightedSum(const absl::Span vars, + const absl::Span coeffs, + Model* model) { + IntegerVariable sum = model->Add(NewIntegerVariable(-10000, 10000)); + std::vector c; + std::vector v; + for (int i = 0; i < coeffs.size(); ++i) { + c.push_back(IntegerValue(coeffs[i])); + v.push_back(vars[i]); + } + c.push_back(IntegerValue(-1)); + v.push_back(sum); + + // <= sum + auto* propag = model->GetOrCreate(); + propag->AddConstraint({}, v, c, IntegerValue(0)); + + // >= sum + for (IntegerValue& ref : c) ref = -ref; + propag->AddConstraint({}, v, c, IntegerValue(0)); + + return sum; +} + +void AddWeightedSumLowerOrEqual(const absl::Span vars, + const absl::Span coeffs, int64_t rhs, + Model* model) { + std::vector c; + std::vector v; + for (int i = 0; i < coeffs.size(); ++i) { + c.push_back(IntegerValue(coeffs[i])); + v.push_back(vars[i]); + } + auto* propag = model->GetOrCreate(); + propag->AddConstraint({}, v, c, IntegerValue(rhs)); +} + +void AddWeightedSumLowerOrEqualReified( + Literal equiv, const absl::Span vars, + const absl::Span coeffs, int64_t rhs, Model* model) { + std::vector c; + std::vector v; + for (int i = 0; i < coeffs.size(); ++i) { + c.push_back(IntegerValue(coeffs[i])); + v.push_back(vars[i]); + } + auto* propag = model->GetOrCreate(); + propag->AddConstraint({equiv}, v, c, IntegerValue(rhs)); + + for (IntegerValue& ref : c) ref = -ref; + propag->AddConstraint({equiv.Negated()}, v, c, IntegerValue(-rhs) - 1); +} + +// A simple macro to make the code more readable. +#define EXPECT_BOUNDS_EQ(var, lb, ub) \ + EXPECT_EQ(model.Get(LowerBound(var)), lb); \ + EXPECT_EQ(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 = AddWeightedSum(vars, {1, -2, 3}, &model); + 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 +} + +// 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 = AddWeightedSum(vars, {3, 3, 3}, &model); + 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(WeightedSumLowerOrEqualTest, UnaryRounding) { + Model model; + IntegerVariable var = model.Add(NewIntegerVariable(0, 10)); + const std::vector coeffs = {-100}; + AddWeightedSumLowerOrEqual({var}, coeffs, -320, &model); + EXPECT_EQ(SatSolver::FEASIBLE, model.GetOrCreate()->Solve()); + EXPECT_EQ(model.Get(LowerBound(var)), 4); +} + +TEST(ReifiedWeightedSumLeTest, ReifToBoundPropagation) { + Model model; + const Literal r = Literal(model.Add(NewBooleanVariable()), true); + const IntegerVariable var = model.Add(NewIntegerVariable(4, 9)); + AddWeightedSumLowerOrEqualReified(r, {var}, {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)); + AddWeightedSumLowerOrEqualReified(r, {var}, {-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)); + AddWeightedSumLowerOrEqualReified(r, {var}, {-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(ReifiedWeightedSumTest, BoundToReifTrueLe) { + Model model; + const Literal r = Literal(model.Add(NewBooleanVariable()), true); + const IntegerVariable var = model.Add(NewIntegerVariable(4, 9)); + AddWeightedSumLowerOrEqualReified(r, {var}, {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)); + AddWeightedSumLowerOrEqualReified(r, {var}, {1}, 3, &model); + EXPECT_TRUE(model.GetOrCreate()->Propagate()); + EXPECT_FALSE(model.Get(Value(r))); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/model_test.cc b/ortools/sat/model_test.cc new file mode 100644 index 0000000000..b3862ff5d4 --- /dev/null +++ b/ortools/sat/model_test.cc @@ -0,0 +1,92 @@ +// 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/model.h" + +#include + +#include "gtest/gtest.h" + +namespace operations_research { +namespace sat { +namespace { + +struct A { + A() = default; + explicit A(Model* model) {} + std::string name; +}; + +class B { + public: + explicit B(A* a) : a_(a) {} + explicit B(Model* model) : a_(model->GetOrCreate()) {} + + std::string name() const { return a_->name; } + + private: + A* a_; +}; + +TEST(ModelTest, RecursiveCreationTest) { + Model model; + B* b = model.GetOrCreate(); + model.GetOrCreate()->name = "test"; + EXPECT_EQ("test", b->name()); +} + +struct C1 { + C1() = default; +}; +struct C2 { + explicit C2(Model* model) {} +}; +struct C3 { + C3() : name("no_arg") {} + explicit C3(Model*) : name("model") {} + std::string name; +}; + +TEST(ModelTest, DefaultConstructorFallback) { + Model model; + model.GetOrCreate(); + model.GetOrCreate(); + EXPECT_EQ(model.GetOrCreate()->name, "model"); +} + +TEST(ModelTest, Register) { + Model model; + C3 c3; + c3.name = "Shared struct"; + model.Register(&c3); + EXPECT_EQ(model.GetOrCreate()->name, c3.name); +} + +TEST(ModelTest, RegisterDeathTest) { + Model model; + C3 c3; + model.Register(&c3); + C3 c3_2; + EXPECT_DEATH(model.Register(&c3_2), ""); +} + +TEST(ModelTest, RegisterDeathTest2) { + Model model; + model.GetOrCreate(); + C3 c3; + EXPECT_DEATH(model.Register(&c3), ""); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/optimization_test.cc b/ortools/sat/optimization_test.cc new file mode 100644 index 0000000000..0401dc74fa --- /dev/null +++ b/ortools/sat/optimization_test.cc @@ -0,0 +1,172 @@ +// 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/optimization.h" + +#include + +#include +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/random/bit_gen_ref.h" +#include "absl/random/distributions.h" +#include "absl/strings/str_format.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/sat/boolean_problem.h" +#include "ortools/sat/boolean_problem.pb.h" +#include "ortools/sat/integer.h" +#include "ortools/sat/integer_search.h" +#include "ortools/sat/model.h" +#include "ortools/sat/pb_constraint.h" +#include "ortools/sat/sat_base.h" +#include "ortools/sat/sat_parameters.pb.h" +#include "ortools/sat/sat_solver.h" +#include "ortools/util/strong_integers.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::testing::ElementsAre; + +// Test the lazy encoding logic on a trivial problem. +TEST(MinimizeIntegerVariableWithLinearScanAndLazyEncodingTest, BasicProblem) { + Model model; + IntegerVariable var = model.Add(NewIntegerVariable(-5, 10)); + model.GetOrCreate()->fixed_search = + FirstUnassignedVarAtItsMinHeuristic({var}, &model); + ConfigureSearchHeuristics(&model); + int num_feasible_solution = 0; + SatSolver::Status status = + MinimizeIntegerVariableWithLinearScanAndLazyEncoding( + var, + /*feasible_solution_observer=*/ + [var, &num_feasible_solution, &model]() { + ++num_feasible_solution; + EXPECT_EQ(model.Get(Value(var)), -5); + }, + + &model); + EXPECT_EQ(num_feasible_solution, 1); + EXPECT_EQ(status, SatSolver::Status::INFEASIBLE); // Search done. +} + +TEST(MinimizeIntegerVariableWithLinearScanAndLazyEncodingTest, + BasicProblemWithSolutionLimit) { + Model model; + SatParameters* parameters = model.GetOrCreate(); + parameters->set_stop_after_first_solution(true); + IntegerVariable var = model.Add(NewIntegerVariable(-5, 10)); + model.GetOrCreate()->fixed_search = + FirstUnassignedVarAtItsMinHeuristic({var}, &model); + ConfigureSearchHeuristics(&model); + + SatSolver::Status status = + MinimizeIntegerVariableWithLinearScanAndLazyEncoding( + var, + /*feasible_solution_observer=*/ + [var, &model]() { EXPECT_EQ(model.Get(Value(var)), -5); }, &model); + EXPECT_EQ(status, SatSolver::Status::LIMIT_REACHED); +} + +TEST(MinimizeIntegerVariableWithLinearScanAndLazyEncodingTest, + BasicProblemWithBadHeuristic) { + Model model; + IntegerVariable var = model.Add(NewIntegerVariable(-5, 10)); + int expected_value = 10; + int num_feasible_solution = 0; + + model.GetOrCreate()->fixed_search = + FirstUnassignedVarAtItsMinHeuristic({NegationOf(var)}, &model); + ConfigureSearchHeuristics(&model); + + SatSolver::Status status = + MinimizeIntegerVariableWithLinearScanAndLazyEncoding( + var, + /*feasible_solution_observer=*/ + [&]() { + ++num_feasible_solution; + EXPECT_EQ(model.Get(Value(var)), expected_value--); + }, + &model); + EXPECT_EQ(num_feasible_solution, 16); + EXPECT_EQ(status, SatSolver::Status::INFEASIBLE); // Search done. +} + +// TODO(user): The core find the best solution right away here, so it doesn't +// really exercise the solution limit... +TEST(MinimizeWithCoreAndLazyEncodingTest, BasicProblemWithSolutionLimit) { + Model model; + SatParameters* parameters = model.GetOrCreate(); + parameters->set_stop_after_first_solution(true); + IntegerVariable var = model.Add(NewIntegerVariable(-5, 10)); + std::vector vars = {var}; + std::vector coeffs = {IntegerValue(1)}; + + model.GetOrCreate()->fixed_search = + FirstUnassignedVarAtItsMinHeuristic({var}, &model); + ConfigureSearchHeuristics(&model); + + int num_solutions = 0; + CoreBasedOptimizer core( + var, vars, coeffs, + /*feasible_solution_observer=*/ + [var, &model, &num_solutions]() { + ++num_solutions; + EXPECT_EQ(model.Get(Value(var)), -5); + }, + &model); + SatSolver::Status status = core.Optimize(); + EXPECT_EQ(status, SatSolver::Status::INFEASIBLE); // i.e. optimal. + EXPECT_EQ(1, num_solutions); +} + +TEST(PresolveBooleanLinearExpressionTest, NegateCoeff) { + Coefficient offset(0); + std::vector literals = Literals({+1}); + std::vector coefficients = {Coefficient(-3)}; + PresolveBooleanLinearExpression(&literals, &coefficients, &offset); + EXPECT_THAT(literals, ElementsAre(Literal(-1))); + EXPECT_THAT(coefficients, ElementsAre(Coefficient(3))); + EXPECT_EQ(offset, -3); +} + +TEST(PresolveBooleanLinearExpressionTest, Duplicate) { + Coefficient offset(0); + std::vector literals = Literals({+1, -4, +1}); + std::vector coefficients = {Coefficient(-3), Coefficient(7), + Coefficient(5)}; + PresolveBooleanLinearExpression(&literals, &coefficients, &offset); + EXPECT_THAT(literals, ElementsAre(Literal(+1), Literal(-4))); + EXPECT_THAT(coefficients, ElementsAre(Coefficient(2), Coefficient(7))); + EXPECT_EQ(offset, 0); +} + +TEST(PresolveBooleanLinearExpressionTest, NegatedLiterals) { + Coefficient offset(0); + std::vector literals = Literals({+1, -4, -1}); + std::vector coefficients = {Coefficient(-3), Coefficient(7), + Coefficient(-5)}; + PresolveBooleanLinearExpression(&literals, &coefficients, &offset); + EXPECT_THAT(literals, ElementsAre(Literal(+1), Literal(-4))); + EXPECT_THAT(coefficients, ElementsAre(Coefficient(2), Coefficient(7))); + EXPECT_EQ(offset, -5); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/parameters_validation_test.cc b/ortools/sat/parameters_validation_test.cc new file mode 100644 index 0000000000..3c039f3e8c --- /dev/null +++ b/ortools/sat/parameters_validation_test.cc @@ -0,0 +1,125 @@ +// 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/parameters_validation.h" + +#include +#include + +#include "google/protobuf/descriptor.h" +#include "google/protobuf/message.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/sat/sat_parameters.pb.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::testing::HasSubstr; +using ::testing::IsEmpty; + +TEST(ValidateParameters, MaxTimeInSeconds) { + SatParameters params; + params.set_max_time_in_seconds(-1); + EXPECT_THAT(ValidateParameters(params), HasSubstr("non-negative")); +} + +TEST(ValidateParameters, ParametersInRange) { + SatParameters params; + params.set_mip_max_bound(-1); + EXPECT_THAT(ValidateParameters(params), + HasSubstr("'mip_max_bound' should be in")); +} + +TEST(ValidateParameters, NumWorkers) { + SatParameters params; + params.set_num_workers(-1); + EXPECT_THAT(ValidateParameters(params), HasSubstr("should be in [0,10000]")); +} + +TEST(ValidateParameters, NumSearchWorkers) { + SatParameters params; + params.set_num_search_workers(-1); + EXPECT_THAT(ValidateParameters(params), HasSubstr("should be in [0,10000]")); +} + +TEST(ValidateParameters, LinearizationLevel) { + SatParameters params; + params.set_linearization_level(-1); + EXPECT_THAT(ValidateParameters(params), HasSubstr("non-negative")); +} + +TEST(ValidateParameters, NumSharedTreeSearchWorkers) { + SatParameters params; + params.set_shared_tree_num_workers(-1); + EXPECT_THAT(ValidateParameters(params), HasSubstr("should be in [0,10000]")); +} + +TEST(ValidateParameters, SharedTreeSearchMaxNodesPerWorker) { + SatParameters params; + params.set_shared_tree_max_nodes_per_worker(0); + EXPECT_THAT(ValidateParameters(params), HasSubstr("positive")); +} + +TEST(ValidateParameters, SharedTreeSearchOpenLeavesPerWorker) { + SatParameters params; + params.set_shared_tree_open_leaves_per_worker(0.0); + EXPECT_THAT(ValidateParameters(params), HasSubstr("should be in [1,10000]")); +} + +TEST(ValidateParameters, UseSharedTreeSearch) { + SatParameters params; + params.set_use_shared_tree_search(true); + EXPECT_THAT(ValidateParameters(params), HasSubstr("only be set on workers")); +} + +TEST(ValidateParameters, NaNs) { + const google::protobuf::Descriptor& descriptor = *SatParameters::descriptor(); + const google::protobuf::Reflection& reflection = + *SatParameters::GetReflection(); + for (int i = 0; i < descriptor.field_count(); ++i) { + const google::protobuf::FieldDescriptor* const field = descriptor.field(i); + SCOPED_TRACE(field->name()); + + SatParameters params; + switch (field->type()) { + case google::protobuf::FieldDescriptor::TYPE_DOUBLE: + reflection.SetDouble(¶ms, field, + std::numeric_limits::quiet_NaN()); + break; + case google::protobuf::FieldDescriptor::TYPE_FLOAT: + reflection.SetFloat(¶ms, field, + std::numeric_limits::quiet_NaN()); + break; + default: + continue; + } + + EXPECT_THAT(ValidateParameters(params), + AllOf(HasSubstr(field->name()), HasSubstr("NaN"))); + } +} + +TEST(ValidateParameters, ValidateSubsolvers) { + SatParameters params; + params.add_extra_subsolvers("not_defined"); + EXPECT_THAT(ValidateParameters(params), HasSubstr("is not valid")); + + params.add_subsolver_params()->set_name("not_defined"); + EXPECT_THAT(ValidateParameters(params), IsEmpty()); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/pb_constraint_test.cc b/ortools/sat/pb_constraint_test.cc new file mode 100644 index 0000000000..5b3d7d91be --- /dev/null +++ b/ortools/sat/pb_constraint_test.cc @@ -0,0 +1,673 @@ +// 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/pb_constraint.h" + +#include +#include +#include +#include + +#include "absl/container/flat_hash_map.h" +#include "absl/log/check.h" +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/strong_vector.h" +#include "ortools/sat/model.h" +#include "ortools/sat/sat_base.h" +#include "ortools/util/strong_integers.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::testing::ContainerEq; + +template +auto LiteralsAre(Args... literals) { + return ::testing::ElementsAre(Literal(literals)...); +} + +std::vector MakePb( + absl::Span> input) { + std::vector result; + result.reserve(input.size()); + for (const auto p : input) { + result.push_back({Literal(p.first), p.second}); + } + return result; +} + +TEST(ComputeBooleanLinearExpressionCanonicalForm, RemoveZeroCoefficient) { + Coefficient bound_shift, max_value; + auto cst = MakePb({{+1, 4}, {+2, 0}, {+3, 4}, {+5, 0}}); + const auto result = MakePb({{+1, 4}, {+3, 4}}); + EXPECT_TRUE(ComputeBooleanLinearExpressionCanonicalForm(&cst, &bound_shift, + &max_value)); + EXPECT_THAT(cst, ContainerEq(result)); + EXPECT_EQ(bound_shift, 0); + EXPECT_EQ(max_value, 8); +} + +TEST(ComputeBooleanLinearExpressionCanonicalForm, MakeAllCoefficientPositive) { + Coefficient bound_shift, max_value; + auto cst = MakePb({{+1, 4}, {+2, -3}, {+3, 4}, {+5, -1}}); + const auto result = MakePb({{-5, 1}, {-2, 3}, {+1, 4}, {+3, 4}}); + EXPECT_TRUE(ComputeBooleanLinearExpressionCanonicalForm(&cst, &bound_shift, + &max_value)); + EXPECT_THAT(cst, ContainerEq(result)); + EXPECT_EQ(bound_shift, 4); + EXPECT_EQ(max_value, 12); +} + +TEST(ComputeBooleanLinearExpressionCanonicalForm, MergeSameVariableCase1) { + Coefficient bound_shift, max_value; + // 4x -3(1-x) +4(1-x) -x is the same as to 2x + 1 + auto cst = MakePb({{+1, 4}, {-1, -3}, {-1, 4}, {+1, -1}}); + const auto result = MakePb({{+1, 2}}); + EXPECT_TRUE(ComputeBooleanLinearExpressionCanonicalForm(&cst, &bound_shift, + &max_value)); + EXPECT_THAT(cst, ContainerEq(result)); + EXPECT_EQ(bound_shift, -1); + EXPECT_EQ(max_value, 2); +} + +TEST(ComputeBooleanLinearExpressionCanonicalForm, MergeSameVariableCase2) { + Coefficient bound_shift, max_value; + // 4x -3(1-x) +4(1-x) -5x is the same as to -2x + 1 + // which is expressed as 2(1-x) -2 +1 + auto cst = MakePb({{+1, 4}, {-1, -3}, {-1, 4}, {+1, -5}}); + const auto result = MakePb({{-1, 2}}); + EXPECT_TRUE(ComputeBooleanLinearExpressionCanonicalForm(&cst, &bound_shift, + &max_value)); + EXPECT_THAT(cst, ContainerEq(result)); + EXPECT_EQ(bound_shift, 1); + EXPECT_EQ(max_value, 2); +} + +TEST(ComputeBooleanLinearExpressionCanonicalForm, MergeSameVariableCase3) { + Coefficient bound_shift, max_value; + // Here the last variable will disappear completely + auto cst = MakePb({{+1, 4}, {+2, -3}, {+2, 4}, {+2, -1}}); + const auto result = MakePb({{+1, 4}}); + EXPECT_TRUE(ComputeBooleanLinearExpressionCanonicalForm(&cst, &bound_shift, + &max_value)); + EXPECT_THAT(cst, ContainerEq(result)); + EXPECT_EQ(bound_shift, 0); + EXPECT_EQ(max_value, 4); +} + +TEST(ComputeBooleanLinearExpressionCanonicalForm, Overflow) { + Coefficient bound_shift, max_value; + auto cst = MakePb({{+1, -kCoefficientMax}, {+2, -kCoefficientMax}}); + EXPECT_FALSE(ComputeBooleanLinearExpressionCanonicalForm(&cst, &bound_shift, + &max_value)); +} + +TEST(ComputeBooleanLinearExpressionCanonicalForm, BigIntCase) { + Coefficient bound_shift, max_value; + auto cst = MakePb({{+1, -kCoefficientMax}, {-1, -kCoefficientMax}}); + const auto result = MakePb({}); + EXPECT_TRUE(ComputeBooleanLinearExpressionCanonicalForm(&cst, &bound_shift, + &max_value)); + EXPECT_THAT(cst, ContainerEq(result)); + EXPECT_EQ(bound_shift, kCoefficientMax); + EXPECT_EQ(max_value, 0); +} + +TEST(ApplyLiteralMappingTest, BasicTest) { + Coefficient bound_shift, max_value; + + // This is needed to initizalize the ITIVector below. + std::vector temp{ + kTrueLiteralIndex, kFalseLiteralIndex, // var1 fixed to true. + Literal(-1).Index(), Literal(+1).Index(), // var2 mapped to not(var1) + Literal(+2).Index(), Literal(-2).Index(), // var3 mapped to var2 + kFalseLiteralIndex, kTrueLiteralIndex, // var4 fixed to false + Literal(+2).Index(), Literal(-2).Index()}; // var5 mapped to var2 + util_intops::StrongVector mapping(temp.begin(), + temp.end()); + + auto cst = MakePb({{+1, 4}, {+3, -3}, {+2, 4}, {+4, 7}, {+5, 5}}); + EXPECT_TRUE(ApplyLiteralMapping(mapping, &cst, &bound_shift, &max_value)); + const auto result = MakePb({{+2, 2}, {-1, 4}}); + EXPECT_THAT(cst, ContainerEq(result)); + EXPECT_EQ(bound_shift, -4); + EXPECT_EQ(max_value, 6); +} + +TEST(SimplifyCanonicalBooleanLinearConstraint, CoefficientsLargerThanRhs) { + auto cst = MakePb({{+1, 4}, {+2, 5}, {+3, 6}, {-4, 7}}); + Coefficient rhs(10); + SimplifyCanonicalBooleanLinearConstraint(&cst, &rhs); + EXPECT_THAT(cst, ContainerEq(cst)); + rhs = Coefficient(5); + SimplifyCanonicalBooleanLinearConstraint(&cst, &rhs); + const auto result = MakePb({{+1, 4}, {+2, 5}, {+3, 6}, {-4, 6}}); + EXPECT_THAT(cst, ContainerEq(result)); +} + +TEST(CanonicalBooleanLinearProblem, BasicTest) { + auto cst = MakePb({{+1, 4}, {+2, -5}, {+3, 6}, {-4, 7}}); + CanonicalBooleanLinearProblem problem; + problem.AddLinearConstraint(true, Coefficient(-5), true, Coefficient(5), + &cst); + + // We have just one constraint because the >= -5 is always true. + EXPECT_EQ(1, problem.NumConstraints()); + const auto result0 = MakePb({{+1, 4}, {-2, 5}, {+3, 6}, {-4, 7}}); + EXPECT_EQ(problem.Rhs(0), 10); + EXPECT_THAT(problem.Constraint(0), ContainerEq(result0)); + + // So lets restrict it and only use the lower bound + // Note that the API destroy the input so we have to reconstruct it. + cst = MakePb({{+1, 4}, {+2, -5}, {+3, 6}, {-4, 7}}); + problem.AddLinearConstraint(true, Coefficient(-4), false, + /*unused*/ Coefficient(-10), &cst); + + // Now we have another constraint corresponding to the >= -4 constraint. + EXPECT_EQ(2, problem.NumConstraints()); + const auto result1 = MakePb({{-1, 4}, {+2, 5}, {-3, 6}, {+4, 7}}); + EXPECT_EQ(problem.Rhs(1), 21); + EXPECT_THAT(problem.Constraint(1), ContainerEq(result1)); +} + +TEST(CanonicalBooleanLinearProblem, BasicTest2) { + auto cst = MakePb({{+1, 1}, {+2, 2}}); + CanonicalBooleanLinearProblem problem; + problem.AddLinearConstraint(true, Coefficient(2), false, + /*unused*/ Coefficient(0), &cst); + + EXPECT_EQ(1, problem.NumConstraints()); + const auto result = MakePb({{-1, 1}, {-2, 2}}); + EXPECT_EQ(problem.Rhs(0), 1); + EXPECT_THAT(problem.Constraint(0), ContainerEq(result)); +} + +TEST(CanonicalBooleanLinearProblem, OverflowCases) { + auto cst = MakePb({}); + CanonicalBooleanLinearProblem problem; + for (int i = 0; i < 2; ++i) { + std::vector reference; + if (i == 0) { + // This is a constraint with a "bound shift" of 10. + reference = MakePb({{+1, -10}, {+2, 10}}); + } else { + // This is a constraint with a "bound shift" of -10 since its domain value + // is actually [10, 10]. + reference = MakePb({{+1, 10}, {-1, 10}}); + } + + // All These constraint are trivially satisfiables, so no new constraints + // should be added. + cst = reference; + EXPECT_TRUE(problem.AddLinearConstraint(true, -kCoefficientMax, true, + kCoefficientMax, &cst)); + cst = reference; + EXPECT_TRUE(problem.AddLinearConstraint(true, -kCoefficientMax - 1, true, + kCoefficientMax, &cst)); + cst = reference; + EXPECT_TRUE(problem.AddLinearConstraint(true, Coefficient(-10), true, + Coefficient(10), &cst)); + + // These are trivially unsat, and all AddLinearConstraint() should return + // false. + cst = reference; + EXPECT_FALSE(problem.AddLinearConstraint(true, kCoefficientMax, true, + kCoefficientMax, &cst)); + cst = reference; + EXPECT_FALSE(problem.AddLinearConstraint(true, -kCoefficientMax, true, + -kCoefficientMax, &cst)); + cst = reference; + EXPECT_FALSE(problem.AddLinearConstraint( + true, -kCoefficientMax, true, -kCoefficientMax - Coefficient(1), &cst)); + } + + // No constraints were actually added. + EXPECT_EQ(problem.NumConstraints(), 0); +} + +// Constructs a vector from the current trail, so we can use LiteralsAre(). +std::vector TrailToVector(const Trail& trail) { + std::vector output; + for (int i = 0; i < trail.Index(); ++i) output.push_back(trail[i]); + return output; +} + +TEST(UpperBoundedLinearConstraintTest, ConstructionAndBasicPropagation) { + Coefficient threshold; + PbConstraintsEnqueueHelper helper; + helper.reasons.resize(10); + Trail trail; + trail.Resize(10); + + UpperBoundedLinearConstraint cst( + MakePb({{+1, 4}, {+2, 4}, {-3, 5}, {+4, 10}})); + cst.InitializeRhs(Coefficient(7), 0, &threshold, &trail, &helper); + EXPECT_EQ(threshold, 2); + EXPECT_THAT(TrailToVector(trail), LiteralsAre(-4)); + + trail.Enqueue(Literal(-3), AssignmentType::kSearchDecision); + threshold -= 5; // The coeff of -3 in cst. + EXPECT_TRUE(cst.Propagate(trail.Info(Literal(-3).Variable()).trail_index, + &threshold, &trail, &helper)); + EXPECT_EQ(threshold, 2); + EXPECT_THAT(TrailToVector(trail), LiteralsAre(-4, -3, -1, -2)); + + // Untrail. + trail.Untrail(0); + threshold += 5; + cst.Untrail(&threshold, 0); + EXPECT_EQ(threshold, 2); +} + +TEST(UpperBoundedLinearConstraintTest, Conflict) { + Coefficient threshold; + Trail trail; + trail.Resize(10); + PbConstraintsEnqueueHelper helper; + helper.reasons.resize(10); + + // At most one constraint. + UpperBoundedLinearConstraint cst( + MakePb({{+1, 1}, {+2, 1}, {+3, 1}, {+4, 1}})); + cst.InitializeRhs(Coefficient(1), 0, &threshold, &trail, &helper); + EXPECT_EQ(threshold, 0); + + // Two assignment from other part of the solver. + trail.SetDecisionLevel(1); + trail.Enqueue(Literal(+1), AssignmentType::kSearchDecision); + trail.SetDecisionLevel(2); + trail.Enqueue(Literal(+2), AssignmentType::kSearchDecision); + + // We propagate only +1. + threshold -= 1; + EXPECT_FALSE(cst.Propagate(trail.Info(Literal(+1).Variable()).trail_index, + &threshold, &trail, &helper)); + EXPECT_THAT(helper.conflict, LiteralsAre(-1, -2)); +} + +TEST(UpperBoundedLinearConstraintTest, CompactReason) { + Coefficient threshold; + Trail trail; + trail.Resize(10); + PbConstraintsEnqueueHelper helper; + helper.reasons.resize(10); + + // At most one constraint. + UpperBoundedLinearConstraint cst( + MakePb({{+1, 1}, {+2, 2}, {+3, 3}, {+4, 4}})); + cst.InitializeRhs(Coefficient(7), 0, &threshold, &trail, &helper); + EXPECT_EQ(threshold, 3); + + // Two assignment from other part of the solver. + trail.SetDecisionLevel(1); + trail.Enqueue(Literal(+1), AssignmentType::kSearchDecision); + trail.SetDecisionLevel(2); + trail.Enqueue(Literal(+2), AssignmentType::kSearchDecision); + trail.SetDecisionLevel(3); + trail.Enqueue(Literal(+3), AssignmentType::kSearchDecision); + + // We propagate when +3 is processed. + threshold = -3; + const int source_trail_index = trail.Info(Literal(+3).Variable()).trail_index; + EXPECT_TRUE(cst.Propagate(source_trail_index, &threshold, &trail, &helper)); + EXPECT_EQ(trail.Index(), 4); + EXPECT_EQ(trail[3], Literal(-4)); + + // -1 do not need to be in the reason since {-3, -2} propagates exactly + // the same way. + cst.FillReason(trail, source_trail_index, Literal(-4).Variable(), + &helper.conflict); + EXPECT_THAT(helper.conflict, LiteralsAre(-3, -2)); +} + +TEST(PbConstraintsTest, Duplicates) { + Model model; + PbConstraints& csts = *(model.GetOrCreate()); + Trail& trail = *(model.GetOrCreate()); + + trail.Resize(10); + csts.Resize(10); + + CHECK_EQ(csts.NumberOfConstraints(), 0); + csts.AddConstraint(MakePb({{-1, 7}, {-2, 7}, {+3, 7}}), Coefficient(20), + &trail); + csts.AddConstraint(MakePb({{-1, 1}, {-2, 3}, {+3, 7}}), Coefficient(20), + &trail); + CHECK_EQ(csts.NumberOfConstraints(), 2); + + // Adding the same constraints will do nothing. + csts.AddConstraint(MakePb({{-1, 7}, {-2, 7}, {+3, 7}}), Coefficient(20), + &trail); + CHECK_EQ(csts.NumberOfConstraints(), 2); + CHECK_EQ(trail.Index(), 0); + + // Over constraining it will fix the 3 literals. + csts.AddConstraint(MakePb({{-1, 7}, {-2, 7}, {+3, 7}}), Coefficient(6), + &trail); + CHECK_EQ(csts.NumberOfConstraints(), 2); + EXPECT_THAT(TrailToVector(trail), LiteralsAre(+1, +2, -3)); +} + +TEST(PbConstraintsTest, BasicPropagation) { + Model model; + PbConstraints& csts = *(model.GetOrCreate()); + Trail& trail = *(model.GetOrCreate()); + + trail.Resize(10); + trail.SetDecisionLevel(1); + trail.Enqueue(Literal(-1), AssignmentType::kSearchDecision); + + csts.Resize(10); + csts.AddConstraint(MakePb({{-1, 1}, {+2, 1}}), Coefficient(1), &trail); + csts.AddConstraint(MakePb({{-1, 7}, {-2, 7}, {+3, 7}}), Coefficient(20), + &trail); + csts.AddConstraint(MakePb({{-1, 1}, {-2, 1}, {-3, 1}, {+4, 1}}), + Coefficient(3), &trail); + + EXPECT_THAT(TrailToVector(trail), LiteralsAre(-1, -2)); + while (!csts.PropagationIsDone(trail)) EXPECT_TRUE(csts.Propagate(&trail)); + EXPECT_THAT(TrailToVector(trail), LiteralsAre(-1, -2, -3, -4)); + + // Test the reason for each assignment. + EXPECT_THAT(trail.Reason(Literal(-2).Variable()), LiteralsAre(+1)); + EXPECT_THAT(trail.Reason(Literal(-3).Variable()), LiteralsAre(+2, +1)); + EXPECT_THAT(trail.Reason(Literal(-4).Variable()), LiteralsAre(+3, +2, +1)); + + // Untrail, and repropagate everything. + csts.Untrail(trail, 0); + trail.Untrail(0); + trail.Enqueue(Literal(-1), AssignmentType::kSearchDecision); + while (!csts.PropagationIsDone(trail)) EXPECT_TRUE(csts.Propagate(&trail)); + EXPECT_THAT(TrailToVector(trail), LiteralsAre(-1, -2, -3, -4)); +} + +TEST(PbConstraintsTest, BasicDeletion) { + Model model; + PbConstraints& csts = *(model.GetOrCreate()); + Trail& trail = *(model.GetOrCreate()); + + PbConstraintsEnqueueHelper helper; + helper.reasons.resize(10); + trail.Resize(10); + trail.SetDecisionLevel(0); + csts.Resize(10); + csts.AddConstraint(MakePb({{-1, 1}, {+2, 1}}), Coefficient(1), &trail); + csts.AddConstraint(MakePb({{-1, 7}, {-2, 7}, {+3, 7}}), Coefficient(20), + &trail); + csts.AddConstraint(MakePb({{-1, 1}, {-2, 1}, {-3, 1}, {+4, 1}}), + Coefficient(3), &trail); + + // Delete the first constraint. + EXPECT_EQ(3, csts.NumberOfConstraints()); + csts.DeleteConstraint(0); + EXPECT_EQ(2, csts.NumberOfConstraints()); + + // The constraint 1 is deleted, so enqueuing -1 shouldn't propagate. + trail.Enqueue(Literal(-1), AssignmentType::kSearchDecision); + while (!csts.PropagationIsDone(trail)) EXPECT_TRUE(csts.Propagate(&trail)); + EXPECT_EQ("-1", trail.DebugString()); + + // But also enqueing -2 should. + trail.Enqueue(Literal(-2), AssignmentType::kSearchDecision); + while (!csts.PropagationIsDone(trail)) EXPECT_TRUE(csts.Propagate(&trail)); + EXPECT_EQ("-1 -2 -3 -4", trail.DebugString()); + + // Let's bactrack. + trail.Untrail(1); + csts.Untrail(trail, 1); + + // Let's delete one more constraint. + csts.DeleteConstraint(0); + EXPECT_EQ(1, csts.NumberOfConstraints()); + + // Now, if we enqueue -2 again, nothing is propagated. + trail.Enqueue(Literal(-2), AssignmentType::kSearchDecision); + while (!csts.PropagationIsDone(trail)) EXPECT_TRUE(csts.Propagate(&trail)); + EXPECT_EQ("-1 -2", trail.DebugString()); + + // We need to also enqueue -3 for -4 to be propagated. + trail.Enqueue(Literal(-3), AssignmentType::kSearchDecision); + while (!csts.PropagationIsDone(trail)) EXPECT_TRUE(csts.Propagate(&trail)); + EXPECT_EQ("-1 -2 -3 -4", trail.DebugString()); + + // Deleting everything doesn't crash. + csts.DeleteConstraint(0); + EXPECT_EQ(0, csts.NumberOfConstraints()); +} + +TEST(PbConstraintsTest, UnsatAtConstruction) { + Model model; + PbConstraints& csts = *(model.GetOrCreate()); + Trail& trail = *(model.GetOrCreate()); + + trail.Resize(10); + trail.SetDecisionLevel(1); + trail.Enqueue(Literal(+1), AssignmentType::kUnitReason); + trail.Enqueue(Literal(+2), AssignmentType::kUnitReason); + trail.Enqueue(Literal(+3), AssignmentType::kUnitReason); + + csts.Resize(10); + + EXPECT_TRUE( + csts.AddConstraint(MakePb({{+1, 1}, {+2, 1}}), Coefficient(2), &trail)); + while (!csts.PropagationIsDone(trail)) EXPECT_TRUE(csts.Propagate(&trail)); + + // We need to propagate before adding this constraint for the AddConstraint() + // to notice that it is unsat. Otherwise, it will be noticed at propagation + // time. + EXPECT_FALSE(csts.AddConstraint(MakePb({{+1, 1}, {+2, 1}, {+3, 1}}), + Coefficient(2), &trail)); + EXPECT_TRUE(csts.AddConstraint(MakePb({{+1, 1}, {+2, 1}, {+4, 1}}), + Coefficient(2), &trail)); +} + +TEST(PbConstraintsTest, AddConstraintWithLevel0Propagation) { + Model model; + PbConstraints& csts = *(model.GetOrCreate()); + Trail& trail = *(model.GetOrCreate()); + + trail.Resize(10); + trail.SetDecisionLevel(0); + csts.Resize(10); + + EXPECT_TRUE(csts.AddConstraint(MakePb({{+1, 1}, {+2, 3}, {+3, 7}}), + Coefficient(2), &trail)); + EXPECT_EQ(trail.Index(), 2); + EXPECT_EQ(trail[0], Literal(-2)); + EXPECT_EQ(trail[1], Literal(-3)); +} + +TEST(PbConstraintsTest, AddConstraintUMR) { + const auto cst = MakePb({{+3, 7}}); + UpperBoundedLinearConstraint c(cst); + // Calling hashing on c generates an UMR that is triggered during the hash_map + // lookup below. + const uint64_t ct_hash = c.hash(); + absl::flat_hash_map> store; + std::vector& vec = store[ct_hash]; + EXPECT_EQ(vec.size(), 0); +} + +TEST(PbConstraintsDeathTest, AddConstraintWithLevel0PropagationInSearch) { + Model model; + PbConstraints& csts = *(model.GetOrCreate()); + Trail& trail = *(model.GetOrCreate()); + + trail.Resize(10); + trail.SetDecisionLevel(10); + csts.Resize(10); + + // If the decision level is not 0, this will fail. + ASSERT_DEATH(csts.AddConstraint(MakePb({{+1, 1}, {+2, 3}, {+3, 7}}), + Coefficient(2), &trail), + "var should have been propagated at an earlier level."); +} + +TEST(PbConstraintsDeathTest, AddConstraintPrecondition) { + Model model; + PbConstraints& csts = *(model.GetOrCreate()); + Trail& trail = *(model.GetOrCreate()); + + trail.Resize(10); + trail.SetDecisionLevel(1); + trail.Enqueue(Literal(+1), AssignmentType::kSearchDecision); + trail.Enqueue(Literal(+2), AssignmentType::kUnitReason); + trail.SetDecisionLevel(2); + trail.Enqueue(Literal(+3), AssignmentType::kSearchDecision); + csts.Resize(10); + + // We can't add this constraint since it is conflicting under the current + // assignment. + EXPECT_FALSE(csts.AddConstraint(MakePb({{+1, 1}, {+2, 1}, {+3, 1}}), + Coefficient(2), &trail)); + + trail.Untrail(trail.Index() - 1); // Remove the +3. + EXPECT_EQ(trail.Index(), 2); + csts.Untrail(trail, 2); + + // Adding this one at a decision level of 2 will also fail because it will + // propagate 3 from decision level 1. + ASSERT_DEATH(csts.AddConstraint(MakePb({{+1, 1}, {+2, 1}, {+3, 2}}), + Coefficient(3), &trail), + "var should have been propagated at an earlier level."); + + // However, adding the same constraint while the decision level is 1 is ok. + // It will propagate -3 at the correct decision level. + trail.SetDecisionLevel(1); + EXPECT_TRUE(csts.AddConstraint(MakePb({{+1, 1}, {+2, 1}, {+3, 2}}), + Coefficient(3), &trail)); + EXPECT_EQ(trail.Index(), 3); + EXPECT_EQ(trail[2], Literal(-3)); +} + +TEST(MutableUpperBoundedLinearConstraintTest, LinearAddition) { + MutableUpperBoundedLinearConstraint cst_a; + cst_a.ClearAndResize(5); + cst_a.AddTerm(Literal(+1), Coefficient(3)); + cst_a.AddTerm(Literal(+2), Coefficient(4)); + cst_a.AddTerm(Literal(+3), Coefficient(5)); + cst_a.AddTerm(Literal(+4), Coefficient(1)); + cst_a.AddTerm(Literal(+5), Coefficient(1)); + cst_a.AddToRhs(Coefficient(10)); + + // The result of cst_a + cst_b is describes in the comments. + MutableUpperBoundedLinearConstraint cst_b; + cst_b.ClearAndResize(5); + cst_b.AddTerm(Literal(+1), Coefficient(3)); // 3x + 3x = 6x + cst_b.AddTerm(Literal(-2), Coefficient(3)); // 4x + 3(1-x) = x + 3 + cst_b.AddTerm(Literal(+3), Coefficient(3)); // 5x + 3x = 8x + cst_b.AddTerm(Literal(-4), Coefficient(6)); // x + 6(1-x) = 5(1-x) + 1 + cst_b.AddTerm(Literal(+5), Coefficient(5)); // x + 5x = 6x + cst_b.AddToRhs(Coefficient(10)); + + for (BooleanVariable var : cst_b.PossibleNonZeros()) { + cst_a.AddTerm(cst_b.GetLiteral(var), cst_b.GetCoefficient(var)); + } + cst_a.AddToRhs(cst_b.Rhs()); + + EXPECT_EQ(cst_a.DebugString(), "6[+1] + 1[+2] + 8[+3] + 5[-4] + 6[+5] <= 16"); +} + +TEST(MutableUpperBoundedLinearConstraintTest, ReduceCoefficients) { + MutableUpperBoundedLinearConstraint cst; + cst.ClearAndResize(100); + Coefficient max_value(0); + for (int i = 1; i <= 10; ++i) { + max_value += Coefficient(i); + cst.AddTerm(Literal(BooleanVariable(i), true), Coefficient(i)); + } + cst.AddToRhs(max_value - 3); + + // The constraint is equivalent to sum i * Literal(i, false) >= 3, + // So we can reduce any coeff > 3 to 3 and change the rhs accordingly. + cst.ReduceCoefficients(); + for (BooleanVariable var : cst.PossibleNonZeros()) { + EXPECT_LE(cst.GetCoefficient(var), 3); + } + EXPECT_EQ(cst.Rhs(), 1 + 2 + 3 * 8 - 3); +} + +TEST(MutableUpperBoundedLinearConstraintTest, ComputeSlackForTrailPrefix) { + MutableUpperBoundedLinearConstraint cst; + cst.ClearAndResize(100); + cst.AddTerm(Literal(+1), Coefficient(3)); + cst.AddTerm(Literal(+2), Coefficient(4)); + cst.AddTerm(Literal(+3), Coefficient(5)); + cst.AddTerm(Literal(+4), Coefficient(6)); + cst.AddTerm(Literal(+5), Coefficient(7)); + cst.AddToRhs(Coefficient(10)); + + Trail trail; + trail.Resize(10); + trail.Enqueue(Literal(+1), AssignmentType::kSearchDecision); + trail.Enqueue(Literal(-2), AssignmentType::kUnitReason); + trail.Enqueue(Literal(+3), AssignmentType::kSearchDecision); + trail.Enqueue(Literal(-5), AssignmentType::kSearchDecision); + trail.Enqueue(Literal(+4), AssignmentType::kSearchDecision); + + EXPECT_EQ(Coefficient(10), cst.ComputeSlackForTrailPrefix(trail, 0)); + EXPECT_EQ(Coefficient(10 - 3), cst.ComputeSlackForTrailPrefix(trail, 1)); + EXPECT_EQ(Coefficient(10 - 3), cst.ComputeSlackForTrailPrefix(trail, 2)); + EXPECT_EQ(Coefficient(10 - 3 - 5), cst.ComputeSlackForTrailPrefix(trail, 3)); + EXPECT_EQ(Coefficient(10 - 3 - 5), cst.ComputeSlackForTrailPrefix(trail, 4)); + EXPECT_EQ(Coefficient(10 - 14), cst.ComputeSlackForTrailPrefix(trail, 5)); + EXPECT_EQ(Coefficient(10 - 14), cst.ComputeSlackForTrailPrefix(trail, 50)); +} + +TEST(MutableUpperBoundedLinearConstraintTest, ReduceSlackToZero) { + MutableUpperBoundedLinearConstraint cst; + cst.ClearAndResize(100); + cst.AddTerm(Literal(+1), Coefficient(3)); + cst.AddTerm(Literal(+2), Coefficient(1)); + cst.AddTerm(Literal(+3), Coefficient(5)); + cst.AddTerm(Literal(+4), Coefficient(6)); + cst.AddTerm(Literal(+5), Coefficient(7)); + cst.AddToRhs(Coefficient(10)); + + Trail trail; + trail.Resize(10); + trail.Enqueue(Literal(+1), AssignmentType::kSearchDecision); + trail.Enqueue(Literal(-2), AssignmentType::kUnitReason); + trail.Enqueue(Literal(+3), AssignmentType::kSearchDecision); + trail.Enqueue(Literal(+5), AssignmentType::kSearchDecision); + trail.Enqueue(Literal(+4), AssignmentType::kSearchDecision); + + // +1, -2 and +3 gives a slack of 2. + EXPECT_EQ(Coefficient(2), cst.ComputeSlackForTrailPrefix(trail, 3)); + + // It also propagate -4 and -5, to have the same propagation but with a slack + // of zero, we can call ReduceSlackToZero(). + cst.ReduceSlackTo(trail, 3, Coefficient(2), Coefficient(0)); + + // +1 and +3 have the same coeff. + EXPECT_EQ(cst.GetCoefficient(BooleanVariable(0)), Coefficient(3)); + EXPECT_EQ(cst.GetCoefficient(BooleanVariable(2)), Coefficient(5)); + + // the variable 1 disappeared. + EXPECT_EQ(cst.GetCoefficient(BooleanVariable(1)), Coefficient(0)); + + // The propagated variable coeff has been reduced by the slack. + EXPECT_EQ(cst.GetCoefficient(BooleanVariable(3)), Coefficient(6 - 2)); + EXPECT_EQ(cst.GetCoefficient(BooleanVariable(4)), Coefficient(7 - 2)); + + // The rhs has been reduced by slack, and the slack is now 0. + EXPECT_EQ(cst.Rhs(), Coefficient(10 - 2)); + EXPECT_EQ(Coefficient(0), cst.ComputeSlackForTrailPrefix(trail, 3)); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/precedences_test.cc b/ortools/sat/precedences_test.cc new file mode 100644 index 0000000000..a18cfe2698 --- /dev/null +++ b/ortools/sat/precedences_test.cc @@ -0,0 +1,592 @@ +// 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/precedences.h" + +#include +#include + +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/sat/integer.h" +#include "ortools/sat/integer_search.h" +#include "ortools/sat/model.h" +#include "ortools/sat/sat_base.h" +#include "ortools/sat/sat_solver.h" +#include "ortools/util/sorted_interval_list.h" +#include "ortools/util/strong_integers.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::testing::ElementsAre; +using ::testing::Pair; +using ::testing::UnorderedElementsAre; + +// A simple macro to make the code more readable. +// TODO(user): move that in a common place. test_utils? +#define EXPECT_BOUNDS_EQ(var, lb, ub) \ + EXPECT_EQ(integer_trail->LowerBound(var), lb); \ + EXPECT_EQ(integer_trail->UpperBound(var), ub) + +// All the tests here uses 10 integer variables initially in [0, 100]. +std::vector AddVariables(IntegerTrail* integer_trail) { + std::vector vars; + const int num_variables = 10; + const IntegerValue lower_bound(0); + const IntegerValue upper_bound(100); + for (int i = 0; i < num_variables; ++i) { + vars.push_back(integer_trail->AddIntegerVariable(lower_bound, upper_bound)); + } + return vars; +} + +TEST(PrecedenceRelationsTest, BasicAPI) { + Model model; + IntegerTrail* integer_trail = model.GetOrCreate(); + const std::vector vars = AddVariables(integer_trail); + + // Note that odd indices are for the negation. + IntegerVariable a(0), b(2), c(4), d(6); + + PrecedenceRelations precedences(&model); + precedences.Add(a, b, 10); + precedences.Add(d, c, 7); + precedences.Add(b, d, 5); + + precedences.Build(); + EXPECT_EQ(precedences.GetOffset(a, b), 10); + EXPECT_EQ(precedences.GetOffset(NegationOf(b), NegationOf(a)), 10); + EXPECT_EQ(precedences.GetOffset(a, c), 22); + EXPECT_EQ(precedences.GetOffset(NegationOf(c), NegationOf(a)), 22); + EXPECT_EQ(precedences.GetOffset(a, d), 15); + EXPECT_EQ(precedences.GetOffset(NegationOf(d), NegationOf(a)), 15); + EXPECT_EQ(precedences.GetOffset(d, a), kMinIntegerValue); + + // Once built, we can update the offsets. + // Note however that this would not propagate through the precedence graphs. + precedences.Add(a, b, 15); + EXPECT_EQ(precedences.GetOffset(a, b), 15); + EXPECT_EQ(precedences.GetOffset(NegationOf(b), NegationOf(a)), 15); +} + +TEST(PrecedenceRelationsTest, CornerCase1) { + Model model; + IntegerTrail* integer_trail = model.GetOrCreate(); + const std::vector vars = AddVariables(integer_trail); + + // Note that odd indices are for the negation. + IntegerVariable a(0), b(2), c(4), d(6); + + PrecedenceRelations precedences(&model); + precedences.Add(a, b, 10); + precedences.Add(b, c, 7); + precedences.Add(b, d, 5); + precedences.Add(NegationOf(b), a, 5); + + precedences.Build(); + EXPECT_EQ(precedences.GetOffset(NegationOf(b), a), 5); + EXPECT_EQ(precedences.GetOffset(NegationOf(b), b), 15); + EXPECT_EQ(precedences.GetOffset(NegationOf(b), c), 22); + EXPECT_EQ(precedences.GetOffset(NegationOf(b), d), 20); +} + +TEST(PrecedenceRelationsTest, CornerCase2) { + Model model; + IntegerTrail* integer_trail = model.GetOrCreate(); + const std::vector vars = AddVariables(integer_trail); + + // Note that odd indices are for the negation. + IntegerVariable a(0), b(2), c(4), d(6); + + PrecedenceRelations precedences(&model); + precedences.Add(NegationOf(a), a, 10); + precedences.Add(a, b, 7); + precedences.Add(a, c, 5); + precedences.Add(a, d, 2); + + precedences.Build(); + EXPECT_EQ(precedences.GetOffset(NegationOf(a), a), 10); + EXPECT_EQ(precedences.GetOffset(NegationOf(a), b), 17); + EXPECT_EQ(precedences.GetOffset(NegationOf(a), c), 15); + EXPECT_EQ(precedences.GetOffset(NegationOf(a), d), 12); +} + +TEST(PrecedenceRelationsTest, ConditionalRelations) { + Model model; + auto* sat_solver = model.GetOrCreate(); + auto* integer_trail = model.GetOrCreate(); + const std::vector vars = AddVariables(integer_trail); + + const Literal l(model.Add(NewBooleanVariable()), true); + EXPECT_TRUE(sat_solver->EnqueueDecisionIfNotConflicting(l)); + + // Note that odd indices are for the negation. + IntegerVariable a(0), b(2); + PrecedenceRelations precedences(&model); + precedences.PushConditionalRelation({l}, a, b, 15); + precedences.PushConditionalRelation({l}, a, b, 20); + + // We only keep the best one. + EXPECT_EQ(precedences.GetConditionalOffset(a, NegationOf(b)), -15); + EXPECT_THAT(precedences.GetConditionalEnforcements(a, NegationOf(b)), + ElementsAre(l)); + + // Backtrack works. + EXPECT_TRUE(sat_solver->ResetToLevelZero()); + EXPECT_EQ(precedences.GetConditionalOffset(a, NegationOf(b)), + kMinIntegerValue); + EXPECT_THAT(precedences.GetConditionalEnforcements(a, NegationOf(b)), + ElementsAre()); +} + +TEST(PrecedencesPropagatorTest, Empty) { + Model model; + Trail* trail = model.GetOrCreate(); + PrecedencesPropagator* propagator = + model.GetOrCreate(); + EXPECT_TRUE(propagator->Propagate(trail)); + EXPECT_TRUE(propagator->Propagate(trail)); + propagator->Untrail(*trail, 0); +} + +TEST(PrecedencesPropagatorTest, BasicPropagationTest) { + Model model; + Trail* trail = model.GetOrCreate(); + IntegerTrail* integer_trail = model.GetOrCreate(); + PrecedencesPropagator* propagator = + model.GetOrCreate(); + + std::vector vars = AddVariables(integer_trail); + propagator->AddPrecedenceWithOffset(vars[0], vars[1], IntegerValue(4)); + propagator->AddPrecedenceWithOffset(vars[0], vars[2], IntegerValue(8)); + propagator->AddPrecedenceWithOffset(vars[1], vars[2], IntegerValue(10)); + + EXPECT_TRUE(propagator->Propagate(trail)); + EXPECT_BOUNDS_EQ(vars[0], 0, 86); + EXPECT_BOUNDS_EQ(vars[1], 4, 90); + EXPECT_BOUNDS_EQ(vars[2], 14, 100); + + // Lets now move vars[1] lower bound. + std::vector lr; + std::vector ir; + EXPECT_TRUE(integer_trail->Enqueue( + IntegerLiteral::GreaterOrEqual(vars[1], IntegerValue(20)), lr, ir)); + + EXPECT_TRUE(propagator->Propagate(trail)); + EXPECT_BOUNDS_EQ(vars[1], 20, 90); + EXPECT_BOUNDS_EQ(vars[2], 30, 100); +} + +TEST(PrecedencesPropagatorTest, PropagationTestWithVariableOffset) { + Model model; + Trail* trail = model.GetOrCreate(); + IntegerTrail* integer_trail = model.GetOrCreate(); + PrecedencesPropagator* propagator = + model.GetOrCreate(); + + std::vector vars = AddVariables(integer_trail); + propagator->AddPrecedenceWithVariableOffset(vars[0], vars[1], vars[2]); + + // Make var[2] >= 10 and propagate + std::vector lr; + std::vector ir; + EXPECT_TRUE(integer_trail->Enqueue( + IntegerLiteral::GreaterOrEqual(vars[2], IntegerValue(10)), lr, ir)); + EXPECT_TRUE(propagator->Propagate(trail)); + EXPECT_BOUNDS_EQ(vars[0], 0, 90); + EXPECT_BOUNDS_EQ(vars[1], 10, 100); + + // Change the lower bound to 40 and propagate again. + EXPECT_TRUE(integer_trail->Enqueue( + IntegerLiteral::GreaterOrEqual(vars[2], IntegerValue(40)), lr, ir)); + EXPECT_TRUE(propagator->Propagate(trail)); + EXPECT_BOUNDS_EQ(vars[0], 0, 60); + EXPECT_BOUNDS_EQ(vars[1], 40, 100); +} + +TEST(PrecedencesPropagatorTest, BasicPropagation) { + Model model; + Trail* trail = model.GetOrCreate(); + IntegerTrail* integer_trail = model.GetOrCreate(); + PrecedencesPropagator* propagator = + model.GetOrCreate(); + trail->Resize(10); + + std::vector vars = AddVariables(integer_trail); + propagator->AddPrecedenceWithOffset(vars[0], vars[1], IntegerValue(4)); + propagator->AddPrecedenceWithOffset(vars[1], vars[2], IntegerValue(8)); + propagator->AddPrecedenceWithOffset(vars[0], vars[3], IntegerValue(90)); + + // These arcs are not possible, because the upper bound of vars[0] is 10. + propagator->AddConditionalPrecedenceWithOffset(vars[1], vars[0], + IntegerValue(7), Literal(+1)); + propagator->AddConditionalPrecedenceWithOffset(vars[2], vars[0], + IntegerValue(-1), Literal(+2)); + + // These are is ok. + propagator->AddConditionalPrecedenceWithOffset(vars[1], vars[0], + IntegerValue(6), Literal(+3)); + propagator->AddConditionalPrecedenceWithOffset(vars[2], vars[0], + IntegerValue(-2), Literal(+4)); + + EXPECT_TRUE(propagator->Propagate(trail)); + EXPECT_TRUE(trail->Assignment().LiteralIsFalse(Literal(+1))); + EXPECT_TRUE(trail->Assignment().LiteralIsFalse(Literal(+2))); + EXPECT_FALSE(trail->Assignment().VariableIsAssigned(Literal(+3).Variable())); + EXPECT_FALSE(trail->Assignment().VariableIsAssigned(Literal(+4).Variable())); +} + +TEST(PrecedencesPropagatorTest, PropagateOnVariableOffset) { + Model model; + Trail* trail = model.GetOrCreate(); + IntegerTrail* integer_trail = model.GetOrCreate(); + PrecedencesPropagator* propagator = + model.GetOrCreate(); + trail->Resize(10); + + std::vector vars = AddVariables(integer_trail); + propagator->AddPrecedenceWithVariableOffset(vars[0], vars[1], vars[2]); + propagator->AddPrecedenceWithOffset(vars[1], vars[3], IntegerValue(50)); + + EXPECT_TRUE(propagator->Propagate(trail)); + EXPECT_BOUNDS_EQ(vars[0], 0, 50); + EXPECT_BOUNDS_EQ(vars[1], 0, 50); + EXPECT_BOUNDS_EQ(vars[2], 0, 50); +} + +TEST(PrecedencesPropagatorTest, Cycles) { + Model model; + Trail* trail = model.GetOrCreate(); + IntegerTrail* integer_trail = model.GetOrCreate(); + PrecedencesPropagator* propagator = + model.GetOrCreate(); + trail->Resize(10); + + std::vector vars = AddVariables(integer_trail); + propagator->AddPrecedenceWithOffset(vars[0], vars[1], IntegerValue(4)); + propagator->AddPrecedenceWithOffset(vars[1], vars[2], IntegerValue(8)); + propagator->AddConditionalPrecedenceWithOffset( + vars[2], vars[3], IntegerValue(-10), Literal(+1)); + propagator->AddConditionalPrecedenceWithOffset(vars[3], vars[0], + IntegerValue(-2), Literal(+2)); + propagator->AddConditionalPrecedence(vars[3], vars[0], Literal(+3)); + + // This one will force the upper bound of vars[0] to be 50, so we can + // check that the cycle is detected before the lower bound of var[0] crosses + // this bound. + propagator->AddConditionalPrecedenceWithOffset(vars[0], vars[4], + IntegerValue(50), Literal(+4)); + + // If we add this one, the cycle will be detected using the integer bound and + // not the graph cycle. TODO(user): Maybe this is a bad thing? but it seems + // difficult to avoid it without extra computations. + propagator->AddConditionalPrecedenceWithOffset(vars[0], vars[4], + IntegerValue(99), Literal(+5)); + + EXPECT_TRUE(propagator->Propagate(trail)); + + // Cycle of weight zero is fine. + trail->SetDecisionLevel(1); + EXPECT_TRUE(integer_trail->Propagate(trail)); + trail->Enqueue(Literal(+1), AssignmentType::kUnitReason); + trail->Enqueue(Literal(+2), AssignmentType::kUnitReason); + trail->Enqueue(Literal(+4), AssignmentType::kUnitReason); + EXPECT_TRUE(propagator->Propagate(trail)); + + // But a cycle of positive length is not! + trail->Enqueue(Literal(+3), AssignmentType::kUnitReason); + EXPECT_FALSE(propagator->Propagate(trail)); + EXPECT_THAT(trail->FailingClause(), + UnorderedElementsAre(Literal(-1), Literal(-3))); + + // Test the untrail. + trail->SetDecisionLevel(0); + integer_trail->Untrail(*trail, 0); + propagator->Untrail(*trail, 0); + trail->Untrail(0); + EXPECT_TRUE(propagator->Propagate(trail)); + + // Still fine here. + trail->SetDecisionLevel(1); + EXPECT_TRUE(integer_trail->Propagate(trail)); + trail->Enqueue(Literal(+5), AssignmentType::kUnitReason); + EXPECT_TRUE(propagator->Propagate(trail)); + + // But fail there with a different and longer reason. + trail->Enqueue(Literal(+1), AssignmentType::kUnitReason); + trail->Enqueue(Literal(+3), AssignmentType::kUnitReason); + EXPECT_FALSE(propagator->Propagate(trail)); + EXPECT_THAT(trail->FailingClause(), + UnorderedElementsAre(Literal(-1), Literal(-3), Literal(-5))); +} + +// This test a tricky situation: +// +// vars[0] + (offset = vars[2]) <= var[1] +// vars[1] <= vars[2] !! +TEST(PrecedencesPropagatorTest, TrickyCycle) { + Model model; + Trail* trail = model.GetOrCreate(); + IntegerTrail* integer_trail = model.GetOrCreate(); + PrecedencesPropagator* propagator = + model.GetOrCreate(); + trail->Resize(10); + + std::vector vars = AddVariables(integer_trail); + propagator->AddPrecedenceWithVariableOffset(vars[0], vars[1], vars[2]); + propagator->AddPrecedence(vars[1], vars[2]); + + // This will cause an infinite cycle. + propagator->AddConditionalPrecedenceWithOffset(vars[3], vars[0], + IntegerValue(1), Literal(+1)); + + // So far so good. + EXPECT_TRUE(propagator->Propagate(trail)); + trail->SetDecisionLevel(1); + EXPECT_TRUE(integer_trail->Propagate(trail)); + + // Conflict. + trail->Enqueue(Literal(+1), AssignmentType::kUnitReason); + EXPECT_FALSE(propagator->Propagate(trail)); + EXPECT_THAT(trail->FailingClause(), ElementsAre(Literal(-1))); + + // Test that the code dectected properly a positive cycle in the dependency + // graph instead of just pushing the bounds until the upper bound is reached. + EXPECT_LT(integer_trail->num_enqueues(), 10); +} + +TEST(PrecedencesPropagatorTest, ZeroWeightCycleOnDiscreteDomain) { + Model model; + IntegerVariable a = model.Add( + NewIntegerVariable(Domain::FromValues({2, 5, 7, 15, 16, 17, 20, 32}))); + IntegerVariable b = model.Add( + NewIntegerVariable(Domain::FromValues({3, 6, 9, 14, 16, 18, 20, 35}))); + + // Add the fact that a == b with two inequalities. + model.Add(LowerOrEqual(a, b)); + model.Add(LowerOrEqual(b, a)); + + // After propagation, we should detect that the only common values fall in + // [16, 20]. + EXPECT_TRUE(model.GetOrCreate()->Propagate()); + + // The integer_trail is only used in the macros below. + IntegerTrail* integer_trail = model.GetOrCreate(); + EXPECT_BOUNDS_EQ(a, 16, 20); + EXPECT_BOUNDS_EQ(b, 16, 20); +} + +// This was failing before CL 135903015. +TEST(PrecedencesPropagatorTest, ConditionalPrecedencesOnFixedLiteral) { + Model model; + + // To trigger the old bug, we need to add some precedences. + IntegerVariable x = model.Add(NewIntegerVariable(0, 100)); + IntegerVariable y = model.Add(NewIntegerVariable(50, 100)); + model.Add(LowerOrEqual(x, y)); + + // We then add a Boolean variable and fix it. + // This will trigger a propagation. + BooleanVariable b = model.Add(NewBooleanVariable()); + model.Add(ClauseConstraint({Literal(b, true)})); // Fix b To true. + + // We now add a conditional precedences using the fixed variable. + // This used to not be taken into account. + model.Add(ConditionalLowerOrEqualWithOffset(y, x, 0, Literal(b, true))); + + EXPECT_EQ(SatSolver::FEASIBLE, SolveIntegerProblemWithLazyEncoding(&model)); + EXPECT_EQ(model.Get(Value(x)), model.Get(Value(y))); +} + +#undef EXPECT_BOUNDS_EQ + +TEST(PrecedenceRelationsTest, CollectPrecedences) { + Model model; + auto* integer_trail = model.GetOrCreate(); + auto* relations = model.GetOrCreate(); + + std::vector vars = AddVariables(integer_trail); + relations->Add(vars[0], vars[2], IntegerValue(1)); + relations->Add(vars[0], vars[5], IntegerValue(1)); + relations->Add(vars[1], vars[2], IntegerValue(1)); + relations->Add(vars[2], vars[4], IntegerValue(1)); + relations->Add(vars[3], vars[4], IntegerValue(1)); + relations->Add(vars[4], vars[5], IntegerValue(1)); + + std::vector p; + relations->CollectPrecedences({vars[0], vars[2], vars[3]}, &p); + + // Note that we do not return precedences with just one variable. + std::vector indices; + std::vector variables; + for (const auto precedence : p) { + indices.push_back(precedence.index); + variables.push_back(precedence.var); + } + EXPECT_EQ(indices, (std::vector{1, 2})); + EXPECT_EQ(variables, (std::vector{vars[4], vars[4]})); + + // Same with NegationOf() and also test that p is cleared. + relations->CollectPrecedences({NegationOf(vars[0]), NegationOf(vars[4])}, &p); + EXPECT_TRUE(p.empty()); +} + +TEST(GreaterThanAtLeastOneOfDetectorTest, AddGreaterThanAtLeastOneOf) { + Model model; + const IntegerVariable a = model.Add(NewIntegerVariable(2, 10)); + const IntegerVariable b = model.Add(NewIntegerVariable(5, 10)); + const IntegerVariable c = model.Add(NewIntegerVariable(3, 10)); + const IntegerVariable d = model.Add(NewIntegerVariable(0, 10)); + const Literal lit_a = Literal(model.Add(NewBooleanVariable()), true); + const Literal lit_b = Literal(model.Add(NewBooleanVariable()), true); + const Literal lit_c = Literal(model.Add(NewBooleanVariable()), true); + model.Add(ClauseConstraint({lit_a, lit_b, lit_c})); + + auto* detector = model.GetOrCreate(); + detector->Add(lit_a, {a, -1}, {d, 1}, 2, 1000); // d >= a + 2 + detector->Add(lit_b, {b, -1}, {d, 1}, -1, 1000); // d >= b -1 + detector->Add(lit_c, {c, -1}, {d, 1}, 0, 1000); // d >= c + + auto* solver = model.GetOrCreate(); + EXPECT_TRUE(solver->Propagate()); + EXPECT_EQ(model.Get(LowerBound(d)), 0); + + EXPECT_EQ(1, detector->AddGreaterThanAtLeastOneOfConstraints(&model)); + EXPECT_TRUE(solver->Propagate()); + EXPECT_EQ(model.Get(LowerBound(d)), std::min({2 + 2, 5 - 1, 3 + 0})); +} + +TEST(GreaterThanAtLeastOneOfDetectorTest, + AddGreaterThanAtLeastOneOfWithAutoDetect) { + Model model; + const IntegerVariable a = model.Add(NewIntegerVariable(2, 10)); + const IntegerVariable b = model.Add(NewIntegerVariable(5, 10)); + const IntegerVariable c = model.Add(NewIntegerVariable(3, 10)); + const IntegerVariable d = model.Add(NewIntegerVariable(0, 10)); + const Literal lit_a = Literal(model.Add(NewBooleanVariable()), true); + const Literal lit_b = Literal(model.Add(NewBooleanVariable()), true); + const Literal lit_c = Literal(model.Add(NewBooleanVariable()), true); + model.Add(ClauseConstraint({lit_a, lit_b, lit_c})); + + auto* detector = model.GetOrCreate(); + detector->Add(lit_a, {a, -1}, {d, 1}, 2, 1000); // d >= a + 2 + detector->Add(lit_b, {b, -1}, {d, 1}, -1, 1000); // d >= b -1 + detector->Add(lit_c, {c, -1}, {d, 1}, 0, 1000); // d >= c + + auto* solver = model.GetOrCreate(); + EXPECT_TRUE(solver->Propagate()); + EXPECT_EQ(model.Get(LowerBound(d)), 0); + + EXPECT_EQ(1, detector->AddGreaterThanAtLeastOneOfConstraints( + &model, /*auto_detect_clauses=*/true)); + EXPECT_TRUE(solver->Propagate()); + EXPECT_EQ(model.Get(LowerBound(d)), std::min({2 + 2, 5 - 1, 3 + 0})); +} + +TEST(PrecedencesPropagatorTest, ComputeFullPrecedencesIfCycle) { + Model model; + std::vector vars(10); + for (int i = 0; i < vars.size(); ++i) { + vars[i] = model.Add(NewIntegerVariable(0, 10)); + } + + // Even if the weight are compatible, we will fail here. + model.Add(LowerOrEqualWithOffset(vars[0], vars[1], 2)); + model.Add(LowerOrEqualWithOffset(vars[1], vars[2], 2)); + model.Add(LowerOrEqualWithOffset(vars[2], vars[1], -10)); + model.Add(LowerOrEqualWithOffset(vars[0], vars[2], 5)); + + std::vector precedences; + model.GetOrCreate()->ComputeFullPrecedences( + {vars[0], vars[1]}, &precedences); + EXPECT_TRUE(precedences.empty()); +} + +TEST(PrecedencesPropagatorTest, BasicFiltering) { + Model model; + std::vector vars(10); + for (int i = 0; i < vars.size(); ++i) { + vars[i] = model.Add(NewIntegerVariable(0, 10)); + } + + // 1 + // / \ + // 0 2 -- 4 + // \ / + // 3 + model.Add(LowerOrEqualWithOffset(vars[0], vars[1], 2)); + model.Add(LowerOrEqualWithOffset(vars[1], vars[2], 2)); + model.Add(LowerOrEqualWithOffset(vars[0], vars[3], 1)); + model.Add(LowerOrEqualWithOffset(vars[3], vars[2], 2)); + model.Add(LowerOrEqualWithOffset(vars[2], vars[4], 2)); + + std::vector precedences; + model.GetOrCreate()->ComputeFullPrecedences( + {vars[0], vars[1], vars[3]}, &precedences); + + // We only output size at least 2, and "relevant" precedences. + // So here only vars[2]. + ASSERT_EQ(precedences.size(), 1); + EXPECT_EQ(precedences[0].var, vars[2]); + EXPECT_THAT(precedences[0].offsets, ElementsAre(4, 2, 2)); + EXPECT_THAT(precedences[0].indices, ElementsAre(0, 1, 2)); +} + +TEST(PrecedencesPropagatorTest, BasicFiltering2) { + Model model; + std::vector vars(10); + for (int i = 0; i < vars.size(); ++i) { + vars[i] = model.Add(NewIntegerVariable(0, 10)); + } + + // 1 + // / \ + // 0 2 -- 4 + // \ / / + // 3 5 + model.Add(LowerOrEqualWithOffset(vars[0], vars[1], 2)); + model.Add(LowerOrEqualWithOffset(vars[1], vars[2], 2)); + model.Add(LowerOrEqualWithOffset(vars[0], vars[3], 1)); + model.Add(LowerOrEqualWithOffset(vars[3], vars[2], 2)); + model.Add(LowerOrEqualWithOffset(vars[2], vars[4], 2)); + model.Add(LowerOrEqualWithOffset(vars[5], vars[4], 7)); + + std::vector precedences; + model.GetOrCreate()->ComputeFullPrecedences( + {vars[0], vars[1], vars[3]}, &precedences); + + // Same as before here. + ASSERT_EQ(precedences.size(), 1); + EXPECT_EQ(precedences[0].var, vars[2]); + EXPECT_THAT(precedences[0].offsets, ElementsAre(4, 2, 2)); + EXPECT_THAT(precedences[0].indices, ElementsAre(0, 1, 2)); + + // But if we ask for 5, we will get two results. + precedences.clear(); + model.GetOrCreate()->ComputeFullPrecedences( + {vars[0], vars[1], vars[3], vars[5]}, &precedences); + ASSERT_EQ(precedences.size(), 2); + EXPECT_EQ(precedences[0].var, vars[2]); + EXPECT_THAT(precedences[0].offsets, ElementsAre(4, 2, 2)); + EXPECT_THAT(precedences[0].indices, ElementsAre(0, 1, 2)); + EXPECT_EQ(precedences[1].var, vars[4]); + EXPECT_THAT(precedences[1].offsets, ElementsAre(6, 4, 4, 7)); + EXPECT_THAT(precedences[1].indices, ElementsAre(0, 1, 2, 3)); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/probing_test.cc b/ortools/sat/probing_test.cc new file mode 100644 index 0000000000..d57e474447 --- /dev/null +++ b/ortools/sat/probing_test.cc @@ -0,0 +1,80 @@ +// 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/probing.h" + +#include + +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/sat/integer.h" +#include "ortools/sat/model.h" +#include "ortools/sat/sat_base.h" +#include "ortools/sat/sat_solver.h" +#include "ortools/util/sorted_interval_list.h" + +namespace operations_research { +namespace sat { +namespace { + +TEST(ProbeBooleanVariablesTest, IntegerBoundInference) { + Model model; + const BooleanVariable a = model.Add(NewBooleanVariable()); + const IntegerVariable b = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable c = model.Add(NewIntegerVariable(0, 10)); + + // Bound restriction. + model.Add(Implication({Literal(a, true)}, + IntegerLiteral::GreaterOrEqual(b, IntegerValue(2)))); + model.Add(Implication({Literal(a, false)}, + IntegerLiteral::GreaterOrEqual(b, IntegerValue(3)))); + model.Add(Implication({Literal(a, true)}, + IntegerLiteral::LowerOrEqual(b, IntegerValue(7)))); + model.Add(Implication({Literal(a, false)}, + IntegerLiteral::LowerOrEqual(b, IntegerValue(9)))); + + // Hole. + model.Add(Implication({Literal(a, true)}, + IntegerLiteral::GreaterOrEqual(c, IntegerValue(7)))); + model.Add(Implication({Literal(a, false)}, + IntegerLiteral::LowerOrEqual(c, IntegerValue(4)))); + + Prober* prober = model.GetOrCreate(); + prober->ProbeBooleanVariables(/*deterministic_time_limit=*/1.0); + auto* integer_trail = model.GetOrCreate(); + EXPECT_EQ("[2,9]", integer_trail->InitialVariableDomain(b).ToString()); + EXPECT_EQ("[0,4][7,10]", integer_trail->InitialVariableDomain(c).ToString()); +} + +TEST(FailedLiteralProbingRoundTest, TrivialExample) { + Model model; + const Literal a(model.Add(NewBooleanVariable()), true); + const Literal b(model.Add(NewBooleanVariable()), true); + const Literal c(model.Add(NewBooleanVariable()), true); + + // Setting a to false will result in a constradiction, so a must be true. + model.Add(ClauseConstraint({a, b, c})); + model.Add(Implication(a.Negated(), b.Negated())); + model.Add(Implication(c, a)); + + auto* sat_soler = model.GetOrCreate(); + EXPECT_TRUE(sat_soler->Propagate()); + EXPECT_FALSE(sat_soler->Assignment().LiteralIsAssigned(a)); + + EXPECT_TRUE(FailedLiteralProbingRound(ProbingOptions(), &model)); + EXPECT_TRUE(sat_soler->Assignment().LiteralIsTrue(a)); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/pseudo_costs_test.cc b/ortools/sat/pseudo_costs_test.cc new file mode 100644 index 0000000000..fc0b98ad9d --- /dev/null +++ b/ortools/sat/pseudo_costs_test.cc @@ -0,0 +1,263 @@ +// 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/pseudo_costs.h" + +#include +#include + +#include "gtest/gtest.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" +#include "ortools/util/strong_integers.h" + +namespace operations_research { +namespace sat { +namespace { + +TEST(GetBoundChangeTest, LowerBoundChange) { + Model model; + auto* encoder = model.GetOrCreate(); + + const IntegerVariable x = model.Add(NewIntegerVariable(0, 10)); + const Literal decision = encoder->GetOrCreateAssociatedLiteral( + IntegerLiteral::GreaterOrEqual(x, IntegerValue(3))); + + PseudoCosts pseudo_costs(&model); + pseudo_costs.SaveBoundChanges(decision, {}); + auto& bound_changes = pseudo_costs.BoundChanges(); + EXPECT_EQ(1, bound_changes.size()); + PseudoCosts::VariableBoundChange bound_change = bound_changes[0]; + EXPECT_EQ(bound_change.var, x); + EXPECT_EQ(bound_change.lower_bound_change, IntegerValue(3)); +} + +TEST(GetBoundChangeTest, UpperBoundChange) { + Model model; + auto* encoder = model.GetOrCreate(); + + const IntegerVariable x = model.Add(NewIntegerVariable(0, 10)); + const Literal decision = encoder->GetOrCreateAssociatedLiteral( + IntegerLiteral::LowerOrEqual(x, IntegerValue(7))); + + PseudoCosts pseudo_costs(&model); + pseudo_costs.SaveBoundChanges(decision, {}); + auto& bound_changes = pseudo_costs.BoundChanges(); + EXPECT_EQ(1, bound_changes.size()); + PseudoCosts::VariableBoundChange bound_change = bound_changes[0]; + EXPECT_EQ(bound_change.var, NegationOf(x)); + EXPECT_EQ(bound_change.lower_bound_change, IntegerValue(3)); +} + +TEST(GetBoundChangeTest, EqualityDecision) { + Model model; + auto* encoder = model.GetOrCreate(); + + const IntegerVariable x = model.Add(NewIntegerVariable(0, 10)); + Literal decision(model.GetOrCreate()->NewBooleanVariable(), true); + encoder->AssociateToIntegerEqualValue(decision, x, IntegerValue(6)); + + PseudoCosts pseudo_costs(&model); + pseudo_costs.SaveBoundChanges(decision, {}); + auto& bound_changes = pseudo_costs.BoundChanges(); + EXPECT_EQ(2, bound_changes.size()); + PseudoCosts::VariableBoundChange lower_bound_change = bound_changes[0]; + EXPECT_EQ(lower_bound_change.var, x); + EXPECT_EQ(lower_bound_change.lower_bound_change, IntegerValue(6)); + PseudoCosts::VariableBoundChange upper_bound_change = bound_changes[1]; + EXPECT_EQ(upper_bound_change.var, NegationOf(x)); + EXPECT_EQ(upper_bound_change.lower_bound_change, IntegerValue(4)); +} + +TEST(PseudoCosts, Initialize) { + Model model; + SatParameters* parameters = model.GetOrCreate(); + parameters->set_pseudo_cost_reliability_threshold(1); + + const IntegerVariable x = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable y = model.Add(NewIntegerVariable(0, 10)); + + PseudoCosts pseudo_costs(&model); + + EXPECT_EQ(0.0, pseudo_costs.GetCost(x)); + EXPECT_EQ(0.0, pseudo_costs.GetCost(NegationOf(x))); + EXPECT_EQ(0.0, pseudo_costs.GetCost(y)); + EXPECT_EQ(0.0, pseudo_costs.GetCost(NegationOf(y))); + EXPECT_EQ(0, pseudo_costs.GetNumRecords(x)); + EXPECT_EQ(0, pseudo_costs.GetNumRecords(NegationOf(x))); + EXPECT_EQ(0, pseudo_costs.GetNumRecords(y)); + EXPECT_EQ(0, pseudo_costs.GetNumRecords(NegationOf(y))); +} + +namespace { +void SimulateDecision(Literal decision, IntegerValue obj_delta, Model* model) { + const IntegerVariable objective_var = + model->GetOrCreate()->objective_var; + auto* integer_trail = model->GetOrCreate(); + auto* pseudo_costs = model->GetOrCreate(); + + pseudo_costs->BeforeTakingDecision(decision); + const IntegerValue lb = integer_trail->LowerBound(objective_var); + EXPECT_TRUE(integer_trail->Enqueue( + IntegerLiteral::GreaterOrEqual(objective_var, lb + obj_delta), {}, {})); + pseudo_costs->AfterTakingDecision(); +} +} // namespace + +TEST(PseudoCosts, UpdateCostOfNewVar) { + Model model; + auto* encoder = model.GetOrCreate(); + SatParameters* parameters = model.GetOrCreate(); + parameters->set_pseudo_cost_reliability_threshold(1); + + const IntegerVariable objective_var = model.Add(NewIntegerVariable(0, 100)); + model.GetOrCreate()->objective_var = objective_var; + + const IntegerVariable x = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable y = model.Add(NewIntegerVariable(0, 10)); + auto* pseudo_costs = model.GetOrCreate(); + + SimulateDecision(encoder->GetOrCreateAssociatedLiteral( + IntegerLiteral::GreaterOrEqual(x, IntegerValue(3))), + IntegerValue(6), &model); + + EXPECT_EQ(2.0, pseudo_costs->GetCost(x)); + EXPECT_EQ(0.0, pseudo_costs->GetCost(NegationOf(x))); + EXPECT_EQ(1, pseudo_costs->GetNumRecords(x)); + EXPECT_EQ(0, pseudo_costs->GetNumRecords(NegationOf(x))); + + SimulateDecision(encoder->GetOrCreateAssociatedLiteral( + IntegerLiteral::LowerOrEqual(y, IntegerValue(8))), + IntegerValue(6), &model); + + EXPECT_EQ(2.0, pseudo_costs->GetCost(x)); + EXPECT_EQ(0.0, pseudo_costs->GetCost(NegationOf(x))); + EXPECT_EQ(0.0, pseudo_costs->GetCost(y)); + EXPECT_EQ(3.0, pseudo_costs->GetCost(NegationOf(y))); + EXPECT_EQ(1, pseudo_costs->GetNumRecords(x)); + EXPECT_EQ(0, pseudo_costs->GetNumRecords(NegationOf(x))); + EXPECT_EQ(0, pseudo_costs->GetNumRecords(y)); + EXPECT_EQ(1, pseudo_costs->GetNumRecords(NegationOf(y))); +} + +TEST(PseudoCosts, BasicCostUpdate) { + Model model; + auto* encoder = model.GetOrCreate(); + SatParameters* parameters = model.GetOrCreate(); + parameters->set_pseudo_cost_reliability_threshold(1); + + const IntegerVariable objective_var = model.Add(NewIntegerVariable(0, 100)); + model.GetOrCreate()->objective_var = objective_var; + + const IntegerVariable x = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable y = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable z = model.Add(NewIntegerVariable(0, 10)); + auto* pseudo_costs = model.GetOrCreate(); + + SimulateDecision(encoder->GetOrCreateAssociatedLiteral( + IntegerLiteral::GreaterOrEqual(x, IntegerValue(3))), + IntegerValue(6), &model); + + EXPECT_EQ(2.0, pseudo_costs->GetCost(x)); + EXPECT_EQ(0.0, pseudo_costs->GetCost(NegationOf(x))); + EXPECT_EQ(0.0, pseudo_costs->GetCost(y)); + EXPECT_EQ(0.0, pseudo_costs->GetCost(NegationOf(y))); + EXPECT_EQ(0.0, pseudo_costs->GetCost(z)); + EXPECT_EQ(0.0, pseudo_costs->GetCost(NegationOf(z))); + EXPECT_EQ(1, pseudo_costs->GetNumRecords(x)); + EXPECT_EQ(0, pseudo_costs->GetNumRecords(NegationOf(x))); + EXPECT_EQ(0, pseudo_costs->GetNumRecords(y)); + EXPECT_EQ(0, pseudo_costs->GetNumRecords(NegationOf(y))); + EXPECT_EQ(0, pseudo_costs->GetNumRecords(z)); + EXPECT_EQ(0, pseudo_costs->GetNumRecords(NegationOf(z))); + + SimulateDecision(encoder->GetOrCreateAssociatedLiteral( + IntegerLiteral::LowerOrEqual(y, IntegerValue(8))), + IntegerValue(6), &model); + + EXPECT_EQ(2.0, pseudo_costs->GetCost(x)); + EXPECT_EQ(0.0, pseudo_costs->GetCost(NegationOf(x))); + EXPECT_EQ(0.0, pseudo_costs->GetCost(y)); + EXPECT_EQ(3.0, pseudo_costs->GetCost(NegationOf(y))); + EXPECT_EQ(0.0, pseudo_costs->GetCost(z)); + EXPECT_EQ(0.0, pseudo_costs->GetCost(NegationOf(z))); + EXPECT_EQ(1, pseudo_costs->GetNumRecords(x)); + EXPECT_EQ(0, pseudo_costs->GetNumRecords(NegationOf(x))); + EXPECT_EQ(0, pseudo_costs->GetNumRecords(y)); + EXPECT_EQ(1, pseudo_costs->GetNumRecords(NegationOf(y))); + EXPECT_EQ(0, pseudo_costs->GetNumRecords(z)); + EXPECT_EQ(0, pseudo_costs->GetNumRecords(NegationOf(z))); +} + +TEST(PseudoCosts, PseudoCostReliabilityTest) { + Model model; + auto* encoder = model.GetOrCreate(); + SatParameters* parameters = model.GetOrCreate(); + parameters->set_pseudo_cost_reliability_threshold(2); + + const IntegerVariable objective_var = model.Add(NewIntegerVariable(0, 100)); + model.GetOrCreate()->objective_var = objective_var; + + const IntegerVariable x = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable y = model.Add(NewIntegerVariable(0, 10)); + auto* pseudo_costs = model.GetOrCreate(); + + SimulateDecision(encoder->GetOrCreateAssociatedLiteral( + IntegerLiteral::GreaterOrEqual(x, IntegerValue(3))), + IntegerValue(6), &model); + + EXPECT_EQ(2.0, pseudo_costs->GetCost(x)); + EXPECT_EQ(0.0, pseudo_costs->GetCost(NegationOf(x))); + EXPECT_EQ(0.0, pseudo_costs->GetCost(y)); + EXPECT_EQ(0.0, pseudo_costs->GetCost(NegationOf(y))); + EXPECT_EQ(1, pseudo_costs->GetNumRecords(x)); + EXPECT_EQ(0, pseudo_costs->GetNumRecords(NegationOf(x))); + EXPECT_EQ(0, pseudo_costs->GetNumRecords(y)); + EXPECT_EQ(0, pseudo_costs->GetNumRecords(NegationOf(y))); + EXPECT_EQ(kNoIntegerVariable, pseudo_costs->GetBestDecisionVar()); + + SimulateDecision(encoder->GetOrCreateAssociatedLiteral( + IntegerLiteral::LowerOrEqual(y, IntegerValue(8))), + IntegerValue(14), &model); + + EXPECT_EQ(2.0, pseudo_costs->GetCost(x)); + EXPECT_EQ(0.0, pseudo_costs->GetCost(NegationOf(x))); + EXPECT_EQ(0.0, pseudo_costs->GetCost(y)); + EXPECT_EQ(7.0, pseudo_costs->GetCost(NegationOf(y))); + EXPECT_EQ(1, pseudo_costs->GetNumRecords(x)); + EXPECT_EQ(0, pseudo_costs->GetNumRecords(NegationOf(x))); + EXPECT_EQ(0, pseudo_costs->GetNumRecords(y)); + EXPECT_EQ(1, pseudo_costs->GetNumRecords(NegationOf(y))); + EXPECT_EQ(kNoIntegerVariable, pseudo_costs->GetBestDecisionVar()); + + SimulateDecision(encoder->GetOrCreateAssociatedLiteral( + IntegerLiteral::LowerOrEqual(x, IntegerValue(8))), + IntegerValue(6), &model); + + EXPECT_EQ(2.0, pseudo_costs->GetCost(x)); + EXPECT_EQ(3.0, pseudo_costs->GetCost(NegationOf(x))); + EXPECT_EQ(0.0, pseudo_costs->GetCost(y)); + EXPECT_EQ(7.0, pseudo_costs->GetCost(NegationOf(y))); + EXPECT_EQ(1, pseudo_costs->GetNumRecords(x)); + EXPECT_EQ(1, pseudo_costs->GetNumRecords(NegationOf(x))); + EXPECT_EQ(0, pseudo_costs->GetNumRecords(y)); + EXPECT_EQ(1, pseudo_costs->GetNumRecords(NegationOf(y))); + EXPECT_EQ(NegationOf(x), pseudo_costs->GetBestDecisionVar()); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/restart_test.cc b/ortools/sat/restart_test.cc new file mode 100644 index 0000000000..fd319ed33d --- /dev/null +++ b/ortools/sat/restart_test.cc @@ -0,0 +1,86 @@ +// 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/restart.h" + +#include + +#include "absl/base/macros.h" +#include "gtest/gtest.h" +#include "ortools/sat/model.h" +#include "ortools/sat/sat_parameters.pb.h" + +namespace operations_research { +namespace sat { +namespace { + +TEST(SUnivTest, Luby) { + const int kSUniv[] = {1, 1, 2, 1, 1, 2, 4, 1, 1, 2, 1, 1, 2, 4, 8, 1}; + for (int i = 0; i < ABSL_ARRAYSIZE(kSUniv); ++i) { + EXPECT_EQ(kSUniv[i], SUniv(i + 1)); + } +} + +TEST(RestartPolicyTest, BasicRunningAverageTest) { + Model model; + RestartPolicy* restart = model.GetOrCreate(); + SatParameters* params = model.GetOrCreate(); + + // The parameters for this test. + params->clear_restart_algorithms(); + params->add_restart_algorithms(SatParameters::DL_MOVING_AVERAGE_RESTART); + params->set_use_blocking_restart(false); + params->set_restart_dl_average_ratio(1.0); + params->set_restart_running_window_size(10); + restart->Reset(); + + EXPECT_FALSE(restart->ShouldRestart()); + int i = 0; + for (; i < 100; ++i) { + const int unused = 0; + const int decision_level = i; + if (restart->ShouldRestart()) break; + restart->OnConflict(unused, decision_level, unused); + } + + // Increasing decision levels, so as soon as we have 11 conflicts and 10 in + // the window, the window average is > global average. + EXPECT_EQ(i, 11); + + // Now the window is reset, but not the global average. So as soon as we have + // 10 conflicts, we restart. + i = 0; + for (; i < 100; ++i) { + const int unused = 0; + const int decision_level = 1000 - i; + if (restart->ShouldRestart()) break; + restart->OnConflict(unused, decision_level, unused); + } + EXPECT_EQ(i, 10); + + // If we call Reset() the global average is reaset, so if we have conflicts at + // a decreasing decision level, we never restart. + restart->Reset(); + i = 0; + for (; i < 1000; ++i) { + const int unused = 0; + const int decision_level = 1000 - i; + if (restart->ShouldRestart()) break; + restart->OnConflict(unused, decision_level, unused); + } + EXPECT_EQ(i, 1000); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/routing_cuts_test.cc b/ortools/sat/routing_cuts_test.cc new file mode 100644 index 0000000000..e2adb9da48 --- /dev/null +++ b/ortools/sat/routing_cuts_test.cc @@ -0,0 +1,422 @@ +// 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/routing_cuts.h" + +#include +#include +#include +#include +#include + +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/strong_vector.h" +#include "ortools/graph/max_flow.h" +#include "ortools/sat/cuts.h" +#include "ortools/sat/integer.h" +#include "ortools/sat/linear_constraint.h" +#include "ortools/sat/linear_constraint_manager.h" +#include "ortools/sat/model.h" +#include "ortools/sat/sat_base.h" +#include "ortools/util/strong_integers.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::testing::ElementsAre; + +// Test on a simple tree: +// 3 +// / \ \ +// 1 0 5 +// / \ +// 2 4 +TEST(ExtractAllSubsetsFromForestTest, Basic) { + std::vector parents = {3, 3, 1, 3, 1, 3}; + + std::vector buffer; + std::vector> subsets; + ExtractAllSubsetsFromForest(parents, &buffer, &subsets); + + // Post order but we explore high number first. + // Alternatively, we could use unordered here, but the order is stable. + EXPECT_THAT(buffer, ElementsAre(5, 4, 2, 1, 0, 3)); + EXPECT_THAT(subsets, + ElementsAre(ElementsAre(5), ElementsAre(4), ElementsAre(2), + ElementsAre(4, 2, 1), ElementsAre(0), + ElementsAre(5, 4, 2, 1, 0, 3))); +} + +// +// 0 3 4 +// / \ | +// 1 2 5 +TEST(ExtractAllSubsetsFromForestTest, BasicForest) { + std::vector parents = {0, 0, 0, 3, 4, 4}; + + std::vector buffer; + std::vector> subsets; + ExtractAllSubsetsFromForest(parents, &buffer, &subsets); + + // Post order but we explore high number first. + // Alternatively, we could use unordered here, but the order is stable. + EXPECT_THAT(buffer, ElementsAre(2, 1, 0, 3, 5, 4)); + EXPECT_THAT(subsets, + ElementsAre(ElementsAre(2), ElementsAre(1), ElementsAre(2, 1, 0), + ElementsAre(3), ElementsAre(5), ElementsAre(5, 4))); +} + +TEST(ExtractAllSubsetsFromForestTest, Random) { + const int num_nodes = 20; + absl::BitGen random; + + // Create a random tree rooted at zero. + std::vector parents(num_nodes, 0); + for (int i = 2; i < num_nodes; ++i) { + parents[i] = absl::Uniform(random, 0, i); // in [0, i - 1]. + } + + std::vector buffer; + std::vector> subsets; + ExtractAllSubsetsFromForest(parents, &buffer, &subsets); + + // We don't test that we are exhaustive, but we check basic property. + std::vector in_subset(num_nodes, false); + for (const auto subset : subsets) { + for (const int n : subset) in_subset[n] = true; + + // There should be at most one out edge. + int root = -1; + for (const int n : subset) { + if (in_subset[parents[n]]) continue; + if (root != -1) EXPECT_EQ(parents[n], root); + root = parents[n]; + } + + // No node outside should point inside. + for (int n = 0; n < num_nodes; ++n) { + if (in_subset[n]) continue; + EXPECT_TRUE(!in_subset[parents[n]]); + } + + for (const int n : subset) in_subset[n] = false; + } +} + +TEST(SymmetrizeArcsTest, BasicTest) { + std::vector arcs{{.tail = 0, .head = 1, .lp_value = 0.5}, + {.tail = 2, .head = 0, .lp_value = 0.5}, + {.tail = 1, .head = 0, .lp_value = 0.5}}; + SymmetrizeArcs(&arcs); + EXPECT_THAT( + arcs, ElementsAre(ArcWithLpValue{.tail = 0, .head = 1, .lp_value = 1.0}, + ArcWithLpValue{.tail = 0, .head = 2, .lp_value = 0.5})); +} + +TEST(ComputeGomoryHuTreeTest, Random) { + absl::BitGen random; + + // Lets generate a random graph on a small number of nodes. + const int num_nodes = 10; + const int num_arcs = 100; + std::vector arcs; + for (int i = 0; i < num_arcs; ++i) { + const int tail = absl::Uniform(random, 0, num_nodes); + const int head = absl::Uniform(random, 0, num_nodes); + if (tail == head) continue; + const double lp_value = absl::Uniform(random, 0, 1); + arcs.push_back({tail, head, lp_value}); + } + + // Get all cut from Gomory-Hu tree. + const std::vector parents = ComputeGomoryHuTree(num_nodes, arcs); + std::vector buffer; + std::vector> subsets; + ExtractAllSubsetsFromForest(parents, &buffer, &subsets); + + // Compute the cost of entering (resp. leaving) each subset. + // TODO(user): We need the same scaling as in ComputeGomoryHu(), not super + // clean. We might want an integer input to the function, but ok for now. + std::vector in_subset(num_nodes, false); + std::vector out_costs(subsets.size(), 0); + std::vector in_costs(subsets.size(), 0); + for (int i = 0; i < subsets.size(); ++i) { + for (const int n : subsets[i]) in_subset[n] = true; + for (const auto& arc : arcs) { + if (in_subset[arc.tail] && !in_subset[arc.head]) { + out_costs[i] += std::round(1.0e6 * arc.lp_value); + } + if (!in_subset[arc.tail] && in_subset[arc.head]) { + in_costs[i] += std::round(1.0e6 * arc.lp_value); + } + } + for (const int n : subsets[i]) in_subset[n] = false; + } + + // We will test with an exhaustive comparison. We are in n ^ 3 ! + // For all (s,t) pair, get the actual max-flow on the scaled graph. + // Check than one of the cuts separate s and t, with this exact weight. + SimpleMaxFlow max_flow; + for (const auto& [tail, head, lp_value] : arcs) { + // TODO(user): the algo only seems to work on an undirected graph, or + // equivalently when we always have a reverse arc with the same weight. + // Note that you can see below that we compute "min" cut for the sum of + // outgoing + incoming arcs this way. + max_flow.AddArcWithCapacity(tail, head, std::round(1.0e6 * lp_value)); + max_flow.AddArcWithCapacity(head, tail, std::round(1.0e6 * lp_value)); + } + for (int s = 0; s < num_nodes; ++s) { + for (int t = s + 1; t < num_nodes; ++t) { + ASSERT_EQ(max_flow.Solve(s, t), SimpleMaxFlow::OPTIMAL); + const int64_t flow = max_flow.OptimalFlow(); + bool found = false; + for (int i = 0; i < subsets.size(); ++i) { + bool s_out = true; + bool t_out = true; + for (const int n : subsets[i]) { + if (n == s) s_out = false; + if (n == t) t_out = false; + } + if (!s_out && t_out && out_costs[i] + in_costs[i] == flow) found = true; + if (s_out && !t_out && in_costs[i] + out_costs[i] == flow) found = true; + if (found) break; + } + + // Debug. + if (!found) { + LOG(INFO) << s << " -> " << t << " flow= " << flow; + for (int i = 0; i < subsets.size(); ++i) { + bool s_out = true; + bool t_out = true; + for (const int n : subsets[i]) { + if (n == s) s_out = false; + if (n == t) t_out = false; + } + if (!s_out && t_out) { + LOG(INFO) << i << " out= " << out_costs[i] + in_costs[i]; + } + if (s_out && !t_out) { + LOG(INFO) << i << " in= " << in_costs[i] + out_costs[i]; + } + } + } + ASSERT_TRUE(found); + } + } +} + +TEST(CreateStronglyConnectedGraphCutGeneratorTest, BasicExample) { + Model model; + + // Lets create a simple square graph with arcs in both directions: + // + // 0 ---- 1 + // | | + // | | + // 2 ---- 3 + const int num_nodes = 4; + std::vector tails{0, 1, 1, 3, 3, 2, 2, 0}; + std::vector heads{1, 0, 3, 1, 2, 3, 0, 2}; + std::vector literals; + std::vector vars; + for (int i = 0; i < 2 * num_nodes; ++i) { + literals.push_back(Literal(model.Add(NewBooleanVariable()), true)); + vars.push_back(model.Add(NewIntegerVariableFromLiteral(literals.back()))); + } + + CutGenerator generator = CreateStronglyConnectedGraphCutGenerator( + num_nodes, tails, heads, literals, &model); + + // Suppose only 0-1 and 2-3 are in the lp solution (values do not matter). + auto& lp_values = *model.GetOrCreate(); + lp_values.resize(16, 0.0); + lp_values[vars[0]] = 0.5; + lp_values[vars[1]] = 0.5; + lp_values[vars[4]] = 1.0; + lp_values[vars[5]] = 1.0; + LinearConstraintManager manager(&model); + generator.generate_cuts(&manager); + + // We should get two cuts. + EXPECT_EQ(manager.num_cuts(), 2); + EXPECT_THAT(manager.AllConstraints().front().constraint.VarsAsSpan(), + ElementsAre(vars[3], vars[6])); + EXPECT_THAT(manager.AllConstraints().back().constraint.VarsAsSpan(), + ElementsAre(vars[2], vars[7])); +} + +TEST(CreateStronglyConnectedGraphCutGeneratorTest, AnotherExample) { + // This time, the graph is fully connected, but we still detect that {1, 2, 3} + // do not have enough outgoing flow: + // + // 0.5 + // 0 <--> 1 + // ^ | 0.5 + // 0.5 | | 1 and 2 ----> 1 + // v v + // 2 <--- 3 + // 1 + const int num_nodes = 4; + std::vector tails{0, 1, 0, 2, 1, 3, 2}; + std::vector heads{1, 0, 2, 0, 3, 2, 1}; + std::vector values{0.5, 0.0, 0.5, 0.0, 1.0, 1.0, 0.5}; + + Model model; + std::vector literals; + auto& lp_values = *model.GetOrCreate(); + lp_values.resize(16, 0.0); + for (int i = 0; i < values.size(); ++i) { + literals.push_back(Literal(model.Add(NewBooleanVariable()), true)); + lp_values[model.Add(NewIntegerVariableFromLiteral(literals.back()))] = + values[i]; + } + + CutGenerator generator = CreateStronglyConnectedGraphCutGenerator( + num_nodes, tails, heads, literals, &model); + + LinearConstraintManager manager(&model); + generator.generate_cuts(&manager); + + // The sets {2, 3} and {1, 2, 3} will generate cuts. + // However as an heuristic, we will wait another round to generate {1, 2 ,3}. + EXPECT_EQ(manager.num_cuts(), 1); + EXPECT_THAT(manager.AllConstraints().back().constraint.DebugString(), + ::testing::StartsWith("1 <= 1*X3 1*X6")); +} + +TEST(GenerateInterestingSubsetsTest, BasicExample) { + const int num_nodes = 6; + const std::vector> arcs = {{0, 5}, {2, 3}, {3, 4}}; + + // Note that the order is not important, but is currently fixed. + // This document the actual order. + std::vector subset_data; + std::vector> subsets; + GenerateInterestingSubsets(num_nodes, arcs, + /*stop_at_num_components=*/2, &subset_data, + &subsets); + EXPECT_THAT( + subsets, + ElementsAre(ElementsAre(1), ElementsAre(5), ElementsAre(0), + ElementsAre(5, 0), ElementsAre(3), ElementsAre(2), + ElementsAre(3, 2), ElementsAre(4), ElementsAre(3, 2, 4))); + + // We can call it more than once. + GenerateInterestingSubsets(num_nodes, arcs, + /*stop_at_num_components=*/2, &subset_data, + &subsets); + EXPECT_THAT( + subsets, + ElementsAre(ElementsAre(1), ElementsAre(5), ElementsAre(0), + ElementsAre(5, 0), ElementsAre(3), ElementsAre(2), + ElementsAre(3, 2), ElementsAre(4), ElementsAre(3, 2, 4))); +} + +TEST(CreateFlowCutGeneratorTest, BasicExample) { + // + // /---> 2 + // 0 ---> 1 ^ + // \---> 3 + // + // With a flow of 2 leaving 0 and a flow of 1 requested at 2 and 3. + // On each arc the flow <= max_flow * arc_indicator where max_flow = 2. + const int num_nodes = 4; + std::vector tails{0, 1, 1, 3}; + std::vector heads{1, 2, 3, 2}; + std::vector values{1.0, 0.5, 0.5, 0.0}; + + Model model; + std::vector capacities; + auto& lp_values = *model.GetOrCreate(); + lp_values.resize(16, 0.0); + for (int i = 0; i < values.size(); ++i) { + AffineExpression expr; + expr.var = model.Add(NewIntegerVariable(0, 1)); + expr.coeff = 2; + expr.constant = 0; + capacities.emplace_back(expr); + lp_values[capacities.back().var] = values[i]; + } + + const auto get_flows = [](const std::vector& in_subset, + IntegerValue* min_incoming_flow, + IntegerValue* min_outgoing_flow) { + IntegerValue demand(0); + if (in_subset[0]) demand -= 2; + if (in_subset[2]) demand += 1; + if (in_subset[3]) demand += 1; + *min_incoming_flow = std::max(IntegerValue(0), demand); + *min_outgoing_flow = std::max(IntegerValue(0), -demand); + }; + const CutGenerator generator = CreateFlowCutGenerator( + num_nodes, tails, heads, capacities, get_flows, &model); + + LinearConstraintManager manager(&model); + generator.generate_cuts(&manager); + + // The sets {2} and {3} will generate incoming flow cuts. + EXPECT_EQ(manager.num_cuts(), 2); + EXPECT_THAT(manager.AllConstraints().front().constraint.DebugString(), + ::testing::StartsWith("1 <= 1*X2")); + EXPECT_THAT(manager.AllConstraints().back().constraint.DebugString(), + ::testing::StartsWith("1 <= 1*X1 1*X3")); +} + +TEST(CreateFlowCutGeneratorTest, WithMinusOneArcs) { + // 0 ---> 1 --> + // | + // \ --> + const int num_nodes = 2; + std::vector tails{0, 1, 1}; + std::vector heads{1, -1, -1}; + std::vector values{1.0, 0.5, 0.0}; + + Model model; + std::vector capacities; + auto& lp_values = *model.GetOrCreate(); + lp_values.resize(16, 0.0); + for (int i = 0; i < values.size(); ++i) { + AffineExpression expr; + expr.var = model.Add(NewIntegerVariable(0, 1)); + expr.coeff = 2; + expr.constant = 0; + capacities.emplace_back(expr); + lp_values[capacities.back().var] = values[i]; + } + + const auto get_flows = [](const std::vector& in_subset, + IntegerValue* min_incoming_flow, + IntegerValue* min_outgoing_flow) { + IntegerValue demand(0); + if (in_subset[0]) demand -= 2; + *min_incoming_flow = std::max(IntegerValue(0), demand); + *min_outgoing_flow = std::max(IntegerValue(0), -demand); + }; + const CutGenerator generator = CreateFlowCutGenerator( + num_nodes, tails, heads, capacities, get_flows, &model); + + LinearConstraintManager manager(&model); + generator.generate_cuts(&manager); + + // We artificially put bad LP values so that {1} generate outgoing flow cut. + EXPECT_EQ(manager.num_cuts(), 1); + EXPECT_THAT(manager.AllConstraints().front().constraint.DebugString(), + ::testing::StartsWith("1 <= 1*X1 1*X2")); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/sat_base_test.cc b/ortools/sat/sat_base_test.cc new file mode 100644 index 0000000000..391efb1d09 --- /dev/null +++ b/ortools/sat/sat_base_test.cc @@ -0,0 +1,74 @@ +// 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/sat_base.h" + +#include + +#include "gtest/gtest.h" +#include "ortools/util/strong_integers.h" + +namespace operations_research { +namespace sat { +namespace { + +TEST(BooleanVariableTest, Api) { + BooleanVariable var1(1); + BooleanVariable var2(2); + BooleanVariable var3(2); + EXPECT_NE(var1, var2); + EXPECT_EQ(var2, var3); +} + +TEST(LiteralTest, Api) { + BooleanVariable var1(1); + BooleanVariable var2(2); + Literal l1(var1, true); + Literal l2(var2, false); + Literal l3 = l2.Negated(); + EXPECT_EQ(l1.Variable(), var1); + EXPECT_EQ(l2.Variable(), var2); + EXPECT_EQ(l3.Variable(), var2); + EXPECT_TRUE(l1.IsPositive()); + EXPECT_TRUE(l2.IsNegative()); + EXPECT_TRUE(l3.IsPositive()); +} + +TEST(VariablesAssignmentTest, Api) { + BooleanVariable var0(0); + BooleanVariable var1(1); + BooleanVariable var2(2); + + VariablesAssignment assignment; + assignment.Resize(3); + assignment.AssignFromTrueLiteral(Literal(var0, true)); + assignment.AssignFromTrueLiteral(Literal(var1, false)); + + EXPECT_TRUE(assignment.LiteralIsTrue(Literal(var0, true))); + EXPECT_TRUE(assignment.LiteralIsFalse(Literal(var0, false))); + EXPECT_TRUE(assignment.LiteralIsTrue(Literal(var1, false))); + EXPECT_FALSE(assignment.VariableIsAssigned(var2)); + + assignment.UnassignLiteral(Literal(var0, true)); + EXPECT_FALSE(assignment.VariableIsAssigned(var0)); + + assignment.AssignFromTrueLiteral(Literal(var2, false)); + EXPECT_TRUE(assignment.LiteralIsTrue(Literal(var2, false))); + EXPECT_FALSE(assignment.LiteralIsTrue(Literal(var2, true))); + EXPECT_TRUE(assignment.LiteralIsFalse(Literal(var2, true))); + EXPECT_FALSE(assignment.LiteralIsFalse(Literal(var2, false))); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/sat_inprocessing_test.cc b/ortools/sat/sat_inprocessing_test.cc new file mode 100644 index 0000000000..291ca0aff9 --- /dev/null +++ b/ortools/sat/sat_inprocessing_test.cc @@ -0,0 +1,287 @@ +// 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/sat_inprocessing.h" + +#include +#include + +#include "absl/container/inlined_vector.h" +#include "absl/log/check.h" +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/sat/clause.h" +#include "ortools/sat/model.h" +#include "ortools/sat/sat_base.h" +#include "ortools/sat/sat_solver.h" + +namespace operations_research { +namespace sat { +namespace { + +TEST(InprocessingTest, ClauseCleanupWithFixedVariables) { + Model model; + auto* sat_solver = model.GetOrCreate(); + auto* clause_manager = model.GetOrCreate(); + auto* inprocessing = model.GetOrCreate(); + + // Lets add some clauses. + sat_solver->SetNumVariables(100); + EXPECT_TRUE(clause_manager->AddClause(Literals({+1, +2, +3, +4}))); + EXPECT_TRUE(clause_manager->AddClause(Literals({+1, -2, -3, +5}))); + EXPECT_TRUE(clause_manager->AddClause(Literals({+2, -2, -3, +1, +1}))); + + // Nothing fixed, we don't even look at the clause. + const bool log_info = true; + EXPECT_TRUE(inprocessing->DetectEquivalencesAndStamp(false, log_info)); + EXPECT_TRUE(inprocessing->RemoveFixedAndEquivalentVariables(log_info)); + { + const auto& all_clauses = clause_manager->AllClausesInCreationOrder(); + EXPECT_EQ(all_clauses.size(), 3); + EXPECT_EQ(all_clauses[2]->AsSpan(), Literals({+2, -2, -3, +1, +1})); + } + + // Lets fix 3. + CHECK(sat_solver->AddUnitClause(Literal(+3))); + EXPECT_TRUE(sat_solver->FinishPropagation()); + EXPECT_TRUE(inprocessing->DetectEquivalencesAndStamp(false, log_info)); + EXPECT_TRUE(inprocessing->RemoveFixedAndEquivalentVariables(log_info)); + { + const auto& all_clauses = clause_manager->AllClausesInCreationOrder(); + EXPECT_EQ(all_clauses.size(), 3); + EXPECT_EQ(all_clauses[0]->AsSpan(), Literals({})); // +3 true. + EXPECT_EQ(all_clauses[1]->AsSpan(), Literals({+1, -2, +5})); + EXPECT_EQ(all_clauses[2]->AsSpan(), Literals({})); // trivially true. + } +} + +TEST(InprocessingTest, ClauseCleanupWithEquivalence) { + Model model; + auto* sat_solver = model.GetOrCreate(); + auto* clause_manager = model.GetOrCreate(); + auto* implication_graph = model.GetOrCreate(); + auto* inprocessing = model.GetOrCreate(); + + // Lets add some clauses. + sat_solver->SetNumVariables(100); + EXPECT_TRUE(clause_manager->AddClause(Literals({+1, +2, +5, +4}))); + EXPECT_TRUE(clause_manager->AddClause(Literals({+1, -2, -3, +5}))); + EXPECT_TRUE(clause_manager->AddClause(Literals({+2, +6, -3, +1, +1}))); + EXPECT_TRUE(clause_manager->AddClause(Literals({+2, +6, -3, +1, -5}))); + + // Lets make 3 and 5 equivalent. + implication_graph->AddBinaryClause(Literal(-3), Literal(+5)); + implication_graph->AddBinaryClause(Literal(+3), Literal(-5)); + + const bool log_info = true; + EXPECT_TRUE(inprocessing->DetectEquivalencesAndStamp(false, log_info)); + EXPECT_TRUE(inprocessing->RemoveFixedAndEquivalentVariables(log_info)); + { + const auto& all_clauses = clause_manager->AllClausesInCreationOrder(); + EXPECT_EQ(all_clauses.size(), 4); + EXPECT_EQ(all_clauses[0]->AsSpan(), Literals({+1, +2, +3, +4})); + EXPECT_EQ(all_clauses[1]->AsSpan(), Literals({})); + EXPECT_EQ(all_clauses[3]->AsSpan(), Literals({+2, +6, -3, +1})); + + // Note that the +1 +1 is not simplified because this clause do not + // need to be rewritten otherwise and we assume initial simplification. + EXPECT_EQ(all_clauses[2]->AsSpan(), Literals({+2, +6, -3, +1, +1})); + } +} + +TEST(InprocessingTest, ClauseSubsumptionAndStrengthening) { + Model model; + auto* sat_solver = model.GetOrCreate(); + auto* clause_manager = model.GetOrCreate(); + auto* inprocessing = model.GetOrCreate(); + + // Lets add some clauses. + // Note that the order currently matter for what is left. + // + // Note that currently the binary clauses are not reprocessed. + // TODO(user): Maybe we should so that we always end up with a reduced set. + sat_solver->SetNumVariables(100); + EXPECT_TRUE(clause_manager->AddClause(Literals({+1, +3, +2}))); + EXPECT_TRUE(clause_manager->AddClause(Literals({+1, +2, -3}))); + EXPECT_TRUE(clause_manager->AddClause(Literals({+1, +3, +2}))); + EXPECT_TRUE(clause_manager->AddClause(Literals({+1, -2, -3}))); + EXPECT_TRUE(clause_manager->AddClause(Literals({+2, +6, -3, +1, +1}))); + EXPECT_TRUE(clause_manager->AddClause(Literals({-3, +6, +2, +1, -5}))); + + const bool log_info = true; + EXPECT_TRUE(inprocessing->DetectEquivalencesAndStamp(false, log_info)); + EXPECT_TRUE(inprocessing->SubsumeAndStrenghtenRound(log_info)); + { + // This function remove empty clauses. + const auto& all_clauses = clause_manager->AllClausesInCreationOrder(); + + // Depending on the order in which clauses are processed (which can + // change as we rely on std::sort()), we have a few cases. + if (all_clauses.size() == 1) { + EXPECT_EQ(all_clauses[0]->AsSpan(), Literals({+1, +2, -3})); + + // We added {+1, +2} and {+1, -3} there. + // TODO(user): make sure we don't add twice the implications. + auto* implication_graph = model.GetOrCreate(); + EXPECT_EQ(implication_graph->num_implications(), 6); + EXPECT_EQ(implication_graph->Implications(Literal(-1)).size(), 3); + EXPECT_THAT(implication_graph->Implications(Literal(-1)), + ::testing::UnorderedElementsAre(Literal(+2), Literal(+2), + Literal(-3))); + } else { + EXPECT_GE(all_clauses.size(), 3); + EXPECT_LE(all_clauses.size(), 4); + EXPECT_EQ(all_clauses[0]->AsSpan(), Literals({+1, +3, +2})); + EXPECT_EQ(all_clauses[1]->AsSpan(), Literals({+1, -2, -3})); + + // Depending on the implication added, we don't get the same clauses. + auto* implication_graph = model.GetOrCreate(); + EXPECT_EQ(implication_graph->num_implications(), 2); + EXPECT_EQ(implication_graph->Implications(Literal(-1)).size(), 1); + if (implication_graph->Implications(Literal(-1))[0] == Literal(+2)) { + EXPECT_EQ(all_clauses[2]->AsSpan(), Literals({+2, +6, +1, +1})); + if (all_clauses.size() == 4) { + EXPECT_EQ(all_clauses[3]->AsSpan(), Literals({+6, +2, +1, -5})); + } + } else { + EXPECT_EQ(implication_graph->Implications(Literal(-1))[0], Literal(-3)); + EXPECT_EQ(all_clauses[2]->AsSpan(), Literals({+6, -3, +1, +1})); + if (all_clauses.size() == 4) { + EXPECT_EQ(all_clauses[3]->AsSpan(), Literals({-3, +6, +1, -5})); + } + } + } + } +} + +TEST(StampingSimplifierTest, StampConstruction) { + Model model; + auto* sat_solver = model.GetOrCreate(); + auto* implication_graph = model.GetOrCreate(); + auto* simplifier = model.GetOrCreate(); + + // Lets add some clauses. + // Note that the order currently matter for what is left. + sat_solver->SetNumVariables(100); + implication_graph->AddImplication(Literal(+1), Literal(+2)); + implication_graph->AddImplication(Literal(+1), Literal(+3)); + implication_graph->AddImplication(Literal(+1), Literal(+4)); + implication_graph->AddImplication(Literal(+2), Literal(+5)); + implication_graph->AddImplication(Literal(+2), Literal(+6)); + implication_graph->AddImplication(Literal(+3), Literal(+7)); + implication_graph->AddImplication(Literal(+4), Literal(+6)); + + EXPECT_TRUE(implication_graph->DetectEquivalences(true)); + + // Lets test some implications. + simplifier->SampleTreeAndFillParent(); + simplifier->ComputeStamps(); + EXPECT_TRUE(simplifier->ImplicationIsInTree(Literal(+1), Literal(+2))); + EXPECT_TRUE(simplifier->ImplicationIsInTree(Literal(+1), Literal(+5))); + EXPECT_TRUE(simplifier->ImplicationIsInTree(Literal(+1), Literal(+6))); + EXPECT_TRUE(simplifier->ImplicationIsInTree(Literal(+1), Literal(+7))); + EXPECT_TRUE(simplifier->ImplicationIsInTree(Literal(-7), Literal(-3))); +} + +TEST(StampingSimplifierTest, BasicSimplification) { + Model model; + auto* sat_solver = model.GetOrCreate(); + auto* clause_manager = model.GetOrCreate(); + auto* implication_graph = model.GetOrCreate(); + auto* simplifier = model.GetOrCreate(); + + // Lets add some clauses. + // Note that the order currently matter for what is left. + sat_solver->SetNumVariables(100); + implication_graph->AddImplication(Literal(+1), Literal(+2)); + implication_graph->AddImplication(Literal(+1), Literal(+3)); + implication_graph->AddImplication(Literal(+1), Literal(+4)); + implication_graph->AddImplication(Literal(+2), Literal(+5)); + implication_graph->AddImplication(Literal(+2), Literal(+6)); + implication_graph->AddImplication(Literal(+3), Literal(+7)); + implication_graph->AddImplication(Literal(+4), Literal(+6)); + + EXPECT_TRUE(implication_graph->DetectEquivalences(true)); + + // Lets add some clause that should be simplifiable + EXPECT_TRUE(clause_manager->AddClause(Literals({+1, +7, +8, +9}))); + EXPECT_TRUE(clause_manager->AddClause(Literals({+1, -6, +8, +9}))); + EXPECT_TRUE(clause_manager->AddClause(Literals({-3, -7, +8, +9}))); + EXPECT_TRUE(clause_manager->AddClause(Literals({-3, +7, +8, +9}))); + + // Lets test some implications. + EXPECT_TRUE(simplifier->DoOneRound(/*log_info=*/true)); + + // Results. I cover all 4 possibilities, 2 strenghtening for clause 0 and 2, + // one subsumption for clause 3 and nothing for clause 1. + const auto& all_clauses = clause_manager->AllClausesInCreationOrder(); + EXPECT_EQ(all_clauses.size(), 4); + EXPECT_EQ(all_clauses[0]->AsSpan(), Literals({+7, +8, +9})); + EXPECT_EQ(all_clauses[1]->AsSpan(), Literals({+1, -6, +8, +9})); + EXPECT_EQ(all_clauses[2]->AsSpan(), Literals({-3, +8, +9})); + EXPECT_EQ(all_clauses[3]->AsSpan(), Literals({})); +} + +TEST(BlockedClauseSimplifierTest, BasicSimplification) { + Model model; + auto* sat_solver = model.GetOrCreate(); + auto* clause_manager = model.GetOrCreate(); + auto* implication_graph = model.GetOrCreate(); + auto* simplifier = model.GetOrCreate(); + + // Lets add some clauses. + // Note that the order currently matter for what is left. + sat_solver->SetNumVariables(100); + implication_graph->AddImplication(Literal(+1), Literal(-7)); + implication_graph->AddImplication(Literal(+1), Literal(-8)); + implication_graph->AddImplication(Literal(+1), Literal(-9)); + + // Lets add some clause that should be blocked + EXPECT_TRUE(clause_manager->AddClause(Literals({-1, +7, -8, +9}))); + EXPECT_TRUE(clause_manager->AddClause(Literals({+1, +7, +8, +9}))); + + simplifier->DoOneRound(/*log_info=*/true); + + clause_manager->DeleteRemovedClauses(); + const auto& all_clauses = clause_manager->AllClausesInCreationOrder(); + EXPECT_EQ(all_clauses.size(), 0); +} + +TEST(BoundedVariableEliminationTest, BasicSimplification) { + Model model; + auto* sat_solver = model.GetOrCreate(); + auto* clause_manager = model.GetOrCreate(); + auto* simplifier = model.GetOrCreate(); + + // Lets add some clauses. + sat_solver->SetNumVariables(100); + EXPECT_TRUE(clause_manager->AddClause(Literals({+1, +2, +3, +7}))); + EXPECT_TRUE(clause_manager->AddClause(Literals({+3, +4, +5, +7}))); + EXPECT_TRUE(clause_manager->AddClause(Literals({-1, +4, +5, -7}))); + EXPECT_TRUE(clause_manager->AddClause(Literals({+3, -2, +5, -7}))); + EXPECT_TRUE(clause_manager->AddClause(Literals({+2, +4, -3, -7}))); + EXPECT_TRUE(clause_manager->AddClause(Literals({+2, +4, -5, -7}))); + EXPECT_TRUE(clause_manager->AddClause(Literals({+2, +3, -4, -7}))); + + simplifier->DoOneRound(/*log_info=*/true); + + // The problem is so simple that everyting should be simplified. + clause_manager->DeleteRemovedClauses(); + const auto& all_clauses = clause_manager->AllClausesInCreationOrder(); + EXPECT_EQ(all_clauses.size(), 0); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/scheduling_cuts_test.cc b/ortools/sat/scheduling_cuts_test.cc new file mode 100644 index 0000000000..23a8b92bac --- /dev/null +++ b/ortools/sat/scheduling_cuts_test.cc @@ -0,0 +1,576 @@ +// 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/scheduling_cuts.h" + +#include + +#include +#include +#include +#include + +#include "absl/base/log_severity.h" +#include "absl/random/random.h" +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/base/strong_vector.h" +#include "ortools/sat/cp_model.h" +#include "ortools/sat/cp_model.pb.h" +#include "ortools/sat/cp_model_solver.h" +#include "ortools/sat/cuts.h" +#include "ortools/sat/integer.h" +#include "ortools/sat/intervals.h" +#include "ortools/sat/linear_constraint.h" +#include "ortools/sat/linear_constraint_manager.h" +#include "ortools/sat/model.h" +#include "ortools/sat/sat_base.h" +#include "ortools/util/strong_integers.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::testing::EndsWith; +using ::testing::StartsWith; + +TEST(CumulativeEnergyCutGenerator, TestCutTimeTableGenerator) { + Model model; + + const IntegerVariable start1 = model.Add(NewIntegerVariable(0, 3)); + const IntegerVariable end1 = model.Add(NewIntegerVariable(7, 10)); + const IntegerVariable size1 = model.Add(NewIntegerVariable(7, 7)); + const IntervalVariable i1 = model.Add(NewInterval(start1, end1, size1)); + + const BooleanVariable b = model.Add(NewBooleanVariable()); + const IntegerVariable b_view = model.Add(NewIntegerVariable(0, 1)); + auto* integer_encoder = model.GetOrCreate(); + integer_encoder->AssociateToIntegerEqualValue(Literal(b, true), b_view, + IntegerValue(1)); + + const IntegerVariable start2 = model.Add(NewIntegerVariable(3, 6)); + const IntegerVariable end2 = model.Add(NewIntegerVariable(10, 13)); + const IntegerVariable size2 = model.Add(NewIntegerVariable(7, 7)); + const IntervalVariable i2 = + model.Add(NewOptionalInterval(start2, end2, size2, Literal(b, true))); + + const IntegerVariable demand1 = model.Add(NewIntegerVariable(5, 10)); + const IntegerVariable demand2 = model.Add(NewIntegerVariable(3, 10)); + const IntegerVariable capacity = model.Add(NewIntegerVariable(10, 10)); + SchedulingConstraintHelper* helper = + model.GetOrCreate()->GetOrCreateHelper({i1, i2}); + SchedulingDemandHelper* demands_helper = + new SchedulingDemandHelper({demand1, demand2}, helper, &model); + model.TakeOwnership(demands_helper); + CutGenerator cumulative = CreateCumulativeTimeTableCutGenerator( + helper, demands_helper, capacity, &model); + LinearConstraintManager* const manager = + model.GetOrCreate(); + const IntegerVariable num_vars = + model.GetOrCreate()->NumIntegerVariables(); + + auto& lp_values = *model.GetOrCreate(); + lp_values.resize(num_vars.value() * 2, 0.0); + lp_values[start1] = 3.0; // x0 + lp_values[end1] = 10.0; // x1 + lp_values[size1] = 7.0; // x2 + lp_values[b_view] = 1.0; // x3 + lp_values[start2] = 6.0; // x4 + lp_values[end2] = 13.0; // x5 + lp_values[size2] = 7.0; // x6 + lp_values[demand1] = 8.0; // x7 + lp_values[demand2] = 7.0; // x8 + lp_values[capacity] = 10.0; // x9 + + cumulative.generate_cuts(manager); + ASSERT_EQ(1, manager->num_cuts()); + + // 3*X3 1*X7 -1*X9 <= 0 -> Normalized to 3*X3 1*X7 <= 10 + EXPECT_THAT(manager->AllConstraints().front().constraint.DebugString(), + EndsWith("3*X3 1*X7 <= 10")); +} + +TEST(CumulativeEnergyCutGenerator, SameDemand) { + Model model; + + const IntegerVariable start1 = model.Add(NewIntegerVariable(0, 3)); + const IntegerVariable end1 = model.Add(NewIntegerVariable(7, 10)); + const IntegerVariable size1 = model.Add(NewIntegerVariable(7, 7)); + const IntervalVariable i1 = model.Add(NewInterval(start1, end1, size1)); + + const IntegerVariable start2 = model.Add(NewIntegerVariable(3, 6)); + const IntegerVariable end2 = model.Add(NewIntegerVariable(10, 13)); + const IntegerVariable size2 = model.Add(NewIntegerVariable(7, 7)); + const IntervalVariable i2 = model.Add(NewInterval(start2, end2, size2)); + + const IntegerVariable start3 = model.Add(NewIntegerVariable(4, 8)); + const IntegerVariable end3 = model.Add(NewIntegerVariable(11, 15)); + const IntegerVariable size3 = model.Add(NewIntegerVariable(7, 7)); + const IntervalVariable i3 = model.Add(NewInterval(start3, end3, size3)); + + const IntegerVariable demand = model.Add(NewIntegerVariable(5, 10)); + const IntegerVariable demand2 = model.Add(NewIntegerVariable(5, 10)); + const IntegerVariable capacity = model.Add(NewIntegerVariable(10, 10)); + + LinearExpression e1; + e1.vars.push_back(demand); + e1.coeffs.push_back(IntegerValue(7)); + LinearExpression e2; + e2.vars.push_back(demand2); + e2.coeffs.push_back(IntegerValue(7)); + + SchedulingConstraintHelper* helper = + model.GetOrCreate()->GetOrCreateHelper({i1, i2, i3}); + SchedulingDemandHelper* demands_helper = + new SchedulingDemandHelper({demand, demand, demand2}, helper, &model); + model.TakeOwnership(demands_helper); + + CutGenerator cumulative = CreateCumulativeEnergyCutGenerator( + helper, demands_helper, capacity, std::optional(), + &model); + LinearConstraintManager* const manager = + model.GetOrCreate(); + const IntegerVariable num_vars = + model.GetOrCreate()->NumIntegerVariables(); + + auto& lp_values = *model.GetOrCreate(); + lp_values.resize(num_vars.value() * 2, 0.0); + lp_values[start1] = 3.0; // x0 + lp_values[end1] = 10.0; // x1 + lp_values[size1] = 7.0; // x2 + lp_values[start2] = 6.0; // x3 + lp_values[end2] = 13.0; // x4 + lp_values[size2] = 7.0; // x5 + lp_values[start3] = 6.0; // x6 + lp_values[end3] = 13.0; // x7 + lp_values[size3] = 7.0; // x8 + lp_values[demand] = 8.0; // x9 + lp_values[demand2] = 8.0; // x10 + lp_values[capacity] = 10.0; // x11 + + cumulative.generate_cuts(manager); + ASSERT_EQ(5, manager->num_cuts()); + + // CumulativeEnergy cut. + EXPECT_THAT( + manager->AllConstraints()[LinearConstraintManager::ConstraintIndex(0)] + .constraint.DebugString(), + EndsWith("1*X9 <= 5")); + EXPECT_THAT( + manager->AllConstraints()[LinearConstraintManager::ConstraintIndex(1)] + .constraint.DebugString(), + EndsWith("1*X9 1*X10 <= 10")); + EXPECT_THAT( + manager->AllConstraints()[LinearConstraintManager::ConstraintIndex(2)] + .constraint.DebugString(), + EndsWith("3*X9 2*X10 <= 30")); + EXPECT_THAT( + manager->AllConstraints()[LinearConstraintManager::ConstraintIndex(3)] + .constraint.DebugString(), + EndsWith("5*X9 2*X10 <= 40")); + EXPECT_THAT( + manager->AllConstraints()[LinearConstraintManager::ConstraintIndex(4)] + .constraint.DebugString(), + EndsWith("2*X9 3*X10 <= 30")); +} + +TEST(CumulativeEnergyCutGenerator, SameDemandTimeTableGenerator) { + Model model; + + const IntegerVariable start1 = model.Add(NewIntegerVariable(0, 3)); + const IntegerVariable end1 = model.Add(NewIntegerVariable(7, 10)); + const IntegerVariable size1 = model.Add(NewIntegerVariable(7, 7)); + const IntervalVariable i1 = model.Add(NewInterval(start1, end1, size1)); + + const IntegerVariable start2 = model.Add(NewIntegerVariable(3, 6)); + const IntegerVariable end2 = model.Add(NewIntegerVariable(10, 13)); + const IntegerVariable size2 = model.Add(NewIntegerVariable(7, 7)); + const IntervalVariable i2 = model.Add(NewInterval(start2, end2, size2)); + + const IntegerVariable start3 = model.Add(NewIntegerVariable(4, 8)); + const IntegerVariable end3 = model.Add(NewIntegerVariable(11, 15)); + const IntegerVariable size3 = model.Add(NewIntegerVariable(7, 7)); + const IntervalVariable i3 = model.Add(NewInterval(start3, end3, size3)); + + const IntegerVariable demand = model.Add(NewIntegerVariable(5, 10)); + const IntegerVariable demand2 = model.Add(NewIntegerVariable(5, 10)); + const IntegerVariable capacity = model.Add(NewIntegerVariable(10, 10)); + + SchedulingConstraintHelper* helper = + model.GetOrCreate()->GetOrCreateHelper({i1, i2, i3}); + SchedulingDemandHelper* demands_helper = + new SchedulingDemandHelper({demand, demand, demand2}, helper, &model); + model.TakeOwnership(demands_helper); + CutGenerator cumulative = CreateCumulativeTimeTableCutGenerator( + helper, demands_helper, capacity, &model); + LinearConstraintManager* const manager = + model.GetOrCreate(); + const IntegerVariable num_vars = + model.GetOrCreate()->NumIntegerVariables(); + + auto& lp_values = *model.GetOrCreate(); + lp_values.resize(num_vars.value() * 2, 0.0); + lp_values[start1] = 3.0; // x0 + lp_values[end1] = 10.0; // x1 + lp_values[size1] = 7.0; // x2 + lp_values[start2] = 6.0; // x3 + lp_values[end2] = 13.0; // x4 + lp_values[size2] = 7.0; // x5 + lp_values[start3] = 6.0; // x6 + lp_values[end3] = 13.0; // x7 + lp_values[size3] = 7.0; // x8 + lp_values[demand] = 8.0; // x9 + lp_values[demand2] = 8.0; // x10 + lp_values[capacity] = 10.0; // x11 + + cumulative.generate_cuts(manager); + ASSERT_EQ(2, manager->num_cuts()); + + // 1*X9 1*X9 <= X11 -> Normalized to 1*X9 <= 5 + EXPECT_THAT(manager->AllConstraints().front().constraint.DebugString(), + EndsWith("1*X9 <= 5")); + // 1*X9 1*X10 <= X11 -> Normalized to 1*X9 1*X10 <= 10 + EXPECT_THAT(manager->AllConstraints().back().constraint.DebugString(), + EndsWith("1*X9 1*X10 <= 10")); +} + +TEST(CumulativeEnergyCutGenerator, DetectedPrecedence) { + Model model; + auto* intervals_repository = model.GetOrCreate(); + + const IntegerValue one(1); + const IntegerVariable start1 = model.Add(NewIntegerVariable(0, 3)); + const IntegerValue size1(3); + const IntervalVariable i1 = intervals_repository->CreateInterval( + start1, AffineExpression(start1, one, size1), AffineExpression(size1), + kNoLiteralIndex, /*add_linear_relation=*/false); + + const IntegerVariable start2 = model.Add(NewIntegerVariable(1, 5)); + const IntegerValue size2(4); + const IntervalVariable i2 = intervals_repository->CreateInterval( + start2, AffineExpression(start2, one, size2), AffineExpression(size2), + kNoLiteralIndex, /*add_linear_relation=*/false); + CutGenerator disjunctive = CreateNoOverlapPrecedenceCutGenerator( + intervals_repository->GetOrCreateHelper({ + i1, + i2, + }), + &model); + LinearConstraintManager* const manager = + model.GetOrCreate(); + const IntegerVariable num_vars = + model.GetOrCreate()->NumIntegerVariables(); + + auto& lp_values = *model.GetOrCreate(); + lp_values.resize(num_vars.value() * 2, 0.0); + lp_values[start1] = 0.0; + lp_values[NegationOf(start1)] = 0.0; + lp_values[start2] = 2.0; + lp_values[NegationOf(start2)] = -2.0; + + disjunctive.generate_cuts(manager); + ASSERT_EQ(1, manager->num_cuts()); + + EXPECT_THAT(manager->AllConstraints().front().constraint.DebugString(), + EndsWith("1*X0 -1*X1 <= -3")); +} + +TEST(CumulativeEnergyCutGenerator, DetectedPrecedenceRev) { + Model model; + auto* intervals_repository = model.GetOrCreate(); + + const IntegerValue one(1); + const IntegerVariable start1 = model.Add(NewIntegerVariable(0, 3)); + const IntegerValue size1(3); + const IntervalVariable i1 = intervals_repository->CreateInterval( + start1, AffineExpression(start1, one, size1), AffineExpression(size1), + kNoLiteralIndex, /*add_linear_relation=*/false); + + const IntegerVariable start2 = model.Add(NewIntegerVariable(1, 5)); + const IntegerValue size2(4); + const IntervalVariable i2 = intervals_repository->CreateInterval( + start2, AffineExpression(start2, one, size2), AffineExpression(size2), + kNoLiteralIndex, /*add_linear_relation=*/false); + + CutGenerator disjunctive = CreateNoOverlapPrecedenceCutGenerator( + intervals_repository->GetOrCreateHelper({ + i2, + i1, + }), + &model); + LinearConstraintManager* const manager = + model.GetOrCreate(); + const IntegerVariable num_vars = + model.GetOrCreate()->NumIntegerVariables(); + + auto& lp_values = *model.GetOrCreate(); + lp_values.resize(num_vars.value() * 2, 0.0); + lp_values[start1] = 0.0; + lp_values[NegationOf(start1)] = 0.0; + lp_values[start2] = 2.0; + lp_values[NegationOf(start2)] = -2.0; + + disjunctive.generate_cuts(manager); + ASSERT_EQ(1, manager->num_cuts()); + + EXPECT_THAT(manager->AllConstraints().front().constraint.DebugString(), + EndsWith("1*X0 -1*X1 <= -3")); +} + +TEST(CumulativeEnergyCutGenerator, DisjunctionOnStart) { + Model model; + auto* intervals_repository = model.GetOrCreate(); + + const IntegerValue one(1); + const IntegerVariable start1 = model.Add(NewIntegerVariable(0, 5)); + const IntegerValue size1(3); + const IntervalVariable i1 = intervals_repository->CreateInterval( + start1, AffineExpression(start1, one, size1), AffineExpression(size1), + kNoLiteralIndex, /*add_linear_relation=*/false); + + const IntegerVariable start2 = model.Add(NewIntegerVariable(1, 5)); + const IntegerValue size2(4); + const IntervalVariable i2 = intervals_repository->CreateInterval( + start2, AffineExpression(start2, one, size2), AffineExpression(size2), + kNoLiteralIndex, /*add_linear_relation=*/false); + + CutGenerator disjunctive = CreateNoOverlapPrecedenceCutGenerator( + intervals_repository->GetOrCreateHelper({ + i2, + i1, + }), + &model); + LinearConstraintManager* const manager = + model.GetOrCreate(); + const IntegerVariable num_vars = + model.GetOrCreate()->NumIntegerVariables(); + + auto& lp_values = *model.GetOrCreate(); + lp_values.resize(num_vars.value() * 2, 0.0); + lp_values[start1] = 0.0; + lp_values[NegationOf(start1)] = 0.0; + lp_values[start2] = 2.0; + lp_values[NegationOf(start2)] = -2.0; + + disjunctive.generate_cuts(manager); + ASSERT_EQ(1, manager->num_cuts()); + + EXPECT_THAT(manager->AllConstraints().front().constraint.DebugString(), + StartsWith("15 <= 2*X0 5*X1")); +} + +TEST(ComputeMinSumOfEndMinsTest, CombinationOf3) { + Model model; + auto* intervals_repository = model.GetOrCreate(); + + IntegerValue one(1); + IntegerValue two(2); + + const IntegerVariable start1 = model.Add(NewIntegerVariable(0, 10)); + const IntegerValue size1(3); + const IntervalVariable i1 = intervals_repository->CreateInterval( + start1, AffineExpression(start1, one, size1), size1, kNoLiteralIndex, + /*add_linear_relation=*/false); + + const IntegerVariable start2 = model.Add(NewIntegerVariable(0, 10)); + const IntegerValue size2(4); + const IntervalVariable i2 = intervals_repository->CreateInterval( + start2, AffineExpression(start2, one, size2), size2, kNoLiteralIndex, + /*add_linear_relation=*/false); + + const IntegerVariable start3 = model.Add(NewIntegerVariable(0, 10)); + const IntegerValue size3(5); + const IntervalVariable i3 = intervals_repository->CreateInterval( + start3, AffineExpression(start3, one, size3), size3, kNoLiteralIndex, + /*add_linear_relation=*/false); + + SchedulingConstraintHelper* helper = + model.GetOrCreate()->GetOrCreateHelper({i1, i2, i3}); + CtEvent e1(0, helper); + e1.y_size_min = two; + CtEvent e2(1, helper); + e2.y_size_min = one; + CtEvent e3(2, helper); + e3.y_size_min = one; + std::vector events = {{0, e1}, {1, e2}, {1, e3}}; + + IntegerValue min_sum_of_end_mins(0); + IntegerValue min_sum_of_weighted_end_mins(0); + ASSERT_TRUE(ComputeMinSumOfWeightedEndMins( + events, two, min_sum_of_end_mins, min_sum_of_weighted_end_mins, + kMinIntegerValue, kMinIntegerValue)); + EXPECT_EQ(min_sum_of_end_mins, 17); + EXPECT_EQ(min_sum_of_weighted_end_mins, 21); +} + +TEST(ComputeMinSumOfEndMinsTest, CombinationOf3ConstraintStart) { + Model model; + auto* intervals_repository = model.GetOrCreate(); + + IntegerValue one(1); + IntegerValue two(2); + + const IntegerVariable start1 = model.Add(NewIntegerVariable(0, 3)); + const IntegerValue size1(3); + const IntervalVariable i1 = intervals_repository->CreateInterval( + start1, AffineExpression(start1, one, size1), size1, kNoLiteralIndex, + /*add_linear_relation=*/false); + + const IntegerVariable start2 = model.Add(NewIntegerVariable(0, 10)); + const IntegerValue size2(4); + const IntervalVariable i2 = intervals_repository->CreateInterval( + start2, AffineExpression(start2, one, size2), size2, kNoLiteralIndex, + /*add_linear_relation=*/false); + + const IntegerVariable start3 = model.Add(NewIntegerVariable(0, 10)); + const IntegerValue size3(5); + const IntervalVariable i3 = intervals_repository->CreateInterval( + start3, AffineExpression(start3, one, size3), size3, kNoLiteralIndex, + /*add_linear_relation=*/false); + + SchedulingConstraintHelper* helper = + model.GetOrCreate()->GetOrCreateHelper({i1, i2, i3}); + CtEvent e1(0, helper); + e1.y_size_min = two; + CtEvent e2(1, helper); + e2.y_size_min = one; + CtEvent e3(2, helper); + e3.y_size_min = one; + std::vector events = {{0, e1}, {1, e2}, {2, e3}}; + + IntegerValue min_sum_of_end_mins(0); + IntegerValue min_sum_of_weighted_end_mins(0); + ASSERT_TRUE(ComputeMinSumOfWeightedEndMins( + events, two, min_sum_of_end_mins, min_sum_of_weighted_end_mins, + kMinIntegerValue, kMinIntegerValue)); + EXPECT_EQ(min_sum_of_end_mins, 18); + EXPECT_EQ(min_sum_of_weighted_end_mins, 21); +} + +TEST(ComputeMinSumOfEndMinsTest, Infeasible) { + Model model; + auto* intervals_repository = model.GetOrCreate(); + + IntegerValue one(1); + IntegerValue two(2); + + const IntegerVariable start1 = model.Add(NewIntegerVariable(1, 3)); + const IntegerValue size1(3); + const IntervalVariable i1 = intervals_repository->CreateInterval( + start1, AffineExpression(start1, one, size1), size1, kNoLiteralIndex, + /*add_linear_relation=*/false); + + const IntegerVariable start2 = model.Add(NewIntegerVariable(0, 3)); + const IntegerValue size2(4); + const IntervalVariable i2 = intervals_repository->CreateInterval( + start2, AffineExpression(start2, one, size2), size2, kNoLiteralIndex, + /*add_linear_relation=*/false); + + const IntegerVariable start3 = model.Add(NewIntegerVariable(0, 3)); + const IntegerValue size3(5); + const IntervalVariable i3 = intervals_repository->CreateInterval( + start3, AffineExpression(start3, one, size3), size3, kNoLiteralIndex, + /*add_linear_relation=*/false); + + SchedulingConstraintHelper* helper = + model.GetOrCreate()->GetOrCreateHelper({i1, i2, i3}); + CtEvent e1(0, helper); + e1.y_size_min = two; + CtEvent e2(1, helper); + e2.y_size_min = one; + CtEvent e3(2, helper); + e3.y_size_min = one; + std::vector events = {{0, e1}, {1, e2}, {2, e3}}; + + IntegerValue min_sum_of_end_mins(0); + IntegerValue min_sum_of_weighted_end_mins(0); + ASSERT_FALSE(ComputeMinSumOfWeightedEndMins( + events, two, min_sum_of_end_mins, min_sum_of_weighted_end_mins, + kMinIntegerValue, kMinIntegerValue)); +} + +int64_t ExactMakespan(const std::vector& sizes, std::vector& demands, + int capacity) { + const int64_t kHorizon = 1000; + CpModelBuilder builder; + LinearExpr obj; + CumulativeConstraint cumul = builder.AddCumulative(capacity); + for (int i = 0; i < sizes.size(); ++i) { + IntVar s = builder.NewIntVar({0, kHorizon}); + IntervalVar v = builder.NewFixedSizeIntervalVar(s, sizes[i]); + obj += s + sizes[i]; + cumul.AddDemand(v, demands[i]); + } + builder.Minimize(obj); + const CpSolverResponse response = + SolveWithParameters(builder.Build(), "num_search_workers:8"); + EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); + return static_cast(response.objective_value()); +} + +int64_t ExactMakespanBruteForce(absl::Span sizes, + std::vector& demands, int capacity) { + const int64_t kHorizon = 1000; + Model model; + auto* intervals_repository = model.GetOrCreate(); + IntegerValue one(1); + + std::vector intervals; + for (int i = 0; i < sizes.size(); ++i) { + const IntegerVariable start = model.Add(NewIntegerVariable(0, kHorizon)); + const IntegerValue size(sizes[i]); + const IntervalVariable interval = intervals_repository->CreateInterval( + start, AffineExpression(start, one, size), size, kNoLiteralIndex, + /*add_linear_relation=*/false); + intervals.push_back(interval); + } + + SchedulingConstraintHelper* helper = + model.GetOrCreate()->GetOrCreateHelper(intervals); + std::vector events; + for (int i = 0; i < demands.size(); ++i) { + CtEvent e(i, helper); + e.y_size_min = demands[i]; + events.emplace_back(i, e); + } + + IntegerValue min_sum_of_end_mins(0); + IntegerValue min_sum_of_weighted_end_mins(0); + EXPECT_TRUE(ComputeMinSumOfWeightedEndMins( + events, IntegerValue(capacity), min_sum_of_end_mins, + min_sum_of_weighted_end_mins, kMinIntegerValue, kMinIntegerValue)); + return min_sum_of_end_mins.value(); +} + +TEST(ComputeMinSumOfEndMinsTest, RandomCases) { + absl::BitGen random; + const int kNumTests = DEBUG_MODE ? 100 : 1000; + const int kNumTasks = 7; + for (int loop = 0; loop < kNumTests; ++loop) { + const int capacity = absl::Uniform(random, 10, 30); + std::vector sizes; + std::vector demands; + for (int t = 0; t < kNumTasks; ++t) { + sizes.push_back(absl::Uniform(random, 2, 15)); + demands.push_back(absl::Uniform(random, 1, capacity)); + } + + EXPECT_EQ(ExactMakespan(sizes, demands, capacity), + ExactMakespanBruteForce(sizes, demands, capacity)); + } +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/subsolver_test.cc b/ortools/sat/subsolver_test.cc new file mode 100644 index 0000000000..06f549cfe7 --- /dev/null +++ b/ortools/sat/subsolver_test.cc @@ -0,0 +1,105 @@ +// 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/subsolver.h" + +#include +#include +#include +#include +#include + +#include "absl/synchronization/mutex.h" +#include "gtest/gtest.h" + +namespace operations_research { +namespace sat { +namespace { + +// Just a trivial example showing how to use the DeterministicLoop() and +// NonDeterministicLoop() functions. +template +void TestLoopFunction() { + struct GlobalState { + int num_task = 0; + const int limit = 100; + + absl::Mutex mutex; + std::vector updates; + + // This one will be always the same after each batch of task. + int64_t max_update_value = 0; + }; + + class TestSubSolver : public SubSolver { + public: + explicit TestSubSolver(GlobalState* state) + : SubSolver("test", FULL_PROBLEM), state_(state) {} + + bool TaskIsAvailable() override { + // Note that the lock is only needed for the non-deterministic test. + absl::MutexLock mutex_lock(&state_->mutex); + return state_->num_task < state_->limit; + } + + std::function GenerateTask(int64_t id) override { + { + // Note that the lock is only needed for the non-deterministic test. + absl::MutexLock mutex_lock(&state_->mutex); + state_->num_task++; + } + return [this, id] { + absl::MutexLock mutex_lock(&state_->mutex); + state_->updates.push_back(id); + }; + } + + void Synchronize() override { + // Note that the lock is only needed for the non-deterministic test. + absl::MutexLock mutex_lock(&state_->mutex); + for (const int64_t i : state_->updates) { + state_->max_update_value = std::max(state_->max_update_value, i); + } + state_->updates.clear(); + } + + private: + GlobalState* state_; + }; + + GlobalState state; + + // The number of subsolver can be independent of the number of threads. Here + // there is actually no need to have 3 of them except for testing the feature. + std::vector> subsolvers; + for (int i = 0; i < 3; ++i) { + subsolvers.push_back(std::make_unique(&state)); + } + + const int num_threads = 4; + if (deterministic) { + const int batch_size = 20; + DeterministicLoop(subsolvers, num_threads, batch_size); + } else { + NonDeterministicLoop(subsolvers, num_threads); + } + EXPECT_EQ(state.max_update_value, state.limit - 1); +} + +TEST(DeterministicLoop, BasicTest) { TestLoopFunction(); } + +TEST(NonDeterministicLoop, BasicTest) { TestLoopFunction(); } + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/symmetry_test.cc b/ortools/sat/symmetry_test.cc new file mode 100644 index 0000000000..7bfe774df0 --- /dev/null +++ b/ortools/sat/symmetry_test.cc @@ -0,0 +1,151 @@ +// 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/symmetry.h" + +#include +#include +#include +#include +#include + +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/algorithms/sparse_permutation.h" +#include "ortools/base/gmock.h" +#include "ortools/sat/sat_base.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::testing::ElementsAre; + +TEST(SymmetryPropagatorTest, Permute) { + const int num_variables = 6; + const int num_literals = 2 * num_variables; + std::unique_ptr perm(new SparsePermutation(num_literals)); + perm->AddToCurrentCycle(Literal(+3).Index().value()); + perm->AddToCurrentCycle(Literal(+2).Index().value()); + perm->AddToCurrentCycle(Literal(-4).Index().value()); + perm->CloseCurrentCycle(); + // Note that the permutation 'p' must be compatible with the negation. + // That is negation(p(l)) = p(negation(l)). This is actually not required + // for this test though. + perm->AddToCurrentCycle(Literal(-3).Index().value()); + perm->AddToCurrentCycle(Literal(-2).Index().value()); + perm->AddToCurrentCycle(Literal(+4).Index().value()); + perm->CloseCurrentCycle(); + + Trail trail; + SymmetryPropagator propagator; + propagator.AddSymmetry(std::move(perm)); + trail.RegisterPropagator(&propagator); + + std::vector literals = Literals({+1, +2, -2, +3}); + std::vector output; + propagator.Permute(0, literals, &output); + EXPECT_THAT(output, + ElementsAre(Literal(+1), Literal(-4), Literal(+4), Literal(+2))); +} + +TEST(SymmetryPropagatorTest, BasicTest) { + const int num_variables = 6; + const int num_literals = 2 * num_variables; + std::unique_ptr perm(new SparsePermutation(num_literals)); + perm->AddToCurrentCycle(Literal(+3).Index().value()); + perm->AddToCurrentCycle(Literal(+2).Index().value()); + perm->AddToCurrentCycle(Literal(-4).Index().value()); + perm->CloseCurrentCycle(); + // Note that the permutation 'p' must be compatible with the negation. + // That is negation(p(l)) = p(negation(l)). + perm->AddToCurrentCycle(Literal(-3).Index().value()); + perm->AddToCurrentCycle(Literal(-2).Index().value()); + perm->AddToCurrentCycle(Literal(+4).Index().value()); + perm->CloseCurrentCycle(); + perm->AddToCurrentCycle(Literal(-5).Index().value()); + perm->AddToCurrentCycle(Literal(+5).Index().value()); + perm->CloseCurrentCycle(); + + Trail trail; + trail.Resize(num_variables); + SymmetryPropagator propagator; + propagator.AddSymmetry(std::move(perm)); + trail.RegisterPropagator(&propagator); + + // We need a mock propagator to inject a reason. + struct MockPropagator : SatPropagator { + MockPropagator() : SatPropagator("MockPropagator") {} + bool Propagate(Trail* trail) final { return true; } + absl::Span Reason(const Trail& /*trail*/, + int /*trail_index*/, + int64_t /*conflict_id*/) const final { + return reason; + } + std::vector reason; + }; + MockPropagator mock_propagator; + trail.RegisterPropagator(&mock_propagator); + + // With such a trail, nothing should propagate because the first non-symmetric + // literal +3 is a decision. + trail.Enqueue(Literal(+3), AssignmentType::kSearchDecision); + trail.Enqueue(Literal(-5), mock_propagator.PropagatorId()); + while (!propagator.PropagationIsDone(trail)) { + EXPECT_TRUE(propagator.Propagate(&trail)); + } + EXPECT_EQ(trail.Index(), 2); + + // Now we take the decision +2 (which is the image of +3). + trail.Enqueue(Literal(+2), AssignmentType::kUnitReason); + + // We need to initialize the reason for -5, because it will be needed during + // the conflict creation that the Propagate() below will trigger. + mock_propagator.reason = Literals({-3}); + + // Because -5 is now the first non-symmetric literal, a conflict is detected + // since +5 can then be propagated. + EXPECT_FALSE(propagator.PropagationIsDone(trail)); + EXPECT_FALSE(propagator.Propagate(&trail)); + + // Let assume that the reason for -5 is the assignment +3 (which make sense + // since it was propagated). The expected conflict is as stated below because + // if -5 and +2 are true, by summetry since we had +3 => -5 we know that +2 => + // 5. + // + // Note: by convention all the literals of a reason or a conflict are false. + EXPECT_THAT(trail.FailingClause(), ElementsAre(Literal(-2), Literal(+5))); + + // Let backtrack to the trail to +3. + trail.Untrail(trail.Index() - 2); + propagator.Untrail(trail, trail.Index()); + + // Let now assume that +3 => +2, by symmetry we can also propagate -4! + while (!propagator.PropagationIsDone(trail)) { + EXPECT_TRUE(propagator.Propagate(&trail)); + } + EXPECT_EQ(trail.Index(), 1); + trail.Enqueue(Literal(+2), mock_propagator.PropagatorId()); + EXPECT_FALSE(propagator.PropagationIsDone(trail)); + EXPECT_TRUE(propagator.Propagate(&trail)); + EXPECT_EQ(trail.Index(), 3); + EXPECT_EQ(trail[2], Literal(-4)); + + // Once again, if the reason for +2 was the assignment +3, we can compute + // the reason for the assignment -4 (it is just the symmetric of the other). + EXPECT_THAT(trail.Reason(Literal(-4).Variable()), ElementsAre(Literal(-2))); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/theta_tree_test.cc b/ortools/sat/theta_tree_test.cc new file mode 100644 index 0000000000..3e3ebb8964 --- /dev/null +++ b/ortools/sat/theta_tree_test.cc @@ -0,0 +1,291 @@ +// 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/theta_tree.h" + +#include +#include +#include +#include + +#include "benchmark/benchmark.h" +#include "gtest/gtest.h" +#include "ortools/sat/integer.h" +#include "ortools/util/random_engine.h" +#include "ortools/util/strong_integers.h" + +namespace operations_research { +namespace sat { +namespace { + +template +class ThetaTreeTest : public ::testing::Test {}; + +using IntegerTypes = ::testing::Types; +TYPED_TEST_SUITE(ThetaTreeTest, IntegerTypes); + +TYPED_TEST(ThetaTreeTest, EnvelopeOfEmptySet) { + ThetaLambdaTree tree; + tree.Reset(0); + EXPECT_EQ(IntegerTypeMinimumValue(), tree.GetEnvelope()); +} + +template +std::vector IntegerTypeVector(std::vector arg) { + return std::vector(arg.begin(), arg.end()); +} + +TYPED_TEST(ThetaTreeTest, Envelope) { + ThetaLambdaTree tree; + std::vector envelope = + IntegerTypeVector({-10, -7, -6, -4, -2}); + std::vector energy = IntegerTypeVector({2, 1, 3, 2, 2}); + tree.Reset(5); + + for (int i = 0; i < 5; i++) { + tree.AddOrUpdateEvent(i, envelope[i], energy[i], energy[i]); + } + EXPECT_EQ(1, tree.GetEnvelope()); // (-7) + (1+3+2+2) or (-6) + (3+2+2) + EXPECT_EQ(2, tree.GetMaxEventWithEnvelopeGreaterThan(TypeParam(0))); + EXPECT_EQ(4, tree.GetMaxEventWithEnvelopeGreaterThan(TypeParam(-1))); + EXPECT_EQ(0, tree.GetEnvelopeOf(0)); + EXPECT_EQ(1, tree.GetEnvelopeOf(1)); + EXPECT_EQ(1, tree.GetEnvelopeOf(2)); + EXPECT_EQ(0, tree.GetEnvelopeOf(3)); + EXPECT_EQ(0, tree.GetEnvelopeOf(4)); +} + +TYPED_TEST(ThetaTreeTest, EnvelopeOpt) { + ThetaLambdaTree tree; + std::vector envelope = + IntegerTypeVector({-10, -7, -6, -4, -2}); + std::vector energy = IntegerTypeVector({2, 1, 3, 3, 2}); + tree.Reset(5); + + int event, optional_event; + TypeParam energy_max; + + tree.AddOrUpdateEvent(0, envelope[0], energy[0], energy[0]); + tree.AddOrUpdateEvent(1, envelope[1], energy[1], energy[1]); + tree.AddOrUpdateEvent(3, envelope[3], TypeParam(0), energy[3]); + tree.AddOrUpdateEvent(4, envelope[4], energy[4], energy[4]); + EXPECT_EQ(1, tree.GetOptionalEnvelope()); + + tree.GetEventsWithOptionalEnvelopeGreaterThan(TypeParam(0), &event, + &optional_event, &energy_max); + EXPECT_EQ(3, event); + EXPECT_EQ(3, optional_event); + EXPECT_EQ(2, energy_max); + + tree.RemoveEvent(4); + tree.AddOrUpdateEvent(2, envelope[2], energy[2], energy[2]); + EXPECT_EQ(0, tree.GetOptionalEnvelope()); + tree.GetEventsWithOptionalEnvelopeGreaterThan(TypeParam(-1), &event, + &optional_event, &energy_max); + EXPECT_EQ(2, event); + EXPECT_EQ(3, optional_event); + EXPECT_EQ(2, energy_max); + EXPECT_EQ(-4, tree.GetEnvelopeOf(0)); + EXPECT_EQ(-3, tree.GetEnvelopeOf(1)); + EXPECT_EQ(-3, tree.GetEnvelopeOf(2)); +} + +TYPED_TEST(ThetaTreeTest, EnvelopeOptWithAddOptional) { + ThetaLambdaTree tree; + std::vector envelope = + IntegerTypeVector({-10, -7, -6, -4, -2}); + std::vector energy = IntegerTypeVector({2, 1, 3, 3, 2}); + tree.Reset(5); + + int event, optional_event; + TypeParam energy_max; + + tree.AddOrUpdateEvent(0, envelope[0], energy[0], energy[0]); + tree.AddOrUpdateEvent(1, envelope[1], energy[1], energy[1]); + tree.AddOrUpdateOptionalEvent(3, envelope[3], energy[3]); + tree.AddOrUpdateEvent(4, envelope[4], energy[4], energy[4]); + EXPECT_EQ(1, tree.GetOptionalEnvelope()); + + tree.GetEventsWithOptionalEnvelopeGreaterThan(TypeParam(0), &event, + &optional_event, &energy_max); + EXPECT_EQ(3, event); + EXPECT_EQ(3, optional_event); + EXPECT_EQ(2, energy_max); + + tree.RemoveEvent(4); + tree.AddOrUpdateEvent(2, envelope[2], energy[2], energy[2]); + EXPECT_EQ(0, tree.GetOptionalEnvelope()); + tree.GetEventsWithOptionalEnvelopeGreaterThan(TypeParam(-1), &event, + &optional_event, &energy_max); + EXPECT_EQ(2, event); + EXPECT_EQ(3, optional_event); + EXPECT_EQ(2, energy_max); + EXPECT_EQ(-4, tree.GetEnvelopeOf(0)); + EXPECT_EQ(-3, tree.GetEnvelopeOf(1)); + EXPECT_EQ(-3, tree.GetEnvelopeOf(2)); +} + +TYPED_TEST(ThetaTreeTest, AddingAndGettingOptionalEvents) { + ThetaLambdaTree tree; + std::vector envelope = + IntegerTypeVector({0, 3, 4, 6, 8}); + std::vector energy = IntegerTypeVector({2, 1, 3, 3, 2}); + tree.Reset(5); + + tree.AddOrUpdateEvent(0, envelope[0], energy[0], energy[0]); + tree.AddOrUpdateEvent(1, envelope[1], energy[1], energy[1]); + EXPECT_EQ(4, tree.GetEnvelope()); + + // Even with 0 energy, standard update takes task 3's envelope into account. + tree.AddOrUpdateEvent(3, envelope[3], TypeParam(0), energy[3]); + EXPECT_EQ(6, tree.GetEnvelope()); + EXPECT_EQ(9, tree.GetOptionalEnvelope()); + tree.RemoveEvent(3); + + // Changing task 3 to optional makes it disappear from GetEnvelope(). + tree.AddOrUpdateOptionalEvent(3, envelope[3], energy[3]); + EXPECT_EQ(4, tree.GetEnvelope()); // Same as before adding task 3. + EXPECT_EQ(9, tree.GetOptionalEnvelope()); + + // Changing task 3 to optional changes its optional values. + tree.AddOrUpdateEvent(3, envelope[3], TypeParam(1), TypeParam(9)); + tree.AddOrUpdateOptionalEvent(3, envelope[3], energy[3]); + EXPECT_EQ(4, tree.GetEnvelope()); + EXPECT_EQ(9, tree.GetOptionalEnvelope()); +} + +TYPED_TEST(ThetaTreeTest, RemoveAndDelayedAddOrUpdateEventTest) { + ThetaLambdaTree tree; + // The tree encoding is tricky, check that RecomputeTreeForDelayedOperations() + // works for all values from a power of two until the next. + for (int num_events = 4; num_events < 8; ++num_events) { + tree.Reset(num_events); + std::vector envelope; + std::vector energy; + // Event start envelope = event, energy min = 2, energy max = 3 + for (int event = 0; event < num_events; ++event) { + envelope.push_back(TypeParam{event}); + energy.push_back(TypeParam{2}); + } + EXPECT_EQ(tree.GetEnvelope(), IntegerTypeMinimumValue()); + EXPECT_EQ(tree.GetOptionalEnvelope(), IntegerTypeMinimumValue()); + // Envelope of events [0, i) is (0) + 2 * i. + for (int event = 0; event < num_events; ++event) { + tree.DelayedAddOrUpdateEvent(event, envelope[event], energy[event], + energy[event] + 1); + tree.RecomputeTreeForDelayedOperations(); + EXPECT_EQ(tree.GetEnvelope(), 2 * (event + 1)); + EXPECT_EQ(tree.GetOptionalEnvelope(), 2 * (event + 1) + 1); + } + // Envelope of events [i, n) is (n-1) + 2 + (n - i) + for (int event = 0; event < num_events; ++event) { + EXPECT_EQ(tree.GetEnvelope(), 2 * num_events - event); + EXPECT_EQ(tree.GetOptionalEnvelope(), 2 * num_events - event + 1); + tree.DelayedRemoveEvent(event); + tree.RecomputeTreeForDelayedOperations(); + } + EXPECT_EQ(tree.GetEnvelope(), IntegerTypeMinimumValue()); + EXPECT_EQ(tree.GetOptionalEnvelope(), IntegerTypeMinimumValue()); + } +} + +TYPED_TEST(ThetaTreeTest, DelayedAddOrUpdateOptionalEventTest) { + ThetaLambdaTree tree; + // The tree encoding is tricky, check that RecomputeTreeForDelayedOperations() + // works for all values from a power of two until the next. + for (int num_events = 4; num_events < 8; ++num_events) { + tree.Reset(num_events); + std::vector envelope; + std::vector energy; + // Event start envelope = event, event energy max = 2. + for (int event = 0; event < num_events; ++event) { + envelope.push_back(TypeParam{event}); + energy.push_back(TypeParam{2}); + } + EXPECT_EQ(tree.GetEnvelope(), IntegerTypeMinimumValue()); + EXPECT_EQ(tree.GetOptionalEnvelope(), IntegerTypeMinimumValue()); + // Optional envelope of events [0, i) is i + 2. + for (int event = 0; event < num_events; ++event) { + tree.DelayedAddOrUpdateOptionalEvent(event, envelope[event], + energy[event]); + tree.RecomputeTreeForDelayedOperations(); + EXPECT_EQ(tree.GetEnvelope(), IntegerTypeMinimumValue()); + EXPECT_EQ(tree.GetOptionalEnvelope(), event + 2); + } + } +} + +static void BM_update(benchmark::State& state) { + random_engine_t random_; + const int size = state.range(0); + const int num_updates = 4 * size; + ThetaLambdaTree tree; + std::uniform_int_distribution event_dist(0, size - 1); + std::uniform_int_distribution enveloppe_dist(-10000, 10000); + std::uniform_int_distribution energy_dist(0, 10000); + for (auto _ : state) { + tree.Reset(size); + for (int i = 0; i < num_updates; ++i) { + const int event = event_dist(random_); + const IntegerValue enveloppe(enveloppe_dist(random_)); + const IntegerValue energy1(energy_dist(random_)); + const IntegerValue energy2(energy_dist(random_)); + tree.AddOrUpdateEvent(event, enveloppe, std::min(energy1, energy2), + std::max(energy1, energy2)); + } + } + // Number of updates. + state.SetBytesProcessed(static_cast(state.iterations()) * + num_updates); +} + +// Note that we didn't pick only power of two +BENCHMARK(BM_update)->Arg(10)->Arg(20)->Arg(64)->Arg(100)->Arg(256)->Arg(800); + +static void BM_delayed_update(benchmark::State& state) { + random_engine_t random_; + const int size = state.range(0); + const int num_updates = 4 * size; + ThetaLambdaTree tree; + std::uniform_int_distribution event_dist(0, size - 1); + std::uniform_int_distribution enveloppe_dist(-10000, 10000); + std::uniform_int_distribution energy_dist(0, 10000); + for (auto _ : state) { + tree.Reset(size); + for (int i = 0; i < num_updates; ++i) { + const int event = event_dist(random_); + const IntegerValue enveloppe(enveloppe_dist(random_)); + const IntegerValue energy1(energy_dist(random_)); + const IntegerValue energy2(energy_dist(random_)); + tree.DelayedAddOrUpdateEvent(event, enveloppe, std::min(energy1, energy2), + std::max(energy1, energy2)); + } + tree.RecomputeTreeForDelayedOperations(); + } + // Number of updates. + state.SetBytesProcessed(static_cast(state.iterations()) * + num_updates); +} + +// Note that we didn't pick only power of two +BENCHMARK(BM_delayed_update) + ->Arg(10) + ->Arg(20) + ->Arg(64) + ->Arg(100) + ->Arg(256) + ->Arg(800); + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/timetable_test.cc b/ortools/sat/timetable_test.cc new file mode 100644 index 0000000000..c8999baedd --- /dev/null +++ b/ortools/sat/timetable_test.cc @@ -0,0 +1,555 @@ +// 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/timetable.h" + +#include + +#include +#include +#include + +#include "absl/container/btree_map.h" +#include "absl/log/check.h" +#include "absl/strings/str_join.h" +#include "absl/types/span.h" +#include "gtest/gtest.h" +#include "ortools/base/logging.h" +#include "ortools/sat/all_different.h" +#include "ortools/sat/cumulative.h" +#include "ortools/sat/integer.h" +#include "ortools/sat/integer_search.h" +#include "ortools/sat/intervals.h" +#include "ortools/sat/model.h" +#include "ortools/sat/precedences.h" +#include "ortools/sat/sat_base.h" +#include "ortools/sat/sat_solver.h" + +namespace operations_research { +namespace sat { +namespace { + +struct CumulativeTasks { + int min_duration; + int min_demand; + int min_start; + int max_end; +}; + +struct Task { + int min_start; + int max_end; +}; + +bool TestTimeTablingPropagation(absl::Span tasks, + absl::Span expected, int capacity) { + Model model; + IntegerTrail* integer_trail = model.GetOrCreate(); + PrecedencesPropagator* precedences = + model.GetOrCreate(); + IntervalsRepository* intervals = model.GetOrCreate(); + + const int num_tasks = tasks.size(); + std::vector interval_vars(num_tasks); + std::vector start_exprs(num_tasks); + std::vector duration_exprs(num_tasks); + std::vector end_exprs(num_tasks); + std::vector demands(num_tasks); + const AffineExpression capacity_expr = + AffineExpression(IntegerValue(capacity)); + + const int kStart(0); + const int kHorizon(10000); + + for (int t = 0; t < num_tasks; ++t) { + const CumulativeTasks& task = tasks[t]; + // Build the task variables. + interval_vars[t] = + model.Add(NewInterval(kStart, kHorizon, task.min_duration)); + start_exprs[t] = intervals->Start(interval_vars[t]); + end_exprs[t] = intervals->End(interval_vars[t]); + demands[t] = AffineExpression(IntegerValue(task.min_demand)); + + // Set task initial minimum starting time. + std::vector no_literal_reason; + std::vector no_integer_reason; + EXPECT_TRUE( + integer_trail->Enqueue(start_exprs[t].GreaterOrEqual(task.min_start), + no_literal_reason, no_integer_reason)); + // Set task initial maximum ending time. + EXPECT_TRUE(integer_trail->Enqueue(end_exprs[t].LowerOrEqual(task.max_end), + no_literal_reason, no_integer_reason)); + } + + // Propagate properly the other bounds of the intervals. + EXPECT_TRUE(precedences->Propagate()); + + SchedulingConstraintHelper* helper = model.TakeOwnership( + new SchedulingConstraintHelper(interval_vars, &model)); + SchedulingDemandHelper* demands_helper = + model.TakeOwnership(new SchedulingDemandHelper(demands, helper, &model)); + + // Propagator responsible for filtering start variables. + TimeTablingPerTask timetabling(capacity_expr, helper, demands_helper, &model); + timetabling.RegisterWith(model.GetOrCreate()); + + // Check initial satisfiability + if (!model.GetOrCreate()->Propagate()) return false; + + // Check consistency of data. + CHECK_EQ(num_tasks, expected.size()); + + for (int t = 0; t < num_tasks; ++t) { + // Check starting time. + EXPECT_EQ(expected[t].min_start, integer_trail->LowerBound(start_exprs[t])) + << "task #" << t; + // Check ending time. + EXPECT_EQ(expected[t].max_end, integer_trail->UpperBound(end_exprs[t])) + << "task #" << t; + } + return true; +} + +// This is an infeasible instance on which the edge finder finds nothing. +// Cumulative Time Table finds the contradiction. +TEST(TimeTablingPropagation, UNSAT) { + EXPECT_FALSE(TestTimeTablingPropagation({{3, 2, 0, 4}, {3, 2, 1, 5}}, {}, 3)); +} + +// This is an instance on Time Table pushes a task. +TEST(TimeTablingPropagation, TimeTablePush1) { + EXPECT_TRUE(TestTimeTablingPropagation({{1, 2, 1, 2}, {3, 2, 0, 10}}, + {{1, 2}, {2, 10}}, 3)); +} + +// This is an instance on Time Table pushes a task. +TEST(TimeTablingPropagation, TimeTablePush2) { + EXPECT_TRUE( + TestTimeTablingPropagation({{1, 2, 1, 2}, {1, 2, 3, 4}, {3, 2, 0, 10}}, + {{1, 2}, {3, 4}, {4, 10}}, 3)); +} + +// This is an instance on which Time Table pushes a task. +// Here the two first tasks have the following profile: +// usage ^ +// 2 | ** +// 1 | **--** +// 0 |**------******************> time +// 0 1 2 3 4 5 6 +// The interval [2, 3] has a profile too high to accommodate the third task. +TEST(TimeTablingPropagation, TimeTablePush3) { + EXPECT_TRUE( + TestTimeTablingPropagation({{3, 1, 0, 4}, {3, 1, 1, 5}, {3, 2, 0, 10}}, + {{0, 4}, {1, 5}, {3, 10}}, 3)); +} + +// This is an instance on which Time Table pushes a task. +// Similar to TimeTablePush3, but the two small tasks have the same profile. +TEST(TimeTablingPropagation, TimeTablePush4) { + EXPECT_TRUE( + TestTimeTablingPropagation({{4, 1, 0, 5}, {3, 1, 1, 4}, {3, 2, 0, 10}}, + {{0, 5}, {1, 4}, {4, 10}}, 3)); +} + +// Regression test: there used to be a bug when no profile delta corresponded +// to the start time of a task. +TEST(TimeTablingPropagation, RegressionTest) { + EXPECT_TRUE(TestTimeTablingPropagation({{3, 1, 0, 3}, {2, 1, 2, 5}}, + {{0, 3}, {3, 5}}, 1)); +} + +// Regression test: there used to be a bug that caused Timetabling to stop +// before reaching its fixed-point. +TEST(TimeTablingPropagation, FixedPoint) { + EXPECT_TRUE(TestTimeTablingPropagation( + {{1, 1, 0, 1}, {4, 1, 0, 8}, {2, 1, 1, 5}, {1, 1, 1, 5}}, + {{0, 1}, {3, 8}, {1, 4}, {1, 4}}, 1)); +} + +// Regression test: there used to be a bug when two back to back +// tasks were exceeding the capacity in the partial sum. +TEST(TimeTablingPropagation, PartialSumBug) { + EXPECT_TRUE(TestTimeTablingPropagation({{510, 142, 0, 510}, + {268, 130, 242, 510}, + {74, 147, 510, 584}, + {197, 204, 584, 781}, + {72, 138, 781, 853}, + {170, 231, 853, 1023}, + {181, 131, 1023, 1204}}, + {{0, 510}, + {242, 510}, + {510, 584}, + {584, 781}, + {781, 853}, + {853, 1023}, + {1023, 1204}}, + 315)); +} + +// TODO(user): build automatic FindAll tests for the cumulative constraint. +// Test that we find all the solutions. +TEST(TimeTablingSolve, FindAll) { + // Instance. + const std::vector durations = {1, 2, 3, 3, 3, 3}; + const std::vector demands = {1, 1, 1, 1, 4, 4}; + const int capacity = 4; + const int horizon = 11; + + Model model; + std::vector intervals(durations.size()); + std::vector demand_exprs(durations.size()); + const AffineExpression capacity_expr = + AffineExpression(IntegerValue(capacity)); + + for (int i = 0; i < durations.size(); ++i) { + intervals[i] = model.Add(NewInterval(0, horizon, durations[i])); + demand_exprs[i] = AffineExpression(IntegerValue(demands[i])); + } + + model.Add(Cumulative(intervals, demand_exprs, capacity_expr)); + + int num_solutions_found = 0; + auto* integer_trail = model.GetOrCreate(); + auto* repository = model.GetOrCreate(); + while (true) { + const SatSolver::Status status = + SolveIntegerProblemWithLazyEncoding(&model); + if (status != SatSolver::Status::FEASIBLE) break; + + // Add the solution. + std::vector solution(durations.size()); + for (int i = 0; i < intervals.size(); ++i) { + solution[i] = + integer_trail->LowerBound(repository->Start(intervals[i])).value(); + } + num_solutions_found++; + LOG(INFO) << "Found solution: {" << absl::StrJoin(solution, ", ") << "}."; + + // Loop to the next solution. + model.Add(ExcludeCurrentSolutionAndBacktrack()); + } + + // Test that we have the right number of solutions. + EXPECT_EQ(num_solutions_found, 2040); +} + +TEST(TimeTablingSolve, FindAllWithVaryingCapacity) { + // Instance. + const std::vector durations = {1, 2, 3}; + const std::vector demands = {1, 2, 3}; + const int horizon = 6; + + // Collect the number of solution for each capacity value. + int sum = 0; + for (const int capacity : {3, 4, 5}) { + Model model; + std::vector intervals(durations.size()); + std::vector demand_exprs(durations.size()); + const AffineExpression capacity_expr = + AffineExpression(IntegerValue(capacity)); + + for (int i = 0; i < durations.size(); ++i) { + intervals[i] = model.Add(NewInterval(0, horizon, durations[i])); + demand_exprs[i] = AffineExpression(IntegerValue(demands[i])); + } + + model.Add(Cumulative(intervals, demand_exprs, capacity_expr)); + + int num_solutions_found = 0; + auto* integer_trail = model.GetOrCreate(); + auto* repository = model.GetOrCreate(); + while (true) { + const SatSolver::Status status = + SolveIntegerProblemWithLazyEncoding(&model); + if (status != SatSolver::Status::FEASIBLE) break; + + // Add the solution. + std::vector solution(durations.size()); + for (int i = 0; i < intervals.size(); ++i) { + solution[i] = + integer_trail->LowerBound(repository->Start(intervals[i])).value(); + } + num_solutions_found++; + LOG(INFO) << "Found solution: {" << absl::StrJoin(solution, ", ") << "}."; + + // Loop to the next solution. + model.Add(ExcludeCurrentSolutionAndBacktrack()); + } + + LOG(INFO) << "capacity: " << capacity + << " num_solutions: " << num_solutions_found; + sum += num_solutions_found; + } + + // Now solve with a varying capacity. + Model model; + std::vector intervals(durations.size()); + std::vector demand_exprs(durations.size()); + const AffineExpression capacity_expr = + AffineExpression(model.Add(NewIntegerVariable(0, 5))); + + for (int i = 0; i < durations.size(); ++i) { + intervals[i] = model.Add(NewInterval(0, horizon, durations[i])); + demand_exprs[i] = AffineExpression(IntegerValue(demands[i])); + } + + model.Add(Cumulative(intervals, demand_exprs, capacity_expr)); + + int num_solutions_found = 0; + auto* integer_trail = model.GetOrCreate(); + auto* repository = model.GetOrCreate(); + while (true) { + const SatSolver::Status status = + SolveIntegerProblemWithLazyEncoding(&model); + if (status != SatSolver::Status::FEASIBLE) break; + + // Add the solution. + std::vector solution(durations.size()); + for (int i = 0; i < intervals.size(); ++i) { + solution[i] = + integer_trail->LowerBound(repository->Start(intervals[i])).value(); + } + num_solutions_found++; + LOG(INFO) << "Found solution: {" << absl::StrJoin(solution, ", ") << "}."; + + // Loop to the next solution. + model.Add(ExcludeCurrentSolutionAndBacktrack()); + } + + // Test that we have the right number of solutions. + EXPECT_EQ(num_solutions_found, sum); +} + +TEST(TimeTablingSolve, FindAllWithOptionals) { + // Instance. + // Up to two tasks can be scheduled at the same time. + const std::vector durations = {3, 3, 3}; + const std::vector demands = {2, 2, 2}; + const int capacity = 5; + const int horizon = 3; + const int num_solutions = 7; + + Model model; + std::vector intervals(durations.size()); + std::vector demand_exprs(durations.size()); + std::vector is_present_literals(durations.size()); + const AffineExpression capacity_expr = + AffineExpression(IntegerValue(capacity)); + + for (int i = 0; i < durations.size(); ++i) { + is_present_literals[i] = Literal(model.Add(NewBooleanVariable()), true); + intervals[i] = model.Add( + NewOptionalInterval(0, horizon, durations[i], is_present_literals[i])); + demand_exprs[i] = AffineExpression(IntegerValue(demands[i])); + } + + model.Add(Cumulative(intervals, demand_exprs, capacity_expr)); + + int num_solutions_found = 0; + auto* integer_trail = model.GetOrCreate(); + auto* repository = model.GetOrCreate(); + while (true) { + const SatSolver::Status status = + SolveIntegerProblemWithLazyEncoding(&model); + if (status != SatSolver::Status::FEASIBLE) break; + + // Add the solution. + std::vector solution(durations.size()); + for (int i = 0; i < intervals.size(); ++i) { + if (model.Get(Value(is_present_literals[i]))) { + solution[i] = + integer_trail->LowerBound(repository->Start(intervals[i])).value(); + } else { + solution[i] = -1; + } + } + num_solutions_found++; + LOG(INFO) << "Found solution: {" << absl::StrJoin(solution, ", ") << "}."; + + // Loop to the next solution. + model.Add(ExcludeCurrentSolutionAndBacktrack()); + } + + // Test that we have the right number of solutions. + EXPECT_EQ(num_solutions_found, num_solutions); +} + +// This construct a reservoir corresponding to a well behaved parenthesis +// sequence. +TEST(ReservoirTest, FindAllParenthesis) { + const int n = 3; + const int size = 2 * n; + + Model model; + std::vector vars(size); + std::vector times(size); + std::vector deltas(size); + for (int i = 0; i < size; ++i) { + vars[i] = model.Add(NewIntegerVariable(0, size - 1)); + times[i] = vars[i]; + deltas[i] = IntegerValue((i % 2 == 1) ? -1 : 1); + } + const Literal true_lit = + model.GetOrCreate()->GetTrueLiteral(); + std::vector all_true(size, true_lit); + + model.Add(AllDifferentOnBounds(vars)); + AddReservoirConstraint(times, deltas, all_true, 0, size, &model); + + absl::btree_map sequence_to_count; + int num_solutions_found = 0; + while (true) { + const SatSolver::Status status = + SolveIntegerProblemWithLazyEncoding(&model); + if (status != SatSolver::Status::FEASIBLE) break; + + // Add the solution. + std::string parenthesis_sequence(size, ' '); + for (int i = 0; i < size; ++i) { + const int v = model.Get(Value(vars[i])); + parenthesis_sequence[v] = (i % 2 == 0) ? '(' : ')'; + } + sequence_to_count[parenthesis_sequence]++; + num_solutions_found++; + + // Loop to the next solution. + model.Add(ExcludeCurrentSolutionAndBacktrack()); + } + + // To help debug the code. + for (const auto entry : sequence_to_count) { + LOG(INFO) << entry.first << " : " << entry.second; + } + LOG(INFO) << "decisions: " << model.GetOrCreate()->num_branches(); + LOG(INFO) << "conflicts: " << model.GetOrCreate()->num_failures(); + + // Test that we have the right number of solutions. + // + // The catalan number n, which is 5 for n equal five, count the number of well + // formed parathesis sequence. But we have to multiply this by the permutation + // for the open and closing parenthesis that are matched to their positions: + // n!. + EXPECT_EQ(num_solutions_found, 5 * 6 * 6); +} + +// Now some might be absent. +TEST(ReservoirTest, FindAllParenthesisWithOptionality) { + const int n = 2; + const int size = 2 * n; + + Model model; + std::vector vars(size); + std::vector times(size); + std::vector deltas(size); + std::vector present(size); + for (int i = 0; i < size; ++i) { + vars[i] = model.Add(NewIntegerVariable(0, size - 1)); + times[i] = vars[i]; + deltas[i] = IntegerValue((i % 2 == 1) ? -1 : 1); + present[i] = Literal(model.Add(NewBooleanVariable()), true); + } + + model.Add(AllDifferentOnBounds(vars)); + AddReservoirConstraint(times, deltas, present, 0, size, &model); + + absl::btree_map sequence_to_count; + int num_solutions_found = 0; + while (true) { + const SatSolver::Status status = + SolveIntegerProblemWithLazyEncoding(&model); + if (status != SatSolver::Status::FEASIBLE) break; + + // Add the solution. + std::string parenthesis_sequence(size, '_'); + for (int i = 0; i < size; ++i) { + if (model.Get(Value(present[i])) == 0) continue; + const int v = model.Get(Value(vars[i])); + parenthesis_sequence[v] = (i % 2 == 0) ? '(' : ')'; + } + sequence_to_count[parenthesis_sequence]++; + num_solutions_found++; + + // Loop to the next solution. + model.Add(ExcludeCurrentSolutionAndBacktrack()); + } + + // To help debug the code. + for (const auto entry : sequence_to_count) { + LOG(INFO) << entry.first << " : " << entry.second; + } + LOG(INFO) << "decisions: " << model.GetOrCreate()->num_branches(); + LOG(INFO) << "conflicts: " << model.GetOrCreate()->num_failures(); + + // Test that we have the right number of solutions. + EXPECT_EQ(num_solutions_found, 184); +} + +// Enumerate all fixed sequence of [-1, +1] with a partial sum >= 0 and <= 1. +TEST(ReservoirTest, VariableLevelChange) { + Model model; + const int size = 8; + std::vector times(size); + std::vector deltas(size); + for (int i = 0; i < size; ++i) { + times[i] = IntegerValue(i); + deltas[i] = model.Add(NewIntegerVariable(-1, 1)); + } + const Literal true_lit = + model.GetOrCreate()->GetTrueLiteral(); + std::vector all_true(size, true_lit); + + const int min_level = 0; + const int max_level = 1; + AddReservoirConstraint(times, deltas, all_true, min_level, max_level, &model); + + absl::btree_map sequence_to_count; + int num_solutions_found = 0; + auto* integer_trail = model.GetOrCreate(); + while (true) { + const SatSolver::Status status = + SolveIntegerProblemWithLazyEncoding(&model); + if (status != SatSolver::Status::FEASIBLE) break; + + // Add the solution. + // Test that it is a valid one. + int sum = 0; + std::vector values; + for (int i = 0; i < size; ++i) { + values.push_back(integer_trail->LowerBound(deltas[i]).value()); + sum += values.back(); + EXPECT_GE(sum, min_level); + EXPECT_LE(sum, max_level); + } + sequence_to_count[absl::StrJoin(values, ",")]++; + num_solutions_found++; + + // Loop to the next solution. + model.Add(ExcludeCurrentSolutionAndBacktrack()); + } + + // To help debug the code. + for (const auto entry : sequence_to_count) { + LOG(INFO) << entry.first << " : " << entry.second; + } + LOG(INFO) << "decisions: " << model.GetOrCreate()->num_branches(); + LOG(INFO) << "conflicts: " << model.GetOrCreate()->num_failures(); + + // Test that we have the right number of solutions. + // For each subset of non-zero position, the value are fixed, it must + // be an alternating sequence starting at 1. + EXPECT_EQ(num_solutions_found, 1 << size); +} + +} // namespace +} // namespace sat +} // namespace operations_research diff --git a/ortools/sat/zero_half_cuts_test.cc b/ortools/sat/zero_half_cuts_test.cc new file mode 100644 index 0000000000..6aea31cdcd --- /dev/null +++ b/ortools/sat/zero_half_cuts_test.cc @@ -0,0 +1,114 @@ +// 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/zero_half_cuts.h" + +#include +#include + +#include "gtest/gtest.h" +#include "ortools/base/gmock.h" +#include "ortools/lp_data/lp_types.h" +#include "ortools/sat/integer.h" + +namespace operations_research { +namespace sat { +namespace { + +using ::testing::ElementsAre; +using ::testing::IsEmpty; +using ::testing::UnorderedElementsAre; + +TEST(SymmetricDifferenceTest, BasicExample) { + ZeroHalfCutHelper helper; + std::vector a = {2, 1, 4}; + std::vector b = {4, 3, 2, 7}; + helper.Reset(10); + helper.SymmetricDifference(a, &b); + EXPECT_THAT(b, ElementsAre(3, 7, 1)); +} + +TEST(SymmetricDifferenceTest, BasicExample2) { + ZeroHalfCutHelper helper; + std::vector a = {2, 1, 4}; + std::vector b = {}; + helper.Reset(10); + helper.SymmetricDifference(a, &b); + EXPECT_THAT(b, ElementsAre(2, 1, 4)); +} + +TEST(EliminateVarUsingRowTest, BasicExample) { + // We need to construct a binary matrix for this test. + ZeroHalfCutHelper helper; + helper.ProcessVariables({0.0, 0.0, 0.0, 0.0, 0.12, 0.0, 0.0, 0.0, 0.0}, + std::vector(9, IntegerValue(0)), + std::vector(9, IntegerValue(1))); + helper.AddBinaryRow({{{glop::RowIndex(1), IntegerValue(1)}}, + {0, 2, 3, 4, 7}, + /*rhs*/ 1, + /*slack*/ 0.1}); + helper.AddBinaryRow({{{glop::RowIndex(2), IntegerValue(1)}}, + {0, 2, 3, 4, 7}, + /*rhs*/ 0, + /*slack*/ 0.0}); + helper.AddBinaryRow({{{glop::RowIndex(1), IntegerValue(1)}, + {glop::RowIndex(3), IntegerValue(1)}}, + {0, 5, 4, 8}, + /*rhs*/ 1, + /*slack*/ 0.0}); + + typedef std::vector> MultiplierType; + typedef std::vector VectorType; + + // Let use row with index 2 to eliminate the variable 4. + helper.EliminateVarUsingRow(4, 2); + + // The multipliers, cols and parity behave like a xor. + EXPECT_EQ(helper.MatrixRow(0).multipliers, + MultiplierType({{glop::RowIndex(3), IntegerValue(1)}})); + EXPECT_EQ(helper.MatrixRow(0).cols, VectorType({2, 3, 7, 5, 8})); + EXPECT_EQ(helper.MatrixRow(0).rhs_parity, 0); + EXPECT_EQ(helper.MatrixRow(0).slack, 0.1); + + EXPECT_EQ(helper.MatrixRow(1).multipliers, + MultiplierType({{glop::RowIndex(1), IntegerValue(1)}, + {glop::RowIndex(2), IntegerValue(1)}, + {glop::RowIndex(3), IntegerValue(1)}})); + EXPECT_EQ(helper.MatrixRow(1).cols, VectorType({2, 3, 7, 5, 8})); + EXPECT_EQ(helper.MatrixRow(1).rhs_parity, 1); + EXPECT_EQ(helper.MatrixRow(1).slack, 0.0); + + // The column is eliminated like a singleton column and the lp value become + // the slack. + EXPECT_EQ(helper.MatrixRow(2).multipliers, + MultiplierType({{glop::RowIndex(1), IntegerValue(1)}, + {glop::RowIndex(3), IntegerValue(1)}})); + EXPECT_EQ(helper.MatrixRow(2).cols, VectorType({5, 8})); + EXPECT_EQ(helper.MatrixRow(2).rhs_parity, 1); + EXPECT_EQ(helper.MatrixRow(2).slack, 0.12); + + // The transposed information is up to date. + EXPECT_THAT(helper.MatrixCol(0), IsEmpty()); + EXPECT_THAT(helper.MatrixCol(1), IsEmpty()); + EXPECT_THAT(helper.MatrixCol(2), UnorderedElementsAre(0, 1)); + EXPECT_THAT(helper.MatrixCol(3), UnorderedElementsAre(0, 1)); + EXPECT_THAT(helper.MatrixCol(4), IsEmpty()); + EXPECT_THAT(helper.MatrixCol(5), UnorderedElementsAre(0, 1, 2)); + EXPECT_THAT(helper.MatrixCol(6), IsEmpty()); + EXPECT_THAT(helper.MatrixCol(7), UnorderedElementsAre(0, 1)); + EXPECT_THAT(helper.MatrixCol(8), UnorderedElementsAre(0, 1, 2)); +} + +} // namespace +} // namespace sat +} // namespace operations_research