def test_redistribute_to_one_route(self):
        r1 = [0, 1, 4, 6, 0]
        rd1_redistribute = RouteData(r1, route_l(r1, self.D),
                                     route_d(r1, self.d), None)
        r2 = [0, 2, 3, 5, 7, 0]
        rd2_recieving = RouteData(r2, route_l(r2, self.D), route_d(r2, self.d),
                                  None)

        result = do_redistribute_move(rd1_redistribute,
                                      rd2_recieving,
                                      self.D,
                                      strategy=LSOPT.FIRST_ACCEPT)
        self.assertEqual(
            len(result), 3,
            "The redistribute operator should return the redistributed and the new combined routes and the delta"
        )
        self.assertEqual(
            result[1].route, [0, 2, 3, 5, 7, 1, 4, 6, 0],
            "It should be possible to insert all and they sould be appended to the route"
        )

        result = do_redistribute_move(rd1_redistribute,
                                      rd2_recieving,
                                      self.D,
                                      strategy=LSOPT.BEST_ACCEPT)
        self.assertEqual(
            len(result), 3,
            "The redistribute operator should return the redistributed and the new combined routes and the delta"
        )
        self.assertEqual(result[1].route, [0, 1, 2, 3, 4, 5, 6, 7, 0],
                         "It should be possible to insert all")
def _refine_solution(sol, D, d, C, L, minimize_K):
    # refine until stuck at a local optima
    local_optima_reached = False
    while not local_optima_reached:
        sol = without_empty_routes(sol)
        if not minimize_K:
            sol.append(0)  #make sure there is an empty route to move the pt to

        # improve with relocation and keep 2-optimal
        sol = do_local_search([do_1point_move, do_2opt_move], sol, D, d, C, L,
                              LSOPT.BEST_ACCEPT)

        # try to redistribute the route with smallest demand
        sol = without_empty_routes(sol)
        routes = RouteData.from_solution(sol, D, d)
        min_rd = min(routes, key=lambda rd: rd.demand)
        routes.remove(min_rd)

        if not minimize_K:
            routes.append(RouteData())

        if __debug__:
            log(
                DEBUG, "Applying do_redistribute_move on %s (%.2f)" %
                (str(sol), objf(sol, D)))

        redisribute_result = do_redistribute_move(
            min_rd,
            routes,
            D,
            d,
            C,
            L,
            strategy=LSOPT.FIRST_ACCEPT,
            #Note: Mole and Jameson do not specify exactly
            # how the redistribution is done (how many
            # different combinations are tried).
            # Increase the recombination_level if the
            # for more agressive and time consuming search
            # for redistributing the customers on other
            # routes.
            recombination_level=0)
        redisribute_delta = redisribute_result[-1]

        if (redisribute_delta is not None) and\
           (minimize_K or redisribute_delta<0.0):

            updated_sol = RouteData.to_solution(redisribute_result[:-1])
            if __debug__:
                log(DEBUG - 1,
                    ("Improved from %s (%.2f) to %s (%.2f)" %
                     (sol, objf(sol, D), updated_sol, objf(updated_sol, D))) +
                    "using inter route heuristic do_redistribute_move\n")
            sol = updated_sol
        else:
            local_optima_reached = True
            if __debug__:
                log(DEBUG - 1, "No move with do_redistribute_move\n")
    return sol
    def test_insert_from_empty(self):
        rd1 = RouteData()
        r2 = [0, 1, 3, 4, 0]
        rd2 = RouteData(r2, route_l(r2, self.D), route_d(r2, self.d), None)

        _, result_rd, result_delta = do_insert_move(rd1, rd2, self.D)
        self.assertEqual(result_rd.route, [0, 1, 3, 4, 0],
                         "All should be inserted")
 def _make_improving_move(self, r1, r2, C=None, d=None, L=None):
     D = self.D
     r1rd = RouteData(r1, route_l(r1, D), route_d(r1, d), None)
     r2rd = RouteData(r2, route_l(r2, D), route_d(r2, d), None)
     return do_2optstar_move(r1rd,
                             r2rd,
                             D,
                             d=d,
                             C=C,
                             L=L,
                             strategy=LSOPT.BEST_ACCEPT)
 def _make_improving_move(self, C=None, d=None, L=None):
     D = self.D
     r1 = [0, 5, 6, 4, 0]
     r2 = [0, 1, 2, 3, 7, 0]
     r1rd = RouteData(r1, route_l(r1, D), route_d(r1, d), None)
     r2rd = RouteData(r2, route_l(r2, D), route_d(r2, d), None)
     return do_2point_move(r1rd,
                           r2rd,
                           D,
                           d=d,
                           C=C,
                           L=L,
                           strategy=LSOPT.BEST_ACCEPT)
 def test_non_improving_move(self):
     D = self.D
     r1 = [0, 5, 6, 7, 0]
     r2 = [0, 1, 2, 3, 4, 0]
     r1d = RouteData(r1, route_l(r1, D), 3, r1[:-1])
     r2d = RouteData(r2, route_l(r2, D), 4, r2[:-1])
     result = do_2point_move(r1d, r2d, D)
     self.assertEqual(
         result, (None, None, None),
         "r1, r2 configuration should already be at local optima")
     result = do_2point_move(r2d, r1d, D)
     self.assertEqual(
         result, (None, None, None),
         "r1, r2 configuration should already be at local optima")
 def _insert_one(self,
                 strategy,
                 result_target,
                 msg_if_fail,
                 C=None,
                 L=None,
                 to_insert=2):
     r1 = [0, 1, 3, 4, 0]
     rd1 = RouteData(r1, route_l(r1, self.D), route_d(r1, self.d), None)
     _, result_rd, result_delta = do_insert_move(to_insert,
                                                 rd1,
                                                 self.D,
                                                 C=C,
                                                 d=self.d,
                                                 L=L,
                                                 strategy=strategy)
     result_route = None if (result_rd is None) else result_rd.route
     self.assertEqual(result_route, result_target, msg_if_fail)
     if result_delta is not None:
         self.assertAlmostEqual(
             rd1.cost + result_delta,
             result_rd.cost,
             msg="The delta based and stored objective functions differ")
         self.assertAlmostEqual(
             rd1.cost + result_delta,
             route_l(result_rd.route, self.D),
             msg=
             "The delta based and recalculated objective functions differ")
    def test_until_no_improvements_move(self):
        D = self.D
        r1 = [0, 4, 6, 1, 0]
        r2 = [0, 7, 2, 3, 5, 0]
        r1d = RouteData(r1, route_l(r1, D), -1, r1[:-1])
        r2d = RouteData(r2, route_l(r2, D), -1, r2[:-1])
        while True:
            result = do_2point_move(r1d, r2d, D)
            if result[0] is None:
                break
            # unpack result
            r1d, r2d, delta = result

        # unconstrained should run until other route is empty
        self.assertEqual(r2d[0], [0, 1, 2, 3, 4, 0],
                         "should reach a local optima")
    def test_redistribute_to_two_routes(self):
        r1 = [0, 1, 4, 6, 0]
        rd1_redistribute = RouteData(r1, route_l(r1, self.D),
                                     route_d(r1, self.d), None)
        r2 = [0, 2, 3, 0]
        rd2_recieving = RouteData(r2, route_l(r2, self.D), route_d(r2, self.d),
                                  None)

        r3 = [0, 7, 5, 0]
        rd3_recieving = RouteData(r3, route_l(r3, self.D), route_d(r3, self.d),
                                  None)

        # depending on how the recombinations are made, the results differ
        FI = LSOPT.FIRST_ACCEPT
        BE = LSOPT.BEST_ACCEPT
        tests = [(0, "FIRST", FI, ([0, 2, 3, 1, 4, 0], [0, 7, 5, 6, 0])),
                 (0, "BEST", BE, ([0, 1, 2, 3, 4, 0], [0, 7, 6, 5, 0])),
                 (1, "FIRST", FI, ([0, 2, 3, 4, 1, 0], [0, 7, 5, 6, 0])),
                 (1, "BEST", BE, ([0, 1, 2, 3, 4, 0], [0, 7, 6, 5, 0])),
                 (2, "FIRST", FI, ([0, 2, 3, 1, 4, 0], [0, 7, 5, 6, 0])),
                 (2, "BEST", BE, ([0, 1, 2, 3, 4, 0], [0, 7, 6, 5, 0])),
                 (3, "FIRST", FI, ([0, 2, 3, 4, 1, 0], [0, 7, 5, 6, 0])),
                 (3, "BEST", BE, ([0, 1, 2, 3, 4, 0], [0, 7, 6, 5, 0]))]
        for recombination_level, strategy_name, strategy, target_result in tests:
            result = do_redistribute_move(
                rd1_redistribute, [rd2_recieving, rd3_recieving],
                self.D,
                C=4.0,
                d=self.d,
                strategy=strategy,
                recombination_level=recombination_level)
            #print("QUALITY", strategy_name, "/", recombination_level, "delta", result[-1])
            self.assertEqual(
                len(result), 4,
                "The redistribute operator should return the redistributed and the new combined routes and the delta"
            )
            self.assertEqual(
                result[1].route, target_result[0],
                "It should be possible to insert all and they sould be appended to the route on recombination level %d with strategy %s"
                % (recombination_level, strategy_name))
            self.assertEqual(
                result[2].route, target_result[1],
                "It should be possible to insert all and they sould be appended to the route on recombination level %d with strategy %s"
                % (recombination_level, strategy_name))
    def _insert_many(self,
                     strategy,
                     result_target,
                     msg_if_fail,
                     C=None,
                     L=None):
        to_insert = [3, 2]
        r1 = [0] + to_insert + [0]
        rd1 = RouteData(r1, route_l(r1, self.D), route_d(r1, self.d), None)
        r2 = [0, 1, 4, 0]
        rd2 = RouteData(r2, route_l(r2, self.D), route_d(r2, self.d), None)

        _, list_input_result_rd, list_input_result_delta = do_insert_move(
            to_insert, rd2, self.D, C=C, d=self.d, L=L, strategy=strategy)
        _, rd_input_result_rd, rd_input_result_delta = do_insert_move(
            rd1, rd2, self.D, C=C, d=self.d, L=L, strategy=strategy)
        list_input_result_route = None if (
            list_input_result_rd is None) else list_input_result_rd.route
        rd_input_result_route = None if (
            rd_input_result_rd is None) else rd_input_result_rd.route
        self.assertEqual(list_input_result_route, result_target, msg_if_fail)
        self.assertEqual(
            list_input_result_route, rd_input_result_route,
            "Inserting from a list or RouteData should lead to same result")
        if list_input_result_delta is not None:
            self.assertAlmostEqual(
                rd2.cost + list_input_result_delta,
                list_input_result_rd.cost,
                msg="The delta based and stored objective functions differ")
            self.assertAlmostEqual(
                rd2.cost + list_input_result_delta,
                route_l(list_input_result_rd.route, self.D),
                msg=
                "The delta based and recalculated objective functions differ")
        if rd_input_result_delta is not None:
            self.assertAlmostEqual(
                rd2.cost + rd_input_result_delta,
                rd_input_result_rd.cost,
                msg="The delta based and stored objective functions differ")
            self.assertAlmostEqual(
                rd2.cost + rd_input_result_delta,
                route_l(rd_input_result_rd.route, self.D),
                msg=
                "The delta based and recalculated objective functions differ")
    def test_redistribute_does_not_fit(self):
        demands = [0.0, 1, 1, 1, 1, 1, 1, 1]
        r1 = [0, 1, 2, 0]
        rd1_redistribute = RouteData(r1, route_l(r1, self.D),
                                     route_d(r1, demands), None)
        r2 = [0, 3, 4, 5, 6, 7, 0]
        rd2_recieving = RouteData(r2, route_l(r2, self.D),
                                  route_d(r2, demands), None)

        result = do_redistribute_move(rd1_redistribute, [rd2_recieving],
                                      self.D,
                                      C=6.0,
                                      d=self.d,
                                      strategy=LSOPT.BEST_ACCEPT,
                                      recombination_level=1)

        # Fails -> None,None...None is returned (no partial fits)
        self.assertTrue(
            all(rp == None for rp in result),
            "The redistribute operator should return the redistributed and the new combined routes and the delta"
        )
    def test_redistribute_find_best_fit(self):
        demands = [0.0, 1, 1, 2, 3, 1, 3, 1]
        r1 = [0, 1, 4, 6, 0]
        rd1_redistribute = RouteData(r1, route_l(r1, self.D),
                                     route_d(r1, demands), None)
        r2 = [0, 2, 3, 0]
        rd2_recieving = RouteData(r2, route_l(r2, self.D),
                                  route_d(r2, demands), None)

        r3 = [0, 5, 0]
        rd3_recieving = RouteData(r3, route_l(r3, self.D),
                                  route_d(r3, demands), None)

        r4 = [0, 7, 0]
        rd4_recieving = RouteData(r4, route_l(r4, self.D),
                                  route_d(r4, demands), None)

        result = do_redistribute_move(
            rd1_redistribute, [rd2_recieving, rd3_recieving, rd4_recieving],
            self.D,
            C=4.0,
            d=demands,
            strategy=LSOPT.BEST_ACCEPT,
            recombination_level=1)

        self.assertEqual(
            len(result), 5,
            "The redistribute operator should return the redistributed and the new combined routes and the delta"
        )
        self.assertEqual(result[0].route, [0, 0],
                         "It should be possible to insert all customers")
        self.assertEqual(_normalise_route_order(result[1].route),
                         [0, 1, 2, 3, 0],
                         "n1 should be redistributed to first route")
        self.assertEqual(_normalise_route_order(result[2].route), [0, 4, 5, 0],
                         "n4 should be redistributed to first route")
        self.assertEqual(_normalise_route_order(result[3].route), [0, 6, 7, 0],
                         "n6 should be redistributed to first route")
