Constraints
Constraints restrict the solution space by specifying relationships between variables. OptalCP distinguishes between Constraint objects and BoolExpr expressions.
Constraint vs BoolExpr
Understanding this distinction matters:
| Type | Creation | Registration | Examples |
|---|---|---|---|
| Constraint | Returned by specific functions | Auto-registers when created | endBeforeStart()noOverlap()alternative() |
| BoolExpr | Returned by comparisons and boolean ops | Requires model.enforce() | task1.end() <= task2.start()x + y <= 10b1 | b2 |
Why the difference?
- BoolExpr can be composed into larger expressions (
b1 | b2,b.implies(c),b1 + b2), so it cannot auto-register—you may want to use it as a building block. - Constraint cannot be used as a subexpression—it's terminal. Since nothing else can be done with it, it auto-registers immediately.
Auto-Registration vs Explicit Enforcement
- Python
- TypeScript
import optalcp as cp
model = cp.Model()
task1 = model.interval_var(length=10, name="task1")
task2 = model.interval_var(length=20, name="task2")
x = model.int_var(min=0, max=100, name="x")
# Constraint - auto-registers
task1.end_before_start(task2) # Registered immediately
# BoolExpr - requires model.enforce()
model.enforce(task1.end() <= task2.start()) # Must explicitly enforce
model.enforce(x >= 10)
import * as CP from '@scheduleopt/optalcp';
const model = new CP.Model();
const task1 = model.intervalVar({ length: 10, name: "task1" });
const task2 = model.intervalVar({ length: 20, name: "task2" });
const x = model.intVar({ min: 0, max: 100, name: "x" });
// Constraint - auto-registers
task1.endBeforeStart(task2); // Registered immediately
// BoolExpr - requires model.enforce()
model.enforce(task1.end().le(task2.start())); // Must explicitly enforce
model.enforce(x.ge(10));
Common mistake: Creating a BoolExpr without enforcing it:
- Python
- TypeScript
# WRONG - does nothing!
task1.end() <= task2.start()
# CORRECT
model.enforce(task1.end() <= task2.start())
// WRONG - does nothing!
task1.end().le(task2.start());
// CORRECT
model.enforce(task1.end().le(task2.start()));
Enforcing BoolExpr Constraints
Use model.enforce() to add any BoolExpr as a constraint:
- Python
- TypeScript
x = model.int_var(min=0, max=100, name="x")
b = model.bool_var(name="b")
task1 = model.interval_var(length=10, optional=True, name="task1")
task2 = model.interval_var(length=20, optional=True, name="task2")
# Arithmetic comparisons
model.enforce(x + 10 <= 100)
model.enforce(task1.end() <= 200)
# Boolean logic
model.enforce(b.implies(task1.presence()))
# Presence synchronization
model.enforce(task1.presence() == task2.presence())
# Precedence (expression style)
model.enforce(task1.end() <= task2.start())
const x = model.intVar({ min: 0, max: 100, name: "x" });
const b = model.boolVar({ name: "b" });
const task1 = model.intervalVar({ length: 10, optional: true, name: "task1" });
const task2 = model.intervalVar({ length: 20, optional: true, name: "task2" });
// Arithmetic comparisons
model.enforce(x.plus(10).le(100));
model.enforce(task1.end().le(200));
// Boolean logic
model.enforce(b.implies(task1.presence()));
// Presence synchronization
model.enforce(task1.presence().eq(task2.presence()));
// Precedence (expression style)
model.enforce(task1.end().le(task2.start()));
See Expressions for all available operators (arithmetic, comparison, boolean).
A constraint is satisfied if its expression is not false—meaning true or absent.
An absent constraint is treated as non-existent: it places no restrictions on the solution. This is the intended behavior—when an optional task is absent, constraints involving it simply don't apply. For example, task1.end() <= task2.start() is trivially satisfied when either task is absent.
Don't wrap constraints in presence checks—this severely hurts propagation and gains nothing. The system is designed so that absent values automatically satisfy constraints. If you need a default value for absent expressions, use guard() instead.
- Python
- TypeScript
# WRONG - hurts propagation, gains nothing
model.enforce((x.presence() & y.presence()).implies(x.end() <= y.end()))
# CORRECT - simpler and propagates better
model.enforce(x.end() <= y.end())
# WRONG
model.enforce(~task.presence() | (task.end() <= 100))
# CORRECT
model.enforce(task.end() <= 100)
# WRONG
model.enforce(task.presence().implies(model.eval(cost_func, task.start()) <= 50))
# CORRECT
model.enforce(model.eval(cost_func, task.start()) <= 50)
// WRONG - hurts propagation, gains nothing
model.enforce(x.presence().and(y.presence()).implies(x.end().le(y.end())));
// CORRECT - simpler and propagates better
model.enforce(x.end().le(y.end()));
// WRONG
model.enforce(task.presence().not().or(task.end().le(100)));
// CORRECT
model.enforce(task.end().le(100));
// WRONG
model.enforce(task.presence().implies(model.eval(costFunc, task.start()).le(50)));
// CORRECT
model.enforce(model.eval(costFunc, task.start()).le(50));
Precedence Constraints
Precedence constraints express temporal relationships between intervals. OptalCP has 8 precedence functions on IntervalVar:
The 8 Precedence Functions
| Function | Relationship | Equivalent Expression |
|---|---|---|
end_before_start | task1 ends before task2 starts | task1.end() + delay <= task2.start() |
end_before_end | task1 ends before task2 ends | task1.end() + delay <= task2.end() |
start_before_start | task1 starts before task2 starts | task1.start() + delay <= task2.start() |
start_before_end | task1 starts before task2 ends | task1.start() + delay <= task2.end() |
end_at_start | task1 ends when task2 starts | task1.end() + delay == task2.start() |
end_at_end | task1 ends when task2 ends | task1.end() + delay == task2.end() |
start_at_start | task1 starts when task2 starts | task1.start() + delay == task2.start() |
start_at_end | task1 starts when task2 ends | task1.start() + delay == task2.end() |
All functions:
- Return a
Constraintobject (auto-register) - Accept an optional
delayparameter (default: 0) - Are satisfied trivially if either interval is absent
Examples
- Python
- TypeScript
task1 = model.interval_var(length=10, name="task1")
task2 = model.interval_var(length=20, name="task2")
task3 = model.interval_var(length=15, name="task3")
# Basic precedence: task1 must finish before task2 starts
task1.end_before_start(task2)
# With delay: task1 must finish at least 5 time units before task2 starts
task1.end_before_start(task2, delay=5)
# task2 and task3 must start at the same time
task2.start_at_start(task3)
# task3 starts when task1 ends
task1.end_at_start(task3)
# task2 ends before task3 ends
task2.end_before_end(task3)
# task1 starts before task2 starts
task1.start_before_start(task2)
# task1 starts before task3 ends
task1.start_before_end(task3)
# task2 and task3 end at the same time
task2.end_at_end(task3)
const task1 = model.intervalVar({ length: 10, name: "task1" });
const task2 = model.intervalVar({ length: 20, name: "task2" });
const task3 = model.intervalVar({ length: 15, name: "task3" });
// Basic precedence: task1 must finish before task2 starts
task1.endBeforeStart(task2);
// With delay: task1 must finish at least 5 time units before task2 starts
task1.endBeforeStart(task2, 5);
// task2 and task3 must start at the same time
task2.startAtStart(task3);
// task3 starts when task1 ends
task1.endAtStart(task3);
// task2 ends before task3 ends
task2.endBeforeEnd(task3);
// task1 starts before task2 starts
task1.startBeforeStart(task2);
// task1 starts before task3 ends
task1.startBeforeEnd(task3);
// task2 and task3 end at the same time
task2.endAtEnd(task3);
Method vs Expression Style
You can express precedence in two equivalent ways:
- Python
- TypeScript
# Method style (clearer intent, auto-registers)
task1.end_before_start(task2, delay=10)
# Expression style (more flexible)
model.enforce(task1.end() + 10 <= task2.start())
// Method style (clearer intent, auto-registers)
task1.endBeforeStart(task2, 10);
// Expression style (more flexible)
model.enforce(task1.end().plus(10).le(task2.start()));
Other Constraints
These constraints also auto-register when created:
- Alternative — Exactly one option interval is present when main is present
- Span — Parent interval covers all present children
- No Overlap — Intervals cannot overlap (disjunctive resource)
- Cumulative — Sum of pulses must not exceed capacity
Complete Example
- Python
- TypeScript
import optalcp as cp
model = cp.Model()
# Tasks
cut = model.interval_var(length=30, name="cut")
sand = model.interval_var(name="sand") # Length determined by alternative
assemble = model.interval_var(length=45, name="assemble")
inspect = model.interval_var(length=10, name="inspect")
# Sanding can use fast machine (expensive) or slow machine (cheap)
sand_fast = model.interval_var(length=10, optional=True, name="sand_fast")
sand_slow = model.interval_var(length=30, optional=True, name="sand_slow")
model.alternative(sand, [sand_fast, sand_slow])
# Precedence constraints (auto-register)
cut.end_before_start(sand)
sand.end_before_start(assemble)
assemble.end_before_start(inspect, delay=5) # 5-unit cooling period
# Time window constraints (require enforce)
model.enforce(cut.start() >= 0)
model.enforce(inspect.end() <= 200)
# Minimize completion time + machine cost
fast_machine_cost = sand_fast.presence().guard(0) * 50
model.minimize(inspect.end() + fast_machine_cost)
result = model.solve()
if result.solution:
print(f"Cut: {result.solution.get_start(cut)}-{result.solution.get_end(cut)}")
print(f"Sand: {result.solution.get_start(sand)}-{result.solution.get_end(sand)}")
if result.solution.is_present(sand_fast):
print(" (using fast machine)")
else:
print(" (using slow machine)")
print(f"Assemble: {result.solution.get_start(assemble)}-{result.solution.get_end(assemble)}")
print(f"Inspect: {result.solution.get_start(inspect)}-{result.solution.get_end(inspect)}")
print(f"Objective: {result.objective}")
import * as CP from '@scheduleopt/optalcp';
const model = new CP.Model();
// Tasks
const cut = model.intervalVar({ length: 30, name: "cut" });
const sand = model.intervalVar({ name: "sand" }); // Length determined by alternative
const assemble = model.intervalVar({ length: 45, name: "assemble" });
const inspect = model.intervalVar({ length: 10, name: "inspect" });
// Sanding can use fast machine (expensive) or slow machine (cheap)
const sandFast = model.intervalVar({ length: 10, optional: true, name: "sand_fast" });
const sandSlow = model.intervalVar({ length: 30, optional: true, name: "sand_slow" });
model.alternative(sand, [sandFast, sandSlow]);
// Precedence constraints (auto-register)
cut.endBeforeStart(sand);
sand.endBeforeStart(assemble);
assemble.endBeforeStart(inspect, 5); // 5-unit cooling period
// Time window constraints (require enforce)
model.enforce(cut.start().ge(0));
model.enforce(inspect.end().le(200));
// Minimize completion time + machine cost
const fastMachineCost = sandFast.presence().guard(0).times(50);
model.minimize(inspect.end().plus(fastMachineCost));
const result = await model.solve();
if (result.solution) {
console.log(`Cut: ${result.solution.getStart(cut)}-${result.solution.getEnd(cut)}`);
console.log(`Sand: ${result.solution.getStart(sand)}-${result.solution.getEnd(sand)}`);
if (result.solution.isPresent(sandFast)) {
console.log(" (using fast machine)");
} else {
console.log(" (using slow machine)");
}
console.log(`Assemble: ${result.solution.getStart(assemble)}-${result.solution.getEnd(assemble)}`);
console.log(`Inspect: ${result.solution.getStart(inspect)}-${result.solution.getEnd(inspect)}`);
console.log(`Objective: ${result.objective}`);
}
See Also
- Intervals — IntervalVar and precedence functions
- Expressions — Building expressions for constraints
- Alternative, Span — Structured interval constraints
- Resources — No-overlap and cumulative constraints