Skip to content

Commit

Permalink
Add option to exit loop early
Browse files Browse the repository at this point in the history
  • Loading branch information
diogomatoschaves committed Feb 20, 2024
1 parent ed15fec commit 6432716
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 111 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ solver = BinaryGenAlgSolver(
mutation_rate=0.05, # mutation rate to apply to the population
selection_rate=0.5, # percentage of the population to select for mating
selection_strategy="roulette_wheel", # strategy to use for selection. see below for more details
fitness_tolerance=(1E-4, 50) # Loop will be exited if the best fitness value does not change more than
# 1E-4 for 50 generations
)

solver.solve()
Expand Down Expand Up @@ -98,6 +100,8 @@ solver = ContinuousGenAlgSolver(
mutation_rate=0.1, # mutation rate to apply to the population
selection_rate=0.6, # percentage of the population to select for mating
selection_strategy="roulette_wheel", # strategy to use for selection. see below for more details
fitness_tolerance=(1E-5, 20) # Loop will be exited if the best fitness value does not change more than
# 1E-5 for 20 generations
)

solver.solve()
Expand Down
7 changes: 7 additions & 0 deletions geneal/genetic_algorithms/_binary.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def __init__(
plot_results: bool = True,
excluded_genes: Sequence = None,
n_crossover_points: int = 1,
fitness_tolerance=None,
random_state: int = None,
):
"""
Expand All @@ -36,6 +37,11 @@ def __init__(
:param verbose: whether to print iterations status
:param show_stats: whether to print stats at the end
:param plot_results: whether to plot results of the run at the end
:param fitness_tolerance: optional. (a, b) tuple consisting of the tolerance on the
change in the best fitness, and the number of generations the condition
holds true. If the best fitness does not change by a value of (a) for a specified
number of iterations (b), the solver stops and exits the loop.
:param random_state: optional. whether the random seed should be set
"""

