Skip to main content

No Overlap Constraints

The no_overlap constraint ensures that a set of interval variables do not overlap in time. This is used for disjunctive resources - resources that can handle only one task at a time.

Basic Usage​

The simplest form takes a list of intervals:

import optalcp as cp

model = cp.Model()

task1 = model.interval_var(length=10, name="task1")
task2 = model.interval_var(length=15, name="task2")
task3 = model.interval_var(length=20, name="task3")

# These tasks cannot overlap
model.no_overlap([task1, task2, task3])

model.minimize(model.max([task1.end(), task2.end(), task3.end()]))
result = model.solve()

if result.solution:
print(f"Task1: [{result.solution.get_start(task1)}, {result.solution.get_end(task1)})")
print(f"Task2: [{result.solution.get_start(task2)}, {result.solution.get_end(task2)})")
print(f"Task3: [{result.solution.get_start(task3)}, {result.solution.get_end(task3)})")

Signature​

model.no_overlap(
intervals: Iterable[IntervalVar],
transitions: Iterable[Iterable[int]] | None = None
) -> Constraint

Parameters:

  • intervals: Interval variables that must not overlap
  • transitions: Optional transition time matrix (see Transition Times)

Returns: Constraint object (auto-registered with the model)

Semantics​

  • Intervals are scheduled sequentially with no time overlap
  • The order is determined by the solver
  • If an interval is optional and absent, it is not part of the sequence
  • There is no enforced ordering beyond non-overlap (use precedence constraints for that)

Sequence Variables​

For more control over task ordering, use a SequenceVar:

import optalcp as cp

model = cp.Model()

task1 = model.interval_var(length=10, name="task1")
task2 = model.interval_var(length=15, name="task2")
task3 = model.interval_var(length=20, name="task3")

# Create sequence variable
seq = model.sequence_var([task1, task2, task3])

# No overlap constraint using sequence
model.no_overlap(seq)

# Query position in sequence (0-indexed)
pos1 = task1.position(seq)

# Example: task1 must be scheduled in the first two positions
model.enforce(pos1 <= 1)

result = model.solve()

SequenceVar Signature​

model.sequence_var(
intervals: Iterable[IntervalVar],
types: Iterable[int] | None = None,
name: str | None = None
) -> SequenceVar

Parameters:

  • intervals: Interval variables in the sequence
  • types: Optional type index for each interval (used with transition times)
  • name: Optional name for the sequence variable

Returns: SequenceVar object

Requirement: Each SequenceVar must be used by exactly one no_overlap constraint.

position() Function​

interval.position(sequence: SequenceVar) -> IntExpr
# Or equivalently:
model.position(interval: IntervalVar, sequence: SequenceVar) -> IntExpr

Returns: Integer expression representing the 0-indexed position of the interval in the sequence.

Behavior: If the interval is optional and absent, the position expression is also absent.

Limitations:

  • Cannot be used with intervals that may have zero length (position of simultaneous zero-length intervals is undefined)
  • Cannot be used when the no_overlap constraint has transition times
# Optional intervals in sequence
task_a = model.interval_var(length=10, optional=True, name="task_a")
task_b = model.interval_var(length=15, optional=True, name="task_b")

seq = model.sequence_var([task_a, task_b])
model.no_overlap(seq)

pos_a = task_a.position(seq)

# If task_a is present, it must be first (position 0)
# When task_a is absent, pos_a is absent and the constraint is satisfied
model.enforce(pos_a == 0)

Transition Times​

Transition times model setup or changeover times between consecutive tasks. The transition time is added between the end of one task and the start of the next.

Basic Transition Times​

Provide a square matrix where transitions[i][j] is the time required between task i and task j:

import optalcp as cp

model = cp.Model()

task1 = model.interval_var(length=10, name="task1")
task2 = model.interval_var(length=15, name="task2")
task3 = model.interval_var(length=20, name="task3")

# Transition time matrix (3x3 for 3 tasks)
transitions = [
[0, 5, 10], # From task1 to task1/task2/task3
[8, 0, 3], # From task2 to task1/task2/task3
[12, 7, 0], # From task3 to task1/task2/task3
]

model.no_overlap([task1, task2, task3], transitions)

result = model.solve()

Matrix requirements:

  • Square matrix: N×N where N is the number of intervals
  • transitions[i][j] ≥ 0 for all i, j
  • Diagonal entries (same task to itself) are typically 0 but can be non-zero

Semantics:

  • For any pair of tasks i and j where task i precedes task j in the sequence, then task_i.end() + transitions[i][j] <= task_j.start()
  • This applies to all pairs, not just consecutive tasks. For example, if the sequence is A → B → C, then both A.end() + transitions[A][C] <= C.start() and A.end() + transitions[A][B] <= B.start() must hold.

Type-Based Transition Times​

