Example #1
0
def do_naive_local_search(ls_ops,
                          sol,
                          D,
                          d,
                          C,
                          L=None,
                          operator_strategy=LSOPT.FIRST_ACCEPT,
                          max_iterations=None):
    """ Repeatedly apply naive ls_ops until no more improvements can be made.
    Optionally maximum number of iterations (of applying all ls_ops operators)
    can be given.
    """

    iteration = 0
    improved = True
    while improved:
        improved = False
        for ls_op in ls_ops:
            start_t = time()
            new_sol, delta = ls_op(sol, D, d, C, L, operator_strategy)
            if delta is None:
                continue
            else:
                improved = True
            elapsed_t = time() - start_t
            if __debug__:
                print("%s improved from %s (%.2f) to %s (%.2f) in %.2f s" %
                      (ls_op.__name__, sol, objf(
                          sol, D), new_sol, objf(new_sol, D), elapsed_t))

            sol = new_sol
            iteration += 1
            if max_iterations and iteration == max_iterations:
                break
    return sol
Example #2
0
 def call_init(points, D, d, C, L, st, wtt, single, minimize_K):
     if minimize_K:
         # todo: remove this when supprot (see TODO notes in algo desc)
         raise NotImplementedError("Nearest neighbor algorithm does "+
                                   " not support minimizing the number"+
                                   " of vehicles")
       
     sol_snn = nearest_neighbor_init(D, d, C, L, emerging_route_count=1)            
     if single:
         return sol_snn
     
     auto_route_count = sol_snn.count(0)-1
     
     # NN is so fast we can try with several K and take the best
     best_sol = sol_snn
     best_f = objf(sol_snn,D)
     best_K = auto_route_count
     for k in range(2,auto_route_count+1):
         sol = nearest_neighbor_init(D, d, C, L, emerging_route_count=k)
         sol = without_empty_routes(sol)
         sol_f = objf(sol,D)
         sol_K = sol.count(0)-1
         
         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
             
     return best_sol
Example #3
0
    def setUp(self):
        self.size = 7
        self.pts = [
            (0, 0),  #0
            (1, 1),  #1
            (1, 2),  #2
            (1, 3),  #3
            (0, 4),  #4
            (-2, 3),  #5
            (-2, 2),  #6
            (-2, 1)
        ]  #7
        self.D = squareform(pdist(self.pts, "euclidean"))
        self.d = [1.0] * len(self.D)
        self.d[0] = 0.0

        self.C = 4.0
        self.L = 14.0
        self.st = 2.0

        self.optf_C = objf([0, 1, 2, 3, 4, 0, 5, 6, 7, 0], self.D)
        self.optf_L = objf([0, 3, 4, 0, 5, 6, 7, 0, 1, 2, 0],
                           self.D) + self.st * self.size
        self.worstf_C = objf([0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0, 7, 0],
                             self.D)
        self.worstf_L = self.worstf_C + self.st * self.size

        self.longMessage = True
Example #4
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
    def _test_ls_vs_vrhp_w_random_sol(self,
                                      vrph_heur,
                                      ls_heur,
                                      times=10,
                                      routes=1):
        for i in range(times):
            initial_sol = list(range(1, len(self.D)))
            shuffle(initial_sol)
            initial_sol = [0] + initial_sol + [0]
            routes_to_create = routes - 1
            while routes_to_create > 0:
                possible_0_positions = len(initial_sol) - initial_sol.count(
                    0) - 2
                insert_0_counter = randint(0, possible_0_positions - 1)
                for j in range(0, len(initial_sol) - 1):
                    if initial_sol[j] != 0 and initial_sol[j + 1] != 0:
                        # valid position to insert

                        if insert_0_counter == 0:
                            initial_sol.insert(j + 1, 0)
                            break
                        else:
                            insert_0_counter -= 1
                routes_to_create -= 1
            #initial_sol = [0, 7, 3, 0, 4, 1, 5, 2, 6, 0]
            #print("initial_solution", initial_sol)
            #route = [max(0,int(n)-1) for n in "0-2-5-7-4-6-8-3-0".split("-")]

            with NamedTemporaryFile(delete=False, suffix='.vrp') as tmpfile:
                tsplib_file_path = tmpfile.name
            write_TSPLIB_file(tsplib_file_path,
                              self.D,
                              float_to_int_precision=1000)

            with NamedTemporaryFile(delete=False, suffix='.opt') as tmpfile:
                opt_file_path = tmpfile.name
            write_OPT_file(opt_file_path, self.D, initial_sol)

            vrph_sol = _do_vrph_ls(tsplib_file_path, opt_file_path,
                                   [vrph_heur])

            ls_sol = do_local_search([ls_heur], initial_sol, self.D, self.d,
                                     self.C, LSOPT.BEST_ACCEPT)

            # make sure the routes are in right order

            vrph_sol = normalize_solution(vrph_sol)
            ls_sol = normalize_solution(ls_sol)

            print(
                "LS on", initial_sol, "vrph_sol = %s (%.2f)" %
                (str(vrph_sol), objf(vrph_sol, self.D)),
                "ls_sol = %s (%.2f)" % (str(ls_sol), objf(ls_sol, self.D)))
            self.assertEqual(vrph_sol, ls_sol)

            if not DEBUG_VRPH_CALL:
                os.remove(tsplib_file_path)
                os.remove(opt_file_path)
