Esempio n. 1
0
def exchange_customer(solution: Solution) -> Solution:
    """
    Performs exchange moves between two customers. Of all such moves, the best
    is performed and the updated solution is returned. O(n^2), where n is the
    number of customers.

    Similar to exchange in Hornstra et al. (2020).

    References
    ----------
    - Savelsbergh, Martin W. P. 1992. "The Vehicle Routing Problem with Time
      Windows: Minimizing Route Duration." *ORSA Journal on Computing* 4 (2):
      146-154.
    """
    improvements = Heap()
    costs = routing_costs(solution)

    for idx1, route1 in enumerate(solution.routes):
        for idx2, route2 in enumerate(solution.routes[idx1 + 1:], idx1 + 1):
            iterable = product(range(len(route1)), range(len(route2)))

            for idx_cust1, idx_cust2 in iterable:
                if _gain(costs, route1, idx_cust1, route2, idx_cust2) >= 0:
                    continue

                new_route1 = deepcopy(route1)
                new_route2 = deepcopy(route2)

                customer1 = route1.customers[idx_cust1]
                customer2 = route2.customers[idx_cust2]

                new_route1.remove_customer(customer1)
                new_route2.remove_customer(customer2)

                if not new_route1.can_insert(customer2, idx_cust1):
                    continue

                if not new_route2.can_insert(customer1, idx_cust2):
                    continue

                new_route1.insert_customer(customer2, idx_cust1)
                new_route2.insert_customer(customer1, idx_cust2)

                current = route1.cost() + route2.cost()
                proposed = new_route1.cost() + new_route2.cost()

                if proposed < current:
                    improvements.push(proposed,
                                      (idx1, new_route1, idx2, new_route2))

    if len(improvements) != 0:
        _, (idx1, new_route1, idx2, new_route2) = improvements.pop()

        solution = copy(solution)

        solution.routes[idx1] = new_route1
        solution.routes[idx2] = new_route2

    return solution
def cross_customer_exchange(solution: Solution) -> Solution:
    """
    Tries to remove crossing links between routes. Of all such moves, the best
    is performed and the updated solution is returned. O(n^2), where n is the
    number of customers.

    References
    ----------
    - Savelsbergh, Martin W. P. 1992. "The Vehicle Routing Problem with Time
      Windows: Minimizing Route Duration." *ORSA Journal on Computing* 4 (2):
      146-154.
    """
    improvements = Heap()

    for idx1, route1 in enumerate(solution.routes):
        for idx2, route2 in enumerate(solution.routes[idx1 + 1:], idx1 + 1):
            iterable = product(range(len(route1)), range(len(route2)))

            for idx_cust1, idx_cust2 in iterable:
                if _gain(route1, idx_cust1, route2, idx_cust2) >= 0:
                    continue

                new_route1 = deepcopy(route1)
                new_route2 = deepcopy(route2)

                first_customers = route1.customers[idx_cust1 + 1:]
                second_customers = route2.customers[idx_cust2 + 1:]

                for customer in first_customers:
                    new_route1.remove_customer(customer)

                for customer in second_customers:
                    new_route2.remove_customer(customer)

                if not new_route1.attempt_append_tail(second_customers):
                    continue

                if not new_route2.attempt_append_tail(first_customers):
                    continue

                current = route1.cost() + route2.cost()
                proposed = new_route1.cost() + new_route2.cost()

                if proposed < current:
                    improvements.push(proposed,
                                      (idx1, new_route1, idx2, new_route2))

    if len(improvements) != 0:
        _, (idx1, new_route1, idx2, new_route2) = improvements.pop()

        solution = copy(solution)

        solution.routes[idx1] = new_route1
        solution.routes[idx2] = new_route2

    return solution
Esempio n. 3
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)
Esempio n. 4
0
def analyse(in_files: str, out_file: str):
    """
    Creates a summary file for the passed-in in-files, at the out-file
    location. This file summarises all passed-in instances.
    """
    file = Path(out_file)

    if file.exists():
        last_modified = file.lstat().st_mtime
        timestamp = datetime.fromtimestamp(last_modified)

        print(f"Printing {out_file} (last modified {timestamp})")
        instances = pd.read_csv(out_file, skipfooter=1, engine="python")
    else:
        instances = pd.DataFrame()

        for location in glob.iglob(in_files):
            problem = Problem.from_file(location, delimiter=',')
            sol = Solution.from_file(f"solutions/oracs_{problem.instance}.csv")

            instance = dict([(func.__name__, func(sol))
                             for func in PARAMETERS + STATISTICS])

            instances = instances.append(instance, ignore_index=True)

    report = make_pivot_table(instances)

    if not file.exists():
        report.to_csv(out_file)

    print(report)
Esempio n. 5
0
    def __call__(self, current: Solution, *args) -> Solution:
        improved = self._improve(deepcopy(current), self._solution_operators)

        for idx, route in enumerate(improved.routes):
            improved.routes[idx] = self._improve(route, self._route_operators)

        assert improved.objective() <= current.objective()
        return improved
