Пример #1
0
def _routestates2solution(routes, D):
    sol = []
    total_cost = 0.0
    for route, _, route_cost, route_l_updated in routes:
        # if L is not set, optimize TSP *in the end*
        if not route_l_updated:
            new_route, route_cost = solve_tsp(D, route)
            route = new_route[:-1]
            if __debug__:
                log(
                    DEBUG - 1, "Got TSP solution %s (%.2f)" %
                    (str(new_route), route_cost))
        total_cost += route_cost
        sol += route
    sol += [0]
    return sol, total_cost
Пример #2
0
def _init_with_tsp(D, d, C, L):
    route_tsp_sol, route_f = solve_tsp(D, list(range(0, len(D))))
    return route_tsp_sol + [0], route_f
Пример #3
0
def tyagi_init(D,
               d,
               C,
               L=None,
               only_large_and_close_customer_single_routes=False,
               try_interchange_with_all_group_customers=True,
               select_grouping="min_penalty"):
    """ This is an implementation of the Tyagi (1968) nearest neighbor 
    heurstic. In the first phase the method distributes customers into groups
    using bulding a nearest neighbor chains leaving from the depot. The group
    is deemed complete when a constraint is violated. Then an interchange 
    heuristic tries to remove one node from the group so that the one left out 
    can be fitted. This interchange is done if the demand of the route 
    increases due to the swap. If a group with only one customer is generated, 
    all of the candidates that are close to the depot and have large demand are 
    considered to be served with a route with a single customer. Furthermore 
    the entire procedure is repeated for all single customer route candidates 
    and the one that has "maximum use of the capacity" is used (see option 
    select_grouping). Then a TSP procedure is used to route the customer
    groups. This this implementation uses 3-Opt local search to do the routing 
    instead of the greedy TSP heuristic described in Tyagi (1968).
    
    The procedure above is used when the function is called with parameters:
     * only_large_and_close_customer_single_routes = True,
     * try_interchange_with_all_group_customers = False,
     * select_grouping = "max_demand"
    The "max_demand" selects the grouping where the non-single routes carry
    most capacity.
      
    However, these settings do not greate the grouping nor solution presented
    as an illustrative example in Tyagi (1968). It seems he uses different
    undocumented variant of his heuristic to solve the 12 customer problem
    from Dantzig and Ramser (1958). Therefore, following changes and options 
    were added, and can be used, to replicate the result:
     * only_large_and_close_customer_single_routes = False, which allows any 
        customer be served with a single customer route. 
     * try_interchange_with_all_group_customers = True, which tries to do the
        interchange with all of the nodes in a group (proto-route), instead of
        trying just the first and last customers.
     * select_grouping = "balanced", which selects the most balanced
        alternative among the grouping candidates with single customer route.
        Here, only the non-single customer routes are considered.
    Alternatively, another option for selecting grouping (which more closely
    follows the idea of Tyagi) can be used:
     * select_grouping = "min_penalty", which selects the option where 
        the routes with lot of free capacity are shortest (individual edges 
        or the used vehicle capacity when traversing them are not considered).
        This is calculated on the optimized routes.
        
    Note that if C is not set the select_grouping and its functionality is not
     used.
    
    With these options the grouping of customers given in illustrative
    example is replicated and as is the solution quality. Unfortunately it 
    is impossible to say if the modifications are the ones Tyagi (1968) used.
        
    Tyagi, M.S. (1968), "A Practical Method for Truck Dispatching Problem",
        J. Operations Research Society of Japan, 10, 76-92.
    Dantzig, G. B., & Ramser, J. H. (1959), "The truck dispatching problem",
        Management science, 6(1), 80-91.
    Helsgaun, K. (2006), "An Effective Implementation of K-opt Moves for the 
      Lin-Kernighan TSP Heuristic." DATALOGISKE SKRIFTER, No. 109, 2006. 
      Roskilde University.
    Helsgaun, K. (2009), "General k-opt submoves for the Lin-Kernighan TSP 
      heuristic." Mathematical Programming Computation, 1(2), 119-163.    
    """

    ## 0. Determine the number of vehicles
    may_have_single_node_route = False
    if C:
        tot_d = sum(d)
        K = ceil(tot_d / float(C))
        may_have_single_node_route = min(d[1:]) > tot_d - C * (K - 1)

    ## 1. "Grouping the delivery points" (nodes) to routes with nearest nbour.
    interchange_variant = lambda r, r_d, r_l, D, d, C, L, svd, nnn : \
        route_end_interchange(r, r_d, r_l, D, d, C, L, svd, nnn,
                              try_interchange_with_all_group_customers)
    solution = \
        nearest_neighbor_init(D, d, C, L,
                              initialize_routes_with=TYAGI_SEED_METHOD,
                              emerging_route_count=1, add_only_to_end=True,
                              route_improvement_callback=interchange_variant)

    routes = sol2routes(solution)

    if not C:
        best_C_utilization = None
    elif select_grouping == "balanced":
        # the best will be one that will have balanced routes
        best_C_utilization = \
            _calculate_route_demand_statistics(solution, d, K=len(routes))
    elif select_grouping == "max_demand":
        # the best will be the one that is best to top up the vehicle capacity
        best_C_utilization = sum(d[n]
                                 for n in solution) / float(len(routes) * C)
    elif select_grouping == "min_penalty":
        # the best will be the one with least distance traveled with unused capacity
        best_C_utilization = _calculate_penalty(solution, D, d, C)
    else:
        raise ValueError('"Invalid select_grouping parameter "%s"' %
                         select_grouping)

    if __debug__:
        if C:
            log(DEBUG - 1, "Initial Grouping = %s\n" % (solution))

    ## Tyagi heuristic has a special case for the single node routes
    try:
        single_nodes = [r[1] for r in routes if len(r) == 3]
        if C and (may_have_single_node_route or len(single_nodes) > 0):
            N = len(D)
            best_K = solution.count(0) - 1

            if only_large_and_close_customer_single_routes:
                # According to Tyagi1967, only the large demand nodes close to the
                #  depot are considered ...
                max_dist_from_depot = max(D[0, :])
                candidates = [n for n in range(1,N) \
                          if (d[n]>0.5*C and D[0,n]<0.5*max_dist_from_depot)]
            else:
                # ... however, using this criteria does not allow replicating the
                #  results (the illustrative example) of the paper. Therefore,
                #  there is an option to try with complete set of nodes.
                candidates = list(range(1, len(D)))

            for single_route_candidate in candidates:
                if __debug__:
                    log(
                        DEBUG - 2, "Try to find grouping with n%d forbidden" %
                        (single_route_candidate))
                cndt_sol = nearest_neighbor_init(
                    D,
                    d,
                    C,
                    L,
                    initialize_routes_with=TYAGI_SEED_METHOD,
                    emerging_route_count=1,
                    forbidden_nodes=[single_route_candidate],
                    route_improvement_callback=interchange_variant)

                cndt_K = cndt_sol.count(
                    0) - 1 + 1  #the single_route_candidate_node
                if cndt_K > best_K:
                    if __debug__:
                        log(DEBUG - 1, "No valid grouping found\n")

                    continue

                if select_grouping == "balanced":
                    cndt_C_utilization = \
                        _calculate_route_demand_statistics(cndt_sol, d, K=cndt_K-1)
                elif select_grouping == "max_demand":
                    cndt_C_utilization = sum(d[n] for n in cndt_sol) / float(
                        cndt_K * C)
                elif select_grouping == "min_penalty":
                    cndt_C_utilization = _calculate_penalty(cndt_sol, D, d, C)

                if __debug__:
                    log(
                        DEBUG - 1, "Grouping = %s (utilization %s)\n" %
                        (cndt_sol + [single_route_candidate, 0],
                         str(cndt_C_utilization)))

                # select the one with maximum possible use of carrier capacity
                if cndt_C_utilization > best_C_utilization:
                    best_C_utilization = cndt_C_utilization
                    solution = cndt_sol + [single_route_candidate, 0]
                    routes = sol2routes(solution)

        ## 2. "Finding the optimal tours"
        #instead of the heuristic of Tyagi, just solve it with a TSP solver
        if __debug__:
            log(
                DEBUG - 1, "Post-optimize solution %s (%.2f)" %
                (solution, objf(solution, D)))
        for i in range(len(routes)):
            routes[i], route_f = solve_tsp(D, routes[i][:-1])

            if __debug__:
                log(DEBUG - 2,
                    "Got TSP solution %s (%.2f)" % (str(routes[i]), route_f))
    except KeyboardInterrupt:  #or SIGINT
        raise KeyboardInterrupt(solution)

    return routes2sol(routes)