def do_1point_move(
        route1_data,
        route2_data,
        D,
        d=None,
        C=None,
        L=None,  # constraints
        strategy=LSOPT.FIRST_ACCEPT,
        best_delta=None):
    """ Move one point from route1 to route2. Tries all possible combinations
    of moving a node from route 1 to any valid position in route 2. Sometimes
    called "relocate" (e.g., Bräysy & M. Gendreau 2005, Savelsbergh 1992), but
    here we use the name "one point move" (Groër et al 2010) to differentiate
    from the intra-route version.
    
    If an improving move was found and made, operation returns new routes in
    same format as route_data inputs, but if C and d are not given the 3.
    field of the tuple is None. If there is no improving move, returns None.

    Groër, C., Golden, B. and Wasil, E., 2010. A library of local search
     heuristics for the vehicle routing problem. Mathematical Programming
     Computation, 2(2), pp.79-101.
    Bräysy, O. & Gendreau, M. 2005. Vehicle Routing Problem, Part I: Route 
     Construction and Local Search Algorithms Transportation Science 39(1),
     pp. 104–118
    Savelsbergh, M. W. P. 1992. The vehicle routing problem with time windows:
     Minimizing route duration. J.Comput. 4 146-154
    """

    if route1_data == route2_data:
        return None, None, None

    # unpack route, current cost, and current demand
    route1, r1_l, r1_d, _ = route1_data
    route2, r2_l, r2_d, _ = route2_data

    if not best_delta:
        best_delta = 0
    best_move = None
    accept_move = False

    for i in xrange(1, len(route1) - 1):
        remove_after = route1[i - 1]
        to_move = route1[i]
        remove_before = route1[i + 1]

        remove_delta = D[remove_after,remove_before]\
                      -D[remove_after,to_move]\
                      -D[to_move,remove_before]

        # capacity constraint feasibility check
        if C and r2_d + d[to_move] - C_EPS > C:
            continue

        for j in xrange(1, len(route2)):
            insert_after = route2[j - 1]
            insert_before = route2[j]


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

            # route cost constraint feasibility check
            if L and r2_l + insert_delta - S_EPS > L:
                continue

            delta = remove_delta + insert_delta
            if delta + S_EPS < best_delta:
                best_delta = delta
                best_move = (i, j, remove_delta, insert_delta)
                if strategy == LSOPT.FIRST_ACCEPT:
                    accept_move = True
                    # break j-loop
                    break
        # break i-loop
        if accept_move:
            break

    if best_move:
        # unpack best move
        i, j, remove_delta, insert_delta = best_move
        to_move = route1[i]
        return (RouteData(route1[:i] + route1[i + 1:], r1_l + remove_delta,
                          None if not C else r1_d - d[to_move]),
                RouteData(route2[:j] + [to_move] + route2[j:],
                          r2_l + insert_delta,
                          None if not C else r2_d + d[to_move]),
                remove_delta + insert_delta)

    return None, None, None
def do_3optstar_3route_move(
        route1_data,
        route2_data,
        route3_data,
        D,
        demands=None,
        C=None,
        L=None,  # constraints
        strategy=LSOPT.FIRST_ACCEPT,
        best_delta=None):
    """ 3-opt* inter-route local search operation for the symmetric distances D
    Remove 3 edges from different routes and check if reconnecting them in any
    configuration would improve the solution, while also making sure the
    move does not violate constraints.
    
    For two route checks, the first and second route can be same i.e. 
    route1_data==route2_data. However, if route2_data==route3_data or 
    route1_data==route3_data a ValueError is raised.
    """

    if route1_data==route2_data or\
       route2_data==route3_data or\
       route1_data==route3_data:
        raise ValueError("Use do_3opt_move to find intra route moves")

# make sure we have the aux data for constant time feasibility checks
    if not isinstance(route1_data, RouteData):
        route1_data = RouteData(*route1_data)
    if not isinstance(route2_data, RouteData):
        route2_data = RouteData(*route2_data)
    if not isinstance(route3_data, RouteData):
        route3_data = RouteData(*route3_data)
    if not route1_data.aux_data_updated:
        route1_data.update_auxiliary_data(D, demands)
    if not route2_data.aux_data_updated:
        route2_data.update_auxiliary_data(D, demands)
    if not route3_data.aux_data_updated:
        route3_data.update_auxiliary_data(D, demands)

    if not best_delta:
        best_delta = 0
    best_move = None
    accept_move = False

    # segment end nodes and cumulative d and l at those nodes
    # Note the indexing of segment end nodes here:
    #
    #    __0/i  1__      route 1
    #   /          \
    # n0---2/j  3--n0    route 2
    #   \__      __/
    #      4/k  5        route 3
    #
    end_n = [0] * 6
    cum_d = [0] * 6
    cum_l = [0] * 6

    for i in xrange(0, len(route1_data.route) - 1):
        end_n[0] = route1_data.route[i]
        end_n[1] = route1_data.route[i + 1]

        # make it so that i<j if route1==route2
        for j in xrange(0, len(route2_data.route) - 1):

            # the edge endpoints
            end_n[2] = route2_data.route[j]
            end_n[3] = route2_data.route[j + 1]
            if C:
                cum_d[0] = route1_data.fwd_d[i]
                cum_d[1] = route1_data.rwd_d[i + 1]
                cum_d[2] = route2_data.fwd_d[j]
                cum_d[3] = route2_data.rwd_d[j + 1]
            if L:
                cum_l[0] = route1_data.fwd_l[i]
                cum_l[1] = route1_data.rwd_l[i + 1]
                cum_l[2] = route2_data.fwd_l[j]
                cum_l[3] = route2_data.rwd_l[j + 1]

            for k in xrange(0, len(route3_data.route) - 1):

                # the edge endpoints
                end_n[4] = route3_data.route[k]
                end_n[5] = route3_data.route[k + 1]
                if C:
                    cum_d[4] = route3_data.fwd_d[k]
                    cum_d[5] = route3_data.rwd_d[k + 1]
                if L:
                    cum_l[4] = route3_data.fwd_l[k]
                    cum_l[5] = route3_data.rwd_l[k + 1]

                removed_weights = D[end_n[0],end_n[1]]+\
                                  D[end_n[2],end_n[3]]+\
                                  D[end_n[4],end_n[5]]

                for e1, e2, e3 in MOVES_3OPTSTAR_3ROUTES:
                    e1_wt = D[end_n[e1[0]], end_n[e1[1]]]
                    e2_wt = D[end_n[e2[0]], end_n[e2[1]]]
                    e3_wt = D[end_n[e3[0]], end_n[e3[1]]]

                    delta = e1_wt + e2_wt + e3_wt - removed_weights

                    if ((delta + S_EPS < best_delta)
                            and (not C or
                                 (cum_d[e1[0]] + cum_d[e1[1]] - C_EPS < C
                                  and cum_d[e2[0]] + cum_d[e2[1]] - C_EPS < C
                                  and cum_d[e3[0]] + cum_d[e3[1]] - C_EPS < C))
                            and
                        (not L or
                         (cum_l[e1[0]] + cum_l[e1[1]] + e1_wt - S_EPS < L and
                          cum_l[e2[0]] + cum_l[e2[1]] + e2_wt - S_EPS < L and
                          cum_l[e3[0]] + cum_l[e3[1]] + e3_wt - S_EPS < L))):
                        best_move = ((i, j, k), (e1, e2, e3), delta)
                        best_delta = delta

                        if strategy == LSOPT.FIRST_ACCEPT:
                            accept_move = True
                            break  # move loop
                if accept_move:
                    break  # k loop
            if accept_move:
                break  # j loop
        if accept_move:
            break  # i loop

    if best_move:
        # unpack the move
        (best_ijk, best_edges, best_delta) = best_move

        routes = [route1_data, route2_data, route3_data]
        ret = []
        for edge in best_edges:

            # Unfortunately, the splicing is a bit complex, but basic idea
            #  is to use the move and its edges in best_edges to get the
            #  route to splice from and if the splice should be inverted.
            #
            # edge[i]//2 gets the route index of the move endpoint
            # edge[i]%2 tells if it is the head segment or tail segment
            #
            #    ___a c___    r1
            #   /         \
            # n0           n1
            #   \___   ___/
            #       b d       r2
            #

            r1_idx = edge[0] // 2
            r2_idx = edge[1] // 2
            r1_i = best_ijk[r1_idx]
            r2_i = best_ijk[r1_idx]
            a = routes[r1_idx].route[r1_i]
            b = routes[r1_idx].route[r1_i + 1]
            c = routes[r2_idx].route[r2_i]
            d = routes[r2_idx].route[r2_i + 1]

            # combine the tails of route1 and route2
            if edge[0] % 2 and edge[1] % 2:
                new_route = (routes[r1_idx].route[None:r1_i:-1] +
                             routes[r2_idx].route[r2_i + 1:None:1])
                new_cost = (D[b, d] + routes[r1_idx].rwd_l[r1_i + 1] +
                            routes[r2_idx].rwd_l[r2_i + 1])
                new_demand = (routes[r1_idx].rwd_d[r1_i + 1] +
                              routes[r2_idx].rwd_d[r2_i + 1])
            # combine the head of route1 with the tail of route2
            elif not edge[0] % 2 and edge[1] % 2:
                new_route = (routes[r1_idx].route[None:r1_i + 1:1] +
                             routes[r2_idx].route[r2_i + 1:None:1])
                new_cost = (D[a, d], routes[r1_idx].fwd_l[r1_i] +
                            routes[r2_idx].rwd_l[r2_i + 1])
                new_demand = (routes[r1_idx].fwd_d[r1_i] +
                              routes[r2_idx].rwd_d[r2_i + 1])
            # combine the heads of route1 and route2
            elif not edge[0] % 2 and not edge[1] % 2:
                new_route = (routes[r1_idx].route[None:r1_i + 1:1] +
                             routes[r2_idx].route[r2_i:None:-1])
                new_cost = (D[a, c], routes[r1_idx].fwd_l[r1_i] +
                            routes[r2_idx].fwd_l[r2_i])
                new_demand = (routes[r1_idx].fwd_d[r1_i] +
                              routes[r2_idx].fwd_d[r2_i])
            else:
                assert False, "there is no move to combine the tail of"+\
                              "route1 with the (reversed) head of route2"

            ret.append(RouteData(new_route, new_cost, new_demand))
        ret.append(best_delta)
        return tuple(ret)

    return None, None, None, None
