def cawlip_savings_init(D, d, C, L, minimize_K=False): """ This implements the Robbins & Turner (1979) extension to the parallel savings algorithm of Clarke and Wright (1964). Clarke and Wright algorithm and its saving function are used as is, but the savings procedure is followed by 2-opt* improvement phase, where all possible ways of connecting any two edges of the solution are searched and improving moves are accepted. Please see the parallel_savings.py:parallel_savings_init for details and description of the parameters. """ # Note: this is not a proper closure. Variables g and f are shared # over all iterations. It is OK like this, but do not use/store the # lambda after this loop. sol = parallel_savings_init(D, d, C, L, minimize_K) sol = do_local_search( [do_2opt_move], #, do_2optstar_move], sol, D, d, C, L, LSOPT.BEST_ACCEPT) return sol
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 _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 setUp(self): self.algorithms = [ ("clarke_wright_savings", lambda pts, D,d,C,L,st:\ parallel_savings_init(D,d,C,L)), ("paessens_savings_M1", lambda pts, D,d,C,L,st:\ paessens_savings_init(D,d,C,L,strategy="M1", do_3opt=False)), ("paessens_savings_M4", lambda pts, D,d,C,L,st:\ paessens_savings_init(D,d,C,L,strategy="M4", do_3opt=False)), ("clarke_wright_savings_3OPT", lambda pts, D,d,C,L,st:\ do_local_search([do_3opt_move], parallel_savings_init(D,d,C,L), D, d, C, L, operator_strategy=LSOPT.BEST_ACCEPT)), ("paessens_savings_M1_3OPT", lambda pts, D,d,C,L,st:\ paessens_savings_init(D,d,C,L,strategy="M1", do_3opt=True)), ("paessens_savings_M4_3OPT", lambda pts, D,d,C,L,st:\ paessens_savings_init(D,d,C,L,strategy="M4", do_3opt=True)) ] self.problem_names = [ "G1.vrp", "G2.vrp", "G3.vrp", "G4.vrp", "C1.vrp", "C2.vrp", "C3.vrp", "C4.vrp", "C5.vrp", "C6.vrp", "C7.vrp", "C8.vrp", "C9.vrp", "C10.vrp", "C11.vrp", "GJ1.vrp" ] # in Gaskell instances service_time was not included in the table # in Christofides et al. service_time was included self.targets = [ #cw (599 - 10 * 21, 956 - 10 * 22, 964 - 10 * 29, 841 - 10 * 32, 585, 907, 889, 834, 876, 1068, 1593, 1140, 1288, 1395, 1539, 5568), #M1 (585 - 10 * 21, 956 - 10 * 22, 938 - 10 * 29, 814 - 10 * 32, 564, 866, 866, 826, 870, 1065, 1584, 1102, 1222, 1370, 1486, 5380), #M4 (598 - 10 * 21, 956 - 10 * 22, 938 - 10 * 29, 814 - 10 * 32, 571, 889, 877, 826, 872, 1068, 1591, 1112, 1222, 1370, 1515, 5476), #cw+3opt (599 - 10 * 21, 956 - 10 * 22, 962 - 10 * 29, 840 - 10 * 32, 579, 902, 880, 824, 869, 1047, 1583, 1134, 1285, 1387, 1522, 5546), #M1+3opt (585 - 10 * 21, 956 - 10 * 22, 937 - 10 * 29, 812 - 10 * 32, 557, 861, 858, 822, 870, 1046, 1568, 1083, 1221, 1359, 1476, 5348), #M4+3opt (598 - 10 * 21, 956 - 10 * 22, 937 - 10 * 29, 812 - 10 * 32, 570, 876, 869, 823, 871, 1047, 1580, 1106, 1221, 1359, 1504, 5449), ] self.problem_path = path.join("Classic", "Paessens1988", "literature")
def main(overridden_args=None): ## 1. parse arguments parser = ArgumentParser( description= "Solve some .vrp problems with the algorithms built into VeRyPy.") parser.add_argument('-l', dest='list_algorithms', help="List the available heuristics and quit", action="store_true") parser.add_argument( '-v', dest='verbosity', help= "Set the verbosity level (to completely disable debug output, run this script with 'python -O')", type=int, default=-1) parser.add_argument( '-a', dest='active_algorithms', help= "Algorithm to apply (argument can be set multiple times to enable multiple algorithms, or one can use 'all' or 'classical')", action='append') parser.add_argument( '-b', dest='objective', choices=['c', 'cost', 'K', 'vehicles'], help="Primary optimization oBjective (default is cost)", default="cost") parser.add_argument( '-m', dest='minimal_output', help= "Overrides the output options and prints only one line CSV report per solved instance", action="store_true") parser.add_argument( '-t', dest='print_elapsed_time', help="Print elapsed wall time for each solution attempt", action="store_true") parser.add_argument( '-c', dest='show_solution_cost', help="Display solution cost instead of solution length", action="store_true") parser.add_argument('-D', dest='dist_weight_format', choices=['ROUND', 'EXACT', 'TRUNCATE'], help="Force distance matrix rounding") parser.add_argument( '-1', dest='use_single_iteration', help="Force the algorithms to use only single iteration.", action="store_true") parser.add_argument( '--iinfo', dest='print_instance_info', help="Print the instance info in the collected results", action="store_true") parser.add_argument( '--routes', dest='print_route_stat', help="Print per route statistics of the final solution", action="store_true") parser.add_argument('--vrph', dest='print_vrph_sol', help="Print the final solution in the VRPH format", action="store_true") parser.add_argument( '--forbid', dest='forbid_algorithms', help= "Forbid applying algorithms (argument can set multiple times to forbid multiple algorithms)", action='append') parser.add_argument( '--simulate', dest='simulate', help= "Do not really invoke algorithms, can be used e.g. to test scripts", action="store_true") #TODO: consider adding more LS opts e.g. 2optstart, 3optstart parser.add_argument( '--post-optimize', dest='local_search_operators', choices=['2opt', '3opt'], help= "Do post-optimization with local search operator(s) (can set multiple)", action='append') parser.add_argument( "problem_file", help= "a path of a .vrp problem file, a directory containing .vrp files, or a text file of paths to .vrp files", action='append') if overridden_args: app_args = parser.parse_args(overridden_args) elif "-l" in sys.argv: print("Select at least one algorithm (with -a) from the list:", file=sys.stderr) print(_build_algorithm_help()) sys.exit(1) elif len(sys.argv) == 1: print("Give at least one .vrp file and use -h to get help.", file=sys.stderr) sys.exit(1) else: app_args = parser.parse_args() # some further argument validation if not app_args.active_algorithms or app_args.list_algorithms: print("Select at least one algorithm (with -a) from the list:", file=sys.stderr) print(_build_algorithm_help()) exit() if len(app_args.problem_file) == 0: print("Provide at least one .vrp file to solve", file=sys.stderr) exit() # get .vrp file list files_to_solve = shared_cli.get_a_problem_file_list(app_args.problem_file) # get algorithms algos = get_algorithms(app_args.active_algorithms) if app_args.forbid_algorithms: forbidden_algos = [ algo_name_aliases[algo_name] for algo_name in app_args.forbid_algorithms if (algo_name in app_args.forbid_algorithms) ] algos = [a for a in algos if (a[0] not in forbidden_algos)] # get primary objective minimize_K = False if app_args.objective == 'K' or app_args.objective == 'vehicles': minimize_K = True run_single_iteration = False if app_args.use_single_iteration: run_single_iteration = True # get post-optimization local search move operators ls_ops = [] ls_algo_names = [] if app_args.local_search_operators: ls_algo_names = app_args.local_search_operators for ls_op_name in ls_algo_names: if ls_op_name == "2opt": ls_ops.append(do_2opt_move) if ls_op_name == "3opt": ls_ops.append(do_3opt_move) # verbosity if app_args.verbosity >= 0: shared_cli.set_logger_level(app_args.verbosity) # minimal header if app_args.minimal_output: print("algo;problem;is_feasible;f;K;t") ## 2. solve results = defaultdict(lambda: defaultdict(float)) instance_data = dict() interrupted = False for pfn in files_to_solve: bn = path.basename(pfn).replace(".vrp", "").replace(".tsp", "").replace(".pickle", "") try: N, points, dd_points, d, D, C, ewt, K, L, st = pickle.load( open(pfn, "rb")) except: N, points, dd_points, d, D, C, ewt = cvrp_io.read_TSPLIB_CVRP(pfn) K, L, st = cvrp_io.read_TSBLIB_additional_constraints(pfn) # We do not have point coodrinates, but we have D! if points is None: if dd_points is not None: points = dd_points else: points, ewt = cvrp_ops.generate_missing_coordinates(D) if app_args.dist_weight_format == "TRUNCATE": D = np.floor(D) ewt = "FLOOR_2D" if app_args.dist_weight_format == "ROUND": D = np.int(D) ewt = "EUC_2D" # Bake service time to D (if needed) D_c = cvrp_ops.D2D_c(D, st) if st else D for algo_abbreviation, algo_name, _, algo_f in algos: if not app_args.minimal_output: print("Solving %s with %s" % (bn, algo_name)) start_t = time() sol = None try: if not app_args.simulate: sol = algo_f(points, D_c, d, C, L, st, ewt, run_single_iteration, minimize_K) except (KeyboardInterrupt, Exception) as e: if type(e) is KeyboardInterrupt: interrupted = True # if interrupted on initial sol gen, return the best of those if len(e.args) > 0 and type(e.args[0]) is list: sol = e.args[0] if not app_args.minimal_output: print("WARNING: Interrupted solving %s with %s" % (bn, algo_abbreviation), file=sys.stderr) else: if not app_args.minimal_output: print("ERROR: Failed to solve %s with %s because %s" % (bn, algo_abbreviation, str(e)), file=sys.stderr) sol = None if sol: sol = cvrp_ops.normalize_solution(sol) if app_args.show_solution_cost: sol_q = cvrp_ops.calculate_objective(sol, D_c) else: sol_q = cvrp_ops.calculate_objective(sol, D) sol_K = sol.count(0) - 1 if app_args.local_search_operators: if not app_args.minimal_output: print("Postoptimize with %s ..." % ", ".join(app_args.local_search_operators), end="") sol = do_local_search(ls_ops, sol, D, d, C, L) sol = cvrp_ops.normalize_solution(sol) if app_args.show_solution_cost: ls_sol_q = cvrp_ops.calculate_objective(sol, D_c) else: ls_sol_q = cvrp_ops.calculate_objective(sol, D) if ls_sol_q < sol_q: if not app_args.minimal_output: print(" improved by %.2f%%." % (1 - ls_sol_q / sol_q)) sol_q = ls_sol_q sol_K = sol.count(0) - 1 else: if not app_args.minimal_output: print(" did not find improving moves.") else: sol_q = float('inf') elapsed_t = time() - start_t if app_args.minimal_output: print("%s;%s" % (algo_abbreviation, bn), end="") timecap_symbol = "*" if interrupted else "" if sol: feasible = all( cvrp_ops.check_solution_feasibility( sol, D_c, d, C, L, st)) print(";%s;%.2f;%d;%.2f%s" % (str(feasible), sol_q, sol_K, elapsed_t, timecap_symbol)) else: print(";False;inf;inf;%.2f%s" % (elapsed_t, timecap_symbol)) elif sol: # Minimal output is not enabled, print like crazy :) if app_args.print_elapsed_time: print("Algorithm produced a solution in %.2f s\n" % (elapsed_t)) else: #just a newline print() tightness = None if C and sol_K: tightness = (sum(d) / (C * sol_K)) if not bn in instance_data or sol_K < instance_data[bn][1]: #"N K C tightness L st" instance_data[bn] = (N, sol_K, C, "%.3f" % tightness, L, st) shared_cli.print_problem_information( points, D_c, d, C, L, st, tightness, verbosity=app_args.verbosity) solution_print_verbosity = 3 if app_args.print_route_stat else 1 shared_cli.print_solution_statistics(sol, D, D_c, d, C, L, st, solution_print_verbosity) if app_args.print_vrph_sol: print("SOLUTION IN VRPH FORMAT:") print(" ".join( str(n) for n in cvrp_io.as_VRPH_solution(sol))) print("\n") short_algo_name = algo_name results[bn][short_algo_name] = sol_q if interrupted: break # algo loop if interrupted: break # problem file loop ## Print collected results sys.stdout.flush() sys.stderr.flush() if not app_args.minimal_output and (len(results) > 1 or len(algos) > 1): print("\n") print_title = True ls_label = "+".join(ls_algo_names) for problem, algo_results in sorted(results.items()): algo_names = [ "%s+%s"%(algo_name,ls_label) if ls_algo_names else (algo_name)\ for algo_name in sorted(algo_results.keys())] if print_title: instance_fields = "instance\t" if PRINT_INSTANCE_DATA: #"N K C tightness L st" instance_fields += "N\tK*\tC\ttightness\tL\tst\t" print(instance_fields + "\t".join(algo_names)) print_title = False print(problem, end="") if PRINT_INSTANCE_DATA: print("\t", end="") print("\t".join(str(e) for e in instance_data[problem]), end="") for _, result in sorted(algo_results.items()): print("\t", result, end="") print() sys.stdout.flush() sys.stderr.flush()
def test_smoke(self): print("in", self.initial_sol) smoke_sol = do_local_search([do_3optstar_move], self.initial_sol, self.D, self.d, self.C) print("out", smoke_sol)
def paessens_savings_init(D, d, C, L, minimize_K=False, strategy="M4", do_3opt=True): """ This implements the Paesses (1988) variant of the parallel savings algorithm of Clarke and Wright (1964). The savings function of (Paesses 1988) is parametrized with multipiers g and f: S_ij = d_0i + d_0j - g * d_ij + f * | d_0i - d_0j | If two merges have the same savings value, the one where i and j are closer to another takes precedence. Otherwise the impelementation details can be read from parallel_savings.py as it contains the actual code implementing the parallel savings procedure. The variant specific parameters are: * strategy which can be: - "M1" for 143 runs of the savings algorithm with all combinations of g = np.linspace(0.8, 2.0, num=13) f = np.linspace(0.0, 1.0, num=11) - "M4" for 8 runs (g,f) = (1.0,0.1), (1.0,0.5), (1.4,0.0), (1.4,0.5) with a parameter combinations +/- 0.1 around the best of these four. - or a list of (g,f) value tuples. * do_3opt (default True) optimize the resulting routes to 3-optimality Note: Due to the use of modern computer, and low priority in computational efficiency of this implementation, not all of the tecninques specified in "reduction of computer requirements" (Paessens 1988) were employed. """ parameters = [] if strategy == "M1": parameters.extend( _cartesian_product(np.linspace(0.8, 2.0, num=13), np.linspace(0.0, 1.0, num=11))) elif strategy == "M4": parameters.extend([(1.0, 0.1), (1.0, 0.5), (1.4, 0.0), (1.4, 0.5)]) else: parameters.extend(strategy) best_params = None best_sol = None best_f = None best_K = None interrupted = False params_idx = 0 while params_idx < len(parameters): g, f = parameters[params_idx] # Note: this is not a proper closure. Variables g and f are shared # over all iterations. It is OK like this, but do not use/store the # lambda after this loop. gf_savings = lambda D: paessens_savings_function(D, g, f) sol, sol_f, sol_K = None, float('inf'), float('inf') try: sol = parallel_savings_init(D, d, C, L, minimize_K, gf_savings) if do_3opt: sol = do_local_search([do_3opt_move], sol, D, d, C, L, LSOPT.BEST_ACCEPT) # 3-opt may make some of the routes empty sol = without_empty_routes(sol) except KeyboardInterrupt as e: # or SIGINT # some parameter combination 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 best_params = (g, f) if interrupted: raise KeyboardInterrupt(best_sol) params_idx += 1 # after the best of 4 for the M4 is found, check 4 more around it if params_idx == 4 and strategy == "M4": g_prime, f_prime = best_params parameters.extend([(g_prime - M4_FINETUNE_STEP, f_prime), (g_prime + M4_FINETUNE_STEP, f_prime), (g_prime, f_prime - M4_FINETUNE_STEP), (g_prime, f_prime + M4_FINETUNE_STEP)]) return best_sol