Google OR-Tools v9.11
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-2024 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/types/span.h"
37#include "ortools/sat/integer.h"
41#include "ortools/sat/model.h"
43#include "ortools/sat/sat_parameters.pb.h"
47
48namespace operations_research {
49namespace sat {
50
51namespace {
52
53IntegerVariable CreateVariableWithTightDomain(
54 absl::Span<const AffineExpression> exprs, Model* model) {
55 IntegerValue min = kMaxIntegerValue;
56 IntegerValue max = kMinIntegerValue;
57 auto* integer_trail = model->GetOrCreate<IntegerTrail>();
58 for (const AffineExpression& e : exprs) {
59 min = std::min(min, integer_trail->LevelZeroLowerBound(e));
60 max = std::max(max, integer_trail->LevelZeroUpperBound(e));
61 }
62 return integer_trail->AddIntegerVariable(min, max);
63}
64
65// TODO(user): Use the faster variable only version if all expressions reduce
66// to a single variable?
67IntegerVariable CreateVariableEqualToMinOf(
68 absl::Span<const AffineExpression> exprs, Model* model) {
69 std::vector<LinearExpression> converted;
70 for (const AffineExpression& affine : exprs) {
71 LinearExpression e;
72 e.offset = affine.constant;
73 if (affine.var != kNoIntegerVariable) {
74 e.vars.push_back(affine.var);
75 e.coeffs.push_back(affine.coeff);
76 }
77 converted.push_back(e);
78 }
79
80 LinearExpression target;
81 const IntegerVariable var = CreateVariableWithTightDomain(exprs, model);
82 target.vars.push_back(var);
83 target.coeffs.push_back(IntegerValue(1));
84 model->Add(IsEqualToMinOf(target, converted));
85 return var;
86}
87
88IntegerVariable CreateVariableEqualToMaxOf(
89 absl::Span<const AffineExpression> exprs, Model* model) {
90 std::vector<LinearExpression> converted;
91 for (const AffineExpression& affine : exprs) {
92 LinearExpression e;
93 e.offset = affine.constant;
94 if (affine.var != kNoIntegerVariable) {
95 e.vars.push_back(affine.var);
96 e.coeffs.push_back(affine.coeff);
97 }
98 converted.push_back(NegationOf(e));
99 }
100
101 LinearExpression target;
102 const IntegerVariable var = CreateVariableWithTightDomain(exprs, model);
103 target.vars.push_back(NegationOf(var));
104 target.coeffs.push_back(IntegerValue(1));
105 model->Add(IsEqualToMinOf(target, converted));
106 return var;
107}
108
109// Add a cumulative relaxation. That is, on one dimension, it does not enforce
110// the rectangle aspect, allowing vertical slices to move freely.
111void AddDiffnCumulativeRelationOnX(SchedulingConstraintHelper* x,
112 SchedulingConstraintHelper* y,
113 Model* model) {
114 // TODO(user): Use conditional affine min/max !!
115 const IntegerVariable min_start_var =
116 CreateVariableEqualToMinOf(y->Starts(), model);
117 const IntegerVariable max_end_var =
118 CreateVariableEqualToMaxOf(y->Ends(), model);
119
120 // (max_end - min_start) >= capacity.
121 auto* integer_trail = model->GetOrCreate<IntegerTrail>();
122 const AffineExpression capacity(model->Add(NewIntegerVariable(
123 0, CapSub(integer_trail->UpperBound(max_end_var).value(),
124 integer_trail->LowerBound(min_start_var).value()))));
125 const std::vector<int64_t> coeffs = {-capacity.coeff.value(), -1, 1};
126 model->Add(
127 WeightedSumGreaterOrEqual({capacity.var, min_start_var, max_end_var},
128 coeffs, capacity.constant.value()));
129
130 SchedulingDemandHelper* demands =
131 model->GetOrCreate<IntervalsRepository>()->GetOrCreateDemandHelper(
132 x, y->Sizes());
133
134 // Propagator responsible for applying Timetabling filtering rule. It
135 // increases the minimum of the start variables, decrease the maximum of the
136 // end variables, and increase the minimum of the capacity variable.
137 const SatParameters& params = *model->GetOrCreate<SatParameters>();
138 if (params.use_timetabling_in_no_overlap_2d()) {
139 TimeTablingPerTask* time_tabling =
140 new TimeTablingPerTask(capacity, x, demands, model);
141 time_tabling->RegisterWith(model->GetOrCreate<GenericLiteralWatcher>());
142 model->TakeOwnership(time_tabling);
143 }
144
145 // Propagator responsible for applying the Overload Checking filtering rule.
146 // It increases the minimum of the capacity variable.
147 if (params.use_energetic_reasoning_in_no_overlap_2d()) {
148 AddCumulativeOverloadChecker(capacity, x, demands, model);
149 }
150}
151
152// This function will fill the helper why the two boxes always overlap on that
153// dimension.
154void ClearAndAddMandatoryOverlapReason(int box1, int box2,
155 SchedulingConstraintHelper* helper) {
156 helper->ClearReason();
157 helper->AddPresenceReason(box1);
158 helper->AddPresenceReason(box2);
159 helper->AddReasonForBeingBefore(box1, box2);
160 helper->AddReasonForBeingBefore(box2, box1);
161}
162
163bool ClearAndAddTwoBoxesConflictReason(int box1, int box2,
164 SchedulingConstraintHelper* x,
165 SchedulingConstraintHelper* y) {
166 ClearAndAddMandatoryOverlapReason(box1, box2, x);
167 ClearAndAddMandatoryOverlapReason(box1, box2, y);
168 x->ImportOtherReasons(*y);
169 return x->ReportConflict();
170}
171
172} // namespace
173
174void AddNonOverlappingRectangles(const std::vector<IntervalVariable>& x,
175 const std::vector<IntervalVariable>& y,
176 Model* model) {
177 IntervalsRepository* repository = model->GetOrCreate<IntervalsRepository>();
178 SchedulingConstraintHelper* x_helper = repository->GetOrCreateHelper(x);
179 SchedulingConstraintHelper* y_helper = repository->GetOrCreateHelper(y);
180
183 model);
184 constraint->Register(/*fast_priority=*/3, /*slow_priority=*/4);
185 model->TakeOwnership(constraint);
186
187 RectanglePairwisePropagator* pairwise_propagator =
188 new RectanglePairwisePropagator(x_helper, y_helper, model);
189 GenericLiteralWatcher* const watcher =
190 model->GetOrCreate<GenericLiteralWatcher>();
191 watcher->SetPropagatorPriority(pairwise_propagator->RegisterWith(watcher), 4);
192 model->TakeOwnership(pairwise_propagator);
193
194 const SatParameters& params = *model->GetOrCreate<SatParameters>();
195 const bool add_cumulative_relaxation =
196 params.use_timetabling_in_no_overlap_2d() ||
197 params.use_energetic_reasoning_in_no_overlap_2d();
198
199 if (add_cumulative_relaxation) {
200 // We must first check if the cumulative relaxation is possible.
201 bool some_boxes_are_only_optional_on_x = false;
202 bool some_boxes_are_only_optional_on_y = false;
203 for (int i = 0; i < x.size(); ++i) {
204 if (x_helper->IsOptional(i) && y_helper->IsOptional(i) &&
205 x_helper->PresenceLiteral(i) != y_helper->PresenceLiteral(i)) {
206 // Abort as the task would be conditioned by two literals.
207 return;
208 }
209 if (x_helper->IsOptional(i) && !y_helper->IsOptional(i)) {
210 // We cannot use x_size as the demand of the cumulative based on
211 // the y_intervals.
212 some_boxes_are_only_optional_on_x = true;
213 }
214 if (y_helper->IsOptional(i) && !x_helper->IsOptional(i)) {
215 // We cannot use y_size as the demand of the cumulative based on
216 // the y_intervals.
217 some_boxes_are_only_optional_on_y = true;
218 }
219 }
220 if (!some_boxes_are_only_optional_on_y) {
221 AddDiffnCumulativeRelationOnX(x_helper, y_helper, model);
222 }
223 if (!some_boxes_are_only_optional_on_x) {
224 AddDiffnCumulativeRelationOnX(y_helper, x_helper, model);
225 }
226 }
227
228 if (params.use_area_energetic_reasoning_in_no_overlap_2d()) {
230 new NonOverlappingRectanglesEnergyPropagator(x_helper, y_helper, model);
231 GenericLiteralWatcher* const watcher =
232 model->GetOrCreate<GenericLiteralWatcher>();
233 watcher->SetPropagatorPriority(energy_constraint->RegisterWith(watcher), 5);
234 model->TakeOwnership(energy_constraint);
235 }
236}
237
238#define RETURN_IF_FALSE(f) \
239 if (!(f)) return false;
240
243 if (!VLOG_IS_ON(1)) return;
244 std::vector<std::pair<std::string, int64_t>> stats;
245 stats.push_back(
246 {"NonOverlappingRectanglesEnergyPropagator/called", num_calls_});
247 stats.push_back(
248 {"NonOverlappingRectanglesEnergyPropagator/conflicts", num_conflicts_});
249 stats.push_back(
250 {"NonOverlappingRectanglesEnergyPropagator/conflicts_two_boxes",
251 num_conflicts_two_boxes_});
252 stats.push_back({"NonOverlappingRectanglesEnergyPropagator/refined",
253 num_refined_conflicts_});
254 stats.push_back(
255 {"NonOverlappingRectanglesEnergyPropagator/conflicts_with_slack",
256 num_conflicts_with_slack_});
257
258 shared_stats_->AddStats(stats);
259}
260
262 // TODO(user): double-check/revisit the algo for box of variable sizes.
263 const int num_boxes = x_.NumTasks();
264 if (!x_.SynchronizeAndSetTimeDirection(true)) return false;
265 if (!y_.SynchronizeAndSetTimeDirection(true)) return false;
266
267 Rectangle bounding_box = {.x_min = std::numeric_limits<IntegerValue>::max(),
268 .x_max = std::numeric_limits<IntegerValue>::min(),
269 .y_min = std::numeric_limits<IntegerValue>::max(),
270 .y_max = std::numeric_limits<IntegerValue>::min()};
271 std::vector<RectangleInRange> active_box_ranges;
272 active_box_ranges.reserve(num_boxes);
273 for (int box = 0; box < num_boxes; ++box) {
274 if (x_.SizeMin(box) == 0 || y_.SizeMin(box) == 0) continue;
275 if (!x_.IsPresent(box) || !y_.IsPresent(box)) continue;
276
277 bounding_box.x_min = std::min(bounding_box.x_min, x_.StartMin(box));
278 bounding_box.x_max = std::max(bounding_box.x_max, x_.EndMax(box));
279 bounding_box.y_min = std::min(bounding_box.y_min, y_.StartMin(box));
280 bounding_box.y_max = std::max(bounding_box.y_max, y_.EndMax(box));
281
282 active_box_ranges.push_back(RectangleInRange{
283 .box_index = box,
284 .bounding_area = {.x_min = x_.StartMin(box),
285 .x_max = x_.StartMax(box) + x_.SizeMin(box),
286 .y_min = y_.StartMin(box),
287 .y_max = y_.StartMax(box) + y_.SizeMin(box)},
288 .x_size = x_.SizeMin(box),
289 .y_size = y_.SizeMin(box)});
290 }
291
292 if (active_box_ranges.size() < 2) {
293 return true;
294 }
295
296 // Our algo is quadratic, so we don't want to run it on really large problems.
297 if (active_box_ranges.size() > 1000) {
298 return true;
299 }
300
301 if (std::max(bounding_box.SizeX(), bounding_box.SizeY()) *
302 active_box_ranges.size() >
303 std::numeric_limits<int32_t>::max()) {
304 // Avoid integer overflows if the area of the boxes get comparable with
305 // INT64_MAX
306 return true;
307 }
308
309 num_calls_++;
310
311 std::optional<Conflict> best_conflict =
312 FindConflict(std::move(active_box_ranges));
313 if (!best_conflict.has_value()) {
314 return true;
315 }
316 num_conflicts_++;
317
318 // We found a conflict, so we can afford to run the propagator again to
319 // search for a best explanation. This is specially the case since we only
320 // want to re-run it over the items that participate in the conflict, so it is
321 // a much smaller problem.
322 IntegerValue best_explanation_size =
323 best_conflict->opp_result.GetItemsParticipatingOnConflict().size();
324 bool refined = false;
325 while (true) {
326 std::vector<RectangleInRange> items_participating_in_conflict;
327 items_participating_in_conflict.reserve(
328 best_conflict->items_for_opp.size());
329 for (const auto& item :
330 best_conflict->opp_result.GetItemsParticipatingOnConflict()) {
331 items_participating_in_conflict.push_back(
332 best_conflict->items_for_opp[item.index]);
333 }
334 std::optional<Conflict> conflict =
335 FindConflict(items_participating_in_conflict);
336 if (!conflict.has_value()) break;
337 // We prefer an explanation with the least number of boxes.
338 if (conflict->opp_result.GetItemsParticipatingOnConflict().size() >=
339 best_explanation_size) {
340 // The new explanation isn't better than the old one. Stop trying.
341 break;
342 }
343 best_explanation_size =
344 conflict->opp_result.GetItemsParticipatingOnConflict().size();
345 best_conflict = std::move(conflict);
346 refined = true;
347 }
348
349 num_refined_conflicts_ += refined;
350 const std::vector<RectangleInRange> generalized_explanation =
351 GeneralizeExplanation(*best_conflict);
352 if (best_explanation_size == 2) {
353 num_conflicts_two_boxes_++;
354 }
355 BuildAndReportEnergyTooLarge(generalized_explanation);
356 return false;
357}
358
359std::optional<NonOverlappingRectanglesEnergyPropagator::Conflict>
360NonOverlappingRectanglesEnergyPropagator::FindConflict(
361 std::vector<RectangleInRange> active_box_ranges) {
362 const auto rectangles_with_too_much_energy =
363 FindRectanglesWithEnergyConflictMC(active_box_ranges, *random_, 1.0, 0.8);
364
365 if (rectangles_with_too_much_energy.conflicts.empty() &&
366 rectangles_with_too_much_energy.candidates.empty()) {
367 return std::nullopt;
368 }
369
370 Conflict best_conflict;
371
372 // Sample 10 rectangles (at least five among the ones for which we already
373 // detected an energy overflow), extract an orthogonal packing subproblem for
374 // each and look for conflict. Sampling avoids making this heuristic too
375 // costly.
376 constexpr int kSampleSize = 10;
377 absl::InlinedVector<Rectangle, kSampleSize> sampled_rectangles;
378 std::sample(rectangles_with_too_much_energy.conflicts.begin(),
379 rectangles_with_too_much_energy.conflicts.end(),
380 std::back_inserter(sampled_rectangles), 5, *random_);
381 std::sample(rectangles_with_too_much_energy.candidates.begin(),
382 rectangles_with_too_much_energy.candidates.end(),
383 std::back_inserter(sampled_rectangles),
384 kSampleSize - sampled_rectangles.size(), *random_);
385 std::sort(sampled_rectangles.begin(), sampled_rectangles.end(),
386 [](const Rectangle& a, const Rectangle& b) {
387 const bool larger = std::make_pair(a.SizeX(), a.SizeY()) >
388 std::make_pair(b.SizeX(), b.SizeY());
389 // Double-check the invariant from
390 // FindRectanglesWithEnergyConflictMC() that given two returned
391 // rectangles there is one that is fully inside the other.
392 if (larger) {
393 // Rectangle b is fully contained inside a
394 DCHECK(a.x_min <= b.x_min && a.x_max >= b.x_max &&
395 a.y_min <= b.y_min && a.y_max >= b.y_max);
396 } else {
397 // Rectangle a is fully contained inside b
398 DCHECK(a.x_min >= b.x_min && a.x_max <= b.x_max &&
399 a.y_min >= b.y_min && a.y_max <= b.y_max);
400 }
401 return larger;
402 });
403 std::vector<IntegerValue> sizes_x, sizes_y;
404 sizes_x.reserve(active_box_ranges.size());
405 sizes_y.reserve(active_box_ranges.size());
406 std::vector<RectangleInRange> filtered_items;
407 filtered_items.reserve(active_box_ranges.size());
408 for (const Rectangle& r : sampled_rectangles) {
409 sizes_x.clear();
410 sizes_y.clear();
411 filtered_items.clear();
412 for (int i = 0; i < active_box_ranges.size(); ++i) {
413 const RectangleInRange& box = active_box_ranges[i];
414 const Rectangle intersection = box.GetMinimumIntersection(r);
415 if (intersection.SizeX() > 0 && intersection.SizeY() > 0) {
416 sizes_x.push_back(intersection.SizeX());
417 sizes_y.push_back(intersection.SizeY());
418 filtered_items.push_back(box);
419 }
420 }
421 // This check the feasibility of a related orthogonal packing problem where
422 // our rectangle is the bounding box, and we need to fit inside it a set of
423 // items corresponding to the minimum intersection of the original items
424 // with this bounding box.
425 const auto opp_result = orthogonal_packing_checker_.TestFeasibility(
426 sizes_x, sizes_y, {r.SizeX(), r.SizeY()},
427 OrthogonalPackingOptions{
428 .use_pairwise = true,
429 .use_dff_f0 = true,
430 .use_dff_f2 = true,
431 .brute_force_threshold = 7,
432 .dff2_max_number_of_parameters_to_check = 100});
433 if (opp_result.GetResult() == OrthogonalPackingResult::Status::INFEASIBLE &&
434 (best_conflict.opp_result.GetResult() !=
436 best_conflict.opp_result.GetItemsParticipatingOnConflict().size() >
437 opp_result.GetItemsParticipatingOnConflict().size())) {
438 best_conflict.items_for_opp = filtered_items;
439 best_conflict.opp_result = opp_result;
440 best_conflict.rectangle_with_too_much_energy = r;
441 }
442 // Use the fact that our rectangles are ordered in shrinking order to remove
443 // all items that will never contribute again.
444 filtered_items.swap(active_box_ranges);
445 }
446 if (best_conflict.opp_result.GetResult() ==
448 return best_conflict;
449 } else {
450 return std::nullopt;
451 }
452}
453
454std::vector<RectangleInRange>
455NonOverlappingRectanglesEnergyPropagator::GeneralizeExplanation(
456 const Conflict& conflict) {
457 const Rectangle& rectangle = conflict.rectangle_with_too_much_energy;
458 OrthogonalPackingResult relaxed_result = conflict.opp_result;
459
460 // Use the potential slack to have a stronger reason.
461 int used_slack_count = 0;
462 const auto& items = relaxed_result.GetItemsParticipatingOnConflict();
463 for (int i = 0; i < items.size(); ++i) {
464 if (!relaxed_result.HasSlack()) {
465 break;
466 }
467 const RectangleInRange& range = conflict.items_for_opp[items[i].index];
468 const RectangleInRange item_in_zero_level_range = {
469 .bounding_area = {.x_min = x_.LevelZeroStartMin(range.box_index),
470 .x_max = x_.LevelZeroStartMax(range.box_index) +
471 range.x_size,
472 .y_min = y_.LevelZeroStartMin(range.box_index),
473 .y_max = y_.LevelZeroStartMax(range.box_index) +
474 range.y_size},
475 .x_size = range.x_size,
476 .y_size = range.y_size};
477 // There is no point trying to intersect less the item with the rectangle
478 // than it would on zero-level. So do not waste the slack with it.
479 const Rectangle max_overlap =
480 item_in_zero_level_range.GetMinimumIntersection(rectangle);
481 used_slack_count += relaxed_result.TryUseSlackToReduceItemSize(
482 i, OrthogonalPackingResult::Coord::kCoordX, max_overlap.SizeX());
483 used_slack_count += relaxed_result.TryUseSlackToReduceItemSize(
484 i, OrthogonalPackingResult::Coord::kCoordY, max_overlap.SizeY());
485 }
486 num_conflicts_with_slack_ += (used_slack_count > 0);
487 VLOG_EVERY_N_SEC(2, 3)
488 << "Found a conflict on the OPP sub-problem of rectangle: " << rectangle
489 << " using "
490 << conflict.opp_result.GetItemsParticipatingOnConflict().size() << "/"
491 << conflict.items_for_opp.size() << " items.";
492
493 std::vector<RectangleInRange> ranges_for_explanation;
494 ranges_for_explanation.reserve(conflict.items_for_opp.size());
495 std::vector<OrthogonalPackingResult::Item> sorted_items =
496 relaxed_result.GetItemsParticipatingOnConflict();
497 // TODO(user) understand why sorting high-impact items first improves the
498 // benchmarks
499 std::sort(sorted_items.begin(), sorted_items.end(),
500 [](const OrthogonalPackingResult::Item& a,
501 const OrthogonalPackingResult::Item& b) {
502 return a.size_x * a.size_y > b.size_x * b.size_y;
503 });
504 for (const auto& item : sorted_items) {
505 const RectangleInRange& range = conflict.items_for_opp[item.index];
506 ranges_for_explanation.push_back(
507 RectangleInRange::BiggestWithMinIntersection(rectangle, range,
508 item.size_x, item.size_y));
509 }
510 return ranges_for_explanation;
511}
512
513int NonOverlappingRectanglesEnergyPropagator::RegisterWith(
514 GenericLiteralWatcher* watcher) {
515 const int id = watcher->Register(this);
516 x_.WatchAllTasks(id);
517 y_.WatchAllTasks(id);
518 return id;
519}
520
521bool NonOverlappingRectanglesEnergyPropagator::BuildAndReportEnergyTooLarge(
522 const std::vector<RectangleInRange>& ranges) {
523 if (ranges.size() == 2) {
524 num_conflicts_two_boxes_++;
525 return ClearAndAddTwoBoxesConflictReason(ranges[0].box_index,
526 ranges[1].box_index, &x_, &y_);
527 }
528 x_.ClearReason();
529 y_.ClearReason();
530 for (const auto& range : ranges) {
531 const int b = range.box_index;
532
533 x_.AddStartMinReason(b, range.bounding_area.x_min);
534 y_.AddStartMinReason(b, range.bounding_area.y_min);
535
536 x_.AddStartMaxReason(b, range.bounding_area.x_max - range.x_size);
537 y_.AddStartMaxReason(b, range.bounding_area.y_max - range.y_size);
538
539 x_.AddSizeMinReason(b);
540 y_.AddSizeMinReason(b);
541
542 x_.AddPresenceReason(b);
543 y_.AddPresenceReason(b);
544 }
545 x_.ImportOtherReasons(y_);
546 return x_.ReportConflict();
547}
548
549namespace {
550
551// We want for different propagation to reuse as much as possible the same
552// line. The idea behind this is to compute the 'canonical' line to use
553// when explaining that boxes overlap on the 'y_dim' dimension. We compute
554// the multiple of the biggest power of two that is common to all boxes.
555IntegerValue FindCanonicalValue(IntegerValue lb, IntegerValue ub) {
556 if (lb == ub) return lb;
557 if (lb <= 0 && ub > 0) return IntegerValue(0);
558 if (lb < 0 && ub <= 0) {
559 return -FindCanonicalValue(-ub, -lb);
560 }
561
562 int64_t mask = 0;
563 IntegerValue candidate = ub;
564 for (int o = 0; o < 62; ++o) {
565 mask = 2 * mask + 1;
566 const IntegerValue masked_ub(ub.value() & ~mask);
567 if (masked_ub >= lb) {
568 candidate = masked_ub;
569 } else {
570 break;
571 }
572 }
573 return candidate;
574}
575
576void SplitDisjointBoxes(const SchedulingConstraintHelper& x,
577 absl::Span<int> boxes,
578 std::vector<absl::Span<int>>* result) {
579 result->clear();
580 std::sort(boxes.begin(), boxes.end(), [&x](int a, int b) {
581 return x.ShiftedStartMin(a) < x.ShiftedStartMin(b);
582 });
583 int current_start = 0;
584 std::size_t current_length = 1;
585 IntegerValue current_max_end = x.EndMax(boxes[0]);
586
587 for (int b = 1; b < boxes.size(); ++b) {
588 const int box = boxes[b];
589 if (x.ShiftedStartMin(box) < current_max_end) {
590 // Merge.
591 current_length++;
592 current_max_end = std::max(current_max_end, x.EndMax(box));
593 } else {
594 if (current_length > 1) { // Ignore lists of size 1.
595 result->emplace_back(&boxes[current_start], current_length);
596 }
597 current_start = b;
598 current_length = 1;
599 current_max_end = x.EndMax(box);
600 }
601 }
602
603 // Push last span.
604 if (current_length > 1) {
605 result->emplace_back(&boxes[current_start], current_length);
606 }
607}
608
609// This function assumes that the left and right boxes overlap on the second
610// dimension, and that left cannot be after right.
611// It checks and pushes the lower bound of the right box and the upper bound
612// of the left box if need.
613//
614// If y is not null, it import the mandatory reason for the overlap on y in
615// the x helper.
616bool LeftBoxBeforeRightBoxOnFirstDimension(int left, int right,
617 SchedulingConstraintHelper* x,
618 SchedulingConstraintHelper* y) {
619 // left box2 pushes right box2.
620 const IntegerValue left_end_min = x->EndMin(left);
621 if (left_end_min > x->StartMin(right)) {
622 x->ClearReason();
623 x->AddPresenceReason(left);
624 x->AddPresenceReason(right);
625 x->AddReasonForBeingBefore(left, right);
626 x->AddEndMinReason(left, left_end_min);
627 if (y != nullptr) {
628 // left and right must overlap on y.
629 ClearAndAddMandatoryOverlapReason(left, right, y);
630 // Propagate with the complete reason.
631 x->ImportOtherReasons(*y);
632 }
633 RETURN_IF_FALSE(x->IncreaseStartMin(right, left_end_min));
634 }
635
636 // right box2 pushes left box2.
637 const IntegerValue right_start_max = x->StartMax(right);
638 if (right_start_max < x->EndMax(left)) {
639 x->ClearReason();
640 x->AddPresenceReason(left);
641 x->AddPresenceReason(right);
642 x->AddReasonForBeingBefore(left, right);
643 x->AddStartMaxReason(right, right_start_max);
644 if (y != nullptr) {
645 // left and right must overlap on y.
646 ClearAndAddMandatoryOverlapReason(left, right, y);
647 // Propagate with the complete reason.
648 x->ImportOtherReasons(*y);
649 }
650 RETURN_IF_FALSE(x->DecreaseEndMax(left, right_start_max));
651 }
652
653 return true;
654}
655
656} // namespace
657
658// Note that x_ and y_ must be initialized with enough intervals when passed
659// to the disjunctive propagators.
660NonOverlappingRectanglesDisjunctivePropagator::
661 NonOverlappingRectanglesDisjunctivePropagator(SchedulingConstraintHelper* x,
663 Model* model)
664 : global_x_(*x),
665 global_y_(*y),
666 x_(x->NumTasks(), model),
667 watcher_(model->GetOrCreate<GenericLiteralWatcher>()),
668 overload_checker_(&x_),
669 forward_detectable_precedences_(true, &x_),
670 backward_detectable_precedences_(false, &x_),
671 forward_not_last_(true, &x_),
672 backward_not_last_(false, &x_),
673 forward_edge_finding_(true, &x_),
674 backward_edge_finding_(false, &x_) {}
675
678
680 int fast_priority, int slow_priority) {
681 fast_id_ = watcher_->Register(this);
682 watcher_->SetPropagatorPriority(fast_id_, fast_priority);
683 global_x_.WatchAllTasks(fast_id_);
684 global_y_.WatchAllTasks(fast_id_);
685
686 // This propagator is the one making sure our propagation is complete, so
687 // we do need to make sure it is called again if it modified some bounds.
689
690 const int slow_id = watcher_->Register(this);
691 watcher_->SetPropagatorPriority(slow_id, slow_priority);
692 global_x_.WatchAllTasks(slow_id);
693 global_y_.WatchAllTasks(slow_id);
694}
695
696bool NonOverlappingRectanglesDisjunctivePropagator::
697 FindBoxesThatMustOverlapAHorizontalLineAndPropagate(
698 bool fast_propagation, const SchedulingConstraintHelper& x,
700 // Note that since we only push bounds on x, we cache the value for y just
701 // once.
702 if (!y->SynchronizeAndSetTimeDirection(true)) return false;
703
704 // Compute relevant boxes, the one with a mandatory part of y. Because we will
705 // need to sort it this way, we consider them by increasing start max.
706 indexed_boxes_.clear();
707 const auto temp = y->TaskByIncreasingNegatedStartMax();
708 for (int i = temp.size(); --i >= 0;) {
709 const int box = temp[i].task_index;
710 // Ignore absent boxes.
711 if (x.IsAbsent(box) || y->IsAbsent(box)) continue;
712
713 // Ignore boxes where the relevant presence literal is only on the y
714 // dimension, or if both intervals are optionals with different literals.
715 if (x.IsPresent(box) && !y->IsPresent(box)) continue;
716 if (!x.IsPresent(box) && !y->IsPresent(box) &&
717 x.PresenceLiteral(box) != y->PresenceLiteral(box)) {
718 continue;
719 }
720
721 const IntegerValue start_max = -temp[i].time;
722 const IntegerValue end_min = y->EndMin(box);
723 if (start_max < end_min) {
724 indexed_boxes_.push_back({box, start_max, end_min});
725 }
726 }
727
728 // Less than 2 boxes, no propagation.
729 if (indexed_boxes_.size() < 2) return true;
730 ConstructOverlappingSets(/*already_sorted=*/true, &indexed_boxes_,
731 &events_overlapping_boxes_);
732
733 // Split lists of boxes into disjoint set of boxes (w.r.t. overlap).
734 boxes_to_propagate_.clear();
735 reduced_overlapping_boxes_.clear();
736 for (int i = 0; i < events_overlapping_boxes_.size(); ++i) {
737 SplitDisjointBoxes(x, absl::MakeSpan(events_overlapping_boxes_[i]),
738 &disjoint_boxes_);
739 for (absl::Span<int> sub_boxes : disjoint_boxes_) {
740 // Boxes are sorted in a stable manner in the Split method.
741 // Note that we do not use reduced_overlapping_boxes_ directly so that
742 // the order of iteration is deterministic.
743 const auto& insertion = reduced_overlapping_boxes_.insert(sub_boxes);
744 if (insertion.second) boxes_to_propagate_.push_back(sub_boxes);
745 }
746 }
747
748 // And finally propagate.
749 //
750 // TODO(user): Sorting of boxes seems influential on the performance. Test.
751 for (const absl::Span<const int> boxes : boxes_to_propagate_) {
752 // The case of two boxes should be taken care of during "fast" propagation,
753 // so we can skip it here.
754 if (!fast_propagation && boxes.size() <= 2) continue;
755
756 x_.ClearOtherHelper();
757 if (!x_.ResetFromSubset(x, boxes)) return false;
758
759 // Collect the common overlapping coordinates of all boxes.
760 IntegerValue lb(std::numeric_limits<int64_t>::min());
761 IntegerValue ub(std::numeric_limits<int64_t>::max());
762 for (const int b : boxes) {
763 lb = std::max(lb, y->StartMax(b));
764 ub = std::min(ub, y->EndMin(b) - 1);
765 }
766 CHECK_LE(lb, ub);
767
768 // We want for different propagation to reuse as much as possible the same
769 // line. The idea behind this is to compute the 'canonical' line to use
770 // when explaining that boxes overlap on the 'y_dim' dimension. We compute
771 // the multiple of the biggest power of two that is common to all boxes.
772 //
773 // TODO(user): We should scan the integer trail to find the oldest
774 // non-empty common interval. Then we can pick the canonical value within
775 // it.
776 const IntegerValue line_to_use_for_reason = FindCanonicalValue(lb, ub);
777
778 // Setup x_dim for propagation.
779 x_.SetOtherHelper(y, boxes, line_to_use_for_reason);
780
781 if (fast_propagation) {
782 if (x_.NumTasks() == 2) {
783 // In that case, we can use simpler algorithms.
784 // Note that this case happens frequently (~30% of all calls to this
785 // method according to our tests).
786 RETURN_IF_FALSE(PropagateOnXWhenOnlyTwoBoxes());
787 } else {
788 RETURN_IF_FALSE(overload_checker_.Propagate());
789 RETURN_IF_FALSE(forward_detectable_precedences_.Propagate());
790 RETURN_IF_FALSE(backward_detectable_precedences_.Propagate());
791 }
792 } else {
793 DCHECK_GT(x_.NumTasks(), 2);
794 RETURN_IF_FALSE(forward_not_last_.Propagate());
795 RETURN_IF_FALSE(backward_not_last_.Propagate());
796 RETURN_IF_FALSE(backward_edge_finding_.Propagate());
797 RETURN_IF_FALSE(forward_edge_finding_.Propagate());
798 }
799 }
800
801 return true;
802}
803
805 global_x_.SetTimeDirection(true);
806 global_y_.SetTimeDirection(true);
807
808 // Note that the code assumes that this was registered twice in fast and slow
809 // mode. So we will not redo some propagation in slow mode that was already
810 // done by the fast mode.
811 const bool fast_propagation = watcher_->GetCurrentId() == fast_id_;
812 RETURN_IF_FALSE(FindBoxesThatMustOverlapAHorizontalLineAndPropagate(
813 fast_propagation, global_x_, &global_y_));
814
815 // We can actually swap dimensions to propagate vertically.
816 RETURN_IF_FALSE(FindBoxesThatMustOverlapAHorizontalLineAndPropagate(
817 fast_propagation, global_y_, &global_x_));
818
819 return true;
820}
821
822// Specialized propagation on only two boxes that must intersect with the
823// given y_line_for_reason.
824bool NonOverlappingRectanglesDisjunctivePropagator::
825 PropagateOnXWhenOnlyTwoBoxes() {
826 if (!x_.IsPresent(0) || !x_.IsPresent(1)) return true;
827
828 // For each direction and each order, we test if the boxes can be disjoint.
829 const int state =
830 (x_.EndMin(0) <= x_.StartMax(1)) + 2 * (x_.EndMin(1) <= x_.StartMax(0));
831 switch (state) {
832 case 0: { // Conflict.
833 ClearAndAddMandatoryOverlapReason(0, 1, &x_);
834 // Note that the secondary helper is set on x.
835 return x_.ReportConflict();
836 }
837 case 1: { // b1 is left of b2.
838 return LeftBoxBeforeRightBoxOnFirstDimension(0, 1, &x_, /*y=*/nullptr);
839 }
840 case 2: { // b2 is left of b1.
841 return LeftBoxBeforeRightBoxOnFirstDimension(1, 0, &x_, /*y=*/nullptr);
842 }
843 default: { // Nothing to deduce.
844 return true;
845 }
846 }
847}
848
850 const int id = watcher->Register(this);
851 global_x_.WatchAllTasks(id);
852 global_y_.WatchAllTasks(id);
854 return id;
855}
856
858 if (!VLOG_IS_ON(1)) return;
859 std::vector<std::pair<std::string, int64_t>> stats;
860 stats.push_back({"RectanglePairwisePropagator/called", num_calls_});
861 stats.push_back({"RectanglePairwisePropagator/pairwise_conflicts",
862 num_pairwise_conflicts_});
863 stats.push_back({"RectanglePairwisePropagator/pairwise_propagations",
864 num_pairwise_propagations_});
865
866 shared_stats_->AddStats(stats);
867}
868
870 if (!global_x_.SynchronizeAndSetTimeDirection(true)) return false;
871 if (!global_y_.SynchronizeAndSetTimeDirection(true)) return false;
872
873 num_calls_++;
874
875 horizontal_zero_area_boxes_.clear();
876 vertical_zero_area_boxes_.clear();
877 point_zero_area_boxes_.clear();
878 non_zero_area_boxes_.clear();
879 for (int b = 0; b < global_x_.NumTasks(); ++b) {
880 if (!global_x_.IsPresent(b) || !global_y_.IsPresent(b)) continue;
881 const IntegerValue x_size_max = global_x_.SizeMax(b);
882 const IntegerValue y_size_max = global_y_.SizeMax(b);
884 if (x_size_max == 0) {
885 if (y_size_max == 0) {
886 box = &point_zero_area_boxes_.emplace_back();
887 } else {
888 box = &vertical_zero_area_boxes_.emplace_back();
889 }
890 } else if (y_size_max == 0) {
891 box = &horizontal_zero_area_boxes_.emplace_back();
892 } else {
893 box = &non_zero_area_boxes_.emplace_back();
894 }
895 *box = ItemForPairwiseRestriction{.index = b,
896 .x = {.start_min = global_x_.StartMin(b),
897 .start_max = global_x_.StartMax(b),
898 .end_min = global_x_.EndMin(b),
899 .end_max = global_x_.EndMax(b)},
900 .y = {.start_min = global_y_.StartMin(b),
901 .start_max = global_y_.StartMax(b),
902 .end_min = global_y_.EndMin(b),
903 .end_max = global_y_.EndMax(b)}};
904 }
905
906 std::vector<PairwiseRestriction> restrictions;
907 RETURN_IF_FALSE(FindRestrictionsAndPropagateConflict(non_zero_area_boxes_,
908 &restrictions));
909
910 // Check zero area boxes against non-zero area boxes.
911 RETURN_IF_FALSE(FindRestrictionsAndPropagateConflict(
912 non_zero_area_boxes_, horizontal_zero_area_boxes_, &restrictions));
913 RETURN_IF_FALSE(FindRestrictionsAndPropagateConflict(
914 non_zero_area_boxes_, vertical_zero_area_boxes_, &restrictions));
915 RETURN_IF_FALSE(FindRestrictionsAndPropagateConflict(
916 non_zero_area_boxes_, point_zero_area_boxes_, &restrictions));
917
918 // Check vertical zero area boxes against horizontal zero area boxes.
919 RETURN_IF_FALSE(FindRestrictionsAndPropagateConflict(
920 vertical_zero_area_boxes_, horizontal_zero_area_boxes_, &restrictions));
921
922 for (const PairwiseRestriction& restriction : restrictions) {
923 RETURN_IF_FALSE(PropagateTwoBoxes(restriction));
924 }
925 return true;
926}
927
928bool RectanglePairwisePropagator::FindRestrictionsAndPropagateConflict(
929 const std::vector<ItemForPairwiseRestriction>& items,
930 std::vector<PairwiseRestriction>* restrictions) {
931 const int max_pairs =
932 params_->max_pairs_pairwise_reasoning_in_no_overlap_2d();
933 if (items.size() * (items.size() - 1) / 2 > max_pairs) {
934 return true;
935 }
936 AppendPairwiseRestrictions(items, restrictions);
937 for (const PairwiseRestriction& restriction : *restrictions) {
938 if (restriction.type ==
940 RETURN_IF_FALSE(PropagateTwoBoxes(restriction));
941 }
942 }
943 return true;
944}
945
946bool RectanglePairwisePropagator::FindRestrictionsAndPropagateConflict(
947 const std::vector<ItemForPairwiseRestriction>& items1,
948 const std::vector<ItemForPairwiseRestriction>& items2,
949 std::vector<PairwiseRestriction>* restrictions) {
950 const int max_pairs =
951 params_->max_pairs_pairwise_reasoning_in_no_overlap_2d();
952 if (items1.size() * items2.size() > max_pairs) {
953 return true;
954 }
955 AppendPairwiseRestrictions(items1, items2, restrictions);
956 for (const PairwiseRestriction& restriction : *restrictions) {
957 if (restriction.type ==
959 RETURN_IF_FALSE(PropagateTwoBoxes(restriction));
960 }
961 }
962 return true;
963}
964
965bool RectanglePairwisePropagator::PropagateTwoBoxes(
966 const PairwiseRestriction& restriction) {
967 const int box1 = restriction.first_index;
968 const int box2 = restriction.second_index;
969 switch (restriction.type) {
971 num_pairwise_conflicts_++;
972 return ClearAndAddTwoBoxesConflictReason(box1, box2, &global_x_,
973 &global_y_);
975 num_pairwise_propagations_++;
976 return LeftBoxBeforeRightBoxOnFirstDimension(box1, box2, &global_x_,
977 &global_y_);
979 num_pairwise_propagations_++;
980 return LeftBoxBeforeRightBoxOnFirstDimension(box2, box1, &global_x_,
981 &global_y_);
983 num_pairwise_propagations_++;
984 return LeftBoxBeforeRightBoxOnFirstDimension(box1, box2, &global_y_,
985 &global_x_);
987 num_pairwise_propagations_++;
988 return LeftBoxBeforeRightBoxOnFirstDimension(box2, box1, &global_y_,
989 &global_x_);
990 }
991}
992
993#undef RETURN_IF_FALSE
994} // namespace sat
995} // namespace operations_research
IntegerValue y
int64_t max
int64_t min
void SetPropagatorPriority(int id, int priority)
Definition integer.cc:2358
int Register(PropagatorInterface *propagator)
Registers a propagator and returns its unique ids.
Definition integer.cc:2332
SchedulingConstraintHelper * GetOrCreateHelper(const std::vector< IntervalVariable > &variables, bool register_as_disjunctive_helper=false)
Definition intervals.cc:190
void Register(int fast_priority, int slow_priority)
Definition diffn.cc:679
Propagates using a box energy reasoning.
Definition diffn.h:39
Propagator that compares the boxes pairwise.
Definition diffn.h:147
int RegisterWith(GenericLiteralWatcher *watcher)
Definition diffn.cc:849
void WatchAllTasks(int id, bool watch_max_side=true)
Definition intervals.cc:802
int NumTasks() const
Returns the number of task.
Definition intervals.h:293
ABSL_MUST_USE_RESULT bool ResetFromSubset(const SchedulingConstraintHelper &other, absl::Span< const int > tasks)
Definition intervals.cc:396
ABSL_MUST_USE_RESULT bool SynchronizeAndSetTimeDirection(bool is_forward)
Definition intervals.cc:483
void SetOtherHelper(SchedulingConstraintHelper *other_helper, absl::Span< const int > map_to_other_helper, IntegerValue event)
Definition intervals.h:497
void AddStats(absl::Span< const std::pair< std::string, int64_t > > stats)
Adds a bunch of stats, adding count for the same key together.
int64_t b
Definition table.cc:45
int64_t a
Definition table.cc:44
IntVar * var
GRBmodel * model
std::function< void(Model *)> WeightedSumGreaterOrEqual(const std::vector< IntegerVariable > &vars, const VectorInt &coefficients, int64_t lower_bound)
Weighted sum >= constant.
constexpr IntegerValue kMaxIntegerValue(std::numeric_limits< IntegerValue::ValueType >::max() - 1)
void AppendPairwiseRestrictions(absl::Span< const ItemForPairwiseRestriction > items, std::vector< PairwiseRestriction > *result)
std::vector< IntegerVariable > NegationOf(const std::vector< IntegerVariable > &vars)
Returns the vector of the negated variables.
Definition integer.cc:51
void ConstructOverlappingSets(bool already_sorted, std::vector< IndexedInterval > *intervals, std::vector< std::vector< int > > *result)
constexpr IntegerValue kMinIntegerValue(-kMaxIntegerValue.value())
const IntegerVariable kNoIntegerVariable(-1)
FindRectanglesResult FindRectanglesWithEnergyConflictMC(const std::vector< RectangleInRange > &intervals, absl::BitGenRef random, double temperature, double candidate_energy_usage_factor)
std::function< void(Model *)> IsEqualToMinOf(IntegerVariable min_var, const std::vector< IntegerVariable > &vars)
std::function< IntegerVariable(Model *)> NewIntegerVariable(int64_t lb, int64_t ub)
Definition integer.h:1907
void AddNonOverlappingRectangles(const std::vector< IntervalVariable > &x, const std::vector< IntervalVariable > &y, Model *model)
Definition diffn.cc:174
void AddCumulativeOverloadChecker(AffineExpression capacity, SchedulingConstraintHelper *helper, SchedulingDemandHelper *demands, Model *model)
In SWIG mode, we don't want anything besides these top-level includes.
int64_t CapSub(int64_t x, int64_t y)
const Variable x
Definition qp_tests.cc:127
#define RETURN_IF_FALSE(f)
Definition diffn.cc:238
Rev< int64_t > start_max
Rev< int64_t > end_min
const std::optional< Range > & range
Definition statistics.cc:37