def do_3optstar_2route_move(
        route1_data,
        route2_data,
        D,
        demands=None,
        C=None,
        L=None,  # constraints
        strategy=LSOPT.FIRST_ACCEPT,
        best_delta=None):

    if route1_data == route2_data:
        raise ValueError("Use do_3opt_move to find intra route moves")

    # make sure we have the aux data for constant time feasibility checks
    if not isinstance(route1_data, RouteData):
        route1_data = RouteData(*route1_data)
    if not isinstance(route2_data, RouteData):
        route2_data = RouteData(*route2_data)
    if not route1_data.aux_data_updated:
        route1_data.update_auxiliary_data(D, demands)
    if not route2_data.aux_data_updated:
        route2_data.update_auxiliary_data(D, demands)

    if not best_delta:
        best_delta = 0
    best_move = None
    accept_move = False

    # segment end nodes and cumulative d and l at those nodes is specified
    # by the indices i,j,k. In the two route case this means:
    #
    #    _0/i  1_2/j  3_     route 1
    #   /               \
    # n0                n0
    #   \____      _____/
    #        4/k  5        route 2
    #
    end_n = [0] * 6
    cum_d = [0] * 6
    cum_l = [0] * 6

    for i in xrange(0, len(route1_data.route) - 1):
        end_n[0] = route1_data.route[i]
        end_n[1] = route1_data.route[i + 1]

        # make it so that i<j
        for j in xrange(i + 1, len(route1_data.route) - 1):

            # the edge endpoints
            end_n[2] = route2_data.route[j]
            end_n[3] = route2_data.route[j + 1]
            if C:
                cum_d[0] = route1_data.fwd_d[i]
                cum_d[1] = route1_data.rwd_d[i+1]\
                          -route1_data.rwd_d[j+1]
                cum_d[2] = route1_data.fwd_d[j]\
                           -route1_data.fwd_d[i]
                cum_d[3] = route1_data.rwd_d[j + 1]
            if L:
                cum_l[0] = route1_data.fwd_l[i]
                cum_l[1] = route1_data.rwd_l[i+1]\
                           -route1_data.rwd_l[j]
                cum_l[2] = route1_data.fwd_l[j]\
                           -route1_data.fwd_l[i+1]
                cum_l[3] = route1_data.rwd_l[j + 1]

            for k in xrange(0, len(route2_data.route) - 1):

                # the edge endpoints
                end_n[4] = route2_data.route[k]
                end_n[5] = route2_data.route[k + 1]
                if C:
                    cum_d[2] = route2_data.fwd_d[k]
                    cum_d[3] = route2_data.rwd_d[k + 1]
                if L:
                    cum_l[2] = route2_data.fwd_l[k]
                    cum_l[3] = route2_data.rwd_l[k + 1]

                removed_weights = D[end_n[0],end_n[1]]+\
                                  D[end_n[2],end_n[3]]+\
                                  D[end_n[4],end_n[5]]

                for e1, e2, e3 in MOVES_3OPTSTAR_2ROUTES:
                    e1_wt = D[end_n[e1[0]], end_n[e1[1]]]
                    e2_wt = D[end_n[e2[0]], end_n[e2[1]]]
                    e3_wt = D[end_n[e3[0]], end_n[e3[1]]]

                    delta = e1_wt + e2_wt + e3_wt - removed_weights

                    if ((delta + S_EPS < best_delta) and
                        (not C or (cum_d[e1[0]] + cum_d[e1[1]] - C_EPS < C
                                   and cum_d[e2[0]] + cum_d[e2[1]] +
                                   cum_d[e3[1]] - C_EPS < C)) and
                        (not L or
                         (cum_l[e1[0]] + cum_l[e1[1]] + e1_wt - S_EPS < L
                          and cum_l[e2[0]] + cum_l[e2[1]] + cum_l[e3[1]] +
                          e2_wt + e3_wt - S_EPS < L))):
                        best_move = ((i, j, k), (e1, e2, e3), delta)
                        best_delta = delta

                        if strategy == LSOPT.FIRST_ACCEPT:
                            accept_move = True
                            break  # move loop
                if accept_move:
                    break  # k loop
            if accept_move:
                break  # j loop
        if accept_move:
            break  # i loop

    if best_move:
        raise NotImplementedError("Not yet completely implemented")
def do_chain_move(
        route1_data,
        route2_data,
        route3_data,
        D,
        d=None,
        C=None,
        L=None,  # constraints
        strategy=LSOPT.FIRST_ACCEPT,
        best_delta=None):
    """ This is the "pair" operation described in Wren and Holliday (1972). 
    It involves moving a repacing a node on route 2 with a node on route 1.
    The replaced node is then inserted on route 3 (if able). Route 1!=2!=3 
    
    This is a very expensive operation, corresponding to 5-opt with chain
    length of 1, use with care.
    
    Wren, A. and 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), pp.333-344.
    """

    if not best_delta:
        best_delta = 0
    best_move = None
    accept_move = False

    route1, r1_l, r1_d, _ = route1_data
    route2, r2_l, r2_d, _ = route2_data
    route3, r3_l, r3_d, _ = route3_data

    for i in xrange(1, len(route1) - 1):

        remove_after = route1[i - 1]
        to_move = route1[i]
        remove_before = route1[i + 1]

        remove_delta =  +D[remove_after,remove_before]\
                        -D[remove_after,to_move]\
                        -D[to_move,remove_before]

        for j in xrange(1, len(route2) - 1):

            replace_after = route2[j - 1]
            to_replace = route2[j]
            replace_before = route2[j + 1]

            # can do capacity constraint feasibility check here
            if C and (r2_d - d[to_replace] + d[to_move] > C
                      or r3_d + d[to_replace] > C):
                continue

            replace_delta = +D[replace_after,to_move]\
                            +D[to_move,replace_before]\
                            -D[replace_after,to_replace]\
                            -D[to_replace,replace_before]

            if L and r2_l + replace_delta > L:
                continue

            for k in xrange(1, len(route3)):

                insert_after = route3[k - 1]
                insert_before = route3[k]

                insert_delta =  +D[insert_after, to_replace]\
                                +D[to_replace, insert_before]\
                                -D[insert_after,insert_before]

                if L and (r3_l + insert_delta > L):
                    continue

                # check if this is best so far
                delta = remove_delta + replace_delta + insert_delta
                if delta + S_EPS < best_delta:
                    best_delta = delta
                    best_move = (i, j, k, remove_delta, replace_delta,
                                 insert_delta)
                    if strategy == LSOPT.FIRST_ACCEPT:
                        accept_move = True
                        break  # k loop
            if accept_move:
                break  # j loop
        if accept_move:
            break  # i loop

    if best_move:
        # unpack best move
        i, j, k, remove_delta, replace_delta, insert_delta = best_move
        to_move = route1[i]
        to_replace = route2[j]
        return (RouteData(route1[:i] + route1[i + 1:], r1_l + remove_delta,
                          None if not C else r1_d - d[to_move]),
                RouteData(route2[:j] + [to_move] + route2[j + 1:],
                          r2_l + replace_delta, None if not C else r2_d -
                          d[to_replace] + d[to_move]),
                RouteData(route3[:k] + [to_replace] + route3[k:],
                          r3_l + insert_delta,
                          None if not C else r3_d + d[to_replace]), best_delta)
    return None, None, None, None
