@@ -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