Google OR-Tools v9.15
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
scheduling_helpers.h
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
14#ifndef ORTOOLS_SAT_SCHEDULING_HELPERS_H_
15#define ORTOOLS_SAT_SCHEDULING_HELPERS_H_
16
17#include <algorithm>
18#include <cstdint>
19#include <functional>
20#include <memory>
21#include <optional>
22#include <string>
23#include <utility>
24#include <vector>
25
26#include "absl/base/attributes.h"
27#include "absl/log/check.h"
28#include "absl/types/span.h"
32#include "ortools/sat/integer.h"
35#include "ortools/sat/model.h"
39#include "ortools/sat/util.h"
40#include "ortools/util/bitset.h"
42
43namespace operations_research {
44namespace sat {
45
46// An helper struct to sort task by time. This is used by the
47// SchedulingConstraintHelper but also by many scheduling propagators to sort
48// tasks.
49struct TaskTime {
51 IntegerValue time;
52 bool operator<(TaskTime other) const { return time < other.time; }
53 bool operator>(TaskTime other) const { return time > other.time; }
54};
55
56// We have some free space in TaskTime.
57// We stick the presence_lit to save an indirection in some algo.
58//
59// TODO(user): Experiment caching more value. In particular
60// TaskByIncreasingShiftedStartMin() could tie break task for better heuristics?
63 LiteralIndex presence_lit;
64 IntegerValue time;
65 bool operator<(CachedTaskBounds other) const { return time < other.time; }
66 bool operator>(CachedTaskBounds other) const { return time > other.time; }
67};
68
73 std::optional<Literal> is_present;
74
75 template <typename H>
76 friend H AbslHashValue(H h, const IntervalDefinition& i) {
77 return H::combine(std::move(h), i.start, i.end, i.size, i.is_present);
78 }
79
80 bool operator==(const IntervalDefinition& other) const {
81 return start == other.start && end == other.end && size == other.size &&
82 is_present == other.is_present;
83 }
84};
85
86// Helper class shared by the propagators that manage a given list of tasks.
87//
88// One of the main advantage of this class is that it allows to share the
89// vectors of tasks sorted by various criteria between propagator for a faster
90// code. It is also helpful to allow in-processing: the intervals that are
91// handled by this class are not necessarily the same as the ones in the model.
93 public:
94 // All the functions below refer to a task by its index t in the tasks
95 // vector given at construction.
96 SchedulingConstraintHelper(std::vector<AffineExpression> starts,
97 std::vector<AffineExpression> ends,
98 std::vector<AffineExpression> sizes,
99 std::vector<LiteralIndex> reason_for_presence,
100 Model* model);
101
102 // Temporary constructor.
103 // The class will not be usable until ResetFromSubset() is called.
104 //
105 // TODO(user): Remove this. It is a hack because the disjunctive class needs
106 // to fetch the maximum possible number of task at construction.
107 SchedulingConstraintHelper(int num_tasks, Model* model);
108
109 // Returns true if and only if all the enforcement literals are true.
110 bool IsEnforced() const;
111
112 // This is a propagator so we can "cache" all the intervals relevant
113 // information. This gives good speedup. Note however that the info is stale
114 // except if a bound was pushed by this helper or if this was called. We run
115 // it at the highest priority, so that will mostly be the case at the
116 // beginning of each Propagate() call of the classes using this.
117 bool Propagate() final;
118 bool IncrementalPropagate(const std::vector<int>& watch_indices) final;
120 absl::Span<const Literal> enforcement_literals);
121
122 // Sets the enforcement ID without registering this propagator with a watcher.
123 // This is used by NoOverlap2DConstraintHelper, which registers itself but
124 // does not register its x and y SchedulingConstraintHelpers.
125 void SetEnforcementId(EnforcementId id) { enforcement_id_ = id; }
126
127 // Resets the class to the same state as if it was constructed with
128 // the given subset of tasks from other (and the same enforcement literals).
129 ABSL_MUST_USE_RESULT bool ResetFromSubset(
130 const SchedulingConstraintHelper& other, absl::Span<const int> tasks);
131
132 // Returns the number of task.
133 int NumTasks() const { return starts_.size(); }
134
135 // Make sure the cached values are up to date. Also sets the time direction to
136 // either forward/backward. This will impact all the functions below. This
137 // MUST be called at the beginning of all Propagate() call that uses this
138 // helper.
139 void SetTimeDirection(bool is_forward);
140 bool CurrentTimeIsForward() const { return current_time_direction_; }
141 ABSL_MUST_USE_RESULT bool SynchronizeAndSetTimeDirection(bool is_forward);
142
143 // Helpers for the current bounds on the current task time window.
144 // [ (size-min) ... (size-min) ]
145 // ^ ^ ^ ^
146 // start-min end-min start-max end-max
147 //
148 // Note that for tasks with variable durations, we don't necessarily have
149 // duration-min between the XXX-min and XXX-max value.
150 //
151 // Remark: We use cached values for most of these function as this is faster.
152 // In practice, the cache will almost always be up to date, but not in corner
153 // cases where pushing the start of one task will change values for many
154 // others. This is fine as the new values will be picked up as we reach the
155 // propagation fixed point.
156 IntegerValue SizeMin(int t) const { return cached_size_min_[t]; }
157 IntegerValue SizeMax(int t) const {
158 // This one is "rare" so we don't cache it.
159 return integer_trail_->UpperBound(sizes_[t]);
160 }
161 IntegerValue StartMin(int t) const { return cached_start_min_[t]; }
162 IntegerValue EndMin(int t) const { return cached_end_min_[t]; }
163 IntegerValue StartMax(int t) const { return -cached_negated_start_max_[t]; }
164 IntegerValue EndMax(int t) const { return -cached_negated_end_max_[t]; }
165
166 IntegerValue LevelZeroSizeMin(int t) const {
167 // Note the variable that encodes the size of an absent task can be
168 // negative.
169 return std::max(IntegerValue(0),
170 integer_trail_->LevelZeroLowerBound(sizes_[t]));
171 }
172 IntegerValue LevelZeroStartMin(int t) const {
173 return integer_trail_->LevelZeroLowerBound(starts_[t]);
174 }
175 IntegerValue LevelZeroStartMax(int t) const {
176 return integer_trail_->LevelZeroUpperBound(starts_[t]);
177 }
178 IntegerValue LevelZeroEndMax(int t) const {
179 return integer_trail_->LevelZeroUpperBound(ends_[t]);
180 }
181
182 // In the presence of tasks with a variable size, we do not necessarily
183 // have start_min + size_min = end_min, we can instead have a situation
184 // like:
185 // | |<--- size-min --->|
186 // ^ ^ ^
187 // start-min | end-min
188 // |
189 // We define the "shifted start min" to be the right most time such that
190 // we known that we must have min-size "energy" to the right of it if the
191 // task is present. Using it in our scheduling propagators allows to propagate
192 // more in the presence of tasks with variable size (or optional task
193 // where we also do not necessarily have start_min + size_min = end_min.
194 //
195 // To explain this shifted start min, one must use the AddEnergyAfterReason().
196 IntegerValue ShiftedStartMin(int t) const {
197 return cached_shifted_start_min_[t];
198 }
199
200 // As with ShiftedStartMin(), we can compute the shifted end max (that is
201 // start_max + size_min.
202 IntegerValue ShiftedEndMax(int t) const {
203 return -cached_negated_shifted_end_max_[t];
204 }
205
206 bool StartIsFixed(int t) const;
207 bool EndIsFixed(int t) const;
208 bool SizeIsFixed(int t) const;
209
210 // Returns true if the corresponding fact is known for sure. A normal task is
211 // always present. For optional task for which the presence is still unknown,
212 // both of these function will return false.
213 bool IsOptional(int t) const;
214 bool IsPresent(int t) const;
215 bool IsAbsent(int t) const;
216
217 // Same if one already have the presence LiteralIndex of a task.
218 bool IsOptional(LiteralIndex lit) const;
219 bool IsPresent(LiteralIndex lit) const;
220 bool IsAbsent(LiteralIndex lit) const;
221
222 // Returns a value so that End(a) + dist <= Start(b).
223 //
224 // TODO(user): we use this to optimize some reason, but ideally we only want
225 // to use linear2 bounds here, not bounds coming from trivial bounds. Make
226 // sure we have the best possible reason.
227 IntegerValue GetCurrentMinDistanceBetweenTasks(int a, int b);
228
229 // We detected a precedence between two tasks at level zero.
230 // This register a new constraint and notify the linear2 root level bounds
231 // repository. Returns false on conflict.
232 //
233 // TODO(user): We could also call this at positive decision level, but it is a
234 // bit harder to exploit as we will also need to store the reasons.
235 bool NotifyLevelZeroPrecedence(int a, int b);
236
237 // Return the minimum overlap of task t with the time window [start..end].
238 //
239 // Note: this is different from the mandatory part of an interval.
240 IntegerValue GetMinOverlap(int t, IntegerValue start, IntegerValue end) const;
241
242 bool TaskIsBeforeOrIsOverlapping(int before, int after);
243
244 // Returns a string with the current task bounds.
245 std::string TaskDebugString(int t) const;
246
247 // Sorts and returns the tasks in corresponding order at the time of the call.
248 // Note that we do not mean strictly-increasing/strictly-decreasing, there
249 // will be duplicate time values in these vectors.
250 //
251 // TODO(user): we could merge the first loop of IncrementalSort() with the
252 // loop that fill TaskTime.time at each call.
253 absl::Span<const TaskTime> TaskByIncreasingStartMin();
254 absl::Span<const TaskTime> TaskByDecreasingEndMax();
255
256 absl::Span<const TaskTime> TaskByIncreasingNegatedStartMax();
257 absl::Span<const TaskTime> TaskByIncreasingEndMin();
258
259 absl::Span<const CachedTaskBounds> TaskByIncreasingShiftedStartMin();
260 absl::Span<const CachedTaskBounds> TaskByIncreasingNegatedShiftedEndMax();
261
262 // Returns a sorted vector where each task appear twice, the first occurrence
263 // is at size (end_min - size_min) and the second one at (end_min).
264 //
265 // This is quite usage specific.
267 IntegerValue time;
268 int task;
270
271 bool operator<(const ProfileEvent& other) const {
272 if (time == other.time) {
273 if (task == other.task) return is_first > other.is_first;
274 return task < other.task;
275 }
276 return time < other.time;
277 }
278 };
279 const std::vector<ProfileEvent>& GetEnergyProfile();
280
281 // Functions to reset and then set the current reason.
282 void ResetReason();
283 void ImportReasonsFromOther(const SchedulingConstraintHelper& other_helper);
284 void AddPresenceReason(int t);
285 void AddAbsenceReason(int t);
286 void AddSizeMinReason(int t);
287 void AddSizeMinReason(int t, IntegerValue lower_bound);
288 void AddSizeMaxReason(int t, IntegerValue upper_bound);
289 void AddStartMinReason(int t, IntegerValue lower_bound);
290 void AddStartMaxReason(int t, IntegerValue upper_bound);
291 void AddEndMinReason(int t, IntegerValue lower_bound);
292 void AddEndMaxReason(int t, IntegerValue upper_bound);
293 void AddShiftedEndMaxReason(int t, IntegerValue upper_bound);
294
295 void AddEnergyAfterReason(int t, IntegerValue energy_min, IntegerValue time);
296 void AddEnergyMinInIntervalReason(int t, IntegerValue min, IntegerValue max);
297
300
301 // Adds the reason why the task "before" must be before task "after", in
302 // the sense that "after" can only start at the same time or later than the
303 // task "before" ends.
304 //
305 // Important: this assumes that the two task cannot overlap. So we can have
306 // a more relaxed reason than Start(after) >= Ends(before).
307 //
308 // There are actually many possibilities to explain such relation:
309 // - StartMax(before) < EndMin(after).
310 // - We have a linear2: Start(after) >= End(before) - SizeMin(before);
311 // - etc...
312 // We try to pick the best one.
313 //
314 // TODO(user): Refine the heuritic. Also consider other reason for the
315 // complex cases where Start() and End() do not use the same integer variable.
316 void AddReasonForBeingBeforeAssumingNoOverlap(int before, int after);
317
318 void AddReasonForUpperBoundLowerThan(LinearExpression2 expr, IntegerValue ub);
319
320 void AppendAndResetReason(std::vector<IntegerLiteral>* integer_reason,
321 std::vector<Literal>* literal_reason);
322
323 // Push something using the current reason. Note that IncreaseStartMin() will
324 // also increase the end-min, and DecreaseEndMax() will also decrease the
325 // start-max.
326 //
327 // Important: IncreaseStartMin() and DecreaseEndMax() can be called on an
328 // optional interval whose presence is still unknown and push a bound
329 // conditioned on its presence. The functions will do the correct thing
330 // depending on whether or not the start_min/end_max are optional variables
331 // whose presence implies the interval presence.
332 ABSL_MUST_USE_RESULT bool IncreaseStartMin(int t, IntegerValue value);
333 ABSL_MUST_USE_RESULT bool IncreaseEndMin(int t, IntegerValue value);
334 ABSL_MUST_USE_RESULT bool DecreaseEndMax(int t, IntegerValue value);
335 ABSL_MUST_USE_RESULT bool PushLiteral(Literal l);
336 ABSL_MUST_USE_RESULT bool PushTaskAbsence(int t);
337 ABSL_MUST_USE_RESULT bool PushTaskPresence(int t);
338 ABSL_MUST_USE_RESULT bool PushIntegerLiteral(IntegerLiteral lit);
339 ABSL_MUST_USE_RESULT bool ReportConflict();
340 ABSL_MUST_USE_RESULT bool PushIntegerLiteralIfTaskPresent(int t,
341 IntegerLiteral lit);
342
343 // Push that t_before must end at the same time or before t_after starts.
344 // This function does the correct thing if t_before or t_after are optional
345 // and their presence is unknown. Returns false on conflict.
346 ABSL_MUST_USE_RESULT bool PushTaskOrderWhenPresent(int t_before, int t_after);
347
348 absl::Span<const AffineExpression> Starts() const { return starts_; }
349 absl::Span<const AffineExpression> Ends() const { return ends_; }
350 absl::Span<const AffineExpression> Sizes() const { return sizes_; }
351
353 return IntervalDefinition{
354 .start = starts_[index],
355 .end = ends_[index],
356 .size = sizes_[index],
357 .is_present = (reason_for_presence_[index] == kNoLiteralIndex
358 ? std::optional<Literal>()
359 : Literal(reason_for_presence_[index]))};
360 }
361
362 Literal PresenceLiteral(int index) const {
363 DCHECK(IsOptional(index));
364 return Literal(reason_for_presence_[index]);
365 }
366
367 // Registers the given propagator id to be called if any of the tasks
368 // in this class change. Note that we do not watch size max though.
369 void WatchAllTasks(int id);
370
371 // Sometimes, typically for no_overlap_2d, we can use the variables that are
372 // fixed at current decision level to define a scheduling sub-problem. For
373 // example, if all the x coordinates are fixed and the intervals overlap on
374 // the x axis, we can just run the disjunction propagators on the y
375 // coordinates.
376 //
377 // To be able to run scheduling propagators on a sub-problem, we can register
378 // a callback that before any explanation or when one of the bounds of an item
379 // is pushed. The callback will get the list of all items that participated in
380 // the reason and/or the bound push and allows the caller to introduce any new
381 // literals needed to make sure that the conditions making it a sub-problem
382 // hold.
384 std::function<void(absl::Span<const int> items,
385 std::vector<Literal>* reason,
386 std::vector<IntegerLiteral>* integer_reason)>
387 extra_explanation_callback) {
388 used_items_for_reason_.ClearAndResize(
389 extra_explanation_callback == nullptr ? 0 : capacity_);
390 extra_explanation_callback_ = std::move(extra_explanation_callback);
391 }
392
393 // TODO(user): Change the propagation loop code so that we don't stop
394 // pushing in the middle of the propagation as more advanced propagator do
395 // not handle this correctly.
396 bool InPropagationLoop() const { return integer_trail_->InPropagationLoop(); }
397
399 return sat_solver_->CurrentDecisionLevel();
400 }
401
402 // This increase as soon as we propagate something.
403 int64_t PropagationTimestamp() const {
404 return integer_trail_->timestamp() + trail_->NumberOfEnqueues();
405 }
406
407 struct TaskInfo {
408 int task;
409 IntegerValue start_min;
410 IntegerValue size_min;
411
412 bool operator<(TaskInfo other) const { return start_min < other.start_min; }
413 };
414 // Provide scratch vector that are cleared and available for use for
415 // propagators inside the call to Propagate().
417 scratch_task_time_vector1_.ClearAndReserve(NumTasks());
418 return scratch_task_time_vector1_;
419 }
421 scratch_task_time_vector2_.ClearAndReserve(NumTasks());
422 return scratch_task_time_vector2_;
423 }
425 scratch_task_set_.ClearAndReserve(NumTasks());
426 return scratch_task_set_;
427 }
428
429 private:
430 // Tricky: when a task is optional, it is possible it size min is negative,
431 // but we know that if a task is present, its size should be >= 0. So in the
432 // reason, when we need the size_min and it is currently negative, we can just
433 // ignore it and use zero instead.
434 AffineExpression NegatedSizeOrZero(int t) {
435 if (integer_trail_->LowerBound(sizes_[t]) <= 0) {
436 return AffineExpression(0);
437 }
438 return sizes_[t].Negated();
439 }
440
441 // Generic reason for a <= upper_bound, given that a = b + c in case the
442 // current upper bound of a is not good enough.
443 void AddGenericReason(const AffineExpression& a, IntegerValue upper_bound,
444 const AffineExpression& b, const AffineExpression& c);
445
446 void InitSortedVectors();
447 ABSL_MUST_USE_RESULT bool UpdateCachedValues(int t);
448
449 // Internal function for IncreaseStartMin()/DecreaseEndMax().
450 bool PushIntervalBound(int t, IntegerLiteral lit);
451
452 // This should be called every time a task that is part of a reason or a
453 // bound push to update the items_in_reason_ vector.
454 void FlagItemAsUsedInReason(int t);
455
456 void RunCallbackIfSet();
457
458 Model* model_;
459 SatSolver* sat_solver_;
460 const VariablesAssignment& assignment_;
461 Trail* trail_;
462 IntegerTrail* integer_trail_;
463 GenericLiteralWatcher* watcher_;
464 Linear2Bounds* linear2_bounds_;
465 RootLevelLinear2Bounds* root_level_lin2_bounds_;
466 EnforcementHelper& enforcement_helper_;
467 EnforcementId enforcement_id_;
468
469 FixedCapacityVector<TaskTime> scratch_task_time_vector1_;
470 FixedCapacityVector<TaskTime> scratch_task_time_vector2_;
471 FixedCapacityVector<TaskInfo> scratch_task_set_;
472
473 // The current direction of time, true for forward, false for backward.
474 bool current_time_direction_ = true;
475
476 // All the underlying variables of the tasks.
477 // The vectors are indexed by the task index t.
478 std::vector<AffineExpression> starts_;
479 std::vector<AffineExpression> ends_;
480 std::vector<AffineExpression> sizes_;
481 std::vector<LiteralIndex> reason_for_presence_;
482
483 // The negation of the start/end variable so that SetTimeDirection()
484 // can do its job in O(1) instead of calling NegationOf() on each entry.
485 std::vector<AffineExpression> minus_starts_;
486 std::vector<AffineExpression> minus_ends_;
487
488 // This is used to detect when we need to invalidate the cache.
489 int64_t saved_num_backtracks_ = 0;
490
491 // The caches of all relevant interval values.
492 // These are initially of size capacity and never resized.
493 //
494 // TODO(user): Because of std::swap() in SetTimeDirection, we cannot mark
495 // most of them as "const" and as a result we loose some performance since
496 // the address need to be re-fetched on most access.
497 const int capacity_;
498 const std::unique_ptr<IntegerValue[]> cached_size_min_;
499 std::unique_ptr<IntegerValue[]> cached_start_min_;
500 std::unique_ptr<IntegerValue[]> cached_end_min_;
501 std::unique_ptr<IntegerValue[]> cached_negated_start_max_;
502 std::unique_ptr<IntegerValue[]> cached_negated_end_max_;
503 std::unique_ptr<IntegerValue[]> cached_shifted_start_min_;
504 std::unique_ptr<IntegerValue[]> cached_negated_shifted_end_max_;
505
506 // Sorted vectors returned by the TasksBy*() functions.
507 std::vector<TaskTime> task_by_increasing_start_min_;
508 std::vector<TaskTime> task_by_decreasing_end_max_;
509
510 bool recompute_by_start_max_ = true;
511 bool recompute_by_end_min_ = true;
512 std::vector<TaskTime> task_by_increasing_negated_start_max_;
513 std::vector<TaskTime> task_by_increasing_end_min_;
514
515 // Sorted vector returned by GetEnergyProfile().
516 bool recompute_energy_profile_ = true;
517 std::vector<ProfileEvent> energy_profile_;
518
519 // This one is the most commonly used, so we optimized a bit more its
520 // computation by detecting when there is nothing to do.
521 std::vector<CachedTaskBounds> task_by_increasing_shifted_start_min_;
522 std::vector<CachedTaskBounds> task_by_negated_shifted_end_max_;
523 bool recompute_shifted_start_min_ = true;
524 bool recompute_negated_shifted_end_max_ = true;
525
526 // If recompute_cache_[t] is true, then we need to update all the cached
527 // value for the task t in SynchronizeAndSetTimeDirection().
528 bool recompute_all_cache_ = true;
529 Bitset64<int> recompute_cache_;
530
531 // For large problems, LNS will have a lot of fixed intervals.
532 // And fixed intervals will never changes, so there is no point recomputing
533 // the cache for them.
534 std::vector<int> non_fixed_intervals_;
535
536 // Reason vectors.
537 std::vector<Literal> literal_reason_;
538 std::vector<IntegerLiteral> integer_reason_;
539
540 std::function<void(absl::Span<const int> items, std::vector<Literal>*,
541 std::vector<IntegerLiteral>*)>
542 extra_explanation_callback_ = nullptr;
543
544 SparseBitset<int> used_items_for_reason_;
545
546 // List of watcher to "wake-up" each time one of the task bounds changes.
547 std::vector<int> propagator_ids_;
548};
549
550// Helper class for cumulative constraint to wrap demands and expose concept
551// like energy.
552//
553// In a cumulative constraint, an interval always has a size and a demand, but
554// it can also have a set of "selector" literals each associated with a fixed
555// size / fixed demands. This allows more precise energy estimation.
556//
557// TODO(user): Cache energy min and reason for the non O(1) cases.
559 public:
560 // Hack: this can be called with and empty demand vector as long as
561 // OverrideEnergies() is called to define the energies.
562 SchedulingDemandHelper(absl::Span<const AffineExpression> demands,
563 SchedulingConstraintHelper* helper, Model* model);
564
565 // When defined, the interval will consume this much demand during its whole
566 // duration. Some propagator only relies on the "energy" and thus never uses
567 // this.
568 IntegerValue DemandMin(int t) const;
569 IntegerValue DemandMax(int t) const;
570 IntegerValue LevelZeroDemandMin(int t) const {
571 return integer_trail_->LevelZeroLowerBound(demands_[t]);
572 }
573 bool DemandIsFixed(int t) const;
574 void AddDemandMinReason(int t);
575 void AddDemandMinReason(int t, IntegerValue min_demand);
576 const std::vector<AffineExpression>& Demands() const { return demands_; }
577
578 // Adds the linearized demand (either the affine demand expression, or the
579 // demand part of the decomposed energy if present) to the builder.
580 // It returns false and do not add any term to the builder.if any literal
581 // involved has no integer view.
582 ABSL_MUST_USE_RESULT bool AddLinearizedDemand(
583 int t, LinearConstraintBuilder* builder) const;
584
585 // The "energy" is usually size * demand, but in some non-conventional usage
586 // it might have a more complex formula. In all case, the energy is assumed
587 // to be only consumed during the interval duration.
588 //
589 // Returns false if the energy can overflow and was not computed.
590 //
591 // IMPORTANT: One must call CacheAllEnergyValues() for the values to be
592 // updated. TODO(user): this is error prone, maybe we should revisit. But if
593 // there is many alternatives, we don't want to rescan the list more than a
594 // linear number of time per propagation.
595 //
596 // TODO(user): Add more complex EnergyMinBefore(time) once we also support
597 // expressing the interval as a set of alternatives.
598 //
599 // At level 0, it will filter false literals from decomposed energies.
601 IntegerValue EnergyMin(int t) const { return cached_energies_min_[t]; }
602 IntegerValue EnergyMax(int t) const { return cached_energies_max_[t]; }
603 bool EnergyIsQuadratic(int t) const { return energy_is_quadratic_[t]; }
604 void AddEnergyMinReason(int t);
605
606 // Returns the energy min in [start, end].
607 //
608 // Note(user): These functions are not in O(1) if the decomposition is used,
609 // so we have to be careful in not calling them too often.
610 IntegerValue EnergyMinInWindow(int t, IntegerValue window_start,
611 IntegerValue window_end);
612 void AddEnergyMinInWindowReason(int t, IntegerValue window_start,
613 IntegerValue window_end);
614
615 // Important: This might not do anything depending on the representation of
616 // the energy we have.
617 ABSL_MUST_USE_RESULT bool DecreaseEnergyMax(int t, IntegerValue value);
618
619 // Different optional representation of the energy of an interval.
620 //
621 // Important: first value is size, second value is demand.
622 const std::vector<std::vector<LiteralValueValue>>& DecomposedEnergies()
623 const {
624 return decomposed_energies_;
625 }
626
627 // Visible for testing.
629 const std::vector<std::vector<LiteralValueValue>>& energies);
630 // Returns the decomposed energy terms compatible with the current literal
631 // assignment. It must not be used to create reasons if not at level 0.
632 // It returns en empty vector if the decomposed energy is not available.
633 //
634 // Important: first value is size, second value is demand.
635 std::vector<LiteralValueValue> FilteredDecomposedEnergy(int index);
636
637 // Init all decomposed energies. It needs probing to be finished. This happens
638 // after the creation of the helper.
640
641 private:
642 IntegerValue SimpleEnergyMin(int t) const;
643 IntegerValue SimpleEnergyMax(int t) const;
644 IntegerValue DecomposedEnergyMin(int t) const;
645 IntegerValue DecomposedEnergyMax(int t) const;
646
647 IntegerTrail* integer_trail_;
648 ProductDecomposer* product_decomposer_;
649 SatSolver* sat_solver_; // To get the current propagation level.
650 const VariablesAssignment& assignment_;
651 std::vector<AffineExpression> demands_;
653
654 // Cached value of the energies, as it can be a bit costly to compute.
655 std::vector<IntegerValue> cached_energies_min_;
656 std::vector<IntegerValue> cached_energies_max_;
657 std::vector<bool> energy_is_quadratic_;
658
659 // A representation of the energies as a set of alternative.
660 // If subvector is empty, we don't have this representation.
661 std::vector<std::vector<LiteralValueValue>> decomposed_energies_;
662};
663
664// =============================================================================
665// Utilities
666// =============================================================================
667
668IntegerValue ComputeEnergyMinInWindow(
669 IntegerValue start_min, IntegerValue start_max, IntegerValue end_min,
670 IntegerValue end_max, IntegerValue size_min, IntegerValue demand_min,
671 absl::Span<const LiteralValueValue> filtered_energy,
672 IntegerValue window_start, IntegerValue window_end);
673
674// =============================================================================
675// SchedulingConstraintHelper inlined functions.
676// =============================================================================
677
678inline bool SchedulingConstraintHelper::StartIsFixed(int t) const {
679 return integer_trail_->IsFixed(starts_[t]);
680}
681
683 return integer_trail_->IsFixed(ends_[t]);
684}
685
687 return integer_trail_->IsFixed(sizes_[t]);
688}
689
691 DCHECK_GE(t, 0);
692 DCHECK_LT(t, reason_for_presence_.size());
693 return reason_for_presence_.data()[t] != kNoLiteralIndex;
695
696inline bool SchedulingConstraintHelper::IsPresent(int t) const {
697 DCHECK_GE(t, 0);
698 DCHECK_LT(t, reason_for_presence_.size());
699 const LiteralIndex lit = reason_for_presence_.data()[t];
700 if (lit == kNoLiteralIndex) return true;
701 return assignment_.LiteralIsTrue(Literal(lit));
702}
703
704inline bool SchedulingConstraintHelper::IsAbsent(int t) const {
705 DCHECK_GE(t, 0);
706 DCHECK_LT(t, reason_for_presence_.size());
707 const LiteralIndex lit = reason_for_presence_.data()[t];
708 if (lit == kNoLiteralIndex) return false;
709 return assignment_.LiteralIsFalse(Literal(lit));
710}
711
712inline bool SchedulingConstraintHelper::IsOptional(LiteralIndex lit) const {
713 return lit != kNoLiteralIndex;
714}
715
716inline bool SchedulingConstraintHelper::IsPresent(LiteralIndex lit) const {
717 if (lit == kNoLiteralIndex) return true;
718 return assignment_.LiteralIsTrue(Literal(lit));
719}
721inline bool SchedulingConstraintHelper::IsAbsent(LiteralIndex lit) const {
722 if (lit == kNoLiteralIndex) return false;
723 return assignment_.LiteralIsFalse(Literal(lit));
724}
727 integer_reason_.clear();
728 literal_reason_.clear();
729 enforcement_helper_.AddEnforcementReason(enforcement_id_, &literal_reason_);
730 used_items_for_reason_.ClearAndResize(capacity_);
731}
732
734 DCHECK(IsPresent(t));
735 FlagItemAsUsedInReason(t);
736 if (reason_for_presence_[t] != kNoLiteralIndex) {
737 literal_reason_.push_back(Literal(reason_for_presence_[t]).Negated());
738 }
739}
740
742 DCHECK(IsAbsent(t));
743 FlagItemAsUsedInReason(t);
744 if (reason_for_presence_[t] != kNoLiteralIndex) {
745 literal_reason_.push_back(Literal(reason_for_presence_[t]));
746 }
747}
748
751}
752
753inline void SchedulingConstraintHelper::AddGenericReason(
754 const AffineExpression& a, IntegerValue upper_bound,
755 const AffineExpression& b, const AffineExpression& c) {
756 if (integer_trail_->UpperBound(a) <= upper_bound) {
757 if (a.var != kNoIntegerVariable) {
758 integer_reason_.push_back(a.LowerOrEqual(upper_bound));
759 }
760 return;
761 }
762 CHECK_NE(a.var, kNoIntegerVariable);
763
764 // Here we assume that the upper_bound on a comes from the bound on b + c.
765 const IntegerValue slack = upper_bound - integer_trail_->UpperBound(b) -
766 integer_trail_->UpperBound(c);
767 CHECK_GE(slack, 0);
768 if (b.var == kNoIntegerVariable && c.var == kNoIntegerVariable) return;
769 if (b.var == kNoIntegerVariable) {
770 integer_reason_.push_back(c.LowerOrEqual(upper_bound - b.constant));
771 } else if (c.var == kNoIntegerVariable) {
772 integer_reason_.push_back(b.LowerOrEqual(upper_bound - c.constant));
773 } else {
774 integer_trail_->AppendRelaxedLinearReason(
775 slack, {b.coeff, c.coeff}, {NegationOf(b.var), NegationOf(c.var)},
776 &integer_reason_);
777 }
778}
779
781 int t, IntegerValue lower_bound) {
782 FlagItemAsUsedInReason(t);
783 DCHECK(!IsAbsent(t));
784 if (lower_bound <= 0) return;
785 AddGenericReason(sizes_[t].Negated(), -lower_bound, minus_ends_[t],
786 starts_[t]);
787}
788
790 int t, IntegerValue upper_bound) {
791 FlagItemAsUsedInReason(t);
792 DCHECK(!IsAbsent(t));
793 AddGenericReason(sizes_[t], upper_bound, ends_[t], minus_starts_[t]);
794}
795
797 int t, IntegerValue lower_bound) {
798 FlagItemAsUsedInReason(t);
799 DCHECK(!IsAbsent(t));
800 AddGenericReason(minus_starts_[t], -lower_bound, minus_ends_[t], sizes_[t]);
801}
802
804 int t, IntegerValue upper_bound) {
805 FlagItemAsUsedInReason(t);
806 DCHECK(!IsAbsent(t));
807 AddGenericReason(starts_[t], upper_bound, ends_[t], NegatedSizeOrZero(t));
808}
809
811 int t, IntegerValue lower_bound) {
812 FlagItemAsUsedInReason(t);
813 DCHECK(!IsAbsent(t));
814 AddGenericReason(minus_ends_[t], -lower_bound, minus_starts_[t],
815 NegatedSizeOrZero(t));
816}
817
819 int t, IntegerValue upper_bound) {
820 FlagItemAsUsedInReason(t);
821 DCHECK(!IsAbsent(t));
822 AddGenericReason(ends_[t], upper_bound, starts_[t], sizes_[t]);
823}
824
826 int t, IntegerValue upper_bound) {
827 AddStartMaxReason(t, upper_bound - SizeMin(t));
828}
831 int t, IntegerValue energy_min, IntegerValue time) {
832 if (StartMin(t) >= time) {
833 AddStartMinReason(t, time);
834 } else {
835 AddEndMinReason(t, time + energy_min);
836 }
837 AddSizeMinReason(t, energy_min);
838}
839
841 int t, IntegerValue time_min, IntegerValue time_max) {
842 const IntegerValue energy_min = SizeMin(t);
843 CHECK_LE(time_min + energy_min, time_max);
844 if (StartMin(t) >= time_min) {
845 AddStartMinReason(t, time_min);
846 } else {
847 AddEndMinReason(t, time_min + energy_min);
848 }
849 if (EndMax(t) <= time_max) {
850 AddEndMaxReason(t, time_max);
851 } else {
852 AddStartMaxReason(t, time_max - energy_min);
853 }
854 AddSizeMinReason(t, energy_min);
855}
856
858 literal_reason_.push_back(l);
859}
860
862 integer_reason_.push_back(l);
863}
864
865// Cuts helpers.
867 kStart = 1 << 0,
868 kEnd = 1 << 1,
869 kSize = 1 << 2,
870 kPresence = 1 << 3,
873 Model* model,
874 std::vector<IntegerVariable>* vars,
875 int mask);
876
878 const AffineExpression& capacity, SchedulingDemandHelper* demands_helper,
879 Model* model, std::vector<IntegerVariable>* vars);
880
881} // namespace sat
882} // namespace operations_research
883
884#endif // ORTOOLS_SAT_SCHEDULING_HELPERS_H_
Definition model.h:345
IntegerValue LowerBound(IntegerVariable i) const
Definition integer.h:1537
bool IsFixed(IntegerVariable i) const
Definition integer.h:1545
bool IncrementalPropagate(const std::vector< int > &watch_indices) final
IntervalDefinition GetIntervalDefinition(int index) const
void AddEnergyAfterReason(int t, IntegerValue energy_min, IntegerValue time)
ABSL_MUST_USE_RESULT bool PushTaskOrderWhenPresent(int t_before, int t_after)
void SetExtraExplanationForItemCallback(std::function< void(absl::Span< const int > items, std::vector< Literal > *reason, std::vector< IntegerLiteral > *integer_reason)> extra_explanation_callback)
void AddStartMaxReason(int t, IntegerValue upper_bound)
ABSL_MUST_USE_RESULT bool IncreaseEndMin(int t, IntegerValue value)
void AddSizeMaxReason(int t, IntegerValue upper_bound)
void AppendAndResetReason(std::vector< IntegerLiteral > *integer_reason, std::vector< Literal > *literal_reason)
ABSL_MUST_USE_RESULT bool DecreaseEndMax(int t, IntegerValue value)
absl::Span< const AffineExpression > Sizes() const
FixedCapacityVector< TaskInfo > & GetScratchTaskSet()
const std::vector< ProfileEvent > & GetEnergyProfile()
ABSL_MUST_USE_RESULT bool PushIntegerLiteralIfTaskPresent(int t, IntegerLiteral lit)
absl::Span< const AffineExpression > Starts() const
void AddEndMaxReason(int t, IntegerValue upper_bound)
absl::Span< const AffineExpression > Ends() const
ABSL_MUST_USE_RESULT bool ResetFromSubset(const SchedulingConstraintHelper &other, absl::Span< const int > tasks)
ABSL_MUST_USE_RESULT bool IncreaseStartMin(int t, IntegerValue value)
void AddEndMinReason(int t, IntegerValue lower_bound)
void AddReasonForUpperBoundLowerThan(LinearExpression2 expr, IntegerValue ub)
SchedulingConstraintHelper(std::vector< AffineExpression > starts, std::vector< AffineExpression > ends, std::vector< AffineExpression > sizes, std::vector< LiteralIndex > reason_for_presence, Model *model)
absl::Span< const CachedTaskBounds > TaskByIncreasingShiftedStartMin()
ABSL_MUST_USE_RESULT bool SynchronizeAndSetTimeDirection(bool is_forward)
FixedCapacityVector< TaskTime > & GetScratchTaskTimeVector1()
ABSL_MUST_USE_RESULT bool PushIntegerLiteral(IntegerLiteral lit)
void AddShiftedEndMaxReason(int t, IntegerValue upper_bound)
IntegerValue GetMinOverlap(int t, IntegerValue start, IntegerValue end) const
void RegisterWith(GenericLiteralWatcher *watcher, absl::Span< const Literal > enforcement_literals)
void AddStartMinReason(int t, IntegerValue lower_bound)
absl::Span< const CachedTaskBounds > TaskByIncreasingNegatedShiftedEndMax()
void AddEnergyMinInIntervalReason(int t, IntegerValue min, IntegerValue max)
FixedCapacityVector< TaskTime > & GetScratchTaskTimeVector2()
void ImportReasonsFromOther(const SchedulingConstraintHelper &other_helper)
std::vector< LiteralValueValue > FilteredDecomposedEnergy(int index)
IntegerValue EnergyMinInWindow(int t, IntegerValue window_start, IntegerValue window_end)
const std::vector< std::vector< LiteralValueValue > > & DecomposedEnergies() const
void AddEnergyMinInWindowReason(int t, IntegerValue window_start, IntegerValue window_end)
SchedulingDemandHelper(absl::Span< const AffineExpression > demands, SchedulingConstraintHelper *helper, Model *model)
void OverrideDecomposedEnergies(const std::vector< std::vector< LiteralValueValue > > &energies)
const std::vector< AffineExpression > & Demands() const
ABSL_MUST_USE_RESULT bool DecreaseEnergyMax(int t, IntegerValue value)
ABSL_MUST_USE_RESULT bool AddLinearizedDemand(int t, LinearConstraintBuilder *builder) const
const LiteralIndex kNoLiteralIndex(-1)
std::vector< IntegerVariable > NegationOf(absl::Span< const IntegerVariable > vars)
Definition integer.cc:52
const IntegerVariable kNoIntegerVariable(-1)
void AddIntegerVariableFromIntervals(const SchedulingConstraintHelper *helper, Model *model, std::vector< IntegerVariable > *vars, int mask)
void AppendVariablesFromCapacityAndDemands(const AffineExpression &capacity, SchedulingDemandHelper *demands_helper, Model *model, std::vector< IntegerVariable > *vars)
IntegerValue ComputeEnergyMinInWindow(IntegerValue start_min, IntegerValue start_max, IntegerValue end_min, IntegerValue end_max, IntegerValue size_min, IntegerValue demand_min, absl::Span< const LiteralValueValue > filtered_energy, IntegerValue window_start, IntegerValue window_end)
OR-Tools root namespace.
ClosedInterval::Iterator end(ClosedInterval interval)
IntegerLiteral LowerOrEqual(IntegerValue bound) const
bool operator>(CachedTaskBounds other) const
bool operator<(CachedTaskBounds other) const
friend H AbslHashValue(H h, const IntervalDefinition &i)
bool operator==(const IntervalDefinition &other) const
bool operator<(TaskTime other) const
bool operator>(TaskTime other) const