Files
ortools-clone/ortools/sat/2d_rectangle_presolve_test.cc
2025-02-24 22:59:21 +01:00

1074 lines
42 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright 2010-2025 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 <algorithm>
#include <limits>
#include <optional>
#include <sstream>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
#include <vector>
#include "absl/algorithm/container.h"
#include "absl/container/flat_hash_map.h"
#include "absl/container/flat_hash_set.h"
#include "absl/log/check.h"
#include "absl/log/log.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/sat/2d_orthogonal_packing_testing.h"
#include "ortools/sat/diffn_util.h"
#include "ortools/sat/integer_base.h"
namespace operations_research {
namespace sat {
namespace {
using ::testing::ElementsAre;
using ::testing::IsEmpty;
std::vector<Rectangle> BuildFromAsciiArt(std::string_view input) {
std::vector<Rectangle> rectangles;
std::vector<std::string_view> lines = absl::StrSplit(input, '\n');
const int num_lines = lines.size();
for (int i = 0; i < lines.size(); i++) {
for (int j = 0; j < lines[i].size(); j++) {
if (lines[i][j] != ' ') {
rectangles.push_back({.x_min = j,
.x_max = j + 1,
.y_min = 2 * num_lines - 2 * i,
.y_max = 2 * num_lines - 2 * i + 2});
}
}
}
std::vector<Rectangle> empty;
ReduceNumberofBoxesGreedy(&rectangles, &empty);
return rectangles;
}
TEST(RectanglePresolve, Basic) {
std::vector<Rectangle> input = BuildFromAsciiArt(R"(
*********** ***********
*********** ***********
*********** ***********
*********** ***********
*********** ***********
*********** ***********
)");
// Note that a single naive pass over the fixed rectangles' gaps would not
// fill the middle region.
std::vector<RectangleInRange> 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<Rectangle> input = {
{.x_min = 0, .x_max = 5, .y_min = 0, .y_max = 5}};
std::vector<RectangleInRange> 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<Rectangle> input = {
{.x_min = 1, .x_max = 5, .y_min = 1, .y_max = 5}};
std::vector<RectangleInRange> 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<Rectangle> input = {
{.x_min = 20, .x_max = 25, .y_min = 0, .y_max = 5}};
std::vector<RectangleInRange> 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<Rectangle> input = {
{.x_min = 0, .x_max = 5, .y_min = 0, .y_max = 5}};
std::vector<RectangleInRange> 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 kFixedRectangleSize = 10;
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<Rectangle> input =
GenerateNonConflictingRectanglesWithPacking({100, 100}, 40, bit_gen);
std::shuffle(input.begin(), input.end(), bit_gen);
absl::Span<const Rectangle> fixed_rectangles =
absl::MakeConstSpan(input).subspan(0, kFixedRectangleSize);
absl::Span<const Rectangle> other_rectangles =
absl::MakeSpan(input).subspan(kFixedRectangleSize);
std::vector<Rectangle> new_fixed_rectangles(fixed_rectangles.begin(),
fixed_rectangles.end());
const std::vector<RectangleInRange> input_in_range =
MakeItemsFromRectangles(other_rectangles, 0.6, bit_gen);
// Presolve the fixed items.
PresolveFixed2dRectangles(input_in_range, &new_fixed_rectangles);
if (run == 0) {
LOG(INFO) << "Presolved:\n"
<< RenderDot(std::nullopt, fixed_rectangles) << "To:\n"
<< RenderDot(std::nullopt, new_fixed_rectangles);
}
if (new_fixed_rectangles.size() > fixed_rectangles.size()) {
LOG(FATAL) << "Presolved:\n"
<< RenderDot(std::nullopt, fixed_rectangles) << "To:\n"
<< RenderDot(std::nullopt, new_fixed_rectangles);
}
CHECK_LE(new_fixed_rectangles.size(), fixed_rectangles.size());
// Check if the original solution is still a solution.
std::vector<Rectangle> 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(absl::Span<const Rectangle> rectangles) {
auto interval_intersect = [](IntegerValue begin1, IntegerValue end1,
IntegerValue begin2, IntegerValue end2) {
return std::max(begin1, begin2) < std::min(end1, end2);
};
std::vector<std::tuple<int, EdgePosition, int>> 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<Rectangle> bb,
absl::Span<const Rectangle> rectangles,
const Neighbours& neighbours) {
const absl::flat_hash_map<EdgePosition, std::string> 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<EdgePosition>(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<Rectangle> bb,
absl::Span<const Rectangle> rectangles,
const ShapePath& path) {
const std::vector<std::string> 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<IntegerValue, IntegerValue> 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<Rectangle> 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<Rectangle> 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<EdgePosition>(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<Rectangle> 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<EdgePosition>(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);
}
}
}
}
}
struct ContourPoint {
IntegerValue x;
IntegerValue y;
int next_box_index;
EdgePosition next_direction;
bool operator!=(const ContourPoint& other) const {
return x != other.x || y != other.y ||
next_box_index != other.next_box_index ||
next_direction != other.next_direction;
}
};
// This function runs in O(log N).
ContourPoint NextByClockwiseOrder(const ContourPoint& point,
absl::Span<const Rectangle> rectangles,
const Neighbours& neighbours) {
// This algorithm is very verbose, but it is about handling four cases. In the
// schema below, "-->" is the current direction, "X" the next point and
// the dashed arrow the next direction.
//
// Case 1:
// ++++++++
// ^ ++++++++
// : ++++++++
// : ++++++++
// ++++++++
// ---> X ++++++++
// ******************
// ******************
// ******************
// ******************
//
// Case 2:
// ^ ++++++++
// : ++++++++
// : ++++++++
// ++++++++
// ---> X ++++++++
// *************++++++++
// *************++++++++
// *************
// *************
//
// Case 3:
// ---> X ...>
// *************++++++++
// *************++++++++
// *************++++++++
// *************++++++++
//
// Case 4:
// ---> X
// ************* :
// ************* :
// ************* :
// ************* \/
ContourPoint result;
const Rectangle& cur_rectangle = rectangles[point.next_box_index];
EdgePosition cur_edge;
bool clockwise;
// Much of the code below need to know two things: in which direction we are
// going and what edge of which rectangle we are touching. For example, in the
// "Case 4" drawing above we are going RIGHT and touching the TOP edge of the
// current rectangle. This switch statement finds this `cur_edge`.
switch (point.next_direction) {
case EdgePosition::TOP:
if (cur_rectangle.x_max == point.x) {
cur_edge = EdgePosition::RIGHT;
clockwise = false;
} else {
cur_edge = EdgePosition::LEFT;
clockwise = true;
}
break;
case EdgePosition::BOTTOM:
if (cur_rectangle.x_min == point.x) {
cur_edge = EdgePosition::LEFT;
clockwise = false;
} else {
cur_edge = EdgePosition::RIGHT;
clockwise = true;
}
break;
case EdgePosition::LEFT:
if (cur_rectangle.y_max == point.y) {
cur_edge = EdgePosition::TOP;
clockwise = false;
} else {
cur_edge = EdgePosition::BOTTOM;
clockwise = true;
}
break;
case EdgePosition::RIGHT:
if (cur_rectangle.y_min == point.y) {
cur_edge = EdgePosition::BOTTOM;
clockwise = false;
} else {
cur_edge = EdgePosition::TOP;
clockwise = true;
}
break;
}
// Test case 1. We need to find the next box after the current point in the
// edge we are following in the current direction.
const auto cur_edge_neighbors =
neighbours.GetSortedNeighbors(point.next_box_index, cur_edge);
const Rectangle fake_box_for_lower_bound = {
.x_min = point.x, .x_max = point.x, .y_min = point.y, .y_max = point.y};
const auto clockwise_cmp = Neighbours::CompareClockwise(cur_edge);
auto it = absl::c_lower_bound(
cur_edge_neighbors, -1,
[&fake_box_for_lower_bound, rectangles, clockwise_cmp, clockwise](int a,
int b) {
const Rectangle& rectangle_a =
(a == -1 ? fake_box_for_lower_bound : rectangles[a]);
const Rectangle& rectangle_b =
(b == -1 ? fake_box_for_lower_bound : rectangles[b]);
if (clockwise) {
return clockwise_cmp(rectangle_a, rectangle_b);
} else {
return clockwise_cmp(rectangle_b, rectangle_a);
}
});
if (it != cur_edge_neighbors.end()) {
// We found box in the current edge. We are in case 1.
result.next_box_index = *it;
const Rectangle& next_rectangle = rectangles[*it];
switch (point.next_direction) {
case EdgePosition::TOP:
result.x = point.x;
result.y = next_rectangle.y_min;
if (cur_edge == EdgePosition::LEFT) {
result.next_direction = EdgePosition::LEFT;
} else {
result.next_direction = EdgePosition::RIGHT;
}
break;
case EdgePosition::BOTTOM:
result.x = point.x;
result.y = next_rectangle.y_max;
if (cur_edge == EdgePosition::LEFT) {
result.next_direction = EdgePosition::LEFT;
} else {
result.next_direction = EdgePosition::RIGHT;
}
break;
case EdgePosition::LEFT:
result.y = point.y;
result.x = next_rectangle.x_max;
if (cur_edge == EdgePosition::TOP) {
result.next_direction = EdgePosition::TOP;
} else {
result.next_direction = EdgePosition::BOTTOM;
}
break;
case EdgePosition::RIGHT:
result.y = point.y;
result.x = next_rectangle.x_min;
if (cur_edge == EdgePosition::TOP) {
result.next_direction = EdgePosition::TOP;
} else {
result.next_direction = EdgePosition::BOTTOM;
}
break;
}
return result;
}
// We now know we are not in Case 1, so know the next (x, y) position: it is
// the corner of the current rectangle in the direction we are going.
switch (point.next_direction) {
case EdgePosition::TOP:
result.x = point.x;
result.y = cur_rectangle.y_max;
break;
case EdgePosition::BOTTOM:
result.x = point.x;
result.y = cur_rectangle.y_min;
break;
case EdgePosition::LEFT:
result.x = cur_rectangle.x_min;
result.y = point.y;
break;
case EdgePosition::RIGHT:
result.x = cur_rectangle.x_max;
result.y = point.y;
break;
}
// Case 2 and 3.
const auto next_edge_neighbors =
neighbours.GetSortedNeighbors(point.next_box_index, point.next_direction);
if (!next_edge_neighbors.empty()) {
// We are looking for the neighbor on the edge of the current box.
const int candidate_index =
clockwise ? next_edge_neighbors.front() : next_edge_neighbors.back();
const Rectangle& next_rectangle = rectangles[candidate_index];
switch (point.next_direction) {
case EdgePosition::TOP:
case EdgePosition::BOTTOM:
if (next_rectangle.x_min < point.x && point.x < next_rectangle.x_max) {
// Case 2
result.next_box_index = candidate_index;
if (cur_edge == EdgePosition::LEFT) {
result.next_direction = EdgePosition::LEFT;
} else {
result.next_direction = EdgePosition::RIGHT;
}
return result;
} else if (next_rectangle.x_min == point.x &&
cur_edge == EdgePosition::LEFT) {
// Case 3
result.next_box_index = candidate_index;
result.next_direction = point.next_direction;
return result;
} else if (next_rectangle.x_max == point.x &&
cur_edge == EdgePosition::RIGHT) {
// Case 3
result.next_box_index = candidate_index;
result.next_direction = point.next_direction;
return result;
}
break;
case EdgePosition::LEFT:
case EdgePosition::RIGHT:
if (next_rectangle.y_min < point.y && point.y < next_rectangle.y_max) {
result.next_box_index = candidate_index;
if (cur_edge == EdgePosition::TOP) {
result.next_direction = EdgePosition::TOP;
} else {
result.next_direction = EdgePosition::BOTTOM;
}
return result;
} else if (next_rectangle.y_max == point.y &&
cur_edge == EdgePosition::TOP) {
result.next_box_index = candidate_index;
result.next_direction = point.next_direction;
return result;
} else if (next_rectangle.y_min == point.y &&
cur_edge == EdgePosition::BOTTOM) {
result.next_box_index = candidate_index;
result.next_direction = point.next_direction;
return result;
}
break;
}
}
// Now we must be in the case 4.
result.next_box_index = point.next_box_index;
switch (point.next_direction) {
case EdgePosition::TOP:
case EdgePosition::BOTTOM:
if (cur_edge == EdgePosition::LEFT) {
result.next_direction = EdgePosition::RIGHT;
} else {
result.next_direction = EdgePosition::LEFT;
}
break;
case EdgePosition::LEFT:
case EdgePosition::RIGHT:
if (cur_edge == EdgePosition::TOP) {
result.next_direction = EdgePosition::BOTTOM;
} else {
result.next_direction = EdgePosition::TOP;
}
break;
}
return result;
}
// Returns a path delimiting a boundary of the union of a set of rectangles. It
// should work for both the exterior boundary and the boundaries of the holes
// inside the union. The path will start on `starting_point` and follow the
// boundary on clockwise order.
//
// `starting_point` should be a point in the boundary and `starting_box_index`
// the index of a rectangle with one edge containing `starting_point`.
//
// The resulting `path` satisfy:
// - path.step_points.front() == path.step_points.back() == starting_point
// - path.touching_box_index.front() == path.touching_box_index.back() ==
// == starting_box_index
//
ShapePath TraceBoundary(
const std::pair<IntegerValue, IntegerValue>& starting_step_point,
int starting_box_index, absl::Span<const Rectangle> rectangles,
const Neighbours& neighbours) {
// First find which direction we need to go to follow the border in the
// clockwise order.
const Rectangle& initial_rec = rectangles[starting_box_index];
bool touching_edge[4];
touching_edge[EdgePosition::LEFT] =
initial_rec.x_min == starting_step_point.first;
touching_edge[EdgePosition::RIGHT] =
initial_rec.x_max == starting_step_point.first;
touching_edge[EdgePosition::TOP] =
initial_rec.y_max == starting_step_point.second;
touching_edge[EdgePosition::BOTTOM] =
initial_rec.y_min == starting_step_point.second;
EdgePosition next_direction;
if (touching_edge[EdgePosition::LEFT]) {
if (touching_edge[EdgePosition::TOP]) {
next_direction = EdgePosition::RIGHT;
} else {
next_direction = EdgePosition::TOP;
}
} else if (touching_edge[EdgePosition::RIGHT]) {
if (touching_edge[EdgePosition::BOTTOM]) {
next_direction = EdgePosition::LEFT;
} else {
next_direction = EdgePosition::BOTTOM;
}
} else if (touching_edge[EdgePosition::TOP]) {
next_direction = EdgePosition::LEFT;
} else if (touching_edge[EdgePosition::BOTTOM]) {
next_direction = EdgePosition::RIGHT;
} else {
LOG(FATAL)
<< "TraceBoundary() got a `starting_step_point` that is not in an edge "
"of the rectangle of `starting_box_index`. This is not allowed.";
}
const ContourPoint starting_point = {.x = starting_step_point.first,
.y = starting_step_point.second,
.next_box_index = starting_box_index,
.next_direction = next_direction};
ShapePath result;
for (ContourPoint point = starting_point; true;
point = NextByClockwiseOrder(point, rectangles, neighbours)) {
if (result.step_points.size() > 3 &&
result.step_points.back() == result.step_points.front() &&
point.x == result.step_points[1].first &&
point.y == result.step_points[1].second) {
break;
}
if (!result.step_points.empty() &&
point.x == result.step_points.back().first &&
point.y == result.step_points.back().second) {
// There is a special corner-case of the algorithm using the neighbours.
// Consider the following set-up:
//
// ******** |
// ******** |
// ******** +---->
// ########++++++++
// ########++++++++
// ########++++++++
//
// In this case, the only way the algorithm could reach the "+" box is via
// the "#" box, but which is doesn't contribute to the path. The algorithm
// returns a technically correct zero-size interval, which might be useful
// for callers that want to count the "#" box as visited, but this is not
// our case.
result.touching_box_index.back() = point.next_box_index;
} else {
result.touching_box_index.push_back(point.next_box_index);
result.step_points.push_back({point.x, point.y});
}
}
return result;
}
std::string RenderShapes(std::optional<Rectangle> bb,
absl::Span<const Rectangle> rectangles,
absl::Span<const SingleShape> shapes) {
const std::vector<std::string> colors = {"black", "white", "orange",
"cyan", "yellow", "purple"};
std::stringstream ss;
ss << " edge[headclip=false, tailclip=false, penwidth=40];\n";
int count = 0;
for (int i = 0; i < shapes.size(); ++i) {
std::string_view shape_color = colors[i % colors.size()];
for (int j = 0; j < shapes[i].boundary.step_points.size(); ++j) {
std::pair<IntegerValue, IntegerValue> p =
shapes[i].boundary.step_points[j];
ss << " p" << count << "[pos=\"" << 2 * p.first << "," << 2 * p.second
<< "!\" shape=point]\n";
if (j != shapes[i].boundary.step_points.size() - 1) {
ss << " p" << count << "->p" << count + 1 << " [color=\""
<< shape_color << "\"];\n";
}
++count;
}
for (const ShapePath& hole : shapes[i].holes) {
for (int j = 0; j < hole.step_points.size(); ++j) {
std::pair<IntegerValue, IntegerValue> p = hole.step_points[j];
ss << " p" << count << "[pos=\"" << 2 * p.first << "," << 2 * p.second
<< "!\" shape=point]\n";
if (j != hole.step_points.size() - 1) {
ss << " p" << count << "->p" << count + 1 << " [color=\""
<< shape_color << "\", penwidth=20];\n";
}
++count;
}
}
}
return RenderDot(bb, rectangles, ss.str());
}
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<Rectangle> input =
GenerateNonConflictingRectanglesWithPacking({100, 100}, 60, bit_gen);
std::shuffle(input.begin(), input.end(), bit_gen);
const int num_fixed_rectangles = input.size() * 2 / 3;
const absl::Span<const Rectangle> fixed_rectangles =
absl::MakeConstSpan(input).subspan(0, num_fixed_rectangles);
const absl::Span<const Rectangle> other_rectangles =
absl::MakeSpan(input).subspan(num_fixed_rectangles);
const std::vector<RectangleInRange> input_in_range =
MakeItemsFromRectangles(other_rectangles, 0.6, bit_gen);
const Neighbours 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<IntegerValue, IntegerValue> min_coord = {
std::numeric_limits<IntegerValue>::max(),
std::numeric_limits<IntegerValue>::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 std::vector<SingleShape> shapes =
BoxesToShapes(fixed_rectangles, neighbours);
for (const SingleShape& shape : shapes) {
const ShapePath& boundary = shape.boundary;
const ShapePath expected_shape =
TraceBoundary(boundary.step_points[0], boundary.touching_box_index[0],
fixed_rectangles, neighbours);
if (boundary.step_points != expected_shape.step_points) {
LOG(ERROR) << "Fast algo:\n"
<< RenderContour(bb, fixed_rectangles, boundary);
LOG(ERROR) << "Naive algo:\n"
<< RenderContour(bb, fixed_rectangles, expected_shape);
LOG(FATAL) << "Found different solutions between naive and fast algo!";
}
EXPECT_EQ(boundary.step_points, expected_shape.step_points);
EXPECT_EQ(boundary.touching_box_index, expected_shape.touching_box_index);
}
if (run == 0) {
LOG(INFO) << RenderShapes(bb, fixed_rectangles, shapes);
}
}
}
TEST(ContourTest, SimpleShapes) {
std::vector<Rectangle> 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)));
}
TEST(ContourTest, ExampleFromPaper) {
const std::vector<Rectangle> input = BuildFromAsciiArt(R"(
*******************
*******************
********** *******************
********** *******************
***************************************
***************************************
***************************************
***************************************
*********** ************** ****
*********** ************** ****
*********** ******* *** ****
*********** ******* *** ****
*********** ************** ****
*********** ************** ****
*********** ************** ****
***************************************
***************************************
***************************************
**************************************
**************************************
**************************************
*******************************
***************************************
***************************************
**************** ****************
**************** ****************
****** ***
****** ***
****** ***
******
)");
const Neighbours neighbours = BuildNeighboursGraph(input);
auto s = BoxesToShapes(input, neighbours);
LOG(INFO) << RenderDot(std::nullopt, input);
const std::vector<Rectangle> output = CutShapeIntoRectangles(s[0]);
LOG(INFO) << RenderDot(std::nullopt, output);
EXPECT_THAT(output.size(), 16);
}
bool RectanglesCoverSameArea(absl::Span<const Rectangle> a,
absl::Span<const Rectangle> b) {
return RegionIncludesOther(a, b) && RegionIncludesOther(b, a);
}
TEST(ReduceNumberOfBoxes, RandomTestNoOptional) {
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<Rectangle> input =
GenerateNonConflictingRectanglesWithPacking({100, 100}, 60, bit_gen);
std::shuffle(input.begin(), input.end(), bit_gen);
std::vector<Rectangle> output = input;
std::vector<Rectangle> optional_rectangles_empty;
ReduceNumberOfBoxesExactMandatory(&output, &optional_rectangles_empty);
if (run == 0) {
LOG(INFO) << "Presolved:\n" << RenderDot(std::nullopt, input);
LOG(INFO) << "To:\n" << RenderDot(std::nullopt, output);
}
if (output.size() > input.size()) {
LOG(INFO) << "Presolved:\n" << RenderDot(std::nullopt, input);
LOG(INFO) << "To:\n" << RenderDot(std::nullopt, output);
ADD_FAILURE() << "ReduceNumberofBoxes() increased the number of boxes, "
"but it should be optimal in reducing them!";
}
CHECK(RectanglesCoverSameArea(output, input));
}
}
TEST(ReduceNumberOfBoxes, Problematic) {
// This example shows that we must consider diagonals that touches only on its
// extremities as "intersecting" for the bipartite graph.
const std::vector<Rectangle> input = {
{.x_min = 26, .x_max = 51, .y_min = 54, .y_max = 81},
{.x_min = 51, .x_max = 78, .y_min = 44, .y_max = 67},
{.x_min = 51, .x_max = 62, .y_min = 67, .y_max = 92},
{.x_min = 78, .x_max = 98, .y_min = 24, .y_max = 54},
};
std::vector<Rectangle> output = input;
std::vector<Rectangle> optional_rectangles_empty;
ReduceNumberOfBoxesExactMandatory(&output, &optional_rectangles_empty);
LOG(INFO) << "Presolved:\n" << RenderDot(std::nullopt, input);
LOG(INFO) << "To:\n" << RenderDot(std::nullopt, output);
}
// This example shows that sometimes the best solution with respect to minimum
// number of boxes requires *not* filling a hole. Actually this follows from the
// formula that the minimum number of rectangles in a partition of a polygon
// with n vertices and h holes is n/2 + h g 1, where g is the number of
// non-intersecting good diagonals. This test-case shows a polygon with 4
// internal vertices, 1 hole and 4 non-intersecting good diagonals that includes
// the hole. Removing the hole reduces the n/2 term by 2, decrease the h term by
// 1, but decrease the g term by 4.
//
// ***********************
// ***********************
// ***********************.....................
// ***********************.....................
// ***********************.....................
// ***********************.....................
// ***********************.....................
// ++++++++++++++++++++++ .....................
// ++++++++++++++++++++++ .....................
// ++++++++++++++++++++++ .....................
// ++++++++++++++++++++++000000000000000000000000
// ++++++++++++++++++++++000000000000000000000000
// ++++++++++++++++++++++000000000000000000000000
// 000000000000000000000000
// 000000000000000000000000
// 000000000000000000000000
// 000000000000000000000000
//
TEST(ReduceNumberOfBoxes, Problematic2) {
const std::vector<Rectangle> input = {
{.x_min = 64, .x_max = 82, .y_min = 76, .y_max = 98},
{.x_min = 39, .x_max = 59, .y_min = 63, .y_max = 82},
{.x_min = 59, .x_max = 78, .y_min = 61, .y_max = 76},
{.x_min = 44, .x_max = 64, .y_min = 82, .y_max = 100},
};
std::vector<Rectangle> output = input;
std::vector<Rectangle> optional_rectangles = {
{.x_min = 59, .x_max = 64, .y_min = 76, .y_max = 82},
};
ReduceNumberOfBoxesExactMandatory(&output, &optional_rectangles);
LOG(INFO) << "Presolving:\n" << RenderDot(std::nullopt, input);
// Presolve will refuse to do anything since removing the hole will increase
// the number of boxes.
CHECK(input == output);
}
TEST(DetectDisjointRegionIn2dPackingTest, Basic) {
// Note that the hole of size 1 is ignored since no box is small enough to
// fit in it.
const std::vector<Rectangle> fixed_rectangles = BuildFromAsciiArt(R"(
#######################################
#######################################
#######################################
########### ############## ####
########### ############## ####
########### ####### ### ####
########### ####### ### ####
########### ############## ####
########### ############## ####
########### #######################
#######################################
#######################################
###################### ################
#######################################)");
Rectangle bb = fixed_rectangles[0];
for (const Rectangle& r : fixed_rectangles) {
bb.GrowToInclude(r);
}
const std::vector<RectangleInRange> non_fixed_rectangles = {
{.box_index = 0, .bounding_area = bb, .x_size = 2, .y_size = 2},
};
auto res = DetectDisjointRegionIn2dPacking(non_fixed_rectangles,
fixed_rectangles, 100);
EXPECT_THAT(res.bins.size(), 3);
std::sort(res.bins.begin(), res.bins.end(),
[](const Disjoint2dPackingResult::Bin& a,
const Disjoint2dPackingResult::Bin& b) {
return a.bin_area[0].SizeY() < b.bin_area[0].SizeY();
});
EXPECT_TRUE(
RectanglesCoverSameArea(res.bins[0].fixed_boxes, BuildFromAsciiArt(R"(
#######################################
#######################################
#######################################
#######################################
#######################################
####################### ############
####################### ############
#######################################
#######################################
#######################################
#######################################
#######################################
#######################################
#######################################)")));
EXPECT_TRUE(
RectanglesCoverSameArea(res.bins[1].fixed_boxes, BuildFromAsciiArt(R"(
#######################################
#######################################
#######################################
############################## ####
############################## ####
############################## ####
############################## ####
############################## ####
############################## ####
#######################################
#######################################
#######################################
#######################################
#######################################)")));
EXPECT_TRUE(
RectanglesCoverSameArea(res.bins[2].fixed_boxes, BuildFromAsciiArt(R"(
#######################################
#######################################
#######################################
########### #######################
########### #######################
########### #######################
########### #######################
########### #######################
########### #######################
########### #######################
#######################################
#######################################
#######################################
#######################################)")));
}
} // namespace
} // namespace sat
} // namespace operations_research