In [ ]:
from ortools.sat.python import cp_model


class SchoolSchedulingProblem(object):

  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


class SchoolSchedulingSatSolver(object):

  def __init__(self, problem):
    # 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

    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)

    self.model = cp_model.CpModel()

    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 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 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 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 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)
    print()
    print('status', status)
    print('Branches', solver.NumBranches())
    print('Conflicts', solver.NumConflicts())
    print('WallTime', solver.WallTime())


class SchoolSchedulingSatSolutionPrinter(cp_model.CpSolverSolutionCallback):

  def __init__(self):
    cp_model.CpSolverSolutionCallback.__init__(self)
    self.__solution_count = 0

  def OnSolutionCallback(self):
    print('Found Solution!')


# 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
}

# 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
]

problem = SchoolSchedulingProblem(
    subjects, teachers, curriculum, specialties_idx_inverse, working_days,
    periods, levels, sections, teachers_work_hours)
solver = SchoolSchedulingSatSolver(problem)
solver.solve()