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