Ejemplo n.º 1
0
def cawlip_savings_init(D, d, C, L, minimize_K=False):
    """
    This implements the Robbins & Turner (1979) extension to the parallel
     savings algorithm of Clarke and Wright (1964). Clarke and Wright algorithm
     and its saving function are used as is, but the savings procedure is
     followed by 2-opt* improvement phase, where all possible ways of
     connecting any two edges of the solution are searched and improving moves
     are accepted. Please see the parallel_savings.py:parallel_savings_init for
     details and description of the parameters.
    """

    # Note: this is not a proper closure. Variables g and f are shared
    #  over all iterations. It is OK like this, but do not use/store the
    #  lambda after this loop.

    sol = parallel_savings_init(D, d, C, L, minimize_K)

    sol = do_local_search(
        [do_2opt_move],  #, do_2optstar_move],
        sol,
        D,
        d,
        C,
        L,
        LSOPT.BEST_ACCEPT)

    return sol
Ejemplo n.º 2
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
Ejemplo n.º 3
0
    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)
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)   
Ejemplo n.º 5
0
    def setUp(self):
        self.algorithms = [
            ("clarke_wright_savings", lambda pts, D,d,C,L,st:\
                parallel_savings_init(D,d,C,L)),
            ("paessens_savings_M1", lambda pts, D,d,C,L,st:\
                paessens_savings_init(D,d,C,L,strategy="M1", do_3opt=False)),
            ("paessens_savings_M4", lambda pts, D,d,C,L,st:\
                paessens_savings_init(D,d,C,L,strategy="M4", do_3opt=False)),
            ("clarke_wright_savings_3OPT", lambda pts, D,d,C,L,st:\
                do_local_search([do_3opt_move], parallel_savings_init(D,d,C,L),
                             D, d, C, L, operator_strategy=LSOPT.BEST_ACCEPT)),
            ("paessens_savings_M1_3OPT", lambda pts, D,d,C,L,st:\
                 paessens_savings_init(D,d,C,L,strategy="M1", do_3opt=True)),
            ("paessens_savings_M4_3OPT", lambda pts, D,d,C,L,st:\
                 paessens_savings_init(D,d,C,L,strategy="M4", do_3opt=True))
        ]

        self.problem_names = [
            "G1.vrp", "G2.vrp", "G3.vrp", "G4.vrp", "C1.vrp", "C2.vrp",
            "C3.vrp", "C4.vrp", "C5.vrp", "C6.vrp", "C7.vrp", "C8.vrp",
            "C9.vrp", "C10.vrp", "C11.vrp", "GJ1.vrp"
        ]

        # in Gaskell instances service_time was not included in the table
        # in Christofides et al. service_time was included
        self.targets = [
            #cw
            (599 - 10 * 21, 956 - 10 * 22, 964 - 10 * 29, 841 - 10 * 32, 585,
             907, 889, 834, 876, 1068, 1593, 1140, 1288, 1395, 1539, 5568),
            #M1
            (585 - 10 * 21, 956 - 10 * 22, 938 - 10 * 29, 814 - 10 * 32, 564,
             866, 866, 826, 870, 1065, 1584, 1102, 1222, 1370, 1486, 5380),
            #M4
            (598 - 10 * 21, 956 - 10 * 22, 938 - 10 * 29, 814 - 10 * 32, 571,
             889, 877, 826, 872, 1068, 1591, 1112, 1222, 1370, 1515, 5476),
            #cw+3opt
            (599 - 10 * 21, 956 - 10 * 22, 962 - 10 * 29, 840 - 10 * 32, 579,
             902, 880, 824, 869, 1047, 1583, 1134, 1285, 1387, 1522, 5546),
            #M1+3opt
            (585 - 10 * 21, 956 - 10 * 22, 937 - 10 * 29, 812 - 10 * 32, 557,
             861, 858, 822, 870, 1046, 1568, 1083, 1221, 1359, 1476, 5348),
            #M4+3opt
            (598 - 10 * 21, 956 - 10 * 22, 937 - 10 * 29, 812 - 10 * 32, 570,
             876, 869, 823, 871, 1047, 1580, 1106, 1221, 1359, 1504, 5449),
        ]

        self.problem_path = path.join("Classic", "Paessens1988", "literature")
