Skip to content

Commit 9d490a4

Browse files
committed
improve sequence constraints sample
1 parent 1a79a66 commit 9d490a4

File tree

4 files changed

+157
-86
lines changed

4 files changed

+157
-86
lines changed

ortools/sat/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2934,6 +2934,7 @@ cc_library(
29342934
"//ortools/util:integer_pq",
29352935
"//ortools/util:strong_integers",
29362936
"@com_google_absl//absl/algorithm:container",
2937+
"@com_google_absl//absl/container:btree",
29372938
"@com_google_absl//absl/container:flat_hash_map",
29382939
"@com_google_absl//absl/container:flat_hash_set",
29392940
"@com_google_absl//absl/container:inlined_vector",

ortools/sat/diffn_util.cc

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
#include <numeric>
2525
#include <optional>
2626
#include <ostream>
27-
#include <set>
2827
#include <sstream>
2928
#include <string>
3029
#include <string_view>
@@ -33,6 +32,7 @@
3332
#include <vector>
3433

3534
#include "absl/algorithm/container.h"
35+
#include "absl/container/btree_set.h"
3636
#include "absl/container/flat_hash_map.h"
3737
#include "absl/container/flat_hash_set.h"
3838
#include "absl/container/inlined_vector.h"
@@ -2022,7 +2022,10 @@ absl::optional<std::pair<int, int>> FindOneIntersectionIfPresent(
20222022
IntegerValue y_min;
20232023
bool operator<(const Element& other) const { return y_min < other.y_min; }
20242024
};
2025-
std::set<Element> interval_set;
2025+
2026+
// Note: To use btree_set that has no iterator stability, we have to be
2027+
// a bit careful below.
2028+
absl::btree_set<Element> interval_set;
20262029

20272030
for (int i = 0; i < rectangles.size(); ++i) {
20282031
const IntegerValue x = rectangles[i].x_min;
@@ -2044,31 +2047,36 @@ absl::optional<std::pair<int, int>> FindOneIntersectionIfPresent(
20442047
// Intersection.
20452048
return {{it->index, i}};
20462049
}
2047-
}
2048-
2049-
// Note that the intersection is either before 'it', or just after it.
2050-
if (it != interval_set.begin()) {
2051-
auto it_before = it;
2052-
--it_before;
2053-
2054-
// Lazy erase stale entry.
2055-
if (rectangles[it_before->index].x_max <= x) {
2056-
interval_set.erase(it_before);
2057-
} else {
2058-
DCHECK_LE(it_before->y_min, y_min);
2059-
const IntegerValue y_max_before = rectangles[it_before->index].y_max;
2060-
if (y_max_before > y_min) {
2061-
// Intersection.
2062-
return {{it_before->index, i}};
2050+
} else {
2051+
// If there was no element at position y_min, we need to test if the
2052+
// interval before is stale or if it overlap with the new one.
2053+
if (it != interval_set.begin()) {
2054+
auto it_before = it;
2055+
--it_before;
2056+
2057+
// Lazy erase stale entry.
2058+
if (rectangles[it_before->index].x_max <= x) {
2059+
// For absl::btree_set we don't have iterator stability, so we do need
2060+
// to re-assign 'it' to the element just after the one we erased.
2061+
it = interval_set.erase(it_before);
2062+
} else {
2063+
DCHECK_LE(it_before->y_min, y_min);
2064+
const IntegerValue y_max_before = rectangles[it_before->index].y_max;
2065+
if (y_max_before > y_min) {
2066+
// Intersection.
2067+
return {{it_before->index, i}};
2068+
}
20632069
}
20642070
}
20652071
}
2072+
2073+
// We handled the part before, now we need to deal with the interval that
2074+
// starts after y_min.
20662075
++it;
20672076
while (it != interval_set.end()) {
20682077
// Lazy erase stale entry.
20692078
if (rectangles[it->index].x_max <= x) {
2070-
auto to_erase = it++;
2071-
interval_set.erase(to_erase);
2079+
it = interval_set.erase(it);
20722080
continue;
20732081
}
20742082

ortools/sat/docs/scheduling.md

Lines changed: 64 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2642,9 +2642,7 @@ def sequence_constraints_with_circuit(
26422642
) -> Sequence[Tuple[cp_model.IntVar, int]]:
26432643
"""This method enforces constraints on sequences of tasks of the same type.
26442644
2645-
This method assumes that all starts are disjoint, meaning that all tasks have
2646-
a strictly positive duration, and they appear in the same NoOverlap
2647-
constraint.
2645+
This method assumes that all durations are strictly positive.
26482646
26492647
The extra node (with id 0) will be used to decide which task is first with
26502648
its only outgoing arc, and which task is last with its only incoming arc.
@@ -2655,26 +2653,23 @@ def sequence_constraints_with_circuit(
26552653
length > 1. If no such path exists, then no tasks are active.
26562654
In this simplified model, all tasks must be performed.
26572655
2658-
Note that we do not enforce the minimum length constraint on the last sequence
2659-
of tasks of the same type.
2660-
26612656
Args:
26622657
model: The CpModel to add the constraints to.
26632658
starts: The array of starts variables of all tasks.
26642659
durations: the durations of all tasks.
26652660
task_types: The type of all tasks.
2666-
lengths: The computed length of the current sequence for each task.
2661+
lengths: the number of tasks of the same type in the current sequence.
26672662
cumuls: The computed cumul of the current sequence for each task.
26682663
sequence_length_constraints: the array of tuple (`task_type`, (`length_min`,
26692664
`length_max`)) that specifies the minimum and maximum length of the
26702665
sequence of tasks of type `task_type`.
26712666
sequence_cumul_constraints: the array of tuple (`task_type`, (`soft_max`,
26722667
`linear_penalty`, `hard_max`)) that specifies that if the cumul of the
26732668
sequence of tasks of type `task_type` is greater than `soft_max`, then
2674-
`linear_penalty` must be added to the cost
2669+
`linear_penalty * (cumul - soft_max)` is added to the cost
26752670
26762671
Returns:
2677-
The list of pairs (Boolean variables, penalty) to be added to the objective.
2672+
The list of pairs (integer variables, penalty) to be added to the objective.
26782673
"""
26792674

26802675
num_tasks = len(starts)
@@ -2697,6 +2692,10 @@ def sequence_constraints_with_circuit(
26972692
end_lit = model.new_bool_var(f"end_{i}")
26982693
arcs.append((i + 1, 0, end_lit))
26992694

2695+
# Make sure the previous length is within bounds.
2696+
type_length_min = sequence_length_constraints[task_types[i]][0]
2697+
model.add(lengths[i] >= type_length_min).only_enforce_if(end_lit)
2698+
27002699
# Penalize the cumul of the last task w.r.t. the soft max
27012700
soft_max, linear_penalty, hard_max = sequence_cumul_constraints[task_types[i]]
27022701
if soft_max < hard_max:
@@ -2714,9 +2713,14 @@ def sequence_constraints_with_circuit(
27142713
lit = model.new_bool_var(f"arc_{i}_to_{j}")
27152714
arcs.append((i + 1, j + 1, lit))
27162715

2717-
# To perform the transitive reduction from precedences to successors,
2718-
# we need to tie the starts of the tasks with 'literal'.
2719-
# In a non pure problem, the following equality must be an inequality.
2716+
# The circuit constraint is use to enforce the consistency between the
2717+
# precedences relations and the successor arcs. This is implemented by
2718+
# adding the constraint that force the implication task j is the next of
2719+
# task i implies that start(j) is greater or equal than the end(i).
2720+
#
2721+
# In the majority of problems, the following equality must be an
2722+
# inequality. In that particular case, as there are no extra constraints,
2723+
# we can keep the equality between start(j) and end(i).
27202724
model.add(starts[j] == starts[i] + durations[i]).only_enforce_if(lit)
27212725

27222726
# We add the constraints to incrementally maintain the length and the
@@ -2725,18 +2729,9 @@ def sequence_constraints_with_circuit(
27252729
# Increase the length of the sequence by 1.
27262730
model.add(lengths[j] == lengths[i] + 1).only_enforce_if(lit)
27272731

2728-
# Make sure the length of the sequence is within the bounds of the task
2729-
# type.
2730-
type_length_max = sequence_length_constraints[task_types[j]][1]
2731-
model.add(lengths[j] <= type_length_max).only_enforce_if(lit)
2732-
27332732
# Increase the cumul of the sequence by the duration of the task.
27342733
model.add(cumuls[j] == cumuls[i] + durations[j]).only_enforce_if(lit)
27352734

2736-
# Make sure the cumul of the sequence is within the bounds.
2737-
type_cumul_hard_max = sequence_cumul_constraints[task_types[j]][2]
2738-
model.add(cumuls[j] <= type_cumul_hard_max).only_enforce_if(lit)
2739-
27402735
else:
27412736
# Switching task type. task[i] is the last task of the previous
27422737
# sequence, task[j] is the first task of the new sequence.
@@ -2749,7 +2744,6 @@ def sequence_constraints_with_circuit(
27492744
model.add(lengths[i] >= type_length_min).only_enforce_if(lit)
27502745

27512746
# Reset the cumul to the duration of the task.
2752-
# Note we do not check that the duration of the task is within bounds.
27532747
model.add(cumuls[j] == durations[j]).only_enforce_if(lit)
27542748

27552749
# Penalize the cumul of the previous task w.r.t. the soft max
@@ -2789,6 +2783,9 @@ def sequences_in_no_overlap_sample_sat():
27892783
]
27902784

27912785
# Sequence length constraints per task_types: (hard_min, hard_max)
2786+
#
2787+
# Note that this constraint is very tight for task type B and will fail with
2788+
# an odd number of tasks of type B.
27922789
sequence_length_constraints = {
27932790
"A": (1, 3),
27942791
"B": (2, 2),
@@ -2827,15 +2824,15 @@ def sequences_in_no_overlap_sample_sat():
28272824

28282825
# Create length variables for each task.
28292826
lengths = []
2830-
max_length = max(c[1] for c in sequence_length_constraints.values())
28312827
for i in all_tasks:
2832-
lengths.append(model.new_int_var(0, max_length, f"length_{i}"))
2828+
max_hard_length = sequence_length_constraints[task_types[i]][1]
2829+
lengths.append(model.new_int_var(0, max_hard_length, f"length_{i}"))
28332830

28342831
# Create cumul variables for each task.
28352832
cumuls = []
2836-
max_cumul = max(c[2] for c in sequence_cumul_constraints.values())
28372833
for i in all_tasks:
2838-
cumuls.append(model.new_int_var(0, max_cumul, f"cumul_{i}"))
2834+
max_hard_cumul = sequence_cumul_constraints[task_types[i]][2]
2835+
cumuls.append(model.new_int_var(0, max_hard_cumul, f"cumul_{i}"))
28392836

28402837
# Adds NoOverlap constraint.
28412838
model.add_no_overlap(intervals)
@@ -2861,7 +2858,8 @@ def sequences_in_no_overlap_sample_sat():
28612858
status = solver.solve(model)
28622859

28632860
if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
2864-
# Prints out the makespan and the start times and ranks of all tasks.
2861+
# Prints out the makespan and the start times and lengths, cumuls at each
2862+
# step.
28652863
if status == cp_model.OPTIMAL:
28662864
print(f"Optimal cost: {solver.objective_value}")
28672865
else:
@@ -2871,12 +2869,45 @@ def sequences_in_no_overlap_sample_sat():
28712869
for t in all_tasks:
28722870
to_sort.append((solver.value(starts[t]), t))
28732871
to_sort.sort()
2874-
for start, t in to_sort:
2875-
print(
2876-
f"Task {t} of type {task_types[t]} with duration"
2877-
f" {durations[t]} starts at {start}, length ="
2878-
f" {solver.value(lengths[t])}, cumul = {solver.value(cumuls[t])} "
2879-
)
2872+
2873+
sum_of_penalties = 0
2874+
for i, (start, t) in enumerate(to_sort):
2875+
# Check length constraints.
2876+
length: int = solver.value(lengths[t])
2877+
hard_min_length, hard_max_length = sequence_length_constraints[
2878+
task_types[t]
2879+
]
2880+
assert length >= 0
2881+
assert length <= hard_max_length
2882+
if (
2883+
i + 1 == len(to_sort) or task_types[t] != task_types[to_sort[i + 1][1]]
2884+
): # End of sequence.
2885+
assert length >= hard_min_length
2886+
2887+
# Check cumul constraints.
2888+
cumul: int = solver.value(cumuls[t])
2889+
soft_max_cumul, penalty, hard_max_cumul = sequence_cumul_constraints[
2890+
task_types[t]
2891+
]
2892+
assert cumul >= 0
2893+
assert cumul <= hard_max_cumul
2894+
2895+
if cumul > soft_max_cumul:
2896+
penalty = penalty * (cumul - soft_max_cumul)
2897+
sum_of_penalties += penalty
2898+
print(
2899+
f"Task {t} of type {task_types[t]} with"
2900+
f" duration={durations[t]} starts at {start}, length={length},"
2901+
f" cumul={cumul} penalty={penalty}"
2902+
)
2903+
else:
2904+
print(
2905+
f"Task {t} of type {task_types[t]} with duration"
2906+
f" {durations[t]} starts at {start}, length ="
2907+
f" {length}, cumul = {cumul} "
2908+
)
2909+
2910+
assert int(solver.objective_value) == sum_of_penalties
28802911
else:
28812912
print(f"Solver exited with the following status: {status}")
28822913

0 commit comments

Comments
 (0)