For tasks with types (e.g., job types, colors, tools), use the types parameter to reduce matrix size:

import optalcp as cp

model = cp.Model()

# 5 tasks with 3 different types
task1 = model.interval_var(length=10, name="task1") # Type 0
task2 = model.interval_var(length=15, name="task2") # Type 1
task3 = model.interval_var(length=12, name="task3") # Type 0
task4 = model.interval_var(length=18, name="task4") # Type 2
task5 = model.interval_var(length=14, name="task5") # Type 1

# Task types (parallel to intervals list)
types = [0, 1, 0, 2, 1]

# Type-based transition matrix (3x3 for 3 types)
transitions = [
[0, 10, 15], # From type 0 to type 0/1/2
[12, 0, 8], # From type 1 to type 0/1/2
[20, 5, 0], # From type 2 to type 0/1/2
]

seq = model.sequence_var([task1, task2, task3, task4, task5], types)
model.no_overlap(seq, transitions)

result = model.solve()

Usage:

  • types must be a list parallel to intervals, with type index for each interval
  • Type indices must be in range [0, num_types)
  • Transition matrix is num_types × num_types
  • Significantly reduces matrix size for large problems with few types

Use Cases​

Machine Scheduling​

A single machine processes multiple jobs:

jobs = [model.interval_var(length=durations[i], name=f"job{i}")
for i in range(num_jobs)]
model.no_overlap(jobs)

Resource Scheduling with Setup Times​

Painting tasks with cleaning time between different colors:

# Paint colors: 0=red, 1=blue, 2=green
types = [0, 0, 1, 2, 1, 0] # Color for each task

# Cleaning times between colors
cleaning_times = [
[0, 10, 15], # Red to red/blue/green
[10, 0, 12], # Blue to red/blue/green
[15, 12, 0], # Green to red/blue/green
]

seq = model.sequence_var(paint_tasks, types)
model.no_overlap(seq, cleaning_times)

Ordering Constraints​

Enforce specific orderings using position():

seq = model.sequence_var(tasks)
model.no_overlap(seq)

# Task C must be first or last
pos_c = task_c.position(seq)
model.enforce((pos_c == 0) | (pos_c == len(tasks) - 1))
Use Precedence for Simple Ordering

If you only need "task A before task B", use precedence constraints instead of position():

# BAD: using position() for simple ordering
seq = model.sequence_var(tasks)
model.no_overlap(seq)
model.enforce(task_a.position(seq) < task_b.position(seq))

# GOOD: use precedence constraint
task_a.end_before_start(task_b)

Precedence constraints are simpler and propagate more efficiently. Use position() when you need to synchronize positions across two sequences or constrain the actual position value.

Synchronizing Two Sequences​

Use position() when tasks must maintain the same order on two different machines:

# Jobs visit machine A then machine B
# Jobs must be processed in the same order on both machines

jobs_on_a = [model.interval_var(length=10, name=f"job{i}_A") for i in range(5)]
jobs_on_b = [model.interval_var(length=15, name=f"job{i}_B") for i in range(5)]

seq_a = model.sequence_var(jobs_on_a)
seq_b = model.sequence_var(jobs_on_b)

model.no_overlap(seq_a)
model.no_overlap(seq_b)

# Synchronize: same position on both machines
for i in range(5):
model.enforce(jobs_on_a[i].position(seq_a) == jobs_on_b[i].position(seq_b))

Edge Cases​

Empty Interval List​

An empty list is valid and creates a trivially satisfied constraint:

model.no_overlap([]) # Valid, no effect

Single Interval​

A single interval is valid and creates a trivially satisfied constraint:

model.no_overlap([task]) # Valid, always satisfied

All Intervals Absent​

If all intervals in a no_overlap constraint are optional and absent, the constraint is satisfied:

task1 = model.interval_var(length=10, optional=True)
task2 = model.interval_var(length=15, optional=True)

model.no_overlap([task1, task2])
model.enforce(~task1.presence())
model.enforce(~task2.presence())

result = model.solve() # Feasible

Transition Matrix Dimension Mismatch​

The transition matrix must match the number of intervals (or types):

# ERROR: 3 tasks but 2x2 matrix
tasks = [task1, task2, task3]
transitions = [[0, 5], [5, 0]]
model.no_overlap(tasks, transitions=transitions) # Runtime error

Performance Considerations​

  • Propagation level: Controlled by noOverlapPropagationLevel parameter (1-4, default 4)

    • Level 1: Basic timetabling
    • Level 4: Full propagation (slowest but strongest)
    • Reduce for very large problems
  • Transition times: Add complexity to the constraint

    • Use type-based transitions to reduce matrix size
    • Consider modeling as separate precedence constraints if transitions are sparse
  • Sequence variables: No overhead - a sequence variable is created automatically when not specified by the user

See also​