def _improve_with_3opt_star(self, problem, solution, strategy): if len(problem) == 5: N, pts, d, D, C = problem L = None else: N, pts, d, D, C, L = problem sol = list(solution) if not PRINT_ONLY_FINAL_RESULT: print("\nin", sol) total_t = 0 while True: start_t = time() out_sol = do_3optstar_move(sol, D, d, C, L, strategy) elapsed_t = time() - start_t total_t += elapsed_t if not PRINT_ONLY_FINAL_RESULT: print("elapsed %.2f s" % elapsed_t) print("out (%s)\n" % _strategy_to_str(strategy)) if out_sol[1] == None: print("no more improvements found") if PRINT_ONLY_FINAL_RESULT: print("final (%s)" % _strategy_to_str(strategy), sol, calculate_objective(sol, D)) print("total elapsed %.2f s" % total_t) break if not PRINT_ONLY_FINAL_RESULT: print(out_sol, calculate_objective(out_sol[0], D)) sol = out_sol[0] self.assertTrue(all(check_solution_feasibility(sol, D, d, C, L)), "must be feasible")
def lr3opt_init(D, d, C, L, initial_lambda1_C=None, initial_lambda1_L=None, initialization_algorithm=_init_with_tsp, postoptimize_with_3optstar=True, max_concecutive_lamba_incs=None): """ An implementation of the Stewart & Golden [1]_ 3-opt* heuristic with Lagrangean relaxation. The algorithm starts from a solution that can be either feasible or infeasible and uses local search to move towards better and feasible solutions. More specifically, it works by replacing the constraint checks of the 3-opt* with a penalty that depends on how much the constraint was violated. The 3-opt* that operates on the entire solution, that is, checks for both intra and inter route moves on one pass, was used. The penalties are iteratively doubled as the local search progresses and it is assumed that this eventually forces the solutions to feasible region. .. [1] Stewart, W. R. and Golden, B. L. (1984). A lagrangean relaxation heuristic for vehicle routing. European Journal of Operational Research, 15(1):84–88. Parameters ---------- D : numpy.ndarray is the full 2D distance matrix. d : list is a list of demands. d[0] should be 0.0 as it is the depot. C : float is the capacity constraint limit for the identical vehicles. L : float is the optional constraint for the maximum route length/duration/cost. initial_lambda1_C : float is the initial Langrange multiplier value for the capacity constraint C. If left empty (None) the formula ``l1_C=average(d)/(20*max(D))`` is used. The alternative value suggested by Stewart & Golden (1984) was 0.05. initial_lambda1_L : float is the initial Langrange multiplier value for the maximum route cost/ duration/length constraint. If left empty (None) the formula ``l1_L=average(distance to nearest neighbor)/(10*max(D))`` is used. initialization_algorithm (function): is a function that retuns a TSP or VRP solution and its objective function value. The default is to use LKH TSP solution, but the function _init_with_random can be used to replicate the results of Stewart & Golden (1984) where a random solution is used. Returns ------- list The solution as a list of node indices to visit. .. todo:: due to how the algorithm works, introducing minimize_K would require balancing between penalizing constraint violations and penalizing new routes with an additional multipiler. This was not implemented. """ sol = None try: ## STEP 1: Generate an initial solution sol, initial_f = initialization_algorithm(D, d, C, L) max_D = None lambdas = [initial_lambda1_C, initial_lambda1_L] if C and lambdas[0] == None: max_D = _get_max(D, sol) lambdas[0] = np.average(d) / (20 * max_D) if L and lambdas[1] == None: # Stewart & Golden (1984) did not propose an extension for the maximum # route duration/length/cost constraint, but here we have something # similar to L than they used for C constraint relaxation. max_D = _get_max(D, sol) if (max_D is None) else max_D closest_neighbor_D = D.copy() np.fill_diagonal(closest_neighbor_D, max_D) lambdas[1] = np.average( closest_neighbor_D.min(axis=0)) / (10 * max_D) if __debug__: log( DEBUG, "Start from initial solution %s (%.2f), and with l1=%.2f, l2=%.2f" % (sol, calculate_objective(sol, D), (0 if lambdas[0] is None else lambdas[0]), (0 if lambdas[1] is None else lambdas[1]))) checker_function = partial(_check_lr3opt_move, lambdas=lambdas) # STEP 2: Solve the relaxed problem using 3-opt* c_lambda_incs = 0 while True: # Make sure there is an empty route (for giving the 3-opt* procedure # the option of adding vehicles) while not (sol[-1] == 0 and sol[-2] == 0): sol += [0] if __debug__: log( DEBUG - 2, "Finding a LR3OPT move for %s (%.2f)" % (sol, calculate_objective(sol, D))) new_sol, delta = do_3optstar_move(sol, D, d, C, L, strategy=LSOPT.FIRST_ACCEPT, move_checker=checker_function) # local optima reached, tighten the relaxation # TODO: it should not happen that the sol==new_sol. However it happens and as a quickfix check for it. if delta is None or sol == new_sol: # return the first feasible solution (note: does not check for covering) if fast_constraint_check(sol, D, d, C, L): if __debug__: log( DEBUG, "Reached feasible solution %s (%.2f)" % (sol, calculate_objective(sol, D))) while postoptimize_with_3optstar: opt_sol, delta = do_3optstar_move( sol, D, d, C, L, strategy=LSOPT.FIRST_ACCEPT) if delta is None: return normalize_solution( sol) # remove any [0,0]'s else: sol = opt_sol #print("REMOVEME improved with post-optimization 3-opt*") log( DEBUG, "Found improving 3-opt* move leading to %s (%.2f)" % (sol, calculate_objective(sol, D))) return normalize_solution(sol) # remove any [0,0]'s else: # STEP 3: Update lambdas lambda_at_inf = False if lambdas[0] is not None: lambdas[0] = lambdas[0] * 2 lambda_at_inf = lambdas[0] == float('inf') if lambdas[1] is not None: lambdas[1] = lambdas[1] * 2 lambda_at_inf = lambda_at_inf or lambdas[0] == float( 'inf') if __debug__: log( DEBUG - 1, "No improving moves left, increasing lambda to l1=%.2f, l2=%.2f" % ((0 if lambdas[0] is None else lambdas[0]), (0 if lambdas[1] is None else lambdas[1]))) #print("No improving moves left, increasing lambda to l1=%.2f, l2=%.2f"% # ((0 if lambdas[0] is None else lambdas[0]), # (0 if lambdas[1] is None else lambdas[1]))) #TODO: if penalty >> cost, break (stuck on a infeasible region) # how much bigger can be determined by finding such a # pathological problem instance? # safeguard for getting stuck c_lambda_incs += 1 #print("REMOVEME: c_lambda_incs", c_lambda_incs) if lambda_at_inf or ( max_concecutive_lamba_incs is not None and c_lambda_incs > max_concecutive_lamba_incs): return _force_feasible(sol, D, d, C, L) else: if __debug__: log( DEBUG, "Found improving LR3OPT move leading to %s (%.2f)" % (new_sol, calculate_objective(new_sol, D))) log( DEBUG - 2, "However, routes %s remain infeasible." % [ r for r in sol2routes(new_sol) if not fast_constraint_check(r, D, d, C, L) ]) sol = new_sol c_lambda_incs = 0 except KeyboardInterrupt: # or SIGINT # Pass on the current solution forced feasbile by splitting routes # according to the constraints. raise KeyboardInterrupt(_force_feasible(sol, D, d, C, L)) return without_empty_routes(sol)
def _intra_route_3optstar_call(sol, D, strategy=LSOPT.FIRST_ACCEPT): return do_3optstar_move(sol, D, [1.0] * len(D), len(D), None, strategy)