Skip to content

Commit 39be73f

Browse files
authored
Create genetic_algorithm_optimization.py
1 parent 03a4251 commit 39be73f

File tree

1 file changed

+214
-0
lines changed

1 file changed

+214
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import random
2+
from collections.abc import Callable, Sequence
3+
from concurrent.futures import ThreadPoolExecutor
4+
5+
import numpy as np
6+
7+
# Parameters
8+
N_POPULATION = 100 # Population size
9+
N_GENERATIONS = 500 # Maximum number of generations
10+
N_SELECTED = 50 # Number of parents selected for the next generation
11+
MUTATION_PROBABILITY = 0.1 # Mutation probability
12+
CROSSOVER_RATE = 0.8 # Probability of crossover
13+
SEARCH_SPACE = (-10, 10) # Search space for the variables
14+
15+
# Random number generator
16+
rng = np.random.default_rng()
17+
18+
19+
class GeneticAlgorithm:
20+
def __init__(
21+
self,
22+
function: Callable[[float, float], float],
23+
bounds: Sequence[tuple[int | float, int | float]],
24+
population_size: int,
25+
generations: int,
26+
mutation_prob: float,
27+
crossover_rate: float,
28+
maximize: bool = True,
29+
) -> None:
30+
self.function = function # Target function to optimize
31+
self.bounds = bounds # Search space bounds (for each variable)
32+
self.population_size = population_size
33+
self.generations = generations
34+
self.mutation_prob = mutation_prob
35+
self.crossover_rate = crossover_rate
36+
self.maximize = maximize
37+
self.dim = len(bounds) # Dimensionality of the function (number of variables)
38+
39+
# Initialize population
40+
self.population = self.initialize_population()
41+
42+
def initialize_population(self) -> list[np.ndarray]:
43+
"""Initialize the population with random individuals within the search space."""
44+
return [
45+
rng.uniform(
46+
low=[self.bounds[j][0] for j in range(self.dim)],
47+
high=[self.bounds[j][1] for j in range(self.dim)],
48+
)
49+
for _ in range(self.population_size)
50+
]
51+
52+
def fitness(self, individual: np.ndarray) -> float:
53+
"""Calculate the fitness value (function value) for an individual."""
54+
value = float(self.function(*individual)) # Ensure fitness is a float
55+
return value if self.maximize else -value # If minimizing, invert the fitness
56+
57+
def select_parents(
58+
self, population_score: list[tuple[np.ndarray, float]]
59+
) -> list[np.ndarray]:
60+
"""Select top N_SELECTED parents based on fitness."""
61+
population_score.sort(key=lambda score_tuple: score_tuple[1], reverse=True)
62+
selected_count = min(N_SELECTED, len(population_score))
63+
return [ind for ind, _ in population_score[:selected_count]]
64+
65+
def crossover(
66+
self, parent1: np.ndarray, parent2: np.ndarray
67+
) -> tuple[np.ndarray, np.ndarray]:
68+
"""
69+
Perform uniform crossover between two parents to generate offspring.
70+
Args:
71+
parent1 (np.ndarray): The first parent.
72+
parent2 (np.ndarray): The second parent.
73+
Returns:
74+
tuple[np.ndarray, np.ndarray]: The two offspring generated by crossover.
75+
Example:
76+
>>> ga = GeneticAlgorithm(
77+
... lambda x, y: -(x**2 + y**2),
78+
... [(-10, 10), (-10, 10)],
79+
... 10, 100, 0.1, 0.8, True
80+
... )
81+
>>> parent1, parent2 = np.array([1, 2]), np.array([3, 4])
82+
>>> len(ga.crossover(parent1, parent2)) == 2
83+
True
84+
"""
85+
if random.random() < self.crossover_rate:
86+
cross_point = random.randint(1, self.dim - 1)
87+
child1 = np.concatenate((parent1[:cross_point], parent2[cross_point:]))
88+
child2 = np.concatenate((parent2[:cross_point], parent1[cross_point:]))
89+
return child1, child2
90+
return parent1, parent2
91+
92+
def mutate(self, individual: np.ndarray) -> np.ndarray:
93+
"""
94+
Apply mutation to an individual.
95+
Args:
96+
individual (np.ndarray): The individual to mutate.
97+
Returns:
98+
np.ndarray: The mutated individual.
99+
Example:
100+
>>> ga = GeneticAlgorithm(
101+
... lambda x, y: -(x**2 + y**2),
102+
... [(-10, 10), (-10, 10)],
103+
... 10, 100, 0.1, 0.8, True
104+
... )
105+
>>> ind = np.array([1.0, 2.0])
106+
>>> mutated = ga.mutate(ind)
107+
>>> len(mutated) == 2 # Ensure it still has the correct number of dimensions
108+
True
109+
"""
110+
for i in range(self.dim):
111+
if random.random() < self.mutation_prob:
112+
individual[i] = rng.uniform(self.bounds[i][0], self.bounds[i][1])
113+
return individual
114+
115+
def evaluate_population(self) -> list[tuple[np.ndarray, float]]:
116+
"""
117+
Evaluate the fitness of the entire population in parallel.
118+
Returns:
119+
list[tuple[np.ndarray, float]]:
120+
The population with their respective fitness values.
121+
Example:
122+
>>> ga = GeneticAlgorithm(
123+
... lambda x, y: -(x**2 + y**2),
124+
... [(-10, 10), (-10, 10)],
125+
... 10, 100, 0.1, 0.8, True
126+
... )
127+
>>> eval_population = ga.evaluate_population()
128+
>>> len(eval_population) == ga.population_size # Ensure population size
129+
True
130+
>>> all(
131+
... isinstance(ind, tuple) and isinstance(ind[1], float)
132+
... for ind in eval_population
133+
... )
134+
True
135+
"""
136+
with ThreadPoolExecutor() as executor:
137+
return list(
138+
executor.map(
139+
lambda individual: (individual, self.fitness(individual)),
140+
self.population,
141+
)
142+
)
143+
144+
def evolve(self, verbose=True) -> np.ndarray:
145+
"""
146+
Evolve the population over the generations to find the best solution.
147+
Returns:
148+
np.ndarray: The best individual found during the evolution process.
149+
"""
150+
for generation in range(self.generations):
151+
# Evaluate population fitness (multithreaded)
152+
population_score = self.evaluate_population()
153+
154+
# Check the best individual
155+
best_individual = max(
156+
population_score, key=lambda score_tuple: score_tuple[1]
157+
)[0]
158+
best_fitness = self.fitness(best_individual)
159+
160+
# Select parents for next generation
161+
parents = self.select_parents(population_score)
162+
next_generation = []
163+
164+
# Generate offspring using crossover and mutation
165+
for i in range(0, len(parents), 2):
166+
parent1, parent2 = parents[i], parents[(i + 1) % len(parents)]
167+
child1, child2 = self.crossover(parent1, parent2)
168+
next_generation.append(self.mutate(child1))
169+
next_generation.append(self.mutate(child2))
170+
171+
# Ensure population size remains the same
172+
self.population = next_generation[: self.population_size]
173+
174+
if verbose and generation % 10 == 0:
175+
print(f"Generation {generation}: Best Fitness = {best_fitness}")
176+
177+
return best_individual
178+
179+
180+
# Example target function for optimization
181+
def target_function(var_x: float, var_y: float) -> float:
182+
"""
183+
Example target function (parabola) for optimization.
184+
Args:
185+
var_x (float): The x-coordinate.
186+
var_y (float): The y-coordinate.
187+
Returns:
188+
float: The value of the function at (var_x, var_y).
189+
Example:
190+
>>> target_function(0, 0)
191+
0
192+
>>> target_function(1, 1)
193+
2
194+
"""
195+
return var_x**2 + var_y**2 # Simple parabolic surface (minimization)
196+
197+
198+
# Set bounds for the variables (var_x, var_y)
199+
bounds = [(-10, 10), (-10, 10)] # Both var_x and var_y range from -10 to 10
200+
201+
# Instantiate and run the genetic algorithm
202+
ga = GeneticAlgorithm(
203+
function=target_function,
204+
bounds=bounds,
205+
population_size=N_POPULATION,
206+
generations=N_GENERATIONS,
207+
mutation_prob=MUTATION_PROBABILITY,
208+
crossover_rate=CROSSOVER_RATE,
209+
maximize=False, # Minimize the function
210+
)
211+
212+
best_solution = ga.evolve()
213+
print(f"Best solution found: {best_solution}")
214+
print(f"Best fitness (minimum value of function): {target_function(*best_solution)}")

0 commit comments

Comments
 (0)