Skip to content

Commit

Permalink
[CP-SAT] more work on hints; control complexity of 2d packing constra…
Browse files Browse the repository at this point in the history
…ints and presolve
  • Loading branch information
lperron committed Nov 26, 2024
1 parent c67700e commit 78d5ba5
Show file tree
Hide file tree
Showing 9 changed files with 310 additions and 148 deletions.
76 changes: 42 additions & 34 deletions ortools/sat/2d_rectangle_presolve.cc
Original file line number Diff line number Diff line change
Expand Up @@ -164,15 +164,19 @@ bool PresolveFixed2dRectangles(

// Now check if there is any space that cannot be occupied by any non-fixed
// item.
std::vector<Rectangle> bounding_boxes;
bounding_boxes.reserve(non_fixed_boxes.size());
for (const RectangleInRange& box : non_fixed_boxes) {
bounding_boxes.push_back(box.bounding_area);
}
std::vector<Rectangle> empty_spaces =
FindEmptySpaces(bounding_box, std::move(bounding_boxes));
for (const Rectangle& r : empty_spaces) {
add_box(r);
// TODO(user): remove the limit of 1000 and reimplement FindEmptySpaces()
// using a sweep line algorithm.
if (non_fixed_boxes.size() < 1000) {
std::vector<Rectangle> bounding_boxes;
bounding_boxes.reserve(non_fixed_boxes.size());
for (const RectangleInRange& box : non_fixed_boxes) {
bounding_boxes.push_back(box.bounding_area);
}
std::vector<Rectangle> empty_spaces =
FindEmptySpaces(bounding_box, std::move(bounding_boxes));
for (const Rectangle& r : empty_spaces) {
add_box(r);
}
}

// Now look for gaps between objects that are too small to place anything.
Expand Down Expand Up @@ -1391,32 +1395,36 @@ bool ReduceNumberOfBoxesExactMandatory(
std::vector<Rectangle> result = *mandatory_rectangles;
std::vector<Rectangle> new_optional_rectangles = *optional_rectangles;

Rectangle mandatory_bounding_box = (*mandatory_rectangles)[0];
for (const Rectangle& box : *mandatory_rectangles) {
mandatory_bounding_box.GrowToInclude(box);
}
const std::vector<Rectangle> mandatory_empty_holes =
FindEmptySpaces(mandatory_bounding_box, *mandatory_rectangles);
const std::vector<std::vector<int>> mandatory_holes_components =
SplitInConnectedComponents(BuildNeighboursGraph(mandatory_empty_holes));

// Now for every connected component of the holes in the mandatory area, see
// if we can fill them with optional boxes.
std::vector<Rectangle> holes_in_component;
for (const std::vector<int>& component : mandatory_holes_components) {
holes_in_component.clear();
holes_in_component.reserve(component.size());
for (const int index : component) {
holes_in_component.push_back(mandatory_empty_holes[index]);
// This heuristic can be slow for very large problems, so gate it with a
// reasonable limit.
if (mandatory_rectangles->size() < 1000) {
Rectangle mandatory_bounding_box = (*mandatory_rectangles)[0];
for (const Rectangle& box : *mandatory_rectangles) {
mandatory_bounding_box.GrowToInclude(box);
}
if (RegionIncludesOther(new_optional_rectangles, holes_in_component)) {
// Fill the hole.
result.insert(result.end(), holes_in_component.begin(),
holes_in_component.end());
// We can modify `optional_rectangles` here since we know that if we
// remove a hole this function will return true.
new_optional_rectangles = PavedRegionDifference(
new_optional_rectangles, std::move(holes_in_component));
const std::vector<Rectangle> mandatory_empty_holes =
FindEmptySpaces(mandatory_bounding_box, *mandatory_rectangles);
const std::vector<std::vector<int>> mandatory_holes_components =
SplitInConnectedComponents(BuildNeighboursGraph(mandatory_empty_holes));

// Now for every connected component of the holes in the mandatory area, see
// if we can fill them with optional boxes.
std::vector<Rectangle> holes_in_component;
for (const std::vector<int>& component : mandatory_holes_components) {
holes_in_component.clear();
holes_in_component.reserve(component.size());
for (const int index : component) {
holes_in_component.push_back(mandatory_empty_holes[index]);
}
if (RegionIncludesOther(new_optional_rectangles, holes_in_component)) {
// Fill the hole.
result.insert(result.end(), holes_in_component.begin(),
holes_in_component.end());
// We can modify `optional_rectangles` here since we know that if we
// remove a hole this function will return true.
new_optional_rectangles = PavedRegionDifference(
new_optional_rectangles, std::move(holes_in_component));
}
}
}
const Neighbours neighbours = BuildNeighboursGraph(result);
Expand Down
10 changes: 5 additions & 5 deletions ortools/sat/2d_try_edge_propagator.cc
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ bool TryEdgeRectanglePropagator::Propagate() {

PopulateActiveBoxRanges();

// Our algo is quadratic, so we don't want to run it on really large problems.
if (changed_item_.size() > 1000) {
return true;
}

// If a mandatory region is changed, we need to replace any cached box that
// now became overlapping with it.
for (const int mandatory_idx : changed_mandatory_) {
Expand All @@ -158,11 +163,6 @@ bool TryEdgeRectanglePropagator::Propagate() {
}
gtl::STLSortAndRemoveDuplicates(&changed_item_);

// Our algo is quadratic, so we don't want to run it on really large problems.
if (changed_item_.size() > 1000) {
return true;
}

potential_x_positions_.clear();
potential_y_positions_.clear();
for (const int i : has_mandatory_region_) {
Expand Down
2 changes: 1 addition & 1 deletion ortools/sat/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -773,7 +773,6 @@ cc_library(
":util",
"//ortools/algorithms:sparse_permutation",
"//ortools/base",
"//ortools/base:stl_util",
"//ortools/port:proto_utils",
"//ortools/util:affine_relation",
"//ortools/util:bitset",
Expand All @@ -782,6 +781,7 @@ cc_library(
"//ortools/util:sorted_interval_list",
"//ortools/util:time_limit",
"@com_google_absl//absl/base:core_headers",
"@com_google_absl//absl/base:log_severity",
"@com_google_absl//absl/container:btree",
"@com_google_absl//absl/container:flat_hash_map",
"@com_google_absl//absl/container:flat_hash_set",
Expand Down
31 changes: 22 additions & 9 deletions ortools/sat/cp_model_presolve.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3472,6 +3472,9 @@ void CpModelPresolver::ProcessOneLinearWithAmo(int ct_index,
if (ct->constraint_case() != ConstraintProto::kLinear) return;
if (ct->linear().vars().size() <= 1) return;

// TODO(user): It is possible in some corner-case that the linear constraint
// is NOT canonicalized. This is because we might detect equivalence here and
// we will continue with ProcessOneLinearWithAmo() in the parent loop.
tmp_terms_.clear();
temp_ct_.Clear();
Domain non_boolean_domain(0);
Expand Down Expand Up @@ -5111,6 +5114,9 @@ bool CpModelPresolver::PresolveElement(int c, ConstraintProto* ct) {
context_->CanonicalizeLinearConstraint(lin);
context_->UpdateNewConstraintsVariableUsage();
context_->UpdateRuleStats("element: rewrite as affine constraint");
VLOG(1) << "element: " << ct->DebugString() << " -> "
<< lin->DebugString() << " with "
<< context_->DomainOf(index_var);
return RemoveConstraint(ct);
}
}
Expand Down Expand Up @@ -7704,6 +7710,16 @@ bool CpModelPresolver::PresolvePureSatPart() {
// Update the constraints <-> variables graph.
context_->UpdateNewConstraintsVariableUsage();

// We mark as removed any variables removed by the pure SAT presolve.
// This is mainly to discover or avoid bug as we might have stale entries
// in our encoding hash-map for instance.
for (int i = 0; i < num_variables; ++i) {
const int var = new_to_old_index[i];
if (context_->VarToConstraints(var).empty()) {
context_->MarkVariableAsRemoved(var);
}
}

// Add the sat_postsolver clauses to mapping_model.
//
// TODO(user): Mark removed variable as removed to detect any potential bugs.
Expand Down Expand Up @@ -13141,16 +13157,13 @@ void CopyEverythingExceptVariablesAndConstraintsFieldsIntoContext(
for (int i = 0; i < num_terms; ++i) {
const int var = in_model.solution_hint().vars(i);
const int64_t value = in_model.solution_hint().values(i);
const auto& domain = in_model.variables(var).domain();
if (domain.empty()) continue; // UNSAT.
const int64_t min = domain[0];
const int64_t max = domain[domain.size() - 1];
if (value < min) {
context->UpdateRuleStats("hint: moved var hint within its domain.");
context->working_model->mutable_solution_hint()->set_values(i, min);
} else if (value > max) {
context->working_model->mutable_solution_hint()->set_values(i, max);
const Domain& domain = ReadDomainFromProto(in_model.variables(var));
if (domain.IsEmpty()) continue; // UNSAT.
const int64_t closest_domain_value = domain.ClosestValue(value);
if (closest_domain_value != value) {
context->UpdateRuleStats("hint: moved var hint within its domain.");
context->working_model->mutable_solution_hint()->set_values(
i, closest_domain_value);
}
}
}
Expand Down
Loading

0 comments on commit 78d5ba5

Please sign in to comment.