Step 1: Basic Course Timetabling¶
Overview¶
This is the first step in the High School Course Timetabling tutorial. It demonstrates how to build a basic timetabling optimization model using LumiX with 3D multi-model indexing.
The goal is to assign lectures (teacher-subject-class combinations) to timeslots and classrooms while respecting scheduling constraints.
What You’ll Learn:
Creating 3D indexed variables
Building complex scheduling constraints
Filtering infeasible combinations
Generating human-readable timetable outputs
Prerequisites:
pip install lumix ortools
Problem Description¶
A high school needs to create a weekly timetable for all classes. The schedule must:
Assign each lecture exactly once to a timeslot and classroom
Avoid classroom conflicts - only one lecture per classroom per timeslot
Avoid teacher conflicts - teachers can’t teach two lectures simultaneously
Avoid class conflicts - classes can’t attend two lectures at the same time
Respect classroom capacity - class size must fit in the assigned classroom
Problem Data¶
5 Teachers: Dr. Smith, Prof. Johnson, Ms. Williams, Mr. Brown, Dr. Davis
4 Classrooms: Room 101 (30), Room 102 (30), Room 201 (25), Lab A (20)
4 Classes: 9A (25 students), 9B (28 students), 10A (24 students), 10B (26 students)
5 Subjects: Mathematics, English, Physics, Chemistry, History
20 Lectures: Individual teaching sessions (e.g., “Dr. Smith teaches Math to Class 9A”)
30 Timeslots: 5 days × 6 periods per day (Monday-Friday, Periods 1-6)
Real-World Context¶
This type of problem appears in:
High school and university course scheduling
Training session planning
Conference scheduling
Resource allocation with time and space constraints
Employee shift scheduling with location assignments
Mathematical Formulation¶
Decision Variables¶
Where:
\(l \in \text{Lectures}\) - a specific lecture (teacher-subject-class combination)
\(t \in \text{TimeSlots}\) - a specific timeslot (day, period)
\(r \in \text{Classrooms}\) - a specific classroom
\(\text{assignment}[l, t, r] = 1\) if lecture \(l\) is assigned to timeslot \(t\) in classroom \(r\)
\(\text{assignment}[l, t, r] = 0\) otherwise
Objective Function¶
This is a feasibility problem - the goal is to find any valid schedule that satisfies all constraints. There is no optimization objective (e.g., minimizing costs or maximizing preferences).
Note
Steps 2-4 will introduce optimization objectives through goal programming.
Constraints¶
1. Lecture Coverage - Each lecture must be assigned exactly once:
2. Classroom Capacity - Class must fit in assigned classroom:
This is enforced through filtering in LumiX using where_multi().
3. No Classroom Conflicts - Maximum one lecture per classroom per timeslot:
4. No Teacher Conflicts - Teacher can’t teach multiple lectures simultaneously:
5. No Class Conflicts - Class can’t attend multiple lectures at the same time:
Key LumiX Concepts¶
1. Three-Dimensional Multi-Model Indexing¶
Variables indexed by tuples of three models:
assignment = (
LXVariable[Tuple[Lecture, TimeSlot, Classroom], int]("assignment")
.binary()
.indexed_by_product(
LXIndexDimension(Lecture, lambda lec: lec.id).from_data(LECTURES),
LXIndexDimension(TimeSlot, lambda ts: ts.id).from_data(TIMESLOTS),
LXIndexDimension(Classroom, lambda room: room.id).from_data(CLASSROOMS),
)
# Filter out invalid assignments (classroom too small for class)
.where_multi(
lambda lec, ts, room: check_class_fits_classroom(lec.class_id, room.id)
)
)
Traditional Approach (Other Libraries):
# Manual indexing - loses semantic meaning
assignment = {}
for i, lecture in enumerate(lectures):
for j, timeslot in enumerate(timeslots):
for k, classroom in enumerate(classrooms):
assignment[i, j, k] = model.add_var()
# Access: assignment[0, 5, 2]
# Which lecture? Which time? Which room? Context lost!
LumiX Approach - THE KEY FEATURE:
# Type-safe, semantic indexing
assignment = LXVariable[Tuple[Lecture, TimeSlot, Classroom], int]("assignment")
.binary()
.indexed_by_product(...)
# Access: solution.variables["assignment"][(lecture.id, timeslot.id, classroom.id)]
# IDE knows the structure! Type-safe! Full context preserved!
Benefits:
Type Safety: IDE autocomplete and type checking
Semantic Meaning: Variable names reflect business logic
Maintainability: Code reads like the problem description
Debugging: Clear what each index represents
2. Cartesian Product with Three Dimensions¶
.indexed_by_product() creates variables for every combination of Lecture × TimeSlot × Classroom:
.indexed_by_product(
LXIndexDimension(Lecture, lambda lec: lec.id).from_data(LECTURES),
LXIndexDimension(TimeSlot, lambda ts: ts.id).from_data(TIMESLOTS),
LXIndexDimension(Classroom, lambda room: room.id).from_data(CLASSROOMS)
)
# Creates: 20 lectures × 30 timeslots × 4 classrooms = 2,400 binary variables
3. Filtering with where_multi()¶
Filter out infeasible combinations based on relationships between models:
.where_multi(
lambda lec, ts, room: check_class_fits_classroom(lec.class_id, room.id)
)
Purpose: Only create variables where the class fits in the classroom. This:
Reduces problem size (fewer variables)
Enforces capacity constraints implicitly
Improves solver performance
4. Multi-Dimensional Summation¶
Sum over specific dimensions using filters:
Pattern 1: Sum over timeslots and classrooms for a specific lecture
expr = LXLinearExpression().add_multi_term(
assignment,
coeff=lambda lec, ts, room: 1.0,
where=lambda lec, ts, room, current_lecture=lecture: lec.id
== current_lecture.id,
)
Pattern 2: Sum over lectures for a specific timeslot and classroom
assignment,
coeff=lambda lec, ts, room: 1.0,
where=lambda lec, ts, room, current_ts=timeslot, current_room=classroom: ts.id
== current_ts.id
and room.id == current_room.id,
)
Important
Notice the closure capture: current_lecture=lecture captures the loop variable by value.
Without this, all constraints would reference the last lecture due to late binding.
Code Walkthrough¶
Step 1: Define Data Models¶
The sample data uses dataclasses for type-safe data representation:
@dataclass
class Teacher:
"""Represents a teacher in the school.
Attributes:
id: Unique identifier for the teacher.
name: Teacher's name.
Example:
>>> teacher = Teacher(id=1, name="Dr. Smith")
>>> print(teacher.name)
Dr. Smith
"""
id: int
name: str
@dataclass
class Lecture:
"""Represents a single lecture session.
A lecture is a specific teaching session: a teacher teaching a subject
to a class. If a subject meets multiple times per week, each meeting
is a separate lecture.
Attributes:
id: Unique identifier for the lecture.
subject_id: ID of the subject being taught.
teacher_id: ID of the teacher teaching the lecture.
class_id: ID of the class attending the lecture.
Example:
>>> lecture = Lecture(id=1, subject_id=1, teacher_id=1, class_id=1)
>>> # Represents: Teacher 1 teaching Subject 1 to Class 1
"""
id: int
subject_id: int
teacher_id: int
class_id: int
Step 2: Create 3D Decision Variable¶
# Decision variable: assignment[lecture, timeslot, classroom]
# Binary: 1 if lecture is assigned to timeslot in classroom, 0 otherwise
assignment = (
LXVariable[Tuple[Lecture, TimeSlot, Classroom], int]("assignment")
.binary()
.indexed_by_product(
LXIndexDimension(Lecture, lambda lec: lec.id).from_data(LECTURES),
LXIndexDimension(TimeSlot, lambda ts: ts.id).from_data(TIMESLOTS),
LXIndexDimension(Classroom, lambda room: room.id).from_data(CLASSROOMS),
)
# Filter out invalid assignments (classroom too small for class)
.where_multi(
lambda lec, ts, room: check_class_fits_classroom(lec.class_id, room.id)
)
)
# Create model
model = LXModel("high_school_timetabling")
model.add_variable(assignment)
Step 3: Add Lecture Coverage Constraints¶
print(" Adding lecture coverage constraints...")
for lecture in LECTURES:
expr = LXLinearExpression().add_multi_term(
assignment,
coeff=lambda lec, ts, room: 1.0,
where=lambda lec, ts, room, current_lecture=lecture: lec.id
== current_lecture.id,
)
model.add_constraint(
LXConstraint(f"lecture_{lecture.id}_assigned").expression(expr).eq().rhs(1)
)
Step 4: Add Classroom Conflict Constraints¶
# Constraint 2: No classroom conflicts (max one lecture per classroom per timeslot)
print(" Adding classroom conflict constraints...")
for timeslot in TIMESLOTS:
for classroom in CLASSROOMS:
expr = LXLinearExpression().add_multi_term(
assignment,
coeff=lambda lec, ts, room: 1.0,
where=lambda lec, ts, room, current_ts=timeslot, current_room=classroom: ts.id
== current_ts.id
and room.id == current_room.id,
)
model.add_constraint(
LXConstraint(f"room_{classroom.id}_slot_{timeslot.id}")
.expression(expr)
.le()
.rhs(1)
)
Step 5: Add Teacher Conflict Constraints¶
# Constraint 3: No teacher conflicts (teacher can't teach multiple lectures at same time)
print(" Adding teacher conflict constraints...")
for teacher in TEACHERS:
# Get all lectures taught by this teacher
teacher_lectures = [lec for lec in LECTURES if lec.teacher_id == teacher.id]
for timeslot in TIMESLOTS:
expr = LXLinearExpression().add_multi_term(
assignment,
coeff=lambda lec, ts, room: 1.0,
where=lambda lec, ts, room, current_ts=timeslot, t_lectures=teacher_lectures: ts.id
== current_ts.id
and lec.id in [tl.id for tl in t_lectures],
)
model.add_constraint(
LXConstraint(f"teacher_{teacher.id}_slot_{timeslot.id}")
.expression(expr)
.le()
.rhs(1)
)
Step 6: Add Class Conflict Constraints¶
# Constraint 4: No class conflicts (class can't attend multiple lectures at same time)
print(" Adding class conflict constraints...")
for school_class in CLASSES:
# Get all lectures for this class
class_lectures = [lec for lec in LECTURES if lec.class_id == school_class.id]
for timeslot in TIMESLOTS:
expr = LXLinearExpression().add_multi_term(
assignment,
coeff=lambda lec, ts, room: 1.0,
where=lambda lec, ts, room, current_ts=timeslot, c_lectures=class_lectures: ts.id
== current_ts.id
and lec.id in [cl.id for cl in c_lectures],
)
model.add_constraint(
LXConstraint(f"class_{school_class.id}_slot_{timeslot.id}")
.expression(expr)
.le()
.rhs(1)
)
Running the Example¶
Prerequisites¶
Install LumiX and OR-Tools:
pip install lumix
pip install ortools
Execute¶
cd tutorials/timetabling/step1_basic_timetabling
python timetabling.py
Expected Output¶
The program will display:
Model Building Information:
Number of variables created
Number of constraints added
Problem size summary
Solution Status:
Whether a feasible solution was found
Solution quality
Teacher Timetables (one for each teacher):
Weekly schedule showing subject, class, and classroom
Organized by day (columns) and period (rows)
Class Timetables (one for each class):
Weekly schedule showing subject, teacher, and classroom
Organized by day (columns) and period (rows)
Example Timetable Output¶
================================================================================
Timetable for Dr. Smith
================================================================================
Period Monday Tuesday Wednesday Thursday Friday
------------------------------------------------------------------------------------------------------------
1 Mathematics Mathematics Mathematics
9A 10A 9B
Room 101 Room 102 Room 101
2 Mathematics Mathematics
10B 9A
Room 102 Room 101
...
Files in This Example¶
sample_data.py: Data models (Teacher, Classroom, SchoolClass, Lecture, TimeSlot) and sample data
timetabling.py: Main optimization model, solver, and timetable display
README.md: Detailed documentation in the tutorial directory
Key Learnings¶
1. Multi-Dimensional Problem Representation¶
The 3D indexing naturally represents the timetabling decision:
What (Lecture) is scheduled
When (TimeSlot) it occurs
Where (Classroom) it takes place
Traditional approaches use nested loops and numerical indices, losing this semantic meaning.
2. Constraint Complexity¶
Timetabling involves multiple types of constraints that sum over different dimensions:
Lecture coverage: Sum over time and space for each lecture
Room conflicts: Sum over lectures for each time-space pair
Teacher conflicts: Sum over lectures (filtered by teacher) for each time
Class conflicts: Sum over lectures (filtered by class) for each time
LumiX’s add_multi_term() with where filters makes these constraints readable and maintainable.
3. Feasibility vs. Optimization¶
This is a constraint satisfaction problem (CSP). The goal is to find any valid schedule, not to optimize an objective function.
See also
Steps 2-4 will add optimization objectives using goal programming.
Common Patterns Demonstrated¶
Pattern 1: 3D Multi-Model Variable¶
decision = LXVariable[Tuple[ModelA, ModelB, ModelC], type]("name")
.indexed_by_product(
LXIndexDimension(ModelA, key_func).from_data(DATA_A),
LXIndexDimension(ModelB, key_func).from_data(DATA_B),
LXIndexDimension(ModelC, key_func).from_data(DATA_C)
)
Pattern 2: Filtering with Three Models¶
.where_multi(lambda a, b, c: is_valid_combination(a, b, c))
Pattern 3: Sum Over One Dimension¶
# For each A and B, sum over all C
for a in DATA_A:
for b in DATA_B:
expr = LXLinearExpression().add_multi_term(
decision,
coeff=lambda a_var, b_var, c_var: 1.0,
where=lambda a_var, b_var, c_var, current_a=a, current_b=b:
a_var.id == current_a.id and b_var.id == current_b.id
)
Pattern 4: Sum Over Two Dimensions¶
# For each A, sum over all B and C
for a in DATA_A:
expr = LXLinearExpression().add_multi_term(
decision,
coeff=lambda a_var, b_var, c_var: 1.0,
where=lambda a_var, b_var, c_var, current_a=a:
a_var.id == current_a.id
)
Extensions and Variations¶
This basic timetabling model can be extended with:
Soft Constraints: Teacher preferences, time preferences (addressed in Step 3)
Multiple Buildings: Add building as a 4th dimension
Resource Constraints: Labs, equipment, projectors
Consecutive Periods: Some lectures must be in consecutive timeslots
Balancing: Distribute lectures evenly across the week
Teacher Availability: Some teachers only available on certain days
Troubleshooting¶
No Feasible Solution Found¶
If the model returns infeasible:
Reduce lecture count: Too many lectures for available timeslots
Add classrooms: Not enough rooms to avoid conflicts
Check classroom capacities: Some classes may not fit in any room
Verify data consistency: Ensure all references (teacher_id, class_id, etc.) are valid
Slow Solving¶
If the model takes too long:
Use CP-SAT solver: Better for scheduling problems (
solver_to_use = "cpsat")Add symmetry breaking: Fix some lectures to specific times
Reduce problem size: Start with fewer lectures/classes/timeslots
Next Steps¶
After completing Step 1, proceed to:
Step 2 (Step 2: Database Integration) - Add SQLite database integration to store and retrieve schedules
Step 3 (Step 3: Goal Programming with Teacher Preferences) - Add teacher preferences using goal programming with priority levels
Step 4 (Step 4: Large-Scale Optimization with Room Types) - Scale to production-ready size with room type constraints
See Also¶
Related Examples:
Driver Scheduling Example - 2D multi-model indexing example
CP-SAT Assignment Example - Alternative solver for scheduling problems
Related User Guide:
Multi-Model Indexing - Multi-dimensional indexing documentation
Constraints Guide - Constraint formulation guide
Using Solvers - Solver selection guide
API Reference:
lumix.core.variables.LXVariable - Variable creation
lumix.core.constraints.LXConstraint - Constraint definition
lumix.core.model.LXModel - Model building
—
Tutorial Step 1 Complete!
You’ve learned how to build a basic timetabling model with 3D multi-model indexing. Now move on to Step 2: Database Integration to add database integration.