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