Example #6
0
    def _solve_instance(self,
                        algo,
                        pfn,
                        round_D_func=None,
                        require_K=False,
                        predefined_k=None,
                        suppress_constraint_check=False):
        N, points, dd_points, d, D, C, _ = cvrp_io.read_TSPLIB_CVRP(pfn)
        K, L, service_time = cvrp_io.read_TSBLIB_additional_constraints(pfn)

        if round_D_func:
            D = round_D_func(D)

        if predefined_k is not None:
            K = predefined_k
        if require_K and (K is None):
            raise IOError(
                "It is required that the VEHICLE field is set in %s" % pfn)

        if service_time:
            half_st = service_time / 2.0
            if int(half_st) == half_st:
                half_st = int(half_st)
                service_time = int(service_time)

            # The service time can be modeled modifying the distance
            #  matrix in a way that any visit to a depot node costs
            #  service_time units.
            D_c = np.copy(D)
            D_c[1:, 1:] += service_time
            D_c[0, :] += half_st
            D_c[:, 0] += half_st
            np.fill_diagonal(D_c, 0.0)
        else:
            D_c = D

        if points is None and dd_points is not None:
            points = dd_points

        startt = time()
        if require_K:
            sol = algo(points, D_c, d, C, L, service_time, K)
        else:
            sol = algo(points, D_c, d, C, L, service_time)
        endt = time()
        elapsedt = endt - startt

        if __debug__:
            print_solution_statistics(sol, D, D_c, d, C, L, service_time)

        cover_ok, capa_ok, rlen_ok = check_solution_feasibility(
            sol, D, d, C, L, True)
        if not suppress_constraint_check:
            self.assertTrue(cover_ok, "Must be a valid solution")
            self.assertTrue(capa_ok, "Must not violate the C constraint")
            self.assertTrue(rlen_ok, "Must not violate the L constraint")

        return sol, objf(sol, D), objf(sol, D_c), elapsedt
Example #7
0
def do_naive_2point_move(solution,
                         D,
                         d,
                         C,
                         L,
                         strategy=LSOPT.BEST_ACCEPT,
                         best_delta=None):
    """ This is an educational version of the inter route node exchange move
    (two point move). A customer is swapped with another on the same or
    different route if it improves the solution.
    """

    sol_f = objf(solution, D)
    best_sol = None
    best_found = False
    if not best_delta:
        best_delta = 0

    for i in range(len(solution)):
        n1 = solution[i]
        if n1 == 0:
            continue

        for j in range(1, len(solution) - 1):
            n2 = solution[j]
            if n2 == 0:
                continue

            ansatz_sol = list(solution)
            ansatz_sol[i], ansatz_sol[j] = ansatz_sol[j], ansatz_sol[i]

            #            print("\nmove", i, j)
            #            print("orig",solution )
            #            print("check",ansatz_sol )
            #            print("quality_delta", objf(ansatz_sol, D)-objf(solution, D))
            #            print("feasibility",fast_constraint_check(ansatz_sol,D,d,C,L) )

            if fast_constraint_check(ansatz_sol, D, d, C, L):
                ansatz_sol_f = objf(ansatz_sol, D)
                delta = ansatz_sol_f - sol_f

                if delta + S_EPS < best_delta:
                    #print("set as the best")

                    best_delta = delta
                    best_sol = ansatz_sol

                    if strategy == LSOPT.FIRST_ACCEPT:
                        best_found = True
                        break
        if best_found:
            break

    if best_sol:
        return best_sol, best_delta
    else:
        return None, None
    def test_2opt_one_crossed(self):
        route = [0, 1, 6, 2, 7, 0]

        initial_f = objf(route, self.D)
        sol, delta_f = do_2opt_move(route, self.D, LSOPT.BEST_ACCEPT)
        do_2opt_f = objf(sol, self.D)
        self.assertEqual(
            sol, [0, 1, 2, 6, 7, 0],
            "chose invalid move, initial %f, optimized %f" %
            (initial_f, do_2opt_f))
        self.assertEqual(
            initial_f + delta_f, do_2opt_f,
            "The delta based and recalculated objective functions differ")
