Пример #1
0
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
Пример #2
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
Пример #3
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
Пример #4
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