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 = 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 = 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[c] for c in associated_cols] sigma_ls = 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