def break_out(destroyed: Solution, generator: Generator) -> Solution: """ Breaks out instruction activities based on the preferences of the unassigned learners. Where possible, a new activity is formed from these learners, along with some self-study learners that strictly prefer the new activity over self-study. If any learners remain that cannot be assigned to a new activity, those are inserted into existing activities using ``greedy_insert``. """ problem = Problem() histogram = destroyed.preferences_by_module() while len(histogram) != 0: _, module, to_assign = heappop(histogram) try: classroom = destroyed.find_classroom_for(module) teacher = destroyed.find_teacher_for(module) except LookupError: continue max_size = min(classroom.capacity, problem.max_batch) for activity in destroyed.activities: if activity.is_instruction(): continue if len(to_assign) >= max_size: break # We snoop off any self-study learners that can be assigned to # this module as well. learners = [learner for learner in activity.learners if learner.is_qualified_for(module) if learner.prefers_over_self_study(module)] learners = learners[:max_size - len(to_assign)] num_removed = activity.remove_learners(learners) to_assign.extend(learners[:num_removed]) activity = Activity(to_assign[:max_size], classroom, teacher, module) destroyed.add_activity(activity) destroyed.unassigned -= set(activity.learners) return break_out(destroyed, generator) # Insert final learners into existing activities, if no new activity # can be scheduled. return greedy_insert(destroyed, generator)
def initial_solution() -> Solution: """ Constructs an initial solution, where all learners are in self-study activities, in appropriate classrooms (and with some random teacher assigned to supervise). """ problem = Problem() solution = Solution([]) # Not all classrooms are suitable for self-study. Such a restriction does, # however, not apply to teachers. classrooms = [classroom for classroom in problem.classrooms if classroom.is_self_study_allowed()] learners_to_assign = problem.learners for classroom, teacher in zip_longest(classrooms, problem.teachers): assert classroom is not None assert teacher is not None learners = learners_to_assign[-min(len(learners_to_assign), classroom.capacity):] activity = Activity(learners, classroom, teacher, problem.self_study_module) solution.add_activity(activity) learners_to_assign = learners_to_assign[:-activity.num_learners] if len(learners_to_assign) == 0: break return solution
def greedy_insert(destroyed: Solution, generator: Generator) -> Solution: """ Greedily inserts learners into the best, feasible activities. If no activity can be found for a learner, (s)he is inserted into self-study instead. """ problem = Problem() unused_teachers = set(problem.teachers) - destroyed.used_teachers() unused_classrooms = set(problem.classrooms) - destroyed.used_classrooms() unused_classrooms = [ classroom for classroom in unused_classrooms if classroom.is_self_study_allowed() ] # It is typically a good idea to prefers using larger rooms for self-study. unused_classrooms.sort(key=attrgetter("capacity")) activities = destroyed.activities_by_module() while len(destroyed.unassigned) != 0: learner = destroyed.unassigned.pop() inserted = False # Attempts to insert the learner into the most preferred, feasible # instruction activity. for module_id in problem.most_preferred[learner.id]: module = problem.modules[module_id] if module not in activities: continue if not learner.prefers_over_self_study(module): break # TODO Py3.8: use assignment expression in if-statement. inserted = _insert(learner, activities[module]) if inserted: break # Could not insert, so the module activities must be exhausted. del activities[module] # Learner could not be inserted into a regular instruction activity, # so now we opt for self-study. if not inserted and not _insert(learner, activities[problem.self_study_module]): if len(unused_classrooms) == 0 or len(unused_teachers) == 0: # This implies we need to remove one or more instruction # activities. Let's do the naive and greedy thing, and switch # the instruction activity with lowest objective value into a # self-study assignment. iterable = [ activity for activity in destroyed.activities if activity.is_instruction() if activity.classroom.is_self_study_allowed() # After switching to self-study, the max_batch # constraint is longer applicable - only capacity. if activity.num_learners < activity.classroom.capacity ] activity = min(iterable, key=methodcaller("objective")) activities[problem.self_study_module].append(activity) activities[activity.module].remove(activity) activity.switch_to_self_study() activity.insert_learner(learner) continue for activity in activities[problem.self_study_module]: biggest_classroom = unused_classrooms[-1] if activity.classroom.capacity < biggest_classroom.capacity: current = activity.classroom activity.classroom = unused_classrooms.pop() unused_classrooms.insert(0, current) destroyed.switch_classrooms(current, activity.classroom) activity.insert_learner(learner) break if activity.can_split(): teacher = unused_teachers.pop() classroom = unused_classrooms.pop() new_activity = activity.split_with(classroom, teacher) activity.insert_learner(learner) destroyed.add_activity(new_activity) activities[problem.self_study_module].append(new_activity) break else: # It could be that there is no self-study activity. In that # case we should make one. Should be rare. classroom = unused_classrooms.pop() teacher = unused_teachers.pop() # TODO what if there are insufficient learners left? learners = [ destroyed.unassigned.pop() for _ in range(problem.min_batch) ] activity = Activity(learners, classroom, teacher, problem.self_study_module) # Since we popped this learner from the unassigned list before, # it is not yet in the new activity. activity.insert_learner(learner) destroyed.add_activity(activity) activities[problem.self_study_module].append(activity) return destroyed