Example #9
0
    def _solve(self, name, desc, algof, C=None, L=None, st=None):
        d = self.d
        if C is None:
            d = None
        if st is not None:
            D_c = D2D_c(self.D, st)
        else:
            D_c = self.D

        for minimize_K in [False, True]:
            print(
                name, "(min_K=%s, C=%s, L=%s)" %
                (str(minimize_K), "%.1f" % C if C else "None",
                 "%.1f" % L if L else "None"))
            try:
                sol = algof(self.pts,
                            D_c,
                            d,
                            C=C,
                            L=L,
                            st=st,
                            wtt="EXACT_2D",
                            single=False,
                            minimize_K=minimize_K)
                #print("done")
            except NotImplementedError:
                continue

            sol_f = objf(sol, D_c)

            print("SOLUTION %s (%.2f)" % (sol, objf(sol, D_c)))
            cover_ok, capa_ok, rlen_ok = check_solution_feasibility(
                sol, D_c, d, C, L)
            self.assertTrue(cover_ok, str(sol) + " is not a valid solution")
            self.assertTrue(capa_ok, str(sol) + " violates C constraint")
            self.assertTrue(rlen_ok, str(sol) + " violates L constraint")

            if L:
                self.assertGreaterEqual(
                    sol_f, self.optf_L,
                    "Cannot be better than the optimal solution")
                self.assertLessEqual(
                    sol_f, self.worstf_L,
                    "Must be better than the worst possible solution")
            else:
                self.assertGreaterEqual(
                    sol_f, self.optf_C,
                    "Cannot be better than the optimal solution")
                self.assertLessEqual(
                    sol_f, self.worstf_C,
                    "Must be better than the worst possible solution")
    def test_a_tricky_move(self):
        route = [0, 7, 6, 4, 1, 2, 3, 5, 0]

        initial_f = objf(route, self.D)
        sol, delta_f = do_exchange_move(route, self.D, LSOPT.BEST_ACCEPT)
        do_2pm_f = objf(sol, self.D)

        sol = _normalise_route_order(sol)
        self.assertEqual(
            sol, [0, 4, 3, 2, 1, 5, 6, 7, 0],
            "chose invalid move, initial %f, optimized %f" %
            (initial_f, do_2pm_f))
        self.assertAlmostEqual(
            initial_f + delta_f,
            do_2pm_f,
            msg="The delta based and recalculated objective functions differ")
    def test_improve_random_solution_first_accept(self):
        special_case_fails_message = (
            "NOTE: This may sometimes fail due to" +
            " how the naive implementation may coincidentally invert also routes"
            +
            " which are between the two routes that have their edges removed."
            +
            " Then subsequent moves may in some cases choose different equally"
            +
            " good improving moves and the final solutions differ. Both 2-opt*"
            " implementations still work properly.")
        sol = _get_random_solution(self.d, self.C)
        _compare_improved_from_solution(self,
                                        sol,
                                        self.D,
                                        self.d,
                                        self.C,
                                        None, [do_2optstar_move, do_2opt_move],
                                        [do_naive_2optstar_move],
                                        extra_msg=special_case_fails_message,
                                        operator_strategy=LSOPT.FIRST_ACCEPT)

        # Test with L constraint
        max_L = max(objf(r, self.D) for r in sol2routes(sol))
        _compare_improved_from_solution(self,
                                        sol,
                                        self.D,
                                        self.d,
                                        self.C,
                                        max_L,
                                        [do_2optstar_move, do_2opt_move],
                                        [do_naive_2optstar_move],
                                        extra_msg=special_case_fails_message,
                                        operator_strategy=LSOPT.FIRST_ACCEPT)
Example #12
0
def _log_after_ls_op(name, changed, routes, D):
    log(DEBUG - 1, "After %s (%s):" % (name, str(changed)))
    sol_f = 0.0
    for ri, rd in enumerate(routes):
        log(DEBUG - 1, "Route #%d : %s" % (ri, str(list(rd.route))))
        sol_f += objf(rd.route, D)
    log(DEBUG - 1, "Cost %.2f\n" % sol_f)
Example #13
0
def gaskell_savings_init(D, d, C, L, minimize_K=False, savings_method="both"):
    """ Savings algorithm with Gaskell (1967) pi and lambda savings criteria.
    Uses parallel_savings.py for the savings procedure. 
    
    * 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 length/duration/cost.
        
    * minimize_K sets the primary optimization objective. If set to True, it is
       the minimum number of routes. If set to False (default) the algorithm 
       optimizes for the mimimum solution/routing cost. In savings algorithms 
       this is done by ignoring negative savings values.
       
    * savings_method selects the savings criteria: "lambda" or "pi". If set to
      "both" (default) the one with better results is returned.
    
    Gaskell, T. (1967). Bases for vehicle fleet scheduling. Journal of the
    Operational Research Society, 18(3):281-295.
    """

    savings_functions = []
    if savings_method == "both":
        savings_functions = [
            gaskell_lambda_savings_function, gaskell_pi_savings_function
        ]
    elif savings_method == "lambda":
        savings_functions = [gaskell_lambda_savings_function]
    elif savings_method == "pi":
        savings_functions = [gaskell_pi_savings_function]
    else:
        raise ValueError("Only 'lambda', 'pi', or 'both' are supported.")

    best_sol = None
    best_f = None
    best_K = None
    interrupted = False
    for sav_f in savings_functions:
        sol, sol_f, sol_K = None, float('inf'), float('inf')
        try:
            sol = parallel_savings_init(D, d, C, L, minimize_K, sav_f)
        except KeyboardInterrupt as e:  #or SIGINT
            # lambda or pi was interrupted
            if len(e.args) > 0 and type(e.args[0]) is list:
                sol = e.args[0]
                interrupted = True
        if sol:
            sol_f = objf(sol, D)
            sol_K = sol.count(0) - 1
        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:
            # pass on the current best_sol
            raise KeyboardInterrupt(best_sol)

    return best_sol
