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