Esempio n. 6
0
def relocate_customer(solution: Solution) -> Solution:
    """
    Performs the best customer relocation move, based on routing costs. Of all
    such moves, the best is performed and the updated solution is returned.
    O(n^2), where n is the number of customers.

    Similar to reinsertion in Hornstra et al. (2020).

    References
    ----------
    - Savelsbergh, Martin W. P. 1992. "The Vehicle Routing Problem with Time
      Windows: Minimizing Route Duration." *ORSA Journal on Computing* 4 (2):
      146-154.
    """
    improvements = Heap()
    costs = routing_costs(solution)

    for idx_route, curr_route in enumerate(solution.routes):
        for customer in curr_route:
            for route in solution.routes[idx_route:]:
                for idx in range(len(route) + 1):
                    gain = _gain(costs, route, idx, customer)

                    if gain >= 0 or not route.can_insert(customer, idx):
                        # This is either infeasible, or not an improving move.
                        continue

                    # The following performs the proposed move on a copy of the
                    # two routes involved. If the move is an improvement, it is
                    # added to the pool of improving moves.
                    old_route = deepcopy(curr_route)
                    new_route = deepcopy(route)

                    old_route.remove_customer(customer)
                    new_route.insert_customer(customer, idx)

                    current = route.cost() + curr_route.cost()
                    proposed = old_route.cost() + new_route.cost()

                    if proposed < current:
                        improvements.push(proposed, (customer, idx, route))

    if len(improvements) != 0:
        _, (customer, insert_idx, next_route) = improvements.pop()

        solution = copy(solution)
        route = solution.find_route(customer)

        if route is next_route and route.customers.index(
                customer) < insert_idx:
            # We re-insert into the same route, and the insert location will
            # shift once we remove the customer. This accounts for that.
            insert_idx -= 1

        route.remove_customer(customer)
        next_route.insert_customer(customer, insert_idx)

    return solution
Esempio n. 7
0
def reinsert_learner(current: Solution, generator: Generator) -> Solution:
    """
    Computes the best reinsertion moves for each learner, stores these in order,
    and executes them. This improves the solution further by moving learners
    into strictly improving assignments, if possible.
    """
    problem = Problem()

    # Get all instruction activities, grouped by module. We only consider
    # moves out of self-study (self-study could be better as well, but the
    # structure of the repair operators makes it unlikely it is preferred over
    # the current learner assignment).
    activities_by_module = current.activities_by_module()
    del activities_by_module[problem.self_study_module]

    moves = []

    for from_activity in current.activities:
        if not from_activity.can_remove_learner():
            continue

        for learner in from_activity.learners:
            for module_id in problem.most_preferred[learner.id]:
                module = problem.modules[module_id]

                if from_activity.is_self_study() \
                        and not learner.prefers_over_self_study(module):
                    break

                gain = problem.preferences[learner.id, module_id]
                gain -= problem.preferences[learner.id, from_activity.module.id]

                if gain <= 0:
                    break

                for to_activity in activities_by_module[module_id]:
                    if to_activity.can_insert_learner():
                        # Random value only to ensure this orders well -
                        # learners and activities cannot be used to compare
                        # the tuples, and gain is the same for many values.
                        item = (-gain, generator.random(),
                                learner, from_activity, to_activity)
                        heappush(moves, item)

    has_moved = set()  # tracks whether we've already moved a learner.

    while len(moves) != 0:
        *_, learner, from_activity, to_activity = heappop(moves)

        if learner not in has_moved \
                and from_activity.can_remove_learner() \
                and to_activity.can_insert_learner():
            from_activity.remove_learner(learner)
            to_activity.insert_learner(learner)
            has_moved.add(learner)

    return current
Esempio n. 8
0
def _remove(destroyed: Solution, customer: int, removed: SetList,
            customers: Set):
    route = destroyed.find_route(customer)
    idx = route.customers.index(customer)

    # Selects the customer and the direct neighbours before and after the
    # customer, should those exist.
    selected = route.customers[max(idx - 1, 0):min(idx + 2, len(route))]

    for candidate in selected:
        removed.append(candidate)
        route.remove_customer(candidate)
        customers.remove(candidate)
Esempio n. 9
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. 10
0
def compute(parser, args):
    measures = []

    for instance in np.arange(1, 101):
        location = Path(f"experiments/{args.experiment}/"
                        f"{instance}-{args.method}.json")

        if not location.exists():
            print(f"{parser.prog}: {location} does not exist; skipping.")
            continue

        Problem.from_instance(args.experiment, instance)
        sol = Solution.from_file(location)

        measures.append({name: fn(sol) for name, fn in MEASURES.items()})

    return pd.DataFrame.from_records(measures, columns=MEASURES.keys())
Esempio n. 11
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)
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."
Esempio n. 13
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. 14
0
def objective(solution: Solution):
    return -solution.objective()
Esempio n. 15
0
def objective(solution: Solution) -> float:
    """
    Returns solution objective.
    """
    return solution.objective()