OR-Tools  9.6
timetable.cc
Go to the documentation of this file.
1 // Copyright 2010-2022 Google LLC
2 // Licensed under the Apache License, Version 2.0 (the "License");
3 // you may not use this file except in compliance with the License.
4 // You may obtain a copy of the License at
5 //
6 // http://www.apache.org/licenses/LICENSE-2.0
7 //
8 // Unless required by applicable law or agreed to in writing, software
9 // distributed under the License is distributed on an "AS IS" BASIS,
10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 // See the License for the specific language governing permissions and
12 // limitations under the License.
13 
14 #include "ortools/sat/timetable.h"
15 
16 #include <algorithm>
17 #include <cstdint>
18 #include <utility>
19 #include <vector>
20 
21 #include "ortools/base/logging.h"
22 #include "ortools/sat/integer.h"
23 #include "ortools/sat/intervals.h"
24 #include "ortools/sat/model.h"
25 #include "ortools/sat/sat_base.h"
28 
29 namespace operations_research {
30 namespace sat {
31 
32 void 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) {
45  model->TakeOwnership(new ReservoirTimeTabling(
46  times, deltas, presences, IntegerValue(max_level), model));
47  }
48  if (min_possible < min_level) {
49  for (AffineExpression& ref : deltas) ref = ref.Negated();
50  model->TakeOwnership(new ReservoirTimeTabling(
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.
102 bool 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 
147 namespace {
148 
149 void 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 
155 void 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.
169 void 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.
198 bool 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 
255 bool 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  positions_in_profile_tasks_.resize(num_tasks_);
327 
328  initial_max_demand_ = IntegerValue(0);
329  const bool capa_is_fixed = integer_trail_->IsFixed(capacity_);
330  const IntegerValue capa_min = integer_trail_->LowerBound(capacity_);
331  for (int t = 0; t < num_tasks_; ++t) {
332  profile_tasks_[t] = t;
333  positions_in_profile_tasks_[t] = t;
334 
335  if (capa_is_fixed && demands_->DemandMin(t) >= capa_min) {
336  // TODO(user): This usually correspond to a makespan interval.
337  // We should just detect and propagate it separately as it would result
338  // in a faster propagation.
339  has_demand_equal_to_capacity_ = true;
340  continue;
341  }
342  initial_max_demand_ = std::max(initial_max_demand_, demands_->DemandMax(t));
343  }
344 }
345 
347  const int id = watcher->Register(this);
348  helper_->WatchAllTasks(id, watcher);
349  watcher->WatchUpperBound(capacity_.var, id);
350  for (int t = 0; t < num_tasks_; t++) {
351  watcher->WatchLowerBound(demands_->Demands()[t], id);
352  }
353  watcher->RegisterReversibleInt(id, &num_profile_tasks_);
354 
355  // Changing the times or pushing task absence migth have side effects on the
356  // other intervals, so we would need to be called again in this case.
358 }
359 
360 // Note that we relly on being called again to reach a fixed point.
362  // This can fail if the profile exceeds the resource capacity.
363  if (!BuildProfile()) return false;
364 
365  // Update the minimum start times.
366  if (!SweepAllTasks()) return false;
367 
368  // We reuse the same profile, but reversed, to update the maximum end times.
369  if (!helper_->SynchronizeAndSetTimeDirection(false)) return false;
370  ReverseProfile();
371 
372  // Update the maximum end times (reversed problem).
373  if (!SweepAllTasks()) return false;
374 
375  return true;
376 }
377 
378 bool TimeTablingPerTask::BuildProfile() {
379  if (!helper_->SynchronizeAndSetTimeDirection(true)) return false;
380 
381  // Update the set of tasks that contribute to the profile. Tasks that were
382  // contributing are still part of the profile so we only need to check the
383  // other tasks.
384  for (int i = num_profile_tasks_; i < num_tasks_; ++i) {
385  const int t1 = profile_tasks_[i];
386  if (helper_->IsPresent(t1) && helper_->StartMax(t1) < helper_->EndMin(t1)) {
387  // Swap values and positions.
388  const int t2 = profile_tasks_[num_profile_tasks_];
389  profile_tasks_[i] = t2;
390  profile_tasks_[num_profile_tasks_] = t1;
391  positions_in_profile_tasks_[t1] = num_profile_tasks_;
392  positions_in_profile_tasks_[t2] = i;
393  num_profile_tasks_++;
394  }
395  }
396 
397  const auto& by_decreasing_start_max = helper_->TaskByDecreasingStartMax();
398  const auto& by_end_min = helper_->TaskByIncreasingEndMin();
399 
400  // Build the profile.
401  // ------------------
402  profile_.clear();
403 
404  // Start and height of the highest profile rectangle.
405  profile_max_height_ = 0;
406  IntegerValue max_height_start = kMinIntegerValue;
407 
408  // Start and height of the currently built profile rectangle.
409  IntegerValue current_start = kMinIntegerValue;
410  IntegerValue height_at_start = IntegerValue(0);
411  IntegerValue current_height = IntegerValue(0);
412 
413  // Any profile height <= relevant_height is not really relevant since nothing
414  // can be pushed. So we artificially put zero or one (if there is a makespan
415  // interval) in the profile instead. This allow to have a lot less
416  // "rectangles" in a profile for exactly the same propagation!
417  const IntegerValue relevant_height =
418  integer_trail_->UpperBound(capacity_) - initial_max_demand_;
419 
420  // Next start/end of the compulsory parts to be processed. Note that only the
421  // task for which IsInProfile() is true must be considered.
422  int next_start = num_tasks_ - 1;
423  int next_end = 0;
424  while (next_end < num_tasks_) {
425  IntegerValue time = by_end_min[next_end].time;
426  if (next_start >= 0) {
427  time = std::min(time, by_decreasing_start_max[next_start].time);
428  }
429 
430  // Process the starting compulsory parts.
431  while (next_start >= 0 &&
432  by_decreasing_start_max[next_start].time == time) {
433  const int t = by_decreasing_start_max[next_start].task_index;
434  if (IsInProfile(t)) current_height += demands_->DemandMin(t);
435  --next_start;
436  }
437 
438  // Process the ending compulsory parts.
439  while (next_end < num_tasks_ && by_end_min[next_end].time == time) {
440  const int t = by_end_min[next_end].task_index;
441  if (IsInProfile(t)) current_height -= demands_->DemandMin(t);
442  ++next_end;
443  }
444 
445  if (current_height > profile_max_height_) {
446  profile_max_height_ = current_height;
447  max_height_start = time;
448  }
449 
450  IntegerValue effective_height = current_height;
451  if (effective_height > 0 && effective_height <= relevant_height) {
452  effective_height = IntegerValue(has_demand_equal_to_capacity_ ? 1 : 0);
453  }
454 
455  // Insert a new profile rectangle if any.
456  if (effective_height != height_at_start) {
457  profile_.emplace_back(current_start, height_at_start);
458  current_start = time;
459  height_at_start = effective_height;
460  }
461  }
462 
463  // Build the last profile rectangle.
464  DCHECK_GE(current_height, 0);
465  profile_.emplace_back(current_start, IntegerValue(0));
466 
467  // Add a sentinel to simplify the algorithm.
468  profile_.emplace_back(kMaxIntegerValue, IntegerValue(0));
469 
470  // Increase the capacity variable if required.
471  return IncreaseCapacity(max_height_start, profile_max_height_);
472 }
473 
474 void TimeTablingPerTask::ReverseProfile() {
475  // We keep the sentinels inchanged.
476  std::reverse(profile_.begin() + 1, profile_.end() - 1);
477  for (int i = 1; i + 1 < profile_.size(); ++i) {
478  profile_[i].start = -profile_[i].start;
479  profile_[i].height = profile_[i + 1].height;
480  }
481 }
482 
483 bool TimeTablingPerTask::SweepAllTasks() {
484  // We can start at one since the first sentinel can always be skipped.
485  int profile_index = 1;
486  const IntegerValue capa_max = CapacityMax();
487  for (const auto& [t, time] : helper_->TaskByIncreasingStartMin()) {
488  // TODO(user): On some problem, a big chunk of the time is spend just
489  // checking these conditions below because it requires indirect memory
490  // access to fetch the demand/size/presence/start ...
491  if (helper_->IsAbsent(t)) continue;
492  if (helper_->SizeMin(t) == 0) continue;
493 
494  // A profile rectangle is in conflict with the task if its height exceeds
495  // conflict_height.
496  const IntegerValue conflict_height = capa_max - demands_->DemandMin(t);
497 
498  // TODO(user): This is never true when we have a makespan interval with
499  // demand equal to the capacity. Find a simple way to detect when there is
500  // no need to scan a task.
501  if (conflict_height >= profile_max_height_) continue;
502 
503  if (!SweepTask(t, time, conflict_height, &profile_index)) return false;
504  }
505 
506  return true;
507 }
508 
509 bool TimeTablingPerTask::SweepTask(int task_id, IntegerValue initial_start_min,
510  IntegerValue conflict_height,
511  int* profile_index) {
512  const IntegerValue start_max = helper_->StartMax(task_id);
513  const IntegerValue initial_end_min = helper_->EndMin(task_id);
514 
515  // Find the profile rectangle that overlaps the minimum start time of task_id.
516  // The sentinel prevents out of bound exceptions.
517  DCHECK(std::is_sorted(profile_.begin(), profile_.end()));
518  while (profile_[*profile_index].start <= initial_start_min) {
519  ++*profile_index;
520  }
521  int rec_id = *profile_index - 1;
522 
523  // Last time point during which task_id was in conflict with a profile
524  // rectangle before being pushed.
525  IntegerValue explanation_start_time = kMinIntegerValue;
526 
527  // Push the task from left to right until it does not overlap any conflicting
528  // rectangle. Pushing the task may push the end of its compulsory part on the
529  // right but will not change its start. The main loop of the propagator will
530  // take care of rebuilding the profile with these possible changes and to
531  // propagate again in order to reach the timetabling consistency or to fail if
532  // the profile exceeds the resource capacity.
533  //
534  // For optimization purpose we have a separate code if the task is in the
535  // profile or not.
536  IntegerValue new_start_min = initial_start_min;
537  if (IsInProfile(task_id)) {
538  DCHECK_LE(start_max, initial_end_min);
539  for (; profile_[rec_id].start < start_max; ++rec_id) {
540  // If the profile rectangle is not conflicting, go to the next rectangle.
541  if (profile_[rec_id].height <= conflict_height) continue;
542 
543  // Compute the next minimum start and end times of task_id. The variables
544  // are not updated yet.
545  new_start_min = profile_[rec_id + 1].start; // i.e. profile_[rec_id].end
546  if (start_max < new_start_min) {
547  // Because the task is part of the profile, we cannot push it further.
548  new_start_min = start_max;
549  explanation_start_time = start_max - 1;
550  break;
551  }
552  explanation_start_time = new_start_min - 1;
553  }
554 
555  // Since the task is part of the profile, try to lower its demand max
556  // if possible.
557  const IntegerValue delta =
558  demands_->DemandMax(task_id) - demands_->DemandMin(task_id);
559  if (delta > 0) {
560  const IntegerValue threshold = CapacityMax() - delta;
561  if (profile_[rec_id].start > start_max) --rec_id;
562  for (; profile_[rec_id].start < initial_end_min; ++rec_id) {
563  DCHECK_GT(profile_[rec_id + 1].start, start_max);
564  if (profile_[rec_id].height <= threshold) continue;
565  const IntegerValue new_max = CapacityMax() - profile_[rec_id].height +
566  demands_->DemandMin(task_id);
567 
568  // Note that the task_id is already part of the profile reason, so
569  // there is nothing else needed.
570  helper_->ClearReason();
571  const IntegerValue time = std::max(start_max, profile_[rec_id].start);
572  AddProfileReason(task_id, time, time + 1, CapacityMax());
573  if (!helper_->PushIntegerLiteralIfTaskPresent(
574  task_id, demands_->Demands()[task_id].LowerOrEqual(new_max))) {
575  return false;
576  }
577  }
578  }
579  } else {
580  IntegerValue limit = initial_end_min;
581  const IntegerValue size_min = helper_->SizeMin(task_id);
582  for (; profile_[rec_id].start < limit; ++rec_id) {
583  // If the profile rectangle is not conflicting, go to the next rectangle.
584  if (profile_[rec_id].height <= conflict_height) continue;
585 
586  // Compute the next minimum start and end times of task_id. The variables
587  // are not updated yet.
588  new_start_min = profile_[rec_id + 1].start; // i.e. profile_[rec_id].end
589  limit = std::max(limit, new_start_min + size_min);
590  if (profile_[rec_id].start < initial_end_min) {
591  explanation_start_time = std::min(new_start_min, initial_end_min) - 1;
592  }
593 
594  if (new_start_min > start_max) {
595  // We have a conflict. We optimize the reason a bit.
596  new_start_min = std::max(profile_[rec_id].start + 1, start_max + 1);
597  explanation_start_time =
598  std::min(explanation_start_time, new_start_min - 1);
599  break;
600  }
601  }
602  }
603 
604  if (new_start_min == initial_start_min) return true;
605  return UpdateStartingTime(task_id, explanation_start_time, new_start_min);
606 }
607 
608 bool TimeTablingPerTask::UpdateStartingTime(int task_id, IntegerValue left,
609  IntegerValue right) {
610  DCHECK_LT(left, right);
611  helper_->ClearReason();
612  AddProfileReason(task_id, left, right,
613  CapacityMax() - demands_->DemandMin(task_id));
614  if (capacity_.var != kNoIntegerVariable) {
615  helper_->MutableIntegerReason()->push_back(
616  integer_trail_->UpperBoundAsLiteral(capacity_.var));
617  }
618 
619  // State of the task to be pushed.
620  helper_->AddEndMinReason(task_id, left + 1);
621  helper_->AddSizeMinReason(task_id);
622  demands_->AddDemandMinReason(task_id);
623 
624  // Explain the increase of the minimum start and end times.
625  return helper_->IncreaseStartMin(task_id, right);
626 }
627 
628 // TODO(user): there is more room for improvements in the reason.
629 // Note that compared to the "easiest" reason (mode == 2) this doesn't seems
630 // to help much. Still the more relaxed the reason, the better it should be.
631 void TimeTablingPerTask::AddProfileReason(int task_id, IntegerValue left,
632  IntegerValue right,
633  IntegerValue capacity_threshold) {
634  IntegerValue sum_of_demand(0);
635 
636  // We optimize a bit the reason depending on the case.
637  int mode;
638  DCHECK_GT(right, left);
639  DCHECK(task_id >= 0 || left + 1 == right);
640  if (left + 1 == right) {
641  // Here we can easily remove extra tasks if the demand is already high
642  // enough.
643  mode = 0;
644  } else if (right - left < helper_->SizeMin(task_id) + 2) {
645  // In this case, only the profile in [left, left + 1) and [right - 1, right)
646  // is enough to push the task. We don't care about what happen in the middle
647  // since the task will not fit.
648  mode = 1;
649  } else {
650  mode = 2;
651  }
652 
653  for (int i = 0; i < num_profile_tasks_; ++i) {
654  const int t = profile_tasks_[i];
655 
656  // Do not consider the task if it does not overlap for sure (left, right).
657  const IntegerValue start_max = helper_->StartMax(t);
658  if (right <= start_max) continue;
659  const IntegerValue end_min = helper_->EndMin(t);
660  if (end_min <= left) continue;
661 
662  helper_->AddPresenceReason(t);
663 
664  // Note that we exclude the demand min for the task we push.
665  // If we push the demand_max, we don't need it. And otherwise the task_id
666  // shouldn't be part of the profile anyway.
667  if (t != task_id) demands_->AddDemandMinReason(t);
668 
669  if (mode == 0) {
670  helper_->AddStartMaxReason(t, left);
671  helper_->AddEndMinReason(t, right);
672 
673  // No need to include more tasks if we have enough demand already.
674  //
675  // TODO(user): Improve what task we "exclude" instead of always taking
676  // the last ones? Note however that profile_tasks_ should be in order in
677  // which task have a mandatory part.
678  sum_of_demand += demands_->DemandMin(t);
679  if (sum_of_demand > capacity_threshold) break;
680  } else if (mode == 1) {
681  helper_->AddStartMaxReason(t, start_max <= left ? left : right - 1);
682  helper_->AddEndMinReason(t, end_min >= right ? right : left + 1);
683  } else {
684  helper_->AddStartMaxReason(t, std::max(left, start_max));
685  helper_->AddEndMinReason(t, std::min(right, end_min));
686  }
687  }
688 }
689 
690 bool TimeTablingPerTask::IncreaseCapacity(IntegerValue time,
691  IntegerValue new_min) {
692  if (new_min <= CapacityMin()) return true;
693 
694  // No need to push further than this. It will result in a conflict anyway,
695  // and we can optimize the reason a bit.
696  new_min = std::min(CapacityMax() + 1, new_min);
697 
698  helper_->ClearReason();
699  AddProfileReason(-1, time, time + 1, new_min);
700  if (capacity_.var == kNoIntegerVariable) {
701  return helper_->ReportConflict();
702  }
703  return helper_->PushIntegerLiteral(capacity_.GreaterOrEqual(new_min));
704 }
705 
706 } // namespace sat
707 } // namespace operations_research
int64_t max
Definition: alldiff_cst.cc:140
int64_t min
Definition: alldiff_cst.cc:139
An Assignment is a variable -> domains mapping, used to report solutions to the user.
void WatchLowerBound(IntegerVariable var, int id, int watch_index=-1)
Definition: integer.h:1681
void WatchUpperBound(IntegerVariable var, int id, int watch_index=-1)
Definition: integer.h:1699
int Register(PropagatorInterface *propagator)
Definition: integer.cc:2286
ABSL_MUST_USE_RESULT bool Enqueue(IntegerLiteral i_lit, absl::Span< const Literal > literal_reason, absl::Span< const IntegerLiteral > integer_reason)
Definition: integer.cc:1228
bool IsFixed(IntegerVariable i) const
Definition: integer.h:1565
bool ReportConflict(absl::Span< const Literal > literal_reason, absl::Span< const IntegerLiteral > integer_reason)
Definition: integer.h:1004
void EnqueueLiteral(Literal literal, absl::Span< const Literal > literal_reason, absl::Span< const IntegerLiteral > integer_reason)
Definition: integer.cc:1387
IntegerValue UpperBound(IntegerVariable i) const
Definition: integer.h:1561
IntegerValue LowerBound(IntegerVariable i) const
Definition: integer.h:1557
IntegerLiteral UpperBoundAsLiteral(IntegerVariable i) const
Definition: integer.h:1594
ABSL_MUST_USE_RESULT bool ConditionalEnqueue(Literal lit, IntegerLiteral i_lit, std::vector< Literal > *literal_reason, std::vector< IntegerLiteral > *integer_reason)
Definition: integer.cc:1235
Class that owns everything related to a particular optimization model.
Definition: sat/model.h:42
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 PushIntegerLiteral(IntegerLiteral lit)
Definition: intervals.cc:496
ABSL_MUST_USE_RESULT bool IncreaseStartMin(int t, IntegerValue value)
Definition: intervals.cc:523
const std::vector< TaskTime > & TaskByIncreasingStartMin()
Definition: intervals.cc:349
void WatchAllTasks(int id, GenericLiteralWatcher *watcher, bool watch_start_max=true, bool watch_end_max=true) const
Definition: intervals.cc:589
const std::vector< TaskTime > & TaskByIncreasingEndMin()
Definition: intervals.cc:361
std::vector< IntegerLiteral > * MutableIntegerReason()
Definition: intervals.h:348
ABSL_MUST_USE_RESULT bool PushIntegerLiteralIfTaskPresent(int t, IntegerLiteral lit)
Definition: intervals.cc:501
void AddEndMinReason(int t, IntegerValue lower_bound)
Definition: intervals.h:736
ABSL_MUST_USE_RESULT bool SynchronizeAndSetTimeDirection(bool is_forward)
Definition: intervals.cc:330
const std::vector< TaskTime > & TaskByDecreasingStartMax()
Definition: intervals.cc:373
void AddStartMaxReason(int t, IntegerValue upper_bound)
Definition: intervals.h:729
const std::vector< AffineExpression > & Demands() const
Definition: intervals.h:521
void RegisterWith(GenericLiteralWatcher *watcher)
Definition: timetable.cc:346
TimeTablingPerTask(AffineExpression capacity, SchedulingConstraintHelper *helper, SchedulingDemandHelper *demands, Model *model)
Definition: timetable.cc:310
bool LiteralIsTrue(Literal literal) const
Definition: sat_base.h:164
bool LiteralIsFalse(Literal literal) const
Definition: sat_base.h:161
int64_t height
int64_t value
IntVar * var
Definition: expr_array.cc:1874
GRBmodel * model
std::function< void(Model *)> GreaterOrEqual(IntegerVariable v, int64_t lb)
Definition: integer.h:1803
constexpr IntegerValue kMaxIntegerValue(std::numeric_limits< IntegerValue::ValueType >::max() - 1)
constexpr IntegerValue kMinIntegerValue(-kMaxIntegerValue.value())
const IntegerVariable kNoIntegerVariable(-1)
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
std::function< void(Model *)> LowerOrEqual(IntegerVariable v, int64_t ub)
Definition: integer.h:1818
std::function< int64_t(const Model &)> LowerBound(IntegerVariable v)
Definition: integer.h:1775
Collection of objects used to extend the Constraint Solver library.
int64_t time
Definition: resource.cc:1694
int64_t delta
Definition: resource.cc:1695
IntVar * upper_bound
Definition: routing.cc:1087
int64_t bound
int64_t capacity
Rev< int64_t > start_max
Rev< int64_t > end_min
std::optional< int64_t > end
int64_t start
IntegerLiteral GreaterOrEqual(IntegerValue bound) const
Definition: integer.h:1528