Example #14
0
def do_naive_1point_move(solution,
                         D,
                         d,
                         C,
                         L,
                         strategy=LSOPT.BEST_ACCEPT,
                         best_delta=None):
    """ This is an educational version of the one point/relocate move.
    A customer is moved to the different position of the same or different
    route if it improves the solution.
    """

    sol_f = objf(solution, D)
    best_sol = None
    best_found = False
    if not best_delta:
        best_delta = 0

    for n in solution:
        if n == 0:
            continue

        for j in range(1, len(solution) - 1):
            ansatz_sol = list(solution)
            ansatz_sol.remove(n)
            ansatz_sol.insert(j, n)

            if fast_constraint_check(ansatz_sol, D, d, C, L):
                ansatz_sol_f = objf(ansatz_sol, D)
                delta = ansatz_sol_f - sol_f

                if delta + S_EPS < best_delta:
                    best_delta = delta
                    best_sol = ansatz_sol

                    if strategy == LSOPT.FIRST_ACCEPT:
                        best_found = True
                        break
        if best_found:
            break

    if best_sol:
        return best_sol, best_delta
    else:
        return None, None
Example #15
0
 def test_2pm_updated_route_cost(self):
     result = self._make_improving_exchage_move()
     for rd in result:
         self.assertAlmostEqual(
             objf(rd[0], self.D),
             rd[1],
             msg=
             "original route cost + savings should match recalculated route cost"
         )
def _compare_improved_from_solution(testcase, sol, D,d,C,L,
                                           ls_ops, naive_ops,
                                           operator_strategy=LSOPT.BEST_ACCEPT,
                                           extra_msg=""):
        """ Improves the solution `sol` for the problem `D`,`d`,`C`,`L` using
        the local_search module operators `ls_ops` and naive implementations
        `naive_ops`. 
        """
        
        if __debug__:
            print("THE PROBLEM:")
            print("D,d,C,L =","np.%s"%repr(D),",",d,",",C,",",L)
        
        rls_ops = list(reversed(ls_ops))
        # note: if given multiple operators, the best single move out of among
        #  all moves is chosen at each step due to ITEROPT.BEST_ACCEPT.
        ls_sol_fwdop = do_local_search(ls_ops, sol, D, d, C, L=L,
                                 operator_strategy=operator_strategy,
                                 iteration_strategy=operator_strategy)
        ls_sol_rwdop = do_local_search(rls_ops, sol, D, d, C, L=L,
                                 operator_strategy=operator_strategy,
                                 iteration_strategy=operator_strategy)
        bf_sol = do_naive_local_search(naive_ops, sol, D, d, C, L=L,
                                       operator_strategy=operator_strategy)
        
        ls_sol_fwdop = normalize_solution(ls_sol_fwdop)
        ls_sol_rwdop = normalize_solution(ls_sol_rwdop)
        bf_sol = normalize_solution(bf_sol)
        
        if __debug__:
            print("\nFINAL SOLUTIONS:")
            print("+".join( op.__name__ for op in ls_ops),"alt 1 :",
                  ls_sol_fwdop, "(%.2f)"%objf(ls_sol_fwdop, D))
            print("+".join( op.__name__ for op in ls_ops),"alt 2 :",
                  ls_sol_rwdop, "(%.2f)"%objf(ls_sol_rwdop, D))
            print("+".join( op.__name__ for op in naive_ops),":", bf_sol,
                  "(%.2f)"%objf(bf_sol, D))
        
        testcase.assertTrue(all(check_solution_feasibility(ls_sol_fwdop, D, d, C, L)))
        testcase.assertTrue(all(check_solution_feasibility(ls_sol_rwdop, D, d, C, L)))
        testcase.assertTrue(all(check_solution_feasibility(bf_sol, D, d, C, L)))
        testcase.assertTrue(ls_sol_fwdop==bf_sol or ls_sol_rwdop==bf_sol, extra_msg)   
