Skip to content

Commit

Permalink
[CP-SAT] improve 2d packing presolve; improve MIR cuts; rewrite some …
Browse files Browse the repository at this point in the history
…of the cuts managements
  • Loading branch information
lperron committed Sep 25, 2024
1 parent 9efab29 commit c709e98
Show file tree
Hide file tree
Showing 16 changed files with 1,123 additions and 277 deletions.
656 changes: 641 additions & 15 deletions ortools/sat/2d_rectangle_presolve.cc

Large diffs are not rendered by default.

26 changes: 20 additions & 6 deletions ortools/sat/2d_rectangle_presolve.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,29 @@ bool PresolveFixed2dRectangles(
absl::Span<const RectangleInRange> non_fixed_boxes,
std::vector<Rectangle>* fixed_boxes);

// Given a set of non-overlapping rectangles split in two groups, mandatory and
// optional, try to build a set of as few non-overlapping rectangles as
// possible defining a region R that satisfy:
// Given two vectors of non-overlapping rectangles defining two regions of the
// space: one mandatory region that must be occupied and one optional region
// that can be occupied, try to build a vector of as few non-overlapping
// rectangles as possible defining a region R that satisfy:
// - R \subset (mandatory \union optional);
// - mandatory \subset R.
//
// The function updates the set of `mandatory_rectangles` with `R` and
// The function updates the vector of `mandatory_rectangles` with `R` and
// `optional_rectangles` with `optional_rectangles \setdiff R`. It returns
// true if the `mandatory_rectangles` was updated.
bool ReduceNumberofBoxes(std::vector<Rectangle>* mandatory_rectangles,
std::vector<Rectangle>* optional_rectangles);
//
// This function uses a greedy algorithm that merge rectangles that share an
// edge.
bool ReduceNumberofBoxesGreedy(std::vector<Rectangle>* mandatory_rectangles,
std::vector<Rectangle>* optional_rectangles);

// Same as above, but this implementation returns the optimal solution in
// minimizing the number of boxes if `optional_rectangles` is empty. On the
// other hand, its handling of optional boxes is rather limited. It simply fills
// the holes in the mandatory boxes with optional boxes, if possible.
bool ReduceNumberOfBoxesExactMandatory(
std::vector<Rectangle>* mandatory_rectangles,
std::vector<Rectangle>* optional_rectangles);

enum EdgePosition { TOP = 0, RIGHT = 1, BOTTOM = 2, LEFT = 3 };

Expand Down Expand Up @@ -172,6 +184,8 @@ struct SingleShape {
std::vector<SingleShape> BoxesToShapes(absl::Span<const Rectangle> rectangles,
const Neighbours& neighbours);

std::vector<Rectangle> CutShapeIntoRectangles(SingleShape shapes);

} // namespace sat
} // namespace operations_research

Expand Down
185 changes: 163 additions & 22 deletions ortools/sat/2d_rectangle_presolve_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,15 @@ std::vector<Rectangle> BuildFromAsciiArt(std::string_view input) {
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});
rectangles.push_back({.x_min = j,
.x_max = j + 1,
.y_min = 2 * lines.size() - 2 * i,
.y_max = 2 * lines.size() - 2 * i + 2});
}
}
}
std::vector<Rectangle> empty;
ReduceNumberofBoxes(&rectangles, &empty);
ReduceNumberofBoxesGreedy(&rectangles, &empty);
return rectangles;
}

Expand Down Expand Up @@ -156,18 +158,16 @@ TEST(RectanglePresolve, RemoveOutsideBB) {
}

TEST(RectanglePresolve, RandomTest) {
constexpr int kTotalRectangles = 100;
constexpr int kFixedRectangleSize = 60;
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 =
GenerateNonConflictingRectangles(kTotalRectangles, bit_gen);
GenerateNonConflictingRectanglesWithPacking({100, 100}, 40, bit_gen);
std::shuffle(input.begin(), input.end(), bit_gen);
CHECK_EQ(input.size(), kTotalRectangles);
absl::Span<const Rectangle> fixed_rectangles =
absl::MakeConstSpan(input).subspan(0, kFixedRectangleSize);
absl::Span<const Rectangle> other_rectangles =
Expand All @@ -185,7 +185,12 @@ TEST(RectanglePresolve, RandomTest) {
<< RenderDot(std::nullopt, new_fixed_rectangles);
}

CHECK_LE(new_fixed_rectangles.size(), kFixedRectangleSize);
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(),
Expand Down Expand Up @@ -742,16 +747,14 @@ TEST(ContourTest, Random) {
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<const Rectangle> fixed_rectangles =
const absl::Span<const Rectangle> fixed_rectangles =
absl::MakeConstSpan(input).subspan(0, num_fixed_rectangles);
absl::Span<const Rectangle> other_rectangles =
const absl::Span<const Rectangle> other_rectangles =
absl::MakeSpan(input).subspan(num_fixed_rectangles);
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);

auto neighbours = BuildNeighboursGraph(fixed_rectangles);
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;
Expand All @@ -766,25 +769,26 @@ TEST(ContourTest, Random) {
}
}

auto s = BoxesToShapes(fixed_rectangles, neighbours);
for (int i = 0; i < s.size(); ++i) {
const ShapePath& shape = s[i].boundary;
const std::vector<SingleShape> shapes =
BoxesToShapes(fixed_rectangles, neighbours);
for (const SingleShape& shape : shapes) {
const ShapePath& boundary = shape.boundary;
const ShapePath expected_shape =
TraceBoundary(shape.step_points[0], shape.touching_box_index[0],
TraceBoundary(boundary.step_points[0], boundary.touching_box_index[0],
fixed_rectangles, neighbours);
if (shape.step_points != expected_shape.step_points) {
if (boundary.step_points != expected_shape.step_points) {
LOG(ERROR) << "Fast algo:\n"
<< RenderContour(bb, fixed_rectangles, shape);
<< 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(shape.step_points, expected_shape.step_points);
EXPECT_EQ(shape.touching_box_index, expected_shape.touching_box_index);
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, s);
LOG(INFO) << RenderShapes(bb, fixed_rectangles, shapes);
}
}
}
Expand Down Expand Up @@ -839,6 +843,143 @@ TEST(ContourTest, SimpleShapes) {
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);
}

} // namespace
} // namespace sat
} // namespace operations_research
1 change: 1 addition & 0 deletions ortools/sat/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -2532,6 +2532,7 @@ cc_library(
":diffn_util",
":integer",
"//ortools/base:stl_util",
"//ortools/graph:max_flow",
"//ortools/graph:strongly_connected_components",
"@com_google_absl//absl/algorithm:container",
"@com_google_absl//absl/container:btree",
Expand Down
Loading

0 comments on commit c709e98

Please sign in to comment.