1use std::marker::PhantomData;
25
26use burn::tensor::{Tensor, TensorData, backend::Backend};
27use rand::Rng;
28
29use crate::ops::{
30 crossover::{blx_alpha, uniform_crossover},
31 mutation::gaussian_mutation,
32 replacement::{elitist, generational},
33 selection::tournament_select,
34};
35use crate::rng::{SeedPurpose, seed_stream};
36use crate::strategy::{Strategy, StrategyMetrics};
37
38#[derive(Debug, Clone, Copy)]
40pub enum GaSelection {
41 Tournament { size: usize },
43}
44
45#[derive(Debug, Clone, Copy)]
47pub enum GaCrossover {
48 BlxAlpha { alpha: f32 },
50 Uniform { p: f32 },
52}
53
54#[derive(Debug, Clone, Copy)]
56pub enum GaReplacement {
57 Generational,
59 Elitist { elitism_k: usize },
61}
62
63#[derive(Debug, Clone)]
65pub struct GaConfig {
66 pub pop_size: usize,
68 pub genome_dim: usize,
70 pub bounds: (f32, f32),
72 pub mutation_sigma: f32,
74 pub selection: GaSelection,
76 pub crossover: GaCrossover,
78 pub replacement: GaReplacement,
80}
81
82impl GaConfig {
83 #[must_use]
85 pub fn default_for(pop_size: usize, genome_dim: usize) -> Self {
86 Self {
87 pop_size,
88 genome_dim,
89 bounds: (-5.12, 5.12),
90 mutation_sigma: 0.3,
91 selection: GaSelection::Tournament { size: 2 },
92 crossover: GaCrossover::BlxAlpha { alpha: 0.5 },
93 replacement: GaReplacement::Elitist { elitism_k: 1 },
94 }
95 }
96}
97
98#[derive(Debug, Clone)]
100pub struct GaState<B: Backend> {
101 pub population: Tensor<B, 2>,
103 pub fitness: Vec<f32>,
106 pub best_genome: Option<Tensor<B, 2>>,
108 pub best_fitness: f32,
110 pub generation: usize,
112}
113
114#[derive(Debug, Clone, Copy, Default)]
137pub struct GeneticAlgorithm<B: Backend> {
138 _backend: PhantomData<fn() -> B>,
139}
140
141impl<B: Backend> GeneticAlgorithm<B> {
142 #[must_use]
144 pub fn new() -> Self {
145 Self {
146 _backend: PhantomData,
147 }
148 }
149
150 fn sample_initial_population(
151 params: &GaConfig,
152 rng: &mut dyn Rng,
153 device: &B::Device,
154 ) -> Tensor<B, 2> {
155 let (lo, hi) = params.bounds;
156 let range = f64::from(hi - lo);
157 let lo_f = f64::from(lo);
158 B::seed(device, rng.next_u64());
159
160 Tensor::<B, 2>::random(
161 [params.pop_size, params.genome_dim],
162 burn::tensor::Distribution::Uniform(lo_f, lo_f + range),
163 device,
164 )
165 }
166}
167
168impl<B: Backend> Strategy<B> for GeneticAlgorithm<B>
169where
170 B::Device: Clone,
171{
172 type Params = GaConfig;
173 type State = GaState<B>;
174 type Genome = Tensor<B, 2>;
175
176 fn init(&self, params: &GaConfig, rng: &mut dyn Rng, device: &B::Device) -> GaState<B> {
177 let population = Self::sample_initial_population(params, rng, device);
178 GaState {
179 population,
180 fitness: Vec::new(),
181 best_genome: None,
182 best_fitness: f32::INFINITY,
183 generation: 0,
184 }
185 }
186
187 fn ask(
188 &self,
189 params: &GaConfig,
190 state: &GaState<B>,
191 rng: &mut dyn Rng,
192 device: &B::Device,
193 ) -> (Tensor<B, 2>, GaState<B>) {
194 if state.fitness.is_empty() {
197 return (state.population.clone(), state.clone());
198 }
199
200 let GaConfig {
201 pop_size,
202 mutation_sigma,
203 selection,
204 crossover,
205 ..
206 } = params;
207
208 let mut crossover_rng = seed_stream(
209 rng.next_u64(),
210 state.generation as u64,
211 SeedPurpose::Crossover,
212 );
213 let mut mutation_rng = seed_stream(
214 rng.next_u64(),
215 state.generation as u64,
216 SeedPurpose::Mutation,
217 );
218 let mut selection_rng = seed_stream(
219 rng.next_u64(),
220 state.generation as u64,
221 SeedPurpose::Selection,
222 );
223
224 let parents_a = match selection {
226 GaSelection::Tournament { size } => tournament_select(
227 &state.population,
228 &state.fitness,
229 *size,
230 *pop_size,
231 &mut selection_rng,
232 device,
233 ),
234 };
235 let parents_b = match selection {
236 GaSelection::Tournament { size } => tournament_select(
237 &state.population,
238 &state.fitness,
239 *size,
240 *pop_size,
241 &mut selection_rng,
242 device,
243 ),
244 };
245
246 B::seed(device, crossover_rng.next_u64());
248 let offspring = match crossover {
249 GaCrossover::BlxAlpha { alpha } => blx_alpha(parents_a, parents_b, *alpha, device),
250 GaCrossover::Uniform { p } => uniform_crossover(parents_a, parents_b, *p, device),
251 };
252
253 B::seed(device, mutation_rng.next_u64());
255 let offspring = gaussian_mutation(offspring, *mutation_sigma, device);
256
257 let (lo, hi) = params.bounds;
259 let offspring = offspring.clamp(lo, hi);
260
261 (offspring, state.clone())
262 }
263
264 fn tell(
265 &self,
266 params: &GaConfig,
267 population: Tensor<B, 2>,
268 fitness: Tensor<B, 1>,
269 mut state: GaState<B>,
270 _rng: &mut dyn Rng,
271 ) -> (GaState<B>, StrategyMetrics) {
272 let fitness_host = fitness.into_data().into_vec::<f32>().unwrap_or_default();
273
274 if state.fitness.is_empty() {
276 state.fitness.clone_from(&fitness_host);
277 state.generation += 1;
278 update_best(&mut state, &population, &fitness_host);
279 let m = StrategyMetrics::from_host_fitness(
280 state.generation,
281 &fitness_host,
282 state.best_fitness,
283 );
284 state.best_fitness = m.best_fitness_ever;
285 return (state, m);
286 }
287
288 let device = state.population.device();
289 let (next_pop, next_fitness) = match params.replacement {
290 GaReplacement::Generational => generational::<B>(
291 state.population.clone(),
292 &state.fitness,
293 population.clone(),
294 fitness_host.clone(),
295 ),
296 GaReplacement::Elitist { elitism_k } => elitist::<B>(
297 state.population.clone(),
298 &state.fitness,
299 population.clone(),
300 &fitness_host,
301 elitism_k,
302 &device,
303 ),
304 };
305
306 update_best(&mut state, &next_pop, &next_fitness);
307 state.population = next_pop;
308 state.fitness.clone_from(&next_fitness);
309 state.generation += 1;
310 let m =
311 StrategyMetrics::from_host_fitness(state.generation, &next_fitness, state.best_fitness);
312 state.best_fitness = m.best_fitness_ever;
313 (state, m)
314 }
315
316 fn best(&self, state: &GaState<B>) -> Option<(Tensor<B, 2>, f32)> {
317 state
318 .best_genome
319 .as_ref()
320 .map(|g| (g.clone(), state.best_fitness))
321 }
322}
323
324fn update_best<B: Backend>(state: &mut GaState<B>, pop: &Tensor<B, 2>, fitness: &[f32]) {
325 if fitness.is_empty() {
326 return;
327 }
328 let mut best_idx = 0_usize;
329 let mut best_f = fitness[0];
330 for (i, &f) in fitness.iter().enumerate().skip(1) {
331 if f < best_f {
332 best_f = f;
333 best_idx = i;
334 }
335 }
336 if best_f < state.best_fitness {
337 let device = pop.device();
338 #[allow(clippy::cast_possible_wrap)]
339 let idx = Tensor::<B, 1, burn::tensor::Int>::from_data(
340 TensorData::new(vec![best_idx as i64], [1]),
341 &device,
342 );
343 state.best_genome = Some(pop.clone().select(0, idx));
344 state.best_fitness = best_f;
345 }
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351 use crate::fitness::FromFitnessEvaluable;
352 use crate::strategy::EvolutionaryHarness;
353 use burn::backend::NdArray;
354 use rlevo_core::fitness::FitnessEvaluable;
355
356 type TestBackend = NdArray;
357
358 struct Sphere;
359 struct SphereFit;
360 impl FitnessEvaluable for SphereFit {
361 type Individual = Vec<f64>;
362 type Landscape = Sphere;
363 fn evaluate(&self, x: &Self::Individual, _: &Self::Landscape) -> f64 {
364 x.iter().map(|v| v * v).sum()
365 }
366 }
367
368 #[test]
369 fn ga_converges_on_sphere_d2() {
370 let device = Default::default();
371 let strategy = GeneticAlgorithm::<TestBackend>::new();
372 let params = GaConfig {
373 pop_size: 64,
374 genome_dim: 2,
375 bounds: (-5.0, 5.0),
376 mutation_sigma: 0.2,
377 selection: GaSelection::Tournament { size: 2 },
378 crossover: GaCrossover::BlxAlpha { alpha: 0.5 },
379 replacement: GaReplacement::Elitist { elitism_k: 1 },
380 };
381 let fitness_fn = FromFitnessEvaluable::new(SphereFit, Sphere);
382
383 let mut harness = EvolutionaryHarness::<TestBackend, _, _>::new(
384 strategy, params, fitness_fn, 42, device, 200,
385 );
386 harness.reset();
387 loop {
388 let step = harness.step(());
389 if step.done {
390 break;
391 }
392 }
393 let m = harness.latest_metrics().unwrap();
394 assert!(
395 m.best_fitness_ever < 1e-2,
396 "expected Sphere-D2 convergence, got best_fitness_ever={}",
397 m.best_fitness_ever
398 );
399 }
400}