Example #17
0
 def __init__(self, seed_customers, D, d):
     self.potential_insertions = []
     if seed_customers:
         lroute = [0] + seed_customers + [0]
         self.route = dllist(lroute)
         self.cost = objf(lroute, D)
         self.used_capacity = sum(d[n] for n in lroute) if d else 0
     else:
         self.route = dllist([0, 0])
         self.used_capacity = 0
         self.cost = 0
    def test_3opt_move_s1_r2_s3(self):
        """ Test the 2nd 3-opt alternative (actually corresponding a 2-opt
        move), where the middle segment (segment2) is reversed."""

        # modify the distance matrix to force a recombination move
        D = self.D.copy()
        set_weight(D, 2, 3, 100)
        set_weight(D, 8, 9, 100)
        set_weight(D, 2, 8, 1)
        set_weight(D, 3, 9, 1)

        initial_sol = self.optimum
        initial_sol_f = objf(initial_sol, D)
        sol, delta_f = self.move_op(initial_sol, D, strategy=LSOPT.BEST_ACCEPT)
        do_3opt_f = objf(sol, D)
        self.assertEqual(
            sol, self.segment1_head + self.segment2[::-1] + self.segment3 +
            self.segment1_tail)
        self.assertAlmostEqual(initial_sol_f+delta_f, do_3opt_f, msg=\
          "The delta based and recalculated objective function values differ")
    def test_3opt_move_s1_r3_s2(self):
        """Test the 7th and final 3-opt alternative, where the middle AND last segments
        (segment2 and segment3) are swapped, and the segment3 is reversed."""

        # modify the distance matrix to force a recombination move
        D = self.D.copy()
        set_weight(D, 2, 3, 100)
        set_weight(D, 8, 9, 100)
        set_weight(D, 16, 17, 100)
        set_weight(D, 2, 16, 1)
        set_weight(D, 9, 3, 1)
        set_weight(D, 8, 17, 1)

        initial_sol = self.optimum
        initial_sol_f = objf(initial_sol, D)
        sol, delta_f = self.move_op(initial_sol, D, strategy=LSOPT.BEST_ACCEPT)
        do_3opt_f = objf(sol, D)
        self.assertEqual(
            sol, self.segment1_head + self.segment3[::-1] + self.segment2 +
            self.segment1_tail)
        self.assertAlmostEqual(initial_sol_f+delta_f, do_3opt_f, msg=\
          "The delta based and recalculated objective function values differ")
    def test_3opt_move_s1_r3_r2(self):
        """ Test the 3rd 3-opt alternative (actually corresponding a 2-opt
        move), where the first segment (segment1) is reversed. However, in
        symmetric case this is equal to  reversing the order and direction of
        the segments 2 and 3 and this is the expected move operation here."""

        # modify the distance matrix to force a recombination move
        D = self.D.copy()
        set_weight(D, 2, 3, 100)
        set_weight(D, 16, 17, 100)
        set_weight(D, 2, 16, 1)
        set_weight(D, 3, 17, 1)

        initial_sol = self.optimum
        initial_sol_f = objf(initial_sol, D)
        sol, delta_f = self.move_op(initial_sol, D, strategy=LSOPT.BEST_ACCEPT)
        do_3opt_f = objf(sol, D)
        self.assertEqual(
            sol, self.segment1_head + self.segment3[::-1] +
            self.segment2[::-1] + self.segment1_tail)
        self.assertEqual(initial_sol_f+delta_f, do_3opt_f, msg=\
          "The delta based and recalculated objective function values differ")
Example #21
0
def random_cmt_2phase(D, d, C, L):
    best_sol = None
    best_f = None

    best_lambda = None
    best_mu = None

    for i in range(RANDOM_TRALS):
        lambda_multiplier = 1.0 + 2.0 * random()
        mu_multiplier = 0.5 + 1.5 * random()

        #pkey = (len(D), C, L)
        #if pkey in stochastic_params:
        #    lambda_multiplier, mu_multiplier = stochastic_params[pkey]
        #
        #    # little wobble
        #    mu_multiplier+=random()*0.2-0.05
        #    lambda_multiplier+=random()*0.2-0.05

        # stochastic with SMAC
        # 8% gap {'pmu': 1.521411377875044, 'plambda': 3.920075418247227}

        sol = cmt_2phase_init(D,
                              d,
                              C,
                              L,
                              False,
                              lambda_multiplier,
                              mu_multiplier,
                              phase1_seed_selection_method="first",
                              phase2_choose_most_associated_route=False,
                              phase2_repeated_association_with_n_routes=1,
                              number_of_randomized_retries=RANDOMIZED_RETRIES)

        sol_f = objf(sol, D)

        if (best_sol is None) or (sol_f < best_f):

            print("~~~~", "updating the best", "~~~~")
            best_sol = sol
            best_f = sol_f
            best_lambda = lambda_multiplier
            best_mu = mu_multiplier

    print("Best parameters for the cmt_2phase after %d trials" % RANDOM_TRALS,
          "lambda=%.2f" % best_lambda, "mu=%.2f" % best_mu)
    return best_sol
Example #22
0
def tsp_cli(tsp_f_name, tsp_f):
    # import here so that the function can be used without these dependencies
    from util import objf
    
    if len(sys.argv)==2 and path.isfile(sys.argv[1]):
        P = cvrp_io.read_TSPLIB_CVRP(sys.argv[1])
        D = P.distance_matrix
        start_t = time()
        tsp_sol, tsp_f = tsp_f(D, list(range(len(D))))
        elapsed_t = time()-start_t
        print("Solved %s with %s in %.2f s"%(path.basename(sys.argv[1]), 
                                             tsp_f_name, elapsed_t))
        tsp_o = objf(tsp_sol,D)
        print("SOLUTION:", str(tsp_sol))
        print("COST:", tsp_o)  
        assert(tsp_f==tsp_o)
    else:
        print("usage: tsp_solver_%s.py TSPLIB_file.tsp"%tsp_f_name, file=sys.stderr)