GenAlgSolver.__init__(
Expand All @@ -53,6 +59,7 @@ def __init__(
excluded_genes=excluded_genes,
n_crossover_points=n_crossover_points,
random_state=random_state,
fitness_tolerance=fitness_tolerance
)

def initialize_population(self):
Expand Down
9 changes: 8 additions & 1 deletion geneal/genetic_algorithms/_continuous.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def __init__(
variables_limits=(-10, 10),
problem_type=float,
n_crossover_points: int = 1,
fitness_tolerance=None,
random_state: int = None,
):
"""
Expand All @@ -39,8 +40,13 @@ def __init__(
:param show_stats: whether to print stats at the end
:param plot_results: whether to plot results of the run at the end
:param variables_limits: limits for each variable [(x1_min, x1_max), (x2_min, x2_max), ...].
If only one tuple is provided, then it is assumed the same for every variable
If only one tuple is provided, then it is assumed the same for every variable
:param problem_type: whether problem is of float or integer type
:param fitness_tolerance: optional. (a, b) tuple consisting of the tolerance on the
change in the best fitness, and the number of generations the condition
holds true. If the best fitness does not change by a value of (a) for a specified
number of iterations (b), the solver stops and exits the loop.
:param random_state: optional. whether the random seed should be set
"""

GenAlgSolver.__init__(
Expand All @@ -58,6 +64,7 @@ def __init__(
excluded_genes=excluded_genes,
n_crossover_points=n_crossover_points,
random_state=random_state,
fitness_tolerance=fitness_tolerance
)

if not variables_limits:
Expand Down
24 changes: 23 additions & 1 deletion geneal/genetic_algorithms/genetic_algorithm_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def __init__(
plot_results: bool = True,
excluded_genes: Sequence = None,
n_crossover_points: int = 1,
fitness_tolerance=None,
random_state: int = None,
):
"""
Expand All @@ -47,7 +48,12 @@ def __init__(
:param show_stats: whether to print stats at the end
:param plot_results: whether to plot results of the run at the end
:param n_crossover_points: number of slices to make for the crossover
:param fitness_tolerance: optional. (a, b) tuple consisting of the tolerance on the
change in the best fitness, and the number of generations the condition
holds true. If the best fitness does not change by a value of (a) for a specified
number of iterations (b), the solver stops and exits the loop.
:param random_state: optional. whether the random seed should be set
"""

if isinstance(random_state, int):
Expand All @@ -72,6 +78,8 @@ def __init__(
self.verbose = verbose
self.show_stats = show_stats
self.plot_results = plot_results
self.fitness_tolerance = fitness_tolerance
self.periods_same_fitness = 0

self.pop_keep = math.floor(selection_rate * pop_size)

Expand Down Expand Up @@ -150,6 +158,8 @@ def solve(self):
gen_n = 0
while True:

best_fitness = fitness[0]

gen_n += 1

if self.verbose and gen_n % gen_interval == 0:
Expand Down Expand Up @@ -185,7 +195,7 @@ def solve(self):

fitness, population = self.sort_by_fitness(fitness, population)

if gen_n >= self.max_gen:
if gen_n >= self.max_gen or self._check_condition_to_stop(best_fitness, fitness):
break

self.generations_ = gen_n
Expand Down Expand Up @@ -457,3 +467,15 @@ def mutate_population(self, population, n_mutations):
)

return mutation_rows, mutation_cols

def _check_condition_to_stop(self, best_fitness, fitness):

if self.fitness_tolerance is None:
return False

if np.abs(best_fitness - fitness[0]) < self.fitness_tolerance[0]:
self.periods_same_fitness += 1
else:
self.periods_same_fitness = 0

return self.periods_same_fitness >= self.fitness_tolerance[1]
157 changes: 49 additions & 108 deletions tests/genetic_algorithms/test_binary_genetic_algorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,125 +87,56 @@ def test_mutate_population(self):

assert np.equal(mutated_population, expected_mutated_population).all()

@pytest.mark.parametrize(
'fitness_tolerance',
[
pytest.param(
None,
id='no_tolerance'
),
pytest.param(
(10, 2),
id='with_tolerance'
),
]
)
@pytest.mark.parametrize(
"fitness_function, n_genes, expected_best_fitness, expected_best_individual",
[
pytest.param(
1,
50,
47.0,
np.array(
[
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
0.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
0.0,
1.0,
1.0,
1.0,
1.0,
1.0,
0.0,
1.0,
1.0,
1.0,
1.0,
]
),
[47.0, 38.0],
[
np.array([
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0,
1.0, 1.0
]),
np.array([
0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1,
0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0,
1, 1,
])
],
id="binary_fitness_function=1",
),
pytest.param(
2,
48,
-4.0,
np.array(
[
1.0,
0.0,
1.0,
1.0,
0.0,
0.0,
0.0,
1.0,
1.0,
0.0,
0.0,
1.0,
1.0,
0.0,
1.0,
1.0,
0.0,
1.0,
0.0,
0.0,
1.0,
0.0,
0.0,
0.0,
1.0,
0.0,
0.0,
1.0,
0.0,
0.0,
1.0,
0.0,
0.0,
0.0,
0.0,
0.0,
1.0,
0.0,
1.0,
0.0,
1.0,
1.0,
1.0,
1.0,
1.0,
0.0,
0.0,
0.0,
]
),
[-4.0, -18],
[
np.array([
1.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 1.0,
0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0
]),
np.array([
1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1,
1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0,
])
],
id="binary_fitness_function=2",
),
],
Expand All @@ -218,6 +149,7 @@ def test_solve(
n_genes,
expected_best_fitness,
expected_best_individual,
fitness_tolerance
):

solver = BinaryGenAlgSolver(
Expand All @@ -228,11 +160,20 @@ def test_solve(
mutation_rate=0.05,
selection_rate=0.5,
random_state=42,
fitness_tolerance=fitness_tolerance
)

solver.solve()

print(solver.best_individual_)

expected_best_fitness = expected_best_fitness[0] \
if fitness_tolerance is None \
else expected_best_fitness[1]

expected_best_individual = expected_best_individual[0] \
if fitness_tolerance is None \
else expected_best_individual[1]

assert solver.best_fitness_ == expected_best_fitness
assert np.equal(solver.best_individual_, expected_best_individual).all()
16 changes: 15 additions & 1 deletion tests/genetic_algorithms/test_continuous_genetic_algorithm.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,19 @@ def test_mutate_population(self):

assert np.allclose(mutated_population, expected_mutated_population, rtol=1e-5)

@pytest.mark.parametrize(
'fitness_tolerance',
[
pytest.param(
None,
id='no_tolerance'
),
pytest.param(
(10, 2),
id='with_tolerance'
),
]
)
@pytest.mark.parametrize(
"fitness_function, n_genes, expected_best_fitness, expected_best_individual",
[
Expand Down Expand Up @@ -195,13 +208,13 @@ def test_mutate_population(self):
)
def test_solve(
self,
mocker,
mock_matplotlib,
mock_logging,
fitness_function,
n_genes,
expected_best_fitness,
expected_best_individual,
fitness_tolerance
):

solver = ContinuousGenAlgSolver(
Expand All @@ -212,6 +225,7 @@ def test_solve(
mutation_rate=0.05,
selection_rate=0.5,
random_state=42,
fitness_tolerance=fitness_tolerance
)

solver.solve()
Expand Down

0 comments on commit 6432716

Please sign in to comment.