Google OR-Tools v9.12
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
cumulative.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 <functional>
18#include <vector>
19
20#include "absl/log/check.h"
21#include "absl/strings/str_join.h"
22#include "absl/types/span.h"
26#include "ortools/sat/integer.h"
31#include "ortools/sat/model.h"
35#include "ortools/sat/sat_parameters.pb.h"
40
41namespace operations_research {
42namespace sat {
43
44std::function<void(Model*)> Cumulative(
45 const std::vector<IntervalVariable>& vars,
46 absl::Span<const AffineExpression> demands, AffineExpression capacity,
48 return [=, demands = std::vector<AffineExpression>(
49 demands.begin(), demands.end())](Model* model) mutable {
50 auto* intervals = model->GetOrCreate<IntervalsRepository>();
51 auto* encoder = model->GetOrCreate<IntegerEncoder>();
52 auto* integer_trail = model->GetOrCreate<IntegerTrail>();
53 auto* watcher = model->GetOrCreate<GenericLiteralWatcher>();
54 SatSolver* sat_solver = model->GetOrCreate<SatSolver>();
55
56 if (!integer_trail->SafeEnqueue(capacity.GreaterOrEqual(0), {})) {
57 sat_solver->NotifyThatModelIsUnsat();
58 }
59 if (demands.empty()) {
60 // If there is no demand, since we already added a constraint that the
61 // capacity is not negative above, we can stop here.
62 return;
63 }
64
65 // Redundant constraints to ensure that the resource capacity is high enough
66 // for each task. Also ensure that no task consumes more resource than what
67 // is available. This is useful because the subsequent propagators do not
68 // filter the capacity variable very well.
69 for (int i = 0; i < demands.size(); ++i) {
70 if (intervals->MaxSize(vars[i]) == 0) continue;
71
72 LinearConstraintBuilder builder(model, kMinIntegerValue, IntegerValue(0));
73 builder.AddTerm(demands[i], IntegerValue(1));
74 builder.AddTerm(capacity, IntegerValue(-1));
75 LinearConstraint ct = builder.Build();
76
77 std::vector<Literal> enforcement_literals;
78 if (intervals->IsOptional(vars[i])) {
79 enforcement_literals.push_back(intervals->PresenceLiteral(vars[i]));
80 }
81
82 // If the interval can be of size zero, it currently do not count towards
83 // the capacity. TODO(user): Change that since we have optional interval
84 // for this.
85 if (intervals->MinSize(vars[i]) == 0) {
86 enforcement_literals.push_back(encoder->GetOrCreateAssociatedLiteral(
87 intervals->Size(vars[i]).GreaterOrEqual(IntegerValue(1))));
88 }
89
90 if (enforcement_literals.empty()) {
91 LoadLinearConstraint(ct, model);
92 } else {
93 LoadConditionalLinearConstraint(enforcement_literals, ct, model);
94 }
95 }
96
97 if (vars.size() == 1) return;
98
99 const SatParameters& parameters = *(model->GetOrCreate<SatParameters>());
100
101 // Detect a subset of intervals that needs to be in disjunction and add a
102 // Disjunctive() constraint over them.
103 if (parameters.use_disjunctive_constraint_in_cumulative()) {
104 // TODO(user): We need to exclude intervals that can be of size zero
105 // because the disjunctive do not "ignore" them like the cumulative
106 // does. That is, the interval [2,2) will be assumed to be in
107 // disjunction with [1, 3) for instance. We need to uniformize the
108 // handling of interval with size zero.
109 std::vector<IntervalVariable> in_disjunction;
110 IntegerValue min_of_demands = kMaxIntegerValue;
111 const IntegerValue capa_max = integer_trail->UpperBound(capacity);
112 for (int i = 0; i < vars.size(); ++i) {
113 const IntegerValue size_min = intervals->MinSize(vars[i]);
114 if (size_min == 0) continue;
115 const IntegerValue demand_min = integer_trail->LowerBound(demands[i]);
116 if (2 * demand_min > capa_max) {
117 in_disjunction.push_back(vars[i]);
118 min_of_demands = std::min(min_of_demands, demand_min);
119 }
120 }
121
122 // Liftable? We might be able to add one more interval!
123 if (!in_disjunction.empty()) {
124 IntervalVariable lift_var;
125 IntegerValue lift_size(0);
126 for (int i = 0; i < vars.size(); ++i) {
127 const IntegerValue size_min = intervals->MinSize(vars[i]);
128 if (size_min == 0) continue;
129 const IntegerValue demand_min = integer_trail->LowerBound(demands[i]);
130 if (2 * demand_min > capa_max) continue;
131 if (min_of_demands + demand_min > capa_max && size_min > lift_size) {
132 lift_var = vars[i];
133 lift_size = size_min;
134 }
135 }
136 if (lift_size > 0) {
137 in_disjunction.push_back(lift_var);
138 }
139 }
140
141 // Add a disjunctive constraint on the intervals in in_disjunction. Do not
142 // create the cumulative at all when all intervals must be in disjunction.
143 //
144 // TODO(user): Do proper experiments to see how beneficial this is, the
145 // disjunctive will propagate more but is also using slower algorithms.
146 // That said, this is more a question of optimizing the disjunctive
147 // propagation code.
148 //
149 // TODO(user): Another "known" idea is to detect pair of tasks that must
150 // be in disjunction and to create a Boolean to indicate which one is
151 // before the other. It shouldn't change the propagation, but may result
152 // in a faster one with smaller explanations, and the solver can also take
153 // decision on such Boolean.
154 //
155 // TODO(user): A better place for stuff like this could be in the
156 // presolver so that it is easier to disable and play with alternatives.
157 if (in_disjunction.size() > 1) AddDisjunctive(in_disjunction, model);
158 if (in_disjunction.size() == vars.size()) return;
159 }
160
161 if (helper == nullptr) {
162 helper = intervals->GetOrCreateHelper(vars);
163 }
164 SchedulingDemandHelper* demands_helper =
165 intervals->GetOrCreateDemandHelper(helper, demands);
166 intervals->RegisterCumulative({capacity, helper, demands_helper});
167
168 // For each variables that is after a subset of task ends (i.e. like a
169 // makespan objective), we detect it and add a special constraint to
170 // propagate it.
171 //
172 // TODO(user): Models that include the makespan as a special interval might
173 // be better, but then not everyone does that. In particular this code
174 // allows to have decent lower bound on the large cumulative minizinc
175 // instances.
176 //
177 // TODO(user): this require the precedence constraints to be already loaded,
178 // and there is no guarantee of that currently. Find a more robust way.
179 //
180 // TODO(user): There is a bit of code duplication with the disjunctive
181 // precedence propagator. Abstract more?
182 if (parameters.use_hard_precedences_in_cumulative()) {
183 // The CumulativeIsAfterSubsetConstraint() always reset the helper to the
184 // forward time direction, so it is important to also precompute the
185 // precedence relation using the same direction! This is needed in case
186 // the helper has already been used and set in the other direction.
187 if (!helper->SynchronizeAndSetTimeDirection(true)) {
188 model->GetOrCreate<SatSolver>()->NotifyThatModelIsUnsat();
189 return;
190 }
191
192 std::vector<IntegerVariable> index_to_end_vars;
193 std::vector<int> index_to_task;
194 index_to_end_vars.clear();
195 for (int t = 0; t < helper->NumTasks(); ++t) {
196 const AffineExpression& end_exp = helper->Ends()[t];
197
198 // TODO(user): Handle generic affine relation?
199 if (end_exp.var == kNoIntegerVariable || end_exp.coeff != 1) continue;
200 index_to_end_vars.push_back(end_exp.var);
201 index_to_task.push_back(t);
202 }
203
204 // TODO(user): This can lead to many constraints. By analyzing a bit more
205 // the precedences, we could restrict that. In particular for cases were
206 // the cumulative is always (bunch of tasks B), T, (bunch of tasks A) and
207 // task T always in the middle, we never need to explicit list the
208 // precedence of a task in B with a task in A.
209 //
210 // TODO(user): If more than one variable are after the same set of
211 // intervals, we should regroup them in a single constraint rather than
212 // having two independent constraint doing the same propagation.
213 std::vector<FullIntegerPrecedence> full_precedences;
214 if (parameters.exploit_all_precedences()) {
215 model->GetOrCreate<PrecedenceRelations>()->ComputeFullPrecedences(
216 index_to_end_vars, &full_precedences);
217 }
218 for (const FullIntegerPrecedence& data : full_precedences) {
219 const int size = data.indices.size();
220 if (size <= 1) continue;
221
222 const IntegerVariable var = data.var;
223 std::vector<int> subtasks;
224 std::vector<IntegerValue> offsets;
225 IntegerValue sum_of_demand_max(0);
226 for (int i = 0; i < size; ++i) {
227 const int t = index_to_task[data.indices[i]];
228 subtasks.push_back(t);
229 sum_of_demand_max += integer_trail->LevelZeroUpperBound(demands[t]);
230
231 // We have var >= end_exp.var + offset, so
232 // var >= (end_exp.var + end_exp.cte) + (offset - end_exp.cte)
233 // var >= task end + new_offset.
234 const AffineExpression& end_exp = helper->Ends()[t];
235 offsets.push_back(data.offsets[i] - end_exp.constant);
236 }
237 if (sum_of_demand_max > integer_trail->LevelZeroLowerBound(capacity)) {
238 VLOG(2) << "Cumulative precedence constraint! var= " << var
239 << " #task: " << absl::StrJoin(subtasks, ",");
241 new CumulativeIsAfterSubsetConstraint(var, capacity, subtasks,
242 offsets, helper,
243 demands_helper, model);
244 constraint->RegisterWith(watcher);
245 model->TakeOwnership(constraint);
246 }
247 }
248 }
249
250 // Propagator responsible for applying Timetabling filtering rule. It
251 // increases the minimum of the start variables, decrease the maximum of the
252 // end variables, and increase the minimum of the capacity variable.
253 TimeTablingPerTask* time_tabling =
254 new TimeTablingPerTask(capacity, helper, demands_helper, model);
255 time_tabling->RegisterWith(watcher);
256 model->TakeOwnership(time_tabling);
257
258 // Propagator responsible for applying the Overload Checking filtering rule.
259 // It increases the minimum of the capacity variable.
260 if (parameters.use_overload_checker_in_cumulative()) {
261 AddCumulativeOverloadChecker(capacity, helper, demands_helper, model);
262 }
263 if (parameters.use_conservative_scale_overload_checker()) {
264 // Since we use the potential DFF conflict on demands to apply the
265 // heuristic, only do so if any demand is greater than 1.
266 bool any_demand_greater_than_one = false;
267 for (int i = 0; i < vars.size(); ++i) {
268 const IntegerValue demand_min = integer_trail->LowerBound(demands[i]);
269 if (demand_min > 1) {
270 any_demand_greater_than_one = true;
271 break;
272 }
273 }
274 if (any_demand_greater_than_one) {
275 AddCumulativeOverloadCheckerDff(capacity, helper, demands_helper,
276 model);
277 }
278 }
279
280 // Propagator responsible for applying the Timetable Edge finding filtering
281 // rule. It increases the minimum of the start variables and decreases the
282 // maximum of the end variables,
283 if (parameters.use_timetable_edge_finding_in_cumulative() &&
284 helper->NumTasks() <=
285 parameters.max_num_intervals_for_timetable_edge_finding()) {
286 TimeTableEdgeFinding* time_table_edge_finding =
287 new TimeTableEdgeFinding(capacity, helper, demands_helper, model);
288 time_table_edge_finding->RegisterWith(watcher);
289 model->TakeOwnership(time_table_edge_finding);
290 }
291 };
292}
293
294std::function<void(Model*)> CumulativeTimeDecomposition(
295 absl::Span<const IntervalVariable> vars,
296 absl::Span<const AffineExpression> demands, AffineExpression capacity,
298 return [=, vars = std::vector<IntervalVariable>(vars.begin(), vars.end()),
299 demands = std::vector<AffineExpression>(
300 demands.begin(), demands.end())](Model* model) {
301 if (vars.empty()) return;
302
303 IntegerTrail* integer_trail = model->GetOrCreate<IntegerTrail>();
304 CHECK(integer_trail->IsFixed(capacity));
305 const Coefficient fixed_capacity(
306 integer_trail->UpperBound(capacity).value());
307
308 const int num_tasks = vars.size();
309 SatSolver* sat_solver = model->GetOrCreate<SatSolver>();
310 IntegerEncoder* encoder = model->GetOrCreate<IntegerEncoder>();
311 IntervalsRepository* repository = model->GetOrCreate<IntervalsRepository>();
312
313 std::vector<AffineExpression> start_exprs;
314 std::vector<AffineExpression> end_exprs;
315 std::vector<IntegerValue> fixed_demands;
316
317 for (int t = 0; t < num_tasks; ++t) {
318 start_exprs.push_back(repository->Start(vars[t]));
319 end_exprs.push_back(repository->End(vars[t]));
320 CHECK(integer_trail->IsFixed(demands[t]));
321 fixed_demands.push_back(integer_trail->LowerBound(demands[t]));
322 }
323
324 // Compute time range.
325 IntegerValue min_start = kMaxIntegerValue;
326 IntegerValue max_end = kMinIntegerValue;
327 for (int t = 0; t < num_tasks; ++t) {
328 min_start =
329 std::min(min_start, integer_trail->LowerBound(start_exprs[t]));
330 max_end = std::max(max_end, integer_trail->UpperBound(end_exprs[t]));
331 }
332
333 for (IntegerValue time = min_start; time < max_end; ++time) {
334 std::vector<LiteralWithCoeff> literals_with_coeff;
335 for (int t = 0; t < num_tasks; ++t) {
336 if (!sat_solver->Propagate()) return;
337 const IntegerValue start_min =
338 integer_trail->LowerBound(start_exprs[t]);
339 const IntegerValue end_max = integer_trail->UpperBound(end_exprs[t]);
340 if (end_max <= time || time < start_min || fixed_demands[t] == 0) {
341 continue;
342 }
343
344 // Task t consumes the resource at time if consume_condition is true.
345 std::vector<Literal> consume_condition;
346 const Literal consume = Literal(model->Add(NewBooleanVariable()), true);
347
348 // Task t consumes the resource at time if it is present.
349 if (repository->IsOptional(vars[t])) {
350 consume_condition.push_back(repository->PresenceLiteral(vars[t]));
351 }
352
353 // Task t overlaps time.
354 consume_condition.push_back(encoder->GetOrCreateAssociatedLiteral(
355 start_exprs[t].LowerOrEqual(IntegerValue(time))));
356 consume_condition.push_back(encoder->GetOrCreateAssociatedLiteral(
357 end_exprs[t].GreaterOrEqual(IntegerValue(time + 1))));
358
359 model->Add(ReifiedBoolAnd(consume_condition, consume));
360
361 // this is needed because we currently can't create a boolean variable
362 // if the model is unsat.
363 if (sat_solver->ModelIsUnsat()) return;
364
365 literals_with_coeff.push_back(
366 LiteralWithCoeff(consume, Coefficient(fixed_demands[t].value())));
367 }
368 // The profile cannot exceed the capacity at time.
369 sat_solver->AddLinearConstraint(false, Coefficient(0), true,
370 fixed_capacity, &literals_with_coeff);
371
372 // Abort if UNSAT.
373 if (sat_solver->ModelIsUnsat()) return;
374 }
375 };
376}
377
378std::function<void(Model*)> CumulativeUsingReservoir(
379 absl::Span<const IntervalVariable> vars,
380 absl::Span<const AffineExpression> demands, AffineExpression capacity,
382 return [=, vars = std::vector<IntervalVariable>(vars.begin(), vars.end()),
383 demands = std::vector<AffineExpression>(
384 demands.begin(), demands.end())](Model* model) {
385 if (vars.empty()) return;
386
387 auto* integer_trail = model->GetOrCreate<IntegerTrail>();
388 auto* encoder = model->GetOrCreate<IntegerEncoder>();
389 auto* repository = model->GetOrCreate<IntervalsRepository>();
390
391 CHECK(integer_trail->IsFixed(capacity));
392 const IntegerValue fixed_capacity(
393 integer_trail->UpperBound(capacity).value());
394
395 std::vector<AffineExpression> times;
396 std::vector<AffineExpression> deltas;
397 std::vector<Literal> presences;
398
399 const int num_tasks = vars.size();
400 for (int t = 0; t < num_tasks; ++t) {
401 CHECK(integer_trail->IsFixed(demands[t]));
402 times.push_back(repository->Start(vars[t]));
403 deltas.push_back(demands[t]);
404 times.push_back(repository->End(vars[t]));
405 deltas.push_back(demands[t].Negated());
406 if (repository->IsOptional(vars[t])) {
407 presences.push_back(repository->PresenceLiteral(vars[t]));
408 presences.push_back(repository->PresenceLiteral(vars[t]));
409 } else {
410 presences.push_back(encoder->GetTrueLiteral());
411 presences.push_back(encoder->GetTrueLiteral());
412 }
413 }
414 AddReservoirConstraint(times, deltas, presences, 0, fixed_capacity.value(),
415 model);
416 };
417}
418
419} // namespace sat
420} // namespace operations_research
IntegerValue LowerBound(IntegerVariable i) const
Returns the current lower/upper bound of the given integer variable.
Definition integer.h:1301
IntegerValue UpperBound(IntegerVariable i) const
Definition integer.h:1305
bool IsFixed(IntegerVariable i) const
Checks if the variable is fixed.
Definition integer.h:1309
void AddTerm(IntegerVariable var, IntegerValue coeff)
bool AddLinearConstraint(bool use_lower_bound, Coefficient lower_bound, bool use_upper_bound, Coefficient upper_bound, std::vector< LiteralWithCoeff > *cst)
ABSL_MUST_USE_RESULT bool Propagate()
int NumTasks() const
Returns the number of task.
absl::Span< const AffineExpression > Ends() const
ABSL_MUST_USE_RESULT bool SynchronizeAndSetTimeDirection(bool is_forward)
void RegisterWith(GenericLiteralWatcher *watcher)
void RegisterWith(GenericLiteralWatcher *watcher)
Definition timetable.cc:344
constexpr IntegerValue kMaxIntegerValue(std::numeric_limits< IntegerValue::ValueType >::max() - 1)
std::function< void(Model *)> CumulativeUsingReservoir(absl::Span< const IntervalVariable > vars, absl::Span< const AffineExpression > demands, AffineExpression capacity, SchedulingConstraintHelper *helper)
Another testing code, same assumptions as the CumulativeTimeDecomposition().
std::function< BooleanVariable(Model *)> NewBooleanVariable()
Definition integer.h:1477
void LoadConditionalLinearConstraint(const absl::Span< const Literal > enforcement_literals, const LinearConstraint &cst, Model *model)
LinearConstraint version.
constexpr IntegerValue kMinIntegerValue(-kMaxIntegerValue.value())
std::function< void(Model *)> ReifiedBoolAnd(absl::Span< const Literal > literals, Literal r)
const IntegerVariable kNoIntegerVariable(-1)
void AddCumulativeOverloadCheckerDff(AffineExpression capacity, SchedulingConstraintHelper *helper, SchedulingDemandHelper *demands, Model *model)
void AddDisjunctive(const std::vector< IntervalVariable > &intervals, Model *model)
std::function< void(Model *)> Cumulative(const std::vector< IntervalVariable > &vars, absl::Span< const AffineExpression > demands, AffineExpression capacity, SchedulingConstraintHelper *helper)
Definition cumulative.cc:44
void AddReservoirConstraint(std::vector< AffineExpression > times, std::vector< AffineExpression > deltas, std::vector< Literal > presences, int64_t min_level, int64_t max_level, Model *model)
Definition timetable.cc:32
std::function< void(Model *)> CumulativeTimeDecomposition(absl::Span< const IntervalVariable > vars, absl::Span< const AffineExpression > demands, AffineExpression capacity, SchedulingConstraintHelper *helper)
void LoadLinearConstraint(const ConstraintProto &ct, Model *m)
void AddCumulativeOverloadChecker(AffineExpression capacity, SchedulingConstraintHelper *helper, SchedulingDemandHelper *demands, Model *model)
In SWIG mode, we don't want anything besides these top-level includes.
IntegerLiteral GreaterOrEqual(IntegerValue bound) const
var * coeff + constant >= bound.
Represents a term in a pseudo-Boolean formula.