|
15 | 15 |
|
16 | 16 | #include <algorithm>
|
17 | 17 | #include <cstdint>
|
18 |
| -#include <functional> |
19 | 18 | #include <string>
|
20 | 19 | #include <utility>
|
21 | 20 | #include <vector>
|
|
26 | 25 | #include "ortools/base/logging.h"
|
27 | 26 | #include "ortools/sat/all_different.h"
|
28 | 27 | #include "ortools/sat/integer.h"
|
29 |
| -#include "ortools/sat/integer_expr.h" |
30 | 28 | #include "ortools/sat/intervals.h"
|
31 | 29 | #include "ortools/sat/model.h"
|
32 | 30 | #include "ortools/sat/precedences.h"
|
33 | 31 | #include "ortools/sat/sat_base.h"
|
34 | 32 | #include "ortools/sat/sat_parameters.pb.h"
|
35 |
| -#include "ortools/sat/sat_solver.h" |
36 | 33 | #include "ortools/sat/theta_tree.h"
|
37 | 34 | #include "ortools/sat/timetable.h"
|
38 | 35 | #include "ortools/util/sort.h"
|
@@ -923,6 +920,9 @@ DisjunctivePrecedences::~DisjunctivePrecedences() {
|
923 | 920 | bool DisjunctivePrecedences::Propagate() {
|
924 | 921 | if (!helper_->SynchronizeAndSetTimeDirection(time_direction_)) return false;
|
925 | 922 | window_.clear();
|
| 923 | + |
| 924 | + // We only need to consider "critical" set of tasks given how we compute the |
| 925 | + // min-offset in PropagateSubwindow(). |
926 | 926 | IntegerValue window_end = kMinIntegerValue;
|
927 | 927 | for (const TaskTime task_time : helper_->TaskByIncreasingShiftedStartMin()) {
|
928 | 928 | const int task = task_time.task_index;
|
@@ -969,66 +969,151 @@ bool DisjunctivePrecedences::PropagateSubwindow() {
|
969 | 969 | index_to_end_vars_.push_back(end_exp.var);
|
970 | 970 | }
|
971 | 971 | 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); |
973 | 977 |
|
974 | 978 | 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; |
977 | 982 | DCHECK_NE(var, kNoIntegerVariable);
|
978 |
| - task_set_.Clear(); |
979 | 983 |
|
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 | + } |
985 | 1016 |
|
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]]; |
989 | 1040 | const AffineExpression& end_exp = helper_->Ends()[task_time.task_index];
|
990 |
| - min_offset = std::min(min_offset, before_[i].offset - end_exp.constant); |
991 | 1041 |
|
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 | + } |
998 | 1050 |
|
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); |
1005 | 1054 |
|
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; |
1011 | 1060 | }
|
1012 | 1061 |
|
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; |
1017 | 1102 | helper_->AddPresenceReason(ct);
|
1018 |
| - helper_->AddEnergyAfterReason(ct, sorted_tasks[i].size_min, |
1019 |
| - window_start); |
| 1103 | + helper_->AddEnergyAfterReason(ct, helper_->SizeMin(ct), window_start); |
1020 | 1104 |
|
| 1105 | + // Fetch the explanation. |
| 1106 | + // This is okay if a bit slow since we only do that when we push. |
1021 | 1107 | 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 | + } |
1025 | 1113 | }
|
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. |
1029 | 1114 | ++num_propagations_;
|
1030 | 1115 | if (!helper_->PushIntegerLiteral(
|
1031 |
| - IntegerLiteral::GreaterOrEqual(var, new_lb))) { |
| 1116 | + IntegerLiteral::GreaterOrEqual(var, best_new_lb))) { |
1032 | 1117 | return false;
|
1033 | 1118 | }
|
1034 | 1119 | }
|
@@ -1219,6 +1304,14 @@ bool DisjunctiveNotLast::PropagateSubwindow() {
|
1219 | 1304 | // Add the reason for t, we only need the start-max.
|
1220 | 1305 | helper_->AddStartMaxReason(t, end_min_of_critical_tasks - 1);
|
1221 | 1306 |
|
| 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 | + |
1222 | 1315 | // Enqueue the new end-max for t.
|
1223 | 1316 | // Note that changing it will not influence the rest of the loop.
|
1224 | 1317 | if (!helper_->DecreaseEndMax(t, largest_ct_start_max)) return false;
|
|
0 commit comments