def do_insert_move(unrouted,
                   recieving_route_data,
                   D,
                   d=None,
                   C=None,
                   L=None,
                   strategy=LSOPT.FIRST_ACCEPT,
                   best_delta=None):
    """ Try to insert (unrouted) node(s) on the existing recieving route. The
    operation succeeds only if all customers can be inserted on the recieving
    route. The difference to the one point move is that there is no delta for
    removing the unrouted node from its RouteData / list.
    
    Returns a 3-tuple: An empty RouteData, A copy of the updated RouteData and
     the route cost (usually route length) delta; or (None,None,None) if the
     insertion fails.
    
    * unrouted
       Can be a single customer (int), a list of those, or a route (RouteData).
       If RouteData is given its .demand field must be set. 
    * recieving_route_data
       is the route (RouteData) that the customers to insert are inserted to.
       Its .demand field must be set. 
    * D,d,C,L
       the problem definition and constraints
    * strategy
       is the applied loca search strategy to use. Please note that if there is
       more than one customer to insert, the maximum route cost constraint L
       is set, and strategy is set to first accept, the best insertion position
       for the customers are still searched for to leave the most amount of
       room for subsequent insertions.
    """

    if type(unrouted) is int:
        unrouted_demand = d[unrouted] if C else 0
        unrouted = [unrouted]
    if isinstance(unrouted, RouteData):
        unrouted_demand = unrouted.demand if C else 0
        unrouted = unrouted.route[1:-1]
    elif C:
        unrouted_demand = sum(d[n] for n in unrouted)

    # make an ansatz for fitting the inserted customers into
    ansatz_route = list(recieving_route_data.route)
    ansatz_d = recieving_route_data.demand
    ansatz_l = recieving_route_data.cost
    total_delta = 0

    # is not possible because of constraint C
    if C and ansatz_d + unrouted_demand - C_EPS > C:
        return None, None, None

    for ni, node in enumerate(unrouted):
        best_insert_pos = None
        if strategy == LSOPT.BEST_ACCEPT or L:
            # need to find a place where it can be inserted (if at all)
            best_insert_delta = None
            for i in xrange(1, len(ansatz_route)):
                insert_after = ansatz_route[i - 1]
                insert_before = ansatz_route[i]

                insert_delta = +D[insert_after,node]\
                               +D[node,insert_before]\
                               -D[insert_after,insert_before]

                if not L or ansatz_l + insert_delta - S_EPS <= L:
                    if (best_insert_delta is None) or\
                       (insert_delta<best_insert_delta):
                        best_insert_pos = i
                        best_insert_delta = insert_delta
                    # to avoid complexity, only allow early termination with
                    #  FIRST_ACCEPT when customers are inserted one
                    #  by one
                    if strategy == LSOPT.FIRST_ACCEPT and ni == len(
                            unrouted) - 1:
                        break  # i loop
        else:
            # we know it fits, just append it
            best_insert_pos = -1
            insert_after = ansatz_route[-2]  # the last non-depot node
            best_insert_delta = +D[insert_after, node] + D[node,
                                                           0] - D[insert_after,
                                                                  0]

        # no L valid routing
        if best_insert_pos is None:
            return None, None, None
        else:
            if C: ansatz_d += d[node]
            ansatz_l += best_insert_delta
            # we found a valid insertion location accept it
            ansatz_route = ansatz_route[:best_insert_pos]\
                           +[node]\
                           +ansatz_route[best_insert_pos:]
            total_delta += best_insert_delta

            # must be better than the preset level (if applicable)
            if (best_delta is not None) and (total_delta + S_EPS > best_delta):
                return None, None, None

    return RouteData(), RouteData(ansatz_route, ansatz_l,
                                  ansatz_d), total_delta
Exemple #18
0
def _improvement_callback(route_data, callback_datastructures, sweep_rhos,
                          sweep_phis, sweep_J_pos, step_inc, routed):
    """ This callback implements the Gillett and Miller (1974) improvement
    heuristic. It involves trying to remove a node and insertion of several 
    candidates. Therefore, the algorithm involves Steps 8-15 in the appedix
    of Gillett and Miller (1974). """

    # unpack callback data structures (packed in pack_datastructures)
    N, D, NN_D, d, C, L, node_to_pos, pos_to_node, avr = callback_datastructures

    # do not try to improve, if the J node is already routed (already Swept
    #  full circle)
    node_J = pos_to_node[sweep_J_pos]
    if routed[node_J]:
        # nothing to do, no nodes added, no nodes removed, route is complete
        return route_data, [], [], True

    # unpack route information, (route, route_cost, route_demand)
    D1_route, D1, D1_demand, D1_nodes = route_data

    # G&M Step 8. This messy looking line vectorizes the minimization of
    #  R(K(I))+An(K(I))*AVR
    #
    # What makes it a little more messier, is that the angle is pointing
    #  "the right way" depending on the cw/ccw direction (encoded in step_inc)
    # +1 is there to convert indexing as there is no depot in node_rho_phis
    route_K_nodes = list(D1_nodes[1:])
    route_K_positions = [node_to_pos[n] for n in route_K_nodes]
    route_rhos = sweep_rhos[route_K_positions]
    route_phis = sweep_phis[route_K_positions]

    rem_choose_function = route_rhos + route_phis * avr
    to_remove_node_KII = route_K_nodes[np.argmin(rem_choose_function)]

    if __debug__:
        log(
            DEBUG - 2,
            "G&M improvement phase for route %s (%.2f). Trying to replace KII=n%d."
            % (str(D1_route), D1, to_remove_node_KII))
        log(
            DEBUG - 3, "This is due to R(K(I))+An(K(I)*AVR = %s" %
            str(zip(route_K_nodes, route_phis, list(rem_choose_function))))
    # take the node J-1 (almost always the node last added on the route)
    sweep_prev_of_J_pos = _step(sweep_J_pos, -step_inc, N - 2)
    prev_node_J = pos_to_node[sweep_prev_of_J_pos]

    # Get the insertion candidates
    try:
        candidate_node_JJX = next((node_idx
                                   for node_idx, _ in NN_D[prev_node_J]
                                   if not routed[node_idx]))
    except StopIteration:
        if __debug__:
            log(DEBUG - 2,
                "G&M Step 9, not enough unrouted nodes left for JJX.")
            log(DEBUG - 2, "-> EXIT with no changes")
        return route_data, [], [], False
    try:
        candidate_node_JII = next(
            (node_idx for node_idx, _ in NN_D[candidate_node_JJX]
             if (not routed[node_idx] and node_idx != candidate_node_JJX)))
    except StopIteration:
        candidate_node_JII = None

    # construct and route to get the modified route cost D2
    D2_route_nodes = OrderedSet(D1_nodes)
    D2_route_nodes.remove(to_remove_node_KII)
    D2_route_nodes.add(candidate_node_JJX)
    D2_route, D2 = solve_tsp(D, list(D2_route_nodes))
    D2_demand = D1_demand - d[to_remove_node_KII] + d[
        candidate_node_JJX] if C else 0

    ## G&M Step 9

    if not ((L and D2 - S_EPS < L) and (C and D2_demand - C_EPS <= C)):
        if __debug__:
            log(
                DEBUG - 2,
                "G&M Step 9, rejecting replacement of KII=n%d with JJX=n%d" %
                (to_remove_node_KII, candidate_node_JJX))
            log(
                DEBUG - 3, " which would have formed a route %s (%.2f)" %
                (str(D2_route), D2))
            if (C and D2_demand - C_EPS > C):
                log(DEBUG - 3, " violating the capacity constraint")
            else:
                log(DEBUG - 3, " violating the maximum route cost constraint")
            log(DEBUG - 2, " -> EXIT with no changes")

        # go to G&M Step 10 ->
        # no changes, no skipping, route complete
        return route_data, [], [], True

    ## G&M Step 11
    D3_nodes = OrderedSet()  # the min. dist. from 0 through J,J+1...J+4 to J+5
    D4_nodes = OrderedSet()  # the min. dist. /w JJX excluded, KII included
    D6_nodes = OrderedSet()  # the min. dist. /w JJX and JII excl., KII incl.
    JJX_in_chain = False
    JII_in_chain = False

    # step back so that the first node to lookahead is J
    lookahead_pos = sweep_prev_of_J_pos
    for i in range(5):
        lookahead_pos = _step(lookahead_pos, step_inc, N - 2)
        lookahead_node = pos_to_node[lookahead_pos]
        if routed[lookahead_node]:
            continue

        D3_nodes.add(lookahead_node)
        if lookahead_node == candidate_node_JJX:
            # inject KII instead of JJX
            D4_nodes.add(to_remove_node_KII)
            D6_nodes.add(to_remove_node_KII)
            JJX_in_chain = True
        elif lookahead_node == candidate_node_JII:
            D4_nodes.add(lookahead_node)
            JII_in_chain = True
        else:
            D4_nodes.add(lookahead_node)
            D6_nodes.add(lookahead_node)

    # if JJX was not in the sequence J, J+1, ... J+5
    if not JJX_in_chain:
        if __debug__:
            log(
                DEBUG - 2, "G&M Step 11, JJX=n%d not in K(J)..K(J+4)" %
                candidate_node_JJX)
            log(DEBUG - 3, " which consists of nodes %s" % str(list(D3_nodes)))
            log(DEBUG - 2, "-> EXIT with no changes")
        # go to G&M Step 10 ->
        # no changes, no skipping, route complete
        return route_data, [], [], True

    # The chain *end point* J+5
    last_chain_pos = _step(lookahead_pos, step_inc, N - 2)
    last_chain_node = pos_to_node[last_chain_pos]

    if routed[last_chain_node]:
        last_chain_node = 0

    # D3 -> EVALUATE the MINIMUM distance from 0 through J,J+1...J+4 to J+5
    _, D3 = _shortest_path_through_nodes(D, 0, last_chain_node, D3_nodes)
    # D4 -> DETERMINE the MINIMUM distance with JJX excluded, KII included
    _, D4 = _shortest_path_through_nodes(D, 0, last_chain_node, D4_nodes)

    if not (D1 + D3 < D2 + D4):
        ## G&M Step 12
        if __debug__:
            log(
                DEBUG - 2, "G&M Step 12, accept an improving move where " +
                "KII=n%d is removed and JJX=n%d is added" %
                (to_remove_node_KII, candidate_node_JJX))
            log(DEBUG - 3,
                " which forms a route %s (%.2f)" % (str(D2_route), D2))
            log(DEBUG - 2, " -> EXIT and continue adding nodes")

        ignored_nodes = [to_remove_node_KII]
        if candidate_node_JJX != node_J:
            ignored_nodes += [node_J]

        # go to G&M Step 4 ->
        # route changed, KII removed and skip current node J, not complete
        return RouteData(D2_route, D2, D2_demand, D2_route_nodes),\
               [candidate_node_JJX], ignored_nodes, False

    else:
        ## G&M Step 13

        # JII and JJX (checked earlier) should be in K(J)...K(J+4) to continue
        if not JII_in_chain:
            if __debug__:
                if candidate_node_JII is None:
                    log(DEBUG - 2,
                        "G&M Step 13, no unrouted nodes left for JII.")
                else:
                    log(
                        DEBUG - 2, "G&M Step 13, JII=n%d not in K(J)..K(J+4)" %
                        candidate_node_JII)
                    log(DEBUG - 3,
                        " which consists of nodes %s" % str(list(D3_nodes)))
                log(DEBUG - 2, "-> EXIT with no changes")
            # go to G&M Step 10 -> no changes, no skipping, route complete
            return route_data, [], [], True

        # construct and route to get the modified route cost D2
        D5_route_nodes = D2_route_nodes
        D5_route_nodes.add(candidate_node_JII)
        D5_route, D5 = solve_tsp(D, list(D5_route_nodes))
        D5_demand = D2_demand + d[candidate_node_JII] if C else 0
        if not ((L and D5 - S_EPS < L) and (C and D5_demand - C_EPS <= C)):
            if __debug__:
                log(
                    DEBUG - 2,
                    "G&M Step 13, rejecting replacement of KII=n%d with JJX=n%d and JII=n%d"
                    % (to_remove_node_KII, candidate_node_JJX,
                       candidate_node_JII))
                log(
                    DEBUG - 3, "  which would have formed a route %s (%.2f)" %
                    (str(D5_route), D5))
                if D5_demand - C_EPS > C:
                    log(DEBUG - 3, " violating the capacity constraint")
                else:
                    log(DEBUG - 3,
                        " violating the maximum route cost constraint")
                log(DEBUG - 2, "-> EXIT with no changes")
            # go to G&M Step 10 -> no changes, no skipping, route complete
            return route_data, [], [], True

        ## G&M Step 14
        # D6 -> DETERMINE the MINIMUM distance with JJX and JII excluded and
        #  KII ncluded
        _, D6 = _shortest_path_through_nodes(D, 0, last_chain_node, D6_nodes)

        if D1 + D3 < D5 + D6:
            if __debug__:
                log(
                    DEBUG - 2,
                    "G&M Step 14, rejecting replacement of KII=n%d with JJX=n%d and JII=n%d"
                    % (to_remove_node_KII, candidate_node_JJX,
                       candidate_node_JII))
                log(
                    DEBUG - 3, " which would have formed a route %s (%.2f)" %
                    (str(D5_route), D5))
                log(DEBUG - 2, "-> EXIT with no changes")
            # go to G&M Step 10 -> no changes, no skipping, route complete
            return route_data, [], [], True

        ## G&M Step 15
        if __debug__:
            log(
                DEBUG - 2, "G&M Step 15, accept improving move where " +
                "KII=n%d is removed and JJX=n%d and JII=n%d are added" %
                (to_remove_node_KII, candidate_node_JJX, candidate_node_JII))
            log(DEBUG - 3,
                " which forms a route %s (%.2f)" % (str(D2_route), D2))
            log(DEBUG - 2, " -> EXIT and continue adding nodes")

        ignored_nodes = [to_remove_node_KII]
        if candidate_node_JJX != node_J and candidate_node_JII != node_J:
            ignored_nodes += [node_J]

        # go to G&M Step 4 ->
        # route changed, KII removed and skip current node J, not complete
        return RouteData(D5_route, D5, D5_demand, D5_route_nodes),\
               [candidate_node_JJX, candidate_node_JII],\
               ignored_nodes, False
