コード例 #1
0
 def test_2pm_updated_route_capacity(self):
     # route 2 capacity 3.0 , route 1 capacity 4.0 (in _make_improving_move)
     #  node #4 is moved and with it the demand of 1.4 from r1 to r2
     d = [0.0, 0.7, 1.0, 1.3, 1.4, 1.5, 0.5, 0.6]
     result = self._make_improving_2pm_move(C=5.0, d=d)
     for rd in result:
         self.assertAlmostEqual(
             totald(rd[0], d),
             rd[2],
             msg=
             "original route cost + savings should match recalculated route cost"
         )
コード例 #2
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
コード例 #3
0
def _calculate_penalty(sol_or_route, D, d, C):
    penalty = 0
    routes = sol2routes(sol_or_route)
    for r in routes:
        penalty += C - totald(r, d) * objf(r, D)
    return penalty
コード例 #4
0
ファイル: petalvrp.py プロジェクト: mschmidt87/VeRyPy
def _regain_feasibility(to_move, r1_delta, r2_delta, rd1_aftermove, ri1, ri2,
                        discard_at_most, d_excess, l_excess, route_datas, D, d,
                        C, L):
    """ From Foster & Ryan (1976) "The result of such a (1 point) move may
    be to cause the receiving route to exceed the mileage or capacity 
    restriction and so it must be permitted to discard deliveries to regain
    feasibility. ... these ... are in turn relocated in the current solution
    schedule without causing further infeasibility and that the total effect 
    of all the relocations must be a reduction in the mileage."
    
    Thus, after the first of the stored 1 point moves causes the solution
    to become infeasible, this function tries to regain the feasibility by
    redistributing some of the customers from the recieving route to other 
    routes so that the moves still produce an improved solution. However,
    to keep the entire operation improving, the redistribution should not
    increase the cost more than delta_slack."""

    ## REPLACE MOVE
    # there is no room, have to replace one *OR MORE* nodes
    # We need to check removing all combinations of customers to make room and
    #  store those that still would remove the problem.

    route2, r2_l, r2_d, _ = route_datas[ri2]
    assert route2[0] == 0 and route2[
        -1] == 0, "All routes must start and end to the depot"
    r2_min_d = min(d[n] for n in route2[1:-1]) if d else 0

    # First route is changed and reserve an empty route to receive the nodes.
    new_empty_rd = RouteData()
    routes_with_slack = [rd for ri, rd in enumerate(route_datas)
                        if (ri!=ri1 and ri!=ri2 and (not C or C-rd.demand+C_EPS>r2_min_d))]\
                        +[rd1_aftermove,new_empty_rd]

    # A depth first search (no recursion) for removing JUST enough customers
    if d:
        stack = [(i + 1, [n], d[n]) for i, n in enumerate(route2[1:-1])]
    else:
        stack = [(i + 1, [n], None) for i, n in enumerate(route2[1:-1])]

    improving_rds = []
    while stack:
        last_n_i, to_remove_ns, to_remove_d = stack.pop()

        # We have to recalculate the delta_r2 as removing nodes almost surely
        #  will change the best position to insert to.
        new_route2 = [n for n in route2 if n not in to_remove_ns]
        new_r2_delta = float('inf')
        new_r2_l = 0.0
        best_j = None
        for j in xrange(1, len(new_route2)):
            insert_after = new_route2[j - 1]
            insert_before = new_route2[j]
            new_r2_l += D[insert_after, insert_before]

            delta = +D[insert_after,to_move]\
                    +D[to_move,insert_before]\
                    -D[insert_after,insert_before]

            if delta < new_r2_delta:
                new_r2_delta = delta
                best_j = j
        to_remove_l = (r2_l + r2_delta) - (new_r2_l + new_r2_delta)
        new_route2 = new_route2[:best_j] + [to_move] + new_route2[best_j:]

        if (not d_excess or to_remove_d+C_EPS >= d_excess) and\
           (not l_excess or to_remove_l+S_EPS >= l_excess):

            # Redistributing ALWAYS increases the cost at least a little,
            #  it makes no sense to even try sometimes.
            # If all of the improvements goes to inserting to_move customer,
            #  there is none left to try to redistribute.
            delta_slack = -(r1_delta + new_r2_delta)
            if delta_slack + S_EPS < 0:
                continue

            # After removing customers in to_remove_ns route2 becomes feasible,
            #  now we have to check if redistributing the removed back to the
            #  solution is possible.

            to_redistribute_r = [0] + to_remove_ns + [0]
            to_redisribute_rd = RouteData(to_redistribute_r,
                                          objf(to_redistribute_r, D),
                                          totald(to_redistribute_r, d))

            result = do_redistribute_move(to_redisribute_rd,
                                          routes_with_slack,
                                          D,
                                          d,
                                          C,
                                          L,
                                          strategy=LSOPT.BEST_ACCEPT,
                                          recombination_level=0,
                                          best_delta=delta_slack)
            redistribute_delta = result[-1]
            if redistribute_delta is not None:

                new_r2_d = r2_d - to_remove_d + d[to_move] if d else 0
                new_rd2 = RouteData(new_route2, new_r2_l + new_r2_delta,
                                    new_r2_d)
                improving_rds.append(new_rd2)
                improving_rds += result[1:-1]  # includes modified rd1

            #TODO: if one would like to explore all discard combinations,
            # one would do branching also here or at least if the redisribute
            # fails.

        elif not discard_at_most or len(to_remove_ns) < discard_at_most:
            # branch out
            for candidate_j, candidate_n in enumerate(route2[last_n_i + 1:-1]):
                stack.append(
                    (last_n_i + 1 + candidate_j, to_remove_ns + [candidate_n],
                     to_remove_d + d[candidate_n] if d else None))

    return improving_rds