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 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 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
def gap_init(points, D, d, C, L=None, st=None, K=None, minimize_K=True, find_optimal_seeds=True, seed_method="cones", seed_edge_weight_type='EUC_2D', use_adaptive_L_constraint_weights=True, increase_K_on_failure=False): #REMOVEME, disable! #increase_K_on_failure=True): """ An implementation of a three phase cluster-first-route-second CVRP construction / route initialization algorithm. The first two phases involve the clustering. First, a seed point is generated for each route, which is then used in approximating customer node service costs in solving generalized assignment problem (GAP) relaxation of the VRP. The resulting assignments are then routed using a TSP solver. The algorithm has been first proposed in (Fisher and Jaikumar 1981). The algorithm assumes that the problem is planar and this implementation allows seed in two ways: * seed_method="cones", the initialization method of Fisher and Jaikumar (1981) which can be described as Sweep with fractional distribution of customer demand and placing the seed points approximately to the center of demand mass of created sectors. * seed_method="kmeans", intialize seed points to k-means cluster centers. * seed_method="large_demands", according to Fisher and Jaikumar (1981) "Customers for which d_i > 1/2 C can also be made seed customers". However applying this rule relies on human operator who then decides the intuitively best seed points. This implementation selects the seed points satisfying the criteria d_i>mC, where m is the fractional capacity multipier, that are farthest from the depot and each other. The m is made iteratively smaller if there are no at least K seed point candidates. * seed_method="ends_of_thoroughfares", this option was descibed in (Fisher and Jaikumar 1981) as "Most distant customers at the end of thoroughfares leaving from the depot are natural seed customers". They relied on human operator. To automate this selection we make a DBSCAN clustering with eps = median 2. nearest neighbor of all nodes and min_samples of 3. The other parameters are: * points is a list of x,y coordinates of the depot [0] and the customers. * D is a numpy ndarray (or equvalent) of the full 2D distance matrix. including the service times (st/2.0 for leaving and entering nodes). * 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. * st is the service time. However, also the D should be modified with service times to allow straight computation of the TSP solutions (see above) * K is the optional parameter specifying the required number of vehicles. The algorithm is only allowed to find solutions with this many vehicles. * minimize_K, if set to True (default), makes the minimum number of routes the primary and the solution cost the secondary objective. If set False the algorithm optimizes for mimimum solution / route cost by increasing K as long as it seems beneficial. WARNING: the algorithm suits this use case (cost at the objective) poorly and setting this option to False may significantly increase the required CPU time. * find_optimal_seeds if set to True, tries all possible Sweep start positions / k-Means with N different seeds. If False, only one sweep from the node closest to the depot is done / k-Means clustering is done only once with one random seed value. * seed_edge_weight_type specifies how to round off the distances from the customer nodes (points) to the seed points. Supports all TSPLIB edge weight types. Note1: The GAP is optimized using Gurobi solver. If L constraint is set, the side constraints may make the GAP instance tricky to solve and it is advisable to set a sensible timeout with config.MAX_MIP_SOLVER_RUNTIME * use_adaptive_L_constraint_weights if set True, and the L constraint is set, the algorithm adaptively adjusts the route cost approximation of the relevant side constraints so that a solution which is not L infeasible or GAP infeasible is found. The exact handling of L consraint is vague in (Fisher and Jaikumar 1981) and this was our best guess on how the feasible region of the problem can be found. Note that if GAP solver is terminated due to a timeout, the adaptive multipier is increased and GAP solution is attempted again. However, if increase_K_on_failure is set, (see below) it takes priority over this. * increase_K_on_failure (default False) is another countermeasure against long running GAP solving attempts for problem instances without L constraint (if there is L constraint, and use_adaptive_L_constraint_- weights is enabled, this is ignored) or instances where K estimation does not work and it takes excessively long time to check all initial seed configurations before increasing K. If Gurobi timeout is encountered or the solution is GAP infeasible, and this option is enabled, the K is temporately increased, new seeds points generated for current sweep start location and another GAP solution attempt is made. K is allowed to increased temporarely up to 10% of the mimimum K allowed (or 1, whichever is larger). Note2: logger controls the debug level but running the script with Python -O option disables all debug output. Fisher, M. L. and Jaikumar, R. (1981), A generalized assignment heuristic for vehicle routing. Networks, 11: 109-124. doi:10.1002/net.3230110205 """ #TODO: other alternatives # customers with maximum demand or most distant customer from origin if seed_method == "cones": seed_f = _sweep_seed_points if seed_method == "kmeans": seed_f = _kmeans_seed_points if seed_method == "large_demands": if not C: raise ValueError( """The "large_demands" seed initialization method requires demands and C constraint to be known.""" ) seed_f = _large_demand_seed_points if seed_method == "ends_of_thoroughfares": seed_f = _end_of_thoroughfares_seed_points int_dists = issubclass(D.dtype.type, np.integer) if seed_edge_weight_type == "EXPLICIT": seed_edge_weight_type = "EUC_2D" if int_dists else "EXACT_2D" if not points: raise ValueError( "The algorithm requires 2D coordinates for the points") N = len(D) if K: startK = K maxK = K else: # start from the smallest K possible if C: startK = int(ceil(sum(d) / C)) elif L: # find a lower bound by checking how many visits from the TSP # tour need to add to have any chance of making this L feasible. _, tsp_f = solve_tsp(D, range(1, N)) shortest_depot_edges = list(D[0, 1:]) shortest_depot_edges.sort() startK = int(ceil(tsp_f / L)) while True: if tsp_f + sum( shortest_depot_edges[:startK * 2]) <= startK * L: break startK += 1 else: raise ValueError("If C and L have not been set, K is required") maxK = N - 1 # We only need first row of the distance matrix to calculcate insertion # costs for GAP objective function D_0 = np.copy(D[0, :]) best_sol = None best_f = None best_K = None seed_trial = 0 incK = 0 maxKinc = max(startK + 1, int(startK * INCREASE_K_ON_FAILURE_UPTO)) L_ctr_multipiler = L_MPLR_DEFAULT if L and use_adaptive_L_constraint_weights: # Adaptive L constraint multipier L_ctr_multipiler = L_ADAPTIVE_MPLR_INIT L_ctr_multipiler_tries = 0 try: for currentK in range(startK, maxK + 1): found_improving_solution_for_this_K = False seed_trial = 0 while True: if __debug__: log( DEBUG, "ITERATION:K=%d, trial=%d, L_ctr_mul=%.6f\n" % (currentK + incK, seed_trial, L_ctr_multipiler)) log(DEBUG - 1, "Getting %d seed points...\n" % (currentK + incK)) # Get seed points seed_points = seed_f(points, D, d, C, currentK + incK, seed_trial) if __debug__: log(DEBUG - 1, "...got seed points %s\n" % str(seed_points)) # Extend the distance matrix with seed distances S = calculate_D(seed_points, points, seed_edge_weight_type) if st: # include the "leaving half" of the service_time in the # distances (the other half is already added to the D # prior to gapvrp_init) halftst = int(st / 2) if int_dists else st / 2.0 S[:, 1:] += halftst D_s = np.vstack((D_0, S)) GAP_infeasible = False L_infeasible = False solution = [0] sol_f = 0 solved = False sol_K = 0 take_next_seed = False try: # Distribute the nodes to vehicles using the approxmate # service costs in D_s and by solving it as GAP # #TODO: the model has the same dimensions for all iterations # with the same K and only the weights differ. Consider # replacing the coefficient matrix e.g. via C interface #https://stackoverflow.com/questions/33461329 assignments = _solve_gap(N, D_s, d, C, currentK + incK, L, L_ctr_multipiler) if not assignments: if __debug__: log(DEBUG, "INFEASIBILITY: GAP infeasible solution") corrective_action = "try with another seed = %d" % seed_trial GAP_infeasible = True else: if __debug__: log(DEBUG - 1, "Assignments = %s" % str(assignments)) # Due to floating point inaccuracies in L constrained # cases the feasrelax may be used, which, in turn, can # in some corner cases return solutions that are not # really feasible. Make sure it is not the case if L: served = set([0]) for route_nodes in assignments: if not route_nodes: continue route, route_l = solve_tsp(D, [0] + route_nodes) # Check for feasibility violations due to feasrelax if L: served |= set(route_nodes) if C and d and totald(route, d) - C_EPS > C: if __debug__: log( DEBUG, "INFEASIBILITY: feasRelax " + "caused GAP infeasible solution " + " (capacity constraint violation)") GAP_infeasible = True break # the route loop solution += route[1:] sol_f += route_l sol_K += 1 if __debug__: log( DEBUG - 2, "DEBUG: Got TSP solution %s (%.2f)" % (str(route), route_l)) if L and route_l - S_EPS > L: if __debug__: log( DEBUG, "INFEASIBILITY: L infeasible solution") L_infeasible = True break # break route for loop # Check for feasibility violations due to feasrelax. # Have all customers been served? if not GAP_infeasible and not L_infeasible and\ L and len(served)<len(D): if __debug__: log( DEBUG, "INFEASIBILITY: feasRelax caused GAP " + "infeasible solution (all customers " + "are not served)") GAP_infeasible = True if not GAP_infeasible and not L_infeasible: if __debug__: log( DEBUG, "Yielded feasible solution = %s (%.2f)" % (str(solution), sol_f)) solved = True except GurobiError as grbe: if __debug__: log(WARNING, str(grbe)) if L and use_adaptive_L_constraint_weights and \ L_ctr_multipiler_tries<L_ADAPTIVE_MPLR_MAX_TRIES: L_ctr_multipiler += L_ADAPTIVE_MPLR_INC L_ctr_multipiler_tries += 1 if __debug__: corrective_action = "Gurobi timeout, try with another L_ctr_multipiler = %.2f" % L_ctr_multipiler elif increase_K_on_failure and currentK + incK + 1 <= maxKinc: if L and use_adaptive_L_constraint_weights and\ L_ctr_multipiler_tries>=L_ADAPTIVE_MPLR_MAX_TRIES: # try with all multiplier values for larger K L_ctr_multipiler = L_ADAPTIVE_MPLR_INIT L_ctr_multipiler_tries = 0 incK += 1 if __debug__: corrective_action = "Gurobi timeout, temporarely increase K by %d" % incK elif find_optimal_seeds: take_next_seed = True else: grbe.message += ", consider increasing the MAX_MIP_SOLVER_RUNTIME in config.py" raise grbe else: if L and use_adaptive_L_constraint_weights: ## Adaptive GAP/L constraint multiplier reset # reset multiplier in case it the L feasibility was not violated # or it has reached the max_value. if solved or L_ctr_multipiler_tries >= L_ADAPTIVE_MPLR_MAX_TRIES: L_ctr_multipiler = L_ADAPTIVE_MPLR_INIT L_ctr_multipiler_tries = 0 take_next_seed = True if not solved and increase_K_on_failure and currentK + incK + 1 <= maxKinc: incK += 1 take_next_seed = False if __debug__: corrective_action = "temporarely increase K by %d" % incK else: if __debug__: corrective_action = "try with another seed = %d" % seed_trial ## Adaptive GAP/L constraint multiplier update else: L_ctr_multipiler += L_ADAPTIVE_MPLR_INC L_ctr_multipiler_tries += 1 if __debug__: corrective_action = "try with another L_ctr_multipiler = %.2f" % L_ctr_multipiler else: if not solved and increase_K_on_failure and currentK + incK + 1 <= maxKinc: incK += 1 if __debug__: corrective_action = "temporarely increase K by %d" % incK else: take_next_seed = True # Store the best so far if solved: if is_better_sol(best_f, best_K, sol_f, sol_K, minimize_K): best_sol = solution best_f = sol_f best_K = sol_K found_improving_solution_for_this_K = True else: # No feasible solution was found for this trial (max route cost # or capacity constraint was violated). if __debug__: if GAP_infeasible or L_infeasible: log(DEBUG, "Constraint is violated, " + corrective_action) else: log(DEBUG, "Continuing search, " + corrective_action) if take_next_seed: incK = 0 seed_trial += 1 if not find_optimal_seeds: break # seed loop, possibly try next K if seed_trial == N: incK = 0 break # seed loop, possibly try next K if minimize_K: # do not try different K if we found a solution if best_sol: break # K loop else: # not minimize_K # We already have an feasible solution for K<K_current, and could # not find a better solution than that on K_current. Therefore, it # is improbable we will find one even if we increase K and we # should stop here. if best_sol and not found_improving_solution_for_this_K: break except KeyboardInterrupt: #or SIGINT # pass on the current best_sol raise KeyboardInterrupt(best_sol) return best_sol
def cmt_2phase_init(D, d, C, L=None, minimize_K=False, lambda_multiplier=2.0, mu_multiplier=1.0, phase1_seed_selection_method = "farthest", phase2_choose_most_associated_route = True, phase2_repeated_association_with_n_routes = 1, number_of_randomized_retries = None): """ Implementation of the Christofides, Mingozzi & Toth (1979) two phase heuristic. In the first phase a customer is selected to act as a seed node and initialize a route. Then, a savings criteria parametrized with lambda_multiplier is used to determine which customers to insert. Insertions are done until a constraint is violated and then a new seed is selected and the insertions continue. This is repeated until no unrouted customers remain or we run out of route seeds. Finally, the routes are made r-optimal with 3-opt. The seed customers are carried over the the second phase of the algorithm. Here, each customer is associated to a seed customer based on a second savings criteria parametrized with mu_multiplier. Also the next closest seed customer has an effect to the score used when associating the nodes. Then, a route is built around each seed customer with the nodes associated to that route taking care not to violate feasibility of the route. Finally, if a feasible solution was generated, the routes from the second phase are made r-optimal with 3-opt. A better of the solutions from the first and second phases is selected and returned. Note that the default parameters are for a deterministic variant of the stochastic algorithm described in (Christofides et al 1979). Basic parameters: * 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/length/duration. Objective parameter: * minimize_K sets the primary optimization objective. If set to True, it is the minimum number of routes and the current best is always replaced with a solution with smaller K. If set to False (default) the algorithm optimizes only for the mimimum solution/routing cost. Route shape parameters: * lambda_multiplier specifies how closely the customer is associated to the emerging route seed customer in the first phase. * mu_multiplier specifies how closely the customer is associated to route seed customers in the second phase. The implementation includes some improvements to the CMT (1979) algorithm to improve the chance of second phase producing feasible solutions: * phase1_seed_selection_method instead of selecting a seed customer for emerging route at random in the first phase, select the "farthest" or "closest" to the depot or the one with the "biggest" demand. Can also be "first", which will be random if randomized_retries is set. * phase2_choose_most_associated_route instead of building the routes in random order in phase 2, start from the route with most associated customers. If set to False implements the original behaviour of (CMT 1979). * phase2_repeated_association_with_n_routes if set to None, the original behaviour of (CMT 1979) is used. That is, terminate phase 2 without an feasible solution if the first route building pass over the route seed customers leaves unrouted customers when S=0. If this is set to 1, the procedure is repeated until a) all customers are routed or b) no feasible insertions can be made. If this parameter is set to be >1, also insertion of 2. best alternatives to associate with a seed customers are tried. Can also be "K". Then the number of routes generated in the first phase is used as the value of this parameter. * number_of_randomized_retries If None the algorithm is deterministic. If set to an integer value. The first phase can generate this many seed customer configurations to second phase in case second phase is unable to produce feasible solutions. """ if phase1_seed_selection_method=="first": seed_f = _first_seed elif phase1_seed_selection_method=="farthest": seed_f = _farthest_seed elif phase1_seed_selection_method=="closest": seed_f = _closest_seed elif phase1_seed_selection_method=="biggest": seed_f = _biggest_seed rr = number_of_randomized_retries best_sol = None best_f = None best_K = None interrupted = False while (rr is None) or (rr>0): phase1_sol, phase1_f, phase1_K = None, float("inf"), float("inf") phase2_sol, phase2_f, phase2_K = None, float("inf"), float("inf") try: phase1_seeds, phase1_sol, phase1_f, rr = \ _phase_one(lambda_multiplier,D,d,C,L, seed_f, rr) phase1_K = len(phase1_seeds) # extension to CMT, option to associate customers multiple times # (to other routes, starting from the route with minimal eps). associate_routes = phase2_repeated_association_with_n_routes if phase2_repeated_association_with_n_routes=="K": associate_routes = phase1_K phase2_K, phase2_sol, phase2_f, rr = \ _phase_two(mu_multiplier,phase1_seeds,D,d,C,L, rr, phase2_choose_most_associated_route, associate_routes) except KeyboardInterrupt as e: #or SIGINT # Phase 1 OR phase 2 was interrupted. if len(e.args)>0 and type(e.args[0]) is list: if phase1_sol is None: phase1_sol = without_empty_routes(e.args[0]) phase1_f = objf(phase1_sol) phase1_K = phase1_sol.count(0)-1 elif phase2_sol is None: phase2_sol = without_empty_routes(e.args[0]) phase2_f = objf(phase2_sol) phase2_K = phase2_sol.count(0)-1 interrupted = True # Pick the better out of the two p1_better_than_p2 = is_better_sol(phase2_f, phase2_K, phase1_f, phase1_K, minimize_K) p1_best_so_far = is_better_sol(best_f, best_K, phase1_f, phase1_K, minimize_K) p2_best_so_far = is_better_sol(best_f, best_K, phase2_f, phase2_K, minimize_K) if p1_better_than_p2 and p1_best_so_far: best_sol = phase1_sol best_f = phase1_f best_K = phase1_K if not p1_better_than_p2 and p2_best_so_far: best_sol = phase2_sol best_f = phase2_f best_K = phase2_K if interrupted: # pass on the current best solution raise KeyboardInterrupt(best_sol) # deterministic version, no retries # stochastic version terminates as soon as phase2 succeeds if (rr is None) or (phase2_sol is not None): break return best_sol
def read_and_solve_a_problem(problem_instance_path, with_algorithm_function, minimize_K, best_of_n=1, verbosity=-1, single=False, measure_time=False): """ Solve a problem instance with the path in problem_instance_path with the agorithm in <with_algorithm_function>. The <with_algorithm_function> has a signature of: init_f(points, D_c, d, C, L, st, wtt, verbosity, single, minimize_K) Options <verbosity>, <single> and <measure_time> may be used to adjust what is printed and if a restricted single iteration search (different meaning for different algorithms) is made.""" pfn = problem_instance_path N, points, dd_points, d, D, C, ewt = cvrp_io.read_TSPLIB_CVRP(pfn) required_K, L, st = cvrp_io.read_TSBLIB_additional_constraints(pfn) # model service time with the distance matrix D_c = cvrp_ops.D2D_c(D, st) if st else D if points is None: if dd_points is not None: points = dd_points else: points, ewt = cvrp_ops.generate_missing_coordinates(D) tightness = None if C and required_K: tightness = (sum(d) / (C * required_K)) if verbosity >= 0: print_problem_information(points, D, d, C, L, st, tightness, verbosity) best_sol = None best_f = float('inf') best_K = len(D) interrupted = False for repeat_n in range(best_of_n): sol, sol_f, sol_K = None, float('inf'), float('inf') start = time() try: sol = with_algorithm_function(points, D_c, d, C, L, st, ewt, single, minimize_K) except KeyboardInterrupt as e: print("WARNING: Solving was interrupted, returning " + "intermediate solution", file=sys.stderr) 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] elapsed = time() - start if sol: sol = cvrp_ops.normalize_solution(sol) sol_f = objf(sol, D_c) 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 best_of_n > 1 and verbosity >= 1: print("SOLUTION QUALITY %d of %d: %.2f" % (repeat_n + 1, best_of_n, objf(best_sol, D_c))) if measure_time or verbosity >= 1: print("SOLVED IN: %.2f s" % elapsed) if interrupted: break if verbosity >= 0 and best_sol: n_best_sol = cvrp_ops.normalize_solution(best_sol) print_solution_statistics(n_best_sol, D, D_c, d, C, L, st, verbosity=verbosity) if interrupted: raise KeyboardInterrupt() return best_sol, objf(best_sol, D), objf(best_sol, D_c)
def mole_jameson_insertion_init(D, d, C, L=None, minimize_K=False, strain_criterion='all'): """ This is the implementation of Mole and Jameson (1976) cheapest insertion algorithm. The emerging route is first initialized according to which strain criterion (insertion cost calculation method) is used, On each step an unrouted customer for which the insertion cost is lowest (between any two nodes on the emerging route) is searched and the insertion made until no feasible insertions remain. For details see the insertion implementation in cheapest_insertion.py:cheapest_insertion_init * strain_criterion can be one of - 'proximity_ranking' - 'min_strain' - 'clarke_wright' each implementing a sligtly different insertion criteria - 'gaskell' - 'augumented_min_strain' try several values and - 'all' (default) tries all of the above For 'clarke_wright' and 'gaskell' and when lambda is 2.0 in 'augumented_min_strain' the routes are initialized according to the primary value of the current strain criteria. For 'proximity_ranking', 'min_strain', and rest of 'augumented_min_strain' the emerging route is initialized with farthest unrouted customer. Mole, R. and Jameson, S. (1976). A sequential route-building algorithm employing a generalised savings criterion. Journal of the Operational ResearchSociety, 27(2):503-511. """ callback_configurations = [] if strain_criterion == 'proximity_ranking' or strain_criterion == 'all': callback_configurations.append( (_create_new_criteria_function(lm=0.0, mm=0.0), "farthest")) if strain_criterion == 'min_strain': #or strain_criterion=='all': # <- this is already is in 'augumented_min_strain' callback_configurations.append( (_create_new_criteria_function(lm=0.0, mm=1.0), "farthest")) if strain_criterion == 'clarke_wright': #or strain_criterion=='all': # <- this is already in 'augumented_min_strain' callback_configurations.append( # when mu = lambda-1, initiate route with savings criteria (_create_new_criteria_function(lm=2.0, mm=1.0), "strain")) if strain_criterion == 'gaskell' or strain_criterion == 'all': lambda_mults = [1.25, 1.5, 1.75, 2.0] for glm in lambda_mults: callback_configurations.append( # when mu = lambda-1, initiate route with savings criteria (_create_new_criteria_function(lm=glm, mm=glm - 1.0), "strain")) if strain_criterion == 'augumented_min_strain' or strain_criterion == 'all': # the lm=2.0, mm=1.0 is already in 'gaskell' lambda_mults = [0, 0.5, 1, 1.5] if strain_criterion=='all' else\ [0, 0.5, 1, 1.5, 2.0] for alm in lambda_mults: callback_configurations.append( (_create_new_criteria_function(lm=alm, mm=1.0), "strain" if alm - 1.0 == 1.0 else "farthest")) ## Find the best solution among the active strain criterions best_sol = None best_f = None best_K = None interrupted = False for strain_function, init_method in callback_configurations: sol, sol_f, sol_K = None, float('inf'), float('inf') try: sol = cheapest_insertion_init( D, d, C, L, minimize_K=False, emerging_route_count=1, initialize_routes_with=init_method, insertion_strain_callback=strain_function, insert_callback=_try_insert_2opt_and_update) sol = _refine_solution(sol, D, d, C, L, minimize_K) # LS may make some of the routes empty sol = without_empty_routes(sol) except KeyboardInterrupt as e: #or SIGINT # some of the strain function insertion runs 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: raise KeyboardInterrupt(best_sol) return best_sol
def cli(init_name, init_desc, init_f): ## Simple command line interface single = False # ask to run only single iteration of the algorithm measure_time = False verbosity = DEFAULT_DEBUG_VERBOSITY minimize_K = False output_logfilepath = None best_of_n = 1 interrupted = False for i in range(0, len(sys.argv) - 1): if sys.argv[i] == "-v" and sys.argv[i + 1].isdigit(): verbosity = int(sys.argv[i + 1]) if sys.argv[i] == "-n" and sys.argv[i + 1].isdigit(): best_of_n = int(sys.argv[i + 1]) if sys.argv[i] == "-1": single = True if sys.argv[i] == "-t": measure_time = True if sys.argv[i] == "-l": output_logfilepath = sys.argv[i + 1] if sys.argv[i] == "-b": otarget = sys.argv[i + 1].lower() if otarget == "cost" or otarget == "c": minimize_K = False elif otarget == "vehicles" or otarget == "k": minimize_K = True else: print("WARNING: Ignoring unknown optimization target %s" % otarget) if verbosity >= 0: set_logger_level(verbosity, logfile=output_logfilepath) if sys.argv[-1].isdigit(): N = int(sys.argv[-1]) problem_name = "random " + str(N) + " point problem" N, points, _, d, D, C, _ = cvrp_io.generate_CVRP(N, 100, 20, 5) d = [int(de) for de in d] D_c = D L, st = None, None wtt = "EXACT_2D" best_sol = None best_f = float('inf') best_K = len(D) for i in range(best_of_n): sol, sol_f, sol_K = None, float('inf'), float('inf') try: sol = init_f(points, D_c, d, C, L, st, wtt, single, minimize_K) except KeyboardInterrupt as e: print("WARNING: Solving was interrupted, returning " + "intermediate solution", file=sys.stderr) 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 sol: sol = cvrp_ops.normalize_solution(sol) sol_f = objf(sol, D_c) 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: break print_solution_statistics(best_sol, D, D_c, d, C, L, st, verbosity=verbosity) problem_file_list = get_a_problem_file_list([sys.argv[-1]]) if not problem_file_list or "-h" in sys.argv or "--help" in sys.argv: print ("Please give a TSPLIB file to solve with "+\ init_name+\ " OR give N (integer) to generate a random problem of N customers."+\ " OR give a path to a folder with .vrp files."+\ "\n\nOptions (before the file name):\n"+\ " -v <int> to set the verbosity level (default %d)\n"%DEFAULT_DEBUG_VERBOSITY+\ " -n <int> run the algorithm this many times and return only the best solution\n"+\ " -1 to run only one iteration (if applicable)\n"+\ " -t to print elapsed wall time\n"+\ " -l <file_path> to store the debug output to a file\n"+\ " -b <'cost'|'vehicles'> or <c|K> sets the primary optimization oBjective (default is cost)", file=sys.stderr) elif problem_file_list: for problem_path in problem_file_list: problem_name = path.basename(problem_path) print("Solve", problem_name, "with", init_name) read_and_solve_a_problem(problem_path, init_f, minimize_K, best_of_n, verbosity, single, measure_time)
def petal_init(points, D, d, C, L, K=None, minimize_K=False, relaxe_SCP_solutions=True, required_iterations=None, min_iterations=None, restricted_route_ratio=0.75, allow_infeasible=True, can_discard_multiple_customers='auto', predefined_petals_generator=None): """ An implementation of Foster and Ryan (1976) Petal algorithm. The VRP is solved with a set covering formulation (SCP->MIP/LP). The decision variables are feasible routes (petals) R_i. The three initial petal sets (RESTRICTED, REDUCED, and EXTENDED) are generated with a Sweep algorithm the RELAXED petal set is grown when improving local search moves are found. The algorithm solves SCP iteratively and the petal set size is increased when no improvements are found or other conditions are not fulfilled: RESTRICTED+RELAXED -> REDUCED+RESTRICTED+RELAXED -> EXTENDED+REDUCED+RESTRICTED+RELAXED The implementation uses Gurobi to solve the SCP. The RESTRICTED set contains routes each with total demand: d_R_i > restricted_route_ratio*C The REDUCED set contains routes not in RESTRICTED but with total demand: d_R_i > C_l C_l = sum_{j}{d_j}-(K-1)C The EXTENDED set contains the ones that have not high enough total demand to be included in RESTRICTED or REDUCED sets. By default, the algorithm terminates as soon as no improving solutions can be found, even with increasing the petal set size (RESTRICTED->REDUCED->EXTENDED) or loosening the K constraint. Args: * points is a list (or 2D numpy ndarray) of coordinate points. * 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. * K can be set to require a predefined number of vehicles. However, this constraint is loosened if no feasible solutions are found. If minimize_K is set to True and K is None, a minimum K is found by starting from sm- allest possible K and increasing it until a feasible solution is found. Else, if K set manually, it is respected (and minimize_K has no effect). * 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 practice disables automatic K constraint. * relaxe_SCP_solutions turns on the improvement heuristic that is similar to Pair (the chain) heuristic of Wren & Holliday (1972). These improved routes are stored to the RELAXED petal set. * required_iterations can be used force the Petal algorithm to run a preset number of iterations, with each exploring one valid SCP solution. If this is set to -1 (default) the algorithm terminates after no SCP improvements seem to be possible. required_iterations can change this by allowing new RELAXED petals to be generated from worse SCP solutions. If set to None (default) the feature is disabled. * min_iterations works similarly to required_iterations, but it only specifies a lower bound for the number of iterations. The rule based termination can still decide to continue the search after min_iterations has been tried. If set to None (default) the feature is disabled. * can_discard_multiple_customers when building relaxed petals, one can set how many customers can be redistributed. This is needed when the route length grows, and the number of combinations becomes too large. The value can be set to: False = discard at most 1 True = discard as many as necessary <int> = discard at most this many 'auto' = the number of maximum discarded customers depend on the length of the route as specified by AUTO_DISCARD_LIMITS e.g. at most 5 for routes with more than 10 customers, at most 3 for routes with more than 20 customers, at most 2 for routes with more than 30 customers, at most 1 for routes with more than 50 customers, Foster, B. A. and Ryan, D. M. (1976). An integer programming approach to the vehicle scheduling problem. JORS, 27(2):367-384. Wren, A., & Holliday, A. (1972). Computer scheduling of vehicles from one or more depots to a number of delivery points. JORS, 23(3), 333-344. """ if not points: raise ValueError( "The algorithm requires 2D coordinates for the points") N = len(points) # Translate the argument to a value if type(can_discard_multiple_customers) is int: discard_at_most = can_discard_multiple_customers elif can_discard_multiple_customers == False: discard_at_most = 1 elif can_discard_multiple_customers == True: discard_at_most = None # no limit elif can_discard_multiple_customers == 'auto': pass # is set when _generate_solution_relaxation_petals is called else: raise ValueError( "Unknown parameter value for can_discard_multiple_customers" + " can be integer, True, False or 'auto'") ## 1. GENERATE PETALS # "Route capacity Lower Bound" (LB) C_t of Foster and Ryan (1976, p. 373) if C: # "smallest integer **greater** than x" (note, not equal) d_tot = sum(d) v = ceil(d_tot / float(C) + C_EPS) K_constraint = v if (not K and minimize_K) else K C_t = d_tot - (K - 1) * C if K else d_tot - (v - 1) * C else: K_constraint = 1 if (not K and minimize_K) else K C_t = 0.0 #TODO: for the larger instances, get the restricted FIRST, then the # reduced and the extended ONLY IF ABSOLUTELY NEEDED! if predefined_petals_generator is None: restricted_ptls, reduced_ptls, extended_ptls = \ _generate_overconstrained_petals( restricted_route_ratio, C_t, N, points, D, d, C, L) else: restricted_ptls, reduced_ptls, extended_ptls = predefined_petals_generator( ) relaxed_ptls = PetalSet([], [], []) active_ptls = None r_cnt = len(restricted_ptls.routes) e_cnt = len(reduced_ptls.routes) x_cnt = len(extended_ptls.routes) ptl_cnt = r_cnt + e_cnt + x_cnt ptl_set = PTL_SET.RESTRICTED max_ptl_set_used = PTL_SET.RESTRICTED seen_ptls = set() ptls_in_use_set = None ## 2. SOLVE SET COVERING UNTIL NO IMPROVEMENTS routes_with_idxs = None forbidden_combinations = [] best_sol_feasible = False best_sol = None best_sol_f = float('inf') best_sol_K = len(D) interrupted = False #TODO: the set covering solving with reduced, then extended (if no # fesible set covering problem, all petals if still no feasible sol.), # and on subsequent iterations relaxed tepalts, are all very similar. # Thus, one should use warm starting and add/modify the constraints on the # fly. Now the LP/MIP is solved from the beginning on each iteration. # Check how this column generation can be implemented in Gurobi. iteration_counter = 0 while not interrupted: iteration_counter += 1 ## SET COVERING PHASE n_base_ptls = 0 n_rlxd_ptls = len(relaxed_ptls.routes) routes_with_idxs = None sol_feasible = False try: if ptl_set == PTL_SET.RESTRICTED and len( restricted_ptls.nodes) > 0: # Try set covering first with a reduced petal set, that are, by # default, at least 75% full. # Update the set of active petals if ptls_in_use_set != ptl_set: seen_ptls.update( set(tuple(r) for r in restricted_ptls.routes)) active_ptls = PetalSet(restricted_ptls.nodes, restricted_ptls.routes, restricted_ptls.costs) ptls_in_use_set = PTL_SET.RESTRICTED n_base_ptls = len(active_ptls.routes) routes_with_idxs, sol_feasible = _solve_set_covering( N, active_ptls, relaxed_ptls, forbidden_combinations, allow_infeasible, D=D, K=K_constraint) if __debug__: active_K = K_constraint if K_constraint else K _log_debug_scp_info(ptl_set, n_base_ptls + n_rlxd_ptls, routes_with_idxs, sol_feasible, active_K) no_rsol = not routes_with_idxs if (no_rsol and ptl_set == PTL_SET.RESTRICTED) or ptl_set == PTL_SET.REDUCED: # The reduced set contains all feasible routes that hit the # constraint limit C_t = \sum(d_i)-(K-1)*C ptl_set = PTL_SET.REDUCED # Update the set of active petals if ptls_in_use_set != ptl_set: seen_ptls.update(set( tuple(r) for r in reduced_ptls.routes)) active_ptls = PetalSet( restricted_ptls.nodes + reduced_ptls.nodes, restricted_ptls.routes + reduced_ptls.routes, restricted_ptls.costs + reduced_ptls.costs) ptls_in_use_set = PTL_SET.REDUCED n_base_ptls = len(active_ptls.routes) routes_with_idxs, sol_feasible = _solve_set_covering( N, active_ptls, relaxed_ptls, forbidden_combinations, allow_infeasible, D=D, K=K_constraint) if __debug__: active_K = K_constraint if K_constraint else K _log_debug_scp_info(ptl_set, n_base_ptls + n_rlxd_ptls, routes_with_idxs, sol_feasible, active_K) # "If the LP defined by this reduced petal set uses more than v # vehicles we can computethe remaining petals with capacitiesless # than c1 and confirm the LP solution on the extended petal set." # - Foster & Ryan 1976 no_esol = not routes_with_idxs if no_esol or (K and len(routes_with_idxs[0]) > K ) or ptl_set == PTL_SET.EXTENDED: # The complete set contains all feasible routes that were generated # with the sweep procedure including single customer routes. ptl_set = PTL_SET.EXTENDED # Update the set of active petals if ptls_in_use_set != ptl_set: seen_ptls.update( set(tuple(r) for r in extended_ptls.routes)) active_ptls = PetalSet( restricted_ptls.nodes + reduced_ptls.nodes + extended_ptls.nodes, restricted_ptls.routes + reduced_ptls.routes + extended_ptls.routes, restricted_ptls.costs + reduced_ptls.costs + extended_ptls.costs) ptls_in_use_set = PTL_SET.EXTENDED n_base_ptls = len(active_ptls.routes) routes_with_idxs, sol_feasible = _solve_set_covering( N, active_ptls, relaxed_ptls, forbidden_combinations, allow_infeasible, D=D, K=K_constraint) if __debug__: active_K = K_constraint if K_constraint else K _log_debug_scp_info(ptl_set, n_base_ptls + n_rlxd_ptls, routes_with_idxs, sol_feasible, active_K) max_ptl_set_used = max(max_ptl_set_used, ptl_set) except KeyboardInterrupt: #or SIGINT # continue and store routes_with_idxs if any interrupted = True try: if routes_with_idxs: chosen_ptl_routes, chosen_plt_indices = zip(*routes_with_idxs) #TODO: in some pathological situations (probaby due to floating point # math inaccuracies) feasRelax may return infeasible solution that does # not really respect the forbidden solution constraints. It seems this # is only in cases where there actually is no solution (e.g. due to K # constraint and forcing it just breaks things). #WARNING: This is just a quickfix, should investigate this "someday". if chosen_plt_indices in forbidden_combinations: found_solution = False else: found_solution = True if not sol_feasible and not interrupted: if __debug__: log( DEBUG - 2, "Check if some customers are served " + "multiple times and remove those " + "visits that are unnecessary") sol_feasible = _remove_multiserved( chosen_ptl_routes, D) else: # Once the first feasible solution is found, we no longer # accept infeasible ones! allow_infeasible = False else: found_solution = False # It may be the case that constraints are so tight that we did not find # even an infeasbile solution. Do our best to find one by loosening K. if not found_solution: if ptl_set == PTL_SET.RESTRICTED: ptl_set = PTL_SET.REDUCED elif ptl_set == PTL_SET.REDUCED: ptl_set = PTL_SET.EXTENDED elif K_constraint: ptl_set = PTL_SET.RESTRICTED K_constraint = K_constraint + 1 else: return best_sol continue petal_sol = routes2sol(chosen_ptl_routes) petal_sol_f = objf(petal_sol, D) petal_sol_K = petal_sol.count(0) - 1 if __debug__: feasibilitys = "feasible" if len( set(petal_sol)) == len(D) else "infeasible" log( DEBUG, "\nGot %s Petal LP sol %s (%.2f)" % (feasibilitys, str(petal_sol), petal_sol_f)) log(DEBUG - 2, "\n...with indices %s" % str(chosen_plt_indices)) #print("REMOVEME: it =", iteration_counter, "f =", petal_sol_f, # "k =", len(chosen_ptl_routes), "is feasible =", sol_feasible) # "a new starting schedule is determined by banning all the routes in # the optimum IP solution and re-converging the IP" # add in the relaxed petals as negative numbers forbidden_combinations.append(chosen_plt_indices) # Check if we still continue: for example, check if an improvement was # made and store the best so far. The condition is a little tricky as # feasible wins infeasible, always. better_or_same = is_better_sol(best_sol_f, best_sol_K, petal_sol_f, petal_sol_K, minimize_K)\ or\ (petal_sol_K==best_sol_K and petal_sol_f==best_sol_f) if (best_sol_feasible and sol_feasible and better_or_same) or\ (not best_sol_feasible and (sol_feasible or better_or_same)): best_sol = petal_sol best_sol_f = petal_sol_f best_sol_K = petal_sol_K best_sol_feasible = sol_feasible elif ptl_set == PTL_SET.RESTRICTED: # "The region is relaxed to the complete petal set when no # further improvements can be found from the reduced set" # -Foster & Ryan 1976 ptl_set = PTL_SET.REDUCED ## Try our best to find a feasible solution # If we were unable to find a feasible solution from REDUCED set, # try with complete set. # Also, if we have used the EXTENDED set prior to relaxing K, we grow # the petal set back to it's former size after checking REDUCED set. elif ( (not best_sol_feasible or ptl_cnt <= ALWAYS_USE_ALL_PETALS_LIMIT) and (ptl_set == PTL_SET.REDUCED)) or (ptl_set < max_ptl_set_used): if __debug__: if (not best_sol_feasible): log( DEBUG, "No feasible solution found, using COMPLETE petal set." ) ptl_set = PTL_SET.EXTENDED elif (not best_sol_feasible) and K_constraint: if __debug__: log(DEBUG, "No feasible solution found, loosening K constraint.") # As a last resort, relax K K_constraint = K_constraint + 1 ptl_set = PTL_SET.RESTRICTED elif not (min_iterations and iteration_counter<min_iterations) and\ (required_iterations is None): # Finally, abort if no improvements can be found. break if required_iterations and iteration_counter>=required_iterations and\ not (min_iterations and iteration_counter<min_iterations): break # main iteration loop ## IMPROVEMENT PHASE # aka. "relaxation ... of moving one delivery between two routes so # as to reduce the total mileage" if not interrupted and relaxe_SCP_solutions: if __debug__: log( DEBUG, "Searching for relaxed petals between the " + "%d routes of the Petal solution" % (petal_sol.count(0) - 1)) if can_discard_multiple_customers == 'auto': # Avoid combinatorial explosion and restrict how many customers # can be removed and redistributed. discard_at_most = None longest_route_len = max(len(r) for r in chosen_ptl_routes) - 2 for limit, set_to in AUTO_DISCARD_LIMITS: if longest_route_len > limit: discard_at_most = set_to new_petal_candidates = _generate_solution_relaxation_petals( chosen_ptl_routes, discard_at_most, D, C, d, L) #TODO: Implement the second secondary relaxation that tries to find # improvements where groups of concecutive customers are moved from # a route to another (p.381, Foster & Ryan 1976) and add an option # to enable it. # Store petals that are new for ird in new_petal_candidates: ird.update_node_set() ird.normalize() tuple_r = tuple(ird.route) if len(tuple_r) > 2 and tuple_r not in seen_ptls: relaxed_ptls.routes.append(ird.route) relaxed_ptls.costs.append(ird.cost) relaxed_ptls.nodes.append(ird.node_set) if __debug__: log( DEBUG - 1, "Added a relaxed petal %s (%.2f)" % (str(ird.route), ird.cost)) seen_ptls.add(tuple_r) ptl_cnt += 1 # Do not store the improved solution as the best petal solution, # as the next set conver solution will cover (pun intented) this. except KeyboardInterrupt: #or SIGINT interrupted = True break #the main loop if interrupted: raise KeyboardInterrupt(best_sol) return best_sol
def sweep_init(coordinates, D, d, C, L=None, minimize_K=False, direction="both", seed_node=BEST_ALTERNATIVE, routing_algo=None, **callbacks): """ This algorithm was proposed in Wren (1971) and in Wren & Holliday (1972). Sweep was also proposed in Gillett and Miller (1974) who gave the algorithm its name. The proposed variants differ in on how many starting locations (seed) for the sweep are considered: four in Wren & Holliday (1972) and all possible in both directions in Gillett and Miller (1974). Also, the improvement procedures differ. The version in this file is basebones as as it does not include any route improvement heuristics. For implementations of Gillett and Miller (1974) or Wren & Holliday (1972) algorithms, please see their Python files (gillet_miller_sweep.py and wren_holliday_sweep.py). The basic principle of the Sweep algorithm is simple: The algorithm assumes that the distances of the CVRP are symmetric, and, furthermore, that the points are located on a plane. The catresian coordinates of these points in relation to the depot are converted to polar coordinates (rho, phi), and then sorted by phi. Starting from an arbitary node (in this implementation the default is the one closest to the depot) create a new route and add next adjecent unrouted node according to their angular coordinate. Repeat as long as the capacity is not exceeded. When this happens, start a new route and repeat the procedure until all nodes are routed. Finally, the routes can optionally be optimized using a TSP algorithm. Note that the algorithm gives different results depending on the direction the nodes are inserted. The direction parameter can be "cw" for clockwise insertion order and "ccw" for counterclockwise. As the algorithm is quite fast, it is recommended to run it in both directions. Please note that the actual implementation of the sweep procedure is in the do_one_sweep function. * coordinates can be either a) a list/array of cartesian coordinates (x,y) b) a lists/arrays (3) of polar coodinates WITH node indexes (i.e. a numpy stack of phi,rho,idx) * 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/cost/duration. * direction is either "cw" or "ccw" depending on the direction the nodes are to be processed * seed_node is optional parameter that specifies how the first node of the sweep is determned. This can be one of CLOSEST_TO_DEPOT (0), SMALLEST_ANGLE (-1), BEST_ALTERNATIVE (-2), which tries every possible staring id, or a positive integer explicitly specifying the node id to start from. Also, a list of indexes can be given. These are explicit sweep indexes and it is adviseable to give also the sweep parameter. Wren, A. (1971), "Computers in Transport Planning and Operation", Ian Allan, London. Wren, A., and Holliday, A. (1972), "Computer scheduling of vehicles from one or more depots to a number of delivery points", Operations Research Quarterly 23, 333-344. Gillett, B., and Miller, L., (1974). "A heuristic algorithm for the vehicle dispatch problem". Operations Research 22, 340-349. """ N = len(D) if len(coordinates[0])==2: sweep = get_sweep_from_cartesian_coordinates(coordinates) elif len(coordinates)==3 and (len(coordinates[0])==len(coordinates[1])==len(coordinates[2])): # it is not necessarily to sweep to contain all nodes in D and d sweep = coordinates else: raise ValueError("The coordinates need to be (x,y) or (phi,rho,node_index,sweep_index_for_node-1). Not "+str(coordinates)) ## specify the direction if direction == "ccw": step_incs = [1] elif direction == "cw": step_incs = [-1] elif direction == "both": step_incs = [1,-1] else: raise ValueError("""Only "cw", "ccw", and "both" are valid values for the direction parameter""") ## specify where to start if seed_node==CLOSEST_TO_DEPOT: starts = [np.argmin(sweep[1])] elif seed_node==SMALLEST_ANGLE: starts = [0] elif seed_node==BEST_ALTERNATIVE: starts = list(range(0,N-1)) elif type(seed_node) is int: # we interpret it as a node idx starts = [np.where(sweep[2]==abs(seed_node)%N)[0][0]] elif type(seed_node) is list: # we interpret it as a node idx starts = seed_node ## Make sure there is a valid route improvement method if routing_algo is None: # Default generates the route from the list of nodes in the order they # were swept. Assume that depot (0) is the first of node_set. routing_algo = lambda D, node_set: (list(node_set)+[0], objf(list(node_set)+[0],D)) ## for exteding Sweep with improvement heuristics callback_data = None intra_route_callback = None inter_route_callback = None if 'prepare_callback_datastructures' in callbacks: pcds_callback = callbacks['prepare_callback_datastructures'] callback_data = pcds_callback(D,d,C,L,sweep) if 'intra_route_improvement' in callbacks: intra_route_callback = callbacks['intra_route_improvement'] if 'inter_route_improvement' in callbacks: inter_route_callback = callbacks['inter_route_improvement'] ## Do the search with the parameter specified above best_sol = None best_f = None best_K = None try: for step_inc in step_incs: for start in starts: if __debug__: log(DEBUG, "\nDo a sweep from position %d (n%d) by steps of %d"% (start,sweep[2][start],step_inc)) ## This does one sweep from one start location to one direction routes = do_one_sweep(N, D, d, C, L, routing_algo, sweep, start, step_inc, False, intra_route_callback, inter_route_callback, callback_data) sol = [n for rd in routes for n in rd.route[:-1]]+[0] # LS of the callbacks may cause empty routes sol = without_empty_routes(sol) sol_f = objf( sol, D ) sol_K = sol.count(0)-1 if __debug__: log(DEBUG, "Previous sweep produced solution %s (%.2f)\n\n" % (str(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 except KeyboardInterrupt: # or SIGINT raise KeyboardInterrupt(best_sol) return best_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
def suppression_savings_init(D, d, C, L, minimize_K=False, Lprime="auto"): """ This is the "vehicle scheduling procedure based upon savings and a solution pertubation scheme" of Holmes & Parker (1976). It works by suppressing a certain number (at most Lprime) best available merges. Thus the parallel savings algorithm of Clarke & Wright (1964) is executed Lprime times. Please see the parallel_savings.py:parallel_savings_init for details and description of the parameters. * Lprime is the maximum suppression number L'. If set to "auto" (default) a linear prediction of a suitable value is used: L'=N/7+5, where N is the number of customers in the problem. Can only be int. Holmes, R. and Parker, R. (1976). A vehicle scheduling procedure based upon savings and a solution perturbation scheme. Journal of the Operational Re- search Society, 27(1):83–92. Clarke, G. and Wright, J. W. (1964). Scheduling of vehicles from a central depot to a number of delivery points. Operations research, 12(4):568-581. """ N = len(D) - 1 if Lprime == "auto": # according to the (limited) experimental data of the Holmes & Parker # (1976), for best results this can grow linearly. Lprime = int(N / 7) + 5 Lprime = min(Lprime, int((N**2 - N) / 2)) best_sol = None best_f = None best_K = None interrupted = False #best_sL = 1 currently_suppressed_merges = set() savings_cache = [] suppressed_f = lambda D: supression_savings_function( D, currently_suppressed_merges, savings_cache) for Lcounter in xrange(Lprime): sol, sol_f, sol_K = None, float('inf'), float('inf') try: # On later invocations of parallel_savings_init, suppressed_f # generates an different set of savings values (suppressing some # the first/best of the previous iteration). sol = parallel_savings_init(D, d, C, L, minimize_K, suppressed_f) sol_f = objf(sol, D) sol_K = sol.count(0) - 1 except KeyboardInterrupt as e: # or SIGINT 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): if __debug__: log( DEBUG, "Best so far solution %s (%.2f) found with L'=%d" % (sol, sol_f, Lcounter)) best_sol = sol best_f = sol_f best_K = sol_K #best_sL = iterL if interrupted: raise KeyboardInterrupt(best_sol) return best_sol