def do_2optstar_move(
        route1_data,
        route2_data,
        D,
        d=None,
        C=None,
        L=None,  # constraints
        strategy=LSOPT.FIRST_ACCEPT,
        best_delta=None):
    """ 2-opt* inter-route local search operation for the symmetric distances D
    Remove 2 edges from different routes and check if swapping the edge halves
    (in two different ways) would yield an improvement, while making sure the
    move does not violate constraints.
    """

    # use 2-opt
    if route1_data == route2_data:
        raise ValueError("Use do_2opt_move to find intra route moves")

    # make sure we have the aux data for constant time feasibility checks
    if not isinstance(route1_data, RouteData):
        route1_data = RouteData(*route1_data)
    if not isinstance(route2_data, RouteData):
        route2_data = RouteData(*route2_data)
    if not route1_data.aux_data_updated:
        route1_data.update_auxiliary_data(D, d)
    if not route2_data.aux_data_updated:
        route2_data.update_auxiliary_data(D, d)

    if not best_delta:
        best_delta = 0
    best_move = None
    accept_move = False

    #print("REMOVEME: 2opt* on %s %s"%(list(route1_data.route), list(route2_data.route)) )

    for i in xrange(0, len(route1_data.route) - 1):
        for j in xrange(0, len(route2_data.route) - 1):
            a = route1_data.route[i]
            b = route1_data.route[i + 1]
            c = route2_data.route[j]
            d = route2_data.route[j + 1]

            #print("REMOVEME: attempt remove %d-%d and %d-%d"%(a,b,c,d) )

            # a->c b->d
            #       __________
            #      /          \
            # 0->-a   b-<-0-<-c   d->-0
            #         \___________/
            #
            delta = D[a,c] + D[b,d] \
                     -D[a,b]-D[c,d]
            if delta + S_EPS < best_delta:
                # is an improving move, check feasibility
                constraint_violated = False
                r1_new_demand = None
                r2_new_demand = None
                if C:
                    r1_new_demand = route1_data.fwd_d[i] + route2_data.fwd_d[j]
                    r2_new_demand = route1_data.rwd_d[
                        i + 1] + route2_data.rwd_d[j + 1]
                    if r1_new_demand - C_EPS > C or r2_new_demand - C_EPS > C:
                        constraint_violated = True
                if not constraint_violated and L and (
                        route1_data.fwd_l[i] + D[a, c] + route2_data.fwd_l[j] -
                        S_EPS > L or route1_data.rwd_l[i + 1] + D[b, d] +
                        route2_data.rwd_l[j + 1] - S_EPS > L):
                    constraint_violated = True

                if not constraint_violated:
                    # store segments, deltas, and demands
                    best_move = (((None, i + 1, 1), (j, None, -1),
                                  D[a, c] - D[a, b], r1_new_demand),
                                 ((None, i, -1), (j + 1, None, 1),
                                  D[b, d] - D[c, d], r2_new_demand))
                    best_delta = delta

                    if strategy == LSOPT.FIRST_ACCEPT:
                        accept_move = True
                        break  # j loop

            # a->d c->b
            #       ______________
            #      /              \
            # 0->-a   b-<-0-<-c   d->-0
            #         \______/
            #
            delta = D[a,d] + D[b,c] \
                     -D[a,b]-D[c,d]
            if delta + S_EPS < best_delta:
                # is an improving move, check feasibility
                constraint_violated = False
                r1_new_demand = None
                r2_new_demand = None
                if C:
                    r1_new_demand = route1_data.fwd_d[i] + route2_data.rwd_d[
                        j + 1]
                    r2_new_demand = route1_data.rwd_d[i +
                                                      1] + route2_data.fwd_d[j]
                    if r1_new_demand - C_EPS > C or r2_new_demand - C_EPS > C:
                        constraint_violated = True
                if not constraint_violated and L and (
                        route1_data.fwd_l[i] + D[a, d] +
                        route2_data.rwd_l[j + 1] - S_EPS > L
                        or route1_data.rwd_l[i + 1] + D[b, c] +
                        route2_data.fwd_l[j] - S_EPS > L):
                    constraint_violated = True

                if not constraint_violated:
                    # store segments, deltas, and demands
                    best_move = (((None, i + 1, 1), (j + 1, None, 1),
                                  D[a, d] - D[a, b], r1_new_demand),
                                 ((None, i, -1), (j, None, -1),
                                  D[b, c] - D[c, d], r2_new_demand))
                    best_delta = delta

                    if strategy == LSOPT.FIRST_ACCEPT:
                        accept_move = True
                        break  # j loop

        if accept_move:
            break  # i loop

    if best_move:
        # unpack the move
        ((r1_sgm1, r2_sgm1, r1_delta, r1_new_demand),
         (r1_sgm2, r2_sgm2, r2_delta, r2_new_demand)) = best_move

        return (
            # route 1
            RouteData(
                route1_data.route[r1_sgm1[0]:r1_sgm1[1]:r1_sgm1[2]]+\
                route2_data.route[r2_sgm1[0]:r2_sgm1[1]:r2_sgm1[2]],
                route1_data.cost+r1_delta,
                r1_new_demand),
            # route 2
            RouteData(
                route1_data.route[r1_sgm2[0]:r1_sgm2[1]:r1_sgm2[2]]+\
                route2_data.route[r2_sgm2[0]:r2_sgm2[1]:r2_sgm2[2]],
                route1_data.cost+r1_delta,
                r1_new_demand),
            # delta
            best_delta)

    return None, None, None
def do_2point_move(
        route1_data,
        route2_data,
        D,
        d=None,
        C=None,
        L=None,  # constraints
        strategy=LSOPT.FIRST_ACCEPT,
        best_delta=None):
    """ Swap one point from route1 with one point on route2 if it improves the
    solution. This operation is sometimes referred to as "exchange" (e.g. in 
    Bräysy & M. Gendreau 2005, Savelsbergh 1992), but we use the name
    "two point move" (Groër et al 2010) to differentiate it from the inter-
    route one.
    
    If an improving move was found and made, operation returns new routes in
     same format as route_data inputs, but if C and d are not given the 3.
     field of the tuple is None. If there is no improving move, returns None.

    Groër, C., Golden, B. and Wasil, E., 2010. A library of local search
     heuristics for the vehicle routing problem. Mathematical Programming
     Computation, 2(2), pp.79-101.
    Bräysy, O. & Gendreau, M. 2005. Vehicle Routing Problem, Part I: Route 
     Construction and Local Search Algorithms Transportation Science 39(1),
     pp. 104–118
    Savelsbergh, M. W. P. 1992. The vehicle routing problem with time windows:
     Minimizing route duration. J.Comput. 4 146-154
    """

    # unpack route, current cost, and current demand
    route1, r1_l, r1_d, _ = route1_data
    route2, r2_l, r2_d, _ = route2_data

    if not best_delta:
        best_delta = 0
    best_move = None
    accept_move = False

    for i in xrange(1, len(route1) - 1):
        to_swap1 = route1[i]

        swap1_after = route1[i - 1]
        swap1_before = route1[i + 1]

        for j in xrange(1, len(route2) - 1):
            to_swap2 = route2[j]

            # capacity constraint feasibility check
            if C and (r1_d - d[to_swap1] + d[to_swap2] - C_EPS > C
                      or r2_d - d[to_swap2] + d[to_swap1] - C_EPS > C):
                continue

            swap2_after = route2[j - 1]
            swap2_before = route2[j + 1]

            route1_delta = -D[swap1_after,to_swap1]\
                           -D[to_swap1,swap1_before]\
                           +D[swap1_after,to_swap2]\
                           +D[to_swap2,swap1_before]
            route2_delta = -D[swap2_after,to_swap2]\
                           -D[to_swap2,swap2_before]\
                           +D[swap2_after,to_swap1]\
                           +D[to_swap1,swap2_before]

            # route cost constraint feasibility check
            if L and (r1_l + route1_delta > L or r2_l + route2_delta > L):
                continue

            delta = route1_delta + route2_delta
            if delta + S_EPS < best_delta:
                best_delta = delta
                best_move = (i, j, route1_delta, route2_delta)
                if strategy == LSOPT.FIRST_ACCEPT:
                    accept_move = True
                    break  # j loop
        if accept_move:
            break  # i loop

    if best_move:
        # unpack best move
        i, j, route1_delta, route2_delta = best_move
        to_swap1 = route1[i]
        to_swap2 = route2[j]
        return (RouteData(route1[:i] + [to_swap2] + route1[i + 1:],
                          r1_l + route1_delta,
                          None if not C else r1_d - d[to_swap1] + d[to_swap2]),
                RouteData(route2[:j] + [to_swap1] + route2[j + 1:],
                          r2_l + route2_delta,
                          None if not C else r2_d - d[to_swap2] + d[to_swap1]),
                route1_delta + route2_delta)

    return None, None, None
