Пример #1
0
 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
Пример #2
0
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
Пример #3
0
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
Пример #4
0
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
Пример #5
0
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
Пример #6
0
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)
Пример #7
0
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
Пример #8
0
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)
Пример #9
0
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
Пример #10
0
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
Пример #11
0
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
Пример #12
0
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