Ejemplo n.º 6
0
def main(overridden_args=None):
    ## 1. parse arguments

    parser = ArgumentParser(
        description=
        "Solve some .vrp problems with the algorithms built into VeRyPy.")
    parser.add_argument('-l',
                        dest='list_algorithms',
                        help="List the available heuristics and quit",
                        action="store_true")
    parser.add_argument(
        '-v',
        dest='verbosity',
        help=
        "Set the verbosity level (to completely disable debug output, run this script with 'python -O')",
        type=int,
        default=-1)
    parser.add_argument(
        '-a',
        dest='active_algorithms',
        help=
        "Algorithm to apply (argument can be set multiple times to enable multiple algorithms, or one can use 'all' or 'classical')",
        action='append')
    parser.add_argument(
        '-b',
        dest='objective',
        choices=['c', 'cost', 'K', 'vehicles'],
        help="Primary optimization oBjective (default is cost)",
        default="cost")
    parser.add_argument(
        '-m',
        dest='minimal_output',
        help=
        "Overrides the output options and prints only one line CSV report per solved instance",
        action="store_true")
    parser.add_argument(
        '-t',
        dest='print_elapsed_time',
        help="Print elapsed wall time for each solution attempt",
        action="store_true")
    parser.add_argument(
        '-c',
        dest='show_solution_cost',
        help="Display solution cost instead of solution length",
        action="store_true")
    parser.add_argument('-D',
                        dest='dist_weight_format',
                        choices=['ROUND', 'EXACT', 'TRUNCATE'],
                        help="Force distance matrix rounding")
    parser.add_argument(
        '-1',
        dest='use_single_iteration',
        help="Force the algorithms to use only single iteration.",
        action="store_true")
    parser.add_argument(
        '--iinfo',
        dest='print_instance_info',
        help="Print the instance info in the collected results",
        action="store_true")
    parser.add_argument(
        '--routes',
        dest='print_route_stat',
        help="Print per route statistics of the final solution",
        action="store_true")
    parser.add_argument('--vrph',
                        dest='print_vrph_sol',
                        help="Print the final solution in the VRPH format",
                        action="store_true")
    parser.add_argument(
        '--forbid',
        dest='forbid_algorithms',
        help=
        "Forbid applying algorithms (argument can set multiple times to forbid multiple algorithms)",
        action='append')
    parser.add_argument(
        '--simulate',
        dest='simulate',
        help=
        "Do not really invoke algorithms, can be used e.g. to test scripts",
        action="store_true")
    #TODO: consider adding more LS opts e.g. 2optstart, 3optstart
    parser.add_argument(
        '--post-optimize',
        dest='local_search_operators',
        choices=['2opt', '3opt'],
        help=
        "Do post-optimization with local search operator(s) (can set multiple)",
        action='append')
    parser.add_argument(
        "problem_file",
        help=
        "a path of a .vrp problem file, a directory containing .vrp files, or a text file of paths to .vrp files",
        action='append')

    if overridden_args:
        app_args = parser.parse_args(overridden_args)
    elif "-l" in sys.argv:
        print("Select at least one algorithm (with -a) from the list:",
              file=sys.stderr)
        print(_build_algorithm_help())
        sys.exit(1)
    elif len(sys.argv) == 1:
        print("Give at least one .vrp file and use -h to get help.",
              file=sys.stderr)
        sys.exit(1)
    else:
        app_args = parser.parse_args()

    # some further argument validation
    if not app_args.active_algorithms or app_args.list_algorithms:
        print("Select at least one algorithm (with -a) from the list:",
              file=sys.stderr)
        print(_build_algorithm_help())
        exit()
    if len(app_args.problem_file) == 0:
        print("Provide at least one .vrp file to solve", file=sys.stderr)
        exit()

    # get .vrp file list
    files_to_solve = shared_cli.get_a_problem_file_list(app_args.problem_file)

    # get algorithms
    algos = get_algorithms(app_args.active_algorithms)
    if app_args.forbid_algorithms:
        forbidden_algos = [
            algo_name_aliases[algo_name]
            for algo_name in app_args.forbid_algorithms
            if (algo_name in app_args.forbid_algorithms)
        ]
        algos = [a for a in algos if (a[0] not in forbidden_algos)]

    # get primary objective
    minimize_K = False
    if app_args.objective == 'K' or app_args.objective == 'vehicles':
        minimize_K = True

    run_single_iteration = False
    if app_args.use_single_iteration:
        run_single_iteration = True

    # get post-optimization local search move operators
    ls_ops = []
    ls_algo_names = []
    if app_args.local_search_operators:
        ls_algo_names = app_args.local_search_operators
        for ls_op_name in ls_algo_names:
            if ls_op_name == "2opt":
                ls_ops.append(do_2opt_move)
            if ls_op_name == "3opt":
                ls_ops.append(do_3opt_move)

    # verbosity
    if app_args.verbosity >= 0:
        shared_cli.set_logger_level(app_args.verbosity)

    # minimal header
    if app_args.minimal_output:
        print("algo;problem;is_feasible;f;K;t")

    ## 2. solve
    results = defaultdict(lambda: defaultdict(float))
    instance_data = dict()

    interrupted = False
    for pfn in files_to_solve:
        bn = path.basename(pfn).replace(".vrp",
                                        "").replace(".tsp",
                                                    "").replace(".pickle", "")

        try:
            N, points, dd_points, d, D, C, ewt, K, L, st = pickle.load(
                open(pfn, "rb"))
        except:
            N, points, dd_points, d, D, C, ewt = cvrp_io.read_TSPLIB_CVRP(pfn)
            K, L, st = cvrp_io.read_TSBLIB_additional_constraints(pfn)

        # We do not have point coodrinates, but we have D!
        if points is None:
            if dd_points is not None:
                points = dd_points
            else:
                points, ewt = cvrp_ops.generate_missing_coordinates(D)

        if app_args.dist_weight_format == "TRUNCATE":
            D = np.floor(D)
            ewt = "FLOOR_2D"
        if app_args.dist_weight_format == "ROUND":
            D = np.int(D)
            ewt = "EUC_2D"

        # Bake service time to D (if needed)
        D_c = cvrp_ops.D2D_c(D, st) if st else D

        for algo_abbreviation, algo_name, _, algo_f in algos:
            if not app_args.minimal_output:
                print("Solving %s with %s" % (bn, algo_name))
            start_t = time()
            sol = None
            try:
                if not app_args.simulate:
                    sol = algo_f(points, D_c, d, C, L, st, ewt,
                                 run_single_iteration, minimize_K)
            except (KeyboardInterrupt, Exception) as e:
                if type(e) is KeyboardInterrupt:
                    interrupted = True
                    # if interrupted on initial sol gen, return the best of those
                    if len(e.args) > 0 and type(e.args[0]) is list:
                        sol = e.args[0]
                    if not app_args.minimal_output:
                        print("WARNING: Interrupted solving %s with %s" %
                              (bn, algo_abbreviation),
                              file=sys.stderr)
                else:
                    if not app_args.minimal_output:
                        print("ERROR: Failed to solve %s with %s because %s" %
                              (bn, algo_abbreviation, str(e)),
                              file=sys.stderr)
                    sol = None

            if sol:
                sol = cvrp_ops.normalize_solution(sol)
                if app_args.show_solution_cost:
                    sol_q = cvrp_ops.calculate_objective(sol, D_c)
                else:
                    sol_q = cvrp_ops.calculate_objective(sol, D)
                sol_K = sol.count(0) - 1

                if app_args.local_search_operators:
                    if not app_args.minimal_output:
                        print("Postoptimize with %s ..." %
                              ", ".join(app_args.local_search_operators),
                              end="")
                    sol = do_local_search(ls_ops, sol, D, d, C, L)
                    sol = cvrp_ops.normalize_solution(sol)

                    if app_args.show_solution_cost:
                        ls_sol_q = cvrp_ops.calculate_objective(sol, D_c)
                    else:
                        ls_sol_q = cvrp_ops.calculate_objective(sol, D)
                    if ls_sol_q < sol_q:
                        if not app_args.minimal_output:
                            print(" improved by %.2f%%." %
                                  (1 - ls_sol_q / sol_q))
                        sol_q = ls_sol_q
                        sol_K = sol.count(0) - 1
                    else:
                        if not app_args.minimal_output:
                            print(" did not find improving moves.")
            else:
                sol_q = float('inf')

            elapsed_t = time() - start_t
            if app_args.minimal_output:
                print("%s;%s" % (algo_abbreviation, bn), end="")
                timecap_symbol = "*" if interrupted else ""
                if sol:
                    feasible = all(
                        cvrp_ops.check_solution_feasibility(
                            sol, D_c, d, C, L, st))
                    print(";%s;%.2f;%d;%.2f%s" % (str(feasible), sol_q, sol_K,
                                                  elapsed_t, timecap_symbol))
                else:
                    print(";False;inf;inf;%.2f%s" %
                          (elapsed_t, timecap_symbol))

            elif sol:
                # Minimal output is not enabled, print like crazy :)

                if app_args.print_elapsed_time:
                    print("Algorithm produced a solution in %.2f s\n" %
                          (elapsed_t))
                else:
                    #just a newline
                    print()

                tightness = None
                if C and sol_K:
                    tightness = (sum(d) / (C * sol_K))
                if not bn in instance_data or sol_K < instance_data[bn][1]:
                    #"N K C tightness L st"
                    instance_data[bn] = (N, sol_K, C, "%.3f" % tightness, L,
                                         st)

                shared_cli.print_problem_information(
                    points,
                    D_c,
                    d,
                    C,
                    L,
                    st,
                    tightness,
                    verbosity=app_args.verbosity)

                solution_print_verbosity = 3 if app_args.print_route_stat else 1
                shared_cli.print_solution_statistics(sol, D, D_c, d, C, L, st,
                                                     solution_print_verbosity)

                if app_args.print_vrph_sol:
                    print("SOLUTION IN VRPH FORMAT:")
                    print(" ".join(
                        str(n) for n in cvrp_io.as_VRPH_solution(sol)))
                print("\n")

            short_algo_name = algo_name
            results[bn][short_algo_name] = sol_q

            if interrupted:
                break  # algo loop
        if interrupted:
            break  # problem file loop

    ## Print collected results
    sys.stdout.flush()
    sys.stderr.flush()
    if not app_args.minimal_output and (len(results) > 1 or len(algos) > 1):
        print("\n")
        print_title = True
        ls_label = "+".join(ls_algo_names)
        for problem, algo_results in sorted(results.items()):
            algo_names = [ "%s+%s"%(algo_name,ls_label) if ls_algo_names else (algo_name)\
                           for algo_name in sorted(algo_results.keys())]

            if print_title:
                instance_fields = "instance\t"
                if PRINT_INSTANCE_DATA:
                    #"N K C tightness L st"
                    instance_fields += "N\tK*\tC\ttightness\tL\tst\t"
                print(instance_fields + "\t".join(algo_names))
                print_title = False
            print(problem, end="")
            if PRINT_INSTANCE_DATA:
                print("\t", end="")
                print("\t".join(str(e) for e in instance_data[problem]),
                      end="")
            for _, result in sorted(algo_results.items()):
                print("\t", result, end="")
            print()
    sys.stdout.flush()
    sys.stderr.flush()
