def do_naive_local_search(ls_ops, sol, D, d, C, L=None, operator_strategy=LSOPT.FIRST_ACCEPT, max_iterations=None): """ Repeatedly apply naive ls_ops until no more improvements can be made. Optionally maximum number of iterations (of applying all ls_ops operators) can be given. """ iteration = 0 improved = True while improved: improved = False for ls_op in ls_ops: start_t = time() new_sol, delta = ls_op(sol, D, d, C, L, operator_strategy) if delta is None: continue else: improved = True elapsed_t = time() - start_t if __debug__: print("%s improved from %s (%.2f) to %s (%.2f) in %.2f s" % (ls_op.__name__, sol, objf( sol, D), new_sol, objf(new_sol, D), elapsed_t)) sol = new_sol iteration += 1 if max_iterations and iteration == max_iterations: break return sol
def call_init(points, D, d, C, L, st, wtt, single, minimize_K): if minimize_K: # todo: remove this when supprot (see TODO notes in algo desc) raise NotImplementedError("Nearest neighbor algorithm does "+ " not support minimizing the number"+ " of vehicles") sol_snn = nearest_neighbor_init(D, d, C, L, emerging_route_count=1) if single: return sol_snn auto_route_count = sol_snn.count(0)-1 # NN is so fast we can try with several K and take the best best_sol = sol_snn best_f = objf(sol_snn,D) best_K = auto_route_count for k in range(2,auto_route_count+1): sol = nearest_neighbor_init(D, d, C, L, emerging_route_count=k) sol = without_empty_routes(sol) sol_f = objf(sol,D) sol_K = sol.count(0)-1 if is_better_sol(best_f, best_K, sol_f, sol_K, minimize_K): best_sol = sol best_f = sol_f best_K = sol_K return best_sol
def setUp(self): self.size = 7 self.pts = [ (0, 0), #0 (1, 1), #1 (1, 2), #2 (1, 3), #3 (0, 4), #4 (-2, 3), #5 (-2, 2), #6 (-2, 1) ] #7 self.D = squareform(pdist(self.pts, "euclidean")) self.d = [1.0] * len(self.D) self.d[0] = 0.0 self.C = 4.0 self.L = 14.0 self.st = 2.0 self.optf_C = objf([0, 1, 2, 3, 4, 0, 5, 6, 7, 0], self.D) self.optf_L = objf([0, 3, 4, 0, 5, 6, 7, 0, 1, 2, 0], self.D) + self.st * self.size self.worstf_C = objf([0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0, 7, 0], self.D) self.worstf_L = self.worstf_C + self.st * self.size self.longMessage = True
def _refine_solution(sol, D, d, C, L, minimize_K): # refine until stuck at a local optima local_optima_reached = False while not local_optima_reached: sol = without_empty_routes(sol) if not minimize_K: sol.append(0) #make sure there is an empty route to move the pt to # improve with relocation and keep 2-optimal sol = do_local_search([do_1point_move, do_2opt_move], sol, D, d, C, L, LSOPT.BEST_ACCEPT) # try to redistribute the route with smallest demand sol = without_empty_routes(sol) routes = RouteData.from_solution(sol, D, d) min_rd = min(routes, key=lambda rd: rd.demand) routes.remove(min_rd) if not minimize_K: routes.append(RouteData()) if __debug__: log( DEBUG, "Applying do_redistribute_move on %s (%.2f)" % (str(sol), objf(sol, D))) redisribute_result = do_redistribute_move( min_rd, routes, D, d, C, L, strategy=LSOPT.FIRST_ACCEPT, #Note: Mole and Jameson do not specify exactly # how the redistribution is done (how many # different combinations are tried). # Increase the recombination_level if the # for more agressive and time consuming search # for redistributing the customers on other # routes. recombination_level=0) redisribute_delta = redisribute_result[-1] if (redisribute_delta is not None) and\ (minimize_K or redisribute_delta<0.0): updated_sol = RouteData.to_solution(redisribute_result[:-1]) if __debug__: log(DEBUG - 1, ("Improved from %s (%.2f) to %s (%.2f)" % (sol, objf(sol, D), updated_sol, objf(updated_sol, D))) + "using inter route heuristic do_redistribute_move\n") sol = updated_sol else: local_optima_reached = True if __debug__: log(DEBUG - 1, "No move with do_redistribute_move\n") return sol
def _test_ls_vs_vrhp_w_random_sol(self, vrph_heur, ls_heur, times=10, routes=1): for i in range(times): initial_sol = list(range(1, len(self.D))) shuffle(initial_sol) initial_sol = [0] + initial_sol + [0] routes_to_create = routes - 1 while routes_to_create > 0: possible_0_positions = len(initial_sol) - initial_sol.count( 0) - 2 insert_0_counter = randint(0, possible_0_positions - 1) for j in range(0, len(initial_sol) - 1): if initial_sol[j] != 0 and initial_sol[j + 1] != 0: # valid position to insert if insert_0_counter == 0: initial_sol.insert(j + 1, 0) break else: insert_0_counter -= 1 routes_to_create -= 1 #initial_sol = [0, 7, 3, 0, 4, 1, 5, 2, 6, 0] #print("initial_solution", initial_sol) #route = [max(0,int(n)-1) for n in "0-2-5-7-4-6-8-3-0".split("-")] with NamedTemporaryFile(delete=False, suffix='.vrp') as tmpfile: tsplib_file_path = tmpfile.name write_TSPLIB_file(tsplib_file_path, self.D, float_to_int_precision=1000) with NamedTemporaryFile(delete=False, suffix='.opt') as tmpfile: opt_file_path = tmpfile.name write_OPT_file(opt_file_path, self.D, initial_sol) vrph_sol = _do_vrph_ls(tsplib_file_path, opt_file_path, [vrph_heur]) ls_sol = do_local_search([ls_heur], initial_sol, self.D, self.d, self.C, LSOPT.BEST_ACCEPT) # make sure the routes are in right order vrph_sol = normalize_solution(vrph_sol) ls_sol = normalize_solution(ls_sol) print( "LS on", initial_sol, "vrph_sol = %s (%.2f)" % (str(vrph_sol), objf(vrph_sol, self.D)), "ls_sol = %s (%.2f)" % (str(ls_sol), objf(ls_sol, self.D))) self.assertEqual(vrph_sol, ls_sol) if not DEBUG_VRPH_CALL: os.remove(tsplib_file_path) os.remove(opt_file_path)
def _solve_instance(self, algo, pfn, round_D_func=None, require_K=False, predefined_k=None, suppress_constraint_check=False): N, points, dd_points, d, D, C, _ = cvrp_io.read_TSPLIB_CVRP(pfn) K, L, service_time = cvrp_io.read_TSBLIB_additional_constraints(pfn) if round_D_func: D = round_D_func(D) if predefined_k is not None: K = predefined_k if require_K and (K is None): raise IOError( "It is required that the VEHICLE field is set in %s" % pfn) if service_time: half_st = service_time / 2.0 if int(half_st) == half_st: half_st = int(half_st) service_time = int(service_time) # The service time can be modeled modifying the distance # matrix in a way that any visit to a depot node costs # service_time units. D_c = np.copy(D) D_c[1:, 1:] += service_time D_c[0, :] += half_st D_c[:, 0] += half_st np.fill_diagonal(D_c, 0.0) else: D_c = D if points is None and dd_points is not None: points = dd_points startt = time() if require_K: sol = algo(points, D_c, d, C, L, service_time, K) else: sol = algo(points, D_c, d, C, L, service_time) endt = time() elapsedt = endt - startt if __debug__: print_solution_statistics(sol, D, D_c, d, C, L, service_time) cover_ok, capa_ok, rlen_ok = check_solution_feasibility( sol, D, d, C, L, True) if not suppress_constraint_check: self.assertTrue(cover_ok, "Must be a valid solution") self.assertTrue(capa_ok, "Must not violate the C constraint") self.assertTrue(rlen_ok, "Must not violate the L constraint") return sol, objf(sol, D), objf(sol, D_c), elapsedt
def do_naive_2point_move(solution, D, d, C, L, strategy=LSOPT.BEST_ACCEPT, best_delta=None): """ This is an educational version of the inter route node exchange move (two point move). A customer is swapped with another on the same or different route if it improves the solution. """ sol_f = objf(solution, D) best_sol = None best_found = False if not best_delta: best_delta = 0 for i in range(len(solution)): n1 = solution[i] if n1 == 0: continue for j in range(1, len(solution) - 1): n2 = solution[j] if n2 == 0: continue ansatz_sol = list(solution) ansatz_sol[i], ansatz_sol[j] = ansatz_sol[j], ansatz_sol[i] # print("\nmove", i, j) # print("orig",solution ) # print("check",ansatz_sol ) # print("quality_delta", objf(ansatz_sol, D)-objf(solution, D)) # print("feasibility",fast_constraint_check(ansatz_sol,D,d,C,L) ) if fast_constraint_check(ansatz_sol, D, d, C, L): ansatz_sol_f = objf(ansatz_sol, D) delta = ansatz_sol_f - sol_f if delta + S_EPS < best_delta: #print("set as the best") best_delta = delta best_sol = ansatz_sol if strategy == LSOPT.FIRST_ACCEPT: best_found = True break if best_found: break if best_sol: return best_sol, best_delta else: return None, None
def test_2opt_one_crossed(self): route = [0, 1, 6, 2, 7, 0] initial_f = objf(route, self.D) sol, delta_f = do_2opt_move(route, self.D, LSOPT.BEST_ACCEPT) do_2opt_f = objf(sol, self.D) self.assertEqual( sol, [0, 1, 2, 6, 7, 0], "chose invalid move, initial %f, optimized %f" % (initial_f, do_2opt_f)) self.assertEqual( initial_f + delta_f, do_2opt_f, "The delta based and recalculated objective functions differ")
def _solve(self, name, desc, algof, C=None, L=None, st=None): d = self.d if C is None: d = None if st is not None: D_c = D2D_c(self.D, st) else: D_c = self.D for minimize_K in [False, True]: print( name, "(min_K=%s, C=%s, L=%s)" % (str(minimize_K), "%.1f" % C if C else "None", "%.1f" % L if L else "None")) try: sol = algof(self.pts, D_c, d, C=C, L=L, st=st, wtt="EXACT_2D", single=False, minimize_K=minimize_K) #print("done") except NotImplementedError: continue sol_f = objf(sol, D_c) print("SOLUTION %s (%.2f)" % (sol, objf(sol, D_c))) cover_ok, capa_ok, rlen_ok = check_solution_feasibility( sol, D_c, d, C, L) self.assertTrue(cover_ok, str(sol) + " is not a valid solution") self.assertTrue(capa_ok, str(sol) + " violates C constraint") self.assertTrue(rlen_ok, str(sol) + " violates L constraint") if L: self.assertGreaterEqual( sol_f, self.optf_L, "Cannot be better than the optimal solution") self.assertLessEqual( sol_f, self.worstf_L, "Must be better than the worst possible solution") else: self.assertGreaterEqual( sol_f, self.optf_C, "Cannot be better than the optimal solution") self.assertLessEqual( sol_f, self.worstf_C, "Must be better than the worst possible solution")
def test_a_tricky_move(self): route = [0, 7, 6, 4, 1, 2, 3, 5, 0] initial_f = objf(route, self.D) sol, delta_f = do_exchange_move(route, self.D, LSOPT.BEST_ACCEPT) do_2pm_f = objf(sol, self.D) sol = _normalise_route_order(sol) self.assertEqual( sol, [0, 4, 3, 2, 1, 5, 6, 7, 0], "chose invalid move, initial %f, optimized %f" % (initial_f, do_2pm_f)) self.assertAlmostEqual( initial_f + delta_f, do_2pm_f, msg="The delta based and recalculated objective functions differ")
def test_improve_random_solution_first_accept(self): special_case_fails_message = ( "NOTE: This may sometimes fail due to" + " how the naive implementation may coincidentally invert also routes" + " which are between the two routes that have their edges removed." + " Then subsequent moves may in some cases choose different equally" + " good improving moves and the final solutions differ. Both 2-opt*" " implementations still work properly.") sol = _get_random_solution(self.d, self.C) _compare_improved_from_solution(self, sol, self.D, self.d, self.C, None, [do_2optstar_move, do_2opt_move], [do_naive_2optstar_move], extra_msg=special_case_fails_message, operator_strategy=LSOPT.FIRST_ACCEPT) # Test with L constraint max_L = max(objf(r, self.D) for r in sol2routes(sol)) _compare_improved_from_solution(self, sol, self.D, self.d, self.C, max_L, [do_2optstar_move, do_2opt_move], [do_naive_2optstar_move], extra_msg=special_case_fails_message, operator_strategy=LSOPT.FIRST_ACCEPT)
def _log_after_ls_op(name, changed, routes, D): log(DEBUG - 1, "After %s (%s):" % (name, str(changed))) sol_f = 0.0 for ri, rd in enumerate(routes): log(DEBUG - 1, "Route #%d : %s" % (ri, str(list(rd.route)))) sol_f += objf(rd.route, D) log(DEBUG - 1, "Cost %.2f\n" % sol_f)
def gaskell_savings_init(D, d, C, L, minimize_K=False, savings_method="both"): """ Savings algorithm with Gaskell (1967) pi and lambda savings criteria. Uses parallel_savings.py for the savings procedure. * D is a numpy ndarray (or equvalent) of the full 2D distance matrix. * d is a list of demands. d[0] should be 0.0 as it is the depot. * C is the capacity constraint limit for the identical vehicles. * L is the optional constraint for the maximum route length/duration/cost. * minimize_K sets the primary optimization objective. If set to True, it is the minimum number of routes. If set to False (default) the algorithm optimizes for the mimimum solution/routing cost. In savings algorithms this is done by ignoring negative savings values. * savings_method selects the savings criteria: "lambda" or "pi". If set to "both" (default) the one with better results is returned. Gaskell, T. (1967). Bases for vehicle fleet scheduling. Journal of the Operational Research Society, 18(3):281-295. """ savings_functions = [] if savings_method == "both": savings_functions = [ gaskell_lambda_savings_function, gaskell_pi_savings_function ] elif savings_method == "lambda": savings_functions = [gaskell_lambda_savings_function] elif savings_method == "pi": savings_functions = [gaskell_pi_savings_function] else: raise ValueError("Only 'lambda', 'pi', or 'both' are supported.") best_sol = None best_f = None best_K = None interrupted = False for sav_f in savings_functions: sol, sol_f, sol_K = None, float('inf'), float('inf') try: sol = parallel_savings_init(D, d, C, L, minimize_K, sav_f) except KeyboardInterrupt as e: #or SIGINT # lambda or pi was interrupted if len(e.args) > 0 and type(e.args[0]) is list: sol = e.args[0] interrupted = True if sol: sol_f = objf(sol, D) sol_K = sol.count(0) - 1 if is_better_sol(best_f, best_K, sol_f, sol_K, minimize_K): best_sol = sol best_f = sol_f best_K = sol_K if interrupted: # pass on the current best_sol raise KeyboardInterrupt(best_sol) return best_sol
def do_naive_1point_move(solution, D, d, C, L, strategy=LSOPT.BEST_ACCEPT, best_delta=None): """ This is an educational version of the one point/relocate move. A customer is moved to the different position of the same or different route if it improves the solution. """ sol_f = objf(solution, D) best_sol = None best_found = False if not best_delta: best_delta = 0 for n in solution: if n == 0: continue for j in range(1, len(solution) - 1): ansatz_sol = list(solution) ansatz_sol.remove(n) ansatz_sol.insert(j, n) if fast_constraint_check(ansatz_sol, D, d, C, L): ansatz_sol_f = objf(ansatz_sol, D) delta = ansatz_sol_f - sol_f if delta + S_EPS < best_delta: best_delta = delta best_sol = ansatz_sol if strategy == LSOPT.FIRST_ACCEPT: best_found = True break if best_found: break if best_sol: return best_sol, best_delta else: return None, None
def test_2pm_updated_route_cost(self): result = self._make_improving_exchage_move() for rd in result: self.assertAlmostEqual( objf(rd[0], self.D), rd[1], msg= "original route cost + savings should match recalculated route cost" )
def _compare_improved_from_solution(testcase, sol, D,d,C,L, ls_ops, naive_ops, operator_strategy=LSOPT.BEST_ACCEPT, extra_msg=""): """ Improves the solution `sol` for the problem `D`,`d`,`C`,`L` using the local_search module operators `ls_ops` and naive implementations `naive_ops`. """ if __debug__: print("THE PROBLEM:") print("D,d,C,L =","np.%s"%repr(D),",",d,",",C,",",L) rls_ops = list(reversed(ls_ops)) # note: if given multiple operators, the best single move out of among # all moves is chosen at each step due to ITEROPT.BEST_ACCEPT. ls_sol_fwdop = do_local_search(ls_ops, sol, D, d, C, L=L, operator_strategy=operator_strategy, iteration_strategy=operator_strategy) ls_sol_rwdop = do_local_search(rls_ops, sol, D, d, C, L=L, operator_strategy=operator_strategy, iteration_strategy=operator_strategy) bf_sol = do_naive_local_search(naive_ops, sol, D, d, C, L=L, operator_strategy=operator_strategy) ls_sol_fwdop = normalize_solution(ls_sol_fwdop) ls_sol_rwdop = normalize_solution(ls_sol_rwdop) bf_sol = normalize_solution(bf_sol) if __debug__: print("\nFINAL SOLUTIONS:") print("+".join( op.__name__ for op in ls_ops),"alt 1 :", ls_sol_fwdop, "(%.2f)"%objf(ls_sol_fwdop, D)) print("+".join( op.__name__ for op in ls_ops),"alt 2 :", ls_sol_rwdop, "(%.2f)"%objf(ls_sol_rwdop, D)) print("+".join( op.__name__ for op in naive_ops),":", bf_sol, "(%.2f)"%objf(bf_sol, D)) testcase.assertTrue(all(check_solution_feasibility(ls_sol_fwdop, D, d, C, L))) testcase.assertTrue(all(check_solution_feasibility(ls_sol_rwdop, D, d, C, L))) testcase.assertTrue(all(check_solution_feasibility(bf_sol, D, d, C, L))) testcase.assertTrue(ls_sol_fwdop==bf_sol or ls_sol_rwdop==bf_sol, extra_msg)
def __init__(self, seed_customers, D, d): self.potential_insertions = [] if seed_customers: lroute = [0] + seed_customers + [0] self.route = dllist(lroute) self.cost = objf(lroute, D) self.used_capacity = sum(d[n] for n in lroute) if d else 0 else: self.route = dllist([0, 0]) self.used_capacity = 0 self.cost = 0
def test_3opt_move_s1_r2_s3(self): """ Test the 2nd 3-opt alternative (actually corresponding a 2-opt move), where the middle segment (segment2) is reversed.""" # modify the distance matrix to force a recombination move D = self.D.copy() set_weight(D, 2, 3, 100) set_weight(D, 8, 9, 100) set_weight(D, 2, 8, 1) set_weight(D, 3, 9, 1) initial_sol = self.optimum initial_sol_f = objf(initial_sol, D) sol, delta_f = self.move_op(initial_sol, D, strategy=LSOPT.BEST_ACCEPT) do_3opt_f = objf(sol, D) self.assertEqual( sol, self.segment1_head + self.segment2[::-1] + self.segment3 + self.segment1_tail) self.assertAlmostEqual(initial_sol_f+delta_f, do_3opt_f, msg=\ "The delta based and recalculated objective function values differ")
def test_3opt_move_s1_r3_s2(self): """Test the 7th and final 3-opt alternative, where the middle AND last segments (segment2 and segment3) are swapped, and the segment3 is reversed.""" # modify the distance matrix to force a recombination move D = self.D.copy() set_weight(D, 2, 3, 100) set_weight(D, 8, 9, 100) set_weight(D, 16, 17, 100) set_weight(D, 2, 16, 1) set_weight(D, 9, 3, 1) set_weight(D, 8, 17, 1) initial_sol = self.optimum initial_sol_f = objf(initial_sol, D) sol, delta_f = self.move_op(initial_sol, D, strategy=LSOPT.BEST_ACCEPT) do_3opt_f = objf(sol, D) self.assertEqual( sol, self.segment1_head + self.segment3[::-1] + self.segment2 + self.segment1_tail) self.assertAlmostEqual(initial_sol_f+delta_f, do_3opt_f, msg=\ "The delta based and recalculated objective function values differ")
def test_3opt_move_s1_r3_r2(self): """ Test the 3rd 3-opt alternative (actually corresponding a 2-opt move), where the first segment (segment1) is reversed. However, in symmetric case this is equal to reversing the order and direction of the segments 2 and 3 and this is the expected move operation here.""" # modify the distance matrix to force a recombination move D = self.D.copy() set_weight(D, 2, 3, 100) set_weight(D, 16, 17, 100) set_weight(D, 2, 16, 1) set_weight(D, 3, 17, 1) initial_sol = self.optimum initial_sol_f = objf(initial_sol, D) sol, delta_f = self.move_op(initial_sol, D, strategy=LSOPT.BEST_ACCEPT) do_3opt_f = objf(sol, D) self.assertEqual( sol, self.segment1_head + self.segment3[::-1] + self.segment2[::-1] + self.segment1_tail) self.assertEqual(initial_sol_f+delta_f, do_3opt_f, msg=\ "The delta based and recalculated objective function values differ")
def random_cmt_2phase(D, d, C, L): best_sol = None best_f = None best_lambda = None best_mu = None for i in range(RANDOM_TRALS): lambda_multiplier = 1.0 + 2.0 * random() mu_multiplier = 0.5 + 1.5 * random() #pkey = (len(D), C, L) #if pkey in stochastic_params: # lambda_multiplier, mu_multiplier = stochastic_params[pkey] # # # little wobble # mu_multiplier+=random()*0.2-0.05 # lambda_multiplier+=random()*0.2-0.05 # stochastic with SMAC # 8% gap {'pmu': 1.521411377875044, 'plambda': 3.920075418247227} sol = cmt_2phase_init(D, d, C, L, False, lambda_multiplier, mu_multiplier, phase1_seed_selection_method="first", phase2_choose_most_associated_route=False, phase2_repeated_association_with_n_routes=1, number_of_randomized_retries=RANDOMIZED_RETRIES) sol_f = objf(sol, D) if (best_sol is None) or (sol_f < best_f): print("~~~~", "updating the best", "~~~~") best_sol = sol best_f = sol_f best_lambda = lambda_multiplier best_mu = mu_multiplier print("Best parameters for the cmt_2phase after %d trials" % RANDOM_TRALS, "lambda=%.2f" % best_lambda, "mu=%.2f" % best_mu) return best_sol
def tsp_cli(tsp_f_name, tsp_f): # import here so that the function can be used without these dependencies from util import objf if len(sys.argv)==2 and path.isfile(sys.argv[1]): P = cvrp_io.read_TSPLIB_CVRP(sys.argv[1]) D = P.distance_matrix start_t = time() tsp_sol, tsp_f = tsp_f(D, list(range(len(D)))) elapsed_t = time()-start_t print("Solved %s with %s in %.2f s"%(path.basename(sys.argv[1]), tsp_f_name, elapsed_t)) tsp_o = objf(tsp_sol,D) print("SOLUTION:", str(tsp_sol)) print("COST:", tsp_o) assert(tsp_f==tsp_o) else: print("usage: tsp_solver_%s.py TSPLIB_file.tsp"%tsp_f_name, file=sys.stderr)
def solve_tsp_ropt(D, selected_idxs, do_shuffle=False, do2opt=True, do3opt=True): # r-Opt (r \in {2,3} ) endp = selected_idxs[0] if do_shuffle: shuffled_idxs = list(selected_idxs[1:]) shuffle(shuffled_idxs) new_route = [endp] + shuffled_idxs + [endp] elif selected_idxs[-1] != endp: new_route = selected_idxs + [endp] else: new_route = selected_idxs new_route_cost = objf(new_route, D) # make first 2-optimal if do2opt: improved = True while improved: improved = False improved_route, delta = do_2opt_move(new_route, D, 1) if improved_route is not None: new_route = improved_route new_route_cost += delta improved = True # then 3-optimal (do not waste time on "easy" 2-opt # operations if the route has already been made 2-optimal if do3opt: improved = True while improved: improved = False improved_route, delta = do_3opt_move(new_route, D, 1) if improved_route is not None: new_route = improved_route new_route_cost += delta improved = True return new_route, new_route_cost
def beasley_rfcs_init(D, d, C, L, trials): random_2opt_tsp_sol = lambda D, nodes: solve_tsp_ropt( D, nodes, do_shuffle=True, do2opt=True, do3opt=False) best_sol = None best_f = None for t in range(trials): # 2-opt it sol = route_first_cluster_second_init(D, d, C, L, minimize_K=False, tsp_gen_algo=random_2opt_tsp_sol) sol_f = objf(sol, D) if (best_sol is None) or (sol_f < best_f): best_sol = sol best_f = sol_f return best_sol
def test_verify_reference_solutions_FosterRyan1976_instances(self): for problem_idx, problem_name in enumerate(self.problem_names): ref_k, ref_f = self.targets[1][problem_idx] if problem_name == r"04-CW64_n30a_k8c.vrp": problem_name = r"04-CW64_n31_k9c.vrp" ref_f = 1377 pfn = path.join(BENCHMARKS_BASEPATH, self.problem_path, problem_name) N, points, dd_points, d, D, C, _ = cvrp_io.read_TSPLIB_CVRP(pfn) K, L, service_time = cvrp_io.read_TSBLIB_additional_constraints( pfn) if service_time: D_c = D2D_c(D, service_time) else: D_c = D ref_sol = self.target_solutions[problem_idx] ref_sol_f = int(objf(ref_sol, D_c)) ref_sol_k = ref_sol.count(0) - 1 cover_ok, capa_ok, rlen_ok = check_solution_feasibility( ref_sol, D, d, C, L, True) self.assertTrue(cover_ok, "Must be a valid solution") self.assertTrue(capa_ok, "Must not violate the C constraint") self.assertTrue(rlen_ok, "Must not violate the L constraint") self.assertEqual( ref_k, ref_sol_k, msg= ("The appendix solution route count differs from the one given " + "in Table 2 for %s (%d vs %d)" % (problem_name, ref_sol_k, ref_k))) self.assertAlmostEqual( ref_f, ref_sol_f, msg=("The appendix solution result differs from the one given " + "in Table 2 for %s : %d (ours) vs %d (theirs)" % (problem_name, ref_sol_f, ref_f)))
def test_improve_random_solution_first_accept(self): special_case_fails_message = ("NOTE: This may sometimes fail due to"+ " how the naive implementation checks first all insertion positions"+ " for one customer before moving on to next, while the local search"+ " Implementation checks all intra route moves first and then the"+ " inter route moves. If there is equally good move in intra and inter"+ " route moves, it may be that both implementations do not select the"+ " same one.") sol = _get_random_solution(self.d,self.C) _compare_improved_from_solution( self, sol, self.D,self.d,self.C,None, [do_relocate_move, do_1point_move], [do_naive_1point_move], extra_msg=special_case_fails_message, operator_strategy=LSOPT.FIRST_ACCEPT) # Test with L constraint max_L = max( objf(r,self.D) for r in sol2routes(sol) ) _compare_improved_from_solution( self, sol, self.D,self.d,self.C,max_L, [do_relocate_move, do_1point_move], [do_naive_1point_move], extra_msg=special_case_fails_message, operator_strategy=LSOPT.FIRST_ACCEPT)
def print_solution_statistics(sol, D, D_cost, d, C, L=None, service_time=None, verbosity=-1): print("\nSOLUTION:", sol) cover_ok, capa_ok, rlen_ok = cvrp_ops.check_solution_feasibility( sol, D_cost, d, C, L, True) if verbosity > 1: print("ALL SERVED:", cover_ok) if C: print("IS C FEASIBLE:", capa_ok) if L: print("IS L FEASIBLE:", rlen_ok) else: print("FEASIBLE:", cover_ok and capa_ok and rlen_ok) print("SOLUTION K:", sol.count(0) - 1) sol_f = None if D is None else objf(sol, D) sol_c = None if D_cost is None else objf(sol, D_cost) if (verbosity > 0 and sol_f != sol_c) or (not sol_c): print("SOLUTION COST:", sol_c, "\n") if sol_c: print("SOLUTION LENGTH:", sol_f) if verbosity > 1: routes = sol2routes(sol) print("ROUTES:") print("No.\tCost\tLength\tLoad\tRoute") for i, route in enumerate(routes): print(i + 1, "%.2f" % objf(route, D_cost), "%.2f" % objf(route, D), sum((d[n] for n in route)) if C else "-", route, sep='\t') print("Total", "%.2f" % objf(sol, D_cost), "%.2f" % objf(sol, D), sep='\t')
def mbsa_init(D, d, C, L, minimize_K=False, W=0.0, solve_tsp=default_solve_tsp, primary_criteria_callback=_calculate_savings, secondary_criteria_callback=_calculate_secondary_criteria): """ An implementation of Desrochers & Verhoog (1989) Matching Based Savings Algortihm. It uses Gurobi to solve the maxumum matching problem (MMP) and the TSP. The routes are merged according to the MMP until no valid merges remain. The parameters for this implementation are: * D is a numpy ndarray (or equvalent) of the full 2D distance matrix. * d is a list of demands. d[0] should be 0.0 as it is the depot. * C is the capacity constraint limit for the identical vehicles. * L is the optional constraint for the maximum route length/duration/cost. * minimize_K sets the primary optimization objective. If set to True, it is the minimum number of routes. If set to False (default) the algorithm optimizes for the mimimum solution/routing cost. In savings algorithms this is done by ignoring negative savings values. * W is the savings cost assigned to infeasible merges. One may need to set this to a large negative value if minimize_K is true. * solve_tsp which TSP solver is used when calculating the savings values * primary_criteria_callback and secondary_criteria_callback can be changed from the defaults to use different savings criteria. See the reference implementations for details on the fucntion signatures. Desrochers, M. and Verhoog, T. (1989). G-89-04 : A matching based savings algorithm for the vehicle routing problem. Technical report, GERAD, Montreal, Canada. """ N = len(D) tsp_cache = {} demand_cache = {} savings = [] ignore_negative_savings = not minimize_K ## Step 0: initalization # serve each customer with a single route route_sets = [frozenset([0, i]) for i in range(1, N)] try: # calculate initial savings or merging *routes* i and j savings = [(primary_criteria_callback(rs1, rs2, demand_cache, tsp_cache, W, D, d, C, L, solve_tsp), (i, j)) for i, rs1 in enumerate(route_sets) for j, rs2 in enumerate(route_sets) if i < j] while True: ## Step 1: Evaluate the weights and solve the weighted matching prolbem w_ij, x_ij_keys = zip(*savings) valid_matchings = any(w for w in w_ij if w != W) if valid_matchings == 0: break s_ij = None if secondary_criteria_callback: s_ij = secondary_criteria_callback(w_ij, x_ij_keys, len(route_sets)) best_matching = _mmp_solve(w_ij, x_ij_keys, len(route_sets), s_ij) if best_matching is None: break # no valid matchings found best_savings, i_prime, j_prime = best_matching if best_savings == W: break if ignore_negative_savings and best_savings < 0: continue # do not allow merges that would make the solution worse # (even if it would mean fewer routes) if __debug__: log( DEBUG, "Best matching joins routes %s (idx:%d) and %s (idx:%d)." % (tsp_cache[route_sets[i_prime]][0], i_prime, tsp_cache[route_sets[j_prime]][0], j_prime)) ## Step 2: merge the route combination # merge routes i and j new_i_prime_set = route_sets[i_prime].union(route_sets[j_prime]) ## Step 3: Update savings ## Filter out all edges adjacent to i' and j' # select only the savings that *do not* contain routes i and j # and while at it, update the indexing savings = [(sa, (si if si < j_prime else si - 1, sj if sj < j_prime else sj - 1)) for sa, (si, sj) in savings if (si != i_prime and sj != i_prime and si != j_prime and sj != j_prime)] del route_sets[j_prime] if i_prime > j_prime: i_prime = i_prime - 1 route_sets[i_prime] = new_i_prime_set # Evaluate the savings associated with the new route rs1 = route_sets[i_prime] merged_route_savings = [ (primary_criteria_callback(rs1, rs2, demand_cache, tsp_cache, W, D, d, C, L, solve_tsp), (i_prime, k)) for k, rs2 in enumerate(route_sets) if k != i_prime ] savings.extend(merged_route_savings) if __debug__: dbg_r, dbg_w = _get_tsp_sol(rs1, tsp_cache, D, solve_tsp) log( DEBUG - 1, "Route#%d after merge %s (%.2f)" % (i_prime, str(dbg_r), objf(dbg_r, D))) except KeyboardInterrupt: #or SIGINT interrupted_sol = _geedy_merge(D, d, C, L, W, savings, route_sets, tsp_cache, demand_cache) raise KeyboardInterrupt(interrupted_sol) # the optimized TSP tours for the routes are cached, reuse final_routes = [ _get_tsp_sol(rs, tsp_cache, D, solve_tsp)[0] for rs in route_sets ] sol = [0] + [n for route in final_routes for n in route[1:]] return sol
def _geedy_merge(D, d, C, L, W, savings, route_sets, tsp_cache, demand_cache): """ This is not the part of the main algorithm proper but a greedy fallback the procedure relies on in case of an interrupt. It simply uses the existing savings list and caches to create solution with minimal amount of computation (it is an O(n) algorithm at this point). """ savings.sort(reverse=True) try: # Do a first pass where the routes in the tsp_cache are used first_pass_joined_routes = set() for saving, route_ids in savings: if saving == W: break # from now one all are infeasible route_i, route_j = route_ids if route_i in first_pass_joined_routes or\ route_j in first_pass_joined_routes: continue # either route is already merged nodes_i = route_sets[route_i] nodes_j = route_sets[route_j] nodes_ij = nodes_i.union(nodes_j) if nodes_ij not in tsp_cache: greedy_routing = [0] + list(nodes_ij) + [0] greedy_f = objf(greedy_routing, D) if not L or greedy_f - S_EPS < L: # remember for the next stage tsp_cache[nodes_ij] = (greedy_routing, greedy_f) continue # all in tsp_cache are feasible # record and do the merge first_pass_joined_routes.add(route_i) first_pass_joined_routes.add(route_j) route_sets[route_i] = nodes_ij route_sets[route_j] = route_i # mark as a reference to the another # the second stage just smash routes together (in all possible 4-ways) for saving, route_ids in savings: # follow the merge path (if any) route_i, route_j = route_ids while isinstance(route_sets[route_i], int): route_i = route_sets[route_i] while isinstance(route_sets[route_j], int): route_j = route_sets[route_j] if route_i == route_j: continue #already merged # get nodes, TSP chain, demand, and lenght for both routes nodes_i = route_sets[route_i] nodes_j = route_sets[route_j] chain_i = tsp_cache[nodes_i][0][1:-1] l_i = tsp_cache[nodes_i][1] d_i = _get_demand(nodes_i, demand_cache, d) chain_j = tsp_cache[nodes_j][0][1:-1] l_j = tsp_cache[nodes_j][1] d_j = _get_demand(nodes_j, demand_cache, d) # check constraints if C and d_i + d_j - C_EPS > C: continue na, nb, nc, nd = chain_i[0], chain_i[-1], chain_j[0], chain_j[-1] alt_costs = [ l_i + l_j + D[nb, nc] - D[nb, 0] - D[nc, 0], #ab-cd l_i + l_j + D[na, nc] - D[na, 0] - D[nc, 0], #ba-cd l_i + l_j + D[nb, nd] - D[nb, 0] - D[nd, 0], #ab-dc l_i + l_j + D[na, nd] - D[na, 0] - D[nd, 0] ] #cd-ab min_cost = min(alt_costs) min_alt = alt_costs.index(min_cost) if L and min_cost - S_EPS > L: continue # find the best merge among the 4-way merges and apply it if min_alt == 0: ij_route = [0] + chain_i + chain_j + [0] #ab-cd if min_alt == 1: ij_route = [0] + chain_i[::-1] + chain_j + [0] #ba-cd if min_alt == 2: ij_route = [0] + chain_i + chain_j[::-1] + [0] #ab-dc if min_alt == 3: ij_route = [0] + chain_j + chain_i + [0] #cd-ab nodes_ij = nodes_i.union(nodes_j) route_sets[route_i] = nodes_ij route_sets[route_j] = route_i # mark as a reference to the another tsp_cache[nodes_ij] = (ij_route, min_cost) except KeyboardInterrupt: #or SIGINT pass greedy_solution = [] for nodes in route_sets: if isinstance(nodes, int): continue greedy_solution += tsp_cache[nodes][0][:-1] greedy_solution.append(0) return greedy_solution
def wren_holliday_init(points, D, d, C, L=None, minimize_K=False, seed_node=BEST_OF_FOUR, direction='both', full_convergence=True): """ This implements the Wren and Holliday improvement heuristic. The initial solution is generated using the generic sweep procedure of `sweep.py`, and the improvement procedure works as specified in Wren & Holliday (1972) Fig 1 (Flowchart of program). Basically, the refining processes, also known as local search improvement / post-optimization phase, repeadedly applies local search operators to improve the initial solutions and returns the best one. * D is a numpy ndarray (or equvalent) of the full 2D distance matrix. * d is a list of demands. d[0] should be 0.0 as it is the depot. * C is the capacity constraint limit for the identical vehicles. * L is the optional constraint for the maximum route cost/duration/length. * seed_node sets how many different seed nodes are tried for the Sweep based initial solution generation. If LEAST_DENSE, the sweep is started from the direction from the depot that has lowest customer density. The same applies to BEST_OF_FOUR (default), but also 3 other directions spaced by ~90deg are considered. Also a complete (but costly) generation of all possible initial solutions with BEST_ALTERNATIVE can be used. * direction can be 'ccw' for counter-clockwise Sweep, 'cw' (default) for clockwise or 'both' for trying both. * full_convergence determines if the improvement is stopped after the "delete" operation fails the first time (False) or if the local search continues until no operation is capable of finding an improving more (True, default). Returns the solution. Wren, A., & Holliday, A. (1972). Computer scheduling of vehicles from one or more depots to a number of delivery points. Journal of the Operational Research Society, 23(3), 333-344. """ if not points: raise ValueError( "The algorithm requires 2D coordinates for the points") N = len(D) # Calculate the sweep coordinates # this is 99% same as _get_sweep..., but we need to get also # the node_phis for later use, so duplicated some code here. np_pts = points if isinstance(points, np.ndarray) else np.asarray(points) depot_x, depot_y = points[0] node_rhos, node_phis = cart2pol(np_pts[:, 0] - depot_x, np_pts[:, 1] - depot_y) sweep = get_sweep_from_polar_coordinates(node_rhos, node_phis) sweep_phis = sweep[0] directions = ['cw', 'ccw'] if direction == 'both' else [direction] sweeps = [] for cur_dir in directions: if seed_node == BEST_OF_FOUR or seed_node == LEAST_DENSE: # Wren & Holliday method of selecting starting customers for the # Sweep initialization from the "least dense direction". # Turn the polar coordinates so that the smallest angles are to the # direction of the smallest density, weighted by their demands. o = np.array([depot_x, depot_y]) if d: weighted_xy_from_origin = np.multiply( np_pts[1:] - o, np.transpose(np.array([d[1:], d[1:]]))) avgx, avgy = np.average(weighted_xy_from_origin, axis=0) else: avgx, avgy = np.average(np_pts[1:] - o, axis=0) avg_rho, avg_phi = cart2pol(np.array([avgx]), np.array([avgy])) # to range [-pi, pi] if avg_phi > 0: least_dense_phi = avg_phi[0] - pi else: least_dense_phi = avg_phi[0] + pi # evenly spaced by pi/2 for i in range(4): angle_tgt = least_dense_phi + i * pi / 2 if angle_tgt > pi: angle_tgt -= pi * 2 # take the first satisfying the condition start_from_here = np.argmax(sweep_phis > angle_tgt) start_node = start_from_here-1 if cur_dir=="cw" \ else start_from_here if start_node == -1: start_node = N - 2 sweeps.append((start_node, cur_dir)) if seed_node == LEAST_DENSE: break # do not take the ones at 90 deg intervals elif seed_node == BEST_ALTERNATIVE: nodes = list(range(N - 1)) sweeps.extend(zip(nodes, [cur_dir] * len(nodes))) elif type(seed_node) == int: sweeps.append([seed_node, cur_dir]) ## PHASE 1 : "Generate ... initial solutions and choose the best" initial_sols = [] try: for start_node, cur_dir in sweeps: isol = sweep_init(sweep, D, d, C, L, seed_node=[start_node], direction=cur_dir, routing_algo=None) initial_sols.append(isol) except KeyboardInterrupt as e: # or SIGINT # if interrupted on initial sol gen, return the best of those if len(e.args) > 0 and type(e.args[0]) is list: initial_sols.append(e.args[0]) if not initial_sols: raise e else: best_isol, best_if, best_iK = None, float('inf'), float('inf') for isol in initial_sols: isol_f = objf(isol, D) isol_K = isol.count(0) - 1 if is_better_sol(best_if, best_iK, isol_f, isol_K, minimize_K): best_isol = isol best_if = isol_f best_iK = isol_K raise KeyboardInterrupt(best_isol) best_sol, best_f, best_K = None, float('inf'), float('inf') interrupted = False for sol in initial_sols: # Construct an array of RouteData objects for local search improvement # heuristics to use routes = RouteData.from_solution(sol, D, d) if __debug__: log(DEBUG, "Improving solution %s (%.2f)" % (sol, objf(sol, D))) _log_after_ls_op("SWEEP(S)", False, routes, D) ## PHASE 2 : Improvement phase (see Wren & Holliday 1974, Figure 1) # Variables storing the state converging = False deleted = True omitted_nodes = set() prev_Q_point_sol_f = None prev_iteration_sol_f = None changed = False try: while True: _remove_empty_in_place(routes) if not minimize_K: # +1 empty route can allow local search to find an improvement routes.append(RouteData()) changed = False ## "INSPECT, SINGLE" ## # run 2opt on each route to remove any crossing edges inspect_improved = inspect_heuristic(routes, D, C, d, L) if inspect_improved and minimize_K: _remove_empty_in_place(routes) changed |= inspect_improved if __debug__: _log_after_ls_op("INSPECT", changed, routes, D) # move a node to a better position on the route or other routes single_improved = single_heuristic(routes, D, C, d, L) if single_improved and minimize_K: _remove_empty_in_place(routes) changed |= single_improved if __debug__: _log_after_ls_op("SINGLE", changed, routes, D) ## "Are customers omitted?" ## omitted_were_assigned = False if omitted_nodes: inserted_nodes = set() ## "Take omitted ... in order and try to fit into existing routes" ## for node in sorted(list(omitted_nodes)): for rdi, rd in enumerate(routes): _, new_rd, delta = do_insert_move( node, rd, D, d, C, L, LSOPT.BEST_ACCEPT) if delta is not None: routes[rdi] = new_rd inserted_nodes.add(node) omitted_were_assigned = True omitted_nodes -= inserted_nodes if omitted_were_assigned and minimize_K: _remove_empty_in_place(routes) changed |= omitted_were_assigned if __debug__: _log_after_ls_op("INSERT", changed, routes, D) ## "Are customers omitted still?" ## if omitted_nodes: omitted_were_assigned |= complain_heuristic( routes, omitted_nodes, D, C, d, L) if omitted_were_assigned and minimize_K: _remove_empty_in_place(routes) changed |= omitted_were_assigned if __debug__: _log_after_ls_op("COMPLAIN", changed, routes, D) sol_f = 0 for rd in routes: sol_f += rd.cost ## Q-point : "Has distance been reduced by more that 5% OR # has a previously omitted customer been assigned?" ## if (prev_Q_point_sol_f is None) or\ (sol_f<prev_Q_point_sol_f*0.95) or \ omitted_were_assigned: prev_Q_point_sol_f = sol_f converging = False continue else: prev_Q_point_sol_f = sol_f converging = True ## "Is problem small?" -> PAIR ## if len(D) <= 80: pair_improved = pair_heuristic(routes, D, C, d, L) if pair_improved and minimize_K: _remove_empty_in_place(routes) changed |= pair_improved if __debug__: _log_after_ls_op("PAIR", changed, routes, D) ## "Is deleted true?" ## if deleted: # "DELETE" -> "Was delete succesful?" ## deleted = delete_heuristic(routes, D, C, d, L) if deleted and minimize_K: _remove_empty_in_place(routes) changed |= deleted if __debug__: _log_after_ls_op("DELETE", changed, routes, D) ## DISENTANGLE ## disentangle_improved = disentangle_heuristic( routes, sweep, node_phis, D, C, d, L) if disentangle_improved and minimize_K: _remove_empty_in_place(routes) changed |= disentangle_improved if __debug__: _log_after_ls_op("DISENTANGLE", changed, routes, D) ## "Has situation changed in interation?" ## solution_changed_between_iterations = True if prev_iteration_sol_f: if prev_iteration_sol_f == sol_f: solution_changed_between_iterations = False prev_iteration_sol_f = sol_f if converging and ((full_convergence and not changed) or (not full_convergence and not deleted) or (not solution_changed_between_iterations)): ## STOP ## break except KeyboardInterrupt: interrupted = True # return the optimized solutios sol = [0] + [n for r in routes for n in r.route[1:]] # LS may cause empty routes sol = without_empty_routes(sol) sol_K = sol.count(0) - 1 sol_f = objf(sol, D) if __debug__: log(DEBUG, "Improved solution %s (%.2f)" % (sol, sol_f)) if is_better_sol(best_f, best_K, sol_f, sol_K, minimize_K): best_sol = sol best_f = sol_f best_K = sol_K if interrupted: raise KeyboardInterrupt(best_sol) return best_sol