Merge branch 'main' of github.com:google/or-tools
This commit is contained in:
444
examples/contrib/school_scheduling_sat.py
Normal file → Executable file
444
examples/contrib/school_scheduling_sat.py
Normal file → Executable file
@@ -1,174 +1,330 @@
|
||||
#!/usr/bin/env python3
|
||||
'''Solve a School Scheduling Problem'''
|
||||
from ortools.sat.python import cp_model
|
||||
|
||||
|
||||
class SchoolSchedulingProblem(object):
|
||||
class SchoolSchedulingProblem():
|
||||
'''Data of the problem.'''
|
||||
|
||||
def __init__(self, subjects, teachers, curriculum, specialties, working_days,
|
||||
periods, levels, sections, teacher_work_hours):
|
||||
self.subjects = subjects
|
||||
self.teachers = teachers
|
||||
self.curriculum = curriculum
|
||||
self.specialties = specialties
|
||||
self.working_days = working_days
|
||||
self.periods = periods
|
||||
self.levels = levels
|
||||
self.sections = sections
|
||||
self.teacher_work_hours = teacher_work_hours
|
||||
def __init__(self, levels, sections, subjects, curriculum, teachers,
|
||||
specialties, time_slots):
|
||||
self._levels = levels
|
||||
self._sections = sections
|
||||
self._subjects = subjects
|
||||
self._curriculum = curriculum
|
||||
assert len(self._curriculum) == len(self._levels) * len(
|
||||
self._subjects), 'Some curriculum are missing'
|
||||
for (lvl, sub) in self._curriculum.keys():
|
||||
assert lvl in self._levels, f'{lvl} not in LEVELS'
|
||||
assert sub in self._subjects, f'{sub} not in SUBJECTS'
|
||||
|
||||
self._teachers = teachers
|
||||
self._specialties = specialties
|
||||
assert len(self._specialties) == len(
|
||||
self._subjects), 'Missing some rows'
|
||||
for s, ts in self._specialties.items():
|
||||
assert s in self._subjects, f'{s} is not in SUBJECTS'
|
||||
for t in ts:
|
||||
assert t in self._teachers, f'{t} is not in TEACHERS'
|
||||
|
||||
self._time_slots = time_slots
|
||||
|
||||
@property
|
||||
def levels(self):
|
||||
return self._levels
|
||||
|
||||
@property
|
||||
def sections(self):
|
||||
return self._sections
|
||||
|
||||
@property
|
||||
def subjects(self):
|
||||
return self._subjects
|
||||
|
||||
@property
|
||||
def curriculum(self):
|
||||
return self._curriculum
|
||||
|
||||
@property
|
||||
def teachers(self):
|
||||
return self._teachers
|
||||
|
||||
def teacher_name(self, teacher_idx):
|
||||
assert 0 <= teacher_idx < len(self._teachers)
|
||||
return list(self._teachers.keys())[teacher_idx]
|
||||
|
||||
def teacher_max_hours(self, teacher_idx):
|
||||
assert 0 <= teacher_idx < len(self._teachers)
|
||||
return list(self._teachers.values())[teacher_idx]
|
||||
|
||||
@property
|
||||
def specialties(self):
|
||||
return self._specialties
|
||||
|
||||
def specialtie_teachers(self, subject):
|
||||
assert subject in self._subjects, f'{subject} not in SUBJECTS'
|
||||
return self._specialties[subject]
|
||||
|
||||
@property
|
||||
def time_slots(self):
|
||||
return self._time_slots
|
||||
|
||||
def slot_duration(self, slot_idx):
|
||||
assert 0 <= slot_idx < len(self._time_slots)
|
||||
return list(self._time_slots.values())[slot_idx]
|
||||
|
||||
|
||||
class SchoolSchedulingSatSolver(object):
|
||||
class SchoolSchedulingSatSolver():
|
||||
'''Solver instance.'''
|
||||
|
||||
def __init__(self, problem):
|
||||
# Problem
|
||||
self.problem = problem
|
||||
def __init__(self, problem: SchoolSchedulingProblem):
|
||||
# Problem
|
||||
self._problem = problem
|
||||
|
||||
# Utilities
|
||||
self.timeslots = [
|
||||
'{0:10} {1:6}'.format(x, y)
|
||||
for x in problem.working_days
|
||||
for y in problem.periods
|
||||
]
|
||||
self.num_days = len(problem.working_days)
|
||||
self.num_periods = len(problem.periods)
|
||||
self.num_slots = len(self.timeslots)
|
||||
self.num_teachers = len(problem.teachers)
|
||||
self.num_subjects = len(problem.subjects)
|
||||
self.num_levels = len(problem.levels)
|
||||
self.num_sections = len(problem.sections)
|
||||
self.courses = [
|
||||
x * self.num_levels + y
|
||||
for x in problem.levels
|
||||
for y in problem.sections
|
||||
]
|
||||
self.num_courses = self.num_levels * self.num_sections
|
||||
# Utilities
|
||||
num_levels = len(self._problem.levels)
|
||||
self._all_levels = range(num_levels)
|
||||
num_sections = len(self._problem.sections)
|
||||
self._all_sections = range(num_sections)
|
||||
num_subjects = len(self._problem.subjects)
|
||||
self._all_subjects = range(num_subjects)
|
||||
num_teachers = len(self._problem.teachers)
|
||||
self._all_teachers = range(num_teachers)
|
||||
num_slots = len(self._problem.time_slots)
|
||||
self._all_slots = range(num_slots)
|
||||
|
||||
all_courses = range(self.num_courses)
|
||||
all_teachers = range(self.num_teachers)
|
||||
all_slots = range(self.num_slots)
|
||||
all_sections = range(self.num_sections)
|
||||
all_subjects = range(self.num_subjects)
|
||||
all_levels = range(self.num_levels)
|
||||
# Create Model
|
||||
self._model = cp_model.CpModel()
|
||||
|
||||
self.model = cp_model.CpModel()
|
||||
# Create Variables
|
||||
self._assignment = {}
|
||||
for lvl_idx, level in enumerate(self._problem.levels):
|
||||
for sec_idx, section in enumerate(self._problem.sections):
|
||||
for sub_idx, subject in enumerate(self._problem.subjects):
|
||||
for tch_idx, teacher in enumerate(self._problem.teachers):
|
||||
for slt_idx, slot in enumerate(self._problem.time_slots):
|
||||
key = (lvl_idx, sec_idx, sub_idx, tch_idx, slt_idx)
|
||||
name = f'{level}-{section} S:{subject} T:{teacher} Slot:{slot}'
|
||||
#print(name)
|
||||
if teacher in self._problem.specialtie_teachers(subject):
|
||||
self._assignment[key] = self._model.NewBoolVar(name)
|
||||
else:
|
||||
name = 'NO DISP ' + name
|
||||
self._assignment[key] = self._model.NewIntVar(0, 0, name)
|
||||
|
||||
self.assignment = {}
|
||||
for c in all_courses:
|
||||
for s in all_subjects:
|
||||
for t in all_teachers:
|
||||
for slot in all_slots:
|
||||
if t in self.problem.specialties[s]:
|
||||
name = 'C:{%i} S:{%i} T:{%i} Slot:{%i}' % (c, s, t, slot)
|
||||
self.assignment[c, s, t, slot] = self.model.NewBoolVar(name)
|
||||
else:
|
||||
name = 'NO DISP C:{%i} S:{%i} T:{%i} Slot:{%i}' % (c, s, t, slot)
|
||||
self.assignment[c, s, t, slot] = self.model.NewIntVar(0, 0, name)
|
||||
# Constraints
|
||||
# Each Level-Section must have the quantity of classes per Subject specified in the Curriculum
|
||||
for lvl_idx, level in enumerate(self._problem.levels):
|
||||
for sec_idx in self._all_sections:
|
||||
for sub_idx, subject in enumerate(self._problem.subjects):
|
||||
required_duration = self._problem.curriculum[level, subject]
|
||||
#print(f'L:{level} S:{subject} duration:{required_duration}h')
|
||||
self._model.Add(
|
||||
sum(self._assignment[lvl_idx, sec_idx, sub_idx, tch_idx, slt_idx] *
|
||||
int(self._problem.slot_duration(slt_idx) * 10)
|
||||
for tch_idx in self._all_teachers
|
||||
for slt_idx in self._all_slots) == int(required_duration * 10))
|
||||
|
||||
# Constraints
|
||||
# Each Level-Section can do at most one class at a time
|
||||
for lvl_idx in self._all_levels:
|
||||
for sec_idx in self._all_sections:
|
||||
for slt_idx in self._all_slots:
|
||||
self._model.Add(
|
||||
sum([
|
||||
self._assignment[lvl_idx, sec_idx, sub_idx, tch_idx, slt_idx]
|
||||
for sub_idx in self._all_subjects
|
||||
for tch_idx in self._all_teachers
|
||||
]) <= 1)
|
||||
|
||||
# Each course must have the quantity of classes specified in the curriculum
|
||||
for level in all_levels:
|
||||
for section in all_sections:
|
||||
course = level * self.num_sections + section
|
||||
for subject in all_subjects:
|
||||
required_slots = self.problem.curriculum[
|
||||
self.problem.levels[level], self.problem.subjects[subject]]
|
||||
self.model.Add(
|
||||
sum(self.assignment[course, subject, teacher, slot]
|
||||
for slot in all_slots
|
||||
for teacher in all_teachers) == required_slots)
|
||||
# Teacher can do at most one class at a time
|
||||
for tch_idx in self._all_teachers:
|
||||
for slt_idx in self._all_slots:
|
||||
self._model.Add(
|
||||
sum([
|
||||
self._assignment[lvl_idx, sec_idx, sub_idx, tch_idx, slt_idx]
|
||||
for lvl_idx in self._all_levels
|
||||
for sec_idx in self._all_sections
|
||||
for sub_idx in self._all_subjects
|
||||
]) <= 1)
|
||||
|
||||
# Teacher can do at most one class at a time
|
||||
for teacher in all_teachers:
|
||||
for slot in all_slots:
|
||||
self.model.Add(
|
||||
sum([
|
||||
self.assignment[c, s, teacher, slot]
|
||||
for c in all_courses
|
||||
for s in all_subjects
|
||||
]) <= 1)
|
||||
# Maximum work hours for each teacher
|
||||
for tch_idx in self._all_teachers:
|
||||
self._model.Add(
|
||||
sum([
|
||||
self._assignment[lvl_idx, sec_idx, sub_idx, tch_idx, slt_idx] *
|
||||
int(self._problem.slot_duration(slt_idx) * 10)
|
||||
for lvl_idx in self._all_levels
|
||||
for sec_idx in self._all_sections
|
||||
for sub_idx in self._all_subjects
|
||||
for slt_idx in self._all_slots
|
||||
]) <= int(self._problem.teacher_max_hours(tch_idx) * 10))
|
||||
|
||||
# Maximum work hours for each teacher
|
||||
for teacher in all_teachers:
|
||||
self.model.Add(
|
||||
sum([
|
||||
self.assignment[c, s, teacher, slot] for c in all_courses
|
||||
for s in all_subjects for slot in all_slots
|
||||
]) <= self.problem.teacher_work_hours[teacher])
|
||||
# Teacher makes all the classes of a subject's course
|
||||
teacher_courses = {}
|
||||
for lvl_idx, level in enumerate(self._problem.levels):
|
||||
for sec_idx, section in enumerate(self._problem.sections):
|
||||
for sub_idx, subject in enumerate(self._problem.subjects):
|
||||
for tch_idx, teacher in enumerate(self._problem.teachers):
|
||||
name = f'{level}-{section} S:{subject} T:{teacher}'
|
||||
teacher_courses[lvl_idx, sec_idx, sub_idx, tch_idx] = self._model.NewBoolVar(name)
|
||||
temp_array = [
|
||||
self._assignment[lvl_idx, sec_idx, sub_idx, tch_idx, slt_idx]
|
||||
for slt_idx in self._all_slots
|
||||
]
|
||||
self._model.AddMaxEquality(
|
||||
teacher_courses[lvl_idx, sec_idx, sub_idx, tch_idx], temp_array)
|
||||
self._model.Add(
|
||||
sum([teacher_courses[lvl_idx, sec_idx, sub_idx, tch_idx]
|
||||
for tch_idx in self._all_teachers
|
||||
]) == 1)
|
||||
|
||||
# Teacher makes all the classes of a subject's course
|
||||
teacher_courses = {}
|
||||
for level in all_levels:
|
||||
for section in all_sections:
|
||||
course = level * self.num_sections + section
|
||||
for subject in all_subjects:
|
||||
for t in all_teachers:
|
||||
name = 'C:{%i} S:{%i} T:{%i}' % (course, subject, teacher)
|
||||
teacher_courses[course, subject, t] = self.model.NewBoolVar(name)
|
||||
temp_array = [
|
||||
self.assignment[course, subject, t, slot] for slot in all_slots
|
||||
]
|
||||
self.model.AddMaxEquality(teacher_courses[course, subject, t],
|
||||
temp_array)
|
||||
self.model.Add(
|
||||
sum(teacher_courses[course, subject, t]
|
||||
for t in all_teachers) == 1)
|
||||
|
||||
def solve(self):
|
||||
print('Solving')
|
||||
solver = cp_model.CpSolver()
|
||||
solution_printer = SchoolSchedulingSatSolutionPrinter()
|
||||
status = solver.Solve(self.model, solution_printer)
|
||||
print()
|
||||
print('status', status)
|
||||
print('Branches', solver.NumBranches())
|
||||
print('Conflicts', solver.NumConflicts())
|
||||
print('WallTime', solver.WallTime())
|
||||
def print_teacher_schedule(self, tch_idx):
|
||||
teacher_name = self._problem.teacher_name(tch_idx)
|
||||
print(f'Teacher: {teacher_name}')
|
||||
total_working_hours = 0
|
||||
for slt_idx, slot in enumerate(self._problem.time_slots):
|
||||
for lvl_idx, level in enumerate(self._problem.levels):
|
||||
for sec_idx, section in enumerate(self._problem.sections):
|
||||
for sub_idx, subject in enumerate(self._problem.subjects):
|
||||
key = (lvl_idx, sec_idx, sub_idx, tch_idx, slt_idx)
|
||||
if self._solver.BooleanValue(self._assignment[key]):
|
||||
total_working_hours += self._problem.slot_duration(slt_idx)
|
||||
print(f'{slot}: C:{level}-{section} S:{subject}')
|
||||
print(f'Total working hours: {total_working_hours}h')
|
||||
|
||||
|
||||
def print_class_schedule(self, lvl_idx, sec_idx):
|
||||
level = self._problem.levels[lvl_idx]
|
||||
section = self._problem.sections[sec_idx]
|
||||
print(f'Class: {level}-{section}')
|
||||
total_working_hours = {}
|
||||
for sub in self._problem.subjects:
|
||||
total_working_hours[sub] = 0
|
||||
for slt_idx, slot in enumerate(self._problem.time_slots):
|
||||
for tch_idx, teacher in enumerate(self._problem.teachers):
|
||||
for sub_idx, subject in enumerate(self._problem.subjects):
|
||||
key = (lvl_idx, sec_idx, sub_idx, tch_idx, slt_idx)
|
||||
if self._solver.BooleanValue(self._assignment[key]):
|
||||
total_working_hours[subject] += self._problem.slot_duration(slt_idx)
|
||||
print(f'{slot}: S:{subject} T:{teacher}')
|
||||
for (subject, hours) in total_working_hours.items():
|
||||
print(f'Total working hours for {subject}: {hours}h')
|
||||
|
||||
|
||||
def solve(self):
|
||||
print('Solving')
|
||||
# Create Solver
|
||||
self._solver = cp_model.CpSolver()
|
||||
|
||||
solution_printer = SchoolSchedulingSatSolutionPrinter()
|
||||
status = self._solver.Solve(self._model, solution_printer)
|
||||
print('Status: ', self._solver.StatusName(status))
|
||||
|
||||
if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
|
||||
print('\n# Teachers')
|
||||
for teacher_idx in self._all_teachers:
|
||||
self.print_teacher_schedule(teacher_idx)
|
||||
|
||||
print('\n# Classes')
|
||||
for level_idx in self._all_levels:
|
||||
for section_idx in self._all_sections:
|
||||
self.print_class_schedule(level_idx, section_idx)
|
||||
|
||||
print('Branches: ', self._solver.NumBranches())
|
||||
print('Conflicts: ', self._solver.NumConflicts())
|
||||
print('WallTime: ', self._solver.WallTime())
|
||||
|
||||
|
||||
class SchoolSchedulingSatSolutionPrinter(cp_model.CpSolverSolutionCallback):
|
||||
|
||||
def __init__(self):
|
||||
cp_model.CpSolverSolutionCallback.__init__(self)
|
||||
self.__solution_count = 0
|
||||
def __init__(self):
|
||||
cp_model.CpSolverSolutionCallback.__init__(self)
|
||||
self.__solution_count = 0
|
||||
|
||||
def OnSolutionCallback(self):
|
||||
print('Found Solution!')
|
||||
def OnSolutionCallback(self):
|
||||
print(
|
||||
f'Solution #{self.__solution_count}, objective: {self.ObjectiveValue()}'
|
||||
)
|
||||
self.__solution_count += 1
|
||||
|
||||
|
||||
def main():
|
||||
# DATA
|
||||
subjects = ['English', 'Math', 'History']
|
||||
levels = ['1-', '2-', '3-']
|
||||
sections = ['A']
|
||||
teachers = ['Mario', 'Elvis', 'Donald', 'Ian']
|
||||
teachers_work_hours = [18, 12, 12, 18]
|
||||
working_days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']
|
||||
periods = ['08:00-09:30', '09:45-11:15', '11:30-13:00']
|
||||
curriculum = {
|
||||
('1-', 'English'): 3,
|
||||
('1-', 'Math'): 3,
|
||||
('1-', 'History'): 2,
|
||||
('2-', 'English'): 4,
|
||||
('2-', 'Math'): 2,
|
||||
('2-', 'History'): 2,
|
||||
('3-', 'English'): 2,
|
||||
('3-', 'Math'): 4,
|
||||
('3-', 'History'): 2
|
||||
}
|
||||
# DATA
|
||||
## Classes
|
||||
LEVELS = [
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
]
|
||||
SECTIONS = [
|
||||
'A',
|
||||
'B',
|
||||
]
|
||||
SUBJECTS = [
|
||||
'English',
|
||||
'Math',
|
||||
#'Science',
|
||||
'History',
|
||||
]
|
||||
CURRICULUM = {
|
||||
('1', 'English'): 3,
|
||||
('1', 'Math'): 3,
|
||||
('1', 'History'): 2,
|
||||
('2', 'English'): 4,
|
||||
('2', 'Math'): 2,
|
||||
('2', 'History'): 2,
|
||||
('3', 'English'): 2,
|
||||
('3', 'Math'): 4,
|
||||
('3', 'History'): 2,
|
||||
}
|
||||
|
||||
# Subject -> List of teachers who can teach it
|
||||
specialties_idx_inverse = [
|
||||
[1, 3], # English -> Elvis & Ian
|
||||
[0, 3], # Math -> Mario & Ian
|
||||
[2, 3] # History -> Donald & Ian
|
||||
]
|
||||
## Teachers
|
||||
TEACHERS = { # name, max_work_hours
|
||||
'Mario': 18,
|
||||
'Elvis': 12,
|
||||
'Donald': 12,
|
||||
'Ian': 18,
|
||||
}
|
||||
# Subject -> List of teachers who can teach it
|
||||
SPECIALTIES = {
|
||||
'English': ['Elvis', 'Ian'],
|
||||
'Math': ['Mario', 'Ian'],
|
||||
'History': ['Donald', 'Ian'],
|
||||
}
|
||||
|
||||
problem = SchoolSchedulingProblem(
|
||||
subjects, teachers, curriculum, specialties_idx_inverse, working_days,
|
||||
periods, levels, sections, teachers_work_hours)
|
||||
solver = SchoolSchedulingSatSolver(problem)
|
||||
solver.solve()
|
||||
## Schedule
|
||||
TIME_SLOTS = {
|
||||
('Monday', '08:00-09:30'): 1.5,
|
||||
('Monday', '09:45-11:15'): 1.5,
|
||||
('Monday', '11:30-12:30'): 1,
|
||||
('Monday', '13:30-15:30'): 2,
|
||||
('Monday', '15:45-17:15'): 1.5,
|
||||
('Tuesday', '08:00-09:30'): 1.5,
|
||||
('Tuesday', '09:45-11:15'): 1.5,
|
||||
('Tuesday', '11:30-12:30'): 1,
|
||||
('Tuesday', '13:30-15:30'): 2,
|
||||
('Tuesday', '15:45-17:15'): 1.5,
|
||||
('Wednesday', '08:00-09:30'): 1.5,
|
||||
('Wednesday', '09:45-11:15'): 1.5,
|
||||
('Wednesday', '11:30-12:30'): 1,
|
||||
('Thursday', '08:00-09:30'): 1.5,
|
||||
('Thursday', '09:45-11:15'): 1.5,
|
||||
('Thursday', '11:30-12:30'): 1,
|
||||
('Thursday', '13:30-15:30'): 2,
|
||||
('Thursday', '15:45-17:15'): 1.5,
|
||||
('Friday', '08:00-09:30'): 1.5,
|
||||
('Friday', '09:45-11:15'): 1.5,
|
||||
('Friday', '11:30-12:30'): 1,
|
||||
('Friday', '13:30-15:30'): 2,
|
||||
('Friday', '15:45-17:15'): 1.5,
|
||||
}
|
||||
|
||||
problem = SchoolSchedulingProblem(LEVELS, SECTIONS, SUBJECTS, CURRICULUM,
|
||||
TEACHERS, SPECIALTIES, TIME_SLOTS)
|
||||
solver = SchoolSchedulingSatSolver(problem)
|
||||
solver.solve()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user