Example #23
0
def solve_tsp_ropt(D,
                   selected_idxs,
                   do_shuffle=False,
                   do2opt=True,
                   do3opt=True):
    # r-Opt (r \in {2,3} )
    endp = selected_idxs[0]

    if do_shuffle:
        shuffled_idxs = list(selected_idxs[1:])
        shuffle(shuffled_idxs)
        new_route = [endp] + shuffled_idxs + [endp]
    elif selected_idxs[-1] != endp:
        new_route = selected_idxs + [endp]
    else:
        new_route = selected_idxs

    new_route_cost = objf(new_route, D)

    # make first 2-optimal
    if do2opt:
        improved = True
        while improved:
            improved = False
            improved_route, delta = do_2opt_move(new_route, D, 1)
            if improved_route is not None:
                new_route = improved_route
                new_route_cost += delta
                improved = True

    # then 3-optimal (do not waste time on "easy" 2-opt
    #  operations if the route has already been made 2-optimal
    if do3opt:
        improved = True
        while improved:
            improved = False
            improved_route, delta = do_3opt_move(new_route, D, 1)
            if improved_route is not None:
                new_route = improved_route
                new_route_cost += delta
                improved = True

    return new_route, new_route_cost
Example #24
0
def beasley_rfcs_init(D, d, C, L, trials):
    random_2opt_tsp_sol = lambda D, nodes: solve_tsp_ropt(
        D, nodes, do_shuffle=True, do2opt=True, do3opt=False)

    best_sol = None
    best_f = None
    for t in range(trials):
        # 2-opt it
        sol = route_first_cluster_second_init(D,
                                              d,
                                              C,
                                              L,
                                              minimize_K=False,
                                              tsp_gen_algo=random_2opt_tsp_sol)
        sol_f = objf(sol, D)
        if (best_sol is None) or (sol_f < best_f):
            best_sol = sol
            best_f = sol_f
    return best_sol
Example #25
0
    def test_verify_reference_solutions_FosterRyan1976_instances(self):
        for problem_idx, problem_name in enumerate(self.problem_names):
            ref_k, ref_f = self.targets[1][problem_idx]

            if problem_name == r"04-CW64_n30a_k8c.vrp":
                problem_name = r"04-CW64_n31_k9c.vrp"
                ref_f = 1377

            pfn = path.join(BENCHMARKS_BASEPATH, self.problem_path,
                            problem_name)
            N, points, dd_points, d, D, C, _ = cvrp_io.read_TSPLIB_CVRP(pfn)
            K, L, service_time = cvrp_io.read_TSBLIB_additional_constraints(
                pfn)
            if service_time:
                D_c = D2D_c(D, service_time)
            else:
                D_c = D

            ref_sol = self.target_solutions[problem_idx]
            ref_sol_f = int(objf(ref_sol, D_c))
            ref_sol_k = ref_sol.count(0) - 1

            cover_ok, capa_ok, rlen_ok = check_solution_feasibility(
                ref_sol, D, d, C, L, True)
            self.assertTrue(cover_ok, "Must be a valid solution")
            self.assertTrue(capa_ok, "Must not violate the C constraint")
            self.assertTrue(rlen_ok, "Must not violate the L constraint")

            self.assertEqual(
                ref_k,
                ref_sol_k,
                msg=
                ("The appendix solution route count differs from the one given "
                 + "in Table 2 for %s (%d vs %d)" %
                 (problem_name, ref_sol_k, ref_k)))
            self.assertAlmostEqual(
                ref_f,
                ref_sol_f,
                msg=("The appendix solution result differs from the one given "
                     + "in Table 2 for %s : %d (ours) vs %d (theirs)" %
                     (problem_name, ref_sol_f, ref_f)))
 def test_improve_random_solution_first_accept(self):
     special_case_fails_message = ("NOTE: This may sometimes fail due to"+
      " how the naive implementation checks first all insertion positions"+
      " for one customer before moving on to next, while the local search"+
      " Implementation checks all intra route moves first and then the"+
      " inter route moves. If there is equally good move in intra and inter"+
      " route moves, it may be that both implementations do not select the"+
      " same one.")
     sol = _get_random_solution(self.d,self.C)
     _compare_improved_from_solution(
         self, sol, self.D,self.d,self.C,None,
         [do_relocate_move, do_1point_move], [do_naive_1point_move],
         extra_msg=special_case_fails_message,
         operator_strategy=LSOPT.FIRST_ACCEPT)
     
     # Test with L constraint        
     max_L = max( objf(r,self.D) for r in sol2routes(sol) )
     _compare_improved_from_solution(
         self, sol, self.D,self.d,self.C,max_L,
         [do_relocate_move, do_1point_move], [do_naive_1point_move],
         extra_msg=special_case_fails_message,
         operator_strategy=LSOPT.FIRST_ACCEPT)
