def in_route_two_opt(route: Route) -> Route: """ Performs the best in-route two-opt swap, based on routing costs. Intra 2-opt in Hornstra et al. (2020). """ problem = Problem() tour = np.array([DEPOT] + route.customers.to_list()) feasible_moves = Heap() feasible_moves.push(route.cost(), route) for first in range(1, len(route)): for second in range(first + 1, len(route)): if _gain(tour, first, second) >= 0: continue # this is not a better move. # This is a better route than the one we have currently. Of course # that does not mean we can find a good handling configuration as # well, so we attempt to create a route for this 2-opt move and skip # if it is infeasible. tour[first:second] = tour[second - 1:first - 1:-1] new_route = Route([], [Stacks(problem.num_stacks)]) if new_route.attempt_append_tail(tour[1:]): feasible_moves.push(new_route.cost(), new_route) _, best_route = feasible_moves.pop() return best_route
def pickup_push_to_front(route: Route) -> Route: """ Pushes pickup items to the front, at various legs of the route. This is somewhat preferred, as these items cannot get in the way if they are positioned at the front. """ if np.isclose(route.handling_cost(), 0.): return route problem = Problem() for idx_customer, customer in enumerate(route, 1): pickup = problem.pickups[customer] idx_stack = route.plan[idx_customer].find_stack(pickup).index # This skips the last offset, as that would not be interesting anyway # (that is the leg towards the depot, where we have only pickups and # handling can no longer be improved). for plan_offset in range(idx_customer, len(route)): new_route = deepcopy(route) for stacks in new_route.plan[plan_offset:]: stack = stacks[idx_stack] idx_current = stack.item_index(pickup) stack.remove(pickup) stack.push(idx_current + 1, pickup) new_route.invalidate_handling_cache() if new_route.handling_cost() < route.handling_cost(): return new_route return route
def item_reinsert(route: Route) -> Route: """ Reinserts customer demands and pickups item in the optimal stack and position. Stops once an improving move has been found. """ if np.isclose(route.handling_cost(), 0.): return route problem = Problem() for idx, customer in enumerate(route, 1): delivery = problem.demands[customer] pickup = problem.pickups[customer] new_route = deepcopy(route) for stacks in new_route.plan[:idx]: stacks.find_stack(delivery).remove(delivery) for stacks in new_route.plan[idx:]: stacks.find_stack(pickup).remove(pickup) next_route = _insert_item(new_route, delivery) next_route = _insert_item(next_route, pickup) if next_route.handling_cost() < route.handling_cost(): return next_route return route
def _gain(tour: np.ndarray, first: int, second: int) -> float: # Proposed changes. gain = Route.distance([tour[first - 1], tour[second - 1]]) gain += Route.distance([tour[first], tour[second]]) # Current situation. gain -= Route.distance([tour[first - 1], tour[first]]) gain -= Route.distance([tour[second - 1], tour[second]]) return gain
def _customer_routing_cost(route: Route, customer: int, idx: int) -> float: customers = route.customers problem = Problem() assert 0 <= idx < len(customers) assert customer in route # There is just one customer, which, once removed, would result in a cost # of zero. Hence the cost for this single customer is just the route cost. if len(customers) == 1: return route.routing_cost() if idx == 0: cost = problem.short_distances[DEPOT, customer, customers[1]] cost -= problem.distances[DEPOT + 1, customers[1] + 1] return cost if idx == len(route) - 1: cost = problem.short_distances[customers[-2], customer, DEPOT] cost -= problem.distances[customers[-2] + 1, DEPOT + 1] return cost cost = problem.short_distances[customers[idx - 1], customer, customers[idx + 1]] cost -= problem.distances[customers[idx - 1] + 1, customers[idx + 1] + 1] return cost
def _gain(costs: np.ndarray, route1: Route, idx1: int, route2: Route, idx2: int) -> float: prev1 = DEPOT if idx1 == 0 else route1.customers[idx1 - 1] next1 = DEPOT if idx1 == len(route1) - 1 else route1.customers[idx1 + 1] prev2 = DEPOT if idx1 == 0 else route2.customers[idx2 - 1] next2 = DEPOT if idx2 == len(route2) - 1 else route2.customers[idx2 + 1] # Proposed changes. gain = Route.distance([prev1, route2.customers[idx2], next1]) gain += Route.distance([prev2, route1.customers[idx1], next2]) # Current situation. gain -= costs[route1.customers[idx1]] gain -= costs[route2.customers[idx2]] return gain
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 _gain(costs: np.ndarray, route: Route, idx: int, customer: int) -> float: pred = DEPOT if idx == 0 else route.customers[idx - 1] succ = DEPOT if idx == len(route) else route.customers[idx] return Route.distance([pred, customer, succ]) - costs[customer]