Ejemplo n.º 7
0
 def test_smoke(self):
     print("in", self.initial_sol)
     smoke_sol = do_local_search([do_3optstar_move], self.initial_sol,
                                 self.D, self.d, self.C)
     print("out", smoke_sol)
Ejemplo n.º 8
0
def paessens_savings_init(D,
                          d,
                          C,
                          L,
                          minimize_K=False,
                          strategy="M4",
                          do_3opt=True):
    """
    This implements the Paesses (1988) variant of the parallel savings
     algorithm of Clarke and Wright (1964). The savings function of
     (Paesses 1988) is parametrized with multipiers g and f:
         
         S_ij  = d_0i + d_0j - g * d_ij + f * | d_0i - d_0j |
    
    If two merges have the same savings value, the one where i and j are closer
     to another takes precedence. Otherwise the impelementation details can be 
     read from parallel_savings.py as it contains the actual code implementing 
     the parallel savings procedure. The variant specific parameters are:
     
    * strategy which can be:
        - "M1" for 143 runs of the savings algorithm with all combinations of
           g = np.linspace(0.8, 2.0, num=13)
           f = np.linspace(0.0, 1.0, num=11)            
        - "M4" for 8 runs (g,f) = (1.0,0.1), (1.0,0.5), (1.4,0.0), (1.4,0.5)
           with a parameter combinations +/- 0.1 around the best of these four. 
        - or a list of (g,f) value tuples.
    * do_3opt (default True) optimize the resulting routes to 3-optimality
    
    Note: Due to the use of modern computer, and low priority in computational
     efficiency of this implementation, not all of the tecninques specified in
     "reduction of computer requirements" (Paessens 1988) were employed.
    """

    parameters = []
    if strategy == "M1":
        parameters.extend(
            _cartesian_product(np.linspace(0.8, 2.0, num=13),
                               np.linspace(0.0, 1.0, num=11)))
    elif strategy == "M4":
        parameters.extend([(1.0, 0.1), (1.0, 0.5), (1.4, 0.0), (1.4, 0.5)])
    else:
        parameters.extend(strategy)

    best_params = None
    best_sol = None
    best_f = None
    best_K = None
    interrupted = False

    params_idx = 0
    while params_idx < len(parameters):
        g, f = parameters[params_idx]

        # Note: this is not a proper closure. Variables g and f are shared
        #  over all iterations. It is OK like this, but do not use/store the
        #  lambda after this loop.
        gf_savings = lambda D: paessens_savings_function(D, g, f)

        sol, sol_f, sol_K = None, float('inf'), float('inf')
        try:
            sol = parallel_savings_init(D, d, C, L, minimize_K, gf_savings)
            if do_3opt:
                sol = do_local_search([do_3opt_move], sol, D, d, C, L,
                                      LSOPT.BEST_ACCEPT)
            # 3-opt may make some of the routes empty
            sol = without_empty_routes(sol)
        except KeyboardInterrupt as e:  # or SIGINT
            # some parameter combination 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
            best_params = (g, f)

        if interrupted:
            raise KeyboardInterrupt(best_sol)

        params_idx += 1
        # after the best of 4 for the M4 is found, check 4 more around it
        if params_idx == 4 and strategy == "M4":
            g_prime, f_prime = best_params
            parameters.extend([(g_prime - M4_FINETUNE_STEP, f_prime),
                               (g_prime + M4_FINETUNE_STEP, f_prime),
                               (g_prime, f_prime - M4_FINETUNE_STEP),
                               (g_prime, f_prime + M4_FINETUNE_STEP)])
    return best_sol