Example #27
0
def print_solution_statistics(sol,
                              D,
                              D_cost,
                              d,
                              C,
                              L=None,
                              service_time=None,
                              verbosity=-1):
    print("\nSOLUTION:", sol)
    cover_ok, capa_ok, rlen_ok = cvrp_ops.check_solution_feasibility(
        sol, D_cost, d, C, L, True)

    if verbosity > 1:
        print("ALL SERVED:", cover_ok)
        if C:
            print("IS C FEASIBLE:", capa_ok)
        if L:
            print("IS L FEASIBLE:", rlen_ok)
    else:
        print("FEASIBLE:", cover_ok and capa_ok and rlen_ok)
    print("SOLUTION K:", sol.count(0) - 1)

    sol_f = None if D is None else objf(sol, D)
    sol_c = None if D_cost is None else objf(sol, D_cost)
    if (verbosity > 0 and sol_f != sol_c) or (not sol_c):
        print("SOLUTION COST:", sol_c, "\n")
    if sol_c:
        print("SOLUTION LENGTH:", sol_f)

    if verbosity > 1:
        routes = sol2routes(sol)
        print("ROUTES:")
        print("No.\tCost\tLength\tLoad\tRoute")
        for i, route in enumerate(routes):
            print(i + 1,
                  "%.2f" % objf(route, D_cost),
                  "%.2f" % objf(route, D),
                  sum((d[n] for n in route)) if C else "-",
                  route,
                  sep='\t')
        print("Total",
              "%.2f" % objf(sol, D_cost),
              "%.2f" % objf(sol, D),
              sep='\t')
Example #28
0
def mbsa_init(D,
              d,
              C,
              L,
              minimize_K=False,
              W=0.0,
              solve_tsp=default_solve_tsp,
              primary_criteria_callback=_calculate_savings,
              secondary_criteria_callback=_calculate_secondary_criteria):
    """ An implementation of Desrochers & Verhoog (1989) Matching Based Savings 
    Algortihm. It uses Gurobi to solve the maxumum matching problem (MMP) and
    the TSP. The routes are merged according to the MMP until no valid merges
    remain.
    
    The parameters for this implementation are:
    
    * 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 length/duration/cost.
    
    * minimize_K sets the primary optimization objective. If set to True, it is
       the minimum number of routes. If set to False (default) the algorithm 
       optimizes for the mimimum solution/routing cost. In savings algorithms 
       this is done by ignoring negative savings values.
      
    * W is the savings cost assigned to infeasible merges. One may need to 
        set this to a large negative value if minimize_K is true.
    
    * solve_tsp which TSP solver is used when calculating the savings values
    * primary_criteria_callback and secondary_criteria_callback can be changed
       from the defaults to use different savings criteria. See the reference
       implementations for details on the fucntion signatures.

    Desrochers, M. and Verhoog, T. (1989). G-89-04 : A matching based
    savings algorithm for the vehicle routing problem. Technical report,
    GERAD, Montreal, Canada.
    """

    N = len(D)
    tsp_cache = {}
    demand_cache = {}
    savings = []

    ignore_negative_savings = not minimize_K

    ## Step 0: initalization
    # serve each customer with a single route
    route_sets = [frozenset([0, i]) for i in range(1, N)]

    try:
        # calculate initial savings or merging *routes* i and j
        savings = [(primary_criteria_callback(rs1, rs2, demand_cache,
                                              tsp_cache, W, D, d, C, L,
                                              solve_tsp), (i, j))
                   for i, rs1 in enumerate(route_sets)
                   for j, rs2 in enumerate(route_sets) if i < j]

        while True:
            ## Step 1: Evaluate the weights and solve the weighted matching prolbem

            w_ij, x_ij_keys = zip(*savings)
            valid_matchings = any(w for w in w_ij if w != W)
            if valid_matchings == 0:
                break

            s_ij = None
            if secondary_criteria_callback:
                s_ij = secondary_criteria_callback(w_ij, x_ij_keys,
                                                   len(route_sets))
            best_matching = _mmp_solve(w_ij, x_ij_keys, len(route_sets), s_ij)
            if best_matching is None:
                break  # no valid matchings found

            best_savings, i_prime, j_prime = best_matching

            if best_savings == W:
                break

            if ignore_negative_savings and best_savings < 0:
                continue  # do not allow merges that would make the solution worse
                # (even if it would mean fewer routes)

            if __debug__:
                log(
                    DEBUG,
                    "Best matching joins routes %s (idx:%d) and %s (idx:%d)." %
                    (tsp_cache[route_sets[i_prime]][0], i_prime,
                     tsp_cache[route_sets[j_prime]][0], j_prime))

            ## Step 2: merge the route combination

            # merge routes i and j
            new_i_prime_set = route_sets[i_prime].union(route_sets[j_prime])

            ## Step 3: Update savings

            ## Filter out all edges adjacent to i' and j'
            # select only the savings that *do not* contain routes i and j
            #  and while at it, update the indexing
            savings = [(sa, (si if si < j_prime else si - 1,
                             sj if sj < j_prime else sj - 1))
                       for sa, (si, sj) in savings
                       if (si != i_prime and sj != i_prime and si != j_prime
                           and sj != j_prime)]

            del route_sets[j_prime]
            if i_prime > j_prime: i_prime = i_prime - 1
            route_sets[i_prime] = new_i_prime_set

            # Evaluate the savings associated with the new route
            rs1 = route_sets[i_prime]
            merged_route_savings = [
                (primary_criteria_callback(rs1, rs2, demand_cache, tsp_cache,
                                           W, D, d, C, L,
                                           solve_tsp), (i_prime, k))
                for k, rs2 in enumerate(route_sets) if k != i_prime
            ]
            savings.extend(merged_route_savings)

            if __debug__:
                dbg_r, dbg_w = _get_tsp_sol(rs1, tsp_cache, D, solve_tsp)
                log(
                    DEBUG - 1, "Route#%d after merge %s (%.2f)" %
                    (i_prime, str(dbg_r), objf(dbg_r, D)))

    except KeyboardInterrupt:  #or SIGINT
        interrupted_sol = _geedy_merge(D, d, C, L, W, savings, route_sets,
                                       tsp_cache, demand_cache)
        raise KeyboardInterrupt(interrupted_sol)

    # the optimized TSP tours for the routes are cached, reuse
    final_routes = [
        _get_tsp_sol(rs, tsp_cache, D, solve_tsp)[0] for rs in route_sets
    ]
    sol = [0] + [n for route in final_routes for n in route[1:]]

    return sol
