Building a Basic Model
Now that we understand the Furniture Workshop problem, let's build our first real scheduling model. We'll create interval variables for each task, enforce the precedence constraints, and minimize the makespan.
Interval Variables
In OptalCP, tasks are represented by interval variables. An interval has three properties:
- Start time: When the task begins
- End time: When the task finishes
- Length: How long the task takes (end - start)
Time in OptalCP is an integer. You decide what one time unit represents (seconds, minutes, hours) and what time 0 means (start of shift, midnight, project kickoff). In this tutorial, we'll use minutes.
Let's create interval variables for our workshop tasks:
- Python
- TypeScript
import optalcp as cp
model = cp.Model()
# Create interval variables for each task
cut_desk_wood = model.interval_var(length=30, name="CutDeskWood")
cut_chair_wood = model.interval_var(length=25, name="CutChairWood")
sand_desk_parts = model.interval_var(length=20, name="SandDeskParts")
sand_chair_parts = model.interval_var(length=15, name="SandChairParts")
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")
import * as CP from '@scheduleopt/optalcp';
const model = new CP.Model();
// Create interval variables for each task
const cutDeskWood = model.intervalVar({ length: 30, name: "CutDeskWood" });
const cutChairWood = model.intervalVar({ length: 25, name: "CutChairWood" });
const sandDeskParts = model.intervalVar({ length: 20, name: "SandDeskParts" });
const sandChairParts = model.intervalVar({ length: 15, name: "SandChairParts" });
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" });
Each interval has a fixed length (duration in minutes) and a descriptive name. The start and end times are decision variables—the solver will choose them to satisfy our constraints and optimize our objective.
Precedence Constraints
Tasks must happen in a specific order. For example, we can't sand the desk parts until we've cut the wood. These are called precedence constraints.
Here are the dependencies we need to model:
OptalCP has convenient functions to express precedence. The most common is end_before_start, which ensures one task finishes before another begins:
- Python
- TypeScript
# Desk production chain
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)
# Chair production chain
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 after finish is applied
apply_finish.end_before_start(final_inspect)
// Desk production chain
cutDeskWood.endBeforeStart(sandDeskParts);
sandDeskParts.endBeforeStart(assembleDesk);
assembleDesk.endBeforeStart(stainDesk);
stainDesk.endBeforeStart(applyFinish);
// Chair production chain
cutChairWood.endBeforeStart(sandChairParts);
sandChairParts.endBeforeStart(assembleChair);
assembleChair.endBeforeStart(stainChair);
stainChair.endBeforeStart(applyFinish);
// Final inspection after finish is applied
applyFinish.endBeforeStart(finalInspect);
Three Styles for Precedence
You can express precedence in three equivalent ways:
- Python
- TypeScript
# On the interval variable
task1.end_before_start(task2)
# On the model
model.end_before_start(task1, task2)
# Boolean expression with enforce
model.enforce(task1.end() <= task2.start())
// On the interval variable
task1.endBeforeStart(task2);
// On the model
model.endBeforeStart(task1, task2);
// Boolean expression with enforce
model.enforce(task1.end().le(task2.start()));
All three are treated exactly the same by the solver—choose whichever you find more readable.
OptalCP has 8 precedence functions with an optional delay parameter. See Modeling / Constraints for the full list.
Minimizing Makespan
Our goal is to finish the entire order as quickly as possible. This means minimizing the makespan: the time when the last task finishes.
The last task is final_inspect, so we minimize its end time:
- Python
- TypeScript
model.minimize(final_inspect.end())
model.minimize(finalInspect.end());
The end() function returns an integer expression representing the task's end time. By minimizing it, we tell the solver to find the schedule that completes everything earliest.
Solving the Model
Now we're ready to solve:
- Python
- TypeScript
result = model.solve()
if result.solution:
print(f"Makespan: {result.objective} minutes")
print(f"Optimal: {result.proof}")
print()
# Print the schedule for each task
for task in [cut_desk_wood, cut_chair_wood, sand_desk_parts,
sand_chair_parts, assemble_desk, assemble_chair,
stain_desk, stain_chair, apply_finish, final_inspect]:
start = result.solution.get_start(task)
end = result.solution.get_end(task)
print(f"{task.name}: {start} - {end}")
else:
print("No solution found")
const result = await model.solve();
if (result.solution) {
console.log(`Makespan: ${result.objective} minutes`);
console.log(`Optimal: ${result.proof}`);
console.log();
// Print the schedule for each task
const tasks = [cutDeskWood, cutChairWood, sandDeskParts,
sandChairParts, assembleDesk, assembleChair,
stainDesk, stainChair, applyFinish, finalInspect];
for (const task of tasks) {
const start = result.solution.getStart(task);
const end = result.solution.getEnd(task);
console.log(`${task.name}: ${start} - ${end}`);
}
} else {
console.log("No solution found");
}
Understanding the Output
The solver returns a SolveResult object with:
solution: The best schedule found (orNone/nullif none exists)objective: The objective value (makespan in this case)proof: Whether optimality is proven (Trueif optimal,Falseif solution is feasible but might not be optimal)
When you run the code, the solver automatically prints a log showing its progress. After the solver finishes, our print statements output the schedule. Here's the complete output:
──────────────────────────────────────────────────────────────────────────────── ScheduleOpt OptalCP Version 2025.12.1 (Linux) CPU: AMD Ryzen 9 5950X (16 physical cores) ──────────────────────────────────────────────────────────────────────────────── Input parse time: 00:00 nbWorkers = 16 (auto: 16 physical cores) preset = Default (auto: < 100,000 variables) noOverlapPropagationLevel = 4 (preset: Default) cumulPropagationLevel = 3 (preset: Default) Workers 0-7: searchType = LNS (preset: Default) Workers 8-13: searchType = FDS (preset: Default) Workers 14-15: searchType = FDSDual (preset: Default) Input: 0 integer variables, 10 interval variables, 9 constraints, 11.2kB 00:00 Presolving.. Presolved: 0 integer variables, 10 interval variables, 9 constraints, 11kB 00:00 Presolve complete, starting search. ──────────────────────────────────────────────────────────────────────────────── 00:00 ↑ Lower bound 155 Worker 5 00:00 ↓ Solution 155 [Verified] Worker 5: LNS 00:00 The current best solution is optimal. ──────────────────────────────────────────────────────────────────────────────── Objective value: 155 (optimal) Lower bound: 155 Solutions: 1 LNS steps: 11 (1026.08 per second) Restarts: 0 (0.00 per second) Branches: 93 (8675.05 per second) Fails: 9 (839.52 per second) Total duration: 00:00.01 Memory: 102MB ──────────────────────────────────────────────────────────────────────────────── Makespan: 155 minutes Optimal: true CutDeskWood: 0 - 30 CutChairWood: 0 - 25 SandDeskParts: 30 - 50 SandChairParts: 25 - 40 AssembleDesk: 50 - 95 AssembleChair: 40 - 75 StainDesk: 95 - 115 StainChair: 75 - 90 ApplyFinish: 115 - 145 FinalInspect: 145 - 155
Understanding the Solver Log
The log has three main parts:
Parameters — At the top, the solver shows which parameters it's using. The right column explains why each parameter has its value:
auto— auto-detected from your system (e.g.,nbWorkersfrom CPU cores)preset— derived from the selected preset (Defaultfor typical problems)- No reason shown — explicitly set by the user
Key parameters include:
nbWorkers: parallel CPU threadspreset: configuration bundle (automatically chosen based on problem size)searchType: which search strategy each worker uses—the solver runs multiple strategies in parallel (LNS, FDS, FDSDual)- Propagation levels: how aggressively constraints are processed
Search progress — During solving, you'll see:
↑ Lower bound 155— proves no solution can be better than 155↓ Solution 155— found a solution with makespan 155- When lower bound equals the solution, optimality is proven
Summary — Statistics about the search: objective value, solution count, branches, and duration.
Interpreting the Schedule
Both cutting tasks start at time 0—this schedule is intentionally unrealistic because we haven't added resource constraints yet. Without a no-overlap constraint on the saw, the solver assumes both cutting tasks can run simultaneously. The desk chain is longer, forming the critical path that determines makespan. We'll add resource constraints in the following chapters to make the model realistic.
If you're using the Preview edition, you will see the correct objective value (155), but solution values (start/end times) will be None/null. See Editions for details.
Complete Code
Here's the complete 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")
sand_desk_parts = model.interval_var(length=20, name="SandDeskParts")
sand_chair_parts = model.interval_var(length=15, name="SandChairParts")
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)
# 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}")
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" });
const sandDeskParts = model.intervalVar({ length: 20, name: "SandDeskParts" });
const sandChairParts = model.intervalVar({ length: 15, name: "SandChairParts" });
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);
// 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}`);
}
What We Learned
In this chapter, we:
- Created interval variables to represent tasks with durations
- Used precedence constraints to enforce task ordering
- Set an objective to minimize makespan
- Solved the model and interpreted the results
Our model captures the logical dependencies, but it's still unrealistic: it assumes we have unlimited resources. In reality, we have one saw, limited workers, and one spray booth.
See Also
- Modeling / Intervals — Complete reference for interval variables
- Modeling / Constraints — All constraint types and precedence functions
- Modeling / Objectives — Optimization objectives
Next Steps
In the next chapter, we'll add our first resource constraint: the saw can only cut one piece of wood at a time.
Continue to: No-Overlap Constraints →