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_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_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")