Google OR-Tools v9.15
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
timetable_edgefinding.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 <limits>
18#include <vector>
19
20#include "absl/log/check.h"
22#include "ortools/sat/integer.h"
24#include "ortools/sat/model.h"
27
28namespace operations_research {
29namespace sat {
30
34 Model* model)
35 : num_tasks_(helper->NumTasks()),
36 capacity_(capacity),
37 helper_(helper),
38 demands_(demands),
39 integer_trail_(model->GetOrCreate<IntegerTrail>()) {
40 // Edge finding structures.
41 mandatory_energy_before_end_max_.resize(num_tasks_);
42 mandatory_energy_before_start_min_.resize(num_tasks_);
43
44 // Energy of free parts.
45 size_free_.resize(num_tasks_);
46 energy_free_.resize(num_tasks_);
47}
48
50 const int id = watcher->Register(this);
51 watcher->WatchUpperBound(capacity_, id);
52 helper_->WatchAllTasks(id);
53 for (int t = 0; t < num_tasks_; t++) {
54 watcher->WatchLowerBound(demands_->Demands()[t], id);
55 }
56 watcher->SetPropagatorPriority(id, 3);
58}
59
61 if (!helper_->IsEnforced()) return true;
62 if (!helper_->SynchronizeAndSetTimeDirection(true)) return false;
63 if (!TimeTableEdgeFindingPass()) return false;
64 if (!helper_->SynchronizeAndSetTimeDirection(false)) return false;
65 if (!TimeTableEdgeFindingPass()) return false;
66 return true;
67}
68
69void TimeTableEdgeFinding::BuildTimeTable() {
70 scp_.clear();
71 ecp_.clear();
72
73 // Build start of compulsory part events.
74 const auto by_negated_smax = helper_->TaskByIncreasingNegatedStartMax();
75 for (const auto [t, negated_smax] : ::gtl::reversed_view(by_negated_smax)) {
76 if (!helper_->IsPresent(t)) continue;
77 const IntegerValue start_max = -negated_smax;
78 if (start_max < helper_->EndMin(t)) {
79 scp_.push_back({t, start_max});
80 }
81 }
82
83 // Build end of compulsory part events.
84 for (const auto task_time : helper_->TaskByIncreasingEndMin()) {
85 const int t = task_time.task_index;
86 if (!helper_->IsPresent(t)) continue;
87 if (helper_->StartMax(t) < task_time.time) {
88 ecp_.push_back(task_time);
89 }
90 }
91
92 DCHECK_EQ(scp_.size(), ecp_.size());
93
94 const auto by_decreasing_end_max = helper_->TaskByDecreasingEndMax();
95 const auto by_start_min = helper_->TaskByIncreasingStartMin();
96
97 IntegerValue height = IntegerValue(0);
98 IntegerValue energy = IntegerValue(0);
99
100 // We don't care since at the beginning height 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
154bool TimeTableEdgeFinding::TimeTableEdgeFindingPass() {
155 if (!demands_->CacheAllEnergyValues()) return true;
156
157 IntegerValue earliest_start_min = std::numeric_limits<IntegerValue>::max();
158 IntegerValue latest_end_max = std::numeric_limits<IntegerValue>::min();
159 IntegerValue maximum_demand_min = IntegerValue(0);
160 // Initialize the data structures and build the free parts.
161 // --------------------------------------------------------
162 for (int t = 0; t < num_tasks_; ++t) {
163 // If the task has no mandatory part, then its free part is the task itself.
164 const IntegerValue start_max = helper_->StartMax(t);
165 const IntegerValue end_min = helper_->EndMin(t);
166 const IntegerValue demand_min = demands_->DemandMin(t);
167 IntegerValue mandatory_energy(0);
168
169 earliest_start_min = std::min(earliest_start_min, helper_->StartMin(t));
170 latest_end_max = std::max(latest_end_max, helper_->EndMax(t));
171 maximum_demand_min = std::max(maximum_demand_min, demand_min);
172
173 if (start_max >= end_min) {
174 size_free_[t] = helper_->SizeMin(t);
175 } else {
176 const IntegerValue mandatory_size = end_min - start_max;
177 size_free_[t] = helper_->SizeMin(t) - mandatory_size;
178 mandatory_energy = mandatory_size * demand_min;
179 }
180
181 const IntegerValue min_energy = demands_->EnergyMin(t);
182 energy_free_[t] = min_energy - mandatory_energy;
183 DCHECK_GE(energy_free_[t], 0);
184 }
185
186 if (AtMinOrMaxInt64I(CapProdI(CapSubI(latest_end_max, earliest_start_min),
187 maximum_demand_min))) {
188 // Avoid possible overflow.
189 return true;
190 }
191
192 // TODO(user): Is it possible to have a 'higher' mandatory profile using
193 // the min energy instead of the demand_min * size_min? How can we incorporate
194 // this extra energy in the mandatory profile ?
195 BuildTimeTable();
196 const auto& by_start_min = helper_->TaskByIncreasingStartMin();
197
198 IntegerValue previous_end = kMaxIntegerValue;
199
200 // Apply the Timetabling Edge Finding filtering rule.
201 // --------------------------------------------------
202 // The loop order is not important for correctness.
203 for (const TaskTime end_task_time : helper_->TaskByDecreasingEndMax()) {
204 const int end_task = end_task_time.task_index;
205
206 // TODO(user): consider optional tasks for additional propagation.
207 if (!helper_->IsPresent(end_task)) continue;
208 if (energy_free_[end_task] == 0) continue;
209
210 // We only need to consider each time point once.
211 if (end_task_time.time == previous_end) continue;
212 previous_end = end_task_time.time;
213
214 // Energy of the free parts contained in the interval
215 // [window_min, window_max].
216 IntegerValue energy_free_parts = IntegerValue(0);
217 reason_tasks_fully_included_in_window_.clear();
218 reason_tasks_partially_included_in_window_.clear();
219
220 // Task that requires the biggest additional amount of energy to be
221 // scheduled at its minimum start time in the task interval
222 // [window_min, window_max].
223 int max_task = -1;
224 IntegerValue free_energy_of_max_task_in_window(0);
225 IntegerValue extra_energy_required_by_max_task = kMinIntegerValue;
226
227 // Process task by decreasing start min.
228 const IntegerValue window_max = end_task_time.time;
229 for (const TaskTime begin_task_time : gtl::reversed_view(by_start_min)) {
230 const int begin_task = begin_task_time.task_index;
231
232 // The considered time window. Note that we use the "cached" values so
233 // that our mandatory energy before computation is correct.
234 const IntegerValue window_min = begin_task_time.time;
235
236 // Not a valid time window.
237 if (window_max <= window_min) continue;
238
239 // TODO(user): consider optional tasks for additional propagation.
240 if (!helper_->IsPresent(begin_task)) continue;
241 if (energy_free_[begin_task] == 0) continue;
242
243 // We consider two different cases: either the free part overlaps the
244 // window_max of the interval (right) or it does not (inside).
245 //
246 // window_min window_max
247 // v v
248 // right: ======|===
249 //
250 // window_min window_max
251 // v v
252 // inside: ========== |
253 //
254 // In the inside case, the additional amount of energy required to
255 // schedule the task at its minimum start time is equal to the whole
256 // energy of the free part. In the right case, the additional energy is
257 // equal to the largest part of the free part that can fit in the task
258 // interval.
259 const IntegerValue end_max = helper_->EndMax(begin_task);
260 if (end_max <= window_max) {
261 // The whole task energy is contained in the window.
262 reason_tasks_fully_included_in_window_.push_back(begin_task);
263 energy_free_parts += energy_free_[begin_task];
264 } else {
265 const IntegerValue demand_min = demands_->DemandMin(begin_task);
266 const IntegerValue extra_energy =
267 std::min(size_free_[begin_task], (window_max - window_min)) *
268 demand_min;
269
270 // This is not in the paper, but it is almost free for us to account for
271 // the free energy of this task that must be present in the window.
272 const IntegerValue free_energy_in_window =
273 std::max(IntegerValue(0),
274 size_free_[begin_task] - (end_max - window_max)) *
275 demand_min;
276
277 // TODO(user): There is no point setting max_task if its start min
278 // is already bigger that what we can push. Maybe we can exploit that?
279 if (extra_energy > extra_energy_required_by_max_task) {
280 if (max_task != -1 && free_energy_of_max_task_in_window > 0) {
281 reason_tasks_partially_included_in_window_.push_back(max_task);
282 }
283
284 max_task = begin_task;
285 extra_energy_required_by_max_task = extra_energy;
286
287 // Account for the free energy of the old max task, and cache the
288 // new one for later.
289 energy_free_parts += free_energy_of_max_task_in_window;
290 free_energy_of_max_task_in_window = free_energy_in_window;
291 } else if (free_energy_in_window > 0) {
292 reason_tasks_partially_included_in_window_.push_back(begin_task);
293 energy_free_parts += free_energy_in_window;
294 }
295 }
296
297 // No task to push. This happens if all the tasks that overlap the task
298 // interval are entirely contained in it.
299 // TODO(user): check that we should not fail if the interval is
300 // overloaded, i.e., available_energy < 0.
301 //
302 // We also defensively abort if the demand_min is 0.
303 // This may happen along a energy_min > 0 if the literals in the
304 // decomposed_energy have been fixed, and not yet propagated to the demand
305 // affine expression.
306 if (max_task == -1 || demands_->DemandMin(max_task) == 0) continue;
307
308 // Compute the amount of energy available to schedule max_task.
309 const IntegerValue window_energy =
310 CapacityMax() * (window_max - window_min);
311 const IntegerValue energy_mandatory =
312 mandatory_energy_before_end_max_[end_task] -
313 mandatory_energy_before_start_min_[begin_task];
314 const IntegerValue available_energy =
315 window_energy - energy_free_parts - energy_mandatory;
316
317 // Enough energy to schedule max_task at its minimum start time?
318 //
319 // TODO(user): In case of alternatives, for each fixed
320 // size/demand pair, we can compute a new_start and use the min of them.
321 if (extra_energy_required_by_max_task <= available_energy) {
322 // If the test below is true, we know the max_task cannot fully
323 // fit in the time window, so at least end_min > window_max.
324 //
325 // TODO(user): We currently only do that if we are not about to push the
326 // start as we assume the start push is just stronger. Maybe we should
327 // do it in more situation?
328 if (energy_free_[max_task] > available_energy &&
329 helper_->EndMin(max_task) <= window_max) {
330 FillEnergyInWindowReason(window_min, window_max, max_task);
331 demands_->AddEnergyMinReason(max_task);
332 helper_->AddStartMinReason(max_task, window_min);
333 if (!helper_->IncreaseEndMin(max_task, window_max + 1)) return false;
334 }
335 continue;
336 }
337
338 // Compute the length of the mandatory subpart of max_task that should be
339 // considered as available.
340 //
341 // TODO(user): Because this use updated bounds, it might be more than what
342 // we accounted for in the precomputation. This is correct but could be
343 // improved upon.
344 const IntegerValue mandatory_size_in_window =
345 std::max(IntegerValue(0),
346 std::min(window_max, helper_->EndMin(max_task)) -
347 std::max(window_min, helper_->StartMax(max_task)));
348
349 // Compute the new minimum start time of max_task.
350 const IntegerValue max_free_size_that_fit =
351 available_energy / demands_->DemandMin(max_task);
352 const IntegerValue new_start =
353 window_max - mandatory_size_in_window - max_free_size_that_fit;
354
355 // Push and explain only if the new start is bigger than the current one.
356 if (helper_->StartMin(max_task) < new_start) {
357 FillEnergyInWindowReason(window_min, window_max, max_task);
358
359 // Reason needed for task_index.
360 // We only need start_min and demand_min to push the start.
361 helper_->AddStartMinReason(max_task, window_min);
362 demands_->AddDemandMinReason(max_task);
363
364 if (!helper_->IncreaseStartMin(max_task, new_start)) return false;
365 }
366 }
367 }
368
369 return true;
370}
371
372void TimeTableEdgeFinding::FillEnergyInWindowReason(IntegerValue window_min,
373 IntegerValue window_max,
374 int task_index) {
375 helper_->ResetReason();
376
377 // Capacity of the resource.
378 if (capacity_.var != kNoIntegerVariable) {
379 helper_->AddIntegerReason(
380 integer_trail_->UpperBoundAsLiteral(capacity_.var));
381 }
382
383 // Tasks contributing to the mandatory energy in the interval.
384 for (int t = 0; t < num_tasks_; ++t) {
385 if (t == task_index) continue;
386 if (!helper_->IsPresent(t)) continue;
387 const IntegerValue smax = helper_->StartMax(t);
388 const IntegerValue emin = helper_->EndMin(t);
389 if (smax >= emin) continue;
390 if (emin <= window_min) continue;
391 if (smax >= window_max) continue;
392 helper_->AddStartMaxReason(t, std::max(smax, window_min));
393 helper_->AddEndMinReason(t, std::min(emin, window_max));
394 helper_->AddPresenceReason(t);
395 demands_->AddDemandMinReason(t);
396 }
397
398 // Tasks contributing to the free energy in [window_min, window_max].
399 //
400 // TODO(user): If a task appears in both, we could avoid adding twice the
401 // same things, but the core solver should merge duplicates anyway.
402 for (const int t : reason_tasks_fully_included_in_window_) {
403 DCHECK_NE(t, task_index);
404 DCHECK(helper_->IsPresent(t));
405 DCHECK_GT(helper_->EndMax(t), window_min);
406 DCHECK_LT(helper_->StartMin(t), window_max);
407 DCHECK_GE(helper_->StartMin(t), window_min);
408
409 helper_->AddStartMinReason(t, helper_->StartMin(t));
410 helper_->AddEndMaxReason(t, std::max(window_max, helper_->EndMax(t)));
411 helper_->AddPresenceReason(t);
412 demands_->AddEnergyMinReason(t);
413 }
414 for (const int t : reason_tasks_partially_included_in_window_) {
415 DCHECK_NE(t, task_index);
416 DCHECK(helper_->IsPresent(t));
417 DCHECK_GT(helper_->EndMax(t), window_min);
418 DCHECK_LT(helper_->StartMin(t), window_max);
419 DCHECK_GE(helper_->StartMin(t), window_min);
420
421 helper_->AddStartMinReason(t, helper_->StartMin(t));
422 helper_->AddEndMaxReason(t, std::max(window_max, helper_->EndMax(t)));
423 helper_->AddPresenceReason(t);
424
425 helper_->AddSizeMinReason(t);
426 demands_->AddDemandMinReason(t);
427 }
428}
429
430} // namespace sat
431} // namespace operations_research
void WatchUpperBound(IntegerVariable var, int id, int watch_index=-1)
Definition integer.h:1734
void WatchLowerBound(IntegerVariable var, int id, int watch_index=-1)
Definition integer.h:1716
void SetPropagatorPriority(int id, int priority)
Definition integer.cc:2700
int Register(PropagatorInterface *propagator)
Definition integer.cc:2674
void RegisterWith(GenericLiteralWatcher *watcher)
TimeTableEdgeFinding(AffineExpression capacity, SchedulingConstraintHelper *helper, SchedulingDemandHelper *demands, Model *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)
IntegerValue CapSubI(IntegerValue a, IntegerValue b)
bool AtMinOrMaxInt64I(IntegerValue t)
IntegerValue CapProdI(IntegerValue a, IntegerValue b)
OR-Tools root namespace.