OR-Tools  9.1
scheduling_cuts.cc
Go to the documentation of this file.
1 // Copyright 2010-2021 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 <cmath>
18 #include <cstdint>
19 #include <functional>
20 #include <limits>
21 #include <memory>
22 #include <string>
23 #include <utility>
24 #include <vector>
25 
27 #include "ortools/base/stl_util.h"
29 #include "ortools/sat/diffn_util.h"
30 #include "ortools/sat/integer.h"
31 #include "ortools/sat/intervals.h"
34 #include "ortools/sat/sat_base.h"
35 #include "ortools/sat/util.h"
37 
38 namespace operations_research {
39 namespace sat {
40 
41 namespace {
42 
43 // Minimum amount of violation of the cut constraint by the solution. This
44 // is needed to avoid numerical issues and adding cuts with minor effect.
45 const double kMinCutViolation = 1e-4;
46 
47 // Returns the lp value of a Literal.
48 double GetLiteralLpValue(
49  const Literal lit,
51  const IntegerEncoder* encoder) {
52  const IntegerVariable direct_view = encoder->GetLiteralView(lit);
53  if (direct_view != kNoIntegerVariable) {
54  return lp_values[direct_view];
55  }
56  const IntegerVariable opposite_view = encoder->GetLiteralView(lit.Negated());
57  DCHECK_NE(opposite_view, kNoIntegerVariable);
58  return 1.0 - lp_values[opposite_view];
59 }
60 
61 void AddIntegerVariableFromIntervals(SchedulingConstraintHelper* helper,
62  Model* model,
63  std::vector<IntegerVariable>* vars) {
64  IntegerEncoder* encoder = model->GetOrCreate<IntegerEncoder>();
65  for (int t = 0; t < helper->NumTasks(); ++t) {
66  if (helper->Starts()[t].var != kNoIntegerVariable) {
67  vars->push_back(helper->Starts()[t].var);
68  }
69  if (helper->Sizes()[t].var != kNoIntegerVariable) {
70  vars->push_back(helper->Sizes()[t].var);
71  }
72  if (helper->Ends()[t].var != kNoIntegerVariable) {
73  vars->push_back(helper->Ends()[t].var);
74  }
75  if (helper->IsOptional(t) && !helper->IsAbsent(t) &&
76  !helper->IsPresent(t)) {
77  const Literal l = helper->PresenceLiteral(t);
78  if (encoder->GetLiteralView(l) == kNoIntegerVariable &&
79  encoder->GetLiteralView(l.Negated()) == kNoIntegerVariable) {
81  }
82  const IntegerVariable direct_view = encoder->GetLiteralView(l);
83  if (direct_view != kNoIntegerVariable) {
84  vars->push_back(direct_view);
85  } else {
86  vars->push_back(encoder->GetLiteralView(l.Negated()));
87  DCHECK_NE(vars->back(), kNoIntegerVariable);
88  }
89  }
90  }
92 }
93 
94 } // namespace
95 
96 std::function<bool(const absl::StrongVector<IntegerVariable, double>&,
97  LinearConstraintManager*)>
98 GenerateCumulativeEnergyCuts(const std::string& cut_name,
100  const std::vector<IntegerVariable>& demands,
101  const std::vector<LinearExpression>& energies,
103  Trail* trail = model->GetOrCreate<Trail>();
104  IntegerTrail* integer_trail = model->GetOrCreate<IntegerTrail>();
105  IntegerEncoder* encoder = model->GetOrCreate<IntegerEncoder>();
106 
107  return [capacity, demands, energies, trail, integer_trail, helper, model,
108  cut_name,
109  encoder](const absl::StrongVector<IntegerVariable, double>& lp_values,
110  LinearConstraintManager* manager) {
111  if (trail->CurrentDecisionLevel() > 0) return true;
112 
113  const auto demand_is_fixed = [integer_trail, &demands](int i) {
114  return demands.empty() || integer_trail->IsFixed(demands[i]);
115  };
116  const auto demand_min = [integer_trail, &demands](int i) {
117  return demands.empty() ? IntegerValue(1)
118  : integer_trail->LowerBound(demands[i]);
119  };
120  const auto demand_max = [integer_trail, &demands](int i) {
121  return demands.empty() ? IntegerValue(1)
122  : integer_trail->UpperBound(demands[i]);
123  };
124 
125  std::vector<int> active_intervals;
126  for (int i = 0; i < helper->NumTasks(); ++i) {
127  if (!helper->IsAbsent(i) && demand_max(i) > 0 && helper->SizeMin(i) > 0) {
128  active_intervals.push_back(i);
129  }
130  }
131 
132  if (active_intervals.size() < 2) return true;
133 
134  std::sort(active_intervals.begin(), active_intervals.end(),
135  [helper](int a, int b) {
136  return helper->StartMin(a) < helper->StartMin(b) ||
137  (helper->StartMin(a) == helper->StartMin(b) &&
138  helper->EndMax(a) < helper->EndMax(b));
139  });
140 
141  const IntegerValue capacity_max = integer_trail->UpperBound(capacity);
142  IntegerValue processed_start = kMinIntegerValue;
143  for (int i1 = 0; i1 + 1 < active_intervals.size(); ++i1) {
144  const int start_index = active_intervals[i1];
145  DCHECK(!helper->IsAbsent(start_index));
146 
147  // We want maximal cuts. For any start_min value, we only need to create
148  // cuts starting from the first interval having this start_min value.
149  if (helper->StartMin(start_index) == processed_start) {
150  continue;
151  } else {
152  processed_start = helper->StartMin(start_index);
153  }
154 
155  // For each start time, we will keep the most violated cut generated while
156  // scanning the residual intervals.
157  int end_index_of_max_violation = -1;
158  double max_relative_violation = 1.01;
159  IntegerValue start_of_max_violation(0);
160  IntegerValue end_of_max_violation(0);
161  std::vector<int> lifted_intervals_of_max_violation;
162 
163  // Accumulate intervals and check for potential cuts.
164  double energy_lp = 0.0;
165  IntegerValue min_of_starts = kMaxIntegerValue;
166  IntegerValue max_of_ends = kMinIntegerValue;
167 
168  // We sort all tasks (start_min(task) >= start_min(start_index) by
169  // increasing end max.
170  std::vector<int> residual_intervals(active_intervals.begin() + i1,
171  active_intervals.end());
172  // Keep track of intervals not included in the potential cut.
173  // TODO(user): remove ?
174  std::set<int> intervals_not_visited(active_intervals.begin(),
175  active_intervals.end());
176  std::sort(
177  residual_intervals.begin(), residual_intervals.end(),
178  [&](int a, int b) { return helper->EndMax(a) < helper->EndMax(b); });
179 
180  // Let's process residual tasks and evaluate the cut violation of the cut
181  // at each step. We follow the same structure as the cut creation code
182  // below.
183  for (int i2 = 0; i2 < residual_intervals.size(); ++i2) {
184  const int t = residual_intervals[i2];
185  intervals_not_visited.erase(t);
186  if (helper->IsPresent(t)) {
187  if (demand_is_fixed(t)) {
188  if (helper->SizeIsFixed(t)) {
189  energy_lp += ToDouble(helper->SizeMin(t) * demand_min(t));
190  } else {
191  energy_lp += ToDouble(demand_min(t)) *
192  helper->Sizes()[t].LpValue(lp_values);
193  }
194  } else if (helper->SizeIsFixed(t)) {
195  DCHECK(!demands.empty());
196  energy_lp += lp_values[demands[t]] * ToDouble(helper->SizeMin(t));
197  } else if (!energies.empty()) {
198  energy_lp += energies[t].LpValue(lp_values);
199  } else { // demand and size are not fixed.
200  DCHECK(!demands.empty());
201  energy_lp +=
202  ToDouble(demand_min(t)) * helper->Sizes()[t].LpValue(lp_values);
203  energy_lp += lp_values[demands[t]] * ToDouble(helper->SizeMin(t));
204  energy_lp -= ToDouble(demand_min(t) * helper->SizeMin(t));
205  }
206  } else {
207  // TODO(user): Use the energy min if better than size_min *
208  // demand_min, here and when building the cut.
209  energy_lp += GetLiteralLpValue(helper->PresenceLiteral(t), lp_values,
210  encoder) *
211  ToDouble(helper->SizeMin(t) * demand_min(t));
212  }
213 
214  min_of_starts = std::min(min_of_starts, helper->StartMin(t));
215  max_of_ends = std::max(max_of_ends, helper->EndMax(t));
216 
217  // Dominance rule. If the next interval also fits in
218  // [min_of_starts, max_of_ends], the cut will be stronger with the
219  // next interval.
220  if (i2 + 1 < residual_intervals.size() &&
221  helper->StartMin(residual_intervals[i2 + 1]) >= min_of_starts &&
222  helper->EndMax(residual_intervals[i2 + 1]) <= max_of_ends) {
223  continue;
224  }
225 
226  // Compute forced contributions from intervals not included in
227  // [min_of_starts..max_of_ends].
228  //
229  // TODO(user): We could precompute possible intervals and store them
230  // by start_max, end_min to reduce the complexity.
231  std::vector<int> lifted_intervals;
232  std::vector<IntegerValue> lifted_min_overlap;
233  double forced_contrib_lp = 0.0;
234  for (const int t : intervals_not_visited) {
235  // It should not happen because of the 2 dominance rules above.
236  if (helper->StartMin(t) >= min_of_starts &&
237  helper->EndMax(t) <= max_of_ends) {
238  continue;
239  }
240 
241  const IntegerValue min_overlap =
242  helper->GetMinOverlap(t, min_of_starts, max_of_ends);
243 
244  if (min_overlap <= 0) continue;
245 
246  lifted_intervals.push_back(t);
247 
248  if (helper->IsPresent(t)) {
249  if (demand_is_fixed(t)) {
250  forced_contrib_lp += ToDouble(min_overlap * demand_min(t));
251  } else {
252  DCHECK(!demands.empty());
253  forced_contrib_lp +=
254  lp_values[demands[t]] * ToDouble(min_overlap);
255  }
256  } else {
257  forced_contrib_lp += GetLiteralLpValue(helper->PresenceLiteral(t),
258  lp_values, encoder) *
259  ToDouble(min_overlap * demand_min(t));
260  }
261  }
262 
263  // Compute the violation of the potential cut.
264  const double relative_violation =
265  (energy_lp + forced_contrib_lp) /
266  ToDouble((max_of_ends - min_of_starts) * capacity_max);
267  if (relative_violation > max_relative_violation) {
268  end_index_of_max_violation = i2;
269  max_relative_violation = relative_violation;
270  start_of_max_violation = min_of_starts;
271  end_of_max_violation = max_of_ends;
272  lifted_intervals_of_max_violation = lifted_intervals;
273  }
274  }
275 
276  if (end_index_of_max_violation == -1) continue;
277 
278  // A maximal violated cut has been found.
279  bool cut_generated = true;
280  bool has_opt_cuts = false;
281  bool lifted = false;
282  bool has_quadratic_cuts = false;
283  bool use_energy = false;
284 
285  LinearConstraintBuilder cut(model, kMinIntegerValue, IntegerValue(0));
286 
287  // Build the cut.
288  cut.AddTerm(capacity, start_of_max_violation - end_of_max_violation);
289  for (int i2 = 0; i2 <= end_index_of_max_violation; ++i2) {
290  const int t = residual_intervals[i2];
291  if (helper->IsPresent(t)) {
292  if (demand_is_fixed(t)) {
293  cut.AddTerm(helper->Sizes()[t], demand_min(t));
294  } else if (!helper->SizeIsFixed(t) && !energies.empty()) {
295  // We favor the energy info instead of the McCormick relaxation.
296  cut.AddLinearExpression(energies[t]);
297  use_energy = true;
298  } else {
299  // This will add linear term if the size is fixed.
300  cut.AddQuadraticLowerBound(helper->Sizes()[t], demands[t],
301  integer_trail);
302  if (!helper->SizeIsFixed(t)) {
303  has_quadratic_cuts = true;
304  }
305  }
306  } else {
307  // TODO(user): use the offset of the energy expression if better
308  // than size_min * demand_min.
309  has_opt_cuts = true;
310  if (!helper->SizeIsFixed(t) || !demand_is_fixed(t)) {
311  has_quadratic_cuts = true;
312  }
313  if (!cut.AddLiteralTerm(helper->PresenceLiteral(t),
314  helper->SizeMin(t) * demand_min(t))) {
315  cut_generated = false;
316  break;
317  }
318  }
319  }
320 
321  for (int i2 = 0; i2 < lifted_intervals_of_max_violation.size(); ++i2) {
322  const int t = lifted_intervals_of_max_violation[i2];
323  const IntegerValue min_overlap = helper->GetMinOverlap(
324  t, start_of_max_violation, end_of_max_violation);
325  lifted = true;
326 
327  if (helper->IsPresent(t)) {
328  if (demand_is_fixed(t)) {
329  cut.AddConstant(min_overlap * demand_min(t));
330  } else {
331  DCHECK(!demands.empty());
332  cut.AddTerm(demands[t], min_overlap);
333  }
334  } else {
335  has_opt_cuts = true;
336  if (!cut.AddLiteralTerm(helper->PresenceLiteral(t),
337  min_overlap * demand_min(t))) {
338  cut_generated = false;
339  break;
340  }
341  }
342  }
343 
344  if (cut_generated) {
345  std::string full_name = cut_name;
346  if (has_opt_cuts) full_name.append("_opt");
347  if (has_quadratic_cuts) full_name.append("_quad");
348  if (lifted) full_name.append("_lifted");
349  if (use_energy) full_name.append("_energy");
350 
351  manager->AddCut(cut.Build(), full_name, lp_values);
352  }
353  }
354  return true;
355  };
356 }
357 
359  const std::vector<IntervalVariable>& intervals,
360  const IntegerVariable capacity, const std::vector<IntegerVariable>& demands,
361  const std::vector<LinearExpression>& energies, Model* model) {
362  CutGenerator result;
363 
365  new SchedulingConstraintHelper(intervals, model);
366  model->TakeOwnership(helper);
367 
368  result.vars = demands;
369  result.vars.push_back(capacity);
370  AddIntegerVariableFromIntervals(helper, model, &result.vars);
371  for (const LinearExpression& energy : energies) {
372  result.vars.insert(result.vars.end(), energy.vars.begin(),
373  energy.vars.end());
374  }
376 
377  // TODO(user): Do not create the cut generator if the capacity is fixed,
378  // all demands are fixed, and the intervals are always performed with a fixed
379  // size.
380  result.generate_cuts =
381  GenerateCumulativeEnergyCuts("CumulativeEnergy", helper, demands,
382  energies, AffineExpression(capacity), model);
383  return result;
384 }
385 
387  const std::vector<IntervalVariable>& intervals, Model* model) {
388  CutGenerator result;
389 
391  new SchedulingConstraintHelper(intervals, model);
392  model->TakeOwnership(helper);
393 
394  AddIntegerVariableFromIntervals(helper, model, &result.vars);
395 
396  // TODO(user): Do not create the cut generator if all intervals are
397  // performed with a fixed size as it will not propagate more than
398  // the overload checker.
400  "NoOverlapEnergy", helper,
401  /*demands=*/{}, /*energies=*/{},
402  /*capacity=*/AffineExpression(IntegerValue(1)), model);
403  return result;
404 }
405 
407  const std::vector<IntervalVariable>& intervals,
408  const IntegerVariable capacity, const std::vector<IntegerVariable>& demands,
409  Model* model) {
410  CutGenerator result;
411 
413  new SchedulingConstraintHelper(intervals, model);
414  model->TakeOwnership(helper);
415 
416  result.vars = demands;
417  result.vars.push_back(capacity);
418  AddIntegerVariableFromIntervals(helper, model, &result.vars);
419 
420  struct Event {
421  int interval_index;
422  IntegerValue time;
423  bool positive;
424  IntegerVariable demand;
425  };
426 
427  Trail* trail = model->GetOrCreate<Trail>();
428  IntegerTrail* integer_trail = model->GetOrCreate<IntegerTrail>();
429 
430  result.generate_cuts =
431  [helper, capacity, demands, trail, integer_trail, model](
433  LinearConstraintManager* manager) {
434  if (trail->CurrentDecisionLevel() > 0) return true;
435 
436  std::vector<Event> events;
437  // Iterate through the intervals. If start_max < end_min, the demand
438  // is mandatory.
439  for (int i = 0; i < helper->NumTasks(); ++i) {
440  if (helper->IsAbsent(i)) continue;
441 
442  const IntegerValue start_max = helper->StartMax(i);
443  const IntegerValue end_min = helper->EndMin(i);
444 
445  if (start_max >= end_min) continue;
446 
447  Event e1;
448  e1.interval_index = i;
449  e1.time = start_max;
450  e1.demand = demands[i];
451  e1.positive = true;
452 
453  Event e2 = e1;
454  e2.time = end_min;
455  e2.positive = false;
456  events.push_back(e1);
457  events.push_back(e2);
458  }
459 
460  // Sort events by time.
461  // It is also important that all positive event with the same time as
462  // negative events appear after for the correctness of the algo below.
463  std::sort(events.begin(), events.end(),
464  [](const Event i, const Event j) {
465  if (i.time == j.time) {
466  if (i.positive == j.positive) {
467  return i.interval_index < j.interval_index;
468  }
469  return !i.positive;
470  }
471  return i.time < j.time;
472  });
473 
474  std::vector<Event> cut_events;
475  bool added_positive_event = false;
476  for (const Event& e : events) {
477  if (e.positive) {
478  added_positive_event = true;
479  cut_events.push_back(e);
480  continue;
481  }
482  if (added_positive_event && cut_events.size() > 1) {
483  // Create cut.
484  bool cut_generated = true;
486  IntegerValue(0));
487  cut.AddTerm(capacity, IntegerValue(-1));
488  for (const Event& cut_event : cut_events) {
489  if (helper->IsPresent(cut_event.interval_index)) {
490  cut.AddTerm(cut_event.demand, IntegerValue(1));
491  } else {
492  cut_generated &= cut.AddLiteralTerm(
493  helper->PresenceLiteral(cut_event.interval_index),
494  integer_trail->LowerBound(cut_event.demand));
495  if (!cut_generated) break;
496  }
497  }
498  if (cut_generated) {
499  // Violation of the cut is checked by AddCut so we don't check
500  // it here.
501  manager->AddCut(cut.Build(), "CumulativeTimeTable", lp_values);
502  }
503  }
504  // Remove the event.
505  int new_size = 0;
506  for (int i = 0; i < cut_events.size(); ++i) {
507  if (cut_events[i].interval_index == e.interval_index) {
508  continue;
509  }
510  cut_events[new_size] = cut_events[i];
511  new_size++;
512  }
513  cut_events.resize(new_size);
514  added_positive_event = false;
515  }
516  return true;
517  };
518  return result;
519 }
520 
521 // Cached Information about one interval.
523  IntegerValue start_min;
524  IntegerValue start_max;
526  IntegerValue end_min;
527  IntegerValue end_max;
529  IntegerValue demand_min;
530 };
531 
533  const std::string& cut_name,
535  std::vector<PrecedenceEvent> events, IntegerValue capacity_max,
536  Model* model, LinearConstraintManager* manager) {
537  const int num_events = events.size();
538  if (num_events <= 1) return;
539 
540  std::sort(events.begin(), events.end(),
541  [](const PrecedenceEvent& e1, const PrecedenceEvent& e2) {
542  return e1.start_min < e2.start_min ||
543  (e1.start_min == e2.start_min && e1.end_max < e2.end_max);
544  });
545 
546  const double tolerance = 1e-4;
547 
548  for (int i = 0; i + 1 < num_events; ++i) {
549  const PrecedenceEvent& e1 = events[i];
550  for (int j = i + 1; j < num_events; ++j) {
551  const PrecedenceEvent& e2 = events[j];
552  if (e2.start_min >= e1.end_max) break; // Break out of the index2 loop.
553 
554  // Encode only the interesting pairs.
555  if (e1.demand_min + e2.demand_min <= capacity_max) continue;
556 
557  const bool interval_1_can_precede_2 = e1.end_min <= e2.start_max;
558  const bool interval_2_can_precede_1 = e2.end_min <= e1.start_max;
559 
560  if (interval_1_can_precede_2 && !interval_2_can_precede_1 &&
561  e1.end.LpValue(lp_values) >=
562  e2.start.LpValue(lp_values) + tolerance) {
563  // interval1.end <= interval2.start
564  LinearConstraintBuilder cut(model, kMinIntegerValue, IntegerValue(0));
565  cut.AddTerm(e1.end, IntegerValue(1));
566  cut.AddTerm(e2.start, IntegerValue(-1));
567  } else if (interval_2_can_precede_1 && !interval_1_can_precede_2 &&
568  e2.end.LpValue(lp_values) >=
569  e1.start.LpValue(lp_values) + tolerance) {
570  // interval2.end <= interval1.start
571  LinearConstraintBuilder cut(model, kMinIntegerValue, IntegerValue(0));
572  cut.AddTerm(e2.end, IntegerValue(1));
573  cut.AddTerm(e1.start, IntegerValue(-1));
574  manager->AddCut(cut.Build(), cut_name, lp_values);
575  }
576  }
577  }
578 }
579 
581  const std::vector<IntervalVariable>& intervals, IntegerVariable capacity,
582  const std::vector<IntegerVariable>& demands, Model* model) {
583  CutGenerator result;
584 
586  new SchedulingConstraintHelper(intervals, model);
587  model->TakeOwnership(helper);
588 
589  result.vars = demands;
590  result.vars.push_back(capacity);
591  AddIntegerVariableFromIntervals(helper, model, &result.vars);
592 
593  Trail* trail = model->GetOrCreate<Trail>();
594  IntegerTrail* integer_trail = model->GetOrCreate<IntegerTrail>();
595 
596  result.generate_cuts =
597  [trail, integer_trail, helper, demands, capacity, model](
599  LinearConstraintManager* manager) {
600  if (trail->CurrentDecisionLevel() > 0) return true;
601 
602  const IntegerValue capacity_max = integer_trail->UpperBound(capacity);
603  std::vector<PrecedenceEvent> events;
604  for (int t = 0; t < helper->NumTasks(); ++t) {
605  if (!helper->IsPresent(t)) continue;
606  PrecedenceEvent event;
607  event.start_min = helper->StartMin(t);
608  event.start_max = helper->StartMax(t);
609  event.start = helper->Starts()[t];
610  event.end_min = helper->EndMin(t);
611  event.end_max = helper->EndMax(t);
612  event.end = helper->Ends()[t];
613  event.demand_min = integer_trail->LowerBound(demands[t]);
614  events.push_back(event);
615  }
616  GeneratePrecedenceCuts("CumulativePrecedence", lp_values,
617  std::move(events), capacity_max, model, manager);
618  return true;
619  };
620  return result;
621 }
622 
624  const std::vector<IntervalVariable>& intervals, Model* model) {
625  CutGenerator result;
626 
628  new SchedulingConstraintHelper(intervals, model);
629  model->TakeOwnership(helper);
630 
631  AddIntegerVariableFromIntervals(helper, model, &result.vars);
632 
633  Trail* trail = model->GetOrCreate<Trail>();
634 
635  result.generate_cuts =
636  [trail, helper, model](
638  LinearConstraintManager* manager) {
639  if (trail->CurrentDecisionLevel() > 0) return true;
640 
641  std::vector<PrecedenceEvent> events;
642  for (int t = 0; t < helper->NumTasks(); ++t) {
643  if (!helper->IsPresent(t)) continue;
644  PrecedenceEvent event;
645  event.start_min = helper->StartMin(t);
646  event.start_max = helper->StartMax(t);
647  event.start = helper->Starts()[t];
648  event.end_min = helper->EndMin(t);
649  event.end_max = helper->EndMax(t);
650  event.end = helper->Ends()[t];
651  event.demand_min = IntegerValue(1);
652  events.push_back(event);
653  }
654  GeneratePrecedenceCuts("NoOverlapPrecedence", lp_values,
655  std::move(events), IntegerValue(1), model,
656  manager);
657  return true;
658  };
659 
660  return result;
661 }
662 
663 // Stores the event for a box along the two axis x and y.
664 // For a no_overlap constraint, y is always of size 1 between 0 and 1.
665 // For a cumulative constraint, y is the demand that must be between 0 and
666 // capacity_max.
667 // For a no_overlap_2d constraint, y the other dimension of the box.
668 struct CtEvent {
669  // The start min of the x interval.
670  IntegerValue x_start_min;
671 
672  // The size min of the x interval.
673  IntegerValue x_size_min;
674 
675  // The end of the x interval.
677 
678  // The lp value of the end of the x interval.
679  double x_lp_end;
680 
681  // The start min of the y interval.
682  IntegerValue y_start_min;
683 
684  // The end max of the y interval.
685  IntegerValue y_end_max;
686 
687  // The min energy of the task (this is always larger or equal to x_size_min *
688  // y_size_min).
689  IntegerValue energy_min;
690 
691  // Indicates if the events used the optional energy information from the
692  // model.
693  bool use_energy = false;
694 
695  // Indicates if the cut is lifted, that is if it includes tasks that are not
696  // strictly contained in the current time window.
697  bool lifted = false;
698 
699  std::string DebugString() const {
700  return absl::StrCat("CtEvent(x_end = ", x_end.DebugString(),
701  ", x_start_min = ", x_start_min.value(),
702  ", x_size_min = ", x_size_min.value(),
703  ", x_lp_end = ", x_lp_end,
704  ", y_start_min = ", y_start_min.value(),
705  ", y_end_max = ", y_end_max.value(),
706  ", energy_min = ", energy_min.value(),
707  ", use_energy = ", use_energy, ", lifted = ", lifted);
708  }
709 };
710 
711 // We generate the cut from the Smith's rule from:
712 // M. Queyranne, Structure of a simple scheduling polyhedron,
713 // Mathematical Programming 58 (1993), 263–285
714 //
715 // The original cut is:
716 // sum(end_min_i * duration_min_i) >=
717 // (sum(duration_min_i^2) + sum(duration_min_i)^2) / 2
718 // We strenghten this cuts by noticing that if all tasks starts after S,
719 // then replacing end_min_i by (end_min_i - S) is still valid.
720 //
721 // A second difference is that we look at a set of intervals starting
722 // after a given start_min, sorted by relative (end_lp - start_min).
724  const std::string& cut_name,
726  std::vector<CtEvent> events, bool use_lifting, Model* model,
727  LinearConstraintManager* manager) {
728  TopNCuts top_n_cuts(15);
729 
730  // Sort by start min to bucketize by start_min.
731  std::sort(events.begin(), events.end(),
732  [](const CtEvent& e1, const CtEvent& e2) {
733  return e1.x_start_min < e2.x_start_min;
734  });
735  for (int start = 0; start + 1 < events.size(); ++start) {
736  // Skip to the next start_min value.
737  if (start > 0 &&
738  events[start].x_start_min == events[start - 1].x_start_min) {
739  continue;
740  }
741 
742  const IntegerValue sequence_start_min = events[start].x_start_min;
743  std::vector<CtEvent> residual_tasks(events.begin() + start, events.end());
744 
745  // We look at event that start before sequence_start_min, but are forced
746  // to cross this time point. In that case, we replace this event by a
747  // truncated event starting at sequence_start_min. To do this, we reduce
748  // the size_min, align the start_min with the sequence_start_min, and
749  // scale the energy down accordingly.
750  if (use_lifting) {
751  for (int before = 0; before < start; ++before) {
752  if (events[before].x_start_min + events[before].x_size_min >
753  sequence_start_min) {
754  CtEvent event = events[before]; // Copy.
755  event.lifted = true;
756  const IntegerValue old_size_min = event.x_size_min;
757  event.x_size_min =
758  event.x_size_min + event.x_start_min - sequence_start_min;
759  event.x_start_min = sequence_start_min;
760  // We can rescale the energy min correctly.
761  //
762  // Let's take the example of a box of size 2 * 20 that overlaps
763  // sequence start min by 1, and that can rotate by 90 degrees.
764  // The energy min is 40, size min is 2, size_max is 20.
765  // If the box is horizontal, the lifted energy is (20 - 1) * 2 = 38.
766  // If the box is vertical, the lifted energy is (2 - 1) * 20 = 20.
767  // The min of the two is always reached when size = size_min.
768  event.energy_min = event.energy_min * event.x_size_min / old_size_min;
769  residual_tasks.push_back(event);
770  }
771  }
772  }
773 
774  std::sort(residual_tasks.begin(), residual_tasks.end(),
775  [](const CtEvent& e1, const CtEvent& e2) {
776  return e1.x_lp_end < e2.x_lp_end;
777  });
778 
779  int best_end = -1;
780  double best_efficacy = 0.01;
781  IntegerValue best_min_contrib(0);
782  IntegerValue sum_duration(0);
783  IntegerValue sum_square_duration(0);
784  IntegerValue best_size_divisor(0);
785  double unscaled_lp_contrib = 0;
786  IntegerValue current_start_min(kMaxIntegerValue);
787  IntegerValue y_start_min = kMaxIntegerValue;
788  IntegerValue y_end_max = kMinIntegerValue;
789 
790  for (int i = 0; i < residual_tasks.size(); ++i) {
791  const CtEvent& event = residual_tasks[i];
792  DCHECK_GE(event.x_start_min, sequence_start_min);
793  const IntegerValue energy = event.energy_min;
794  sum_duration += energy;
795  sum_square_duration += energy * energy;
796  unscaled_lp_contrib += event.x_lp_end * ToDouble(energy);
797  current_start_min = std::min(current_start_min, event.x_start_min);
798  y_start_min = std::min(y_start_min, event.y_start_min);
799  y_end_max = std::max(y_end_max, event.y_end_max);
800 
801  const IntegerValue size_divisor = y_end_max - y_start_min;
802 
803  // We compute the cuts with all the sizes actually equal to
804  // size_min * demand_min / size_divisor
805  // but to keep the computation in the integer domain, we multiply by
806  // size_divisor where needed instead.
807  const IntegerValue min_contrib =
808  (sum_duration * sum_duration + sum_square_duration) / 2 +
809  current_start_min * sum_duration * size_divisor;
810  const double efficacy = (ToDouble(min_contrib) -
811  unscaled_lp_contrib * ToDouble(size_divisor)) /
812  std::sqrt(ToDouble(sum_square_duration));
813  // TODO(user): Check overflow and ignore if too big.
814  if (efficacy > best_efficacy) {
815  best_efficacy = efficacy;
816  best_end = i;
817  best_min_contrib = min_contrib;
818  best_size_divisor = size_divisor;
819  }
820  }
821  if (best_end != -1) {
822  LinearConstraintBuilder cut(model, best_min_contrib, kMaxIntegerValue);
823  bool is_lifted = false;
824  bool use_energy = false;
825  for (int i = 0; i <= best_end; ++i) {
826  const CtEvent& event = residual_tasks[i];
827  is_lifted |= event.lifted;
828  use_energy |= event.use_energy;
829  cut.AddTerm(event.x_end, event.energy_min * best_size_divisor);
830  }
831  std::string full_name = cut_name;
832  if (is_lifted) full_name.append("_lifted");
833  if (use_energy) full_name.append("_energy");
834  top_n_cuts.AddCut(cut.Build(), full_name, lp_values);
835  }
836  }
837  top_n_cuts.TransferToManager(lp_values, manager);
838 }
839 
841  const std::vector<IntervalVariable>& intervals, Model* model) {
842  CutGenerator result;
843 
845  new SchedulingConstraintHelper(intervals, model);
846  model->TakeOwnership(helper);
847 
848  AddIntegerVariableFromIntervals(helper, model, &result.vars);
849 
850  Trail* trail = model->GetOrCreate<Trail>();
851 
852  result.generate_cuts =
853  [trail, helper, model](
855  LinearConstraintManager* manager) {
856  if (trail->CurrentDecisionLevel() > 0) return true;
857 
858  auto generate_cuts = [&lp_values, model, manager,
859  helper](const std::string& cut_name) {
860  std::vector<CtEvent> events;
861  for (int index = 0; index < helper->NumTasks(); ++index) {
862  if (!helper->IsPresent(index)) continue;
863  const IntegerValue size_min = helper->SizeMin(index);
864  if (size_min > 0) {
865  const AffineExpression end_expr = helper->Ends()[index];
866  CtEvent event;
867  event.x_start_min = helper->StartMin(index);
868  event.x_size_min = size_min;
869  event.x_end = end_expr;
870  event.x_lp_end = end_expr.LpValue(lp_values);
871  event.y_start_min = IntegerValue(0);
872  event.y_end_max = IntegerValue(1);
873  event.energy_min = size_min;
874  events.push_back(event);
875  }
876  }
877  GenerateCompletionTimeCuts(cut_name, lp_values, std::move(events),
878  /*use_lifting=*/false, model, manager);
879  };
880  if (!helper->SynchronizeAndSetTimeDirection(true)) return false;
881  generate_cuts("NoOverlapCompletionTime");
882  if (!helper->SynchronizeAndSetTimeDirection(false)) return false;
883  generate_cuts("NoOverlapCompletionTimeMirror");
884  return true;
885  };
886  return result;
887 }
888 
890  const std::vector<IntervalVariable>& intervals,
891  const IntegerVariable capacity, const std::vector<IntegerVariable>& demands,
892  const std::vector<LinearExpression>& energies, Model* model) {
893  CutGenerator result;
894 
896  new SchedulingConstraintHelper(intervals, model);
897  model->TakeOwnership(helper);
898 
899  result.vars = demands;
900  result.vars.push_back(capacity);
901  AddIntegerVariableFromIntervals(helper, model, &result.vars);
902 
903  Trail* trail = model->GetOrCreate<Trail>();
904  IntegerTrail* integer_trail = model->GetOrCreate<IntegerTrail>();
905 
906  result.generate_cuts =
907  [trail, integer_trail, helper, demands, energies, capacity, model](
909  LinearConstraintManager* manager) {
910  if (trail->CurrentDecisionLevel() > 0) return true;
911 
912  const IntegerValue capacity_max = integer_trail->UpperBound(capacity);
913  auto generate_cuts = [&lp_values, model, manager, helper, capacity_max,
914  integer_trail, &demands,
915  &energies](const std::string& cut_name) {
916  std::vector<CtEvent> events;
917  for (int index = 0; index < helper->NumTasks(); ++index) {
918  if (!helper->IsPresent(index)) continue;
919  if (helper->SizeMin(index) > 0 &&
920  integer_trail->LowerBound(demands[index]) > 0) {
921  const AffineExpression end_expr = helper->Ends()[index];
922  IntegerValue energy_min =
923  energies.empty()
924  ? IntegerValue(0)
925  : LinExprLowerBound(energies[index], *integer_trail);
926 
927  const IntegerValue size_min = helper->SizeMin(index);
928  const IntegerValue demand_min =
929  integer_trail->LowerBound(demands[index]);
930  CtEvent event;
931  event.x_start_min = helper->StartMin(index);
932  event.x_size_min = size_min;
933  event.x_end = end_expr;
934  event.x_lp_end = end_expr.LpValue(lp_values);
935  event.y_start_min = IntegerValue(0);
936  event.y_end_max = IntegerValue(capacity_max);
937  if (energy_min > size_min * demand_min) {
938  event.energy_min = energy_min;
939  event.use_energy = true;
940  } else {
941  event.energy_min = size_min * demand_min;
942  }
943  events.push_back(event);
944  }
945  }
946  GenerateCompletionTimeCuts(cut_name, lp_values, std::move(events),
947  /*use_lifting=*/true, model, manager);
948  };
949  if (!helper->SynchronizeAndSetTimeDirection(true)) return false;
950  generate_cuts("CumulativeCompletionTime");
951  if (!helper->SynchronizeAndSetTimeDirection(false)) return false;
952  generate_cuts("CumulativeCompletionTimeMirror");
953  return true;
954  };
955  return result;
956 }
957 
959  const std::vector<IntervalVariable>& x_intervals,
960  const std::vector<IntervalVariable>& y_intervals, Model* model) {
961  CutGenerator result;
962 
963  SchedulingConstraintHelper* x_helper =
964  new SchedulingConstraintHelper(x_intervals, model);
965  model->TakeOwnership(x_helper);
966 
967  SchedulingConstraintHelper* y_helper =
968  new SchedulingConstraintHelper(y_intervals, model);
969  model->TakeOwnership(y_helper);
970  AddIntegerVariableFromIntervals(x_helper, model, &result.vars);
971  AddIntegerVariableFromIntervals(y_helper, model, &result.vars);
972 
973  Trail* trail = model->GetOrCreate<Trail>();
974 
975  result.generate_cuts =
976  [trail, x_helper, y_helper, model](
978  LinearConstraintManager* manager) {
979  if (trail->CurrentDecisionLevel() > 0) return true;
980 
981  if (!x_helper->SynchronizeAndSetTimeDirection(true)) return false;
982  if (!y_helper->SynchronizeAndSetTimeDirection(true)) return false;
983 
984  const int num_boxes = x_helper->NumTasks();
985  std::vector<int> active_boxes;
986  std::vector<IntegerValue> cached_areas(num_boxes);
987  std::vector<Rectangle> cached_rectangles(num_boxes);
988  for (int box = 0; box < num_boxes; ++box) {
989  cached_areas[box] = x_helper->SizeMin(box) * y_helper->SizeMin(box);
990  if (cached_areas[box] == 0) continue;
991  if (!y_helper->IsPresent(box) || !y_helper->IsPresent(box)) continue;
992 
993  // TODO(user): It might be possible/better to use some shifted value
994  // here, but for now this code is not in the hot spot, so better be
995  // defensive and only do connected components on really disjoint
996  // boxes.
997  Rectangle& rectangle = cached_rectangles[box];
998  rectangle.x_min = x_helper->StartMin(box);
999  rectangle.x_max = x_helper->EndMax(box);
1000  rectangle.y_min = y_helper->StartMin(box);
1001  rectangle.y_max = y_helper->EndMax(box);
1002 
1003  active_boxes.push_back(box);
1004  }
1005 
1006  if (active_boxes.size() <= 1) return true;
1007 
1008  std::vector<absl::Span<int>> components =
1009  GetOverlappingRectangleComponents(cached_rectangles,
1010  absl::MakeSpan(active_boxes));
1011  for (absl::Span<int> boxes : components) {
1012  if (boxes.size() <= 1) continue;
1013 
1014  auto generate_cuts = [&lp_values, model, manager, &boxes,
1015  &cached_areas](
1016  const std::string& cut_name,
1017  SchedulingConstraintHelper* x_helper,
1018  SchedulingConstraintHelper* y_helper) {
1019  std::vector<CtEvent> events;
1020 
1021  for (const int box : boxes) {
1022  const AffineExpression x_end_expr = x_helper->Ends()[box];
1023  CtEvent event;
1024  event.x_start_min = x_helper->ShiftedStartMin(box);
1025  event.x_size_min = x_helper->SizeMin(box);
1026  event.x_end = x_end_expr;
1027  event.x_lp_end = x_end_expr.LpValue(lp_values);
1028  event.y_start_min = y_helper->ShiftedStartMin(box);
1029  event.y_end_max = y_helper->ShiftedEndMax(box);
1030  event.energy_min =
1031  x_helper->SizeMin(box) * y_helper->SizeMin(box);
1032  events.push_back(event);
1033  }
1034 
1035  GenerateCompletionTimeCuts(cut_name, lp_values, std::move(events),
1036  /*use_lifting=*/true, model, manager);
1037  };
1038 
1039  if (!x_helper->SynchronizeAndSetTimeDirection(true)) return false;
1040  if (!y_helper->SynchronizeAndSetTimeDirection(true)) return false;
1041  generate_cuts("NoOverlap2dXCompletionTime", x_helper, y_helper);
1042  generate_cuts("NoOverlap2dYCompletionTime", y_helper, x_helper);
1043  if (!x_helper->SynchronizeAndSetTimeDirection(false)) return false;
1044  if (!y_helper->SynchronizeAndSetTimeDirection(false)) return false;
1045  generate_cuts("NoOverlap2dXCompletionTimeMirror", x_helper, y_helper);
1046  generate_cuts("NoOverlap2dYCompletionTimeMirror", y_helper, x_helper);
1047  }
1048  return true;
1049  };
1050  return result;
1051 }
1052 
1053 } // namespace sat
1054 } // namespace operations_research
CutGenerator CreateNoOverlapEnergyCutGenerator(const std::vector< IntervalVariable > &intervals, Model *model)
int64_t min
Definition: alldiff_cst.cc:139
IntegerValue LinExprLowerBound(const LinearExpression &expr, const IntegerTrail &integer_trail)
CutGenerator CreateCumulativeTimeTableCutGenerator(const std::vector< IntervalVariable > &intervals, const IntegerVariable capacity, const std::vector< IntegerVariable > &demands, Model *model)
Class that owns everything related to a particular optimization model.
Definition: sat/model.h:38
constexpr IntegerValue kMinIntegerValue(-kMaxIntegerValue)
std::vector< IntegerVariable > vars
Definition: cuts.h:43
CutGenerator CreateCumulativePrecedenceCutGenerator(const std::vector< IntervalVariable > &intervals, IntegerVariable capacity, const std::vector< IntegerVariable > &demands, Model *model)
void AddLinearExpression(const LinearExpression &expr)
GRBmodel * model
void AddTerm(IntegerVariable var, IntegerValue coeff)
double LpValue(const absl::StrongVector< IntegerVariable, double > &lp_values) const
Definition: integer.h:253
std::function< IntegerVariable(Model *)> NewIntegerVariableFromLiteral(Literal lit)
Definition: integer.h:1501
ABSL_MUST_USE_RESULT bool AddLiteralTerm(Literal lit, IntegerValue coeff)
void STLSortAndRemoveDuplicates(T *v, const LessFunc &less_func)
Definition: stl_util.h:58
constexpr IntegerValue kMaxIntegerValue(std::numeric_limits< IntegerValue::ValueType >::max() - 1)
int64_t b
double ToDouble(IntegerValue value)
Definition: integer.h:70
int64_t max
Definition: alldiff_cst.cc:140
const std::string DebugString() const
Definition: integer.h:259
Rev< int64_t > start_max
#define DCHECK_NE(val1, val2)
Definition: base/logging.h:887
Rev< int64_t > end_min
int64_t demand
Definition: resource.cc:125
int64_t energy
Definition: resource.cc:354
std::function< bool(const absl::StrongVector< IntegerVariable, double > &, LinearConstraintManager *)> GenerateCumulativeEnergyCuts(const std::string &cut_name, SchedulingConstraintHelper *helper, const std::vector< IntegerVariable > &demands, const std::vector< LinearExpression > &energies, AffineExpression capacity, Model *model)
void TransferToManager(const absl::StrongVector< IntegerVariable, double > &lp_solution, LinearConstraintManager *manager)
int64_t capacity
int index
Definition: pack.cc:509
#define DCHECK_GE(val1, val2)
Definition: base/logging.h:890
CutGenerator CreateNoOverlapPrecedenceCutGenerator(const std::vector< IntervalVariable > &intervals, Model *model)
std::vector< absl::Span< int > > GetOverlappingRectangleComponents(const std::vector< Rectangle > &rectangles, absl::Span< int > active_rectangles)
Definition: diffn_util.cc:26
#define DCHECK(condition)
Definition: base/logging.h:885
void GeneratePrecedenceCuts(const std::string &cut_name, const absl::StrongVector< IntegerVariable, double > &lp_values, std::vector< PrecedenceEvent > events, IntegerValue capacity_max, Model *model, LinearConstraintManager *manager)
void AddCut(LinearConstraint ct, const std::string &name, const absl::StrongVector< IntegerVariable, double > &lp_solution)
void GenerateCompletionTimeCuts(const std::string &cut_name, const absl::StrongVector< IntegerVariable, double > &lp_values, std::vector< CtEvent > events, bool use_lifting, Model *model, LinearConstraintManager *manager)
Collection of objects used to extend the Constraint Solver library.
CutGenerator CreateNoOverlap2dCompletionTimeCutGenerator(const std::vector< IntervalVariable > &x_intervals, const std::vector< IntervalVariable > &y_intervals, Model *model)
const IntegerVariable kNoIntegerVariable(-1)
int64_t time
Definition: resource.cc:1691
CutGenerator CreateCumulativeCompletionTimeCutGenerator(const std::vector< IntervalVariable > &intervals, const IntegerVariable capacity, const std::vector< IntegerVariable > &demands, const std::vector< LinearExpression > &energies, Model *model)
bool AddCut(LinearConstraint ct, std::string type_name, const absl::StrongVector< IntegerVariable, double > &lp_solution, std::string extra_info="")
std::function< bool(const absl::StrongVector< IntegerVariable, double > &lp_values, LinearConstraintManager *manager)> generate_cuts
Definition: cuts.h:47
CutGenerator CreateNoOverlapCompletionTimeCutGenerator(const std::vector< IntervalVariable > &intervals, Model *model)
void AddQuadraticLowerBound(AffineExpression left, AffineExpression right, IntegerTrail *integer_trail)
CutGenerator CreateCumulativeEnergyCutGenerator(const std::vector< IntervalVariable > &intervals, const IntegerVariable capacity, const std::vector< IntegerVariable > &demands, const std::vector< LinearExpression > &energies, Model *model)
int64_t a