Esempio n. 1
0
def worst_learners(current: Solution, generator: Generator):
    """
    Computes the costs for each learner, as the difference between their best
    and current assignments. Using a skewed distribution, q of the worst cost
    learners are randomly selected and removed from the solution.
    """
    destroyed = deepcopy(current)

    problem = Problem()
    costs = np.zeros(problem.num_learners)

    assigned_activities = {}

    for activity in destroyed.activities:
        for learner in activity.learners:
            assigned_activities[learner.id] = activity

        learner_ids = activity.learner_ids()

        # The cost is the cost of the best possible assignment for this
        # learner, minus the cost of the current assignment (including
        # self-study penalty, if applicable). The larger the cost, the more
        # suboptimal the current assignment.
        best_module_id = problem.most_preferred[learner_ids, 0]
        curr_module_id = activity.module.id

        costs[learner_ids] = problem.preferences[learner_ids, best_module_id]
        costs[learner_ids] -= problem.preferences[learner_ids, curr_module_id]

        if activity.is_self_study():
            # Per the paper:   pref(best) - (pref(curr) - <maybe penalty>)
            #                = pref(best) - pref(curr) + <maybe penalty>.
            costs[learner_ids] += problem.penalty

    learners = np.argsort(costs)
    learners = learners[-random_selection(generator)]

    for learner_id in learners:
        activity = assigned_activities[learner_id]

        if activity.can_remove_learner():
            learner = problem.learners[learner_id]

            destroyed.unassigned.add(learner)
            activity.remove_learner(learner)

    return destroyed
def create_single_customer_route(customer: int) -> Route:
    """
    Creates a single customer route for the passed-in customer. This route
    visits the DEPOT, then the customer, and returns to the DEPOT. O(1).
    """
    problem = Problem()

    # After depot, and after customer: two configurations in total.
    stacks = [Stacks(problem.num_stacks) for _ in range(2)]

    # We place the deliveries and pickups in the shortest stack - this
    # does not really matter much, as each stack is empty at this point
    # anyway.
    stacks[0].shortest_stack().push_rear(problem.demands[customer])
    stacks[1].shortest_stack().push_rear(problem.pickups[customer])

    return Route([customer], stacks)
def classrooms_to_modules(solution: List[Tuple]) -> bool:
    """
    Verifies the number of classrooms assigned is less than the total number
    of classrooms available, and each classroom is assigned to *one* module
    only.
    """
    problem = Problem()
    classroom_modules = defaultdict(set)

    for assignment in solution:
        _, module, classroom, _ = assignment
        classroom_modules[classroom].add(module)

    if len(classroom_modules) > len(problem.classrooms):
        return False

    return all(len(value) == 1 for value in classroom_modules.values())
Esempio n. 4
0
def _setup_decision_variables(solver: Model):
    """
    Prepares and applies the decision variables to the model.
    """
    problem = Problem()

    assignment_problem = [
        list(range(len(problem.learners))),
        list(range(len(problem.modules))),
        list(range(len(problem.classrooms))),
        list(range(len(problem.teachers)))
    ]

    solver.assignment = solver.binary_var_matrix(*assignment_problem[:2],
                                                 name="learner_module")

    solver.module_resources = solver.binary_var_cube(*assignment_problem[1:],
                                                     name="module_resources")
def _gain(route1: Route, idx1: int, route2: Route, idx2: int) -> float:
    next1 = DEPOT if idx1 == len(route1) - 1 else route1.customers[idx1 + 1]
    next2 = DEPOT if idx2 == len(route2) - 1 else route2.customers[idx2 + 1]

    problem = Problem()

    customer1 = route1.customers[idx1]
    customer2 = route2.customers[idx2]

    # Proposed changes.
    gain = problem.distances[customer2 + 1, next1 + 1]
    gain += problem.distances[customer1 + 1, next2 + 1]

    # Current situation.
    gain -= problem.distances[customer1 + 1, next1 + 1]
    gain -= problem.distances[customer2 + 1, next2 + 1]

    return gain
Esempio n. 6
0
def main():
    if len(sys.argv) < 2:
        raise ValueError(f"{sys.argv[0]}: expected file location.")

    problem = Problem.from_file(sys.argv[1], delimiter=',')
    solution = Solution.from_file(f"solutions/oracs_{problem.instance}.csv")

    is_feasible = True

    for idx, rule in enumerate(RULES):
        result, message = rule(solution)

        print(f"{idx}: {message}")

        if not result:
            is_feasible = False

    exit(0 if is_feasible else 1)