Exemple #21
0
def do_local_search(ls_ops, sol, D, d, C, L=None,
                    operator_strategy=LSOPT.FIRST_ACCEPT,
                    iteration_strategy=ITEROPT.ALL_ACCEPT,
                    max_iterations=None):
    """ Repeatedly apply ls_ops until no more improvements can be made. The
    procedure keeps track of the changed routes and searches only combinations
    that have been changed.
    
    Optionally the operator_strategy FIRST_ACCEPT (default)/BEST_ACCEPT can be
    given as well as the maximum number of iterations (that is, how many times
    all given operations are applied until giving up on reaching local optima).
    
    The iteration_strategy has an effect on which order the operations
    are applied. If ALL_ACCEPT (default), each operator is applied in turn
    until no improving moves are found. The options are:
     * FIRST_ACCEPT accept every improving move returned by the operator, and 
        start again from the first operator.
     * BEST_ACCEPT accept the very best (single) move over all operators.
     * ALL_ACCEPT accept every improving move of each operator and continue.
     * REPEATED_ACCEPT run operator until no improving moves are found before
        moving on to the next operator.
    Note that these may freely be combined with the operator_strategy.   
    """
    
    current_sol = sol
    route_datas = RouteData.from_solution(sol, D, d)
    route_data_idxs = list(range(len(route_datas)))

    # We keep track of the operations to avoid search when it has already been
    #  unsuccesfully applied   
    at_lsop_optimal = defaultdict(set)
    customer_to_at_lsopt_optimal = defaultdict(list)
    
    iteration = 0
    improving_iteration = True
    while improving_iteration:
        improving_iteration = False
        
        best_iteration_result = None
        best_iteration_delta = None 
        best_iteration_operator = None
        
        ls_op_idx = 0
        while ls_op_idx<len(ls_ops):
            ls_op = ls_ops[ls_op_idx]
            ls_op_args = getargspec(ls_op)[0]
            route_count = ls_op_args.index('D')
            op_order_sensitive = ls_op in ROUTE_ORDER_SENSITIVE_OPERATORS
            
            op_improved = False
            
            if __debug__:
                log(DEBUG-1, "Applying %s on %s"%(ls_op.__name__, str(current_sol)))
            
            # TODO: Consider using a counter to check for this
            # check if we already reached local optima on all routes with ls_op
            #if all( (ls_op in lsop_optimal[ri]) for ri in route_data_idxs ):
            #    if __debug__:
            #        log(DEBUG-2, "All route combinations already searched for %s, skipping it."%ls_op.__name__)
            #    break
            
            best_delta = None
            best_result = None
            
            no_improving_lsop_found = set()                
            for route_indices in permutations(route_data_idxs,route_count):
                # If the order does not matter, require that the route indices
                #  are ordered from smallest to largest.
                if (not op_order_sensitive) and (not is_sorted(route_indices)):
                    continue
                
                # ls_op is already at local optima with this combination of routes
                if ls_op in at_lsop_optimal[route_indices]:
                    if __debug__:
                        log(DEBUG-2, "Route combination %s already searched for %s, skipping it."%
                            (str(route_indices), ls_op.__name__))
                    continue

                # The one route case has different call signature
                if route_count==1:
                    op_params = [route_datas[route_indices[0]].route,
                                 D, operator_strategy]
                else:
                    op_params = [route_datas[ri] for ri in route_indices]+\
                                 [D, d, C, L, operator_strategy]
                                 # Ideally, best_delta can be used as an upper
                                 # bound to avoid unnecessary result generation
                                 # and to allow early ls_op termination.
                                 # However, then we lose the ability to mark
                                 # some route combinations as ls_optimal.
                                 #+[best_delta]
                result = ls_op(*op_params)
                #print("REMOVEME:",route_datas[route_indices[0]].route, "->", result)
                
                # route was changed, record the change in route datas
                delta = result[-1]
                if delta is None:
                    no_improving_lsop_found.update((route_indices,))
                else:
                    # For route_count==1 every route contributes for the same
                    # best_delta (unless trying to find the very best *single*
                    # move!)
                    if route_count==1:
                        skip_result = (
                            (best_delta != None and delta+S_EPS>best_delta) and
                            (iteration_strategy==ITEROPT.BEST_ACCEPT) )
                        
                        if not skip_result:
                            if ((best_result is None) or
                                (iteration_strategy==ITEROPT.BEST_ACCEPT)):
                                best_result = []
                                best_delta = 0
                           
                            old_rd = route_datas[route_indices[0]]
                            new_rd = RouteData(result[0],old_rd.cost+delta,old_rd.demand)
                            best_result.append( (route_indices[0], new_rd) )
                            best_delta+=delta
                    else:
                        if (best_result is None) or (delta+S_EPS<best_delta):
                            best_result = zip(route_indices, result[:-1])
                            best_delta = delta
                    
                    # Found a first improving with this operator, move on.
                    if operator_strategy==LSOPT.FIRST_ACCEPT:
                        break # route combination loop
                
            # end route combination loop
                        
            # Mark the routes that had no potential improvements to be at
            #  local optima to avoid checking the same moves again.
            for ris in no_improving_lsop_found:
                at_lsop_optimal[ris].add(ls_op)
                for ri in ris:
                    customer_to_at_lsopt_optimal[ri].append(ris)
                
            if best_result is not None:    
                if iteration_strategy==ITEROPT.BEST_ACCEPT:
                    if (best_iteration_result is None) or \
                       (best_delta+S_EPS<best_iteration_delta):
                        best_iteration_result = best_result
                        best_iteration_delta = best_delta 
                        best_iteration_operator = ls_op.__name__
                else:
                    op_improved = True
                    improving_iteration = True
                    for ri, new_rd in best_result:
                        route_datas[ri] = new_rd
                        # The route was modified, allow other operators to 
                        #  check if it can be improved again.
                        for ris in customer_to_at_lsopt_optimal[ri]:
                            at_lsop_optimal[ris].clear()
                            
                        # Check if route is [0,0] or [0] or []
                        if len(new_rd.route)<=2:
                            # remove this route from the future search 
                            route_data_idxs.remove(ri)
                        
                    if __debug__:
                        op_improved = True
                        opt_sol = RouteData.to_solution(route_datas)
                        log(DEBUG, "Improved from %s (%.2f) to %s (%.2f) using %s"%
                                (str(current_sol),objf(current_sol,D),str(opt_sol),objf(opt_sol,D),ls_op.__name__))
                        current_sol = opt_sol
                        
                    if iteration_strategy==ITEROPT.FIRST_ACCEPT:
                        ls_op_idx = 0
                        break # the ls_op loop (start from the beginning)
                    
            if __debug__:
                 if best_result is None:
                    log(DEBUG-1, "No improving move with %s"%ls_op.__name__)
                
            if op_improved and iteration_strategy==ITEROPT.FIRST_ACCEPT:
                # after an improvement start from the first operator
                ls_op_idx = 0
            if op_improved and iteration_strategy==ITEROPT.REPEATED_ACCEPT:
                # keep repeating the operator until no improvement is found
                ls_op_idx = ls_op_idx 
            else:
                # BEST_ACCEPT and ALL_ACCEPT always move on
                ls_op_idx += 1
                
            #END OF LS_OP LOOP
        
        if (iteration_strategy==ITEROPT.BEST_ACCEPT) and\
           (best_iteration_result is not None):
            improving_iteration = True
            
            for ri, new_rd in best_iteration_result:
                route_datas[ri] = new_rd
                # The route was modified, allow other operators to 
                #  check if it can be improved again.
                for ris in customer_to_at_lsopt_optimal[ri]:
                    at_lsop_optimal[ris].clear()
                # Check if route is [0,0] or [0] or []
                if len(new_rd.route)<=2:
                    # remove this route from the future search 
                    route_data_idxs.remove(ri)

            if __debug__:
                op_improved = True
                opt_sol = RouteData.to_solution(route_datas)
                log(DEBUG, "Improved from %s (%.2f) to %s (%.2f) using %s"%
                        (str(current_sol),objf(current_sol,D),str(opt_sol),objf(opt_sol,D),best_iteration_operator))
                current_sol = opt_sol
        
        iteration+=1
        if max_iterations and iteration>=max_iterations:
            break # iteration loop 

    current_sol = RouteData.to_solution(route_datas)              
    if __debug__:
        log(DEBUG,"Repeadedly applying %s resulted in %s"%
            (",".join(ls_op.__name__ for ls_op in ls_ops),str(current_sol)))
                  
                  
    return current_sol
def do_redistribute_move(redisributed_route_data,
                         receiving_route_or_routes_data,
                         D,
                         d=None,
                         C=None,
                         L=None,
                         strategy=LSOPT.FIRST_ACCEPT,
                         best_delta=None,
                         recombination_level=0):
    """
    Try to insert the nodes of the first route on the other routes if possible.
    Note that second argument receiving_route_data can also be a list of routes.
    
    The insertion order matters, so it is possible to try all permutations
    of nodes to be inserted and routes to insert to (and return the best).
    However, this will be very computationally intesive as the number of
    combinations explode. Therefore, there are different options available:
        
    * recombination_level=0, insert the nodes in the order they are on the
       route that will be destroyed and consider the recieving routes in the
       order of the "routes" parameter.
    * recombination_level=1, the nodes are inserted in all possible 
       order. The recieving routes are checked in the order of the "routes"
       parameter.
    * recombination_level=2, insert the nodes in the order they are on the
       route that will be destroyed. However, every possible order of the 
       recieving routes is checked.
    * recombination_level=3, all possible orderings of the nodes to be
       inserted AND the recieving routes is checked.       
       
    There could be yet another level (try_all_insertions_level=4), where for
    each insertion of each insertion node ordering, but it has not been
    implemented.
    """

    # allow only single redistributing to a single other route e.g. when using
    #  (local_search API)
    if isinstance(receiving_route_or_routes_data, RouteData):
        receiving_route_or_routes_data = [receiving_route_or_routes_data]

    # before iterating the permuations, check that it is at all possible to
    #  fit all from the route to be destroyed to the other routes.
    if C:
        total_d_slack = sum(C - rrd.demand
                            for rrd in receiving_route_or_routes_data)
        if redisributed_route_data.demand - C_EPS > total_d_slack:
            return [None] * (len(receiving_route_or_routes_data) + 2)

    if not best_delta:
        # allows move that makes the overall solution cost worse
        best_delta = float("inf")
    redistribution_succesfull = False
    best_insertion_routes = None

    # unpack route, current cost, and current demand
    route1, r1l, r1d, _ = redisributed_route_data
    if recombination_level == 0:
        insertion_node_orderings = [(route1[1:-1],
                                     receiving_route_or_routes_data)]
    elif recombination_level == 1:
        insertion_node_orderings = product(permutations(route1[1:-1]),
                                           [receiving_route_or_routes_data])
    elif recombination_level == 2:
        insertion_node_orderings = product(
            [route1[1:-1]], permutations(receiving_route_or_routes_data))
    elif recombination_level == 3:
        insertion_node_orderings = product(
            list(permutations(route1[1:-1])),
            list(permutations(receiving_route_or_routes_data)))

    #print(list(po for po in insertion_node_orderings))
    for route_customers, candidate_routes in insertion_node_orderings:
        total_delta = 0
        all_succesfully_inserted = True
        ansatz_route_data = list(candidate_routes)

        for to_insert in route_customers:
            node_succesfully_inserted = False
            for rd2_ar_idx, rd2 in enumerate(ansatz_route_data):
                _, new_rd2, delta = do_insert_move(to_insert,
                                                   rd2,
                                                   D,
                                                   d,
                                                   C,
                                                   L,
                                                   strategy=strategy)
                if delta is None:
                    continue

                #print("REMOVEME:", "succesfully inserted %d to create %s"%(to_insert, new_rd2.route))
                ansatz_route_data[rd2_ar_idx] = new_rd2
                total_delta += delta
                if total_delta + S_EPS < best_delta:
                    node_succesfully_inserted = True
                    break
                else:
                    continue

            if not node_succesfully_inserted:
                all_succesfully_inserted = False
                break

        if all_succesfully_inserted:
            #print("REMOVEME:", "all inserted to create %s (delta %.2f)"%([rd.route for rd in ansatz_route_data], total_delta))
            if total_delta < best_delta:
                best_delta = total_delta
                best_insertion_routes = ansatz_route_data
                redistribution_succesfull = True

    if redistribution_succesfull:
        # return an empty route and n routes the customers were distributed to
        return tuple([RouteData()] + best_insertion_routes + [best_delta])
    else:
        return [None] * (len(receiving_route_or_routes_data) + 2)
