def add_class_from_collisions(self, collisions): """ Adds a ClassConstraint to the set of class constraints. The course to use for the class is determined by the given list of collisions. The most common course among the collisions is used. :param collisions: A list of collision objects generated from the last attempt at scheduling :return: None (use self.get_constraints() to see the results of this action) """ # find the most popular course among the collisions course_collision_list = {} for col in collisions: if col.collision_type == 'full class': continue course_id = col.scheduled_class.course_id if course_id not in course_collision_list: course_collision_list[course_id] = [] course_collision_list[course_id].append(col) best_count = 0 best_collision = None for course_id, coll_list in course_collision_list.items(): if len(coll_list) > best_count: best_count = len(coll_list) best_collision = coll_list[0] # now add a class for that collisions course course_id = best_collision.scheduled_class.course_id course = Course.query.get(course_id) new_class = ClassConstraint(course_id, course.name) self.class_constraints[course_id].append(new_class) self.class_count += 1 msg = "Added another class for the course: {} {}".format(course.id, course.name) SchedUtil.log_note("info", "Scheduler", msg) self.reset_constraints()
def relax_constraints(self, class_id): if len(self.mand_timeblock_ids) == 0 and len(self.high_timeblock_ids) > 0: SchedUtil.log_note("info", "Scheduler", "Relaxing the classroom->timeblock constraints for class {} (classroom: {})".format( class_id, self.classroom_id)) self.high_classroom_ids = [] return return False
def check_if_student_is_overscheduled(student, required_courses): timeblock_count = Timeblock.query.count() if len(required_courses) > timeblock_count: msg = "Student {} course requirements ({}) are greater than the available number of timeblocks ({})".format( student.id, len(required_courses), timeblock_count) SchedUtil.log_note("error", "Scheduler", msg) raise SchedulerNoSolution(msg) elif len(required_courses) < timeblock_count: msg = "Student {} course requirements are less than the number of timeblocks".format(student.id) SchedUtil.log_note("warning", "Scheduler", msg)
def check_fact_utilization(self): """ Looks at the number of needed classes v.s. teachers, classrooms, and timeblocks to see if the amount we have is close to the min required """ teacher_utils = {} classroom_utils = {} for course_id, class_list in self.class_constraints.items(): # make sure theres enough teachers for the number of courses teacher_ids = class_list[0].get_teacher_ids() timeblock_ids = class_list[0].get_timeblock_ids() classroom_ids = class_list[0].get_classroom_ids() timeblock_count = len(timeblock_ids) teacher_count = len(teacher_ids) classroom_count = len(classroom_ids) class_count = len(class_list) teacher_max = timeblock_count * teacher_count classroom_max = timeblock_count * classroom_count if class_count > teacher_max: msg = "No solution, Not enough teachers for course: {} {}, teachers={}, classes={}".format( course_id, class_list[0].course_name, teacher_count, class_count) SchedUtil.log_note("error", "Scheduler", msg) raise SchedulerNoSolution("Not enough classrooms") elif len(class_list) > classroom_max: msg = "No solution, Not enough classrooms for course: {} {}, classrooms={}, classes={}".format( course_id, class_list[0].course_name, classroom_count, class_count) SchedUtil.log_note("error", "Scheduler", msg) raise SchedulerNoSolution("Not enough classrooms") # else: # print('\033[92m Course {} {} \033[0m'.format(course_id, class_list[0].course_name)) # print('\033[92m class count {} \033[0m'.format(class_count)) # print('\033[92m teachers {} \n\033[0m'.format(teacher_ids)) for t_id in teacher_ids: if t_id not in teacher_utils: teacher_utils[t_id] = 0.0 teacher_utils[t_id] += 1/teacher_count * class_count for c_id in classroom_ids: if c_id not in classroom_utils: classroom_utils[c_id] = 0.0 classroom_utils[c_id] += 1/classroom_count * class_count
def relax_constraints(self, class_id): if len(self.mand_classroom_constraints) == 0: if len(self.high_classroom_constraints) > 0: SchedUtil.log_note("info", "Scheduler", "Relaxing the teacher->classroom constraints for class {} (teacher: {})".format( class_id, self.teacher_id)) self.high_classroom_constraints = [] return else: for cc in self.low_classroom_constraints: if cc.can_relax_constraints(): cc.relax_constraints(class_id) return if len(self.mand_timeblock_ids) == 0 and len(self.high_timeblock_ids) > 0: SchedUtil.log_note("info", "Scheduler", "Relaxing the teacher->timeblock constraints for class {} (teacher: {})".format( class_id, self.teacher_id)) self.high_timeblock_ids = [] return return False
def relax_constraints(self): #TODO should take a more intellegent approach to this, but for now, blindly grab one and relax it if len(self.mand_teacher_constraints) == 0: if len(self.high_teacher_constraints) > 0: msg = "Relaxing the course->teacher constraints for class {} (course: {} {})".format( self.z3_index, self.course_id, self.course_name) SchedUtil.log_note("info", "Scheduler", msg) self.high_teacher_constraints = [] return else: for tc in self.low_teacher_constraints: if tc.can_relax_constraints(): tc.relax_constraints(self.z3_index) return if len(self.mand_classroom_constraints) == 0: if len(self.high_classroom_constraints) > 0: msg = "Relaxing the course->classroom constraints for class {} (course: {} {})".format( self.z3_index, self.course_id, self.course_name) SchedUtil.log_note("info", "Scheduler", msg) self.high_classroom_constraints = [] return else: for cc in self.low_classroom_constraints: if cc.can_relax_constraints(): cc.relax_constraints(self.z3_index) return if len(self.mand_timeblock_ids) == 0 and len(self.high_timeblock_ids) > 0: msg = "Relaxing the course->timeblock constraints for class {} (course: {} {})".format( self.z3_index, self.course_id, self.course_name) SchedUtil.log_note("info", "Scheduler", msg) self.high_timeblock_ids = [] return return False
def make_schedule(self): SchedUtil.log_note("info", "Scheduler", "Scheduler started") # first step, decide how many classes of each course we need # this is decided based on the need of the students, and what # teachers and rooms are available for each course. sched_constraints = ScheduleConstraints() # try a bunch of times before resorting to adding a class # attempts = int(len(sched_constraints.student_requirement_set) / 10) self.solver.push() for i in range(50): start_time = time.time() collisions = [] SchedUtil.log_note("info", "Scheduler", "Attempting to solve using {} classes".format(sched_constraints.class_count)) for i in range(20): # try 20 different z3 mappings # find a valid z3 mapping, or quit because none exist SchedUtil.log_note("info", "Scheduler", "Generating a mapping of teachers, courses, classrooms and timeblocks") while True: self.set_constraints(sched_constraints.get_constraints()) if self.solver.check() != sat: SchedUtil.log_note("warning", "Scheduler", "Solver could not find a solution for the current constraint set") if sched_constraints.can_relax_constraints(): sched_constraints.relax_constraints() sched_constraints.reset_constraints() else: SchedUtil.log_note("error", "Scheduler", "No valid mapping exists for current constraint set") raise SchedulerNoSolution('Not satisfiable') else: SchedUtil.log_note("info", "Scheduler", "Valid mapping found, attempting to schedule students") break schedule = self.gen_sched_classes(self.solver.model(), sched_constraints) # now start assigning students to classes and see if we can find # a place for every student collisions = self.place_students(schedule, sched_constraints.student_requirement_set) if len(collisions) == 0: SchedUtil.log_note("success", "Scheduler", "Solution found, saving schedule now") schedule.save() return else: if i < 20: SchedUtil.log_note("warning", "Scheduler", "Failed placing students, attempting again with a new mapping") # end_time = time.time() if sched_constraints.can_relax_constraints(): SchedUtil.log_note("warning", "Scheduler", "Failed placing students, relaxing constraints and trying again") sched_constraints.relax_constraints() sched_constraints.reset_constraints() else: SchedUtil.log_note("warning", "Scheduler", "Failed placing students, adding another class and trying again") sched_constraints.add_class_from_collisions(collisions) SchedUtil.log_note("error", "Scheduler", "Scheduler failed to place students") raise SchedulerNoSolution()