Esempio n. 7
0
def random_selection(generator: Generator):
    """
    Implements a random selection mechanism, which selects random indices for
    a certain list of num_learners length (e.g., for a cost computation),
    favouring smaller indices.
    """
    problem = Problem()

    triangle = np.arange(learners_to_remove(), 0, -1)

    probabilities = np.ones(problem.num_learners)
    probabilities[:learners_to_remove()] = triangle
    probabilities = probabilities / np.sum(probabilities)

    return generator.choice(problem.num_learners,
                            learners_to_remove(),
                            replace=False,
                            p=probabilities)
def random_customers(current: Solution, rnd_state: Generator) -> Solution:
    """
    Removes a number of randomly selected customers from the passed-in solution.
    See ``customers_to_remove`` for the degree of destruction done.

    Random removal in Hornstra et al. (2020).
    """
    destroyed = deepcopy(current)

    for customer in rnd_state.choice(Problem().num_customers,
                                     customers_to_remove(),
                                     replace=False):
        destroyed.unassigned.append(customer)

        route = destroyed.find_route(customer)
        route.remove_customer(customer)

    return destroyed
Esempio n. 9
0
def module_classroom_room_type(solution: List[Tuple]) -> bool:
    """
    Verifies each classroom-module assignment satisfies the room type
    requirement.
    """
    problem = Problem()
    classrooms = {}

    for assignment in solution:
        _, module, classroom, _ = assignment
        classrooms[classroom] = module

    for classroom_idx, module_idx in classrooms.items():
        classroom = problem.classrooms[classroom_idx]
        module = problem.modules[module_idx]

        if not classroom.is_qualified_for(module):
            return False

    return True
def strictly_positive_assignment(solver):
    """
    Ensures learners are assigned only to modules for which they hold a
    strictly positive preference. This guarantees learners are not assigned
    to modules they are currently ineligible to take.
    """
    problem = Problem()

    for learner, module in itertools.product(range(len(problem.learners)),
                                             range(len(problem.modules))):
        preference = problem.preferences[learner, module] \
                     * solver.assignment[learner, module]

        grace = solver.B * (1 - solver.assignment[learner, module])

        # For each learner and module assignment, the preference for that
        # module needs to be strictly positive - unless the learner is not
        # assigned, in which case we use a grace term to ensure the constraint
        # holds.
        solver.add_constraint(preference + grace >= 0.00001)
Esempio n. 11
0
def teacher_module_qualifications(solution: List[Tuple]) -> bool:
    """
    Verifies each teacher-module assignment satisfies the required teacher
    qualification.
    """
    problem = Problem()
    teacher_modules = {}

    for assignment in solution:
        _, module, _, teacher = assignment
        teacher_modules[teacher] = module

    for teacher_idx, module_idx in teacher_modules.items():
        teacher = problem.teachers[teacher_idx]
        module = problem.modules[module_idx]

        if not teacher.is_qualified_for(module):
            return False

    return True
Esempio n. 12
0
def self_study_allowed(solver):
    """
    For each classroom, checks if a self-study assignment is allowed. This
    ensures only classrooms that allow self-study are used for self-study
    assignments.

    Note
    ----
    Multiple room types allow self-study, so self-study has its own boolean
    flag to differentiate.
    """
    problem = Problem()

    for classroom in range(len(problem.classrooms)):
        assignment = solver.sum(solver.module_resources[len(problem.modules) -
                                                        1, classroom, teacher]
                                for teacher in range(len(problem.teachers)))

        is_allowed = problem.classrooms[classroom].is_self_study_allowed()
        solver.add_constraint(assignment <= is_allowed)
def all_pickups_are_satisfied(solution: Solution) -> Tuple[bool, str]:
    """
    Verifies all pickups are satisfied, that is, the pickup items are loaded
    according to a feasible loading plan for each customer.
    """
    problem = Problem()

    for customer in range(problem.num_customers):
        route = solution.find_route(customer)
        pickup = problem.pickups[customer]

        for stacks in route.plan[route.customers.index(customer) + 1:]:
            try:
                # Quickly finds the stack this item is stored in, or raises
                # if no such stack exists. Just the existence is sufficient.
                stacks.find_stack(pickup)
            except LookupError:
                return False, f"{pickup} is not in the solution for all " \
                              f"appropriate legs of the route."

    return True, "All pick-ups are satisfied."