Exemple #23
0
def disentangle_heuristic(routes, sweep, node_phis, D, C, d, L):
    # find a overlapping / "tanged" pair
    improvement_found = False
    entangled = True

    route_phi_ranges = []
    for rd in routes:
        route = rd.route
        if rd.is_empty():
            route_phi_ranges.append(None)
        else:
            r_phis = node_phis[route[1:-1]]
            r_phi_range = _get_route_phi_range(r_phis)
            route_phi_ranges.append(r_phi_range)

    while (entangled):
        entangled = False
        for rd1_idx, rd2_idx in permutations(range(len(routes)), 2):
            # unpack route, current cost, and current demand
            route1, r1l, r1d, _ = routes[rd1_idx]
            route2, r2l, r2d, _ = routes[rd2_idx]
            r1_phi_range = route_phi_ranges[rd1_idx]
            r2_phi_range = route_phi_ranges[rd2_idx]

            # the route may have become empty
            if (r1_phi_range is None) or (r2_phi_range is None):
                continue

            # ranges overlap
            overlap = ((r1_phi_range[0][0] < r2_phi_range[0][1] and
                        r2_phi_range[0][0] < r1_phi_range[0][1]) or\
                       (r1_phi_range[1][0] < r2_phi_range[1][1] and\
                        r2_phi_range[1][0] < r1_phi_range[1][1]))
            if overlap:
                et_nodes = route1[:-1] + route2[1:-1]
                et_nodes.sort()

                # Check for backwards compatibility: numpy.isin was introduced
                #  in 1.13.0. If it not availabe (as is the case e.g. in Ubuntu
                #  16.04 LTS), use the equivalent list comprehension instead.
                if hasattr(np, 'isin'):
                    sweep_mask = np.isin(sweep[2],
                                         et_nodes,
                                         assume_unique=True)
                else:
                    sweep_mask = np.array(
                        [item in et_nodes for item in sweep[2]])

                et_sweep = sweep[:, sweep_mask]

                over_wrap_phi = (et_sweep[0][0] + 2 * pi) - et_sweep[0][-1]
                et_seed_node = [
                    np.argmax(np.ediff1d(et_sweep[0], to_end=over_wrap_phi))
                ]

                reconstructed_et_sol = sweep_init(et_sweep,
                                                  D,
                                                  d,
                                                  C,
                                                  L,
                                                  seed_node=et_seed_node,
                                                  direction="cw",
                                                  routing_algo=None)

                # translate nodes back to master problem node indices
                #reconstructed_et_sol = [et_nodes[n] for n in reconstructed_et_sol]

                # construct an array of RouteData objects for heuristics
                et_routes = RouteData.from_solution(reconstructed_et_sol, D, d)
                omitted = set()
                for extra_route in et_routes[2:]:
                    omitted |= set(extra_route.route[1:-1])
                et_routes = et_routes[:2]

                if __debug__:
                    log(DEBUG - 1,
                        "DISENTANGLE omitted %s" % str(list(omitted)))
                    _log_after_ls_op("DISENTANGLE/SWEEP", True, et_routes, D)

                refined = True
                while refined:
                    refined = False
                    refined |= inspect_heuristic(et_routes, D, C, d, L)
                    if __debug__:
                        _log_after_ls_op("DISENTANGLE/INSPECT", refined,
                                         et_routes, D)
                    refined |= single_heuristic(et_routes, D, C, d, L)
                    if __debug__:
                        _log_after_ls_op("DISENTANGLE/SINGLE", refined,
                                         et_routes, D)
                    # there were omitted nodes, try to complain them in
                    if omitted:
                        refined |= complain_heuristic(et_routes, omitted, D, C,
                                                      d, L)
                        if __debug__:
                            _log_after_ls_op("DISENTANGLE/COMPLAIN", refined,
                                             et_routes, D)

                # all nodes routed and disentangling improves -> accept this
                if len(et_routes) == 1:
                    # keep an empty route (for now) to avoid indexing errors
                    et_routes.append(RouteData([0, 0], 0.0, 0.0))
                    disentange_improves = True
                else:
                    disentange_improves = et_routes[0].cost + et_routes[
                        1].cost + S_EPS < r1l + r2l

                if len(omitted) == 0 and disentange_improves:
                    routes[rd1_idx] = et_routes[0]
                    routes[rd2_idx] = et_routes[1]

                    # The routes were updated, update also the ranges
                    r1_phis = node_phis[et_routes[0].route[1:-1]]
                    r2_phis = node_phis[et_routes[1].route[1:-1]]
                    r1_phi_range = _get_route_phi_range(r1_phis)
                    r2_phi_range = _get_route_phi_range(r2_phis)
                    route_phi_ranges[rd1_idx] = r1_phi_range
                    route_phi_ranges[rd2_idx] = r2_phi_range

                if __debug__:
                    if len(omitted) > 0:
                        log(
                            DEBUG - 1,
                            "DISENTANGLE rejected ( due to omitted %s)" %
                            str(omitted))
                    elif disentange_improves:
                        log(
                            DEBUG - 1,
                            "DISENTANGLE rejected ( due to not improving %.2f vs. %.2f )"
                            %
                            (et_routes[0].cost + et_routes[1].cost, r1l + r2l))
                    else:
                        log(
                            DEBUG - 1,
                            "DISENTANGLE accepted ( due to inserting omitted and improving %.2f vs. %.2f )"
                            %
                            (et_routes[0].cost + et_routes[1].cost, r1l + r2l))

    return improvement_found
