Alternative Constraints
So far, every task in our model has been mandatory: it must be executed exactly once. But real scheduling problems often involve choices:
- A task can be performed on machine A or machine B
- A delivery can go by truck or by air
- A manufacturing step can use method 1 (slow, cheap) or method 2 (fast, expensive)
In our furniture workshop, let's introduce a choice for sanding: hand sanding (slow but doesn't require equipment) or power sander (faster but the power sander is a shared resource).
Sanding Options
We'll modify our model so that sanding can be done two ways:
-
Hand sanding:
- Desk: 30 minutes (slower than the original 20 minutes)
- Chair: 25 minutes (slower than the original 15 minutes)
- No special equipment needed
-
Power sanding:
- Desk: 15 minutes (faster)
- Chair: 10 minutes (faster)
- Requires the power sander (one available, shared between desk and chair)
The solver will choose which method to use for each item to minimize makespan.
Optional Intervals
To model choices, we use optional intervals: intervals that may or may not be executed. We create one interval for each option, mark them all as optional=True, and then add an alternative constraint to ensure exactly one is chosen.
- Python
- TypeScript
# Sanding the desk parts - two options
sand_desk_parts = model.interval_var(name="SandDeskParts") # Main task (always present)
sand_desk_hand = model.interval_var(length=30, optional=True, name="SandDeskHand")
sand_desk_power = model.interval_var(length=15, optional=True, name="SandDeskPower")
# Alternative: exactly one option is executed
model.alternative(sand_desk_parts, [sand_desk_hand, sand_desk_power])
// Sanding the desk parts - two options
const sandDeskParts = model.intervalVar({ name: "SandDeskParts" }); // Main task (always present)
const sandDeskHand = model.intervalVar({ length: 30, optional: true, name: "SandDeskHand" });
const sandDeskPower = model.intervalVar({ length: 15, optional: true, name: "SandDeskPower" });
// Alternative: exactly one option is executed
model.alternative(sandDeskParts, [sandDeskHand, sandDeskPower]);
How Alternative Works
The alternative(main, [option1, option2, ...]) constraint ensures:
- Exactly one option is present: One of the options must be executed, and the others are absent
- Main interval covers the chosen option: The main interval starts when the chosen option starts and ends when it ends
- Presence is synchronized: If main is present, exactly one option is present; if main is absent, all options are absent
In our model, sand_desk_parts is mandatory (not optional), so exactly one sanding method will always be chosen. But the main interval can also be optional—useful when the entire choice is conditional.
This pattern lets you model choices while maintaining a single "handle" (sand_desk_parts) that other constraints can reference.
Power Sander as a Shared Resource
If both desk and chair are power-sanded, they can't use the power sander simultaneously. We add a no-overlap constraint on the power sanding options:
- Python
- TypeScript
# Power sander is shared - can only be used by one item at a time
model.no_overlap([sand_desk_power, sand_chair_power])
// Power sander is shared - can only be used by one item at a time
model.noOverlap([sandDeskPower, sandChairPower]);
If one or both sanding operations choose hand sanding instead, those intervals are absent, and they don't participate in the no-overlap constraint. OptalCP handles this automatically.
Constraining the Main Interval
Use the main interval in constraints and expressions whenever possible—not just for precedence, but for cumulative constraints, objectives, and any other constraints that apply regardless of which option is chosen.
Our precedence constraints reference the main intervals (sand_desk_parts, sand_chair_parts), not the individual options:
- Python
- TypeScript
# Precedence uses the main intervals
cut_desk_wood.end_before_start(sand_desk_parts)
sand_desk_parts.end_before_start(assemble_desk)
// Precedence uses the main intervals
cutDeskWood.endBeforeStart(sandDeskParts);
sandDeskParts.endBeforeStart(assembleDesk);
This approach has two benefits:
- Simpler model: You write one constraint instead of duplicating it for each option
- Better performance: The solver can propagate constraints more effectively through the main interval
Since sand_desk_parts covers whichever option is chosen, the precedence works correctly regardless of which sanding method is selected.
When to constrain option intervals instead: Use the option interval when the constraint is specific to that option. For example, the power sander no-overlap constraint only applies to sand_desk_power and sand_chair_power—it wouldn't make sense to put it on the main intervals.
Complete Model with Alternatives
Let's integrate the sanding alternatives into our full model:
- Python
- TypeScript
import optalcp as cp
model = cp.Model()
# Create interval variables
cut_desk_wood = model.interval_var(length=30, name="CutDeskWood")
cut_chair_wood = model.interval_var(length=25, name="CutChairWood")
# [NEW] Sanding alternatives for desk
sand_desk_parts = model.interval_var(name="SandDeskParts")
sand_desk_hand = model.interval_var(length=30, optional=True, name="SandDeskHand")
sand_desk_power = model.interval_var(length=15, optional=True, name="SandDeskPower")
model.alternative(sand_desk_parts, [sand_desk_hand, sand_desk_power])
# [NEW] Sanding alternatives for chair
sand_chair_parts = model.interval_var(name="SandChairParts")
sand_chair_hand = model.interval_var(length=25, optional=True, name="SandChairHand")
sand_chair_power = model.interval_var(length=10, optional=True, name="SandChairPower")
model.alternative(sand_chair_parts, [sand_chair_hand, sand_chair_power])
# Rest of the tasks
assemble_desk = model.interval_var(length=45, name="AssembleDesk")
assemble_chair = model.interval_var(length=35, name="AssembleChair")
stain_desk = model.interval_var(length=20, name="StainDesk")
stain_chair = model.interval_var(length=15, name="StainChair")
apply_finish = model.interval_var(length=30, name="ApplyFinish")
final_inspect = model.interval_var(length=10, name="FinalInspect")
# Precedence constraints - Desk
cut_desk_wood.end_before_start(sand_desk_parts)
sand_desk_parts.end_before_start(assemble_desk)
assemble_desk.end_before_start(stain_desk)
stain_desk.end_before_start(apply_finish)
# Precedence constraints - Chair
cut_chair_wood.end_before_start(sand_chair_parts)
sand_chair_parts.end_before_start(assemble_chair)
assemble_chair.end_before_start(stain_chair)
stain_chair.end_before_start(apply_finish)
# Final inspection
apply_finish.end_before_start(final_inspect)
# Resource constraint: one saw
model.no_overlap([cut_desk_wood, cut_chair_wood])
# [NEW] Resource constraint: power sander (only if both choose power sanding)
model.no_overlap([sand_desk_power, sand_chair_power])
# Resource constraint: 2 workers
# Note: Use main intervals for worker counting
worker_usage = [
model.pulse(cut_desk_wood, 1),
model.pulse(cut_chair_wood, 1),
model.pulse(sand_desk_parts, 1),
model.pulse(sand_chair_parts, 1),
model.pulse(assemble_desk, 2),
model.pulse(assemble_chair, 1),
model.pulse(stain_desk, 1),
model.pulse(stain_chair, 1),
model.pulse(apply_finish, 1),
model.pulse(final_inspect, 1),
]
model.enforce(model.sum(worker_usage) <= 2)
# Resource constraint: spray booth with transitions
spray_booth_seq = model.sequence_var([stain_desk, stain_chair, apply_finish])
transitions = [
[0, 10, 5],
[10, 0, 5],
[5, 5, 0],
]
model.no_overlap(spray_booth_seq, transitions)
# Minimize makespan
model.minimize(final_inspect.end())
# Solve
result = model.solve()
if result.solution:
print(f"Makespan: {result.objective} minutes")
print(f"Optimal: {result.proof}")
# Check which sanding method was chosen
if result.solution.is_present(sand_desk_hand):
print("Desk: hand sanding")
elif result.solution.is_present(sand_desk_power):
print("Desk: power sanding")
if result.solution.is_present(sand_chair_hand):
print("Chair: hand sanding")
elif result.solution.is_present(sand_chair_power):
print("Chair: power sanding")
import * as CP from '@scheduleopt/optalcp';
const model = new CP.Model();
// Create interval variables
const cutDeskWood = model.intervalVar({ length: 30, name: "CutDeskWood" });
const cutChairWood = model.intervalVar({ length: 25, name: "CutChairWood" });
// [NEW] Sanding alternatives for desk
const sandDeskParts = model.intervalVar({ name: "SandDeskParts" });
const sandDeskHand = model.intervalVar({ length: 30, optional: true, name: "SandDeskHand" });
const sandDeskPower = model.intervalVar({ length: 15, optional: true, name: "SandDeskPower" });
model.alternative(sandDeskParts, [sandDeskHand, sandDeskPower]);
// [NEW] Sanding alternatives for chair
const sandChairParts = model.intervalVar({ name: "SandChairParts" });
const sandChairHand = model.intervalVar({ length: 25, optional: true, name: "SandChairHand" });
const sandChairPower = model.intervalVar({ length: 10, optional: true, name: "SandChairPower" });
model.alternative(sandChairParts, [sandChairHand, sandChairPower]);
// Rest of the tasks
const assembleDesk = model.intervalVar({ length: 45, name: "AssembleDesk" });
const assembleChair = model.intervalVar({ length: 35, name: "AssembleChair" });
const stainDesk = model.intervalVar({ length: 20, name: "StainDesk" });
const stainChair = model.intervalVar({ length: 15, name: "StainChair" });
const applyFinish = model.intervalVar({ length: 30, name: "ApplyFinish" });
const finalInspect = model.intervalVar({ length: 10, name: "FinalInspect" });
// Precedence constraints - Desk
cutDeskWood.endBeforeStart(sandDeskParts);
sandDeskParts.endBeforeStart(assembleDesk);
assembleDesk.endBeforeStart(stainDesk);
stainDesk.endBeforeStart(applyFinish);
// Precedence constraints - Chair
cutChairWood.endBeforeStart(sandChairParts);
sandChairParts.endBeforeStart(assembleChair);
assembleChair.endBeforeStart(stainChair);
stainChair.endBeforeStart(applyFinish);
// Final inspection
applyFinish.endBeforeStart(finalInspect);
// Resource constraint: one saw
model.noOverlap([cutDeskWood, cutChairWood]);
// [NEW] Resource constraint: power sander (only if both choose power sanding)
model.noOverlap([sandDeskPower, sandChairPower]);
// Resource constraint: 2 workers
const workerUsage = [
model.pulse(cutDeskWood, 1),
model.pulse(cutChairWood, 1),
model.pulse(sandDeskParts, 1),
model.pulse(sandChairParts, 1),
model.pulse(assembleDesk, 2),
model.pulse(assembleChair, 1),
model.pulse(stainDesk, 1),
model.pulse(stainChair, 1),
model.pulse(applyFinish, 1),
model.pulse(finalInspect, 1),
];
model.enforce(model.sum(workerUsage).le(2));
// Resource constraint: spray booth with transitions
const sprayBoothSeq = model.sequenceVar([stainDesk, stainChair, applyFinish]);
const transitions = [
[0, 10, 5],
[10, 0, 5],
[5, 5, 0],
];
model.noOverlap(sprayBoothSeq, transitions);
// Minimize makespan
model.minimize(finalInspect.end());
// Solve
const result = await model.solve();
if (result.solution) {
console.log(`Makespan: ${result.objective} minutes`);
console.log(`Optimal: ${result.proof}`);
// Check which sanding method was chosen
if (result.solution.isPresent(sandDeskHand)) {
console.log("Desk: hand sanding");
} else if (result.solution.isPresent(sandDeskPower)) {
console.log("Desk: power sanding");
}
if (result.solution.isPresent(sandChairHand)) {
console.log("Chair: hand sanding");
} else if (result.solution.isPresent(sandChairPower)) {
console.log("Chair: power sanding");
}
}
What Will the Solver Choose?
The solver will compare scenarios:
- Both hand sanding: Slower (30 + 25 = 55 minutes total), but can be done in parallel
- Both power sanding: Faster (15 + 10 = 25 minutes total), but must be sequential (shared resource)
- Mixed: One hand, one power—combines benefits and drawbacks
The optimal choice depends on the rest of the schedule. If there's parallelism to exploit, hand sanding might win. If the critical path benefits from speed, power sanding might be better. Building on our previous model (makespan: 200 minutes with transition times), adding alternatives gives the optimal makespan of 195 minutes—the alternatives provide flexibility that improves the schedule.
Absent Semantics
Optional intervals interact naturally with other constraints:
- No-overlap: Absent intervals don't participate (as if they don't exist)
- Cumulative: Pulses on absent intervals contribute 0
- Precedence: If an optional interval is absent, constraints involving it are ignored
- Expressions: Expressions involving absent intervals are also absent (unless guarded)
This makes modeling flexible: you don't need special cases for "what if this task isn't chosen."
Other Uses of Optional Intervals
With Alternatives
Alternative constraints work well for many scenarios:
- Machine selection: An operation can be performed on multiple machines (create one optional interval per machine)
- Method selection: A task can use different methods with different durations and resource requirements
- Alternative routes: A delivery can take route A or route B
Without Alternatives
Optional intervals are also useful without the alternative constraint:
- Oversubscribed systems: You have more requests than you can fulfill—make each request an optional interval and maximize the number of present intervals
- Soft constraints: A task is preferred but not required—make it optional and add a penalty to the objective if it's absent
Don't fake absence with dummy values. Don't model "not chosen" by length=0 or start=999999. This creates unnecessary complexity and hurts solver performance. Use optional=True instead—that's exactly what optional intervals are for.
Don't use alternatives for equivalent resources. If you have multiple identical workers or machines, use a cumulative constraint (as we did in the cumulative chapter), not alternatives. You can always assign tasks to specific workers in postprocessing. Use alternatives when the options have different characteristics (different durations, costs, or capabilities).
What We Learned
In this chapter, we:
- Created optional intervals to represent choices
- Used the alternative constraint to select exactly one option
- Modeled a shared resource (power sander) that's only used if certain options are chosen
- Checked which options were selected in the solution using
is_present()
Alternatives are useful for modeling real-world flexibility and trade-offs.
See Also
- Modeling / Alternative — Complete reference for alternative constraints
- Modeling / Intervals — Optional intervals and absent semantics
- Resources / No Overlap — How absent intervals interact with no-overlap constraints
Next Steps
In the final chapter, we'll model reservoir resources: cutting and sanding generate dust that accumulates over time, and the workshop must be cleaned before dust exceeds safety limits.
Continue to: Reservoir Constraints →