diff --git a/cgp/ea/mu_plus_lambda.py b/cgp/ea/mu_plus_lambda.py index f2a62211..09844e16 100644 --- a/cgp/ea/mu_plus_lambda.py +++ b/cgp/ea/mu_plus_lambda.py @@ -52,6 +52,8 @@ def __init__( self.local_search = local_search self.k_local_search = k_local_search + self.n_objective_calls: int = 0 + def initialize_fitness_parents( self, pop: Population, objective: Callable[[IndividualBase], IndividualBase] ) -> None: @@ -145,6 +147,9 @@ def _create_new_offspring_generation(self, pop: Population) -> List[IndividualBa def _compute_fitness( self, combined: List[IndividualBase], objective: Callable[[IndividualBase], IndividualBase] ) -> List[IndividualBase]: + + self.update_n_objective_calls(combined) + # computes fitness on all individuals, objective functions # should return immediately if fitness is not None if self.n_processes == 1: @@ -182,3 +187,11 @@ def _create_new_parent_population( """ return combined[:n_parents] + + def update_n_objective_calls(self, combined: List[IndividualBase]) -> None: + """Increase n_objective_calls by the number of individuals with fitness=None, + i.e., for which the objective function will be evaluated. + """ + for individual in combined: + if individual.fitness is None: + self.n_objective_calls += 1 diff --git a/cgp/hl_api.py b/cgp/hl_api.py index 93f7c2d6..cbce3b78 100644 --- a/cgp/hl_api.py +++ b/cgp/hl_api.py @@ -11,8 +11,9 @@ def evolve( pop: Population, objective: Callable[[IndividualBase], IndividualBase], ea: MuPlusLambda, - max_generations: int, min_fitness: float, + max_generations: int = np.inf, + max_objective_calls: int = np.inf, print_progress: Optional[bool] = False, callback: Optional[Callable[[Population], None]] = None, n_processes: int = 1, @@ -31,10 +32,15 @@ def evolve( ea : EA algorithm instance The evolution algorithm. Needs to be a class instance with an `initialize_fitness_parents` and `step` method. - max_generations : int - Maximum number of generations. min_fitness : float Minimum fitness at which the evolution is stopped. + max_generations : int + Maximum number of generations. + Defaults to positive infinity. + Either this or `max_objective_calls` needs to be set to a finite value. + max_objective_calls: int + Maximum number of function evaluations. + Defaults to positive infinity. print_progress : boolean, optional Switch to print out the progress of the algorithm. Defaults to False. callback : callable, optional @@ -49,6 +55,8 @@ def evolve( ------- None """ + if np.isinf(max_generations) and np.isinf(max_objective_calls): + raise ValueError("Either max_generations or max_objective_calls must be finite.") ea.initialize_fitness_parents(pop, objective) if callback is not None: @@ -57,7 +65,7 @@ def evolve( # perform evolution max_fitness = np.finfo(np.float).min # Main loop: -1 offset since the last loop iteration will still increase generation by one - while pop.generation < max_generations - 1: + while pop.generation < max_generations - 1 and ea.n_objective_calls < max_objective_calls: pop = ea.step(pop, objective) @@ -69,11 +77,21 @@ def evolve( max_fitness = pop.champion.fitness if print_progress: - print( - f"\r[{pop.generation + 1}/{max_generations}] max fitness: {max_fitness}\033[K", - end="", - flush=True, - ) + if np.isfinite(max_generations): + print( + f"\r[{pop.generation + 1}/{max_generations}] max fitness: {max_fitness}\033[K", + end="", + flush=True, + ) + elif np.isfinite(max_objective_calls): + print( + f"\r[{ea.n_objective_calls}/{max_objective_calls}] " + f"max fitness: {max_fitness}\033[K", + end="", + flush=True, + ) + else: + assert False # should never be reached if callback is not None: callback(pop) diff --git a/test/test_ea_mu_plus_lambda.py b/test/test_ea_mu_plus_lambda.py index 4753fef7..57348a9f 100644 --- a/test/test_ea_mu_plus_lambda.py +++ b/test/test_ea_mu_plus_lambda.py @@ -221,3 +221,43 @@ def test_create_new_parent_population(population_params, genome_params, ea_param # we picked the first three individuals new_parents = ea._create_new_parent_population(3, pop.parents) assert new_parents == pop.parents[:3] + + +def test_update_n_objective_calls(population_params, genome_params, ea_params): + def objective(individual): + individual.fitness = float(individual.idx) + return individual + + n_objective_calls_expected = 0 + pop = cgp.Population(**population_params, genome_params=genome_params) + ea = cgp.ea.MuPlusLambda(**ea_params) + assert ea.n_objective_calls == n_objective_calls_expected + + ea.initialize_fitness_parents(pop, objective) + n_objective_calls_expected = population_params["n_parents"] + assert ea.n_objective_calls == n_objective_calls_expected + + n_generations = 100 + for _ in range(n_generations): + offsprings = ea._create_new_offspring_generation(pop) + combined = offsprings + pop.parents + n_objective_calls_expected += sum([1 for ind in combined if ind.fitness is None]) + combined = ea._compute_fitness(combined, objective) + assert n_objective_calls_expected == ea.n_objective_calls + + +def test_update_n_objective_calls_mutation_rate_one(population_params, genome_params, ea_params): + def objective(individual): + individual.fitness = float(individual.idx) + return individual + + population_params["mutation_rate"] = 1.0 + pop = cgp.Population(**population_params, genome_params=genome_params) + ea = cgp.ea.MuPlusLambda(**ea_params) + ea.initialize_fitness_parents(pop, objective) + n_objective_calls_expected = population_params["n_parents"] + n_step_calls = 100 + for idx_current_step in range(n_step_calls): + ea.step(pop, objective) + n_objective_calls_expected += ea_params["n_offsprings"] + assert ea.n_objective_calls == n_objective_calls_expected diff --git a/test/test_hl_api.py b/test/test_hl_api.py index 62f70a0e..84a55ff9 100644 --- a/test/test_hl_api.py +++ b/test/test_hl_api.py @@ -135,6 +135,24 @@ def f1(x): assert abs(pop.champion.fitness) == pytest.approx(0.0) +def test_finite_max_generations_or_max_objective_calls( + population_params, genome_params, ea_params +): + def objective(individual): + individual.fitness = float(individual.idx) + return individual + + pop = cgp.Population(**population_params, genome_params=genome_params) + ea = cgp.ea.MuPlusLambda(**ea_params) + evolve_params = { + "max_generations": np.inf, + "min_fitness": 0, + "max_objective_calls": np.inf, + } + with pytest.raises(ValueError): + cgp.evolve(pop, objective, ea, **evolve_params) + + def _objective_speedup_parallel_evolve(individual): time.sleep(0.25)