Google OR-Tools v9.15
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
timetable.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 <vector>
19
20#include "absl/log/check.h"
21#include "absl/types/span.h"
23#include "ortools/sat/integer.h"
25#include "ortools/sat/model.h"
29
30namespace operations_research {
31namespace sat {
32
33void AddReservoirConstraint(absl::Span<const Literal> enforcement_literals,
34 absl::Span<const AffineExpression> times,
35 absl::Span<const AffineExpression> deltas,
36 absl::Span<const Literal> presences,
37 int64_t min_level, int64_t max_level,
38 Model* model) {
39 // We only create a side if it can fail.
40 IntegerValue min_possible(0);
41 IntegerValue max_possible(0);
42 auto* integer_trail = model->GetOrCreate<IntegerTrail>();
43 for (const AffineExpression d : deltas) {
44 min_possible += std::min(IntegerValue(0), integer_trail->LowerBound(d));
45 max_possible += std::max(IntegerValue(0), integer_trail->UpperBound(d));
46 }
47 if (max_possible > max_level) {
48 model->TakeOwnership(
49 new ReservoirTimeTabling(enforcement_literals, times, deltas, presences,
50 IntegerValue(max_level), model));
51 }
52 if (min_possible < min_level) {
53 std::vector<AffineExpression> negated_deltas;
54 for (const AffineExpression& ref : deltas) {
55 negated_deltas.push_back(ref.Negated());
56 }
57 model->TakeOwnership(
58 new ReservoirTimeTabling(enforcement_literals, times, negated_deltas,
59 presences, IntegerValue(-min_level), model));
60 }
61}
62
64 absl::Span<const Literal> enforcement_literals,
65 absl::Span<const AffineExpression> times,
66 absl::Span<const AffineExpression> deltas,
67 absl::Span<const Literal> presences, IntegerValue capacity, Model* model)
68 : enforcement_literals_(enforcement_literals.begin(),
69 enforcement_literals.end()),
70 times_(times.begin(), times.end()),
71 deltas_(deltas.begin(), deltas.end()),
72 presences_(presences.begin(), presences.end()),
73 capacity_(capacity),
74 assignment_(model->GetOrCreate<Trail>()->Assignment()),
75 integer_trail_(*model->GetOrCreate<IntegerTrail>()),
76 enforcement_helper_(*model->GetOrCreate<EnforcementHelper>()) {
77 auto* watcher = model->GetOrCreate<GenericLiteralWatcher>();
78 const int id = watcher->Register(this);
79 const int num_events = times.size();
80 for (int e = 0; e < num_events; e++) {
81 watcher->WatchLowerBound(deltas_[e], id);
82 if (integer_trail_.UpperBound(deltas_[e]) > 0) {
83 watcher->WatchUpperBound(times_[e].var, id);
84 watcher->WatchLiteral(presences_[e], id);
85 }
86 if (integer_trail_.LowerBound(deltas_[e]) < 0) {
87 watcher->WatchLowerBound(times_[e].var, id);
88 watcher->WatchLiteral(presences_[e].Negated(), id);
89 }
90 }
91 watcher->NotifyThatPropagatorMayNotReachFixedPointInOnePass(id);
92 enforcement_id_ =
93 enforcement_helper_.Register(enforcement_literals, watcher, id);
94}
95
97 const EnforcementStatus status = enforcement_helper_.Status(enforcement_id_);
98 if (status == EnforcementStatus::IS_FALSE ||
100 return true;
101 }
102
103 const int num_events = times_.size();
104 if (!BuildProfile()) return false;
105 if (status != EnforcementStatus::IS_ENFORCED) return true;
106 for (int e = 0; e < num_events; e++) {
107 if (assignment_.LiteralIsFalse(presences_[e])) continue;
108
109 // For positive delta_min, we can maybe increase the min.
110 const IntegerValue min_d = integer_trail_.LowerBound(deltas_[e]);
111 if (min_d > 0 && !TryToIncreaseMin(e)) return false;
112
113 // For negative delta_min, we can maybe decrease the max.
114 if (min_d < 0 && !TryToDecreaseMax(e)) return false;
115 }
116 return true;
117}
118
119// We compute the lowest possible profile at time t.
120//
121// TODO(user): If we have precedences between events, we should be able to do
122// more.
123bool ReservoirTimeTabling::BuildProfile() {
124 // Starts by copying the "events" in the profile and sort them by time.
125 profile_.clear();
126 const int num_events = times_.size();
127 profile_.emplace_back(kMinIntegerValue, IntegerValue(0)); // Sentinel.
128 for (int e = 0; e < num_events; e++) {
129 const IntegerValue min_d = integer_trail_.LowerBound(deltas_[e]);
130 if (min_d > 0) {
131 // Only consider present event for positive delta.
132 if (!assignment_.LiteralIsTrue(presences_[e])) continue;
133 const IntegerValue ub = integer_trail_.UpperBound(times_[e]);
134 profile_.push_back({ub, min_d});
135 } else if (min_d < 0) {
136 // Only consider non-absent event for negative delta.
137 if (assignment_.LiteralIsFalse(presences_[e])) continue;
138 profile_.push_back({integer_trail_.LowerBound(times_[e]), min_d});
139 }
140 }
141 profile_.emplace_back(kMaxIntegerValue, IntegerValue(0)); // Sentinel.
142 std::sort(profile_.begin(), profile_.end());
143
144 // Accumulate delta and collapse entries.
145 int last = 0;
146 for (const ProfileRectangle& rect : profile_) {
147 if (rect.start == profile_[last].start) {
148 profile_[last].height += rect.height;
149 } else {
150 ++last;
151 profile_[last].start = rect.start;
152 profile_[last].height = rect.height + profile_[last - 1].height;
153 }
154 }
155 profile_.resize(last + 1);
156
157 // Conflict?
158 const bool is_enforced = enforcement_helper_.Status(enforcement_id_) ==
160 for (const ProfileRectangle& rect : profile_) {
161 if (rect.height <= capacity_) continue;
162
163 FillReasonForProfileAtGivenTime(rect.start);
164 if (is_enforced) {
165 return enforcement_helper_.ReportConflict(
166 enforcement_id_, literal_reason_, integer_reason_);
167 } else {
168 return enforcement_helper_.PropagateWhenFalse(
169 enforcement_id_, literal_reason_, integer_reason_);
170 }
171 }
172
173 return true;
174}
175
176namespace {
177
178void AddLowerOrEqual(const AffineExpression& expr, IntegerValue bound,
179 std::vector<IntegerLiteral>* reason) {
180 if (expr.IsConstant()) return;
181 reason->push_back(expr.LowerOrEqual(bound));
182}
183
184void AddGreaterOrEqual(const AffineExpression& expr, IntegerValue bound,
185 std::vector<IntegerLiteral>* reason) {
186 if (expr.IsConstant()) return;
187 reason->push_back(expr.GreaterOrEqual(bound));
188}
189
190} // namespace
191
192// TODO(user): Minimize with how high the profile needs to be. We can also
193// remove from the reason the absence of a negative event provided that the
194// level zero min of the event is greater than t anyway.
195//
196// TODO(user): Make sure the code work with fixed time since pushing always
197// true/false literal to the reason is not completely supported.
198void ReservoirTimeTabling::FillReasonForProfileAtGivenTime(
199 IntegerValue t, int event_to_ignore) {
200 integer_reason_.clear();
201 literal_reason_.clear();
202 const int num_events = times_.size();
203 for (int e = 0; e < num_events; e++) {
204 if (e == event_to_ignore) continue;
205 const IntegerValue min_d = integer_trail_.LowerBound(deltas_[e]);
206 if (min_d > 0) {
207 if (!assignment_.LiteralIsTrue(presences_[e])) continue;
208 if (integer_trail_.UpperBound(times_[e]) > t) continue;
209 AddGreaterOrEqual(deltas_[e], min_d, &integer_reason_);
210 AddLowerOrEqual(times_[e], t, &integer_reason_);
211 literal_reason_.push_back(presences_[e].Negated());
212 } else if (min_d <= 0) {
213 if (assignment_.LiteralIsFalse(presences_[e])) {
214 literal_reason_.push_back(presences_[e]);
215 continue;
216 }
217 AddGreaterOrEqual(deltas_[e], min_d, &integer_reason_);
218 if (min_d < 0 && integer_trail_.LowerBound(times_[e]) > t) {
219 AddGreaterOrEqual(times_[e], t + 1, &integer_reason_);
220 }
221 }
222 }
223}
224
225// Note that a negative event will always be in the profile, even if its
226// presence is still not settled.
227bool ReservoirTimeTabling::TryToDecreaseMax(int event) {
228 const IntegerValue min_d = integer_trail_.LowerBound(deltas_[event]);
229 CHECK_LT(min_d, 0);
230 const IntegerValue start = integer_trail_.LowerBound(times_[event]);
231 const IntegerValue end = integer_trail_.UpperBound(times_[event]);
232
233 // We already tested for conflict in BuildProfile().
234 if (start == end) return true;
235
236 // Find the profile rectangle that overlaps the start of the given event.
237 // The sentinel prevents out of bound exceptions.
238 DCHECK(std::is_sorted(profile_.begin(), profile_.end()));
239 int rec_id =
240 std::upper_bound(profile_.begin(), profile_.end(), start,
241 [&](IntegerValue value, const ProfileRectangle& rect) {
242 return value < rect.start;
243 }) -
244 profile_.begin();
245 --rec_id;
246
247 bool push = false;
248 IntegerValue new_end = end;
249 for (; profile_[rec_id].start < end; ++rec_id) {
250 if (profile_[rec_id].height - min_d > capacity_) {
251 new_end = profile_[rec_id].start;
252 push = true;
253 break;
254 }
255 }
256 if (!push) return true;
257
258 // The reason is simply why the capacity at new_end (without the event)
259 // would overflow.
260 FillReasonForProfileAtGivenTime(new_end, event);
261
262 // Note(user): I don't think this is possible since it would have been
263 // detected at profile construction, but then, since the bound might have been
264 // updated, better be defensive.
265 if (new_end < start) {
266 AddGreaterOrEqual(times_[event], new_end + 1, &integer_reason_);
267 return enforcement_helper_.ReportConflict(enforcement_id_, literal_reason_,
268 integer_reason_);
269 }
270
271 // First, the task MUST be present, otherwise we have a conflict.
272 //
273 // TODO(user): We actually need to look after 'end' to potentially push the
274 // presence in more situation.
275 if (!assignment_.LiteralIsTrue(presences_[event])) {
276 if (!enforcement_helper_.EnqueueLiteral(enforcement_id_, presences_[event],
277 literal_reason_, integer_reason_)) {
278 return false;
279 }
280 }
281
282 // Push new_end too. Note that we don't need the presence reason.
283 return enforcement_helper_.Enqueue(enforcement_id_,
284 times_[event].LowerOrEqual(new_end),
285 literal_reason_, integer_reason_);
286}
287
288bool ReservoirTimeTabling::TryToIncreaseMin(int event) {
289 const IntegerValue min_d = integer_trail_.LowerBound(deltas_[event]);
290 CHECK_GT(min_d, 0);
291 const IntegerValue start = integer_trail_.LowerBound(times_[event]);
292 const IntegerValue end = integer_trail_.UpperBound(times_[event]);
293
294 // We already tested for conflict in BuildProfile().
295 if (start == end) return true;
296
297 // Find the profile rectangle containing the end of the given event.
298 // The sentinel prevents out of bound exceptions.
299 //
300 // TODO(user): If the task is no present, we should actually look at the
301 // maximum profile after end to maybe push its absence.
302 DCHECK(std::is_sorted(profile_.begin(), profile_.end()));
303 int rec_id =
304 std::upper_bound(profile_.begin(), profile_.end(), end,
305 [&](IntegerValue value, const ProfileRectangle& rect) {
306 return value < rect.start;
307 }) -
308 profile_.begin();
309 --rec_id;
310
311 bool push = false;
312 IntegerValue new_start = start;
313 if (profile_[rec_id].height + min_d > capacity_) {
314 if (!assignment_.LiteralIsTrue(presences_[event])) {
315 // Push to false since it wasn't part of the profile and cannot fit.
316 push = true;
317 new_start = end + 1;
318 } else if (profile_[rec_id].start < end) {
319 // It must be at end in this case.
320 push = true;
321 new_start = end;
322 }
323 }
324 if (!push) {
325 for (; profile_[rec_id].start > start; --rec_id) {
326 if (profile_[rec_id - 1].height + min_d > capacity_) {
327 push = true;
328 new_start = profile_[rec_id].start;
329 break;
330 }
331 }
332 }
333 if (!push) return true;
334
335 // The reason is simply the capacity at new_start - 1;
336 FillReasonForProfileAtGivenTime(new_start - 1, event);
337 AddGreaterOrEqual(deltas_[event], min_d, &integer_reason_);
338 return enforcement_helper_.ConditionalEnqueue(
339 enforcement_id_, presences_[event],
340 times_[event].GreaterOrEqual(new_start), literal_reason_,
341 integer_reason_);
342}
343
346 SchedulingDemandHelper* demands,
347 Model* model)
348 : num_tasks_(helper->NumTasks()),
349 capacity_(capacity),
350 helper_(helper),
351 demands_(demands),
352 integer_trail_(model->GetOrCreate<IntegerTrail>()) {
353 // Each task may create at most two profile rectangles. Such pattern appear if
354 // the profile is shaped like the Hanoi tower. The additional space is for
355 // both extremities and the sentinels.
356 profile_.reserve(2 * num_tasks_ + 4);
357
358 num_profile_tasks_ = 0;
359 profile_tasks_.resize(num_tasks_);
360
361 initial_max_demand_ = IntegerValue(0);
362 const bool capa_is_fixed = integer_trail_->IsFixed(capacity_);
363 const IntegerValue capa_min = integer_trail_->LowerBound(capacity_);
364 for (int t = 0; t < num_tasks_; ++t) {
365 profile_tasks_[t] = t;
366
367 if (capa_is_fixed && demands_->DemandMin(t) >= capa_min) {
368 // TODO(user): This usually correspond to a makespan interval.
369 // We should just detect and propagate it separately as it would result
370 // in a faster propagation.
371 has_demand_equal_to_capacity_ = true;
372 continue;
373 }
374 initial_max_demand_ = std::max(initial_max_demand_, demands_->DemandMax(t));
375 }
376}
377
379 const int id = watcher->Register(this);
380 helper_->WatchAllTasks(id);
381 watcher->WatchUpperBound(capacity_.var, id);
382 for (int t = 0; t < num_tasks_; t++) {
383 watcher->WatchLowerBound(demands_->Demands()[t], id);
384 }
385 watcher->RegisterReversibleInt(id, &num_profile_tasks_);
386
387 // Changing the times or pushing task absence migth have side effects on the
388 // other intervals, so we would need to be called again in this case.
390}
391
392// Note that we rely on being called again to reach a fixed point.
394 if (!helper_->IsEnforced()) return true;
395 // This can fail if the profile exceeds the resource capacity.
396 if (!BuildProfile()) return false;
397
398 // Update the minimum start times.
399 if (!SweepAllTasks()) return false;
400
401 // We reuse the same profile, but reversed, to update the maximum end times.
402 if (!helper_->SynchronizeAndSetTimeDirection(false)) return false;
403 ReverseProfile();
404
405 // Update the maximum end times (reversed problem).
406 if (!SweepAllTasks()) return false;
407
408 return true;
409}
410
411bool TimeTablingPerTask::BuildProfile() {
412 if (!helper_->SynchronizeAndSetTimeDirection(true)) return false;
413
414 // Update the set of tasks that contribute to the profile. Tasks that were
415 // contributing are still part of the profile so we only need to check the
416 // other tasks.
417 for (int i = num_profile_tasks_; i < num_tasks_; ++i) {
418 const int t = profile_tasks_[i];
419 if (helper_->IsPresent(t) && helper_->StartMax(t) < helper_->EndMin(t)) {
420 std::swap(profile_tasks_[i], profile_tasks_[num_profile_tasks_]);
421 num_profile_tasks_++;
422 }
423 }
424
425 // Cache the demand min. If not in profile, it will be zero.
426 cached_demands_min_.assign(num_tasks_, 0);
427 absl::Span<IntegerValue> demands_min = absl::MakeSpan(cached_demands_min_);
428 for (int i = 0; i < num_profile_tasks_; ++i) {
429 const int t = profile_tasks_[i];
430 demands_min[t] = demands_->DemandMin(t);
431 }
432
433 // Build the profile.
434 // ------------------
435 profile_.clear();
436
437 // Start and height of the highest profile rectangle.
438 IntegerValue max_height = 0;
439 IntegerValue max_height_start = kMinIntegerValue;
440
441 // Start and height of the currently built profile rectangle.
442 IntegerValue current_start = kMinIntegerValue;
443 IntegerValue height_at_start = IntegerValue(0);
444 IntegerValue current_height = IntegerValue(0);
445
446 // Any profile height <= relevant_height is not really relevant since nothing
447 // can be pushed. So we artificially put zero or one (if there is a makespan
448 // interval) in the profile instead. This allow to have a lot less
449 // "rectangles" in a profile for exactly the same propagation!
450 const IntegerValue relevant_height =
451 integer_trail_->UpperBound(capacity_) - initial_max_demand_;
452 const IntegerValue default_non_relevant_height =
453 has_demand_equal_to_capacity_ ? 1 : 0;
454
455 const auto& by_negated_start_max = helper_->TaskByIncreasingNegatedStartMax();
456 const auto& by_end_min = helper_->TaskByIncreasingEndMin();
457
458 // Next start/end of the compulsory parts to be processed. Note that only the
459 // task for which IsInProfile() is true must be considered.
460 int next_start = num_tasks_ - 1;
461 int next_end = 0;
462 const int num_tasks = num_tasks_;
463 while (next_end < num_tasks) {
464 IntegerValue time = by_end_min[next_end].time;
465 if (next_start >= 0) {
466 time = std::min(time, -by_negated_start_max[next_start].time);
467 }
468
469 // Process the starting compulsory parts.
470 while (next_start >= 0 && -by_negated_start_max[next_start].time == time) {
471 const int t = by_negated_start_max[next_start].task_index;
472 current_height += demands_min[t];
473 --next_start;
474 }
475
476 // Process the ending compulsory parts.
477 while (next_end < num_tasks && by_end_min[next_end].time == time) {
478 const int t = by_end_min[next_end].task_index;
479 current_height -= demands_min[t];
480 ++next_end;
481 }
482
483 if (current_height > max_height) {
484 max_height = current_height;
485 max_height_start = time;
486 }
487
488 IntegerValue effective_height = current_height;
489 if (effective_height > 0 && effective_height <= relevant_height) {
490 effective_height = default_non_relevant_height;
491 }
492
493 // Insert a new profile rectangle if any.
494 if (effective_height != height_at_start) {
495 profile_.emplace_back(current_start, height_at_start);
496 current_start = time;
497 height_at_start = effective_height;
498 }
499 }
500
501 // Build the last profile rectangle.
502 DCHECK_GE(current_height, 0);
503 profile_.emplace_back(current_start, IntegerValue(0));
504
505 // Add a sentinel to simplify the algorithm.
506 profile_.emplace_back(kMaxIntegerValue, IntegerValue(0));
507
508 // Save max_height.
509 profile_max_height_ = max_height;
510
511 // Increase the capacity variable if required.
512 return IncreaseCapacity(max_height_start, profile_max_height_);
513}
514
515void TimeTablingPerTask::ReverseProfile() {
516 // We keep the sentinels inchanged.
517 std::reverse(profile_.begin() + 1, profile_.end() - 1);
518 for (int i = 1; i + 1 < profile_.size(); ++i) {
519 profile_[i].start = -profile_[i].start;
520 profile_[i].height = profile_[i + 1].height;
521 }
522}
523
524bool TimeTablingPerTask::SweepAllTasks() {
525 // We can start at one since the first sentinel can always be skipped.
526 int profile_index = 1;
527 const IntegerValue capa_max = CapacityMax();
528 for (const auto& [t, time] : helper_->TaskByIncreasingStartMin()) {
529 // TODO(user): On some problem, a big chunk of the time is spend just
530 // checking these conditions below because it requires indirect memory
531 // access to fetch the demand/size/presence/start ...
532 if (helper_->IsAbsent(t)) continue;
533 if (helper_->SizeMin(t) == 0) continue;
534
535 // A profile rectangle is in conflict with the task if its height exceeds
536 // conflict_height.
537 const IntegerValue conflict_height = capa_max - demands_->DemandMin(t);
538
539 // TODO(user): This is never true when we have a makespan interval with
540 // demand equal to the capacity. Find a simple way to detect when there is
541 // no need to scan a task.
542 if (conflict_height >= profile_max_height_) continue;
543
544 if (!SweepTask(t, time, conflict_height, &profile_index)) return false;
545 }
546
547 return true;
548}
549
550bool TimeTablingPerTask::SweepTask(int task_id, IntegerValue initial_start_min,
551 IntegerValue conflict_height,
552 int* profile_index) {
553 const IntegerValue start_max = helper_->StartMax(task_id);
554 const IntegerValue initial_end_min = helper_->EndMin(task_id);
555
556 // Find the profile rectangle that overlaps the minimum start time of task_id.
557 // The sentinel prevents out of bound exceptions.
558 DCHECK(std::is_sorted(profile_.begin(), profile_.end()));
559 while (profile_[*profile_index].start <= initial_start_min) {
560 ++*profile_index;
561 }
562 int rec_id = *profile_index - 1;
563
564 // Last time point during which task_id was in conflict with a profile
565 // rectangle before being pushed.
566 IntegerValue explanation_start_time = kMinIntegerValue;
567
568 // Push the task from left to right until it does not overlap any conflicting
569 // rectangle. Pushing the task may push the end of its compulsory part on the
570 // right but will not change its start. The main loop of the propagator will
571 // take care of rebuilding the profile with these possible changes and to
572 // propagate again in order to reach the timetabling consistency or to fail if
573 // the profile exceeds the resource capacity.
574 //
575 // For optimization purpose we have a separate code if the task is in the
576 // profile or not.
577 IntegerValue new_start_min = initial_start_min;
578 if (IsInProfile(task_id)) {
579 DCHECK_LE(start_max, initial_end_min);
580 for (; profile_[rec_id].start < start_max; ++rec_id) {
581 // If the profile rectangle is not conflicting, go to the next rectangle.
582 if (profile_[rec_id].height <= conflict_height) continue;
583
584 // Compute the next minimum start and end times of task_id. The variables
585 // are not updated yet.
586 new_start_min = profile_[rec_id + 1].start; // i.e. profile_[rec_id].end
587 if (start_max < new_start_min) {
588 // Because the task is part of the profile, we cannot push it further.
589 new_start_min = start_max;
590 explanation_start_time = start_max - 1;
591 break;
592 }
593 explanation_start_time = new_start_min - 1;
594 }
595
596 // Since the task is part of the profile, try to lower its demand max
597 // if possible.
598 const IntegerValue delta =
599 demands_->DemandMax(task_id) - demands_->DemandMin(task_id);
600 if (delta > 0) {
601 const IntegerValue threshold = CapacityMax() - delta;
602 if (profile_[rec_id].start > start_max) --rec_id;
603 for (; profile_[rec_id].start < initial_end_min; ++rec_id) {
604 DCHECK_GT(profile_[rec_id + 1].start, start_max);
605 if (profile_[rec_id].height <= threshold) continue;
606 const IntegerValue new_max = CapacityMax() - profile_[rec_id].height +
607 demands_->DemandMin(task_id);
608
609 // Note that the task_id is already part of the profile reason, so
610 // there is nothing else needed.
611 helper_->ResetReason();
612 const IntegerValue time = std::max(start_max, profile_[rec_id].start);
613 AddProfileReason(task_id, time, time + 1, CapacityMax());
614 if (!helper_->PushIntegerLiteralIfTaskPresent(
615 task_id, demands_->Demands()[task_id].LowerOrEqual(new_max))) {
616 return false;
617 }
618 }
619 }
620 } else {
621 IntegerValue limit = initial_end_min;
622 const IntegerValue size_min = helper_->SizeMin(task_id);
623 for (; profile_[rec_id].start < limit; ++rec_id) {
624 // If the profile rectangle is not conflicting, go to the next rectangle.
625 if (profile_[rec_id].height <= conflict_height) continue;
626
627 // Compute the next minimum start and end times of task_id. The variables
628 // are not updated yet.
629 new_start_min = profile_[rec_id + 1].start; // i.e. profile_[rec_id].end
630 limit = std::max(limit, new_start_min + size_min);
631 if (profile_[rec_id].start < initial_end_min) {
632 explanation_start_time = std::min(new_start_min, initial_end_min) - 1;
633 }
634
635 if (new_start_min > start_max) {
636 // We have a conflict. We optimize the reason a bit.
637 new_start_min = std::max(profile_[rec_id].start + 1, start_max + 1);
638 explanation_start_time =
639 std::min(explanation_start_time, new_start_min - 1);
640 break;
641 }
642 }
643 }
644
645 if (new_start_min == initial_start_min) return true;
646 return UpdateStartingTime(task_id, explanation_start_time, new_start_min);
647}
648
649bool TimeTablingPerTask::UpdateStartingTime(int task_id, IntegerValue left,
650 IntegerValue right) {
651 DCHECK_LT(left, right);
652 helper_->ResetReason();
653 AddProfileReason(task_id, left, right,
654 CapacityMax() - demands_->DemandMin(task_id));
655 if (capacity_.var != kNoIntegerVariable) {
656 helper_->AddIntegerReason(
657 integer_trail_->UpperBoundAsLiteral(capacity_.var));
658 }
659
660 // State of the task to be pushed.
661 helper_->AddEndMinReason(task_id, left + 1);
662 helper_->AddSizeMinReason(task_id);
663 demands_->AddDemandMinReason(task_id);
664
665 // Explain the increase of the minimum start and end times.
666 return helper_->IncreaseStartMin(task_id, right);
667}
668
669// TODO(user): there is more room for improvements in the reason.
670// Note that compared to the "easiest" reason (mode == 2) this doesn't seems
671// to help much. Still the more relaxed the reason, the better it should be.
672void TimeTablingPerTask::AddProfileReason(int task_id, IntegerValue left,
673 IntegerValue right,
674 IntegerValue capacity_threshold) {
675 IntegerValue sum_of_demand(0);
676
677 // We optimize a bit the reason depending on the case.
678 int mode;
679 DCHECK_GT(right, left);
680 DCHECK(task_id >= 0 || left + 1 == right);
681 if (left + 1 == right) {
682 // Here we can easily remove extra tasks if the demand is already high
683 // enough.
684 mode = 0;
685 } else if (right - left < helper_->SizeMin(task_id) + 2) {
686 // In this case, only the profile in [left, left + 1) and [right - 1, right)
687 // is enough to push the task. We don't care about what happen in the middle
688 // since the task will not fit.
689 mode = 1;
690 } else {
691 mode = 2;
692 }
693
694 for (int i = 0; i < num_profile_tasks_; ++i) {
695 const int t = profile_tasks_[i];
696
697 // Do not consider the task if it does not overlap for sure (left, right).
698 const IntegerValue start_max = helper_->StartMax(t);
699 if (right <= start_max) continue;
700 const IntegerValue end_min = helper_->EndMin(t);
701 if (end_min <= left) continue;
702
703 helper_->AddPresenceReason(t);
704
705 // Note that we exclude the demand min for the task we push.
706 // If we push the demand_max, we don't need it. And otherwise the task_id
707 // shouldn't be part of the profile anyway.
708 if (t != task_id) demands_->AddDemandMinReason(t);
709
710 if (mode == 0) {
711 helper_->AddStartMaxReason(t, left);
712 helper_->AddEndMinReason(t, right);
713
714 // No need to include more tasks if we have enough demand already.
715 //
716 // TODO(user): Improve what task we "exclude" instead of always taking
717 // the last ones? Note however that profile_tasks_ should be in order in
718 // which task have a mandatory part.
719 sum_of_demand += demands_->DemandMin(t);
720 if (sum_of_demand > capacity_threshold) break;
721 } else if (mode == 1) {
722 helper_->AddStartMaxReason(t, start_max <= left ? left : right - 1);
723 helper_->AddEndMinReason(t, end_min >= right ? right : left + 1);
724 } else {
725 helper_->AddStartMaxReason(t, std::max(left, start_max));
726 helper_->AddEndMinReason(t, std::min(right, end_min));
727 }
728 }
729}
730
731bool TimeTablingPerTask::IncreaseCapacity(IntegerValue time,
732 IntegerValue new_min) {
733 if (new_min <= CapacityMin()) return true;
734
735 // No need to push further than this. It will result in a conflict anyway,
736 // and we can optimize the reason a bit.
737 new_min = std::min(CapacityMax() + 1, new_min);
738
739 helper_->ResetReason();
740 AddProfileReason(-1, time, time + 1, new_min);
741 if (capacity_.var == kNoIntegerVariable) {
742 return helper_->ReportConflict();
743 }
744 return helper_->PushIntegerLiteral(capacity_.GreaterOrEqual(new_min));
745}
746
747} // namespace sat
748} // namespace operations_research
void WatchUpperBound(IntegerVariable var, int id, int watch_index=-1)
Definition integer.h:1734
void WatchLowerBound(IntegerVariable var, int id, int watch_index=-1)
Definition integer.h:1716
int Register(PropagatorInterface *propagator)
Definition integer.cc:2674
IntegerValue LowerBound(IntegerVariable i) const
Definition integer.h:1537
IntegerValue UpperBound(IntegerVariable i) const
Definition integer.h:1541
ReservoirTimeTabling(absl::Span< const Literal > enforcement_literals, absl::Span< const AffineExpression > times, absl::Span< const AffineExpression > deltas, absl::Span< const Literal > presences, IntegerValue capacity, Model *model)
Definition timetable.cc:63
ABSL_MUST_USE_RESULT bool SynchronizeAndSetTimeDirection(bool is_forward)
void RegisterWith(GenericLiteralWatcher *watcher)
Definition timetable.cc:378
TimeTablingPerTask(AffineExpression capacity, SchedulingConstraintHelper *helper, SchedulingDemandHelper *demands, Model *model)
Definition timetable.cc:344
bool LiteralIsFalse(Literal literal) const
Definition sat_base.h:203
bool LiteralIsTrue(Literal literal) const
Definition sat_base.h:206
constexpr IntegerValue kMaxIntegerValue(std::numeric_limits< IntegerValue::ValueType >::max() - 1)
std::function< void(Model *)> LowerOrEqual(IntegerVariable v, int64_t ub)
Definition integer.h:1862
constexpr IntegerValue kMinIntegerValue(-kMaxIntegerValue.value())
const IntegerVariable kNoIntegerVariable(-1)
void AddReservoirConstraint(absl::Span< const Literal > enforcement_literals, absl::Span< const AffineExpression > times, absl::Span< const AffineExpression > deltas, absl::Span< const Literal > presences, int64_t min_level, int64_t max_level, Model *model)
Definition timetable.cc:33
std::function< void(Model *)> GreaterOrEqual(IntegerVariable v, int64_t lb)
Definition integer.h:1847
OR-Tools root namespace.
ClosedInterval::Iterator end(ClosedInterval interval)
ClosedInterval::Iterator begin(ClosedInterval interval)