Example #29
0
def _geedy_merge(D, d, C, L, W, savings, route_sets, tsp_cache, demand_cache):
    """ This is not the part of the main algorithm proper but a greedy fallback 
    the procedure relies on in case of an interrupt. It simply uses the 
    existing  savings list and caches to create solution with minimal amount of
    computation (it is an O(n) algorithm at this point).
    """
    savings.sort(reverse=True)

    try:

        # Do a first pass where the routes in the tsp_cache are used
        first_pass_joined_routes = set()
        for saving, route_ids in savings:
            if saving == W:
                break  # from now one all are infeasible

            route_i, route_j = route_ids
            if route_i in first_pass_joined_routes or\
               route_j in first_pass_joined_routes:
                continue  # either route is already merged

            nodes_i = route_sets[route_i]
            nodes_j = route_sets[route_j]
            nodes_ij = nodes_i.union(nodes_j)

            if nodes_ij not in tsp_cache:
                greedy_routing = [0] + list(nodes_ij) + [0]
                greedy_f = objf(greedy_routing, D)
                if not L or greedy_f - S_EPS < L:
                    # remember for the next stage
                    tsp_cache[nodes_ij] = (greedy_routing, greedy_f)
                continue  # all in tsp_cache are feasible

            # record and do the merge
            first_pass_joined_routes.add(route_i)
            first_pass_joined_routes.add(route_j)
            route_sets[route_i] = nodes_ij
            route_sets[route_j] = route_i  # mark as a reference to the another

        # the second stage just smash routes together (in all possible 4-ways)
        for saving, route_ids in savings:
            # follow the merge path (if any)
            route_i, route_j = route_ids
            while isinstance(route_sets[route_i], int):
                route_i = route_sets[route_i]
            while isinstance(route_sets[route_j], int):
                route_j = route_sets[route_j]
            if route_i == route_j: continue  #already merged

            # get nodes, TSP chain, demand, and lenght for both routes
            nodes_i = route_sets[route_i]
            nodes_j = route_sets[route_j]
            chain_i = tsp_cache[nodes_i][0][1:-1]
            l_i = tsp_cache[nodes_i][1]
            d_i = _get_demand(nodes_i, demand_cache, d)
            chain_j = tsp_cache[nodes_j][0][1:-1]
            l_j = tsp_cache[nodes_j][1]
            d_j = _get_demand(nodes_j, demand_cache, d)

            # check constraints
            if C and d_i + d_j - C_EPS > C: continue
            na, nb, nc, nd = chain_i[0], chain_i[-1], chain_j[0], chain_j[-1]
            alt_costs = [
                l_i + l_j + D[nb, nc] - D[nb, 0] - D[nc, 0],  #ab-cd
                l_i + l_j + D[na, nc] - D[na, 0] - D[nc, 0],  #ba-cd
                l_i + l_j + D[nb, nd] - D[nb, 0] - D[nd, 0],  #ab-dc
                l_i + l_j + D[na, nd] - D[na, 0] - D[nd, 0]
            ]  #cd-ab
            min_cost = min(alt_costs)
            min_alt = alt_costs.index(min_cost)
            if L and min_cost - S_EPS > L: continue

            # find the best merge among the 4-way merges and apply it
            if min_alt == 0: ij_route = [0] + chain_i + chain_j + [0]  #ab-cd
            if min_alt == 1:
                ij_route = [0] + chain_i[::-1] + chain_j + [0]  #ba-cd
            if min_alt == 2:
                ij_route = [0] + chain_i + chain_j[::-1] + [0]  #ab-dc
            if min_alt == 3: ij_route = [0] + chain_j + chain_i + [0]  #cd-ab
            nodes_ij = nodes_i.union(nodes_j)
            route_sets[route_i] = nodes_ij
            route_sets[route_j] = route_i  # mark as a reference to the another
            tsp_cache[nodes_ij] = (ij_route, min_cost)
    except KeyboardInterrupt:  #or SIGINT
        pass

    greedy_solution = []
    for nodes in route_sets:
        if isinstance(nodes, int): continue
        greedy_solution += tsp_cache[nodes][0][:-1]
    greedy_solution.append(0)
    return greedy_solution
Example #30
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