Step 3: Goal Programming with Teacher Preferences¶
Overview¶
This step extends Step 2 by adding teacher preferences as soft constraints using LumiX’s goal programming feature. Teachers can express preferences, and these are converted to goals with priorities based on teacher seniority.
What’s New in Step 3:
Teacher preferences (DAY_OFF, SPECIFIC_TIME)
Priority-based scheduling using teacher seniority
Goal programming with weighted objectives
Preference satisfaction analysis
Prerequisites:
pip install lumix gurobi sqlalchemy # Gurobi recommended for goal programming
Problem Description¶
Same as Steps 1 & 2, but now with:
Hard constraints: Basic timetabling rules (must be satisfied)
Soft constraints (goals): Teacher preferences (minimize violations)
Priority levels: Senior teachers (Priority 1) > Mid-level (Priority 2) > Junior (Priority 3)
Mathematical Formulation¶
Hard Constraints (Same as Steps 1 & 2)¶
Each lecture assigned exactly once
No classroom/teacher/class conflicts
Classroom capacity constraints
Soft Constraints (Goals)¶
DAY_OFF Preference: Minimize work on preferred day off
SPECIFIC_TIME Preference: Assign lecture to preferred timeslot
Priorities¶
Based on years of service (work_years):
Priority 1: Teachers with 15+ years (highest priority)
Priority 2: Teachers with 7-14 years (medium priority)
Priority 3: Teachers with 0-6 years (lower priority)
Key Features Demonstrated¶
1. Goal Programming¶
Converting preferences to soft constraints:
# Hard constraint (must satisfy)
model.add_constraint(
LXConstraint("lecture_coverage")
.expression(expr)
.eq()
.rhs(1)
)
# Soft goal (minimize violation with priority)
model.add_constraint(
LXConstraint("teacher_preference")
.expression(expr)
.le()
.rhs(0)
.as_goal(priority=1, weight=1.0) # Priority 1 = highest
)
# Prepare goal programming
model.set_goal_mode("weighted")
model.prepare_goal_programming()
2. Priority-Based Scheduling¶
def calculate_priority_from_work_years(work_years: int) -> int:
"""Calculate goal programming priority from teacher work years.
Priority scheme:
- 15+ years → Priority 1 (highest)
- 7-14 years → Priority 2
- 0-6 years → Priority 3 (lowest)
Args:
work_years: Number of years the teacher has worked.
Returns:
Priority level (1, 2, or 3).
Example:
>>> priority = calculate_priority_from_work_years(15)
>>> print(priority) # 1 (senior teacher)
"""
if work_years >= 15:
return 1 # Senior teachers
elif work_years >= 7:
return 2 # Mid-level teachers
else:
return 3 # Junior teachers
3. Preference Types¶
DAY_OFF: Teacher wants a specific day completely free
if not teacher:
continue
# Calculate priority from teacher's work years
priority = calculate_priority_from_work_years(teacher.work_years)
teacher_name = teacher.name
if pref.preference_type == "DAY_OFF":
# Goal: Minimize assignments on the preferred day off
# Get all timeslots for the specified day using ORM filtering
day_timeslot_ids = [
ts.id for ts in session.query(TimeSlot).filter_by(day_of_week=pref.day_of_week).all()
]
# Get all lectures taught by this teacher using ORM filtering
teacher_lecture_ids = [
lec.id for lec in session.query(Lecture).filter_by(teacher_id=teacher.id).all()
]
# Sum of all assignments on that day for this teacher
expr = LXLinearExpression().add_multi_term(
assignment,
coeff=lambda lec, ts, room: 1.0,
where=lambda lec, ts, room, t_lec_ids=teacher_lecture_ids, d_slot_ids=day_timeslot_ids: lec.id
in t_lec_ids
and ts.id in d_slot_ids,
)
# Goal: minimize this sum (ideally 0 = complete day off)
day_timeslot = session.query(TimeSlot).filter_by(day_of_week=pref.day_of_week).first()
day_name = day_timeslot.day_name if day_timeslot else "Unknown"
goal_name = f"day_off_teacher_{teacher.id}_{day_name}"
model.add_constraint(
SPECIFIC_TIME: Teacher wants to teach a specific lecture at a specific time
.expression(expr)
.le()
.rhs(0) # Target: 0 assignments on this day
.as_goal(priority=priority, weight=1.0)
)
print(
f" [P{priority}] {teacher_name}: wants {day_name} off (goal: 0 lectures)"
)
elif pref.preference_type == "SPECIFIC_TIME":
# Goal: Assign specific lecture to specific timeslot
# Expression: sum of assignments for this lecture at this timeslot (across all classrooms)
expr = LXLinearExpression().add_multi_term(
assignment,
coeff=lambda lec, ts, room: 1.0,
where=lambda lec, ts, room, target_lec=pref.lecture_id, target_ts=pref.timeslot_id: lec.id
== target_lec
and ts.id == target_ts,
)
# Goal: this sum should equal 1 (lecture is assigned to that timeslot)
goal_name = f"specific_time_teacher_{teacher.id}_lecture_{pref.lecture_id}"
model.add_constraint(
LXConstraint(goal_name)
.expression(expr)
.eq()
.rhs(1) # Target: exactly 1 (assigned to this timeslot)
.as_goal(priority=priority, weight=1.0)
Database Schema Extensions¶
New Tables¶
teacher_preferences table:
id: Primary keyteacher_id: Foreign key to teacherspreference_type: ‘DAY_OFF’ or ‘SPECIFIC_TIME’day_of_week: Preferred day (for DAY_OFF)lecture_id: Specific lecture (for SPECIFIC_TIME)timeslot_id: Specific timeslot (for SPECIFIC_TIME)description: Human-readable description
Updated Models¶
Teacher model now includes work_years:
class Teacher(Base):
"""Teacher ORM model with seniority information.
Attributes:
id: Primary key
name: Teacher's name
work_years: Years of service (used for priority calculation)
"""
__tablename__ = 'teachers'
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
work_years = Column(Integer, nullable=False, default=0)
def __repr__(self):
return f"<Teacher(id={self.id}, name='{self.name}', work_years={self.work_years})>"
Running the Example¶
Step 1: Populate Database with Preferences¶
cd tutorials/timetabling/step3_goal_programming
python sample_data.py
Step 2: Run Goal Programming¶
python timetabling_goals.py
Expected Output¶
Data loading with preference counts
Model building with goal constraints
Solution status
Timetables for teachers and classes
Preference satisfaction analysis: - Per-teacher satisfaction rates - Priority-level satisfaction rates - Overall satisfaction percentage
Code Walkthrough¶
1. Load Preferences from Database¶
Args:
session: SQLAlchemy Session instance.
Returns:
An LXModel instance with both hard constraints and soft goals.
"""
print("\nBuilding course timetabling model with goal programming...")
print(" Using LumiX's from_model() for direct database querying")
# Create cached checker for efficient lookups (avoids redundant DB queries)
fits_checker = create_cached_class_fits_checker(session)
# Decision variable: assignment[lecture, timeslot, classroom]
# LumiX queries the database directly using from_model()
2. Set Goal Programming Mode¶
.as_goal(priority=priority, weight=1.0)
)
3. Add Goal Constraints¶
For each preference, create a goal constraint with calculated priority.
4. Analyze Goal Satisfaction¶
print("GOAL SATISFACTION ANALYSIS")
print(f"{'=' * 80}")
# Group preferences by priority
priority_groups = {1: [], 2: [], 3: []}
# Query preferences directly from database (no pre-loading into list)
for pref in session.query(TeacherPreference).all():
# Query teacher using ORM
teacher = session.query(Teacher).filter_by(id=pref.teacher_id).first()
if not teacher:
continue
priority = calculate_priority_from_work_years(teacher.work_years)
if pref.preference_type == "DAY_OFF":
# Query timeslot for day name using ORM
day_timeslot = session.query(TimeSlot).filter_by(day_of_week=pref.day_of_week).first()
day_name = day_timeslot.day_name if day_timeslot else "Unknown"
goal_name = f"day_off_teacher_{teacher.id}_{day_name}"
else: # SPECIFIC_TIME
goal_name = f"specific_time_teacher_{teacher.id}_lecture_{pref.lecture_id}"
# Check goal satisfaction
try:
deviations = solution.get_goal_deviations(goal_name)
satisfied = solution.is_goal_satisfied(goal_name, tolerance=1e-6)
priority_groups[priority].append(
{
"teacher": teacher,
"preference": pref,
"goal_name": goal_name,
"satisfied": satisfied,
"deviations": deviations,
}
)
except Exception as e:
print(f" Warning: Could not analyze goal {goal_name}: {e}")
# Display by priority
for priority in [1, 2, 3]:
priority_name = (
"Senior (15+ years)"
if priority == 1
else "Mid-level (7-14 years)" if priority == 2 else "Junior (0-6 years)"
)
print(f"\nPriority {priority} ({priority_name}):")
print("-" * 80)
group = priority_groups[priority]
if not group:
print(" (No preferences at this priority level)")
continue
satisfied_count = sum(1 for g in group if g["satisfied"])
total_count = len(group)
Key Learnings¶
Goal Programming Concepts¶
Hard vs. Soft Constraints:
Hard: Must be satisfied (infeasible if violated)
Soft (Goals): Minimize violations weighted by priority
Priority Levels:
Higher priority goals are satisfied first, even if lower priority goals must be violated.
Weighted vs. Sequential:
Weighted: Single optimization with priority weights
Sequential: Multiple optimizations, one per priority level
Performance Considerations¶
Goal programming increases solve time:
More variables (deviation variables added automatically)
More constraints (one per goal)
Use commercial solvers (Gurobi, CPLEX) for better performance
Typical Results¶
With 7 preferences across 3 priority levels:
Priority 1 (Senior): 80-100% satisfaction
Priority 2 (Mid-level): 60-80% satisfaction
Priority 3 (Junior): 40-60% satisfaction
Overall: 60-80% satisfaction
Next Steps¶
After completing Step 3, proceed to:
Step 4 (Step 4: Large-Scale Optimization with Room Types) - Scale to production-ready size with room types
See Also¶
Related Examples:
Goal Programming Example - Goal programming basics
Related User Guide:
Goal Programming - Comprehensive goal programming guide
Weighted Goal Programming - Weighted goal programming
Sequential Goal Programming - Sequential goal programming
API Reference:
lumix.core.model.LXModel - Goal programming methods
—
Tutorial Step 3 Complete!
You’ve learned how to use goal programming for multi-objective optimization with teacher preferences. Now move on to Step 4: Large-Scale Optimization with Room Types to see how LumiX scales to production-ready problems.