OR-Tools  9.6
timetable_edgefinding.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 
15 
16 #include <algorithm>
17 #include <cstdint>
18 #include <vector>
19 
21 #include "ortools/base/logging.h"
23 #include "ortools/sat/integer.h"
24 #include "ortools/sat/intervals.h"
26 
27 namespace operations_research {
28 namespace sat {
29 
32  SchedulingDemandHelper* demands,
33  Model* model)
34  : num_tasks_(helper->NumTasks()),
35  capacity_(capacity),
36  helper_(helper),
37  demands_(demands),
38  integer_trail_(model->GetOrCreate<IntegerTrail>()) {
39  // Edge finding structures.
40  mandatory_energy_before_end_max_.resize(num_tasks_);
41  mandatory_energy_before_start_min_.resize(num_tasks_);
42 
43  // Energy of free parts.
44  size_free_.resize(num_tasks_);
45  energy_free_.resize(num_tasks_);
46 }
47 
49  const int id = watcher->Register(this);
50  watcher->WatchUpperBound(capacity_, id);
51  helper_->WatchAllTasks(id, watcher);
52  for (int t = 0; t < num_tasks_; t++) {
53  watcher->WatchLowerBound(demands_->Demands()[t], id);
54  }
55  watcher->SetPropagatorPriority(id, 3);
57 }
58 
60  if (!helper_->SynchronizeAndSetTimeDirection(true)) return false;
61  if (!TimeTableEdgeFindingPass()) return false;
62  if (!helper_->SynchronizeAndSetTimeDirection(false)) return false;
63  if (!TimeTableEdgeFindingPass()) return false;
64  return true;
65 }
66 
67 void TimeTableEdgeFinding::BuildTimeTable() {
68  scp_.clear();
69  ecp_.clear();
70 
71  // Build start of compulsory part events.
72  for (const auto task_time :
74  const int t = task_time.task_index;
75  if (!helper_->IsPresent(t)) continue;
76  if (task_time.time < helper_->EndMin(t)) {
77  scp_.push_back(task_time);
78  }
79  }
80 
81  // Build end of compulsory part events.
82  for (const auto task_time : helper_->TaskByIncreasingEndMin()) {
83  const int t = task_time.task_index;
84  if (!helper_->IsPresent(t)) continue;
85  if (helper_->StartMax(t) < task_time.time) {
86  ecp_.push_back(task_time);
87  }
88  }
89 
90  DCHECK_EQ(scp_.size(), ecp_.size());
91 
92  const std::vector<TaskTime>& by_decreasing_end_max =
93  helper_->TaskByDecreasingEndMax();
94  const std::vector<TaskTime>& by_start_min =
95  helper_->TaskByIncreasingStartMin();
96 
97  IntegerValue height = IntegerValue(0);
98  IntegerValue energy = IntegerValue(0);
99 
100  // We don't care since at the beginning heigh is zero, and previous_time will
101  // be correct after the first iteration.
102  IntegerValue previous_time = IntegerValue(0);
103 
104  int index_scp = 0; // index of the next value in scp
105  int index_ecp = 0; // index of the next value in ecp
106  int index_smin = 0; // index of the next value in by_start_min_
107  int index_emax = num_tasks_ - 1; // index of the next value in by_end_max_
108 
109  while (index_emax >= 0) {
110  // Next time point.
111  IntegerValue time = by_decreasing_end_max[index_emax].time;
112  if (index_smin < num_tasks_) {
113  time = std::min(time, by_start_min[index_smin].time);
114  }
115  if (index_scp < scp_.size()) {
116  time = std::min(time, scp_[index_scp].time);
117  }
118  if (index_ecp < ecp_.size()) {
119  time = std::min(time, ecp_[index_ecp].time);
120  }
121 
122  // Total amount of energy contained in the timetable until time.
123  energy += (time - previous_time) * height;
124  previous_time = time;
125 
126  // Store the energy contained in the timetable just before those events.
127  while (index_smin < num_tasks_ && by_start_min[index_smin].time == time) {
128  mandatory_energy_before_start_min_[by_start_min[index_smin].task_index] =
129  energy;
130  index_smin++;
131  }
132 
133  // Store the energy contained in the timetable just before those events.
134  while (index_emax >= 0 && by_decreasing_end_max[index_emax].time == time) {
135  mandatory_energy_before_end_max_[by_decreasing_end_max[index_emax]
136  .task_index] = energy;
137  index_emax--;
138  }
139 
140  // Process the starting compulsory parts.
141  while (index_scp < scp_.size() && scp_[index_scp].time == time) {
142  height += demands_->DemandMin(scp_[index_scp].task_index);
143  index_scp++;
144  }
145 
146  // Process the ending compulsory parts.
147  while (index_ecp < ecp_.size() && ecp_[index_ecp].time == time) {
148  height -= demands_->DemandMin(ecp_[index_ecp].task_index);
149  index_ecp++;
150  }
151  }
152 }
153 
154 bool TimeTableEdgeFinding::TimeTableEdgeFindingPass() {
155  demands_->CacheAllEnergyValues();
156 
157  // Initialize the data structures and build the free parts.
158  // --------------------------------------------------------
159  for (int t = 0; t < num_tasks_; ++t) {
160  // If the task has no mandatory part, then its free part is the task itself.
161  const IntegerValue start_max = helper_->StartMax(t);
162  const IntegerValue end_min = helper_->EndMin(t);
163  const IntegerValue demand_min = demands_->DemandMin(t);
164  IntegerValue mandatory_energy(0);
165 
166  if (start_max >= end_min) {
167  size_free_[t] = helper_->SizeMin(t);
168  } else {
169  const IntegerValue mandatory_size = end_min - start_max;
170  size_free_[t] = helper_->SizeMin(t) - mandatory_size;
171  mandatory_energy = mandatory_size * demand_min;
172  }
173 
174  const IntegerValue min_energy = demands_->EnergyMin(t);
175  energy_free_[t] = min_energy - mandatory_energy;
176  DCHECK_GE(energy_free_[t], 0);
177  }
178 
179  // TODO(user): Is it possible to have a 'higher' mandatory profile using
180  // the min energy instead of the demand_min * size_min? How can we incorporate
181  // this extra energy in the mandatory profile ?
182  BuildTimeTable();
183  const auto& by_start_min = helper_->TaskByIncreasingStartMin();
184 
185  IntegerValue previous_end = kMaxIntegerValue;
186 
187  // Apply the Timetabling Edge Finding filtering rule.
188  // --------------------------------------------------
189  // The loop order is not important for correctness.
190  for (const TaskTime end_task_time : helper_->TaskByDecreasingEndMax()) {
191  const int end_task = end_task_time.task_index;
192 
193  // TODO(user): consider optional tasks for additional propagation.
194  if (!helper_->IsPresent(end_task)) continue;
195  if (energy_free_[end_task] == 0) continue;
196 
197  // We only need to consider each time point once.
198  if (end_task_time.time == previous_end) continue;
199  previous_end = end_task_time.time;
200 
201  // Energy of the free parts contained in the interval
202  // [window_min, window_max].
203  IntegerValue energy_free_parts = IntegerValue(0);
204  reason_tasks_fully_included_in_window_.clear();
205  reason_tasks_partially_included_in_window_.clear();
206 
207  // Task that requires the biggest additional amount of energy to be
208  // scheduled at its minimum start time in the task interval
209  // [window_min, window_max].
210  int max_task = -1;
211  IntegerValue free_energy_of_max_task_in_window(0);
212  IntegerValue extra_energy_required_by_max_task = kMinIntegerValue;
213 
214  // Process task by decreasing start min.
215  const IntegerValue window_max = end_task_time.time;
216  for (const TaskTime begin_task_time : gtl::reversed_view(by_start_min)) {
217  const int begin_task = begin_task_time.task_index;
218 
219  // The considered time window. Note that we use the "cached" values so
220  // that our mandatory energy before computation is correct.
221  const IntegerValue window_min = begin_task_time.time;
222 
223  // Not a valid time window.
224  if (window_max <= window_min) continue;
225 
226  // TODO(user): consider optional tasks for additional propagation.
227  if (!helper_->IsPresent(begin_task)) continue;
228  if (energy_free_[begin_task] == 0) continue;
229 
230  // We consider two different cases: either the free part overlaps the
231  // window_max of the interval (right) or it does not (inside).
232  //
233  // window_min window_max
234  // v v
235  // right: ======|===
236  //
237  // window_min window_max
238  // v v
239  // inside: ========== |
240  //
241  // In the inside case, the additional amount of energy required to
242  // schedule the task at its minimum start time is equal to the whole
243  // energy of the free part. In the right case, the additional energy is
244  // equal to the largest part of the free part that can fit in the task
245  // interval.
246  const IntegerValue end_max = helper_->EndMax(begin_task);
247  if (end_max <= window_max) {
248  // The whole task energy is contained in the window.
249  reason_tasks_fully_included_in_window_.push_back(begin_task);
250  energy_free_parts += energy_free_[begin_task];
251  } else {
252  const IntegerValue demand_min = demands_->DemandMin(begin_task);
253  const IntegerValue extra_energy =
254  std::min(size_free_[begin_task], (window_max - window_min)) *
255  demand_min;
256 
257  // This is not in the paper, but it is almost free for us to account for
258  // the free energy of this task that must be present in the window.
259  const IntegerValue free_energy_in_window =
260  std::max(IntegerValue(0),
261  size_free_[begin_task] - (end_max - window_max)) *
262  demand_min;
263 
264  // TODO(user): There is no point setting max_task if its start min
265  // is already bigger that what we can push. Maybe we can exploit that?
266  if (extra_energy > extra_energy_required_by_max_task) {
267  if (max_task != -1 && free_energy_of_max_task_in_window > 0) {
268  reason_tasks_partially_included_in_window_.push_back(max_task);
269  }
270 
271  max_task = begin_task;
272  extra_energy_required_by_max_task = extra_energy;
273 
274  // Account for the free energy of the old max task, and cache the
275  // new one for later.
276  energy_free_parts += free_energy_of_max_task_in_window;
277  free_energy_of_max_task_in_window = free_energy_in_window;
278  } else if (free_energy_in_window > 0) {
279  reason_tasks_partially_included_in_window_.push_back(begin_task);
280  energy_free_parts += free_energy_in_window;
281  }
282  }
283 
284  // No task to push. This happens if all the tasks that overlap the task
285  // interval are entirely contained in it.
286  // TODO(user): check that we should not fail if the interval is
287  // overloaded, i.e., available_energy < 0.
288  if (max_task == -1) continue;
289 
290  // Compute the amount of energy available to schedule max_task.
291  const IntegerValue window_energy =
292  CapacityMax() * (window_max - window_min);
293  const IntegerValue energy_mandatory =
294  mandatory_energy_before_end_max_[end_task] -
295  mandatory_energy_before_start_min_[begin_task];
296  const IntegerValue available_energy =
297  window_energy - energy_free_parts - energy_mandatory;
298 
299  // Enough energy to schedule max_task at its minimum start time?
300  //
301  // TODO(user): In case of alternatives, for each fixed
302  // size/demand pair, we can compute a new_start and use the min of them.
303  if (extra_energy_required_by_max_task <= available_energy) {
304  // If the test below is true, we know the max_task cannot fully
305  // fit in the time window, so at least end_min > window_max.
306  //
307  // TODO(user): We currently only do that if we are not about to push the
308  // start as we assume the start push is just stronger. Maybe we should
309  // do it in more situation?
310  if (energy_free_[max_task] > available_energy &&
311  helper_->EndMin(max_task) <= window_max) {
312  FillEnergyInWindowReason(window_min, window_max, max_task);
313  demands_->AddEnergyMinReason(max_task);
314  helper_->AddStartMinReason(max_task, window_min);
315  if (!helper_->IncreaseEndMin(max_task, window_max + 1)) return false;
316  }
317  continue;
318  }
319 
320  // Compute the length of the mandatory subpart of max_task that should be
321  // considered as available.
322  //
323  // TODO(user): Because this use updated bounds, it might be more than what
324  // we accounted for in the precomputation. This is correct but could be
325  // improved uppon.
326  const IntegerValue mandatory_size_in_window =
327  std::max(IntegerValue(0),
328  std::min(window_max, helper_->EndMin(max_task)) -
329  std::max(window_min, helper_->StartMax(max_task)));
330 
331  // Compute the new minimum start time of max_task.
332  const IntegerValue max_free_size_that_fit =
333  available_energy / demands_->DemandMin(max_task);
334  const IntegerValue new_start =
335  window_max - mandatory_size_in_window - max_free_size_that_fit;
336 
337  // Push and explain only if the new start is bigger than the current one.
338  if (helper_->StartMin(max_task) < new_start) {
339  FillEnergyInWindowReason(window_min, window_max, max_task);
340 
341  // Reason needed for task_index.
342  // We only need start_min and demand_min to push the start.
343  helper_->AddStartMinReason(max_task, window_min);
344  demands_->AddDemandMinReason(max_task);
345 
346  if (!helper_->IncreaseStartMin(max_task, new_start)) return false;
347  }
348  }
349  }
350 
351  return true;
352 }
353 
354 void TimeTableEdgeFinding::FillEnergyInWindowReason(IntegerValue window_min,
355  IntegerValue window_max,
356  int task_index) {
357  helper_->ClearReason();
358 
359  // Capacity of the resource.
360  if (capacity_.var != kNoIntegerVariable) {
361  helper_->MutableIntegerReason()->push_back(
362  integer_trail_->UpperBoundAsLiteral(capacity_.var));
363  }
364 
365  // Tasks contributing to the mandatory energy in the interval.
366  for (int t = 0; t < num_tasks_; ++t) {
367  if (t == task_index) continue;
368  if (!helper_->IsPresent(t)) continue;
369  const IntegerValue smax = helper_->StartMax(t);
370  const IntegerValue emin = helper_->EndMin(t);
371  if (smax >= emin) continue;
372  if (emin <= window_min) continue;
373  if (smax >= window_max) continue;
374  helper_->AddStartMaxReason(t, std::max(smax, window_min));
375  helper_->AddEndMinReason(t, std::min(emin, window_max));
376  helper_->AddPresenceReason(t);
377  demands_->AddDemandMinReason(t);
378  }
379 
380  // Tasks contributing to the free energy in [window_min, window_max].
381  //
382  // TODO(user): If a task appears in both, we could avoid adding twice the
383  // same things, but the core solver should merge duplicates anyway.
384  for (const int t : reason_tasks_fully_included_in_window_) {
385  DCHECK_NE(t, task_index);
386  DCHECK(helper_->IsPresent(t));
387  DCHECK_GT(helper_->EndMax(t), window_min);
388  DCHECK_LT(helper_->StartMin(t), window_max);
389  DCHECK_GE(helper_->StartMin(t), window_min);
390 
391  helper_->AddStartMinReason(t, helper_->StartMin(t));
392  helper_->AddEndMaxReason(t, std::max(window_max, helper_->EndMax(t)));
393  helper_->AddPresenceReason(t);
394  demands_->AddEnergyMinReason(t);
395  }
396  for (const int t : reason_tasks_partially_included_in_window_) {
397  DCHECK_NE(t, task_index);
398  DCHECK(helper_->IsPresent(t));
399  DCHECK_GT(helper_->EndMax(t), window_min);
400  DCHECK_LT(helper_->StartMin(t), window_max);
401  DCHECK_GE(helper_->StartMin(t), window_min);
402 
403  helper_->AddStartMinReason(t, helper_->StartMin(t));
404  helper_->AddEndMaxReason(t, std::max(window_max, helper_->EndMax(t)));
405  helper_->AddPresenceReason(t);
406 
407  helper_->AddSizeMinReason(t);
408  demands_->AddDemandMinReason(t);
409  }
410 }
411 
412 } // namespace sat
413 } // namespace operations_research
int64_t max
Definition: alldiff_cst.cc:140
int64_t min
Definition: alldiff_cst.cc:139
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
void SetPropagatorPriority(int id, int priority)
Definition: integer.cc:2309
int Register(PropagatorInterface *propagator)
Definition: integer.cc:2286
IntegerLiteral UpperBoundAsLiteral(IntegerVariable i) const
Definition: integer.h:1594
Class that owns everything related to a particular optimization model.
Definition: sat/model.h:42
const std::vector< TaskTime > & TaskByDecreasingEndMax()
Definition: intervals.cc:386
ABSL_MUST_USE_RESULT bool IncreaseStartMin(int t, IntegerValue value)
Definition: intervals.cc:523
const std::vector< TaskTime > & TaskByIncreasingStartMin()
Definition: intervals.cc:349
void AddStartMinReason(int t, IntegerValue lower_bound)
Definition: intervals.h:722
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
ABSL_MUST_USE_RESULT bool IncreaseEndMin(int t, IntegerValue value)
Definition: intervals.cc:531
std::vector< IntegerLiteral > * MutableIntegerReason()
Definition: intervals.h:348
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 AddEndMaxReason(int t, IntegerValue upper_bound)
Definition: intervals.h:744
void AddStartMaxReason(int t, IntegerValue upper_bound)
Definition: intervals.h:729
const std::vector< AffineExpression > & Demands() const
Definition: intervals.h:521
TimeTableEdgeFinding(AffineExpression capacity, SchedulingConstraintHelper *helper, SchedulingDemandHelper *demands, Model *model)
void RegisterWith(GenericLiteralWatcher *watcher)
int64_t height
GRBmodel * model
ReverseView< Container > reversed_view(const Container &c)
constexpr IntegerValue kMaxIntegerValue(std::numeric_limits< IntegerValue::ValueType >::max() - 1)
constexpr IntegerValue kMinIntegerValue(-kMaxIntegerValue.value())
const IntegerVariable kNoIntegerVariable(-1)
Collection of objects used to extend the Constraint Solver library.
int64_t energy
Definition: resource.cc:355
int64_t time
Definition: resource.cc:1694
int64_t capacity
Rev< int64_t > start_max
Rev< int64_t > end_max
Rev< int64_t > end_min