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()