Пример #4
0
def _phase_one(lambda_multiplier, D, d, C, L, seed_f, rr):
    """ This imlements the fist phase of the algorithm. Sequentally
     add nodes to an emerging node. Different seed node selection 
     functions (above) can be used. """

    route_seeds = []
    N = len(D)
    sol = []
    total_cost = 0.0

    customer_nodes = list(range(1, N))
    if rr is not None:
        shuffle(customer_nodes)
        rr -= 1
    unrouted = OrderedSet(customer_nodes)

    if __debug__:
        log(DEBUG, "## Sequential route bulding phase ##")

    route_idx = 0

    try:
        while unrouted:
            ## Step 1: choose unrouted i_k to act as a route seed point

            route_seed_k = seed_f(D, d, unrouted)
            unrouted.remove(route_seed_k)

            route_seeds.append(route_seed_k)
            route_demand = d[route_seed_k] if C else 0
            route = [0, route_seed_k]
            route_cost = D[0, route_seed_k] + D[route_seed_k, 0]
            route_l_updated = True
            route_idx += 1

            if __debug__:
                log(
                    DEBUG, "Initialize route #%d with n%d" %
                    (route_idx, route_seed_k))

            ## Step 2: Compute savings

            s_vals = (
                D[[0], unrouted] +
                lambda_multiplier * D[unrouted, [route_seed_k]]).tolist()
            savings = list(zip(s_vals, unrouted))
            savings.sort()
            for best_saving, i in savings:
                ## Step 3: insert until feasibility is broken

                if __debug__:
                    log(
                        DEBUG,
                        "Check feasibility of inserting n%d with savings=%.2f"
                        % (i, best_saving))

                #TODO: To improve performance keep track of minimal unrouted d and
                # break the savings loop if we see that there are no feasible
                # insertions to be made.

                if C and route_demand + d[i] - C_EPS > C:
                    # capacity constraint violated, route complete
                    if __debug__:
                        log(DEBUG, "Insertion would break C constraint, skip.")
                    continue

                #TODO: To improve performance, keep track of minimal possible
                # cost increase to the route. This would involve updating e.g.
                # Held-Karp (1970) lower bound for the route.

                # Use upper bound estimate to save some computations. We know
                #  that the maximum route cost constraint cannot be violated.
                UB_route_cost = route_cost - D[route[-1], 0] + D[route[-1],
                                                                 i] + D[i, 0]
                if L and UB_route_cost - S_EPS > L:
                    new_route, new_route_cost = solve_tsp(D, route + [i])
                    if __debug__:
                        log(
                            DEBUG - 1, "Got TSP solution %s (%.2f)" %
                            (list(new_route), new_route_cost))

                    if new_route_cost - S_EPS > L:
                        if __debug__:
                            log(DEBUG,
                                "Insertion would break L constraint, skip.")
                        continue
                    route_cost = new_route_cost
                    route_l_updated = True
                else:
                    route_l_updated = False
                    route_cost = UB_route_cost
                    new_route = route + [i, 0]

                # accept including node i
                route = new_route[:-1]
                if C: route_demand += d[i]
                unrouted.remove(i)

                if __debug__:
                    log(
                        DEBUG, "Inserted n%d to create a route %s (%.2f)." %
                        (i, route, route_cost))

            # if L is not set, optimize TSP after the route is full
            if not route_l_updated:
                new_route, route_cost = solve_tsp(D, route)
                route = new_route[:-1]
                if __debug__:
                    if not L:
                        log(
                            DEBUG - 1, "Got TSP solution %s (%.2f)" %
                            (str(route + [0]), route_cost))
            if __debug__:
                log(
                    DEBUG, "Route %s (%.2f) complete.\n" %
                    (str(route + [0]), route_cost))

            total_cost += route_cost
            sol += route
    except KeyboardInterrupt:  #or SIGINT
        interrupted_sol = sol + routes2sol([n]
                                           for n in unrouted if n not in sol)
        raise KeyboardInterrupt(interrupted_sol)

    sol += [0]

    if __debug__:
        log(DEBUG,
            "Phase 1 solution %s (%.2f) complete.\n" % (str(sol), total_cost))
        log(DEBUG - 1,
            "Pass on route seeds %s to Phase 2.\n" % str(route_seeds))

    return route_seeds, sol, total_cost, rr
