Google OR-Tools v9.12
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
scheduling_helpers.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
15
16#include <algorithm>
17#include <cstdint>
18#include <functional>
19#include <string>
20#include <utility>
21#include <vector>
22
23#include "absl/log/check.h"
24#include "absl/meta/type_traits.h"
25#include "absl/strings/str_cat.h"
26#include "absl/types/span.h"
30#include "ortools/sat/integer.h"
34#include "ortools/sat/model.h"
38#include "ortools/util/sort.h"
40
41namespace operations_research {
42namespace sat {
43
45 std::vector<AffineExpression> starts, std::vector<AffineExpression> ends,
46 std::vector<AffineExpression> sizes,
47 std::vector<LiteralIndex> reason_for_presence, Model* model)
48 : model_(model),
49 sat_solver_(model->GetOrCreate<SatSolver>()),
50 assignment_(sat_solver_->Assignment()),
51 integer_trail_(model->GetOrCreate<IntegerTrail>()),
52 watcher_(model->GetOrCreate<GenericLiteralWatcher>()),
53 precedence_relations_(model->GetOrCreate<PrecedenceRelations>()),
54 starts_(std::move(starts)),
55 ends_(std::move(ends)),
56 sizes_(std::move(sizes)),
57 reason_for_presence_(std::move(reason_for_presence)),
58 capacity_(starts_.size()),
59 cached_size_min_(new IntegerValue[capacity_]),
60 cached_start_min_(new IntegerValue[capacity_]),
61 cached_end_min_(new IntegerValue[capacity_]),
62 cached_negated_start_max_(new IntegerValue[capacity_]),
63 cached_negated_end_max_(new IntegerValue[capacity_]),
64 cached_shifted_start_min_(new IntegerValue[capacity_]),
65 cached_negated_shifted_end_max_(new IntegerValue[capacity_]) {
66 DCHECK_EQ(starts_.size(), ends_.size());
67 DCHECK_EQ(starts_.size(), sizes_.size());
68 DCHECK_EQ(starts_.size(), reason_for_presence_.size());
69
70 minus_starts_.clear();
71 minus_starts_.reserve(starts_.size());
72 minus_ends_.clear();
73 minus_ends_.reserve(starts_.size());
74 for (int i = 0; i < starts_.size(); ++i) {
75 minus_starts_.push_back(starts_[i].Negated());
76 minus_ends_.push_back(ends_[i].Negated());
77 }
78
79 InitSortedVectors();
81 model->GetOrCreate<SatSolver>()->NotifyThatModelIsUnsat();
82 }
83}
84
86 Model* model)
87 : model_(model),
88 sat_solver_(model->GetOrCreate<SatSolver>()),
89 assignment_(sat_solver_->Assignment()),
90 integer_trail_(model->GetOrCreate<IntegerTrail>()),
91 precedence_relations_(model->GetOrCreate<PrecedenceRelations>()),
92 capacity_(num_tasks),
93 cached_size_min_(new IntegerValue[capacity_]),
94 cached_start_min_(new IntegerValue[capacity_]),
95 cached_end_min_(new IntegerValue[capacity_]),
96 cached_negated_start_max_(new IntegerValue[capacity_]),
97 cached_negated_end_max_(new IntegerValue[capacity_]),
98 cached_shifted_start_min_(new IntegerValue[capacity_]),
99 cached_negated_shifted_end_max_(new IntegerValue[capacity_]) {
100 starts_.resize(num_tasks);
101 CHECK_EQ(NumTasks(), num_tasks);
102}
103
105 recompute_all_cache_ = true;
106 for (const int id : propagator_ids_) watcher_->CallOnNextPropagate(id);
107 return true;
108}
109
111 const std::vector<int>& watch_indices) {
112 for (const int t : watch_indices) recompute_cache_.Set(t);
113 for (const int id : propagator_ids_) watcher_->CallOnNextPropagate(id);
114 return true;
115}
116
118 const int id = watcher->Register(this);
119 const int num_tasks = starts_.size();
120 for (int t = 0; t < num_tasks; ++t) {
121 watcher->WatchIntegerVariable(sizes_[t].var, id, t);
122 watcher->WatchIntegerVariable(starts_[t].var, id, t);
123 watcher->WatchIntegerVariable(ends_[t].var, id, t);
124
125 // This class do not need to be waked up on presence change, since this is
126 // not cached. However given that we can have many propagators that use the
127 // same helper, it is nicer to only register this one, and wake up all
128 // propagator through it rather than registering all of them individually.
129 // Note that IncrementalPropagate() will do nothing if this is the only
130 // change except waking up registered propagators.
131 if (!IsPresent(t) && !IsAbsent(t)) {
132 watcher_->WatchLiteral(Literal(reason_for_presence_[t]), id);
133 }
134 }
135 watcher->SetPropagatorPriority(id, 0);
136}
137
138bool SchedulingConstraintHelper::UpdateCachedValues(int t) {
139 if (IsAbsent(t)) return true;
140
141 IntegerValue smin = integer_trail_->LowerBound(starts_[t]);
142 IntegerValue smax = integer_trail_->UpperBound(starts_[t]);
143 IntegerValue emin = integer_trail_->LowerBound(ends_[t]);
144 IntegerValue emax = integer_trail_->UpperBound(ends_[t]);
145
146 // We take the max for the corner case where the size of an optional interval
147 // is used elsewhere and has a domain with negative value.
148 //
149 // TODO(user): maybe we should just disallow size with a negative domain, but
150 // is is harder to enforce if we have a linear expression for size.
151 IntegerValue dmin =
152 std::max(IntegerValue(0), integer_trail_->LowerBound(sizes_[t]));
153 IntegerValue dmax = integer_trail_->UpperBound(sizes_[t]);
154
155 // Detect first if we have a conflict using the relation start + size = end.
156 if (dmax < 0) {
157 ClearReason();
158 AddSizeMaxReason(t, dmax);
159 return PushTaskAbsence(t);
160 }
161 if (smin + dmin - emax > 0) {
162 ClearReason();
163 AddStartMinReason(t, smin);
164 AddSizeMinReason(t, dmin);
165 AddEndMaxReason(t, emax);
166 return PushTaskAbsence(t);
167 }
168 if (smax + dmax - emin < 0) {
169 ClearReason();
170 AddStartMaxReason(t, smax);
171 AddSizeMaxReason(t, dmax);
172 AddEndMinReason(t, emin);
173 return PushTaskAbsence(t);
174 }
175
176 // Sometimes, for optional interval with non-optional bounds, this propagation
177 // give tighter bounds. We always consider the value assuming
178 // the interval is present.
179 //
180 // Note that this is also useful in case not everything was propagated. Note
181 // also that since there is no conflict, we reach the fix point in one pass.
182 smin = std::max(smin, emin - dmax);
183 smax = std::min(smax, emax - dmin);
184 dmin = std::max(dmin, emin - smax);
185 emin = std::max(emin, smin + dmin);
186 emax = std::min(emax, smax + dmax);
187
188 if (emin != cached_end_min_[t]) {
189 recompute_energy_profile_ = true;
190 }
191
192 // We might only want to do that if the value changed, but I am not sure it
193 // is worth the test.
194 recompute_by_start_max_ = true;
195 recompute_by_end_min_ = true;
196
197 cached_start_min_[t] = smin;
198 cached_end_min_[t] = emin;
199 cached_negated_start_max_[t] = -smax;
200 cached_negated_end_max_[t] = -emax;
201 cached_size_min_[t] = dmin;
202
203 // Note that we use the cached value here for EndMin()/StartMax().
204 const IntegerValue new_shifted_start_min = emin - dmin;
205 if (new_shifted_start_min != cached_shifted_start_min_[t]) {
206 recompute_energy_profile_ = true;
207 recompute_shifted_start_min_ = true;
208 cached_shifted_start_min_[t] = new_shifted_start_min;
209 }
210 const IntegerValue new_negated_shifted_end_max = -(smax + dmin);
211 if (new_negated_shifted_end_max != cached_negated_shifted_end_max_[t]) {
212 recompute_negated_shifted_end_max_ = true;
213 cached_negated_shifted_end_max_[t] = new_negated_shifted_end_max;
214 }
215 return true;
216}
217
219 const SchedulingConstraintHelper& other, absl::Span<const int> tasks) {
220 current_time_direction_ = other.current_time_direction_;
221
222 const int num_tasks = tasks.size();
223 CHECK_LE(num_tasks, capacity_);
224 starts_.resize(num_tasks);
225 ends_.resize(num_tasks);
226 minus_ends_.resize(num_tasks);
227 minus_starts_.resize(num_tasks);
228 sizes_.resize(num_tasks);
229 reason_for_presence_.resize(num_tasks);
230 for (int i = 0; i < num_tasks; ++i) {
231 const int t = tasks[i];
232 starts_[i] = other.starts_[t];
233 ends_[i] = other.ends_[t];
234 minus_ends_[i] = other.minus_ends_[t];
235 minus_starts_[i] = other.minus_starts_[t];
236 sizes_[i] = other.sizes_[t];
237 reason_for_presence_[i] = other.reason_for_presence_[t];
238 }
239
240 InitSortedVectors();
242}
243
244void SchedulingConstraintHelper::InitSortedVectors() {
245 const int num_tasks = starts_.size();
246
247 recompute_all_cache_ = true;
248 recompute_cache_.Resize(num_tasks);
249 for (int t = 0; t < num_tasks; ++t) {
250 recompute_cache_.Set(t);
251 }
252
253 // Make sure all the cached_* arrays can hold enough data.
254 CHECK_LE(num_tasks, capacity_);
255
256 task_by_increasing_start_min_.resize(num_tasks);
257 task_by_increasing_end_min_.resize(num_tasks);
258 task_by_increasing_negated_start_max_.resize(num_tasks);
259 task_by_decreasing_end_max_.resize(num_tasks);
260 task_by_increasing_shifted_start_min_.resize(num_tasks);
261 task_by_negated_shifted_end_max_.resize(num_tasks);
262 for (int t = 0; t < num_tasks; ++t) {
263 task_by_increasing_start_min_[t].task_index = t;
264 task_by_increasing_end_min_[t].task_index = t;
265 task_by_increasing_negated_start_max_[t].task_index = t;
266 task_by_decreasing_end_max_[t].task_index = t;
267
268 task_by_increasing_shifted_start_min_[t].task_index = t;
269 task_by_increasing_shifted_start_min_[t].presence_lit =
270 reason_for_presence_[t];
271 task_by_negated_shifted_end_max_[t].task_index = t;
272 task_by_negated_shifted_end_max_[t].presence_lit = reason_for_presence_[t];
273 }
274
275 recompute_by_start_max_ = true;
276 recompute_by_end_min_ = true;
277 recompute_energy_profile_ = true;
278 recompute_shifted_start_min_ = true;
279 recompute_negated_shifted_end_max_ = true;
280}
281
283 if (current_time_direction_ != is_forward) {
284 current_time_direction_ = is_forward;
285
286 std::swap(starts_, minus_ends_);
287 std::swap(ends_, minus_starts_);
288
289 std::swap(task_by_increasing_start_min_, task_by_decreasing_end_max_);
290 std::swap(task_by_increasing_end_min_,
291 task_by_increasing_negated_start_max_);
292 std::swap(recompute_by_end_min_, recompute_by_start_max_);
293 std::swap(task_by_increasing_shifted_start_min_,
294 task_by_negated_shifted_end_max_);
295
296 recompute_energy_profile_ = true;
297 std::swap(cached_start_min_, cached_negated_end_max_);
298 std::swap(cached_end_min_, cached_negated_start_max_);
299 std::swap(cached_shifted_start_min_, cached_negated_shifted_end_max_);
300 std::swap(recompute_shifted_start_min_, recompute_negated_shifted_end_max_);
301 }
302}
303
305 bool is_forward) {
306 SetTimeDirection(is_forward);
307
308 // If there was any backtracks since the last time this was called, we
309 // recompute our cache.
310 if (sat_solver_->num_backtracks() != saved_num_backtracks_) {
311 recompute_all_cache_ = true;
312 saved_num_backtracks_ = sat_solver_->num_backtracks();
313 }
314
315 if (recompute_all_cache_) {
316 for (int t = 0; t < recompute_cache_.size(); ++t) {
317 if (!UpdateCachedValues(t)) return false;
318 }
319 } else {
320 for (const int t : recompute_cache_) {
321 if (!UpdateCachedValues(t)) return false;
322 }
323 }
324 recompute_cache_.ClearAll();
325 recompute_all_cache_ = false;
326 return true;
327}
328
329// TODO(user): be more precise when we know a and b are in disjunction.
330// we really just need start_b > start_a, or even >= if duration is non-zero.
332 int a, int b, bool add_reason_if_after) {
333 const AffineExpression before = ends_[a];
334 const AffineExpression after = starts_[b];
335 if (before.var == kNoIntegerVariable || before.coeff != 1 ||
336 after.var == kNoIntegerVariable || after.coeff != 1) {
337 return kMinIntegerValue;
338 }
339
340 // We take the max of the level zero offset and the one coming from a
341 // conditional precedence at true.
342 const IntegerValue conditional_offset =
343 precedence_relations_->GetConditionalOffset(before.var, after.var);
344 const IntegerValue known = integer_trail_->LevelZeroLowerBound(after.var) -
345 integer_trail_->LevelZeroUpperBound(before.var);
346 const IntegerValue offset = std::max(conditional_offset, known);
347
348 const IntegerValue needed_offset = before.constant - after.constant;
349 const IntegerValue distance = offset - needed_offset;
350 if (add_reason_if_after && distance >= 0 && known < conditional_offset) {
351 for (const Literal l : precedence_relations_->GetConditionalEnforcements(
352 before.var, after.var)) {
353 literal_reason_.push_back(l.Negated());
354 }
355 }
356 return distance;
357}
358
359// Note that we could call this at a positive level to propagate any literal
360// associated to task a before task b. However we only call this for task that
361// are in detectable precedence, which means the normal precedence or linear
362// propagator should have already propagated that Boolean too.
364 CHECK(IsPresent(a));
365 CHECK(IsPresent(b));
366 CHECK_EQ(sat_solver_->CurrentDecisionLevel(), 0);
367
368 const AffineExpression before = ends_[a];
369 const AffineExpression after = starts_[b];
370 if (after.coeff != 1) return true;
371 if (before.coeff != 1) return true;
372 if (after.var == kNoIntegerVariable) return true;
373 if (before.var == kNoIntegerVariable) return true;
374 if (before.var == after.var) {
375 if (before.constant <= after.constant) {
376 return true;
377 } else {
378 sat_solver_->NotifyThatModelIsUnsat();
379 return false;
380 }
381 }
382 const IntegerValue offset = before.constant - after.constant;
383 if (precedence_relations_->Add(before.var, after.var, offset)) {
384 VLOG(2) << "new relation " << TaskDebugString(a)
385 << " <= " << TaskDebugString(b);
386 if (before.var == NegationOf(after.var)) {
387 AddWeightedSumLowerOrEqual({}, {before.var}, {int64_t{2}},
388 -offset.value(), model_);
389 } else {
390 // TODO(user): Adding new constraint during propagation might not be the
391 // best idea as it can create some complication.
392 AddWeightedSumLowerOrEqual({}, {before.var, after.var},
393 {int64_t{1}, int64_t{-1}}, -offset.value(),
394 model_);
395 }
396 if (sat_solver_->ModelIsUnsat()) return false;
397 }
398 return true;
399}
400
401absl::Span<const TaskTime>
403 for (TaskTime& ref : task_by_increasing_start_min_) {
404 ref.time = StartMin(ref.task_index);
405 }
406 IncrementalSort(task_by_increasing_start_min_.begin(),
407 task_by_increasing_start_min_.end());
408 return task_by_increasing_start_min_;
409}
410
411absl::Span<const TaskTime>
413 if (!recompute_by_end_min_) return task_by_increasing_end_min_;
414 for (TaskTime& ref : task_by_increasing_end_min_) {
415 ref.time = EndMin(ref.task_index);
416 }
417 IncrementalSort(task_by_increasing_end_min_.begin(),
418 task_by_increasing_end_min_.end());
419 recompute_by_end_min_ = false;
420 return task_by_increasing_end_min_;
421}
422
423absl::Span<const TaskTime>
425 if (!recompute_by_start_max_) return task_by_increasing_negated_start_max_;
426 for (TaskTime& ref : task_by_increasing_negated_start_max_) {
427 ref.time = cached_negated_start_max_[ref.task_index];
428 }
429 IncrementalSort(task_by_increasing_negated_start_max_.begin(),
430 task_by_increasing_negated_start_max_.end());
431 recompute_by_start_max_ = false;
432 return task_by_increasing_negated_start_max_;
433}
434
435absl::Span<const TaskTime>
437 for (TaskTime& ref : task_by_decreasing_end_max_) {
438 ref.time = EndMax(ref.task_index);
439 }
440 IncrementalSort(task_by_decreasing_end_max_.begin(),
441 task_by_decreasing_end_max_.end(), std::greater<TaskTime>());
442 return task_by_decreasing_end_max_;
443}
444
445absl::Span<const CachedTaskBounds>
447 if (recompute_shifted_start_min_) {
448 recompute_shifted_start_min_ = false;
449 bool is_sorted = true;
450 IntegerValue previous = kMinIntegerValue;
451 for (CachedTaskBounds& ref : task_by_increasing_shifted_start_min_) {
452 ref.time = ShiftedStartMin(ref.task_index);
453 is_sorted = is_sorted && ref.time >= previous;
454 previous = ref.time;
455 }
456 if (is_sorted) return task_by_increasing_shifted_start_min_;
457 IncrementalSort(task_by_increasing_shifted_start_min_.begin(),
458 task_by_increasing_shifted_start_min_.end());
459 }
460 return task_by_increasing_shifted_start_min_;
461}
462
463// TODO(user): Avoid recomputing it if nothing changed.
464const std::vector<SchedulingConstraintHelper::ProfileEvent>&
466 if (energy_profile_.empty()) {
467 const int num_tasks = NumTasks();
468 for (int t = 0; t < num_tasks; ++t) {
469 energy_profile_.push_back(
470 {cached_shifted_start_min_[t], t, /*is_first=*/true});
471 energy_profile_.push_back({cached_end_min_[t], t, /*is_first=*/false});
472 }
473 } else {
474 if (!recompute_energy_profile_) return energy_profile_;
475 for (ProfileEvent& ref : energy_profile_) {
476 const int t = ref.task;
477 if (ref.is_first) {
478 ref.time = cached_shifted_start_min_[t];
479 } else {
480 ref.time = cached_end_min_[t];
481 }
482 }
483 }
484 IncrementalSort(energy_profile_.begin(), energy_profile_.end());
485 recompute_energy_profile_ = false;
486 return energy_profile_;
487}
488
489// Produces a relaxed reason for StartMax(before) < EndMin(after).
491 int after) {
492 AddOtherReason(before);
493 AddOtherReason(after);
494
495 // The reason will be a linear expression greater than a value. Note that all
496 // coeff must be positive, and we will use the variable lower bound.
497 std::vector<IntegerVariable> vars;
498 std::vector<IntegerValue> coeffs;
499
500 // Reason for StartMax(before).
501 const IntegerValue smax_before = StartMax(before);
502 if (smax_before >= integer_trail_->UpperBound(starts_[before])) {
503 if (starts_[before].var != kNoIntegerVariable) {
504 vars.push_back(NegationOf(starts_[before].var));
505 coeffs.push_back(starts_[before].coeff);
506 }
507 } else {
508 if (ends_[before].var != kNoIntegerVariable) {
509 vars.push_back(NegationOf(ends_[before].var));
510 coeffs.push_back(ends_[before].coeff);
511 }
512 if (sizes_[before].var != kNoIntegerVariable) {
513 vars.push_back(sizes_[before].var);
514 coeffs.push_back(sizes_[before].coeff);
515 }
516 }
517
518 // Reason for EndMin(after);
519 const IntegerValue emin_after = EndMin(after);
520 if (emin_after <= integer_trail_->LowerBound(ends_[after])) {
521 if (ends_[after].var != kNoIntegerVariable) {
522 vars.push_back(ends_[after].var);
523 coeffs.push_back(ends_[after].coeff);
524 }
525 } else {
526 if (starts_[after].var != kNoIntegerVariable) {
527 vars.push_back(starts_[after].var);
528 coeffs.push_back(starts_[after].coeff);
529 }
530 if (sizes_[after].var != kNoIntegerVariable) {
531 vars.push_back(sizes_[after].var);
532 coeffs.push_back(sizes_[after].coeff);
533 }
534 }
535
536 DCHECK_LT(smax_before, emin_after);
537 const IntegerValue slack = emin_after - smax_before - 1;
538 integer_trail_->AppendRelaxedLinearReason(slack, coeffs, vars,
539 &integer_reason_);
540}
541
543 CHECK(other_helper_ == nullptr);
544 return integer_trail_->Enqueue(lit, literal_reason_, integer_reason_);
545}
546
548 int t, IntegerLiteral lit) {
549 if (IsAbsent(t)) return true;
550 AddOtherReason(t);
552 if (IsOptional(t)) {
553 return integer_trail_->ConditionalEnqueue(
554 PresenceLiteral(t), lit, &literal_reason_, &integer_reason_);
555 }
556 return integer_trail_->Enqueue(lit, literal_reason_, integer_reason_);
557}
558
559// We also run directly the precedence propagator for this variable so that when
560// we push an interval start for example, we have a chance to push its end.
561bool SchedulingConstraintHelper::PushIntervalBound(int t, IntegerLiteral lit) {
562 if (!PushIntegerLiteralIfTaskPresent(t, lit)) return false;
563 if (IsAbsent(t)) return true;
564 if (!UpdateCachedValues(t)) return false;
565 recompute_cache_.Clear(t);
566 return true;
567}
568
569bool SchedulingConstraintHelper::IncreaseStartMin(int t, IntegerValue value) {
570 if (starts_[t].var == kNoIntegerVariable) {
571 if (value > starts_[t].constant) return PushTaskAbsence(t);
572 return true;
573 }
574 return PushIntervalBound(t, starts_[t].GreaterOrEqual(value));
575}
576
577bool SchedulingConstraintHelper::IncreaseEndMin(int t, IntegerValue value) {
578 if (ends_[t].var == kNoIntegerVariable) {
579 if (value > ends_[t].constant) return PushTaskAbsence(t);
580 return true;
581 }
582 return PushIntervalBound(t, ends_[t].GreaterOrEqual(value));
583}
584
585bool SchedulingConstraintHelper::DecreaseEndMax(int t, IntegerValue value) {
586 if (ends_[t].var == kNoIntegerVariable) {
587 if (value < ends_[t].constant) return PushTaskAbsence(t);
588 return true;
589 }
590 return PushIntervalBound(t, ends_[t].LowerOrEqual(value));
591}
592
594 integer_trail_->EnqueueLiteral(l, literal_reason_, integer_reason_);
595 return true;
596}
597
599 if (IsAbsent(t)) return true;
600 if (!IsOptional(t)) return ReportConflict();
601
602 AddOtherReason(t);
603
604 if (IsPresent(t)) {
605 literal_reason_.push_back(Literal(reason_for_presence_[t]).Negated());
606 return ReportConflict();
607 }
609 integer_trail_->EnqueueLiteral(Literal(reason_for_presence_[t]).Negated(),
610 literal_reason_, integer_reason_);
611 return true;
612}
613
615 DCHECK_NE(reason_for_presence_[t], kNoLiteralIndex);
616 DCHECK(!IsPresent(t));
617
618 AddOtherReason(t);
619
620 if (IsAbsent(t)) {
621 literal_reason_.push_back(Literal(reason_for_presence_[t]));
622 return ReportConflict();
623 }
625 integer_trail_->EnqueueLiteral(Literal(reason_for_presence_[t]),
626 literal_reason_, integer_reason_);
627 return true;
628}
629
632 return integer_trail_->ReportConflict(literal_reason_, integer_reason_);
633}
634
636 // It is more efficient to enqueue the propagator
637 // when the helper Propagate() is called. This result in less entries in our
638 // watched lists.
639 propagator_ids_.push_back(id);
640}
641
642void SchedulingConstraintHelper::AddOtherReason(int t) {
643 if (other_helper_ == nullptr || already_added_to_other_reasons_[t]) return;
644 already_added_to_other_reasons_[t] = true;
645 const int mapped_t = map_to_other_helper_[t];
646 other_helper_->AddStartMaxReason(mapped_t, event_for_other_helper_);
647 other_helper_->AddEndMinReason(mapped_t, event_for_other_helper_ + 1);
648}
649
650void SchedulingConstraintHelper::ImportOtherReasons() {
651 if (other_helper_ != nullptr) ImportOtherReasons(*other_helper_);
652}
653
655 const SchedulingConstraintHelper& other_helper) {
656 literal_reason_.insert(literal_reason_.end(),
657 other_helper.literal_reason_.begin(),
658 other_helper.literal_reason_.end());
659 integer_reason_.insert(integer_reason_.end(),
660 other_helper.integer_reason_.begin(),
661 other_helper.integer_reason_.end());
662}
663
665 return absl::StrCat("t=", t, " is_present=",
666 (IsPresent(t) ? "1"
667 : IsAbsent(t) ? "0"
668 : "?"),
669 " size=[", SizeMin(t).value(), ",", SizeMax(t).value(),
670 "]", " start=[", StartMin(t).value(), ",",
671 StartMax(t).value(), "]", " end=[", EndMin(t).value(),
672 ",", EndMax(t).value(), "]");
673}
674
676 IntegerValue start,
677 IntegerValue end) const {
678 return std::min(std::min(end - start, SizeMin(t)),
679 std::min(EndMin(t) - start, end - StartMax(t)));
680}
681
683 IntegerValue start_min, IntegerValue start_max, IntegerValue end_min,
684 IntegerValue end_max, IntegerValue size_min, IntegerValue demand_min,
685 absl::Span<const LiteralValueValue> filtered_energy,
686 IntegerValue window_start, IntegerValue window_end) {
687 if (window_end <= window_start) return IntegerValue(0);
688
689 // Returns zero if the interval do not necessarily overlap.
690 if (end_min <= window_start) return IntegerValue(0);
691 if (start_max >= window_end) return IntegerValue(0);
692 const IntegerValue window_size = window_end - window_start;
693 const IntegerValue simple_energy_min =
694 demand_min * std::min({end_min - window_start, window_end - start_max,
695 size_min, window_size});
696 if (filtered_energy.empty()) return simple_energy_min;
697
698 IntegerValue result = kMaxIntegerValue;
699 for (const auto [lit, fixed_size, fixed_demand] : filtered_energy) {
700 const IntegerValue alt_end_min = std::max(end_min, start_min + fixed_size);
701 const IntegerValue alt_start_max =
702 std::min(start_max, end_max - fixed_size);
703 const IntegerValue energy_min =
704 fixed_demand *
705 std::min({alt_end_min - window_start, window_end - alt_start_max,
706 fixed_size, window_size});
707 result = std::min(result, energy_min);
708 }
709 if (result == kMaxIntegerValue) return simple_energy_min;
710 return std::max(simple_energy_min, result);
711}
712
714 absl::Span<const AffineExpression> demands,
715 SchedulingConstraintHelper* helper, Model* model)
716 : integer_trail_(model->GetOrCreate<IntegerTrail>()),
717 product_decomposer_(model->GetOrCreate<ProductDecomposer>()),
718 sat_solver_(model->GetOrCreate<SatSolver>()),
719 assignment_(model->GetOrCreate<SatSolver>()->Assignment()),
720 demands_(demands.begin(), demands.end()),
721 helper_(helper) {
722 const int num_tasks = helper->NumTasks();
723 decomposed_energies_.resize(num_tasks);
724 cached_energies_min_.resize(num_tasks, kMinIntegerValue);
725 cached_energies_max_.resize(num_tasks, kMaxIntegerValue);
726 energy_is_quadratic_.resize(num_tasks, false);
727
728 // We try to init decomposed energies. This is needed for the cuts that are
729 // created after we call InitAllDecomposedEnergies().
731}
732
734 // For the special case were demands is empty.
735 const int num_tasks = helper_->NumTasks();
736 if (demands_.size() != num_tasks) return;
737 for (int t = 0; t < num_tasks; ++t) {
738 const AffineExpression size = helper_->Sizes()[t];
739 const AffineExpression demand = demands_[t];
740 decomposed_energies_[t] = product_decomposer_->TryToDecompose(size, demand);
741 }
742}
743
744IntegerValue SchedulingDemandHelper::SimpleEnergyMin(int t) const {
745 if (demands_.empty()) return kMinIntegerValue;
746 return CapProdI(DemandMin(t), helper_->SizeMin(t));
747}
748
749IntegerValue SchedulingDemandHelper::DecomposedEnergyMin(int t) const {
750 if (decomposed_energies_[t].empty()) return kMinIntegerValue;
751 IntegerValue result = kMaxIntegerValue;
752 for (const auto [lit, fixed_size, fixed_demand] : decomposed_energies_[t]) {
753 if (assignment_.LiteralIsTrue(lit)) {
754 return fixed_size * fixed_demand;
755 }
756 if (assignment_.LiteralIsFalse(lit)) continue;
757 result = std::min(result, fixed_size * fixed_demand);
758 }
759 DCHECK_NE(result, kMaxIntegerValue);
760 return result;
761}
762
763IntegerValue SchedulingDemandHelper::SimpleEnergyMax(int t) const {
764 if (demands_.empty()) return kMaxIntegerValue;
765 return CapProdI(DemandMax(t), helper_->SizeMax(t));
766}
767
768IntegerValue SchedulingDemandHelper::DecomposedEnergyMax(int t) const {
769 if (decomposed_energies_[t].empty()) return kMaxIntegerValue;
770 IntegerValue result = kMinIntegerValue;
771 for (const auto [lit, fixed_size, fixed_demand] : decomposed_energies_[t]) {
772 if (assignment_.LiteralIsTrue(lit)) {
773 return fixed_size * fixed_demand;
774 }
775 if (assignment_.LiteralIsFalse(lit)) continue;
776 result = std::max(result, fixed_size * fixed_demand);
777 }
778 DCHECK_NE(result, kMinIntegerValue);
779 return result;
780}
781
783 const int num_tasks = cached_energies_min_.size();
784 const bool is_at_level_zero = sat_solver_->CurrentDecisionLevel() == 0;
785 for (int t = 0; t < num_tasks; ++t) {
786 // Try to reduce the size of the decomposed energy vector.
787 if (is_at_level_zero) {
788 int new_size = 0;
789 for (int i = 0; i < decomposed_energies_[t].size(); ++i) {
790 if (assignment_.LiteralIsFalse(decomposed_energies_[t][i].literal)) {
791 continue;
792 }
793 decomposed_energies_[t][new_size++] = decomposed_energies_[t][i];
794 }
795 decomposed_energies_[t].resize(new_size);
796 }
797
798 cached_energies_min_[t] =
799 std::max(SimpleEnergyMin(t), DecomposedEnergyMin(t));
800 if (cached_energies_min_[t] <= kMinIntegerValue) return false;
801 energy_is_quadratic_[t] =
802 decomposed_energies_[t].empty() && !demands_.empty() &&
803 !integer_trail_->IsFixed(demands_[t]) && !helper_->SizeIsFixed(t);
804 cached_energies_max_[t] =
805 std::min(SimpleEnergyMax(t), DecomposedEnergyMax(t));
806 if (cached_energies_max_[t] >= kMaxIntegerValue) return false;
807 }
808
809 return true;
810}
811
812IntegerValue SchedulingDemandHelper::DemandMin(int t) const {
813 DCHECK_LT(t, demands_.size());
814 return integer_trail_->LowerBound(demands_[t]);
815}
816
817IntegerValue SchedulingDemandHelper::DemandMax(int t) const {
818 DCHECK_LT(t, demands_.size());
819 return integer_trail_->UpperBound(demands_[t]);
820}
821
823 return integer_trail_->IsFixed(demands_[t]);
824}
825
826bool SchedulingDemandHelper::DecreaseEnergyMax(int t, IntegerValue value) {
827 if (value < EnergyMin(t)) {
828 if (helper_->IsOptional(t)) {
829 return helper_->PushTaskAbsence(t);
830 } else {
831 return helper_->ReportConflict();
832 }
833 } else if (!decomposed_energies_[t].empty()) {
834 for (const auto [lit, fixed_size, fixed_demand] : decomposed_energies_[t]) {
835 if (fixed_size * fixed_demand > value) {
836 if (assignment_.LiteralIsTrue(lit)) return helper_->ReportConflict();
837 if (assignment_.LiteralIsFalse(lit)) continue;
838 if (!helper_->PushLiteral(lit.Negated())) return false;
839 }
840 }
841 } else {
842 // TODO(user): Propagate if possible.
843 VLOG(3) << "Cumulative energy missed propagation";
844 }
845 return true;
846}
847
849 DCHECK_LT(t, demands_.size());
850 if (demands_[t].var != kNoIntegerVariable) {
851 helper_->MutableIntegerReason()->push_back(
852 integer_trail_->LowerBoundAsLiteral(demands_[t].var));
853 }
854}
855
857 IntegerValue min_demand) {
858 DCHECK_LT(t, demands_.size());
859 if (demands_[t].var != kNoIntegerVariable) {
860 helper_->MutableIntegerReason()->push_back(
861 demands_[t].GreaterOrEqual(min_demand));
862 }
863}
864
866 // We prefer these reason in order.
867 const IntegerValue value = cached_energies_min_[t];
868 if (DecomposedEnergyMin(t) >= value) {
869 auto* reason = helper_->MutableLiteralReason();
870 const int old_size = reason->size();
871 for (const auto [lit, fixed_size, fixed_demand] : decomposed_energies_[t]) {
872 if (assignment_.LiteralIsTrue(lit)) {
873 reason->resize(old_size);
874 reason->push_back(lit.Negated());
875 return;
876 } else if (fixed_size * fixed_demand < value &&
877 assignment_.LiteralIsFalse(lit)) {
878 reason->push_back(lit);
879 }
880 }
881 } else if (SimpleEnergyMin(t) >= value) {
883 helper_->AddSizeMinReason(t);
884 }
885}
886
888 int t, LinearConstraintBuilder* builder) const {
889 if (helper_->IsPresent(t)) {
890 if (!decomposed_energies_[t].empty()) {
891 for (const LiteralValueValue& entry : decomposed_energies_[t]) {
892 if (!builder->AddLiteralTerm(entry.literal, entry.right_value)) {
893 return false;
894 }
895 }
896 } else {
897 builder->AddTerm(demands_[t], IntegerValue(1));
898 }
899 } else if (!helper_->IsAbsent(t)) {
900 return builder->AddLiteralTerm(helper_->PresenceLiteral(t), DemandMin(t));
901 }
902 return true;
903}
904
906 int index) {
907 if (decomposed_energies_[index].empty()) return {};
908 if (sat_solver_->CurrentDecisionLevel() == 0) {
909 // CacheAllEnergyValues has already filtered false literals.
910 return decomposed_energies_[index];
911 }
912
913 // Scan and filter false literals.
914 std::vector<LiteralValueValue> result;
915 for (const auto& e : decomposed_energies_[index]) {
916 if (assignment_.LiteralIsFalse(e.literal)) continue;
917 result.push_back(e);
918 }
919 return result;
920}
921
923 const std::vector<std::vector<LiteralValueValue>>& energies) {
924 DCHECK_EQ(energies.size(), helper_->NumTasks());
925 decomposed_energies_ = energies;
926}
927
929 int t, IntegerValue window_start, IntegerValue window_end) {
931 helper_->StartMin(t), helper_->StartMax(t), helper_->EndMin(t),
932 helper_->EndMax(t), helper_->SizeMin(t), DemandMin(t),
933 FilteredDecomposedEnergy(t), window_start, window_end);
934}
935
936// Since we usually ask way less often for the reason, we redo the computation
937// here.
939 int t, IntegerValue window_start, IntegerValue window_end) {
940 const IntegerValue actual_energy_min =
941 EnergyMinInWindow(t, window_start, window_end);
942 if (actual_energy_min == 0) return;
943
944 // Return simple reason right away if there is no decomposition or the simple
945 // energy is enough.
946 const IntegerValue start_max = helper_->StartMax(t);
947 const IntegerValue end_min = helper_->EndMin(t);
948 const IntegerValue min_overlap =
949 helper_->GetMinOverlap(t, window_start, window_end);
950 const IntegerValue simple_energy_min = DemandMin(t) * min_overlap;
951 if (simple_energy_min == actual_energy_min) {
953 helper_->AddSizeMinReason(t);
954 helper_->AddStartMaxReason(t, start_max);
955 helper_->AddEndMinReason(t, end_min);
956 return;
957 }
958
959 // TODO(user): only include the one we need?
960 const IntegerValue start_min = helper_->StartMin(t);
961 const IntegerValue end_max = helper_->EndMax(t);
962 DCHECK(!decomposed_energies_[t].empty());
963 helper_->AddStartMinReason(t, start_min);
964 helper_->AddStartMaxReason(t, start_max);
965 helper_->AddEndMinReason(t, end_min);
966 helper_->AddEndMaxReason(t, end_max);
967
968 auto* literal_reason = helper_->MutableLiteralReason();
969 const int old_size = literal_reason->size();
970
971 DCHECK(!decomposed_energies_[t].empty());
972 for (const auto [lit, fixed_size, fixed_demand] : decomposed_energies_[t]) {
973 // Should be the same in most cases.
974 if (assignment_.LiteralIsTrue(lit)) {
975 literal_reason->resize(old_size);
976 literal_reason->push_back(lit.Negated());
977 return;
978 }
979 if (assignment_.LiteralIsFalse(lit)) {
980 const IntegerValue alt_em = std::max(end_min, start_min + fixed_size);
981 const IntegerValue alt_sm = std::min(start_max, end_max - fixed_size);
982 const IntegerValue energy_min =
983 fixed_demand *
984 std::min({alt_em - window_start, window_end - alt_sm, fixed_size});
985 if (energy_min >= actual_energy_min) continue;
986 literal_reason->push_back(lit);
987 }
988 }
989}
990
992 Model* model,
993 std::vector<IntegerVariable>* vars,
994 int mask) {
995 IntegerEncoder* encoder = model->GetOrCreate<IntegerEncoder>();
996 for (int t = 0; t < helper->NumTasks(); ++t) {
998 helper->Starts()[t].var != kNoIntegerVariable) {
999 vars->push_back(helper->Starts()[t].var);
1000 }
1001 if ((mask & IntegerVariablesToAddMask::kSize) &&
1002 helper->Sizes()[t].var != kNoIntegerVariable) {
1003 vars->push_back(helper->Sizes()[t].var);
1004 }
1005 if ((mask & IntegerVariablesToAddMask::kEnd) &&
1006 helper->Ends()[t].var != kNoIntegerVariable) {
1007 vars->push_back(helper->Ends()[t].var);
1008 }
1010 helper->IsOptional(t) && !helper->IsAbsent(t) &&
1011 !helper->IsPresent(t)) {
1012 const Literal l = helper->PresenceLiteral(t);
1013 IntegerVariable view = kNoIntegerVariable;
1014 if (!encoder->LiteralOrNegationHasView(l, &view)) {
1015 view = model->Add(NewIntegerVariableFromLiteral(l));
1016 }
1017 vars->push_back(view);
1018 }
1019 }
1020}
1021
1023 const AffineExpression& capacity, SchedulingDemandHelper* demands_helper,
1024 Model* model, std::vector<IntegerVariable>* vars) {
1025 auto* integer_trail = model->GetOrCreate<IntegerTrail>();
1026 for (const AffineExpression& demand_expr : demands_helper->Demands()) {
1027 if (!integer_trail->IsFixed(demand_expr)) {
1028 vars->push_back(demand_expr.var);
1029 }
1030 }
1031 IntegerEncoder* encoder = model->GetOrCreate<IntegerEncoder>();
1032 for (const auto& product : demands_helper->DecomposedEnergies()) {
1033 for (const auto& lit_val_val : product) {
1034 IntegerVariable view = kNoIntegerVariable;
1035 if (!encoder->LiteralOrNegationHasView(lit_val_val.literal, &view)) {
1036 view = model->Add(NewIntegerVariableFromLiteral(lit_val_val.literal));
1037 }
1038 vars->push_back(view);
1039 }
1040 }
1041
1042 if (!integer_trail->IsFixed(capacity)) {
1043 vars->push_back(capacity.var);
1044 }
1045}
1046
1047} // namespace sat
1048} // namespace operations_research
void Clear(IndexType i)
Sets the bit at position i to 0.
Definition bitset.h:506
void Resize(IndexType size)
Definition bitset.h:475
void Set(IndexType i)
Sets the bit at position i to 1.
Definition bitset.h:544
void SetPropagatorPriority(int id, int priority)
Definition integer.cc:2353
void WatchIntegerVariable(IntegerVariable i, int id, int watch_index=-1)
Definition integer.h:1460
int Register(PropagatorInterface *propagator)
Registers a propagator and returns its unique ids.
Definition integer.cc:2327
ABSL_MUST_USE_RESULT bool LiteralOrNegationHasView(Literal lit, IntegerVariable *view=nullptr, bool *view_is_direct=nullptr) const
Definition integer.cc:583
IntegerValue LowerBound(IntegerVariable i) const
Returns the current lower/upper bound of the given integer variable.
Definition integer.h:1301
IntegerValue UpperBound(IntegerVariable i) const
Definition integer.h:1305
ABSL_MUST_USE_RESULT bool AddLiteralTerm(Literal lit, IntegerValue coeff=IntegerValue(1))
void AddTerm(IntegerVariable var, IntegerValue coeff)
T Add(std::function< T(Model *)> f)
Definition model.h:87
Helper class to express a product as a linear constraint.
bool IncrementalPropagate(const std::vector< int > &watch_indices) final
void AddReasonForBeingBefore(int before, int after)
Produces a relaxed reason for StartMax(before) < EndMin(after).
void AddStartMaxReason(int t, IntegerValue upper_bound)
ABSL_MUST_USE_RESULT bool IncreaseEndMin(int t, IntegerValue value)
void AddSizeMaxReason(int t, IntegerValue upper_bound)
ABSL_MUST_USE_RESULT bool DecreaseEndMax(int t, IntegerValue value)
int NumTasks() const
Returns the number of task.
absl::Span< const AffineExpression > Sizes() const
const std::vector< ProfileEvent > & GetEnergyProfile()
ABSL_MUST_USE_RESULT bool PushIntegerLiteralIfTaskPresent(int t, IntegerLiteral lit)
absl::Span< const AffineExpression > Starts() const
void AddEndMaxReason(int t, IntegerValue upper_bound)
absl::Span< const AffineExpression > Ends() const
ABSL_MUST_USE_RESULT bool ResetFromSubset(const SchedulingConstraintHelper &other, absl::Span< const int > tasks)
ABSL_MUST_USE_RESULT bool IncreaseStartMin(int t, IntegerValue value)
void AddEndMinReason(int t, IntegerValue lower_bound)
void ClearReason()
Functions to clear and then set the current reason.
SchedulingConstraintHelper(std::vector< AffineExpression > starts, std::vector< AffineExpression > ends, std::vector< AffineExpression > sizes, std::vector< LiteralIndex > reason_for_presence, Model *model)
absl::Span< const CachedTaskBounds > TaskByIncreasingShiftedStartMin()
ABSL_MUST_USE_RESULT bool SynchronizeAndSetTimeDirection(bool is_forward)
std::string TaskDebugString(int t) const
Returns a string with the current task bounds.
ABSL_MUST_USE_RESULT bool PushIntegerLiteral(IntegerLiteral lit)
void ImportOtherReasons(const SchedulingConstraintHelper &other_helper)
IntegerValue GetCurrentMinDistanceBetweenTasks(int a, int b, bool add_reason_if_after=false)
IntegerValue GetMinOverlap(int t, IntegerValue start, IntegerValue end) const
void AddStartMinReason(int t, IntegerValue lower_bound)
std::vector< LiteralValueValue > FilteredDecomposedEnergy(int index)
IntegerValue EnergyMinInWindow(int t, IntegerValue window_start, IntegerValue window_end)
const std::vector< std::vector< LiteralValueValue > > & DecomposedEnergies() const
void AddEnergyMinInWindowReason(int t, IntegerValue window_start, IntegerValue window_end)
SchedulingDemandHelper(absl::Span< const AffineExpression > demands, SchedulingConstraintHelper *helper, Model *model)
void OverrideDecomposedEnergies(const std::vector< std::vector< LiteralValueValue > > &energies)
Visible for testing.
const std::vector< AffineExpression > & Demands() const
ABSL_MUST_USE_RESULT bool DecreaseEnergyMax(int t, IntegerValue value)
ABSL_MUST_USE_RESULT bool AddLinearizedDemand(int t, LinearConstraintBuilder *builder) const
bool LiteralIsFalse(Literal literal) const
Definition sat_base.h:185
bool LiteralIsTrue(Literal literal) const
Definition sat_base.h:188
std::function< IntegerVariable(Model *)> NewIntegerVariableFromLiteral(Literal lit)
Definition integer.h:1532
constexpr IntegerValue kMaxIntegerValue(std::numeric_limits< IntegerValue::ValueType >::max() - 1)
const LiteralIndex kNoLiteralIndex(-1)
std::function< int64_t(const Model &)> LowerBound(IntegerVariable v)
Definition integer.h:1539
std::function< void(Model *)> LowerOrEqual(IntegerVariable v, int64_t ub)
Definition integer.h:1582
std::vector< IntegerVariable > NegationOf(absl::Span< const IntegerVariable > vars)
Returns the vector of the negated variables.
Definition integer.cc:52
constexpr IntegerValue kMinIntegerValue(-kMaxIntegerValue.value())
const IntegerVariable kNoIntegerVariable(-1)
void AddIntegerVariableFromIntervals(const SchedulingConstraintHelper *helper, Model *model, std::vector< IntegerVariable > *vars, int mask)
void AppendVariablesFromCapacityAndDemands(const AffineExpression &capacity, SchedulingDemandHelper *demands_helper, Model *model, std::vector< IntegerVariable > *vars)
void AddWeightedSumLowerOrEqual(absl::Span< const Literal > enforcement_literals, absl::Span< const IntegerVariable > vars, absl::Span< const int64_t > coefficients, int64_t upper_bound, Model *model)
enforcement_literals => sum <= upper_bound
std::function< void(Model *)> GreaterOrEqual(IntegerVariable v, int64_t lb)
Definition integer.h:1567
IntegerValue CapProdI(IntegerValue a, IntegerValue b)
Overflows and saturated arithmetic.
IntegerValue ComputeEnergyMinInWindow(IntegerValue start_min, IntegerValue start_max, IntegerValue end_min, IntegerValue end_max, IntegerValue size_min, IntegerValue demand_min, absl::Span< const LiteralValueValue > filtered_energy, IntegerValue window_start, IntegerValue window_end)
In SWIG mode, we don't want anything besides these top-level includes.
void IncrementalSort(int max_comparisons, Iterator begin, Iterator end, Compare comp=Compare{}, bool is_stable=false)
Definition sort.h:46
STL namespace.
IntegerLiteral Negated() const
The negation of x >= bound is x <= bound - 1.