def teaching_qualification(solver):
    """
    The teaching qualification constraint ensures teacher-module assignments
    are feasible. Each module has a teaching qualification requirement, and
    each teacher has a qualification for each module. Qualifications are
    ordinal as {0, 1, 2, 3}, where ``0`` indicates no qualification, and the
    rest are in decreasing level of qualification: a ``1`` is better than a
    ``2``.

    Example
    -------
    Suppose a module requires a qualification of ``2``. Then any teacher with
    an equal or better (in this case, ``2`` or ``1``) qualification is eligible
    to teach this activity.

    Note
    ----
    Since self-study has a required qualification of ``3``, every teacher is
    eligible to supervise the activity. As such, we can ignore this assignment
    here.
    """
    problem = Problem()

    for module, teacher in itertools.product(range(len(problem.modules) - 1),
                                             range(len(problem.teachers))):
        is_assigned = solver.sum(
            solver.module_resources[module, classroom, teacher]
            for classroom in range(len(problem.classrooms)))

        module_qualification = problem.modules[module].qualification
        teacher_qualification = problem.qualifications[teacher, module]

        # Module requirement must be 'less' than teacher qualification.
        solver.add_constraint(module_qualification * is_assigned
                              >= teacher_qualification * is_assigned)

        # Zero implies the teacher is not qualified, so we should ensure those
        # assignments cannot happen.
        solver.add_constraint(teacher_qualification * is_assigned
                              >= is_assigned)
Esempio n. 15
0
def minimum_quantity(current: Solution, rnd_state: Generator) -> Solution:
    """
    Removes customers based on quantity (demand + pickup). Randomly selects q
    customers based on a distribution over these quantities, favouring smaller
    over larger quantity customers.

    Similar - but not equivalent - to minimum quantity removal in Hornstra et
    al. (2020).
    """
    problem = Problem()
    destroyed = deepcopy(current)

    indices = random_selection(rnd_state)
    customers = problem.smallest_quantity_customers[indices]

    for customer in customers:
        destroyed.unassigned.append(customer)

        route = destroyed.find_route(customer)
        route.remove_customer(customer)

    return destroyed
Esempio n. 16
0
def activity_size(solution: List[Tuple]) -> bool:
    """
    Verifies each activity satisfies both the minimum and maximum group size
    constraints.
    """
    problem = Problem()
    classroom_learners = defaultdict(set)

    for assignment in solution:
        learner, module, classroom, _ = assignment
        classroom_learners[classroom, module].add(learner)

    for (classroom, module), learners in classroom_learners.items():
        max_capacity = problem.classrooms[classroom].capacity

        if module != SELF_STUDY_MODULE_ID:
            max_capacity = min(problem.max_batch, max_capacity)

        if not problem.min_batch <= len(learners) <= max_capacity:
            return False

    return True
Esempio n. 17
0
def _near_best_insert(nearness: int, current: Solution,
                      rnd_state: Generator) -> Solution:
    """
    Sequentially inserts a random permutation of the unassigned customers
    into a near-best feasible route. The distance is controlled by the nearness
    parameter: the feasible route is within nearness steps from the best
    insertion point.

    Note: nearness == 1 implies a full greedy insert.
    """
    rnd_state.shuffle(current.unassigned)
    problem = Problem()

    while len(current.unassigned) != 0:
        customer = current.unassigned.pop()
        feasible_routes = Heap()

        for route in current.routes:
            insert_idx, cost = route.opt_insert(customer)

            if route.can_insert(customer, insert_idx):
                feasible_routes.push(cost, (insert_idx, route))

        if len(feasible_routes) != 0:
            num_smallest = min(nearness, len(feasible_routes))
            routes = feasible_routes.nsmallest(num_smallest)

            cost, (insert_idx, route) = routes[rnd_state.choice(num_smallest)]
            cost_new = problem.short_distances[DEPOT, customer, DEPOT]

            if cost_new > cost:
                route.insert_customer(customer, insert_idx)
                continue

        route = create_single_customer_route(customer)
        current.routes.append(route)

    return current
Esempio n. 18
0
def handling_costs(solution: Solution) -> np.ndarray:
    """
    Computes handling costs for each customer. This is an approximation: only
    the handling costs *at* the customer are computed, any costs made at other
    legs of the tour are not considered. O(|customers|).

    Note: this is a lower bound on the actual handling costs, as those are
    rather hard to compute. Some experimentation suggests it is e.g. not
    worthwhile to also count the costs incurred due to the delivery and pickup
    items at other legs of the route.
    """
    problem = Problem()
    costs = np.zeros(problem.num_customers)

    for route in solution.routes:
        for idx, customer in enumerate(route):
            before, after = route.plan[idx], route.plan[idx + 1]

            # This is the handling cost for just this customer, as an
            # approximation to the total handling costs.
            costs[customer] += Stacks.cost(customer, before, after)

    return costs
