Constraints¶
Scheduling constraints for interval and sequence variables.
Precedence Constraints¶
Before Constraints (Inequalities)¶
- pycsp3_scheduling.constraints.precedence.end_before_start(a, b, delay=0)[source]¶
Enforce that interval a ends before interval b starts.
This is the classic precedence constraint used in job-shop scheduling.
- Semantics (when both present):
start(b) >= end(a) + delay start(b) >= start(a) + length(a) + delay
- Parameters:
a (IntervalVar) – First interval variable (predecessor).
b (IntervalVar) – Second interval variable (successor).
delay (int) – Minimum time between end of a and start of b. Default 0.
- Returns:
A pycsp3 Node representing the constraint.
- Raises:
TypeError – If inputs are not IntervalVar or delay is not int.
Example
>>> task1 = IntervalVar(size=10, name="task1") >>> task2 = IntervalVar(size=5, name="task2") >>> satisfy(end_before_start(task1, task2)) # task2 starts after task1 ends
- pycsp3_scheduling.constraints.precedence.start_before_start(a, b, delay=0)[source]¶
Enforce that interval b cannot start before interval a starts (plus delay).
- Semantics (when both present):
start(b) >= start(a) + delay
- Parameters:
a (IntervalVar) – First interval variable.
b (IntervalVar) – Second interval variable.
delay (int) – Minimum time between start of a and start of b. Default 0.
- Returns:
A pycsp3 Node representing the constraint.
- Raises:
TypeError – If inputs are not IntervalVar or delay is not int.
Example
>>> task1 = IntervalVar(size=10, name="task1") >>> task2 = IntervalVar(size=5, name="task2") >>> satisfy(start_before_start(task1, task2)) # task2 starts after task1 starts
- pycsp3_scheduling.constraints.precedence.end_before_end(a, b, delay=0)[source]¶
Enforce that interval b cannot end before interval a ends (plus delay).
- Semantics (when both present):
end(b) >= end(a) + delay start(b) + length(b) >= start(a) + length(a) + delay
- Parameters:
a (IntervalVar) – First interval variable.
b (IntervalVar) – Second interval variable.
delay (int) – Minimum time between end of a and end of b. Default 0.
- Returns:
A pycsp3 Node representing the constraint.
- Raises:
TypeError – If inputs are not IntervalVar or delay is not int.
Example
>>> task1 = IntervalVar(size=10, name="task1") >>> task2 = IntervalVar(size=5, name="task2") >>> satisfy(end_before_end(task1, task2)) # task2 ends after task1 ends
- pycsp3_scheduling.constraints.precedence.start_before_end(a, b, delay=0)[source]¶
Enforce that interval b cannot end before interval a starts (plus delay).
- Semantics (when both present):
end(b) >= start(a) + delay
- Parameters:
a (IntervalVar) – First interval variable.
b (IntervalVar) – Second interval variable.
delay (int) – Minimum time between start of a and end of b. Default 0.
- Returns:
A pycsp3 Node representing the constraint.
- Raises:
TypeError – If inputs are not IntervalVar or delay is not int.
Example
>>> task1 = IntervalVar(size=10, name="task1") >>> task2 = IntervalVar(size=5, name="task2") >>> satisfy(start_before_end(task1, task2)) # task2 ends after task1 starts
At Constraints (Equalities)¶
- pycsp3_scheduling.constraints.precedence.start_at_start(a, b, delay=0)[source]¶
Enforce that interval b starts exactly when interval a starts (plus delay).
- Semantics (when both present):
start(b) == start(a) + delay
- Parameters:
a (IntervalVar) – First interval variable.
b (IntervalVar) – Second interval variable.
delay (int) – Time delay between start of a and start of b. Default 0.
- Returns:
A pycsp3 Node representing the constraint.
- Raises:
TypeError – If inputs are not IntervalVar or delay is not int.
Example
>>> task1 = IntervalVar(size=10, name="task1") >>> task2 = IntervalVar(size=5, name="task2") >>> satisfy(start_at_start(task1, task2)) # Start together >>> satisfy(start_at_start(task1, task2, delay=5)) # task2 starts 5 after task1
- pycsp3_scheduling.constraints.precedence.start_at_end(a, b, delay=0)[source]¶
Enforce that interval b starts exactly when interval a ends (plus delay).
- Semantics (when both present):
start(b) == end(a) + delay start(b) == start(a) + length(a) + delay
- Parameters:
a (IntervalVar) – First interval variable.
b (IntervalVar) – Second interval variable.
delay (int) – Time delay between end of a and start of b. Default 0.
- Returns:
A pycsp3 Node representing the constraint.
- Raises:
TypeError – If inputs are not IntervalVar or delay is not int.
Example
>>> task1 = IntervalVar(size=10, name="task1") >>> task2 = IntervalVar(size=5, name="task2") >>> satisfy(start_at_end(task1, task2)) # task2 starts exactly when task1 ends
- pycsp3_scheduling.constraints.precedence.end_at_start(a, b, delay=0)[source]¶
Enforce that interval a ends exactly when interval b starts (plus delay).
- Semantics (when both present):
end(a) == start(b) + delay start(a) + length(a) == start(b) + delay
Note: This is equivalent to start_at_end(b, a, -delay) but expressed from a’s perspective.
- Parameters:
a (IntervalVar) – First interval variable.
b (IntervalVar) – Second interval variable.
delay (int) – Time delay between end of a and start of b. Default 0.
- Returns:
A pycsp3 Node representing the constraint.
- Raises:
TypeError – If inputs are not IntervalVar or delay is not int.
Example
>>> task1 = IntervalVar(size=10, name="task1") >>> task2 = IntervalVar(size=5, name="task2") >>> satisfy(end_at_start(task1, task2)) # task1 ends exactly when task2 starts
- pycsp3_scheduling.constraints.precedence.end_at_end(a, b, delay=0)[source]¶
Enforce that interval b ends exactly when interval a ends (plus delay).
- Semantics (when both present):
end(b) == end(a) + delay start(b) + length(b) == start(a) + length(a) + delay
- Parameters:
a (IntervalVar) – First interval variable.
b (IntervalVar) – Second interval variable.
delay (int) – Time delay between end of a and end of b. Default 0.
- Returns:
A pycsp3 Node representing the constraint.
- Raises:
TypeError – If inputs are not IntervalVar or delay is not int.
Example
>>> task1 = IntervalVar(size=10, name="task1") >>> task2 = IntervalVar(size=5, name="task2") >>> satisfy(end_at_end(task1, task2)) # Both end at the same time
Grouping Constraints¶
- pycsp3_scheduling.constraints.grouping.span(main, subtasks)[source]¶
Constrain main interval to span all present subtasks.
The main interval covers exactly the time range occupied by its subtasks: - start(main) = min{start(i) : i present in subtasks} - end(main) = max{end(i) : i present in subtasks}
Semantics for optional intervals: - If main is present, at least one subtask must be present - If main is absent, all subtasks must be absent - If all subtasks are absent, main must be absent
- Parameters:
main (IntervalVar) – The spanning interval variable.
subtasks (Sequence[IntervalVar]) – List of interval variables to be spanned.
- Returns:
List of pycsp3 constraint nodes.
- Raises:
TypeError – If main is not IntervalVar or subtasks contains non-IntervalVar.
ValueError – If subtasks is empty.
- Return type:
Example
>>> main_task = IntervalVar(name="project") >>> phases = [IntervalVar(size=10, name=f"phase_{i}") for i in range(3)] >>> satisfy(span(main_task, phases)) # project spans all phases
- pycsp3_scheduling.constraints.grouping.alternative(main, alternatives, cardinality=1)[source]¶
Constrain exactly k alternatives to be selected and match the main interval.
When main is present, exactly cardinality intervals from alternatives are present, and they have the same start and end times as main.
Semantics: - If main is present: exactly cardinality alternatives are present - Selected alternatives have start(alt) == start(main) and end(alt) == end(main) - If main is absent: all alternatives are absent
- Parameters:
main (IntervalVar) – The main interval variable.
alternatives (Sequence[IntervalVar]) – List of alternative interval variables.
cardinality (int) – Number of alternatives to select (default 1).
- Returns:
List of pycsp3 constraint nodes.
- Raises:
TypeError – If main is not IntervalVar or alternatives contains non-IntervalVar.
ValueError – If alternatives is empty or cardinality is invalid.
- Return type:
Example
>>> main_op = IntervalVar(size=10, name="operation") >>> machines = [IntervalVar(size=10, optional=True, name=f"m{i}") for i in range(3)] >>> satisfy(alternative(main_op, machines)) # Assign to exactly one machine
- pycsp3_scheduling.constraints.grouping.synchronize(main, intervals)[source]¶
Constrain all present intervals to synchronize with the main interval.
All present intervals in the array have the same start and end times as main.
Semantics: - If main is present: present intervals have same start/end as main - If main is absent: all intervals in array are absent - Intervals in array can be independently present or absent
- Parameters:
main (IntervalVar) – The main interval variable.
intervals (Sequence[IntervalVar]) – List of interval variables to synchronize.
- Returns:
List of pycsp3 constraint nodes.
- Raises:
TypeError – If main is not IntervalVar or intervals contains non-IntervalVar.
ValueError – If intervals is empty.
- Return type:
Example
>>> main = IntervalVar(size=10, name="meeting") >>> attendees = [IntervalVar(size=10, optional=True, name=f"person_{i}") for i in range(5)] >>> satisfy(synchronize(main, attendees)) # Present attendees sync with meeting
Sequence Constraints¶
No-Overlap¶
- pycsp3_scheduling.constraints.sequence.SeqNoOverlap(sequence, transition_matrix=None, is_direct=False, zero_ignored=True)[source]¶
Enforce non-overlap on a sequence of intervals with optional transition times.
When intervals in the sequence are assigned to the same resource, they cannot overlap in time. If a transition matrix is provided, setup times between consecutive intervals are enforced based on their types.
- Parameters:
- is_direct: If True, transition times apply only between immediately
consecutive intervals in schedule order (IBM “Next” semantics). If False, transition times apply between any pair where one precedes the other (IBM “After” semantics).
- zero_ignored: If True, intervals with zero length are ignored
in the non-overlap constraint.
- Returns:
A pycsp3 constraint (ECtr) or list of constraints.
- Raises:
TypeError – If sequence is not a SequenceVar or iterable of IntervalVar.
ValueError – If transition_matrix dimensions don’t match types.
Example
>>> # Simple no-overlap >>> tasks = [IntervalVar(size=10, name=f"t{i}") for i in range(3)] >>> satisfy(SeqNoOverlap(tasks))
>>> # With transition times by job type >>> seq = SequenceVar(intervals=tasks, types=[0, 1, 0], name="machine") >>> matrix = [[0, 5], [3, 0]] # Setup time from type i to type j >>> satisfy(SeqNoOverlap(seq, transition_matrix=matrix))
Ordering Constraints¶
- pycsp3_scheduling.constraints.sequence.first(sequence, interval)[source]¶
Constrain an interval to be the first in a sequence.
The specified interval must start before all other present intervals in the sequence.
- Parameters:
sequence – SequenceVar or iterable of IntervalVar.
interval (IntervalVar) – The interval that must be first.
- Returns:
List of pycsp3 constraint nodes.
- Raises:
TypeError – If interval is not an IntervalVar.
ValueError – If interval is not in the sequence.
- Return type:
Example
>>> tasks = [IntervalVar(size=5, name=f"t{i}") for i in range(3)] >>> seq = SequenceVar(intervals=tasks, name="machine") >>> satisfy(first(seq, tasks[0])) # tasks[0] must be first
- pycsp3_scheduling.constraints.sequence.last(sequence, interval)[source]¶
Constrain an interval to be the last in a sequence.
The specified interval must end after all other present intervals in the sequence.
- Parameters:
sequence – SequenceVar or iterable of IntervalVar.
interval (IntervalVar) – The interval that must be last.
- Returns:
List of pycsp3 constraint nodes.
- Raises:
TypeError – If interval is not an IntervalVar.
ValueError – If interval is not in the sequence.
- Return type:
Example
>>> tasks = [IntervalVar(size=5, name=f"t{i}") for i in range(3)] >>> seq = SequenceVar(intervals=tasks, name="machine") >>> satisfy(last(seq, tasks[2])) # tasks[2] must be last
- pycsp3_scheduling.constraints.sequence.before(sequence, interval1, interval2)[source]¶
Constrain interval1 to come before interval2 in a sequence.
If both intervals are present, interval1 must end before interval2 starts.
- Parameters:
sequence – SequenceVar or iterable of IntervalVar.
interval1 (IntervalVar) – The interval that must come first.
interval2 (IntervalVar) – The interval that must come second.
- Returns:
List of pycsp3 constraint nodes.
- Raises:
TypeError – If either interval is not an IntervalVar.
ValueError – If either interval is not in the sequence.
- Return type:
Example
>>> tasks = [IntervalVar(size=5, name=f"t{i}") for i in range(3)] >>> seq = SequenceVar(intervals=tasks, name="machine") >>> satisfy(before(seq, tasks[0], tasks[2])) # t0 before t2
- pycsp3_scheduling.constraints.sequence.previous(sequence, interval1, interval2)[source]¶
Constrain interval1 to immediately precede interval2 in a sequence.
If both intervals are present, interval1 must come directly before interval2 with no other present intervals between them.
Note: This is a complex constraint that requires tracking sequence ordering. The current implementation enforces that interval1 ends before interval2 starts, and that no other interval can fit between them.
- Parameters:
sequence – SequenceVar or iterable of IntervalVar.
interval1 (IntervalVar) – The interval that must immediately precede.
interval2 (IntervalVar) – The interval that must immediately follow.
- Returns:
List of pycsp3 constraint nodes.
- Raises:
TypeError – If either interval is not an IntervalVar.
ValueError – If either interval is not in the sequence.
- Return type:
Example
>>> tasks = [IntervalVar(size=5, name=f"t{i}") for i in range(3)] >>> seq = SequenceVar(intervals=tasks, name="machine") >>> satisfy(previous(seq, tasks[0], tasks[1])) # t0 directly before t1
Consistency Constraints¶
- pycsp3_scheduling.constraints.sequence.same_sequence(sequence1, sequence2)[source]¶
Constrain common intervals to have the same position in both sequences.
For any interval that appears in both sequences, if it is present, it must occupy the same position (index) in both sequences.
This constraint is useful when the same operations need to maintain consistent ordering across different resources.
- Parameters:
sequence1 – First SequenceVar or iterable of IntervalVar.
sequence2 – Second SequenceVar or iterable of IntervalVar.
- Returns:
List of pycsp3 constraint nodes.
- Return type:
Example
>>> # Operations processed on two parallel machines in same order >>> ops = [IntervalVar(size=5, name=f"op{i}") for i in range(3)] >>> seq1 = SequenceVar(intervals=ops, name="machine1") >>> seq2 = SequenceVar(intervals=ops, name="machine2") >>> satisfy(same_sequence(seq1, seq2))
- pycsp3_scheduling.constraints.sequence.same_common_subsequence(sequence1, sequence2)[source]¶
Constrain common intervals to have the same relative ordering in both sequences.
For any pair of intervals that appear in both sequences, if both are present, their relative order (which one comes first) must be the same in both sequences.
This is weaker than same_sequence - it only requires the same relative order, not the same absolute positions.
- Parameters:
sequence1 – First SequenceVar or iterable of IntervalVar.
sequence2 – Second SequenceVar or iterable of IntervalVar.
- Returns:
List of pycsp3 constraint nodes.
- Return type:
Example
>>> # Same jobs on different machines maintain relative order >>> jobs = [IntervalVar(size=5, optional=True, name=f"job{i}") for i in range(4)] >>> seq1 = SequenceVar(intervals=jobs[:3], name="m1") # jobs 0,1,2 >>> seq2 = SequenceVar(intervals=jobs[1:], name="m2") # jobs 1,2,3 >>> # Common jobs (1,2) must have same relative order >>> satisfy(same_common_subsequence(seq1, seq2))
Forbidden Time Constraints¶
- pycsp3_scheduling.constraints.forbidden.forbid_start(interval, forbidden_periods)[source]¶
Constrain the interval to not start during any of the forbidden time periods.
For each forbidden period (s, e), the interval’s start time must not be in the range [s, e). That is: NOT (s <= start < e).
- Parameters:
interval (IntervalVar) – The interval variable to constrain.
forbidden_periods (Sequence[tuple[int, int]]) – List of (start, end) tuples defining forbidden periods. Each period is a half-open interval [start, end).
- Returns:
List of pycsp3 constraint nodes.
- Raises:
TypeError – If interval is not an IntervalVar or periods are malformed.
ValueError – If any period has start >= end.
- Return type:
Example
>>> task = IntervalVar(size=10, name="task") >>> # Cannot start during lunch break (12-13) or after hours (17-24) >>> satisfy(forbid_start(task, [(12, 13), (17, 24)]))
- pycsp3_scheduling.constraints.forbidden.forbid_end(interval, forbidden_periods)[source]¶
Constrain the interval to not end during any of the forbidden time periods.
For each forbidden period (s, e), the interval’s end time must not be in the range (s, e]. That is: NOT (s < end <= e).
- Parameters:
interval (IntervalVar) – The interval variable to constrain.
forbidden_periods (Sequence[tuple[int, int]]) – List of (start, end) tuples defining forbidden periods. Each period is a half-open interval (start, end].
- Returns:
List of pycsp3 constraint nodes.
- Raises:
TypeError – If interval is not an IntervalVar or periods are malformed.
ValueError – If any period has start >= end.
- Return type:
Example
>>> task = IntervalVar(size=10, name="task") >>> # Cannot end during maintenance window >>> satisfy(forbid_end(task, [(6, 8)]))
- pycsp3_scheduling.constraints.forbidden.forbid_extent(interval, forbidden_periods)[source]¶
Constrain the interval to not overlap any of the forbidden time periods.
For each forbidden period (s, e), the interval must be completely before or completely after. That is: (end <= s) OR (start >= e).
- Parameters:
interval (IntervalVar) – The interval variable to constrain.
forbidden_periods (Sequence[tuple[int, int]]) – List of (start, end) tuples defining forbidden periods. The interval cannot span across any of these periods.
- Returns:
List of pycsp3 constraint nodes.
- Raises:
TypeError – If interval is not an IntervalVar or periods are malformed.
ValueError – If any period has start >= end.
- Return type:
Example
>>> task = IntervalVar(size=10, name="task") >>> # Task cannot span across lunch break - must be entirely before or after >>> satisfy(forbid_extent(task, [(12, 13)]))
Presence Constraints¶
Binary Presence Constraints¶
- pycsp3_scheduling.constraints.presence.presence_implies(a, b)[source]¶
Constrain that if interval a is present, then interval b must also be present.
Implements the logical implication: presence(a) => presence(b) Equivalent to: NOT presence(a) OR presence(b)
- Parameters:
a (IntervalVar) – The antecedent interval (if this is present…).
b (IntervalVar) – The consequent interval (…then this must be present).
- Returns:
List of pycsp3 constraint nodes.
- Raises:
TypeError – If either argument is not an IntervalVar.
- Return type:
Notes
If a is mandatory (not optional), b must be present (mandatory behavior).
If b is mandatory (not optional), constraint is always satisfied.
Example
>>> setup = IntervalVar(size=5, optional=True, name="setup") >>> main_task = IntervalVar(size=20, optional=True, name="main") >>> # If we do the main task, we must do setup >>> satisfy(presence_implies(main_task, setup))
- pycsp3_scheduling.constraints.presence.presence_or(a, b)[source]¶
Constrain that at least one of the intervals must be present.
Implements: presence(a) OR presence(b)
- Parameters:
a (IntervalVar) – First interval.
b (IntervalVar) – Second interval.
- Returns:
List of pycsp3 constraint nodes.
- Raises:
TypeError – If either argument is not an IntervalVar.
- Return type:
Notes
If either interval is mandatory, constraint is always satisfied.
Example
>>> option_a = IntervalVar(size=10, optional=True, name="option_a") >>> option_b = IntervalVar(size=15, optional=True, name="option_b") >>> # Must choose at least one option >>> satisfy(presence_or(option_a, option_b))
- pycsp3_scheduling.constraints.presence.presence_xor(a, b)[source]¶
Constrain that exactly one of the intervals must be present (exclusive or).
Implements: presence(a) XOR presence(b) Equivalent to: (presence(a) OR presence(b)) AND NOT (presence(a) AND presence(b))
- Parameters:
a (IntervalVar) – First interval.
b (IntervalVar) – Second interval.
- Returns:
List of pycsp3 constraint nodes.
- Raises:
TypeError – If either argument is not an IntervalVar.
- Return type:
Notes
Both intervals should be optional for meaningful behavior.
If either is mandatory, this may result in infeasibility or forced absence.
Example
>>> route_a = IntervalVar(size=30, optional=True, name="route_a") >>> route_b = IntervalVar(size=45, optional=True, name="route_b") >>> # Must take exactly one route >>> satisfy(presence_xor(route_a, route_b))
Group Presence Constraints¶
- pycsp3_scheduling.constraints.presence.all_present_or_all_absent(intervals)[source]¶
Constrain that either all intervals are present, or all are absent.
All presence values must be equal: either all 0 or all 1.
- Parameters:
intervals (Sequence[IntervalVar]) – List of optional interval variables.
- Returns:
List of pycsp3 constraint nodes.
- Raises:
TypeError – If any element is not an IntervalVar.
- Return type:
Notes
All intervals should be optional for meaningful behavior.
If any interval is mandatory, all must be present.
Example
>>> subtasks = [IntervalVar(size=5, optional=True, name=f"sub_{i}") for i in range(3)] >>> # Either do all subtasks or none >>> satisfy(all_present_or_all_absent(subtasks))
- pycsp3_scheduling.constraints.presence.presence_or_all(*intervals)[source]¶
Constrain that at least one of the intervals must be present.
Variadic version of presence_or for multiple intervals.
- Parameters:
*intervals (IntervalVar) – Variable number of interval variables.
- Returns:
List of pycsp3 constraint nodes.
- Raises:
TypeError – If any argument is not an IntervalVar.
- Return type:
Example
>>> options = [IntervalVar(size=10, optional=True, name=f"opt_{i}") for i in range(5)] >>> # At least one option must be selected >>> satisfy(presence_or_all(*options))
- pycsp3_scheduling.constraints.presence.if_present_then(interval, constraint)[source]¶
Apply a constraint only when the interval is present.
Implements: presence(interval) => constraint The constraint is only enforced if the interval is present.
- Parameters:
interval (IntervalVar) – The interval whose presence gates the constraint.
constraint – A pycsp3 Node constraint to apply when present.
- Returns:
List of pycsp3 constraint nodes.
- Raises:
TypeError – If interval is not an IntervalVar.
- Return type:
Notes
If interval is mandatory, constraint is always enforced.
The constraint argument should be a pycsp3 Node object.
Example
>>> from pycsp3.classes.nodes import Node, TypeNode >>> task = IntervalVar(size=10, optional=True, name="task") >>> start = start_var(task) >>> # If task is present, it must start after time 5 >>> min_start = Node.build(TypeNode.GE, start, 5) >>> satisfy(if_present_then(task, min_start))
Cardinality Constraints¶
- pycsp3_scheduling.constraints.presence.at_least_k_present(intervals, k)[source]¶
Constrain that at least k intervals must be present.
- Parameters:
intervals (Sequence[IntervalVar]) – List of interval variables.
k (int) – Minimum number of intervals that must be present.
- Returns:
List of pycsp3 constraint nodes.
- Raises:
TypeError – If any element is not an IntervalVar or k is not int.
ValueError – If k is negative.
- Return type:
Example
>>> tasks = [IntervalVar(size=10, optional=True, name=f"t_{i}") for i in range(10)] >>> # Must complete at least 5 tasks >>> satisfy(at_least_k_present(tasks, 5))
- pycsp3_scheduling.constraints.presence.at_most_k_present(intervals, k)[source]¶
Constrain that at most k intervals can be present.
- Parameters:
intervals (Sequence[IntervalVar]) – List of interval variables.
k (int) – Maximum number of intervals that can be present.
- Returns:
List of pycsp3 constraint nodes.
- Raises:
TypeError – If any element is not an IntervalVar or k is not int.
ValueError – If k is negative.
- Return type:
Example
>>> features = [IntervalVar(size=10, optional=True, name=f"f_{i}") for i in range(10)] >>> # Can implement at most 3 features >>> satisfy(at_most_k_present(features, 3))
- pycsp3_scheduling.constraints.presence.exactly_k_present(intervals, k)[source]¶
Constrain that exactly k intervals must be present.
- Parameters:
intervals (Sequence[IntervalVar]) – List of interval variables.
k (int) – Exact number of intervals that must be present.
- Returns:
List of pycsp3 constraint nodes.
- Raises:
TypeError – If any element is not an IntervalVar or k is not int.
ValueError – If k is negative.
- Return type:
Example
>>> shifts = [IntervalVar(size=480, optional=True, name=f"shift_{i}") for i in range(7)] >>> # Exactly 5 shifts must be worked >>> satisfy(exactly_k_present(shifts, 5))
Chain Constraints¶
- pycsp3_scheduling.constraints.chain.chain(intervals, delays=None)[source]¶
Enforce that intervals execute in sequence order with optional minimum delays.
- For each consecutive pair (i, i+1):
end(intervals[i]) + delays[i] <= start(intervals[i+1])
This is equivalent to multiple end_before_start constraints but provides a cleaner, more semantic API for sequential workflows.
- Parameters:
intervals (Sequence[IntervalVar]) – Ordered list of intervals to chain. Must have at least 2.
delays (int | Sequence[int] | None) –
Minimum delays between consecutive intervals. - If None: all delays are 0 (immediate succession allowed) - If int: same delay between all pairs - If list[int]: delays[i] is delay between intervals[i] and intervals[i+1]
Must have length = len(intervals) - 1
- Returns:
List of pycsp3 constraint nodes.
- Raises:
TypeError – If intervals are not IntervalVar or delays are not int.
ValueError – If fewer than 2 intervals or delays list has wrong length.
- Return type:
- Optional Intervals Handling:
If any interval is optional, the constraint between it and its neighbors is only enforced when both intervals are present. The chain “skips” absent intervals.
Example
>>> steps = [IntervalVar(size=d, name=f"step_{i}") for i, d in enumerate([5, 10, 3, 8])] >>> # Execute steps in order with no gaps allowed >>> satisfy(chain(steps))
>>> # With cleaning time of 2 between all steps >>> satisfy(chain(steps, delays=2))
>>> # With variable delays between steps >>> satisfy(chain(steps, delays=[1, 2, 1]))
- pycsp3_scheduling.constraints.chain.strict_chain(intervals, delays=None)[source]¶
Enforce strict sequential execution: each interval starts exactly when previous ends.
- For each consecutive pair (i, i+1):
end(intervals[i]) + delays[i] == start(intervals[i+1])
Unlike chain(), this enforces equality (no gaps between intervals).
- Parameters:
intervals (Sequence[IntervalVar]) – Ordered list of intervals to chain. Must have at least 2.
delays (int | Sequence[int] | None) – Fixed delays between consecutive intervals. - If None: all delays are 0 (no gap) - If int: same delay between all pairs - If list[int]: delays[i] is delay between intervals[i] and intervals[i+1]
- Returns:
List of pycsp3 constraint nodes.
- Raises:
TypeError – If intervals are not IntervalVar or delays are not int.
ValueError – If fewer than 2 intervals or delays list has wrong length.
- Return type:
Example
>>> tasks = [IntervalVar(size=d, name=f"task_{i}") for i, d in enumerate([5, 10, 3])] >>> # Tasks execute back-to-back with no gaps >>> satisfy(strict_chain(tasks))
>>> # With fixed 2-unit break between tasks >>> satisfy(strict_chain(tasks, delays=2))
Overlap Constraints¶
- pycsp3_scheduling.constraints.overlap.must_overlap(a, b)[source]¶
Constrain two intervals to share at least some time (overlap).
The intervals must have a non-zero overlap duration.
- Parameters:
a (IntervalVar) – First interval.
b (IntervalVar) – Second interval.
- Returns:
List of pycsp3 constraint nodes.
- Raises:
TypeError – If either argument is not an IntervalVar.
- Return type:
- Semantics:
start(a) < end(b) AND start(b) < end(a)
When either interval is optional and absent, the constraint behavior depends on the use case. By default, both must be present for the constraint to be meaningful.
Example
>>> meeting = IntervalVar(size=60, name="meeting") >>> availability = IntervalVar(start=9*60, end=17*60, size=480, name="available") >>> # Meeting must occur during availability >>> satisfy(must_overlap(meeting, availability))
- pycsp3_scheduling.constraints.overlap.overlap_at_least(a, b, min_overlap)[source]¶
Constrain two intervals to overlap by at least a minimum duration.
- Parameters:
a (IntervalVar) – First interval.
b (IntervalVar) – Second interval.
min_overlap (int) – Minimum required overlap duration.
- Returns:
List of pycsp3 constraint nodes.
- Raises:
TypeError – If intervals are not IntervalVar or min_overlap is not int.
ValueError – If min_overlap is negative.
- Return type:
- Semantics:
overlap_length(a, b) >= min_overlap
Where overlap_length = max(0, min(end(a), end(b)) - max(start(a), start(b)))
Example
>>> mentor = IntervalVar(size=120, name="mentor_shift") >>> trainee = IntervalVar(size=120, name="trainee_shift") >>> # Must overlap by at least 30 minutes for handoff >>> satisfy(overlap_at_least(mentor, trainee, 30))
- pycsp3_scheduling.constraints.overlap.no_overlap_pairwise(intervals)[source]¶
Constrain intervals to not overlap, using simple pairwise decomposition.
This is a simpler alternative to SeqNoOverlap when you don’t need sequence ordering or transition times.
- Parameters:
intervals (Sequence[IntervalVar]) – List of intervals that cannot overlap.
- Returns:
List of pycsp3 constraint nodes.
- Raises:
TypeError – If any element is not an IntervalVar.
- Return type:
- Semantics:
For each pair (i, j): end(i) <= start(j) OR end(j) <= start(i)
O(n²) constraints for n intervals
Example
>>> meetings = [IntervalVar(size=30, name=f"meeting_{i}") for i in range(5)] >>> # No two meetings can overlap >>> satisfy(no_overlap_pairwise(meetings))
- pycsp3_scheduling.constraints.overlap.disjunctive(intervals, transition_times=None)[source]¶
Constrain that at most one interval can be active at any time (unary resource).
This is equivalent to SeqNoOverlap but without creating a SequenceVar. Use this when you don’t need sequence ordering expressions.
- Parameters:
intervals (Sequence[IntervalVar]) – List of intervals sharing the unary resource.
transition_times (Sequence[Sequence[int]] | None) – Optional transition time matrix. If provided, intervals must have types assigned and transition_times[i][j] is the minimum time between an interval of type i and type j.
- Returns:
List of pycsp3 constraint nodes.
- Raises:
TypeError – If any element is not an IntervalVar.
ValueError – If transition_times provided but intervals don’t have types.
- Return type:
- Semantics:
For any two intervals: they cannot overlap in time
With transition_times: additional setup time required between types
Example
>>> tasks = [IntervalVar(size=d, name=f"task_{i}") for i, d in enumerate(durations)] >>> # Single machine - only one task at a time >>> satisfy(disjunctive(tasks))
Bounds Constraints¶
- pycsp3_scheduling.constraints.bounds.release_date(interval, time)[source]¶
Constrain an interval to not start before a given time.
The interval’s start time must be >= the release date.
- Parameters:
interval (IntervalVar) – The interval variable to constrain.
time (int) – The earliest allowed start time (release date).
- Returns:
List of pycsp3 constraint nodes.
- Return type:
Example
>>> task = IntervalVar(size=10, name="task") >>> satisfy(release_date(task, 8)) # Cannot start before time 8
- pycsp3_scheduling.constraints.bounds.deadline(interval, time)[source]¶
Constrain an interval to complete by a given time.
The interval’s end time must be <= the deadline.
- Parameters:
interval (IntervalVar) – The interval variable to constrain.
time (int) – The latest allowed end time (deadline).
- Returns:
List of pycsp3 constraint nodes.
- Return type:
Example
>>> task = IntervalVar(size=10, name="task") >>> satisfy(deadline(task, 50)) # Must finish by time 50
- pycsp3_scheduling.constraints.bounds.time_window(interval, earliest_start, latest_end)[source]¶
Constrain an interval to execute within a time window.
Combines release_date and deadline constraints.
- Parameters:
interval (IntervalVar) – The interval variable to constrain.
earliest_start (int) – The earliest allowed start time.
latest_end (int) – The latest allowed end time.
- Returns:
List of pycsp3 constraint nodes.
- Raises:
ValueError – If earliest_start > latest_end.
- Return type:
Example
>>> delivery = IntervalVar(size=30, name="delivery") >>> # Delivery must occur during business hours (9am-5pm as minutes) >>> satisfy(time_window(delivery, earliest_start=9*60, latest_end=17*60))
State Helpers¶
- pycsp3_scheduling.functions.state_functions.requires_state(interval, state_func, required_state)[source]¶
Simplified constraint that interval requires a specific state.
This is a convenience wrapper around always_equal with a more intuitive parameter order (interval first, like other constraint functions).
- Parameters:
interval (IntervalVar) – The interval requiring the state.
state_func (StateFunction) – The state function (resource).
required_state (int) – The required state value.
- Returns:
A StateConstraint representing the requirement.
- Return type:
StateConstraint
Example
>>> oven = StateFunction(name="oven_temp") >>> bake_task = IntervalVar(size=30, name="bake") >>> # Baking requires oven at temperature state 2 (e.g., 350F) >>> satisfy(requires_state(bake_task, oven, 2))
- pycsp3_scheduling.functions.state_functions.sets_state(interval, state_func, before_state, after_state)[source]¶
Interval transitions the state from one value to another.
This constraint models a task that changes the state of a resource. The state must be before_state when the interval starts (if specified), and becomes after_state when the interval ends.
- Parameters:
interval (IntervalVar) – The interval performing the state change.
state_func (StateFunction) – The state function.
before_state (int | None) – Required state before interval (None = any state).
after_state (int) – State after interval completes.
- Returns:
List of StateConstraints representing the state transition.
- Return type:
list[StateConstraint]
Example
>>> machine_mode = StateFunction(name="machine_mode") >>> changeover = IntervalVar(size=15, name="changeover_A_to_B") >>> # This changeover task transitions machine from mode A (0) to mode B (1) >>> satisfy(sets_state(changeover, machine_mode, before_state=0, after_state=1))
Aggregate Expressions¶
- pycsp3_scheduling.expressions.aggregate.count_present(intervals)[source]¶
Expression counting how many intervals are present.
- Parameters:
intervals (Sequence[IntervalVar]) – List of interval variables (can be optional or mandatory).
- Returns:
A pycsp3 Node representing the sum of presence values.
- Raises:
TypeError – If any element is not an IntervalVar.
ValueError – If intervals list is empty.
- Return type:
Node
- Semantics:
Returns sum(presence(i) for i in intervals)
Mandatory intervals contribute 1
Optional absent intervals contribute 0
Example
>>> tasks = [IntervalVar(size=10, optional=True, name=f"t_{i}") for i in range(10)] >>> # Must complete at least 5 tasks >>> satisfy(count_present(tasks) >= 5) >>> # Maximize number of completed tasks >>> maximize(count_present(tasks))
- pycsp3_scheduling.expressions.aggregate.earliest_start(intervals, absent_value=1073741823)[source]¶
Expression for the earliest start time among present intervals.
- Parameters:
intervals (Sequence[IntervalVar]) – List of interval variables.
absent_value (int) – Value to use for absent optional intervals. Defaults to INTERVAL_MAX so absent intervals don’t affect the minimum.
- Returns:
A pycsp3 Node representing min(start(i) for present i).
- Raises:
TypeError – If any element is not an IntervalVar.
ValueError – If intervals list is empty.
- Return type:
Node
- Semantics:
Returns the minimum start time among all present intervals
Absent intervals use absent_value (default: very large, so ignored in min)
Example
>>> tasks = [IntervalVar(size=10, name=f"t_{i}") for i in range(5)] >>> # All tasks must start after time 10 >>> satisfy(earliest_start(tasks) >= 10)
- pycsp3_scheduling.expressions.aggregate.latest_end(intervals, absent_value=0)[source]¶
Expression for the latest end time among present intervals.
- Parameters:
intervals (Sequence[IntervalVar]) – List of interval variables.
absent_value (int) – Value to use for absent optional intervals. Defaults to INTERVAL_MIN so absent intervals don’t affect the maximum.
- Returns:
A pycsp3 Node representing max(end(i) for present i).
- Raises:
TypeError – If any element is not an IntervalVar.
ValueError – If intervals list is empty.
- Return type:
Node
- Semantics:
Returns the maximum end time among all present intervals
Absent intervals use absent_value (default: very small, so ignored in max)
Example
>>> tasks = [IntervalVar(size=10, name=f"t_{i}") for i in range(5)] >>> # Minimize makespan >>> minimize(latest_end(tasks))
- pycsp3_scheduling.expressions.aggregate.span_length(intervals, absent_value=0)[source]¶
Expression for the total span from earliest start to latest end.
- Parameters:
intervals (Sequence[IntervalVar]) – List of interval variables.
absent_value (int) – Value to return if all intervals are absent.
- Returns:
A pycsp3 Node representing latest_end - earliest_start.
- Raises:
TypeError – If any element is not an IntervalVar.
ValueError – If intervals list is empty.
- Return type:
Node
- Semantics:
Returns max(end(i)) - min(start(i)) for present intervals
This is the makespan if all intervals must be scheduled
Example
>>> tasks = [IntervalVar(size=10, name=f"t_{i}") for i in range(5)] >>> # Minimize total span >>> minimize(span_length(tasks))
- pycsp3_scheduling.expressions.aggregate.makespan(intervals)[source]¶
Expression for the makespan (latest end time) of a set of intervals.
This is a convenience alias for latest_end() commonly used in scheduling.
- Parameters:
intervals (Sequence[IntervalVar]) – List of interval variables.
- Returns:
A pycsp3 Node representing max(end(i) for all i).
- Return type:
Node
Example
>>> tasks = [IntervalVar(size=d, name=f"t_{i}") for i, d in enumerate(durations)] >>> minimize(makespan(tasks))
Constraint Reference¶
Precedence Constraint Semantics¶
Constraint |
Semantics (when both present) |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Optional Interval Behavior¶
When one or both intervals are optional:
If either interval is absent, the constraint is trivially satisfied
If both intervals are present, the constraint is enforced
Usage Examples¶
Precedence Constraints¶
from pycsp3 import satisfy
from pycsp3_scheduling import IntervalVar, end_before_start, start_at_start
task1 = IntervalVar(size=10, name="task1")
task2 = IntervalVar(size=15, name="task2")
# task1 must complete before task2 starts
satisfy(end_before_start(task1, task2))
# task1 must complete at least 5 time units before task2 starts
satisfy(end_before_start(task1, task2, delay=5))
# task1 and task2 must start at the same time
satisfy(start_at_start(task1, task2))
No-Overlap Constraints¶
from pycsp3 import satisfy
from pycsp3_scheduling import IntervalVar, SequenceVar, SeqNoOverlap
tasks = [IntervalVar(size=10, name=f"task{i}") for i in range(3)]
# Simple no-overlap
satisfy(SeqNoOverlap(tasks))
# With sequence variable and transition matrix
seq = SequenceVar(intervals=tasks, types=[0, 1, 0], name="machine")
transition_matrix = [
[0, 5], # From type 0: 0→0=0, 0→1=5
[3, 0], # From type 1: 1→0=3, 1→1=0
]
satisfy(SeqNoOverlap(seq, transition_matrix=transition_matrix))
Grouping Constraints¶
from pycsp3 import satisfy
from pycsp3_scheduling import IntervalVar, span, alternative, synchronize
# Span: main interval covers all sub-intervals
project = IntervalVar(size=(0, 1000), name="project")
phases = [IntervalVar(size=s, name=f"phase{i}") for i, s in enumerate([10, 15, 20])]
satisfy(span(project, phases))
# Alternative: select exactly one alternative
main = IntervalVar(size=10, name="main")
alts = [IntervalVar(size=10, optional=True, name=f"alt{i}") for i in range(3)]
satisfy(alternative(main, alts))
# Synchronize: all intervals align with main
leader = IntervalVar(size=10, name="leader")
followers = [IntervalVar(size=10, name=f"f{i}") for i in range(2)]
satisfy(synchronize(leader, followers))
Sequence Ordering¶
from pycsp3 import satisfy
from pycsp3_scheduling import IntervalVar, SequenceVar, first, last, before, previous
tasks = [IntervalVar(size=5, name=f"t{i}") for i in range(4)]
seq = SequenceVar(intervals=tasks, name="machine")
# task[0] must be first in sequence
satisfy(first(seq, tasks[0]))
# task[3] must be last in sequence
satisfy(last(seq, tasks[3]))
# task[1] must come before task[2] (not necessarily immediately)
satisfy(before(seq, tasks[1], tasks[2]))
# task[1] must immediately precede task[2]
satisfy(previous(seq, tasks[1], tasks[2]))
Forbidden Time Constraints¶
from pycsp3 import satisfy
from pycsp3_scheduling import IntervalVar, forbid_start, forbid_end, forbid_extent
task = IntervalVar(size=10, name="task")
# Cannot start during lunch break (12-13) or after hours (17-24)
satisfy(forbid_start(task, [(12, 13), (17, 24)]))
# Cannot end during maintenance window (6-8)
satisfy(forbid_end(task, [(6, 8)]))
# Cannot overlap the lunch break at all
satisfy(forbid_extent(task, [(12, 13)]))
Presence Constraints¶
from pycsp3 import satisfy
from pycsp3_scheduling import (
IntervalVar, presence_implies, presence_or, presence_xor,
all_present_or_all_absent, at_least_k_present, exactly_k_present
)
# Setup must run if main task runs
setup = IntervalVar(size=5, optional=True, name="setup")
main = IntervalVar(size=20, optional=True, name="main")
satisfy(presence_implies(main, setup))
# At least one delivery method must be chosen
delivery_a = IntervalVar(size=30, optional=True, name="delivery_a")
delivery_b = IntervalVar(size=45, optional=True, name="delivery_b")
satisfy(presence_or(delivery_a, delivery_b))
# Exactly one route must be taken
route_a = IntervalVar(size=30, optional=True, name="route_a")
route_b = IntervalVar(size=45, optional=True, name="route_b")
satisfy(presence_xor(route_a, route_b))
# All subtasks must run together or not at all
subtasks = [IntervalVar(size=5, optional=True, name=f"sub_{i}") for i in range(3)]
satisfy(all_present_or_all_absent(subtasks))
# At least 3 of 5 tasks must be completed
tasks = [IntervalVar(size=10, optional=True, name=f"t_{i}") for i in range(5)]
satisfy(at_least_k_present(tasks, 3))
# Exactly 2 workers must be assigned
workers = [IntervalVar(size=480, optional=True, name=f"w_{i}") for i in range(4)]
satisfy(exactly_k_present(workers, 2))
Chain Constraints¶
from pycsp3 import satisfy
from pycsp3_scheduling import IntervalVar, chain, strict_chain
# Assembly line: steps must execute in order
steps = [IntervalVar(size=d, name=f"step_{i}") for i, d in enumerate([5, 10, 3, 8])]
satisfy(chain(steps))
# With minimum gap of 2 between each step
satisfy(chain(steps, delays=2))
# With variable gaps between steps
satisfy(chain(steps, delays=[1, 2, 3]))
# Back-to-back execution (no gaps)
satisfy(strict_chain(steps))
Overlap Constraints¶
from pycsp3 import satisfy
from pycsp3_scheduling import (
IntervalVar, must_overlap, overlap_at_least,
no_overlap_pairwise, disjunctive
)
# Two intervals must overlap
meeting = IntervalVar(size=60, name="meeting")
availability = IntervalVar(size=480, name="available")
satisfy(must_overlap(meeting, availability))
# Shifts must overlap by at least 30 minutes for handoff
mentor_shift = IntervalVar(size=120, name="mentor")
trainee_shift = IntervalVar(size=120, name="trainee")
satisfy(overlap_at_least(mentor_shift, trainee_shift, 30))
# Simple pairwise no-overlap (without sequence variable)
meetings = [IntervalVar(size=30, name=f"meeting_{i}") for i in range(5)]
satisfy(no_overlap_pairwise(meetings))
# Disjunctive resource (unary - only one task at a time)
tasks = [IntervalVar(size=d, name=f"task_{i}") for i, d in enumerate([10, 15, 8])]
satisfy(disjunctive(tasks))
# Disjunctive with transition times between types
transition = [[0, 5], [3, 0]]
satisfy(disjunctive(tasks, transition_times=transition))
Bounds Constraints¶
from pycsp3 import satisfy
from pycsp3_scheduling import (
IntervalVar, release_date, deadline, time_window
)
task = IntervalVar(size=30, name="delivery")
# Task cannot start before 9am (time 540 in minutes)
satisfy(release_date(task, 540))
# Task must complete by 5pm (time 1020 in minutes)
satisfy(deadline(task, 1020))
# Combined: task must execute within business hours
satisfy(time_window(task, earliest_start=540, latest_end=1020))
# Works with optional intervals too
optional_task = IntervalVar(size=30, optional=True, name="optional_delivery")
satisfy(time_window(optional_task, earliest_start=540, latest_end=1020))
# Operator syntax (shorthand) - more concise!
satisfy(task >= 540) # same as release_date(task, 540)
satisfy(task <= 1020) # same as deadline(task, 1020)
satisfy(task > 500) # start strictly after 500
satisfy(task < 1100) # end strictly before 1100
# Works great in list comprehensions
tasks = [IntervalVar(size=30, name=f"task_{i}") for i in range(5)]
satisfy(t >= 0 for t in tasks) # all tasks start at or after 0
satisfy(t <= 1000 for t in tasks) # all tasks end by time 1000
State Helpers¶
from pycsp3 import satisfy
from pycsp3_scheduling import (
IntervalVar, StateFunction, requires_state, sets_state
)
# Create a state function for oven temperature
oven = StateFunction(name="oven_temp")
# Baking task requires oven at temperature state 2
bake = IntervalVar(size=30, name="bake")
satisfy(requires_state(bake, oven, 2))
# Preheat task transitions oven from cold (0) to hot (2)
preheat = IntervalVar(size=15, name="preheat")
satisfy(sets_state(preheat, oven, before_state=0, after_state=2))
# Cooldown task transitions oven from any state to cold
cooldown = IntervalVar(size=20, name="cooldown")
satisfy(sets_state(cooldown, oven, before_state=None, after_state=0))
Aggregate Expressions¶
from pycsp3 import satisfy, minimize, maximize
from pycsp3_scheduling import (
IntervalVar, count_present, earliest_start, latest_end,
span_length, makespan
)
tasks = [IntervalVar(size=10, optional=True, name=f"t_{i}") for i in range(10)]
# Must complete at least 5 tasks
satisfy(count_present(tasks) >= 5)
# Maximize number of completed tasks
maximize(count_present(tasks))
# All tasks must start after time 10
satisfy(earliest_start(tasks) >= 10)
# All tasks must end before time 100
satisfy(latest_end(tasks) <= 100)
# Minimize makespan (latest end time)
minimize(makespan(tasks))
# Minimize total span
minimize(span_length(tasks))