Exemple #1
0
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