Google OR-Tools v9.14
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
diffn.cc
Go to the documentation of this file.
1// Copyright 2010-2025 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14#include "ortools/sat/diffn.h"
15
16#include <stddef.h>
17
18#include <algorithm>
19#include <cstddef>
20#include <cstdint>
21#include <iterator>
22#include <limits>
23#include <optional>
24#include <string>
25#include <utility>
26#include <vector>
27
28#include "absl/container/flat_hash_set.h"
29#include "absl/container/inlined_vector.h"
30#include "absl/log/check.h"
31#include "absl/log/log.h"
32#include "absl/log/vlog_is_on.h"
33#include "absl/numeric/bits.h"
34#include "absl/types/span.h"
40#include "ortools/sat/clause.h"
44#include "ortools/sat/integer.h"
48#include "ortools/sat/model.h"
58
59namespace operations_research {
60namespace sat {
61
62namespace {
63
64IntegerVariable CreateVariableWithTightDomain(
65 absl::Span<const AffineExpression> exprs, Model* model) {
66 IntegerValue min = kMaxIntegerValue;
67 IntegerValue max = kMinIntegerValue;
68 auto* integer_trail = model->GetOrCreate<IntegerTrail>();
69 for (const AffineExpression& e : exprs) {
70 min = std::min(min, integer_trail->LevelZeroLowerBound(e));
71 max = std::max(max, integer_trail->LevelZeroUpperBound(e));
72 }
73 return integer_trail->AddIntegerVariable(min, max);
74}
75
76IntegerVariable CreateVariableAtOrAboveMinOf(
77 absl::Span<const AffineExpression> exprs, Model* model) {
78 const IntegerVariable var = CreateVariableWithTightDomain(exprs, model);
79 auto* constraint = new MinPropagator({exprs.begin(), exprs.end()}, var,
80 model->GetOrCreate<IntegerTrail>());
81 constraint->RegisterWith(model->GetOrCreate<GenericLiteralWatcher>());
82 model->TakeOwnership(constraint);
83
84 return var;
85}
86
87IntegerVariable CreateVariableAtOrBelowMaxOf(
88 absl::Span<const AffineExpression> exprs, Model* model) {
89 std::vector<AffineExpression> negated_exprs(exprs.begin(), exprs.end());
90 for (AffineExpression& affine : negated_exprs) affine = affine.Negated();
91
92 const IntegerVariable var =
93 CreateVariableWithTightDomain(negated_exprs, model);
94 auto* constraint = new MinPropagator(std::move(negated_exprs), var,
95 model->GetOrCreate<IntegerTrail>());
96 constraint->RegisterWith(model->GetOrCreate<GenericLiteralWatcher>());
97 model->TakeOwnership(constraint);
98
99 return NegationOf(var);
100}
101
102// Add a cumulative relaxation. That is, on one dimension, it does not enforce
103// the rectangle aspect, allowing vertical slices to move freely.
104void AddDiffnCumulativeRelationOnX(SchedulingConstraintHelper* x,
106 Model* model) {
107 // Note that we only need one side!
108 // We want something <= max_end - min_start
109 //
110 // TODO(user): Use conditional affine min/max !!
111 bool all_optional = true;
112 for (int i = 0; i < y->NumTasks(); ++i) {
113 if (!y->IsOptional(i)) {
114 all_optional = false;
115 break;
116 }
117 }
118
119 AffineExpression capacity;
120 if (all_optional) {
121 IntegerValue min_start = kMaxIntegerValue;
122 IntegerValue max_end = kMinIntegerValue;
123 for (int i = 0; i < y->NumTasks(); ++i) {
124 min_start = std::min(min_start, y->LevelZeroStartMin(i));
125 max_end = std::max(max_end, y->LevelZeroEndMax(i));
126 }
127 if (max_end < min_start) {
128 return;
129 }
130 capacity = AffineExpression(CapSubI(max_end, min_start).value());
131 } else {
132 // This might not work if all task are optional, since the min could be
133 // greater than the max.
134 const IntegerVariable min_start_var =
135 CreateVariableAtOrAboveMinOf(y->Starts(), model);
136 const IntegerVariable max_end_var =
137 CreateVariableAtOrBelowMaxOf(y->Ends(), model);
138
139 auto* integer_trail = model->GetOrCreate<IntegerTrail>();
140 if (integer_trail->UpperBound(max_end_var) <
141 integer_trail->LowerBound(min_start_var)) {
142 // Trivial infeasible case, will be handled by the linear constraint
143 // from the interval.
144 return;
145 }
146 // (max_end - min_start) >= capacity.
147 capacity = model->Add(NewIntegerVariable(
148 0, CapSub(integer_trail->UpperBound(max_end_var).value(),
149 integer_trail->LowerBound(min_start_var).value())));
150 const std::vector<int64_t> coeffs = {-capacity.coeff.value(), -1, 1};
151 model->Add(
152 WeightedSumGreaterOrEqual({capacity.var, min_start_var, max_end_var},
153 coeffs, capacity.constant.value()));
154 }
155
156 SchedulingDemandHelper* demands =
157 model->GetOrCreate<IntervalsRepository>()->GetOrCreateDemandHelper(
158 x, y->Sizes());
159
160 // Propagator responsible for applying Timetabling filtering rule. It
161 // increases the minimum of the start variables, decrease the maximum of the
162 // end variables, and increase the minimum of the capacity variable.
163 const SatParameters& params = *model->GetOrCreate<SatParameters>();
164 if (params.use_timetabling_in_no_overlap_2d()) {
165 TimeTablingPerTask* time_tabling =
166 new TimeTablingPerTask(capacity, x, demands, model);
167 time_tabling->RegisterWith(model->GetOrCreate<GenericLiteralWatcher>());
168 model->TakeOwnership(time_tabling);
169 }
170
171 // Propagator responsible for applying the Overload Checking filtering rule.
172 // It increases the minimum of the capacity variable.
173 if (params.use_energetic_reasoning_in_no_overlap_2d()) {
174 AddCumulativeOverloadChecker(capacity, x, demands, model);
175 }
176}
177
178} // namespace
179
180void AddNonOverlappingRectangles(const std::vector<IntervalVariable>& x,
181 const std::vector<IntervalVariable>& y,
182 Model* model) {
184 NoOverlap2DConstraintHelper* helper_2d =
185 repository->GetOrCreate2DHelper(x, y);
186
187 GenericLiteralWatcher* const watcher =
189
190 CreateAndRegisterMandatoryOverlapPropagator(helper_2d, model, watcher, 3);
191 if (model->GetOrCreate<SatParameters>()
193 Precedences2DPropagator* propagator =
194 new Precedences2DPropagator(helper_2d, model);
195 watcher->SetPropagatorPriority(propagator->RegisterWith(watcher), 4);
196 model->TakeOwnership(propagator);
197 }
198
201 constraint->Register(/*fast_priority=*/3, /*slow_priority=*/4);
202 model->TakeOwnership(constraint);
203
204 RectanglePairwisePropagator* pairwise_propagator =
205 new RectanglePairwisePropagator(helper_2d, model);
206 watcher->SetPropagatorPriority(pairwise_propagator->RegisterWith(watcher), 4);
207 model->TakeOwnership(pairwise_propagator);
208
209 const SatParameters& params = *model->GetOrCreate<SatParameters>();
210 const bool add_cumulative_relaxation =
213
214 if (add_cumulative_relaxation) {
215 SchedulingConstraintHelper* x_helper = repository->GetOrCreateHelper(x);
216 SchedulingConstraintHelper* y_helper = repository->GetOrCreateHelper(y);
217
218 // We must first check if the cumulative relaxation is possible.
219 bool some_boxes_are_only_optional_on_x = false;
220 bool some_boxes_are_only_optional_on_y = false;
221 for (int i = 0; i < x.size(); ++i) {
222 if (x_helper->IsOptional(i) && y_helper->IsOptional(i) &&
223 x_helper->PresenceLiteral(i) != y_helper->PresenceLiteral(i)) {
224 // Abort as the task would be conditioned by two literals.
225 return;
226 }
227 if (x_helper->IsOptional(i) && !y_helper->IsOptional(i)) {
228 // We cannot use x_size as the demand of the cumulative based on
229 // the y_intervals.
230 some_boxes_are_only_optional_on_x = true;
231 }
232 if (y_helper->IsOptional(i) && !x_helper->IsOptional(i)) {
233 // We cannot use y_size as the demand of the cumulative based on
234 // the y_intervals.
235 some_boxes_are_only_optional_on_y = true;
236 }
237 }
238 if (!some_boxes_are_only_optional_on_y) {
239 AddDiffnCumulativeRelationOnX(x_helper, y_helper, model);
240 }
241 if (!some_boxes_are_only_optional_on_x) {
242 AddDiffnCumulativeRelationOnX(y_helper, x_helper, model);
243 }
244 }
245
248 new NonOverlappingRectanglesEnergyPropagator(helper_2d, model);
249 GenericLiteralWatcher* const watcher =
251 watcher->SetPropagatorPriority(energy_constraint->RegisterWith(watcher), 5);
252 model->TakeOwnership(energy_constraint);
253 }
254
256 CreateAndRegisterTryEdgePropagator(helper_2d, model, watcher, 5);
257 }
258
259 // Create all 2D "precedence" Booleans.
260 //
261 // TODO(user): For now we only deal with mandatory boxes.
262 //
263 // TODO(user): Like we do for 1D, one way to scale this is to only create such
264 // Boolean dynamically as we need to take a decision, and the previously
265 // created one are all assigned. It is a bit trickier though because of
266 // the extra constraints between these Booleans. Maybe one easy step is to
267 // create all 4 Booleans for a given pair of boxes at once.
268 //
269 // TODO(user): Tricky, it would be better to use the helper_2d instead of the
270 // general repository, however, as we add constraints, this propagates which
271 // might swap/change the underlying x_helper and y_helper...
272 const int num_boxes = x.size();
273 if (num_boxes < params.no_overlap_2d_boolean_relations_limit()) {
274 auto* implications = model->GetOrCreate<BinaryImplicationGraph>();
275 auto* sat_solver = model->GetOrCreate<SatSolver>();
276 auto* integer_trail = model->GetOrCreate<IntegerTrail>();
277 DCHECK_EQ(sat_solver->CurrentDecisionLevel(), 0);
278
279 for (int i = 0; i < num_boxes; ++i) {
280 if (repository->IsAbsent(x[i])) continue;
281 if (repository->IsAbsent(y[i])) continue;
282 for (int j = i + 1; j < num_boxes; ++j) {
283 if (repository->IsAbsent(x[j])) continue;
284 if (repository->IsAbsent(y[j])) continue;
285
286 // At most one of these two x options is true.
287 const Literal x_ij = repository->GetOrCreatePrecedenceLiteral(
288 repository->End(x[i]), repository->Start(x[j]));
289 const Literal x_ji = repository->GetOrCreatePrecedenceLiteral(
290 repository->End(x[j]), repository->Start(x[i]));
291 if ((integer_trail->LowerBound(repository->Size(x[i])) > 0 ||
292 integer_trail->LowerBound(repository->Size(x[j])) > 0) &&
293 !implications->AddAtMostOne({x_ij, x_ji})) {
294 sat_solver->NotifyThatModelIsUnsat();
295 return;
296 }
297
298 // At most one of these two y options is true.
299 const Literal y_ij = repository->GetOrCreatePrecedenceLiteral(
300 repository->End(y[i]), repository->Start(y[j]));
301 const Literal y_ji = repository->GetOrCreatePrecedenceLiteral(
302 repository->End(y[j]), repository->Start(y[i]));
303 if ((integer_trail->LowerBound(repository->Size(y[i])) > 0 ||
304 integer_trail->LowerBound(repository->Size(y[j])) > 0) &&
305 !implications->AddAtMostOne({y_ij, y_ji})) {
306 sat_solver->NotifyThatModelIsUnsat();
307 return;
308 }
309
310 // At least one of the 4 options is true.
311 std::vector<Literal> clause = {x_ij, x_ji, y_ij, y_ji};
312 if (repository->IsOptional(x[i])) {
313 clause.push_back(repository->PresenceLiteral(x[i]).Negated());
314 }
315 if (repository->IsOptional(y[i])) {
316 clause.push_back(repository->PresenceLiteral(y[i]).Negated());
317 }
318 if (repository->IsOptional(x[j])) {
319 clause.push_back(repository->PresenceLiteral(x[j]).Negated());
320 }
321 if (repository->IsOptional(y[j])) {
322 clause.push_back(repository->PresenceLiteral(y[j]).Negated());
323 }
325 if (!sat_solver->AddProblemClause(clause)) {
326 return;
327 }
328 }
329 }
330 }
331}
332
333#define RETURN_IF_FALSE(f) \
334 if (!(f)) return false;
335
338 if (!VLOG_IS_ON(1)) return;
339 std::vector<std::pair<std::string, int64_t>> stats;
340 stats.push_back(
341 {"NonOverlappingRectanglesEnergyPropagator/called", num_calls_});
342 stats.push_back(
343 {"NonOverlappingRectanglesEnergyPropagator/conflicts", num_conflicts_});
344 stats.push_back(
345 {"NonOverlappingRectanglesEnergyPropagator/conflicts_two_boxes",
346 num_conflicts_two_boxes_});
347 stats.push_back({"NonOverlappingRectanglesEnergyPropagator/refined",
348 num_refined_conflicts_});
349 stats.push_back(
350 {"NonOverlappingRectanglesEnergyPropagator/conflicts_with_slack",
351 num_conflicts_with_slack_});
352
353 shared_stats_->AddStats(stats);
354}
355
357 // TODO(user): double-check/revisit the algo for box of variable sizes.
358 const int num_boxes = helper_.NumBoxes();
359 if (!helper_.SynchronizeAndSetDirection()) return false;
360
361 Rectangle bounding_box = {.x_min = std::numeric_limits<IntegerValue>::max(),
362 .x_max = std::numeric_limits<IntegerValue>::min(),
363 .y_min = std::numeric_limits<IntegerValue>::max(),
364 .y_max = std::numeric_limits<IntegerValue>::min()};
365 std::vector<RectangleInRange> active_box_ranges;
366 active_box_ranges.reserve(num_boxes);
367 for (int box = 0; box < num_boxes; ++box) {
368 if (!helper_.IsPresent(box)) continue;
369 RectangleInRange rec = helper_.GetItemRangeForSizeMin(box);
370 if (rec.x_size == 0 || rec.y_size == 0) continue;
371 bounding_box.GrowToInclude(rec.bounding_area);
372 active_box_ranges.push_back(std::move(rec));
373 }
374
375 if (active_box_ranges.size() < 2) {
376 return true;
377 }
378
379 // Our algo is quadratic, so we don't want to run it on really large problems.
380 if (active_box_ranges.size() > 1000) {
381 return true;
382 }
383
385 CapProdI(CapProdI(bounding_box.SizeX(), bounding_box.SizeY()),
386 active_box_ranges.size()))) {
387 // Avoid integer overflows if the area of the boxes get comparable with
388 // INT64_MAX.
389 return true;
390 }
391
392 num_calls_++;
393
394 std::optional<Conflict> best_conflict =
395 FindConflict(std::move(active_box_ranges));
396 if (!best_conflict.has_value()) {
397 return true;
398 }
399 num_conflicts_++;
400
401 // We found a conflict, so we can afford to run the propagator again to
402 // search for a best explanation. This is specially the case since we only
403 // want to re-run it over the items that participate in the conflict, so it is
404 // a much smaller problem.
405 IntegerValue best_explanation_size =
406 best_conflict->opp_result.GetItemsParticipatingOnConflict().size();
407 bool refined = false;
408 while (true) {
409 std::vector<RectangleInRange> items_participating_in_conflict;
410 items_participating_in_conflict.reserve(
411 best_conflict->items_for_opp.size());
412 for (const auto& item :
413 best_conflict->opp_result.GetItemsParticipatingOnConflict()) {
414 items_participating_in_conflict.push_back(
415 best_conflict->items_for_opp[item.index]);
416 }
417 std::optional<Conflict> conflict =
418 FindConflict(items_participating_in_conflict);
419 if (!conflict.has_value()) break;
420 // We prefer an explanation with the least number of boxes.
421 if (conflict->opp_result.GetItemsParticipatingOnConflict().size() >=
422 best_explanation_size) {
423 // The new explanation isn't better than the old one. Stop trying.
424 break;
425 }
426 best_explanation_size =
427 conflict->opp_result.GetItemsParticipatingOnConflict().size();
428 best_conflict = std::move(conflict);
429 refined = true;
430 }
431
432 num_refined_conflicts_ += refined;
433 const std::vector<RectangleInRange> generalized_explanation =
434 GeneralizeExplanation(*best_conflict);
435 if (best_explanation_size == 2) {
436 num_conflicts_two_boxes_++;
437 }
438 helper_.ReportConflictFromInfeasibleBoxRanges(generalized_explanation);
439 return false;
440}
441
442std::optional<NonOverlappingRectanglesEnergyPropagator::Conflict>
443NonOverlappingRectanglesEnergyPropagator::FindConflict(
444 std::vector<RectangleInRange> active_box_ranges) {
445 const auto rectangles_with_too_much_energy =
446 FindRectanglesWithEnergyConflictMC(active_box_ranges, *random_, 1.0, 0.8);
447
448 if (rectangles_with_too_much_energy.conflicts.empty() &&
449 rectangles_with_too_much_energy.candidates.empty()) {
450 return std::nullopt;
451 }
452
453 Conflict best_conflict;
454
455 // Sample 10 rectangles (at least five among the ones for which we already
456 // detected an energy overflow), extract an orthogonal packing subproblem for
457 // each and look for conflict. Sampling avoids making this heuristic too
458 // costly.
459 constexpr int kSampleSize = 10;
460 absl::InlinedVector<Rectangle, kSampleSize> sampled_rectangles;
461 std::sample(rectangles_with_too_much_energy.conflicts.begin(),
462 rectangles_with_too_much_energy.conflicts.end(),
463 std::back_inserter(sampled_rectangles), 5, *random_);
464 std::sample(rectangles_with_too_much_energy.candidates.begin(),
465 rectangles_with_too_much_energy.candidates.end(),
466 std::back_inserter(sampled_rectangles),
467 kSampleSize - sampled_rectangles.size(), *random_);
468 std::sort(sampled_rectangles.begin(), sampled_rectangles.end(),
469 [](const Rectangle& a, const Rectangle& b) {
470 const bool larger = std::make_pair(a.SizeX(), a.SizeY()) >
471 std::make_pair(b.SizeX(), b.SizeY());
472 // Double-check the invariant from
473 // FindRectanglesWithEnergyConflictMC() that given two returned
474 // rectangles there is one that is fully inside the other.
475 if (larger) {
476 // Rectangle b is fully contained inside a
477 DCHECK(a.x_min <= b.x_min && a.x_max >= b.x_max &&
478 a.y_min <= b.y_min && a.y_max >= b.y_max);
479 } else {
480 // Rectangle a is fully contained inside b
481 DCHECK(a.x_min >= b.x_min && a.x_max <= b.x_max &&
482 a.y_min >= b.y_min && a.y_max <= b.y_max);
483 }
484 return larger;
485 });
486 std::vector<IntegerValue> sizes_x, sizes_y;
487 sizes_x.reserve(active_box_ranges.size());
488 sizes_y.reserve(active_box_ranges.size());
489 std::vector<RectangleInRange> filtered_items;
490 filtered_items.reserve(active_box_ranges.size());
491 for (const Rectangle& r : sampled_rectangles) {
492 sizes_x.clear();
493 sizes_y.clear();
494 filtered_items.clear();
495 for (int i = 0; i < active_box_ranges.size(); ++i) {
496 const RectangleInRange& box = active_box_ranges[i];
497 const Rectangle intersection = box.GetMinimumIntersection(r);
498 if (intersection.SizeX() > 0 && intersection.SizeY() > 0) {
499 sizes_x.push_back(intersection.SizeX());
500 sizes_y.push_back(intersection.SizeY());
501 filtered_items.push_back(box);
502 }
503 }
504 // This check the feasibility of a related orthogonal packing problem where
505 // our rectangle is the bounding box, and we need to fit inside it a set of
506 // items corresponding to the minimum intersection of the original items
507 // with this bounding box.
508 const auto opp_result = orthogonal_packing_checker_.TestFeasibility(
509 sizes_x, sizes_y, {r.SizeX(), r.SizeY()},
511 .use_pairwise = true,
512 .use_dff_f0 = true,
513 .use_dff_f2 = true,
514 .brute_force_threshold = 7,
515 .dff2_max_number_of_parameters_to_check = 100});
516 if (opp_result.GetResult() == OrthogonalPackingResult::Status::INFEASIBLE &&
517 (best_conflict.opp_result.GetResult() !=
519 best_conflict.opp_result.GetItemsParticipatingOnConflict().size() >
520 opp_result.GetItemsParticipatingOnConflict().size())) {
521 best_conflict.items_for_opp = filtered_items;
522 best_conflict.opp_result = opp_result;
523 best_conflict.rectangle_with_too_much_energy = r;
524 }
525 // Use the fact that our rectangles are ordered in shrinking order to remove
526 // all items that will never contribute again.
527 filtered_items.swap(active_box_ranges);
528 }
529 if (best_conflict.opp_result.GetResult() ==
531 return best_conflict;
532 } else {
533 return std::nullopt;
534 }
535}
536
537std::vector<RectangleInRange>
538NonOverlappingRectanglesEnergyPropagator::GeneralizeExplanation(
539 const Conflict& conflict) {
540 const Rectangle& rectangle = conflict.rectangle_with_too_much_energy;
541 OrthogonalPackingResult relaxed_result = conflict.opp_result;
542
543 // Use the potential slack to have a stronger reason.
544 int used_slack_count = 0;
545 const auto& items = relaxed_result.GetItemsParticipatingOnConflict();
546 for (int i = 0; i < items.size(); ++i) {
547 if (!relaxed_result.HasSlack()) {
548 break;
549 }
550 const RectangleInRange& range = conflict.items_for_opp[items[i].index];
551 const RectangleInRange item_in_zero_level_range = {
552 .bounding_area =
553 {.x_min = helper_.x_helper().LevelZeroStartMin(range.box_index),
554 .x_max = helper_.x_helper().LevelZeroStartMax(range.box_index) +
555 range.x_size,
556 .y_min = helper_.y_helper().LevelZeroStartMin(range.box_index),
557 .y_max = helper_.y_helper().LevelZeroStartMax(range.box_index) +
558 range.y_size},
559 .x_size = range.x_size,
560 .y_size = range.y_size};
561 // There is no point trying to intersect less the item with the rectangle
562 // than it would on zero-level. So do not waste the slack with it.
563 const Rectangle max_overlap =
564 item_in_zero_level_range.GetMinimumIntersection(rectangle);
565 used_slack_count += relaxed_result.TryUseSlackToReduceItemSize(
566 i, OrthogonalPackingResult::Coord::kCoordX, max_overlap.SizeX());
567 used_slack_count += relaxed_result.TryUseSlackToReduceItemSize(
568 i, OrthogonalPackingResult::Coord::kCoordY, max_overlap.SizeY());
569 }
570 num_conflicts_with_slack_ += (used_slack_count > 0);
571 VLOG_EVERY_N_SEC(2, 3)
572 << "Found a conflict on the OPP sub-problem of rectangle: " << rectangle
573 << " using "
574 << conflict.opp_result.GetItemsParticipatingOnConflict().size() << "/"
575 << conflict.items_for_opp.size() << " items.";
576
577 std::vector<RectangleInRange> ranges_for_explanation;
578 ranges_for_explanation.reserve(conflict.items_for_opp.size());
579 std::vector<OrthogonalPackingResult::Item> sorted_items =
580 relaxed_result.GetItemsParticipatingOnConflict();
581 // TODO(user) understand why sorting high-impact items first improves the
582 // benchmarks
583 std::sort(sorted_items.begin(), sorted_items.end(),
584 [](const OrthogonalPackingResult::Item& a,
585 const OrthogonalPackingResult::Item& b) {
586 return a.size_x * a.size_y > b.size_x * b.size_y;
587 });
588 for (const auto& item : sorted_items) {
589 const RectangleInRange& range = conflict.items_for_opp[item.index];
590 ranges_for_explanation.push_back(
591 RectangleInRange::BiggestWithMinIntersection(rectangle, range,
592 item.size_x, item.size_y));
593 }
594 return ranges_for_explanation;
595}
596
598 GenericLiteralWatcher* watcher) {
599 const int id = watcher->Register(this);
600 helper_.WatchAllBoxes(id);
601 return id;
602}
603
604namespace {
605
606// We want for different propagation to reuse as much as possible the same
607// line. The idea behind this is to compute the 'canonical' line to use
608// when explaining that boxes overlap on the 'y_dim' dimension. We compute
609// the multiple of the biggest power of two that is common to all boxes.
610IntegerValue FindCanonicalValue(IntegerValue lb, IntegerValue ub) {
611 if (lb == ub) return lb;
612 if (lb <= 0 && ub > 0) return IntegerValue(0);
613 if (lb < 0 && ub <= 0) {
614 return -FindCanonicalValue(-ub, -lb);
615 }
616
617 int64_t mask = 0;
618 IntegerValue candidate = ub;
619 for (int o = 0; o < 62; ++o) {
620 mask = 2 * mask + 1;
621 const IntegerValue masked_ub(ub.value() & ~mask);
622 if (masked_ub >= lb) {
623 candidate = masked_ub;
624 } else {
625 break;
626 }
627 }
628 return candidate;
629}
630
631void SplitDisjointBoxes(const SchedulingConstraintHelper& x,
632 absl::Span<const int> boxes,
633 std::vector<absl::Span<const int>>* result) {
634 result->clear();
635 DCHECK(std::is_sorted(boxes.begin(), boxes.end(), [&x](int a, int b) {
636 return x.ShiftedStartMin(a) < x.ShiftedStartMin(b);
637 }));
638 int current_start = 0;
639 std::size_t current_length = 1;
640 IntegerValue current_max_end = x.EndMax(boxes[0]);
641
642 for (int b = 1; b < boxes.size(); ++b) {
643 const int box = boxes[b];
644 if (x.ShiftedStartMin(box) < current_max_end) {
645 // Merge.
646 current_length++;
647 current_max_end = std::max(current_max_end, x.EndMax(box));
648 } else {
649 if (current_length > 1) { // Ignore lists of size 1.
650 result->emplace_back(&boxes[current_start], current_length);
651 }
652 current_start = b;
653 current_length = 1;
654 current_max_end = x.EndMax(box);
655 }
656 }
657
658 // Push last span.
659 if (current_length > 1) {
660 result->emplace_back(&boxes[current_start], current_length);
661 }
662}
663
664} // namespace
665
666// Note that x_ and y_ must be initialized with enough intervals when passed
667// to the disjunctive propagators.
668NonOverlappingRectanglesDisjunctivePropagator::
669 NonOverlappingRectanglesDisjunctivePropagator(
670 NoOverlap2DConstraintHelper* helper, Model* model)
671 : helper_(helper),
672 x_(helper->NumBoxes(), model),
673 watcher_(model->GetOrCreate<GenericLiteralWatcher>()),
674 time_limit_(model->GetOrCreate<TimeLimit>()),
675 overload_checker_(&x_),
676 forward_detectable_precedences_(true, &x_),
677 backward_detectable_precedences_(false, &x_),
678 forward_not_last_(true, &x_),
679 backward_not_last_(false, &x_),
680 forward_edge_finding_(true, &x_),
681 backward_edge_finding_(false, &x_),
682 disjunctive_with_two_items_(&x_) {}
683
686
688 int fast_priority, int slow_priority) {
689 fast_id_ = watcher_->Register(this);
690 watcher_->SetPropagatorPriority(fast_id_, fast_priority);
691 helper_->WatchAllBoxes(fast_id_);
692
693 // This propagator is the one making sure our propagation is complete, so
694 // we do need to make sure it is called again if it modified some bounds.
695 watcher_->NotifyThatPropagatorMayNotReachFixedPointInOnePass(fast_id_);
696
697 const int slow_id = watcher_->Register(this);
698 watcher_->SetPropagatorPriority(slow_id, slow_priority);
699 helper_->WatchAllBoxes(slow_id);
700}
701
702bool NonOverlappingRectanglesDisjunctivePropagator::
703 FindBoxesThatMustOverlapAHorizontalLineAndPropagate(
704 bool fast_propagation, absl::Span<const int> requested_boxes) {
705 // When they are many fixed box that we know do not overlap, we compute
706 // the bounding box of the others, and we can exclude all boxes outside this
707 // region. This can help, especially for some LNS neighborhood.
708 int num_fixed = 0;
709 int num_others = 0;
710 Rectangle other_bounding_box;
711
712 // push_back() can be slow as it might not be inlined, so we manage directly
713 // our "boxes" in boxes_data[0 .. num_boxes], with a memory that is always big
714 // enough.
715 indexed_boxes_.resize(helper_->NumBoxes());
716 int num_boxes = 0;
717 IndexedInterval* boxes_data = indexed_boxes_.data();
718
719 SchedulingConstraintHelper* x = &helper_->x_helper();
720 SchedulingConstraintHelper* y = &helper_->y_helper();
721
722 // Optimization: we only initialize the set if we don't have all task here.
723 absl::flat_hash_set<int> requested_boxes_set;
724 const bool not_all_boxes = requested_boxes.size() != helper_->NumBoxes();
725 if (not_all_boxes) {
726 requested_boxes_set = {requested_boxes.begin(), requested_boxes.end()};
727 }
728
729 // Compute relevant boxes, the one with a mandatory part on y. Because we will
730 // need to sort it this way, we consider them by increasing start max.
731 const auto temp = y->TaskByIncreasingNegatedStartMax();
732 auto fixed_boxes = already_checked_fixed_boxes_.view();
733 for (int i = temp.size(); --i >= 0;) {
734 const int box = temp[i].task_index;
735 if (not_all_boxes && !requested_boxes_set.contains(box)) continue;
736
737 // By definition, fixed boxes are always present.
738 // Doing this check optimize a bit the case where we have many fixed boxes.
739 if (!fixed_boxes[box]) {
740 // Ignore absent boxes.
741 if (x->IsAbsent(box) || y->IsAbsent(box)) continue;
742
743 // Ignore boxes where the relevant presence literal is only on the y
744 // dimension, or if both intervals are optionals with different literals.
745 if (x->IsPresent(box) && !y->IsPresent(box)) continue;
746 if (!x->IsPresent(box) && !y->IsPresent(box) &&
747 x->PresenceLiteral(box) != y->PresenceLiteral(box)) {
748 continue;
749 }
750 }
751
752 // Only consider box with a mandatory part on y.
753 const IntegerValue start_max = -temp[i].time;
754 const IntegerValue end_min = y->EndMin(box);
755 if (start_max < end_min) {
756 boxes_data[num_boxes++] = {box, start_max, end_min};
757
758 // Optim: If many rectangle are fixed and known not to overlap, we might
759 // filter them out.
760 if (fixed_boxes[box]) {
761 ++num_fixed;
762 } else {
763 if (helper_->IsFixed(box)) {
764 // We will "check it" below, so it will be checked next time.
765 fixed_boxes.Set(box);
766 }
767
768 const Rectangle r = {x->StartMin(box), x->EndMax(box), start_max,
769 end_min};
770 if (num_others == 0) {
771 other_bounding_box = r;
772 } else {
773 other_bounding_box.GrowToInclude(r);
774 }
775 ++num_others;
776 }
777 }
778 }
779
780 // We remove from boxes_data all the fixed and checked box outside the
781 // other_bounding_box.
782 //
783 // TODO(user): We could be smarter here, if we have just a few non-fixed
784 // boxes, likely their mandatory y-part do not span the whole horizon, so
785 // we could remove any fixed boxes outside these "stripes".
786 if (num_others == 0) return true;
787 if (num_fixed > 0) {
788 int new_size = 0;
789 for (int i = 0; i < num_boxes; ++i) {
790 const IndexedInterval& interval = boxes_data[i];
791 const int box = interval.index;
792 const Rectangle r = {x->StartMin(box), x->EndMax(box), interval.start,
793 interval.end};
794 if (other_bounding_box.IsDisjoint(r)) continue;
795 boxes_data[new_size++] = interval;
796 }
797 num_boxes = new_size;
798 }
799
800 // Less than 2 boxes, no propagation.
801 const auto boxes = absl::MakeSpan(boxes_data, num_boxes);
802 if (boxes.size() < 2) return true;
803
804 // Optim: Abort if all rectangle can be fixed to their mandatory y +
805 // minimum x position without any overlap.
806 //
807 // This is guaranteed to be O(N log N) whereas the algo below is O(N ^ 2).
808 //
809 // TODO(user): We might still propagate the x end in this setting, but the
810 // current code will just abort below in SplitDisjointBoxes() anyway.
811 {
812 rectangles_.clear();
813 rectangles_.reserve(boxes.size());
814 for (const auto [box, y_mandatory_start, y_mandatory_end] : boxes) {
815 // Note that we invert the x/y position here in order to be already
816 // sorted for FindOneIntersectionIfPresent()
817 rectangles_.push_back(
818 {/*x_min=*/y_mandatory_start, /*x_max=*/y_mandatory_end,
819 /*y_min=*/x->StartMin(box), /*y_max=*/x->EndMin(box)});
820 }
821 const auto opt_pair = FindOneIntersectionIfPresent(rectangles_);
822 {
823 const size_t n = rectangles_.size();
824 time_limit_->AdvanceDeterministicTime(
825 static_cast<double>(n) * static_cast<double>(absl::bit_width(n)) *
826 1e-8);
827 }
828 if (opt_pair == std::nullopt) {
829 return true;
830 } else {
831 // TODO(user): Test if we have a conflict here.
832 }
833 }
834
835 order_.assign(x->NumTasks(), 0);
836 {
837 int i = 0;
838 for (const auto [t, _lit, _time] : x->TaskByIncreasingShiftedStartMin()) {
839 order_[t] = i++;
840 }
841 }
842 ConstructOverlappingSets(boxes, &events_overlapping_boxes_, order_);
843
844 // Split lists of boxes into disjoint set of boxes (w.r.t. overlap).
845 boxes_to_propagate_.clear();
846 reduced_overlapping_boxes_.clear();
847 int work_done = boxes.size();
848 for (int i = 0; i < events_overlapping_boxes_.size(); ++i) {
849 work_done += events_overlapping_boxes_[i].size();
850 SplitDisjointBoxes(*x, events_overlapping_boxes_[i], &disjoint_boxes_);
851 for (const absl::Span<const int> sub_boxes : disjoint_boxes_) {
852 // Boxes are sorted in a stable manner in the Split method.
853 // Note that we do not use reduced_overlapping_boxes_ directly so that
854 // the order of iteration is deterministic.
855 const auto& insertion = reduced_overlapping_boxes_.insert(sub_boxes);
856 if (insertion.second) boxes_to_propagate_.push_back(sub_boxes);
857 }
858 }
859
860 // TODO(user): This is a poor dtime, but we want it not to be zero here.
861 time_limit_->AdvanceDeterministicTime(static_cast<double>(work_done) * 1e-8);
862
863 // And finally propagate.
864 //
865 // TODO(user): Sorting of boxes seems influential on the performance.
866 // Test.
867 for (const absl::Span<const int> boxes : boxes_to_propagate_) {
868 // The case of two boxes should be taken care of during "fast"
869 // propagation, so we can skip it here.
870 if (!fast_propagation && boxes.size() <= 2) continue;
871
872 x_.ClearOtherHelper();
873 if (!x_.ResetFromSubset(*x, boxes)) return false;
874
875 // Collect the common overlapping coordinates of all boxes.
876 IntegerValue lb(std::numeric_limits<int64_t>::min());
877 IntegerValue ub(std::numeric_limits<int64_t>::max());
878 for (const int b : boxes) {
879 lb = std::max(lb, y->StartMax(b));
880 ub = std::min(ub, y->EndMin(b) - 1);
881 }
882 CHECK_LE(lb, ub);
883
884 // We want for different propagation to reuse as much as possible the same
885 // line. The idea behind this is to compute the 'canonical' line to use
886 // when explaining that boxes overlap on the 'y_dim' dimension. We compute
887 // the multiple of the biggest power of two that is common to all boxes.
888 //
889 // TODO(user): We should scan the integer trail to find the oldest
890 // non-empty common interval. Then we can pick the canonical value within
891 // it.
892 const IntegerValue line_to_use_for_reason = FindCanonicalValue(lb, ub);
893
894 // Setup x_dim for propagation.
895 x_.SetOtherHelper(y, boxes, line_to_use_for_reason);
896
897 if (fast_propagation) {
898 if (x_.NumTasks() == 2) {
899 // In that case, we can use simpler algorithms.
900 // Note that this case happens frequently (~30% of all calls to this
901 // method according to our tests).
902 RETURN_IF_FALSE(disjunctive_with_two_items_.Propagate());
903 } else {
904 RETURN_IF_FALSE(overload_checker_.Propagate());
905 RETURN_IF_FALSE(forward_detectable_precedences_.Propagate());
906 RETURN_IF_FALSE(backward_detectable_precedences_.Propagate());
907 }
908 } else {
909 DCHECK_GT(x_.NumTasks(), 2);
910 RETURN_IF_FALSE(forward_not_last_.Propagate());
911 RETURN_IF_FALSE(backward_not_last_.Propagate());
912 RETURN_IF_FALSE(backward_edge_finding_.Propagate());
913 RETURN_IF_FALSE(forward_edge_finding_.Propagate());
914 }
915 }
916
917 return true;
918}
919
920// Note that we optimized this function for two main use cases:
921// - smallish problem where we don't have more than 100 boxes.
922// - large problem with many 1000s boxes, but with only a small subset that is
923// not fixed (mainly coming from LNS).
925 if (!helper_->SynchronizeAndSetDirection(true, true, false)) return false;
926
927 // If we are "diving" we maintain the set of fixed boxes for which we know
928 // that they are not overlapping.
929 const bool backtrack_since_last_call = !rev_is_in_dive_;
930 watcher_->SetUntilNextBacktrack(&rev_is_in_dive_);
931 if (backtrack_since_last_call ||
932 last_helper_inprocessing_count_ != helper_->InProcessingCount()) {
933 last_helper_inprocessing_count_ = helper_->InProcessingCount();
934 const int num_tasks = helper_->NumBoxes();
935 already_checked_fixed_boxes_.ClearAndResize(num_tasks);
936 }
937
938 // Note that the code assumes that this was registered twice in fast and slow
939 // mode. So we will not redo some propagation in slow mode that was already
940 // done by the fast mode.
941 const bool fast_propagation = watcher_->GetCurrentId() == fast_id_;
942 for (const auto subset : helper_->connected_components().AsVectorOfSpan()) {
943 if (!FindBoxesThatMustOverlapAHorizontalLineAndPropagate(fast_propagation,
944 subset)) {
945 return false;
946 }
947 }
948 // We can actually swap dimensions to propagate vertically.
949 if (!helper_->SynchronizeAndSetDirection(true, true, true)) return false;
950
951 for (const auto subset : helper_->connected_components().AsVectorOfSpan()) {
952 if (!FindBoxesThatMustOverlapAHorizontalLineAndPropagate(fast_propagation,
953 subset)) {
954 return false;
955 }
956 }
957
958 return true;
959}
960
962 const int id = watcher->Register(this);
963 helper_->WatchAllBoxes(id);
965 return id;
966}
967
969 if (!VLOG_IS_ON(1)) return;
970 std::vector<std::pair<std::string, int64_t>> stats;
971 stats.push_back({"RectanglePairwisePropagator/called", num_calls_});
972 stats.push_back({"RectanglePairwisePropagator/pairwise_conflicts",
973 num_pairwise_conflicts_});
974 stats.push_back({"RectanglePairwisePropagator/pairwise_propagations",
975 num_pairwise_propagations_});
976
977 shared_stats_->AddStats(stats);
978}
979
981 if (!helper_->SynchronizeAndSetDirection()) return false;
982
983 num_calls_++;
984 std::vector<PairwiseRestriction> restrictions;
985
986 for (int component_index = 0;
987 component_index < helper_->connected_components().size();
988 ++component_index) {
989 horizontal_zero_area_boxes_.clear();
990 vertical_zero_area_boxes_.clear();
991 point_zero_area_boxes_.clear();
992 fixed_non_zero_area_boxes_.clear();
993 non_fixed_non_zero_area_boxes_.clear();
994 for (int b : helper_->connected_components()[component_index]) {
995 if (!helper_->IsPresent(b)) continue;
996 const auto [x_size_max, y_size_max] = helper_->GetBoxSizesMax(b);
997 ItemWithVariableSize box = helper_->GetItemWithVariableSize(b);
998 if (x_size_max == 0) {
999 if (y_size_max == 0) {
1000 point_zero_area_boxes_.push_back(std::move(box));
1001 } else {
1002 vertical_zero_area_boxes_.push_back(std::move(box));
1003 }
1004 } else if (y_size_max == 0) {
1005 horizontal_zero_area_boxes_.push_back(std::move(box));
1006 } else {
1007 // TODO(user): if the number of fixed boxes is large, we keep
1008 // reconstructing them each time this is called for no reason.
1009 if (box.IsFixed()) {
1010 fixed_non_zero_area_boxes_.push_back(std::move(box));
1011 } else {
1012 non_fixed_non_zero_area_boxes_.push_back(std::move(box));
1013 }
1014 }
1015 }
1016
1017 // We ignore pairs of two fixed boxes. The only thing to propagate between
1018 // two fixed boxes is a conflict and it should already have been taken care
1019 // of by the MandatoryOverlapPropagator propagator.
1020 RETURN_IF_FALSE(FindRestrictionsAndPropagateConflict(
1021 non_fixed_non_zero_area_boxes_, fixed_non_zero_area_boxes_,
1022 &restrictions));
1023
1024 // Check zero area boxes against non-zero area boxes.
1025 for (auto& non_zero_area_boxes :
1026 {fixed_non_zero_area_boxes_, non_fixed_non_zero_area_boxes_}) {
1027 RETURN_IF_FALSE(FindRestrictionsAndPropagateConflict(
1028 non_zero_area_boxes, horizontal_zero_area_boxes_, &restrictions));
1029 RETURN_IF_FALSE(FindRestrictionsAndPropagateConflict(
1030 non_zero_area_boxes, vertical_zero_area_boxes_, &restrictions));
1031 RETURN_IF_FALSE(FindRestrictionsAndPropagateConflict(
1032 non_zero_area_boxes, point_zero_area_boxes_, &restrictions));
1033 }
1034
1035 // Check vertical zero area boxes against horizontal zero area boxes.
1036 RETURN_IF_FALSE(FindRestrictionsAndPropagateConflict(
1037 vertical_zero_area_boxes_, horizontal_zero_area_boxes_, &restrictions));
1038 }
1039 for (const PairwiseRestriction& restriction : restrictions) {
1040 RETURN_IF_FALSE(PropagateTwoBoxes(restriction));
1041 }
1042 return true;
1043}
1044
1045bool RectanglePairwisePropagator::FindRestrictionsAndPropagateConflict(
1046 absl::Span<const ItemWithVariableSize> items1,
1047 absl::Span<const ItemWithVariableSize> items2,
1048 std::vector<PairwiseRestriction>* restrictions) {
1049 const int max_pairs =
1051 if (items1.size() * items2.size() > max_pairs) {
1052 return true;
1053 }
1054 AppendPairwiseRestrictions(items1, items2, restrictions);
1055 for (const PairwiseRestriction& restriction : *restrictions) {
1056 if (restriction.type ==
1058 RETURN_IF_FALSE(PropagateTwoBoxes(restriction));
1059 }
1060 }
1061 return true;
1062}
1063
1064bool RectanglePairwisePropagator::PropagateTwoBoxes(
1065 const PairwiseRestriction& restriction) {
1066 if (restriction.type ==
1068 num_pairwise_conflicts_++;
1069 } else {
1070 num_pairwise_propagations_++;
1071 }
1072 return helper_->PropagateRelativePosition(
1073 restriction.first_index, restriction.second_index, restriction.type);
1074}
1075
1076#undef RETURN_IF_FALSE
1077} // namespace sat
1078} // namespace operations_research
void SetPropagatorPriority(int id, int priority)
Definition integer.cc:2352
int Register(PropagatorInterface *propagator)
Registers a propagator and returns its unique ids.
Definition integer.cc:2326
IntegerVariable AddIntegerVariable(IntegerValue lower_bound, IntegerValue upper_bound)
Definition integer.cc:830
AffineExpression Start(IntervalVariable i) const
Definition intervals.h:92
AffineExpression Size(IntervalVariable i) const
Definition intervals.h:91
AffineExpression End(IntervalVariable i) const
Definition intervals.h:93
Literal GetOrCreatePrecedenceLiteral(AffineExpression x, AffineExpression y)
Definition intervals.cc:242
bool IsAbsent(IntervalVariable i) const
Definition intervals.h:79
Literal PresenceLiteral(IntervalVariable i) const
Definition intervals.h:72
bool IsOptional(IntervalVariable i) const
Returns whether or not a interval is optional and the associated literal.
Definition intervals.h:69
SchedulingConstraintHelper * GetOrCreateHelper(const std::vector< IntervalVariable > &variables, bool register_as_disjunctive_helper=false)
Definition intervals.cc:258
NoOverlap2DConstraintHelper * GetOrCreate2DHelper(const std::vector< IntervalVariable > &x_variables, const std::vector< IntervalVariable > &y_variables)
Definition intervals.cc:297
void RegisterWith(GenericLiteralWatcher *watcher)
void Register(int fast_priority, int slow_priority)
Definition diffn.cc:687
Propagates using a box energy reasoning.
Definition diffn.h:41
Propagator that compares the boxes pairwise.
Definition diffn.h:152
int RegisterWith(GenericLiteralWatcher *watcher)
Definition diffn.cc:961
::int32_t max_pairs_pairwise_reasoning_in_no_overlap_2d() const
void STLSortAndRemoveDuplicates(T *v, const LessFunc &less_func)
Definition stl_util.h:55
constexpr IntegerValue kMaxIntegerValue(std::numeric_limits< IntegerValue::ValueType >::max() - 1)
std::function< void(Model *)> WeightedSumGreaterOrEqual(absl::Span< const IntegerVariable > vars, const VectorInt &coefficients, int64_t lower_bound)
Weighted sum >= constant.
void CreateAndRegisterTryEdgePropagator(NoOverlap2DConstraintHelper *helper, Model *model, GenericLiteralWatcher *watcher, int priority)
std::vector< IntegerVariable > NegationOf(absl::Span< const IntegerVariable > vars)
Returns the vector of the negated variables.
Definition integer.cc:52
std::optional< std::pair< int, int > > FindOneIntersectionIfPresent(absl::Span< const Rectangle > rectangles)
constexpr IntegerValue kMinIntegerValue(-kMaxIntegerValue.value())
void AppendPairwiseRestrictions(absl::Span< const ItemWithVariableSize > items, std::vector< PairwiseRestriction > *result)
void ConstructOverlappingSets(absl::Span< IndexedInterval > intervals, CompactVectorVector< int > *result, absl::Span< const int > order)
void CreateAndRegisterMandatoryOverlapPropagator(NoOverlap2DConstraintHelper *helper, Model *model, GenericLiteralWatcher *watcher, int priority)
FindRectanglesResult FindRectanglesWithEnergyConflictMC(const std::vector< RectangleInRange > &intervals, absl::BitGenRef random, double temperature, double candidate_energy_usage_factor)
std::function< IntegerVariable(Model *)> NewIntegerVariable(int64_t lb, int64_t ub)
Definition integer.h:1533
void AddNonOverlappingRectangles(const std::vector< IntervalVariable > &x, const std::vector< IntervalVariable > &y, Model *model)
Definition diffn.cc:180
IntegerValue CapSubI(IntegerValue a, IntegerValue b)
bool AtMinOrMaxInt64I(IntegerValue t)
void AddCumulativeOverloadChecker(AffineExpression capacity, SchedulingConstraintHelper *helper, SchedulingDemandHelper *demands, Model *model)
IntegerValue CapProdI(IntegerValue a, IntegerValue b)
Overflows and saturated arithmetic.
In SWIG mode, we don't want anything besides these top-level includes.
int64_t CapSub(int64_t x, int64_t y)
#define RETURN_IF_FALSE(f)
Definition diffn.cc:333
void GrowToInclude(const Rectangle &other)
Definition diffn_util.h:50
bool IsDisjoint(const Rectangle &other) const
Definition diffn_util.cc:59