Esempio n. 19
0
def singular_use(solver):
    """
    Ensures each classroom and each teacher are used exactly once.
    """
    problem = Problem()

    for teacher in range(len(problem.teachers)):
        classrooms = solver.sum(
            solver.module_resources[module, classroom, teacher]
            for module in range(len(problem.modules))
            for classroom in range(len(problem.classrooms)))

        # Each teacher may be assigned to *at most* one classroom.
        solver.add_constraint(classrooms <= 1)

    for classroom in range(len(problem.classrooms)):
        teachers = solver.sum(solver.module_resources[module, classroom,
                                                      teacher]
                              for module in range(len(problem.modules))
                              for teacher in range(len(problem.teachers)))

        # Each classroom may be assigned to *at most* one teacher.
        solver.add_constraint(teachers <= 1)
Esempio n. 20
0
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
Esempio n. 21
0
def max_batch(solver):
    """
    This constraints guarantees the number of learners assigned to a module
    are supported by a sufficient number of classroom-teacher activities, such
    that the maximum batch and capacity constraints are satisfied.

    Note
    ----
    The ``max_batch`` constraint only holds for regular instruction activities,
    for self-study the constraint defaults to a capacity requirement.
    """
    problem = Problem()

    for module in problem.modules:
        module_learners = solver.sum(solver.assignment[learner.id, module.id]
                                     for learner in problem.learners)

        activities = solver.sum(
            solver.module_resources[module.id, classroom.id, teacher.id]
            * _max_capacity(classroom, module)
            for classroom in problem.classrooms
            for teacher in problem.teachers)

        solver.add_constraint(module_learners <= activities)
Esempio n. 22
0
def customers(solution: Solution) -> int:
    """
    Returns the number of customers in the problem instance.
    """
    return Problem().num_customers
Esempio n. 23
0
def handling(solution: Solution) -> float:
    """
    Returns the problem's handling cost parameter.
    """
    return Problem().handling_cost
Esempio n. 24
0
def instance(_):
    return Problem().instance
Esempio n. 25
0
def stacks(solution: Solution) -> int:
    """
    Returns the number of stacks in the problem instance's vehicles.
    """
    return Problem().num_stacks
Esempio n. 26
0
def instance(solution: Solution) -> int:
    """
    Returns problem instance number.
    """
    return Problem().instance
Esempio n. 27
0
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
Esempio n. 28
0
def learners_to_remove() -> int:
    problem = Problem()
    return int(DEGREE_OF_DESTRUCTION * problem.num_learners)
Esempio n. 29
0
def _to_assignments(solver: Model) -> List[Tuple]:
    """
    Turns the solver's decision variables into a series of (learner, module,
    classroom, teacher) assignments, which are then stored to the file system.

    TODO this is legacy code, taken from the old State object. It's not the
     prettiest, but it should work.
    """
    problem = Problem()

    learner_assignments = [
        module for learner in range(len(problem.learners))
        for module in range(len(problem.modules))
        if solver.assignment[learner, module].solution_value
    ]

    classroom_teacher_assignments = {
        (classroom, teacher): module
        for classroom in range(len(problem.classrooms))
        for teacher in range(len(problem.teachers))
        for module in range(len(problem.modules))
        if solver.module_resources[module, classroom, teacher].solution_value
    }

    assignments = []
    counters = defaultdict(lambda: 0)

    for module in range(len(problem.modules)):
        # Select learners and activities belonging to each module, such
        # that we can assign them below.
        learners = [
            learner for learner in range(len(problem.learners))
            if learner_assignments[learner] == module
        ]

        activities = [
            activity for activity, activity_module in
            classroom_teacher_assignments.items() if module == activity_module
        ]

        # Assign at least min_batch number of learners to each activity.
        # This ensures the minimum constraint is met for all activities.
        for classroom, teacher in activities:
            for _ in range(problem.min_batch):
                if not learners:
                    break

                assignment = (learners.pop(), module, classroom, teacher)
                assignments.append(list(assignment))

                counters[classroom] += 1

        # Next we flood-fill these activities with learners, until none
        # remain to be assigned.
        for classroom, teacher in activities:
            capacity = problem.classrooms[classroom].capacity

            if module != SELF_STUDY_MODULE_ID:
                capacity = min(problem.max_batch, capacity)

            while learners:
                if counters[classroom] == capacity:  # classroom is full
                    break

                assignment = (learners.pop(), module, classroom, teacher)
                assignments.append(list(assignment))

                counters[classroom] += 1

    return assignments
def customers_to_remove() -> int:
    """
    Returns the number of customers to remove from the solution.
    """
    return int(Problem().num_customers * DEGREE_OF_DESTRUCTION)