Skip to content

Commit 7cc5a1b

Browse files
committed
[CP-SAT] improve no_overlap_2d propagation; improve linear propagator + precedences + disjunctive connection; change the way maximization is implemented in python
1 parent bb8f340 commit 7cc5a1b

13 files changed

+629
-321
lines changed

ortools/sat/2d_packing_brute_force.cc

+162-88
Large diffs are not rendered by default.

ortools/sat/BUILD.bazel

+4-2
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313

1414
# Home of CP/SAT solver (which includes SAT, max-SAT and PB problems).
1515

16-
load("@rules_proto//proto:defs.bzl", "proto_library")
17-
load("@rules_java//java:defs.bzl", "java_proto_library")
1816
load("@rules_cc//cc:defs.bzl", "cc_library", "cc_proto_library")
17+
load("@rules_java//java:defs.bzl", "java_proto_library")
18+
load("@rules_proto//proto:defs.bzl", "proto_library")
1919
load("@rules_python//python:proto.bzl", "py_proto_library")
2020

2121
package(default_visibility = ["//visibility:public"])
@@ -1139,6 +1139,7 @@ cc_library(
11391139
deps = [
11401140
":integer",
11411141
":model",
1142+
":precedences",
11421143
":sat_base",
11431144
":sat_solver",
11441145
":synchronization",
@@ -2267,6 +2268,7 @@ cc_library(
22672268
":integer",
22682269
":integer_search",
22692270
":model",
2271+
":restart",
22702272
":sat_base",
22712273
":sat_parameters_cc_proto",
22722274
":sat_solver",

ortools/sat/disjunctive.cc

+139-46
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
#include <algorithm>
1717
#include <cstdint>
18-
#include <functional>
1918
#include <string>
2019
#include <utility>
2120
#include <vector>
@@ -26,13 +25,11 @@
2625
#include "ortools/base/logging.h"
2726
#include "ortools/sat/all_different.h"
2827
#include "ortools/sat/integer.h"
29-
#include "ortools/sat/integer_expr.h"
3028
#include "ortools/sat/intervals.h"
3129
#include "ortools/sat/model.h"
3230
#include "ortools/sat/precedences.h"
3331
#include "ortools/sat/sat_base.h"
3432
#include "ortools/sat/sat_parameters.pb.h"
35-
#include "ortools/sat/sat_solver.h"
3633
#include "ortools/sat/theta_tree.h"
3734
#include "ortools/sat/timetable.h"
3835
#include "ortools/util/sort.h"
@@ -923,6 +920,9 @@ DisjunctivePrecedences::~DisjunctivePrecedences() {
923920
bool DisjunctivePrecedences::Propagate() {
924921
if (!helper_->SynchronizeAndSetTimeDirection(time_direction_)) return false;
925922
window_.clear();
923+
924+
// We only need to consider "critical" set of tasks given how we compute the
925+
// min-offset in PropagateSubwindow().
926926
IntegerValue window_end = kMinIntegerValue;
927927
for (const TaskTime task_time : helper_->TaskByIncreasingShiftedStartMin()) {
928928
const int task = task_time.task_index;
@@ -969,66 +969,151 @@ bool DisjunctivePrecedences::PropagateSubwindow() {
969969
index_to_end_vars_.push_back(end_exp.var);
970970
}
971971
window_.resize(new_size);
972-
precedences_->ComputePrecedences(index_to_end_vars_, &before_);
972+
973+
// Because we use the cached value in the window, we don't really care
974+
// on which order we process them.
975+
precedences_->ComputePrecedences(index_to_end_vars_, &before_,
976+
/*sort_by_var_lb=*/false);
973977

974978
const int size = before_.size();
975-
for (int i = 0; i < size;) {
976-
const IntegerVariable var = before_[i].var;
979+
for (int global_i = 0; global_i < size;) {
980+
const int global_start_i = global_i;
981+
const IntegerVariable var = before_[global_i].var;
977982
DCHECK_NE(var, kNoIntegerVariable);
978-
task_set_.Clear();
979983

980-
const int initial_i = i;
981-
IntegerValue min_offset = kMaxIntegerValue;
982-
for (; i < size && before_[i].var == var; ++i) {
983-
// Because we resized the window, the index is valid.
984-
const TaskTime task_time = window_[before_[i].index];
984+
// Decode the set of task before var.
985+
// Note that like in Propagate() we split this set of task into critical
986+
// subpart as there is no point considering them together.
987+
//
988+
// TODO(user): we should probably change the api to return a Span.
989+
//
990+
// TODO(user): If more than one set of task push the same variable, we
991+
// probabaly only want to keep the best push? Maybe we want to process them
992+
// in reverse order of what we do here?
993+
//
994+
// TODO(user): Currently we don't really use the inner_offsets_ except for
995+
// checking that our hash-maps are up to date. The idea is to get rid of
996+
// them for a faster and maybe more dynamic ComputePrecedences().
997+
indices_before_.clear();
998+
inner_offsets_.clear();
999+
IntegerValue local_start;
1000+
IntegerValue local_end;
1001+
for (; global_i < size; ++global_i) {
1002+
const PrecedencesPropagator::IntegerPrecedences& data = before_[global_i];
1003+
if (data.var != var) break;
1004+
const int index = data.index;
1005+
const auto [t, start_of_t] = window_[index];
1006+
if (global_i == global_start_i) {
1007+
local_start = start_of_t;
1008+
local_end = local_start + helper_->SizeMin(t);
1009+
} else {
1010+
if (start_of_t >= local_end) break;
1011+
local_end += helper_->SizeMin(t);
1012+
}
1013+
indices_before_.push_back(index);
1014+
inner_offsets_.push_back(data.offset);
1015+
}
9851016

986-
// We have var >= end_exp.var + offset, so
987-
// var >= (end_exp.var + end_exp.constant) + (offset - end_exp.constant)
988-
// var >= task end + new_offset.
1017+
// No need to consider if we don't have at least two tasks before var.
1018+
const int num_before = indices_before_.size();
1019+
if (num_before < 2) continue;
1020+
skip_.assign(num_before, false);
1021+
1022+
// Heuristic.
1023+
// We will use the current end-min of all the task in indices_before_
1024+
// to skip task with an offset not large enough.
1025+
const IntegerValue best_end_min = local_end;
1026+
1027+
// We will consider the end-min of all the subsets [i, num_items) to try to
1028+
// push var using the min-offset between var and items of such subset. This
1029+
// can be done in linear time by scanning from i = num_items - 1 to 0.
1030+
//
1031+
// Note that this needs the items in indices_before_ to be sorted by
1032+
// their shifted start min (it should be the case).
1033+
int best_index = -1;
1034+
IntegerValue best_new_lb = kMinIntegerValue;
1035+
IntegerValue min_offset = kMaxIntegerValue;
1036+
IntegerValue sum_of_duration = 0;
1037+
const IntegerValue current_var_lb = integer_trail_->LowerBound(var);
1038+
for (int i = num_before; --i >= 0;) {
1039+
const TaskTime task_time = window_[indices_before_[i]];
9891040
const AffineExpression& end_exp = helper_->Ends()[task_time.task_index];
990-
min_offset = std::min(min_offset, before_[i].offset - end_exp.constant);
9911041

992-
// The task are actually in sorted order, so we do not need to call
993-
// task_set_.Sort(). This property is DCHECKed.
994-
task_set_.AddUnsortedEntry({task_time.task_index, task_time.time,
995-
helper_->SizeMin(task_time.task_index)});
996-
}
997-
DCHECK_GE(task_set_.SortedTasks().size(), 2);
1042+
// Heuristic: do not consider this relations if its offset is clearly bad.
1043+
// If we want to get rid of inner_offsets_[], we will have to only do it
1044+
// below after the somewhat costly hash lookup to find the offset.
1045+
const IntegerValue known_offset = inner_offsets_[i] - end_exp.constant;
1046+
if (best_end_min + known_offset <= current_var_lb) {
1047+
skip_[i] = true;
1048+
continue;
1049+
}
9981050

999-
// TODO(user): Only use the min_offset of the critical task? Or maybe do a
1000-
// more general computation to find by how much we can push var?
1001-
const IntegerValue new_lb = task_set_.ComputeEndMin() + min_offset;
1002-
if (new_lb > integer_trail_->LowerBound(var)) {
1003-
const std::vector<TaskSet::Entry>& sorted_tasks = task_set_.SortedTasks();
1004-
helper_->ClearReason();
1051+
// TODO(user): The hash lookup here is a bit slow.
1052+
const IntegerValue inner_offset =
1053+
precedence_relations_->GetConditionalOffset(end_exp.var, var);
10051054

1006-
// Fill task_to_arc_index_ since we need it for the reason.
1007-
// Note that we do not care about the initial content of this vector.
1008-
for (int j = initial_i; j < i; ++j) {
1009-
const int task = window_[before_[j].index].task_index;
1010-
task_to_arc_index_[task] = before_[j].arc_index;
1055+
// TODO(user): This happens for relations true at level zero, maybe we
1056+
// should deal with them differently.
1057+
if (inner_offset == kMinIntegerValue) {
1058+
skip_[i] = true;
1059+
continue;
10111060
}
10121061

1013-
const int critical_index = task_set_.GetCriticalIndex();
1014-
const IntegerValue window_start = sorted_tasks[critical_index].start_min;
1015-
for (int i = critical_index; i < sorted_tasks.size(); ++i) {
1016-
const int ct = sorted_tasks[i].task;
1062+
// TODO(user): The code should work in all cases, but this DCHECK still
1063+
// fail rarely in multithread. I think this happens for linear of size 3
1064+
// with some fixed variable that get converted in the precedence
1065+
// propagator to size 2.
1066+
DCHECK_GE(inner_offset, inner_offsets_[i]);
1067+
1068+
// We have var >= end_exp.var + inner_offset, so
1069+
// var >= (end_exp.var + end_exp.constant)
1070+
// + (inner_offset - end_exp.constant)
1071+
// var >= task end + offset.
1072+
const IntegerValue offset = inner_offset - end_exp.constant;
1073+
1074+
// Heuristic: do not consider this relations if its offset is clearly bad.
1075+
// Same as what is done above with inner_offsets_[i].
1076+
if (best_end_min + offset <= current_var_lb) {
1077+
skip_[i] = true;
1078+
continue;
1079+
}
1080+
1081+
// Add this task to the current subset and compute the new bound.
1082+
min_offset = std::min(min_offset, offset);
1083+
sum_of_duration += helper_->SizeMin(task_time.task_index);
1084+
const IntegerValue start = task_time.time;
1085+
const IntegerValue new_lb = start + sum_of_duration + min_offset;
1086+
1087+
if (new_lb > best_new_lb) {
1088+
best_new_lb = new_lb;
1089+
best_index = i;
1090+
}
1091+
}
1092+
1093+
// Push?
1094+
if (best_new_lb > current_var_lb) {
1095+
DCHECK_NE(best_index, -1);
1096+
helper_->ClearReason();
1097+
const IntegerValue window_start =
1098+
window_[indices_before_[best_index]].time;
1099+
for (int i = best_index; i < num_before; ++i) {
1100+
if (skip_[i]) continue;
1101+
const int ct = window_[indices_before_[i]].task_index;
10171102
helper_->AddPresenceReason(ct);
1018-
helper_->AddEnergyAfterReason(ct, sorted_tasks[i].size_min,
1019-
window_start);
1103+
helper_->AddEnergyAfterReason(ct, helper_->SizeMin(ct), window_start);
10201104

1105+
// Fetch the explanation.
1106+
// This is okay if a bit slow since we only do that when we push.
10211107
const AffineExpression& end_exp = helper_->Ends()[ct];
1022-
precedences_->AddPrecedenceReason(
1023-
task_to_arc_index_[ct], min_offset + end_exp.constant,
1024-
helper_->MutableLiteralReason(), helper_->MutableIntegerReason());
1108+
for (const Literal l :
1109+
precedence_relations_->GetConditionalEnforcements(end_exp.var,
1110+
var)) {
1111+
helper_->MutableLiteralReason()->push_back(l.Negated());
1112+
}
10251113
}
1026-
1027-
// TODO(user): If var is actually a start-min of an interval, we
1028-
// could push the end-min and check the interval consistency right away.
10291114
++num_propagations_;
10301115
if (!helper_->PushIntegerLiteral(
1031-
IntegerLiteral::GreaterOrEqual(var, new_lb))) {
1116+
IntegerLiteral::GreaterOrEqual(var, best_new_lb))) {
10321117
return false;
10331118
}
10341119
}
@@ -1219,6 +1304,14 @@ bool DisjunctiveNotLast::PropagateSubwindow() {
12191304
// Add the reason for t, we only need the start-max.
12201305
helper_->AddStartMaxReason(t, end_min_of_critical_tasks - 1);
12211306

1307+
// If largest_ct_start_max == kMinIntegerValue, we have a conflict. To
1308+
// avoid integer overflow, we report it directly. This might happen
1309+
// because the task is known to be after all the other, and thus it cannot
1310+
// be "not last".
1311+
if (largest_ct_start_max == kMinIntegerValue) {
1312+
return helper_->ReportConflict();
1313+
}
1314+
12221315
// Enqueue the new end-max for t.
12231316
// Note that changing it will not influence the rest of the loop.
12241317
if (!helper_->DecreaseEndMax(t, largest_ct_start_max)) return false;

ortools/sat/disjunctive.h

+6-5
Original file line numberDiff line numberDiff line change
@@ -267,9 +267,8 @@ class DisjunctivePrecedences : public PropagatorInterface {
267267
helper_(helper),
268268
integer_trail_(model->GetOrCreate<IntegerTrail>()),
269269
precedences_(model->GetOrCreate<PrecedencesPropagator>()),
270-
shared_stats_(model->GetOrCreate<SharedStatistics>()),
271-
task_set_(helper->NumTasks()),
272-
task_to_arc_index_(helper->NumTasks()) {}
270+
precedence_relations_(model->GetOrCreate<PrecedenceRelations>()),
271+
shared_stats_(model->GetOrCreate<SharedStatistics>()) {}
273272
~DisjunctivePrecedences() override;
274273

275274
bool Propagate() final;
@@ -282,15 +281,17 @@ class DisjunctivePrecedences : public PropagatorInterface {
282281
SchedulingConstraintHelper* helper_;
283282
IntegerTrail* integer_trail_;
284283
PrecedencesPropagator* precedences_;
284+
PrecedenceRelations* precedence_relations_;
285285
SharedStatistics* shared_stats_;
286286

287287
int64_t num_propagations_ = 0;
288288

289289
std::vector<TaskTime> window_;
290290
std::vector<IntegerVariable> index_to_end_vars_;
291291

292-
TaskSet task_set_;
293-
std::vector<int> task_to_arc_index_;
292+
std::vector<int> indices_before_;
293+
std::vector<IntegerValue> inner_offsets_;
294+
std::vector<bool> skip_;
294295
std::vector<PrecedencesPropagator::IntegerPrecedences> before_;
295296
};
296297

ortools/sat/integer_expr.h

+4
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ class LinearConstraintPropagator : public PropagatorInterface {
155155
using IntegerSumLE = LinearConstraintPropagator<false>;
156156
using IntegerSumLE128 = LinearConstraintPropagator<true>;
157157

158+
// Explicit instantiations in integer_expr.cc.
159+
extern template class LinearConstraintPropagator<true>;
160+
extern template class LinearConstraintPropagator<false>;
161+
158162
// This assumes target = SUM_i coeffs[i] * vars[i], and detects that the target
159163
// must be of the form (a*X + b).
160164
//

ortools/sat/intervals.cc

+15-15
Original file line numberDiff line numberDiff line change
@@ -499,24 +499,24 @@ IntegerValue SchedulingConstraintHelper::GetCurrentMinDistanceBetweenTasks(
499499
int a, int b, bool add_reason_if_after) {
500500
const AffineExpression before = ends_[a];
501501
const AffineExpression after = starts_[b];
502-
if (before.var == kNoIntegerVariable) return kMinIntegerValue;
503-
if (after.var == kNoIntegerVariable) return kMinIntegerValue;
504-
505-
const IntegerValue needed = before.constant - after.constant;
506-
const IntegerValue static_known =
507-
precedence_relations_->GetOffset(before.var, after.var);
508-
509-
const std::pair<Literal, IntegerValue> dynamic_known =
510-
precedences_->GetConditionalOffset(before.var, after.var);
502+
if (before.var == kNoIntegerVariable || before.coeff != 1 ||
503+
after.var == kNoIntegerVariable || after.coeff != 1) {
504+
return kMinIntegerValue;
505+
}
511506

512-
const IntegerValue best = std::max(static_known, dynamic_known.second);
513-
if (best == kMinIntegerValue) return kMinIntegerValue;
507+
const IntegerValue offset =
508+
precedence_relations_->GetConditionalOffset(before.var, after.var);
509+
if (offset == kMinIntegerValue) return kMinIntegerValue;
514510

515-
if (add_reason_if_after && dynamic_known.second > static_known &&
516-
dynamic_known.second >= needed) {
517-
literal_reason_.push_back(dynamic_known.first.Negated());
511+
const IntegerValue needed_offset = before.constant - after.constant;
512+
const IntegerValue distance = offset - needed_offset;
513+
if (add_reason_if_after && distance >= 0) {
514+
for (const Literal l : precedence_relations_->GetConditionalEnforcements(
515+
before.var, after.var)) {
516+
literal_reason_.push_back(l.Negated());
517+
}
518518
}
519-
return best - needed;
519+
return distance;
520520
}
521521

522522
void SchedulingConstraintHelper::AddLevelZeroPrecedence(int a, int b) {

ortools/sat/java/BUILD.bazel

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313

1414
# Description: java wrapping of the code in ../
1515

16-
load("//bazel:swig_java.bzl", "ortools_java_wrap_cc")
17-
load("@rules_jvm_external//:defs.bzl", "artifact")
1816
load("@contrib_rules_jvm//java:defs.bzl", "java_junit5_test")
17+
load("@rules_jvm_external//:defs.bzl", "artifact")
18+
load("//bazel:swig_java.bzl", "ortools_java_wrap_cc")
1919

2020
ortools_java_wrap_cc(
2121
name = "sat",

0 commit comments

Comments
 (0)