Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue#6 #19

Merged
merged 1 commit into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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 @@
: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 @@
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 @@
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 @@

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 @@
)

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

Check warning on line 479 in geneal/genetic_algorithms/genetic_algorithm_base.py

View check run for this annotation

Codecov / codecov/patch

geneal/genetic_algorithms/genetic_algorithm_base.py#L479

Added line #L479 was not covered by tests

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
Loading