Skip to content

Commit a650027

Browse files
committed
[CP-SAT] improve propagation; more hint preservation; polish last sample
1 parent d7ea4f5 commit a650027

File tree

9 files changed

+237
-117
lines changed

9 files changed

+237
-117
lines changed

ortools/sat/cp_model_presolve.cc

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ bool CpModelPresolver::PresolveEnforcementLiteral(ConstraintProto* ct) {
177177
if (context_->VariableIsUniqueAndRemovable(literal)) {
178178
// We can simply set it to false and ignore the constraint in this case.
179179
context_->UpdateRuleStats("enforcement: literal not used");
180-
CHECK(context_->SetLiteralToFalse(literal));
180+
CHECK(context_->SetLiteralAndHintToFalse(literal));
181181
return RemoveConstraint(ct);
182182
}
183183

@@ -190,7 +190,7 @@ bool CpModelPresolver::PresolveEnforcementLiteral(ConstraintProto* ct) {
190190
if (RefIsPositive(literal) == (obj_coeff > 0)) {
191191
// It is just more advantageous to set it to false!
192192
context_->UpdateRuleStats("enforcement: literal with unique direction");
193-
CHECK(context_->SetLiteralToFalse(literal));
193+
CHECK(context_->SetLiteralAndHintToFalse(literal));
194194
return RemoveConstraint(ct);
195195
}
196196
}
@@ -346,7 +346,7 @@ bool CpModelPresolver::PresolveBoolOr(ConstraintProto* ct) {
346346
// objective var usage by 1).
347347
if (context_->VariableIsUniqueAndRemovable(literal)) {
348348
context_->UpdateRuleStats("bool_or: singleton");
349-
if (!context_->SetLiteralToTrue(literal)) return true;
349+
if (!context_->SetLiteralAndHintToTrue(literal)) return true;
350350
return RemoveConstraint(ct);
351351
}
352352
if (context_->tmp_literal_set.contains(NegatedRef(literal))) {
@@ -448,8 +448,7 @@ bool CpModelPresolver::PresolveBoolAnd(ConstraintProto* ct) {
448448
changed = true;
449449
context_->UpdateRuleStats(
450450
"bool_and: setting unused literal in rhs to true");
451-
context_->UpdateLiteralSolutionHint(literal, true);
452-
if (!context_->SetLiteralToTrue(literal)) return true;
451+
if (!context_->SetLiteralAndHintToTrue(literal)) return true;
453452
continue;
454453
}
455454

@@ -495,8 +494,15 @@ bool CpModelPresolver::PresolveBoolAnd(ConstraintProto* ct) {
495494
// The other case where the constraint is redundant is treated elsewhere.
496495
if (obj_coeff < 0) {
497496
context_->UpdateRuleStats("bool_and: dual equality.");
498-
context_->StoreBooleanEqualityRelation(enforcement,
499-
ct->bool_and().literals(0));
497+
// Extending `ct` = "enforcement => implied_literal" to an equality can
498+
// break the hint only if hint(implied_literal) = 1 and
499+
// hint(enforcement) = 0. But in this case the `enforcement` hint can be
500+
// increased to 1 to preserve the hint feasibility.
501+
const int implied_literal = ct->bool_and().literals(0);
502+
if (context_->LiteralSolutionHintIs(implied_literal, true)) {
503+
context_->UpdateLiteralSolutionHint(enforcement, true);
504+
}
505+
context_->StoreBooleanEqualityRelation(enforcement, implied_literal);
500506
}
501507
}
502508
}
@@ -7758,7 +7764,7 @@ bool CpModelPresolver::PresolvePureSatPart() {
77587764
// Such variable needs to be fixed to some value for the SAT postsolve to
77597765
// work.
77607766
if (!context_->IsFixed(var)) {
7761-
CHECK(context_->IntersectDomainWith(
7767+
CHECK(context_->IntersectDomainWithAndUpdateHint(
77627768
var, Domain(context_->DomainOf(var).SmallestValue())));
77637769
}
77647770
context_->MarkVariableAsRemoved(var);

ortools/sat/cp_model_solver.cc

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1271,7 +1271,7 @@ class LnsSolver : public SubSolver {
12711271
static_cast<double>(generator_->num_fully_solved_calls()) /
12721272
static_cast<double>(num_calls);
12731273
const std::string lns_info = absl::StrFormat(
1274-
"%s (d=%0.3f s=%i t=%0.2f p=%0.2f stall=%d h=%s)", source_info,
1274+
"%s (d=%0.2e s=%i t=%0.2f p=%0.2f stall=%d h=%s)", source_info,
12751275
data.difficulty, task_id, data.deterministic_limit,
12761276
fully_solved_proportion, stall, search_info);
12771277

@@ -1550,18 +1550,22 @@ class LnsSolver : public SubSolver {
15501550
}
15511551
if (neighborhood.is_simple) {
15521552
absl::StrAppend(
1553-
&s, " [", "relaxed:", neighborhood.num_relaxed_variables,
1554-
" in_obj:", neighborhood.num_relaxed_variables_in_objective,
1553+
&s, " [",
1554+
"relaxed:", FormatCounter(neighborhood.num_relaxed_variables),
1555+
" in_obj:",
1556+
FormatCounter(neighborhood.num_relaxed_variables_in_objective),
15551557
" compo:",
15561558
neighborhood.variables_that_can_be_fixed_to_local_optimum.size(),
15571559
"]");
15581560
}
1559-
SOLVER_LOG(shared_->logger, s, " [d:", data.difficulty,
1560-
", id:", task_id, ", dtime:", data.deterministic_time, "/",
1561-
data.deterministic_limit,
1562-
", status:", ProtoEnumToString<CpSolverStatus>(data.status),
1563-
", #calls:", generator_->num_calls(),
1564-
", p:", fully_solved_proportion, "]");
1561+
SOLVER_LOG(
1562+
shared_->logger, s,
1563+
" [d:", absl::StrFormat("%0.2e", data.difficulty), ", id:", task_id,
1564+
", dtime:", absl::StrFormat("%0.2f", data.deterministic_time), "/",
1565+
data.deterministic_limit,
1566+
", status:", ProtoEnumToString<CpSolverStatus>(data.status),
1567+
", #calls:", generator_->num_calls(),
1568+
", p:", fully_solved_proportion, "]");
15651569
}
15661570
};
15671571
}

ortools/sat/diffn.cc

Lines changed: 109 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -706,56 +706,109 @@ bool NonOverlappingRectanglesDisjunctivePropagator::
706706
FindBoxesThatMustOverlapAHorizontalLineAndPropagate(
707707
bool fast_propagation, SchedulingConstraintHelper* x,
708708
SchedulingConstraintHelper* y) {
709-
// Note that since we only push bounds on x, we cache the value for y just
710-
// once.
711-
if (!y->SynchronizeAndSetTimeDirection(true)) return false;
712-
713-
// Compute relevant boxes, the one with a mandatory part of y. Because we will
709+
// When they are many fixed box that we know do not overlap, we compute
710+
// the bounding box of the others, and we can exclude all boxes outside this
711+
// region. This can help, especially for some LNS neighborhood.
712+
int num_fixed = 0;
713+
int num_others = 0;
714+
Rectangle other_bounding_box;
715+
716+
// push_back() can be slow as it might not be inlined, so we manage directly
717+
// our "boxes" in boxes_data[0 .. num_boxes], with a memory that is always big
718+
// enough.
719+
indexed_boxes_.resize(y->NumTasks());
720+
int num_boxes = 0;
721+
IndexedInterval* boxes_data = indexed_boxes_.data();
722+
723+
// Compute relevant boxes, the one with a mandatory part on y. Because we will
714724
// need to sort it this way, we consider them by increasing start max.
715-
indexed_boxes_.clear();
716725
const auto temp = y->TaskByIncreasingNegatedStartMax();
726+
auto fixed_boxes = already_checked_fixed_boxes_.view();
717727
for (int i = temp.size(); --i >= 0;) {
718728
const int box = temp[i].task_index;
719-
// Ignore absent boxes.
720-
if (x->IsAbsent(box) || y->IsAbsent(box)) continue;
721-
722-
// Ignore boxes where the relevant presence literal is only on the y
723-
// dimension, or if both intervals are optionals with different literals.
724-
if (x->IsPresent(box) && !y->IsPresent(box)) continue;
725-
if (!x->IsPresent(box) && !y->IsPresent(box) &&
726-
x->PresenceLiteral(box) != y->PresenceLiteral(box)) {
727-
continue;
729+
730+
// By definition, fixed boxes are always present.
731+
// Doing this check optimize a bit the case where we have many fixed boxes.
732+
if (!fixed_boxes[box]) {
733+
// Ignore absent boxes.
734+
if (x->IsAbsent(box) || y->IsAbsent(box)) continue;
735+
736+
// Ignore boxes where the relevant presence literal is only on the y
737+
// dimension, or if both intervals are optionals with different literals.
738+
if (x->IsPresent(box) && !y->IsPresent(box)) continue;
739+
if (!x->IsPresent(box) && !y->IsPresent(box) &&
740+
x->PresenceLiteral(box) != y->PresenceLiteral(box)) {
741+
continue;
742+
}
728743
}
729744

745+
// Only consider box with a mandatory part on y.
730746
const IntegerValue start_max = -temp[i].time;
731747
const IntegerValue end_min = y->EndMin(box);
732748
if (start_max < end_min) {
733-
indexed_boxes_.push_back({box, start_max, end_min});
749+
boxes_data[num_boxes++] = {box, start_max, end_min};
750+
751+
// Optim: If many rectangle are fixed and known not to overlap, we might
752+
// filter them out.
753+
if (fixed_boxes[box]) {
754+
++num_fixed;
755+
} else {
756+
const bool is_fixed = x->StartIsFixed(box) && x->EndIsFixed(box) &&
757+
y->StartIsFixed(box) && y->EndIsFixed(box);
758+
if (is_fixed) {
759+
// We will "check it" below, so it will be checked next time.
760+
fixed_boxes.Set(box);
761+
}
762+
763+
const Rectangle r = {x->StartMin(box), x->EndMax(box), start_max,
764+
end_min};
765+
if (num_others == 0) {
766+
other_bounding_box = r;
767+
} else {
768+
other_bounding_box.GrowToInclude(r);
769+
}
770+
++num_others;
771+
}
734772
}
735773
}
736774

737-
// Less than 2 boxes, no propagation.
738-
if (indexed_boxes_.size() < 2) return true;
739-
740-
// In ConstructOverlappingSets() we will always sort the output by
741-
// x.ShiftedStartMin(t). We want to speed that up so we cache the order here.
742-
if (!x->SynchronizeAndSetTimeDirection(x->CurrentTimeIsForward())) {
743-
return false;
775+
// We remove from boxes_data all the fixed and checked box outside the
776+
// other_bounding_box.
777+
//
778+
// TODO(user): We could be smarter here, if we have just a few non-fixed
779+
// boxes, likely their mandatory y-part do not span the whole horizon, so
780+
// we could remove any fixed boxes outside these "stripes".
781+
if (num_others == 0) return true;
782+
if (num_fixed > 0) {
783+
int new_size = 0;
784+
for (int i = 0; i < num_boxes; ++i) {
785+
const IndexedInterval& interval = boxes_data[i];
786+
const int box = interval.index;
787+
const Rectangle r = {x->StartMin(box), x->EndMax(box), interval.start,
788+
interval.end};
789+
if (other_bounding_box.IsDisjoint(r)) continue;
790+
boxes_data[new_size++] = interval;
791+
}
792+
num_boxes = new_size;
744793
}
745794

746-
// Optim: Abort if all rectangle can be fixed to their mandatory y + minimium
747-
// x position without any overlap. Technically we might still propagate the x
748-
// end in this setting, but the current code will just abort below in
749-
// SplitDisjointBoxes() anyway.
795+
// Less than 2 boxes, no propagation.
796+
const auto boxes = absl::MakeSpan(boxes_data, num_boxes);
797+
if (boxes.size() < 2) return true;
798+
799+
// Optim: Abort if all rectangle can be fixed to their mandatory y +
800+
// minimium x position without any overlap.
750801
//
751802
// This is guaranteed to be O(N log N) whereas the algo below is O(N ^ 2).
752-
if (indexed_boxes_.size() > 100) {
803+
//
804+
// TODO(user): We might still propagate the x end in this setting, but the
805+
// current code will just abort below in SplitDisjointBoxes() anyway.
806+
{
753807
rectangles_.clear();
754-
rectangles_.reserve(indexed_boxes_.size());
755-
for (const auto [box, y_mandatory_start, y_mandatory_end] :
756-
indexed_boxes_) {
757-
// Note that we invert the x/y position here in order to be already sorted
758-
// for FindOneIntersectionIfPresent()
808+
rectangles_.reserve(boxes.size());
809+
for (const auto [box, y_mandatory_start, y_mandatory_end] : boxes) {
810+
// Note that we invert the x/y position here in order to be already
811+
// sorted for FindOneIntersectionIfPresent()
759812
rectangles_.push_back(
760813
{/*x_min=*/y_mandatory_start, /*x_max=*/y_mandatory_end,
761814
/*y_min=*/x->StartMin(box), /*y_max=*/x->EndMin(box)});
@@ -769,6 +822,8 @@ bool NonOverlappingRectanglesDisjunctivePropagator::
769822
}
770823
if (opt_pair == std::nullopt) {
771824
return true;
825+
} else {
826+
// TODO(user): Test if we have a conflict here.
772827
}
773828
}
774829

@@ -779,13 +834,12 @@ bool NonOverlappingRectanglesDisjunctivePropagator::
779834
order_[t] = i++;
780835
}
781836
}
782-
ConstructOverlappingSets(absl::MakeSpan(indexed_boxes_),
783-
&events_overlapping_boxes_, order_);
837+
ConstructOverlappingSets(boxes, &events_overlapping_boxes_, order_);
784838

785839
// Split lists of boxes into disjoint set of boxes (w.r.t. overlap).
786840
boxes_to_propagate_.clear();
787841
reduced_overlapping_boxes_.clear();
788-
int work_done = indexed_boxes_.size();
842+
int work_done = boxes.size();
789843
for (int i = 0; i < events_overlapping_boxes_.size(); ++i) {
790844
work_done += events_overlapping_boxes_[i].size();
791845
SplitDisjointBoxes(*x, events_overlapping_boxes_[i], &disjoint_boxes_);
@@ -803,10 +857,11 @@ bool NonOverlappingRectanglesDisjunctivePropagator::
803857

804858
// And finally propagate.
805859
//
806-
// TODO(user): Sorting of boxes seems influential on the performance. Test.
860+
// TODO(user): Sorting of boxes seems influential on the performance.
861+
// Test.
807862
for (const absl::Span<const int> boxes : boxes_to_propagate_) {
808-
// The case of two boxes should be taken care of during "fast" propagation,
809-
// so we can skip it here.
863+
// The case of two boxes should be taken care of during "fast"
864+
// propagation, so we can skip it here.
810865
if (!fast_propagation && boxes.size() <= 2) continue;
811866

812867
x_.ClearOtherHelper();
@@ -857,9 +912,22 @@ bool NonOverlappingRectanglesDisjunctivePropagator::
857912
return true;
858913
}
859914

915+
// Note that we optimized this function for two main use cases:
916+
// - smallish problem where we don't have more than 100 boxes.
917+
// - large problem with many 1000s boxes, but with only a small subset that is
918+
// not fixed (mainly coming from LNS).
860919
bool NonOverlappingRectanglesDisjunctivePropagator::Propagate() {
861-
global_x_.SetTimeDirection(true);
862-
global_y_.SetTimeDirection(true);
920+
if (!global_x_.SynchronizeAndSetTimeDirection(true)) return false;
921+
if (!global_y_.SynchronizeAndSetTimeDirection(true)) return false;
922+
923+
// If we are "diving" we maintain the set of fixed boxes for which we know
924+
// that they are not overlapping.
925+
const bool backtrack_since_last_call = !rev_is_in_dive_;
926+
watcher_->SetUntilNextBacktrack(&rev_is_in_dive_);
927+
if (backtrack_since_last_call) {
928+
const int num_tasks = global_x_.NumTasks();
929+
already_checked_fixed_boxes_.ClearAndResize(num_tasks);
930+
}
863931

864932
// Note that the code assumes that this was registered twice in fast and slow
865933
// mode. So we will not redo some propagation in slow mode that was already

ortools/sat/diffn.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,11 @@ class NonOverlappingRectanglesDisjunctivePropagator
127127
std::vector<int> order_;
128128
CompactVectorVector<int> events_overlapping_boxes_;
129129

130+
// List of box that are fully fixed in the current dive, and for which we
131+
// know they are no conflict between them.
132+
bool rev_is_in_dive_ = false;
133+
Bitset64<int> already_checked_fixed_boxes_;
134+
130135
absl::flat_hash_set<absl::Span<const int>> reduced_overlapping_boxes_;
131136
std::vector<absl::Span<const int>> boxes_to_propagate_;
132137
std::vector<absl::Span<const int>> disjoint_boxes_;

0 commit comments

Comments
 (0)