def _routestates2solution(routes, D): sol = [] total_cost = 0.0 for route, _, route_cost, route_l_updated in routes: # if L is not set, optimize TSP *in the end* if not route_l_updated: new_route, route_cost = solve_tsp(D, route) route = new_route[:-1] if __debug__: log( DEBUG - 1, "Got TSP solution %s (%.2f)" % (str(new_route), route_cost)) total_cost += route_cost sol += route sol += [0] return sol, total_cost
def _init_with_tsp(D, d, C, L): route_tsp_sol, route_f = solve_tsp(D, list(range(0, len(D)))) return route_tsp_sol + [0], route_f
def tyagi_init(D, d, C, L=None, only_large_and_close_customer_single_routes=False, try_interchange_with_all_group_customers=True, select_grouping="min_penalty"): """ This is an implementation of the Tyagi (1968) nearest neighbor heurstic. In the first phase the method distributes customers into groups using bulding a nearest neighbor chains leaving from the depot. The group is deemed complete when a constraint is violated. Then an interchange heuristic tries to remove one node from the group so that the one left out can be fitted. This interchange is done if the demand of the route increases due to the swap. If a group with only one customer is generated, all of the candidates that are close to the depot and have large demand are considered to be served with a route with a single customer. Furthermore the entire procedure is repeated for all single customer route candidates and the one that has "maximum use of the capacity" is used (see option select_grouping). Then a TSP procedure is used to route the customer groups. This this implementation uses 3-Opt local search to do the routing instead of the greedy TSP heuristic described in Tyagi (1968). The procedure above is used when the function is called with parameters: * only_large_and_close_customer_single_routes = True, * try_interchange_with_all_group_customers = False, * select_grouping = "max_demand" The "max_demand" selects the grouping where the non-single routes carry most capacity. However, these settings do not greate the grouping nor solution presented as an illustrative example in Tyagi (1968). It seems he uses different undocumented variant of his heuristic to solve the 12 customer problem from Dantzig and Ramser (1958). Therefore, following changes and options were added, and can be used, to replicate the result: * only_large_and_close_customer_single_routes = False, which allows any customer be served with a single customer route. * try_interchange_with_all_group_customers = True, which tries to do the interchange with all of the nodes in a group (proto-route), instead of trying just the first and last customers. * select_grouping = "balanced", which selects the most balanced alternative among the grouping candidates with single customer route. Here, only the non-single customer routes are considered. Alternatively, another option for selecting grouping (which more closely follows the idea of Tyagi) can be used: * select_grouping = "min_penalty", which selects the option where the routes with lot of free capacity are shortest (individual edges or the used vehicle capacity when traversing them are not considered). This is calculated on the optimized routes. Note that if C is not set the select_grouping and its functionality is not used. With these options the grouping of customers given in illustrative example is replicated and as is the solution quality. Unfortunately it is impossible to say if the modifications are the ones Tyagi (1968) used. Tyagi, M.S. (1968), "A Practical Method for Truck Dispatching Problem", J. Operations Research Society of Japan, 10, 76-92. Dantzig, G. B., & Ramser, J. H. (1959), "The truck dispatching problem", Management science, 6(1), 80-91. Helsgaun, K. (2006), "An Effective Implementation of K-opt Moves for the Lin-Kernighan TSP Heuristic." DATALOGISKE SKRIFTER, No. 109, 2006. Roskilde University. Helsgaun, K. (2009), "General k-opt submoves for the Lin-Kernighan TSP heuristic." Mathematical Programming Computation, 1(2), 119-163. """ ## 0. Determine the number of vehicles may_have_single_node_route = False if C: tot_d = sum(d) K = ceil(tot_d / float(C)) may_have_single_node_route = min(d[1:]) > tot_d - C * (K - 1) ## 1. "Grouping the delivery points" (nodes) to routes with nearest nbour. interchange_variant = lambda r, r_d, r_l, D, d, C, L, svd, nnn : \ route_end_interchange(r, r_d, r_l, D, d, C, L, svd, nnn, try_interchange_with_all_group_customers) solution = \ nearest_neighbor_init(D, d, C, L, initialize_routes_with=TYAGI_SEED_METHOD, emerging_route_count=1, add_only_to_end=True, route_improvement_callback=interchange_variant) routes = sol2routes(solution) if not C: best_C_utilization = None elif select_grouping == "balanced": # the best will be one that will have balanced routes best_C_utilization = \ _calculate_route_demand_statistics(solution, d, K=len(routes)) elif select_grouping == "max_demand": # the best will be the one that is best to top up the vehicle capacity best_C_utilization = sum(d[n] for n in solution) / float(len(routes) * C) elif select_grouping == "min_penalty": # the best will be the one with least distance traveled with unused capacity best_C_utilization = _calculate_penalty(solution, D, d, C) else: raise ValueError('"Invalid select_grouping parameter "%s"' % select_grouping) if __debug__: if C: log(DEBUG - 1, "Initial Grouping = %s\n" % (solution)) ## Tyagi heuristic has a special case for the single node routes try: single_nodes = [r[1] for r in routes if len(r) == 3] if C and (may_have_single_node_route or len(single_nodes) > 0): N = len(D) best_K = solution.count(0) - 1 if only_large_and_close_customer_single_routes: # According to Tyagi1967, only the large demand nodes close to the # depot are considered ... max_dist_from_depot = max(D[0, :]) candidates = [n for n in range(1,N) \ if (d[n]>0.5*C and D[0,n]<0.5*max_dist_from_depot)] else: # ... however, using this criteria does not allow replicating the # results (the illustrative example) of the paper. Therefore, # there is an option to try with complete set of nodes. candidates = list(range(1, len(D))) for single_route_candidate in candidates: if __debug__: log( DEBUG - 2, "Try to find grouping with n%d forbidden" % (single_route_candidate)) cndt_sol = nearest_neighbor_init( D, d, C, L, initialize_routes_with=TYAGI_SEED_METHOD, emerging_route_count=1, forbidden_nodes=[single_route_candidate], route_improvement_callback=interchange_variant) cndt_K = cndt_sol.count( 0) - 1 + 1 #the single_route_candidate_node if cndt_K > best_K: if __debug__: log(DEBUG - 1, "No valid grouping found\n") continue if select_grouping == "balanced": cndt_C_utilization = \ _calculate_route_demand_statistics(cndt_sol, d, K=cndt_K-1) elif select_grouping == "max_demand": cndt_C_utilization = sum(d[n] for n in cndt_sol) / float( cndt_K * C) elif select_grouping == "min_penalty": cndt_C_utilization = _calculate_penalty(cndt_sol, D, d, C) if __debug__: log( DEBUG - 1, "Grouping = %s (utilization %s)\n" % (cndt_sol + [single_route_candidate, 0], str(cndt_C_utilization))) # select the one with maximum possible use of carrier capacity if cndt_C_utilization > best_C_utilization: best_C_utilization = cndt_C_utilization solution = cndt_sol + [single_route_candidate, 0] routes = sol2routes(solution) ## 2. "Finding the optimal tours" #instead of the heuristic of Tyagi, just solve it with a TSP solver if __debug__: log( DEBUG - 1, "Post-optimize solution %s (%.2f)" % (solution, objf(solution, D))) for i in range(len(routes)): routes[i], route_f = solve_tsp(D, routes[i][:-1]) if __debug__: log(DEBUG - 2, "Got TSP solution %s (%.2f)" % (str(routes[i]), route_f)) except KeyboardInterrupt: #or SIGINT raise KeyboardInterrupt(solution) return routes2sol(routes)
def _phase_one(lambda_multiplier, D, d, C, L, seed_f, rr): """ This imlements the fist phase of the algorithm. Sequentally add nodes to an emerging node. Different seed node selection functions (above) can be used. """ route_seeds = [] N = len(D) sol = [] total_cost = 0.0 customer_nodes = list(range(1, N)) if rr is not None: shuffle(customer_nodes) rr -= 1 unrouted = OrderedSet(customer_nodes) if __debug__: log(DEBUG, "## Sequential route bulding phase ##") route_idx = 0 try: while unrouted: ## Step 1: choose unrouted i_k to act as a route seed point route_seed_k = seed_f(D, d, unrouted) unrouted.remove(route_seed_k) route_seeds.append(route_seed_k) route_demand = d[route_seed_k] if C else 0 route = [0, route_seed_k] route_cost = D[0, route_seed_k] + D[route_seed_k, 0] route_l_updated = True route_idx += 1 if __debug__: log( DEBUG, "Initialize route #%d with n%d" % (route_idx, route_seed_k)) ## Step 2: Compute savings s_vals = ( D[[0], unrouted] + lambda_multiplier * D[unrouted, [route_seed_k]]).tolist() savings = list(zip(s_vals, unrouted)) savings.sort() for best_saving, i in savings: ## Step 3: insert until feasibility is broken if __debug__: log( DEBUG, "Check feasibility of inserting n%d with savings=%.2f" % (i, best_saving)) #TODO: To improve performance keep track of minimal unrouted d and # break the savings loop if we see that there are no feasible # insertions to be made. if C and route_demand + d[i] - C_EPS > C: # capacity constraint violated, route complete if __debug__: log(DEBUG, "Insertion would break C constraint, skip.") continue #TODO: To improve performance, keep track of minimal possible # cost increase to the route. This would involve updating e.g. # Held-Karp (1970) lower bound for the route. # Use upper bound estimate to save some computations. We know # that the maximum route cost constraint cannot be violated. UB_route_cost = route_cost - D[route[-1], 0] + D[route[-1], i] + D[i, 0] if L and UB_route_cost - S_EPS > L: new_route, new_route_cost = solve_tsp(D, route + [i]) if __debug__: log( DEBUG - 1, "Got TSP solution %s (%.2f)" % (list(new_route), new_route_cost)) if new_route_cost - S_EPS > L: if __debug__: log(DEBUG, "Insertion would break L constraint, skip.") continue route_cost = new_route_cost route_l_updated = True else: route_l_updated = False route_cost = UB_route_cost new_route = route + [i, 0] # accept including node i route = new_route[:-1] if C: route_demand += d[i] unrouted.remove(i) if __debug__: log( DEBUG, "Inserted n%d to create a route %s (%.2f)." % (i, route, route_cost)) # if L is not set, optimize TSP after the route is full if not route_l_updated: new_route, route_cost = solve_tsp(D, route) route = new_route[:-1] if __debug__: if not L: log( DEBUG - 1, "Got TSP solution %s (%.2f)" % (str(route + [0]), route_cost)) if __debug__: log( DEBUG, "Route %s (%.2f) complete.\n" % (str(route + [0]), route_cost)) total_cost += route_cost sol += route except KeyboardInterrupt: #or SIGINT interrupted_sol = sol + routes2sol([n] for n in unrouted if n not in sol) raise KeyboardInterrupt(interrupted_sol) sol += [0] if __debug__: log(DEBUG, "Phase 1 solution %s (%.2f) complete.\n" % (str(sol), total_cost)) log(DEBUG - 1, "Pass on route seeds %s to Phase 2.\n" % str(route_seeds)) return route_seeds, sol, total_cost, rr
def _phase_two(mu_multiplier, route_seeds, D, d, C, L, rr, choose_most_associated_route=True, repeated_association_with_n_routes=1): ## Step 0: reuse seed nodes from phase 1 to act as route seed points N = len(D) K = len(route_seeds) customer_nodes = list(range(1, N)) if rr is not None: #->stochastic version, resolve the ties randomly shuffle(customer_nodes) unrouted_nodes = OrderedSet(customer_nodes) unrouted_nodes.difference_update(route_seeds) # routes are stored in dict with a key of route seed, # the route, demand, cost, and if it is updated are all stored routes = [ RouteState( [0, rs], #initial route d[rs] if C else 0, #initial demand D[0, rs] + D[rs, 0], #initial cost True) for rs in route_seeds ] insertion_infeasible = [[False] * N for rs in route_seeds] if __debug__: log( DEBUG, "## Parallel route building phase with seeds %s ##" % str(list(route_seeds))) ## Step 1.1: vectorized calculation of eps # TODO: this also calculates eps for depot and seeds. Omitting those would # be possible and save few CPU cycles, but it would make indexing more # complex and because accuracy>simplicity>speed, it is the way it is. eps = (np.tile(D[[0], :], (K, 1)) + mu_multiplier * D[:, route_seeds].transpose() - np.tile(D[0, route_seeds], (N, 1)).transpose()) associate_to_nth_best_route = 1 insertions_made = False route_seed_idxs = [] insertions_made = False first_try = True try: while unrouted_nodes: ## Main while loop bookkeeping if not route_seed_idxs: idxs = list(range(K)) if rr is not None: #->stocastic version, construct routes in random order shuffle(idxs) route_seed_idxs = deque(idxs) if not first_try: # The CMT1979 exits when all routes have been tried if repeated_association_with_n_routes is None: break if not insertions_made: associate_to_nth_best_route += 1 # for the next round if associate_to_nth_best_route>\ repeated_association_with_n_routes: break first_try = False insertions_made = False ## Step 2.1: Choose a (any) route to add customers to. # some nodes may have been routed, update these eps_unrouted = eps[:, unrouted_nodes] ## Step 1.2: Associate each node to a route # note: the assignments cannot be calculated beforehand, as we do # not know which customers will be (were?) "left over" in the # previous route building steps 3. if associate_to_nth_best_route == 1 or len(route_seed_idxs) == 1: r_stars_unrouted = np.argmin(eps_unrouted[route_seed_idxs, :], axis=0) else: ## note: an extension for the deterministic variant, # get smallest AND 2. smallest at the same time using argpartition #top = np.argsort(eps, axis=0)[:associate_with_n_routes, :] #r_stars = [ top[i,:] for i in range(associate_with_n_routes) ] if len(route_seed_idxs) < associate_to_nth_best_route: route_seed_idxs = [] continue #take_nth = min(len(route_seed_idxs), associate_to_nth_best_route)-1 take_nth = associate_to_nth_best_route - 1 reorder_rows_per_col_idxs = np.argpartition( eps_unrouted[route_seed_idxs, :], take_nth, axis=0) nth_best, unrouted_node_order = np.where( reorder_rows_per_col_idxs == take_nth) r_stars_unrouted = nth_best[np.argsort(unrouted_node_order)] if choose_most_associated_route: unique, counts = np.unique(r_stars_unrouted, return_counts=True) seed_idx_idx = unique[np.argmax(counts)] route_seed_idx = route_seed_idxs[seed_idx_idx] route_seed_idxs.remove(route_seed_idx) associated_cols = list( np.where(r_stars_unrouted == seed_idx_idx)[0]) else: route_seed_idx = route_seed_idxs.popleft() associated_cols = list(np.where(r_stars_unrouted == 0)[0]) route, route_demand, route_cost, route_l_updated = routes[ route_seed_idx] ## Step 2.2: Vectorized calculation of sigma score for the customers # associated to the chosen route. eps_bar = eps_unrouted[route_seed_idx, associated_cols] # NOTE: CMT 1979 does not specify what happens if S is empty, we assume # we need (and can) omit the calculation of eps_prime in this case. brdcast_rs_idxs = [[rsi] for rsi in route_seed_idxs] if route_seed_idxs: eps_prime = np.min(eps_unrouted[brdcast_rs_idxs, associated_cols], axis=0) sigmas = eps_prime - eps_bar else: # last route, try to add rest of the nodes eps_prime = None sigmas = -eps_bar col_to_node = [unrouted_nodes[int(c)] for c in associated_cols] sigma_ls = list(zip(sigmas.tolist(), col_to_node)) sigma_ls.sort(reverse=True) if __debug__: log( DEBUG, "Assigning associated nodes %s to a route %s (seed n%d)" % (str(col_to_node), str(route + [0]), route_seeds[route_seed_idx])) ## Step 3: insert feasible customers from the biggest sigma first for sigma, l_star in sigma_ls: if __debug__: log(DEBUG-1, "Check feasibility of inserting "+\ "n%d with sigma=%.2f"%(l_star,sigma)) if C and route_demand + d[l_star] - C_EPS > C: if __debug__: log(DEBUG - 1, "Insertion would break C constraint.") continue # use cached L feasibility check if L and insertion_infeasible[route_seed_idx][l_star]: continue # Do not run TSP algorithm after every insertion, instead calculate # a simple a upper bound for the route_cost and use that. UB_route_cost = (route_cost - D[route[-1], 0] + D[route[-1], l_star] + D[l_star, 0]) if L and UB_route_cost - S_EPS > L: # check the real TSP cost new_route, new_route_cost = solve_tsp(D, route + [l_star]) if __debug__: log( DEBUG - 1, "Got TSP solution %s (%.2f)" % ( str(new_route), new_route_cost, )) if new_route_cost - S_EPS > L: if __debug__: log(DEBUG - 1, "DEBUG: Insertion would break L constraint.") insertion_infeasible[route_seed_idx][l_star] = True continue route_cost = new_route_cost route = new_route[:-1] route_l_updated = True else: route_l_updated = False route_cost = UB_route_cost route = route + [l_star] if C: route_demand += d[l_star] unrouted_nodes.remove(l_star) insertions_made = True if __debug__: log(DEBUG, "Inserted n%d to create a route %s." % (l_star, route)) # All feasible insertions of the associated customers is done, record # the modified route. if insertions_made: routes[route_seed_idx] = RouteState( route, #updated route route_demand, #updated demand route_cost, #updated cost route_l_updated) #cost state except KeyboardInterrupt: #or SIGINT rs_sol, _ = _routestates2solution(routes, D) interrupted_sol = rs_sol[:-1] + routes2sol( [n] for n in unrouted_nodes if n not in rs_sol) raise KeyboardInterrupt(interrupted_sol) ## Step 4: Redo step 1 or construct the solution and exit if len(unrouted_nodes) > 0: if __debug__: log( DEBUG, "Phase 2 failed to create feasbile solution with %d routes." % K) log(DEBUG - 1, "Nodes %s remain unrouted." % str(list(unrouted_nodes))) return 0, None, None, rr else: sol, total_cost = _routestates2solution(routes, D) if __debug__: log( DEBUG, "Phase 2 solution %s (%.2f) complete." % (str(sol), total_cost)) return K, sol, total_cost, rr