1
1
import random
2
2
from collections .abc import Callable , Sequence
3
3
from concurrent .futures import ThreadPoolExecutor
4
-
5
4
import numpy as np
6
5
7
6
# Parameters
@@ -40,7 +39,25 @@ def __init__(
40
39
self .population = self .initialize_population ()
41
40
42
41
def initialize_population (self ) -> list [np .ndarray ]:
43
- """Initialize the population with random individuals within the search space."""
42
+ """
43
+ Initialize the population with random individuals within the search space.
44
+
45
+ Example:
46
+ >>> ga = GeneticAlgorithm(
47
+ ... function=lambda x, y: x**2 + y**2,
48
+ ... bounds=[(-10, 10), (-10, 10)],
49
+ ... population_size=5,
50
+ ... generations=10,
51
+ ... mutation_prob=0.1,
52
+ ... crossover_rate=0.8,
53
+ ... maximize=False
54
+ ... )
55
+ >>> len(ga.initialize_population())
56
+ 5 # The population size should be equal to 5.
57
+ >>> all(len(ind) == 2 for ind in ga.initialize_population())
58
+ # Each individual should have 2 variables
59
+ True
60
+ """
44
61
return [
45
62
rng .uniform (
46
63
low = [self .bounds [j ][0 ] for j in range (self .dim )],
@@ -50,14 +67,58 @@ def initialize_population(self) -> list[np.ndarray]:
50
67
]
51
68
52
69
def fitness (self , individual : np .ndarray ) -> float :
53
- """Calculate the fitness value (function value) for an individual."""
70
+ """
71
+ Calculate the fitness value (function value) for an individual.
72
+
73
+ Example:
74
+ >>> ga = GeneticAlgorithm(
75
+ ... function=lambda x, y: x**2 + y**2,
76
+ ... bounds=[(-10, 10), (-10, 10)],
77
+ ... population_size=10,
78
+ ... generations=10,
79
+ ... mutation_prob=0.1,
80
+ ... crossover_rate=0.8,
81
+ ... maximize=False
82
+ ... )
83
+ >>> individual = np.array([1.0, 2.0])
84
+ >>> ga.fitness(individual)
85
+ 5.0 # The fitness should be 1^2 + 2^2 = 5
86
+ >>> ga.maximize = True
87
+ >>> ga.fitness(individual)
88
+ -5.0 # The fitness should be -5 when maximizing
89
+ """
54
90
value = float (self .function (* individual )) # Ensure fitness is a float
55
91
return value if self .maximize else - value # If minimizing, invert the fitness
56
92
57
93
def select_parents (
58
94
self , population_score : list [tuple [np .ndarray , float ]]
59
95
) -> list [np .ndarray ]:
60
- """Select top N_SELECTED parents based on fitness."""
96
+ """
97
+ Select top N_SELECTED parents based on fitness.
98
+
99
+ Example:
100
+ >>> ga = GeneticAlgorithm(
101
+ ... function=lambda x, y: x**2 + y**2,
102
+ ... bounds=[(-10, 10), (-10, 10)],
103
+ ... population_size=10,
104
+ ... generations=10,
105
+ ... mutation_prob=0.1,
106
+ ... crossover_rate=0.8,
107
+ ... maximize=False
108
+ ... )
109
+ >>> population_score = [
110
+ ... (np.array([1.0, 2.0]), 5.0),
111
+ ... (np.array([-1.0, -2.0]), 5.0),
112
+ ... (np.array([0.0, 0.0]), 0.0),
113
+ ... ]
114
+ >>> selected_parents = ga.select_parents(population_score)
115
+ >>> len(selected_parents)
116
+ 2 # Should select the two parents with the best fitness scores.
117
+ >>> np.array_equal(selected_parents[0], np.array([1.0, 2.0])) # Parent 1 should be [1.0, 2.0]
118
+ True
119
+ >>> np.array_equal(selected_parents[1], np.array([-1.0, -2.0])) # Parent 2 should be [-1.0, -2.0]
120
+ True
121
+ """
61
122
population_score .sort (key = lambda score_tuple : score_tuple [1 ], reverse = True )
62
123
selected_count = min (N_SELECTED , len (population_score ))
63
124
return [ind for ind , _ in population_score [:selected_count ]]
@@ -67,11 +128,13 @@ def crossover(
67
128
) -> tuple [np .ndarray , np .ndarray ]:
68
129
"""
69
130
Perform uniform crossover between two parents to generate offspring.
131
+
70
132
Args:
71
133
parent1 (np.ndarray): The first parent.
72
134
parent2 (np.ndarray): The second parent.
73
135
Returns:
74
136
tuple[np.ndarray, np.ndarray]: The two offspring generated by crossover.
137
+
75
138
Example:
76
139
>>> ga = GeneticAlgorithm(
77
140
... lambda x, y: -(x**2 + y**2),
@@ -92,10 +155,13 @@ def crossover(
92
155
def mutate (self , individual : np .ndarray ) -> np .ndarray :
93
156
"""
94
157
Apply mutation to an individual.
158
+
95
159
Args:
96
160
individual (np.ndarray): The individual to mutate.
161
+
97
162
Returns:
98
163
np.ndarray: The mutated individual.
164
+
99
165
Example:
100
166
>>> ga = GeneticAlgorithm(
101
167
... lambda x, y: -(x**2 + y**2),
@@ -115,9 +181,11 @@ def mutate(self, individual: np.ndarray) -> np.ndarray:
115
181
def evaluate_population (self ) -> list [tuple [np .ndarray , float ]]:
116
182
"""
117
183
Evaluate the fitness of the entire population in parallel.
184
+
118
185
Returns:
119
186
list[tuple[np.ndarray, float]]:
120
187
The population with their respective fitness values.
188
+
121
189
Example:
122
190
>>> ga = GeneticAlgorithm(
123
191
... lambda x, y: -(x**2 + y**2),
@@ -141,11 +209,33 @@ def evaluate_population(self) -> list[tuple[np.ndarray, float]]:
141
209
)
142
210
)
143
211
144
- def evolve (self , verbose = True ) -> np .ndarray :
212
+ def evolve (self , verbose : bool = True ) -> np .ndarray :
145
213
"""
146
214
Evolve the population over the generations to find the best solution.
215
+
216
+ Args:
217
+ verbose (bool): If True, prints the progress of the generations.
218
+
147
219
Returns:
148
220
np.ndarray: The best individual found during the evolution process.
221
+
222
+ Example:
223
+ >>> ga = GeneticAlgorithm(
224
+ ... function=lambda x, y: x**2 + y**2,
225
+ ... bounds=[(-10, 10), (-10, 10)],
226
+ ... population_size=10,
227
+ ... generations=10,
228
+ ... mutation_prob=0.1,
229
+ ... crossover_rate=0.8,
230
+ ... maximize=False
231
+ ... )
232
+ >>> best_solution = ga.evolve(verbose=False)
233
+ >>> len(best_solution)
234
+ 2 # The best solution should be a 2-element array (var_x, var_y)
235
+ >>> isinstance(best_solution[0], float) # First element should be a float
236
+ True
237
+ >>> isinstance(best_solution[1], float) # Second element should be a float
238
+ True
149
239
"""
150
240
for generation in range (self .generations ):
151
241
# Evaluate population fitness (multithreaded)
@@ -186,6 +276,7 @@ def target_function(var_x: float, var_y: float) -> float:
186
276
var_y (float): The y-coordinate.
187
277
Returns:
188
278
float: The value of the function at (var_x, var_y).
279
+
189
280
Example:
190
281
>>> target_function(0, 0)
191
282
0
0 commit comments