Shifts
A shift is a working window. Tasks must fit inside one shift (or, depending on policy, be split / disallowed from crossing shifts).
Synthetic shift breaks
Insert a tiny "fake break" interval at each shift boundary and forbid any
task from overlapping it with add_no_overlap. This prevents shift-crossing
without enumerating shift assignments.
for (s, e) in synthetic_shift_breaks:
br = model.new_fixed_size_interval_var(start=s, size=e - s, name="shift_edge")
for t in tasks:
model.add_no_overlap([task_interval[t], br])
Example: example_16_shift_crossing_fake_time_unit.py.
Explicit shift assignment
Alternatively, give every task a one-hot presence[shift, task] and enforce
the shift window when present:
for t in tasks:
model.add_exactly_one(presence[s, t] for s in shifts)
for s in shifts:
model.add(start[t] >= shift_start[s]).only_enforce_if(presence[s, t])
model.add(end[t] <= shift_end[s] ).only_enforce_if(presence[s, t])
More variables, but the assignment is explicit and easy to extend (e.g. to per-shift capacity).
Example: example_17_shift_crossing_mathieu.py.