Exemple #24
0
def do_one_sweep(N, D, d, C, L, routing_algo,
                  sweep, start, step_inc,
                  generate_alternative_first_routes = False,
                  intra_route_callback=None, inter_route_callback=None,
                  callback_data=None):
    """ This function does one full circle starting from start index of the 
    sweep and proceeds to add the nodes one by one in the direction indicated 
    by the step_inc parameter. A new route is started if either of the
    constraints constraints C and L (L can be None) are violated.

    If generate_alternative_first_routes is set, every possible route until 
    C OR L constraints are generated and then the sweep terminates.
    This is used by some constuction heuristics to build potential routes.
    
    The generated routes are TSP optimized using routing_algo and may be 
    further optimized with intra and inter route  improvement callbacks.""" 
    
    # make sure all before the start node have larger angles than the start node
    # and that they are of range [0-2*pi]
    if intra_route_callback or inter_route_callback:
        sweep_phis = np.copy( sweep[0] )
        #sweep_phis = -step_inc/abs(step_inc)*sweep_phis
        sweep_phis-=(pi+sweep_phis[start]) # the first is at -pi
        sweep_phis[start-step_inc::-step_inc]+=2*pi
        sweep_phis = -1*sweep_phis
    #node_idx_to_sweep_pos = lambda n: int(sweep[3,n-1])
    sweep_pos_to_node_idx = lambda idx: int(sweep[2,idx])
    max_sweep_idx = len(sweep[2])-1
    
    if __debug__:
        int_sweep = list(sweep[2].astype(int))
        log(DEBUG-1, "Sweep node order %s\n"%str(int_sweep[start:]+int_sweep[:start]))

    # Routes
    routes = [] 
    routed = [False]*N
    routed[0] = True
    routed_cnt = 0
    total_to_route = len(sweep[0])
    
    # Emerging route
    current_route = RouteData([0])
    current_route.node_set = OrderedSet([0])
    current_route_cost_upper_bound = 0
    route_complete = False
    
    # Backlog
    # improvement heuristics may leave some nodes unrouted, collect them here
    #  and insert them on next (or last) routes
    blocked_nodes = set()
    
    # THE MAIN SWEEP LOOP
    # iterate until a full sweep is done and the backlog is empty    
    sweep_pos = start
    sweep_node = sweep_pos_to_node_idx(sweep_pos)  
    while True:
        if __debug__:
            if sweep_node:
                prev_pos = _step(sweep_pos, -step_inc, max_sweep_idx)
                next_pos = _step(sweep_pos, step_inc, max_sweep_idx)
                prev_ray = bisect_angle(sweep[0][prev_pos], sweep[0][sweep_pos], direction=step_inc) 
                next_ray = bisect_angle(sweep[0][sweep_pos], sweep[0][next_pos], direction=step_inc) 
                log(DEBUG-2, "Considering n%d between rays %.2f, %.2f" %
                             (sweep_node,prev_ray,next_ray))
        
        # append route to the list of routes if
        # 1. all nodes have been added to the routes OR
        # 2. the route demand would be higher than the vehicle capacity 
        #    (check possible L constraint later) OR
        # 3. if there is only L constraint, check if adding sweep node would
        #    break the maximum route distance constraint.
        if not route_complete and C:
            would_break_C_ctr = current_route.demand+d[sweep_node]-C_EPS>C
            route_complete = would_break_C_ctr            
        if not route_complete and L and current_route_cost_upper_bound>L:
            tsp_sol, tsp_cost = \
                routing_algo(D, list(current_route.node_set)+[sweep_node]) 
            current_route_cost_upper_bound = tsp_cost
            would_break_L_ctr = tsp_cost-S_EPS>L
            route_complete = would_break_L_ctr
            
        route_interesting = generate_alternative_first_routes and \
                            len(current_route.node_set)>1
        if route_complete or route_interesting:  
            tsp_sol, tsp_cost = routing_algo(D, list(current_route.node_set))
            current_route.route = tsp_sol
            current_route.cost = tsp_cost
            current_route_cost_upper_bound = current_route.cost
            
            if __debug__:
                if route_complete:
                    log(DEBUG-2, "Route %s full."%str(list(current_route.node_set)))
                log(DEBUG-3, "Got TSP solution %s (%.2f)"%(str(tsp_sol),tsp_cost))
                     
            if L:
                L_feasible_pos = _ensure_L_feasible(D, d, C, L, 
                    current_route, sweep_pos, step_inc, max_sweep_idx, routed, 
                    sweep_pos_to_node_idx, routing_algo)
                current_route_cost_upper_bound = current_route.cost
                
                if L_feasible_pos!=sweep_pos:
                    sweep_pos = L_feasible_pos  
                    if sweep_node!=None:
                        sweep_node = sweep_pos_to_node_idx(sweep_pos)
                    # route is full if L constraint was violated
                    route_complete = True
                    route_interesting = False
                
            if sweep_node!=None and intra_route_callback:                
                # Do intra route improvement. It may include new unrouted
                #  customers to the route or remove existing ones!
                current_route, included_nodes, ignored_nodes, route_complete = \
                    intra_route_callback( current_route, callback_data,
                                          sweep[1], sweep_phis,
                                          sweep_pos, step_inc, routed )
                current_route_cost_upper_bound = current_route.cost
                
                if __debug__:
                    log(DEBUG-1, "Improved the route to %s (%.2f)"%(current_route.route, current_route.cost))
                
                # Make sure all routed nodes are routed.
                for rn in included_nodes:
                    routed[rn]=True  
                # Make sure skipped and removed nodes are added in later.
                for lon in ignored_nodes:
                    routed[lon] = False
                    if __debug__:
                        log(DEBUG-2, "Block n%d for now"%lon)
                    # avoid trying to add it on next iter.
                    blocked_nodes.add( lon )
                    if lon==sweep_node:
                        # skip adding current node
                        sweep_node = None
                
             # If route is complete, store it and start a new one   
            if route_complete:
                if __debug__:
                    log(DEBUG-1, "Route completed %s\n"%str(current_route.route))
                    #log(DEBUG-1, "with backlog of %s\n"%str(node_backlog))

                # Check if we have all routed, and can exit the main sweep loop                
                route_customer_cnt = len(current_route.node_set)
                if route_customer_cnt>1:
                    routed_cnt+=route_customer_cnt-1
                    routes.append( current_route )
                    
                if routed_cnt>=total_to_route:
                    if __debug__:
                        log(DEBUG-1,"Last route completed %s\n"%str(current_route.route))
                    break # SWEEP
                
                current_route = RouteData([0])
                current_route.node_set = OrderedSet([0])
                current_route_cost_upper_bound = 0.0
                route_complete = False
                
                # New route, allow customers in backlog
                blocked_nodes = set()
                
                if generate_alternative_first_routes:
                    break
            
            # if the route demand is higher than the lower bound, record it
            elif route_interesting:
                if __debug__:
                    log(DEBUG-1, "Route recorded %s\n"%str(current_route.route))
                # store a deep copy
                routes.append( RouteData(list(current_route.route),
                                         current_route.cost,
                                         current_route.demand,
                                         OrderedSet(current_route.node_set)) )
                
        # Route improvement can route nodes, so ensure this was not routed.
        if (sweep_node is not None) and (not routed[sweep_node]):
            current_route.node_set.add(sweep_node)
            routed[sweep_node] = True
            if C:
                current_route.demand+=d[sweep_node]
            if L:
                prev_node = 0
                if len(current_route.route)>2:
                    prev_node = current_route.route[-1] \
                                if current_route.route[-1]!=0\
                                else current_route.route[-2]
                # calculate and add the delta of just appending the swept node
                ub_delta = -D[prev_node, 0]+D[prev_node, sweep_node]+D[sweep_node,0]
                current_route_cost_upper_bound+=ub_delta
            
            if __debug__:
                log(DEBUG-2, "Added n%d to the route set"%sweep_node)
        
        if __debug__:
            log(DEBUG-3, "Step to a next sweep node from position %d (n%d) with %s blocked."%
                (sweep_pos, sweep_pos_to_node_idx(sweep_pos), list(blocked_nodes)))
        start_stepping_from = sweep_pos
        while True:
            sweep_pos = _step(sweep_pos, step_inc, max_sweep_idx)
            sweep_node = sweep_pos_to_node_idx(sweep_pos)
            
            if (not routed[sweep_node]) and (sweep_node not in blocked_nodes):
                break # found an unrouted node continue with it
                
            if sweep_pos == start_stepping_from:
                # We checked, and it seems there is no unrouted non-blocked
                # nodes left -> start a new route, reset blocked and try again.
                sweep_node = None
                route_complete = True
                blocked_nodes = set()
                break
        
    # (optional) improvement phase         
    if inter_route_callback:
        routes = inter_route_callback(routes, callback_data)
        
    return routes
Exemple #25
0
def _generate_solution_relaxation_petals(routes,
                                         discard_at_most,
                                         D,
                                         C=None,
                                         d=None,
                                         L=None):
    """ This is the route improvement, or overconstraint i&ii relaxations
    phase, where customers of a LP solution can be moved from route to another
    if the move improves the solution.
    
    Note that this differs from the do_1point_move local_search because the
    prodecure allows regaining feasibility if the move would improve the 
    solution (see _regain_feasibility for details)."""

    relaxed_route_datas = []
    route_datas = RouteData.from_routes(routes, D, d)
    route_indices = range(len(route_datas))

    # Try to move a customer from route 1 to route 2
    for ri1 in route_indices:
        route1, r1_l, r1_d, _ = route_datas[ri1]

        i = 0
        while i < len(route1) - 2:
            i += 1

            remove_after = route1[i - 1]
            to_move = route1[i]
            remove_before = route1[i + 1]

            r1_delta =  +D[remove_after,remove_before]\
                        -D[remove_after,to_move]\
                        -D[to_move,remove_before]

            for ri2 in route_indices:
                # Do not try to move nodes from ri1 to ri1!
                if ri1 == ri2:
                    continue

                route2, r2_l, r2_d, _ = route_datas[ri2]

                # Find the best place to insert it to
                j = 0
                best_r2_delta = float('inf')
                best_j = None
                while j < len(route2) - 1:
                    j += 1

                    insert_after = route2[j - 1]
                    insert_before = route2[j]

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

                    if r2_delta < best_r2_delta:
                        best_r2_delta = r2_delta
                        best_j = j

                if r1_delta + best_r2_delta < -S_EPS:


                    d_excess = r2_d+d[to_move]-C \
                               if (C and r2_d+d[to_move]-C_EPS>C) \
                               else None
                    l_excess = r2_l+best_r2_delta-L \
                               if (L and r2_l+best_r2_delta-S_EPS>L) \
                               else None

                    new_route1 = route1[:i] + route1[i + 1:]
                    new_rd1 = RouteData(new_route1, r1_l + r1_delta,
                                        r1_d - d[to_move] if d else 0)
                    new_route2 = route2[:best_j] + [to_move] + route2[best_j:]
                    new_rd2 = RouteData(new_route2, r2_l + best_r2_delta,
                                        r2_d + d[to_move] if d else 0)

                    # If operation would break a constraint...
                    if d_excess or l_excess:
                        # ... discard and redistribute some nodes
                        mod_route_datas = _regain_feasibility(
                            to_move, r1_delta, best_r2_delta, new_rd1, ri1,
                            ri2, discard_at_most, d_excess, l_excess,
                            route_datas, D, d, C, L)
                    else:
                        mod_route_datas = [new_rd1, new_rd2]

                    relaxed_route_datas.extend(mod_route_datas)

    return relaxed_route_datas
Exemple #26
0
def complain_heuristic(
        routes,
        unrouted_nodes,  #input / outputs
        D,
        C,
        d,
        L):
    """ This procedure tries to add unrouted nodes that were e.g. omitted from
    the initial routes on the existing routes by removing one node from them
    and reinserting it to another route.
    
    It is possible that not all nodes can be routed. Then a list of nodes that
    could not be inserted, or that were removed to make a better insertions, is
    returned. Empty list means that unrouted_nodes were successfully inserted.
    """
    was_assigned = set()
    was_omitted = set()
    improvement_found = False

    for node in unrouted_nodes:
        best_replace_delta = None
        best_replace_move = None
        node_routed = False
        for rd1_idx in range(len(routes)):
            # unpack route, current cost, and current demand
            rd1 = routes[rd1_idx]
            if rd1.is_empty():
                continue
            route1, r1l, r1d, _ = rd1

            for i in xrange(1, len(route1) - 1):
                replace_after = route1[i - 1]
                was_replaced = route1[i]
                replace_before = route1[i + 1]

                replace_d = 0
                if C:
                    replace_d = r1d - d[was_replaced] + d[node]
                    if replace_d - C_EPS > C:
                        continue

                replace_delta = +D[replace_after, node]\
                        +D[node, replace_before]\
                        -D[replace_after, was_replaced ]\
                        -D[was_replaced , replace_before]
                if L and r1l + replace_delta - S_EPS > L:
                    continue

                if (best_replace_delta is None) or\
                   (replace_delta<best_replace_delta):
                    best_replace_delta = replace_delta
                    best_replace_move = (rd1_idx, i)

                # the unrouted insertion is feasible, now fit the removed back
                for rd2_idx in range(len(routes)):
                    if rd1_idx == rd2_idx:
                        continue
                    rd2 = routes[rd2_idx]

                    # the insertion position does not matter, but it must fit
                    #  and be C and L feasible
                    _, new_rd2, insert_delta = do_insert_move(
                        was_replaced, rd2, D, d, C, L, LSOPT.FIRST_ACCEPT)

                    if insert_delta is not None:
                        # operation successful, accept the move
                        routes[rd2_idx] = new_rd2
                        routes[rd1_idx] = RouteData(
                            route1[:i] + [node] + route1[i + 1:],
                            r1l + replace_delta, replace_d)
                        node_routed = True
                        improvement_found = True
                        was_assigned.add(node)
                        break

                if node_routed:
                    break
            if node_routed:
                break

        if not node_routed:
            if best_replace_delta is not None:
                (best_rd_idx, i) = best_replace_move
                # place that of highest priority on the route
                best_route, best_rl, best_rd, _ = routes[best_rd_idx]
                to_remove = best_route[i]

                # Wren and Holliday propose that priority is given to the customer
                #  requesting the greater load.
                if C and d[node] > d[to_remove]:
                    routes[best_rd_idx] = RouteData(
                        best_route[:i] + [node] + best_route[i + 1:],
                        best_rl + best_replace_delta,
                        best_rd - d[to_remove] + d[node])

                    was_assigned.add(node)
                    was_omitted.add(to_remove)
                    improvement_found = True

    # update the situation of unrouted_nodes
    unrouted_nodes -= was_assigned
    unrouted_nodes |= was_omitted
    return improvement_found
Exemple #27
0
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
Exemple #28
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
 def test_insert_from_empty_to_empty(self):
     _, result_rd, result_delta = do_insert_move(RouteData(), RouteData(),
                                                 self.D)
     self.assertEqual(result_rd.route, [0, 0], "Should remain empty")