Пример #5
0
def _phase_two(mu_multiplier,
               route_seeds,
               D,
               d,
               C,
               L,
               rr,
               choose_most_associated_route=True,
               repeated_association_with_n_routes=1):
    ## Step 0: reuse seed nodes from phase 1 to act as route seed points
    N = len(D)
    K = len(route_seeds)

    customer_nodes = list(range(1, N))
    if rr is not None:
        #->stochastic version, resolve the ties randomly
        shuffle(customer_nodes)
    unrouted_nodes = OrderedSet(customer_nodes)
    unrouted_nodes.difference_update(route_seeds)

    # routes are stored in dict with a key of route seed,
    #  the route, demand, cost, and if it is updated are all stored
    routes = [
        RouteState(
            [0, rs],  #initial route
            d[rs] if C else 0,  #initial demand 
            D[0, rs] + D[rs, 0],  #initial cost
            True) for rs in route_seeds
    ]

    insertion_infeasible = [[False] * N for rs in route_seeds]

    if __debug__:
        log(
            DEBUG, "## Parallel route building phase with seeds %s ##" %
            str(list(route_seeds)))

    ## Step 1.1: vectorized calculation of eps
    # TODO: this also calculates eps for depot and seeds. Omitting those would
    #  be possible and save few CPU cycles, but it would make indexing more
    #  complex and because accuracy>simplicity>speed, it is the way it is.

    eps = (np.tile(D[[0], :],
                   (K, 1)) + mu_multiplier * D[:, route_seeds].transpose() -
           np.tile(D[0, route_seeds], (N, 1)).transpose())

    associate_to_nth_best_route = 1
    insertions_made = False
    route_seed_idxs = []
    insertions_made = False
    first_try = True

    try:
        while unrouted_nodes:
            ## Main while loop bookkeeping
            if not route_seed_idxs:
                idxs = list(range(K))
                if rr is not None:  #->stocastic version, construct routes in random order
                    shuffle(idxs)
                route_seed_idxs = deque(idxs)

                if not first_try:
                    # The CMT1979 exits when all routes have been tried
                    if repeated_association_with_n_routes is None:
                        break

                    if not insertions_made:
                        associate_to_nth_best_route += 1  # for the next round
                    if associate_to_nth_best_route>\
                       repeated_association_with_n_routes:
                        break

                first_try = False
                insertions_made = False

            ## Step 2.1: Choose a (any) route to add customers to.

            # some nodes may have been routed, update these
            eps_unrouted = eps[:, unrouted_nodes]

            ## Step 1.2: Associate each node to a route
            # note: the assignments cannot be calculated beforehand, as we do
            #  not know which customers will be (were?) "left over" in the
            #  previous route building steps 3.

            if associate_to_nth_best_route == 1 or len(route_seed_idxs) == 1:
                r_stars_unrouted = np.argmin(eps_unrouted[route_seed_idxs, :],
                                             axis=0)
            else:
                ## note: an extension for the deterministic variant,
                # get smallest AND 2. smallest at the same time using argpartition
                #top = np.argsort(eps, axis=0)[:associate_with_n_routes, :]
                #r_stars = [ top[i,:] for i in range(associate_with_n_routes) ]
                if len(route_seed_idxs) < associate_to_nth_best_route:
                    route_seed_idxs = []
                    continue

                #take_nth = min(len(route_seed_idxs), associate_to_nth_best_route)-1
                take_nth = associate_to_nth_best_route - 1
                reorder_rows_per_col_idxs = np.argpartition(
                    eps_unrouted[route_seed_idxs, :], take_nth, axis=0)
                nth_best, unrouted_node_order = np.where(
                    reorder_rows_per_col_idxs == take_nth)
                r_stars_unrouted = nth_best[np.argsort(unrouted_node_order)]

            if choose_most_associated_route:
                unique, counts = np.unique(r_stars_unrouted,
                                           return_counts=True)
                seed_idx_idx = unique[np.argmax(counts)]
                route_seed_idx = route_seed_idxs[seed_idx_idx]
                route_seed_idxs.remove(route_seed_idx)
                associated_cols = list(
                    np.where(r_stars_unrouted == seed_idx_idx)[0])
            else:
                route_seed_idx = route_seed_idxs.popleft()
                associated_cols = list(np.where(r_stars_unrouted == 0)[0])

            route, route_demand, route_cost, route_l_updated = routes[
                route_seed_idx]

            ## Step 2.2: Vectorized calculation of sigma score for the customers
            #             associated to the chosen route.
            eps_bar = eps_unrouted[route_seed_idx, associated_cols]

            # NOTE: CMT 1979 does not specify what happens if S is empty, we assume
            #  we need (and can) omit the calculation of eps_prime in this case.

            brdcast_rs_idxs = [[rsi] for rsi in route_seed_idxs]
            if route_seed_idxs:
                eps_prime = np.min(eps_unrouted[brdcast_rs_idxs,
                                                associated_cols],
                                   axis=0)
                sigmas = eps_prime - eps_bar
            else:
                # last route, try to add rest of the nodes
                eps_prime = None
                sigmas = -eps_bar

            col_to_node = [unrouted_nodes[int(c)] for c in associated_cols]
            sigma_ls = list(zip(sigmas.tolist(), col_to_node))
            sigma_ls.sort(reverse=True)

            if __debug__:
                log(
                    DEBUG,
                    "Assigning associated nodes %s to a route %s (seed n%d)" %
                    (str(col_to_node), str(route + [0]),
                     route_seeds[route_seed_idx]))

            ## Step 3: insert feasible customers from the biggest sigma first
            for sigma, l_star in sigma_ls:
                if __debug__:
                    log(DEBUG-1, "Check feasibility of inserting "+\
                        "n%d with sigma=%.2f"%(l_star,sigma))

                if C and route_demand + d[l_star] - C_EPS > C:
                    if __debug__:
                        log(DEBUG - 1, "Insertion would break C constraint.")
                    continue

                # use cached L feasibility check
                if L and insertion_infeasible[route_seed_idx][l_star]:
                    continue

                # Do not run TSP algorithm after every insertion, instead calculate
                #  a simple a upper bound for the route_cost and use that.
                UB_route_cost = (route_cost - D[route[-1], 0] +
                                 D[route[-1], l_star] + D[l_star, 0])
                if L and UB_route_cost - S_EPS > L:

                    # check the real TSP cost
                    new_route, new_route_cost = solve_tsp(D, route + [l_star])

                    if __debug__:
                        log(
                            DEBUG - 1, "Got TSP solution %s (%.2f)" % (
                                str(new_route),
                                new_route_cost,
                            ))

                    if new_route_cost - S_EPS > L:
                        if __debug__:
                            log(DEBUG - 1,
                                "DEBUG: Insertion would break L constraint.")
                        insertion_infeasible[route_seed_idx][l_star] = True
                        continue

                    route_cost = new_route_cost
                    route = new_route[:-1]
                    route_l_updated = True
                else:
                    route_l_updated = False
                    route_cost = UB_route_cost
                    route = route + [l_star]

                if C: route_demand += d[l_star]
                unrouted_nodes.remove(l_star)
                insertions_made = True

                if __debug__:
                    log(DEBUG,
                        "Inserted n%d to create a route %s." % (l_star, route))

            # All feasible insertions of the associated customers is done, record
            #  the modified route.
            if insertions_made:
                routes[route_seed_idx] = RouteState(
                    route,  #updated route
                    route_demand,  #updated demand
                    route_cost,  #updated cost 
                    route_l_updated)  #cost state
    except KeyboardInterrupt:  #or SIGINT
        rs_sol, _ = _routestates2solution(routes, D)
        interrupted_sol = rs_sol[:-1] + routes2sol(
            [n] for n in unrouted_nodes if n not in rs_sol)
        raise KeyboardInterrupt(interrupted_sol)

    ## Step 4: Redo step 1 or construct the solution and exit
    if len(unrouted_nodes) > 0:
        if __debug__:
            log(
                DEBUG,
                "Phase 2 failed to create feasbile solution with %d routes." %
                K)
            log(DEBUG - 1,
                "Nodes %s remain unrouted." % str(list(unrouted_nodes)))
        return 0, None, None, rr
    else:
        sol, total_cost = _routestates2solution(routes, D)
        if __debug__:
            log(
                DEBUG, "Phase 2 solution %s (%.2f) complete." %
                (str(sol), total_cost))
        return K, sol, total_cost, rr