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