1use std::marker::PhantomData;
25
26use burn::tensor::{Tensor, TensorData, backend::Backend};
27use rand::Rng;
28use rand::RngExt;
29
30use crate::ops::{
31 crossover::{blx_alpha, uniform_crossover},
32 mutation::gaussian_mutation,
33 replacement::{elitist, generational},
34 selection::tournament_select,
35};
36use crate::rng::{SeedPurpose, seed_stream};
37use crate::strategy::{Strategy, StrategyMetrics};
38
39#[derive(Debug, Clone, Copy)]
41pub enum GaSelection {
42 Tournament { size: usize },
44}
45
46#[derive(Debug, Clone, Copy)]
48pub enum GaCrossover {
49 BlxAlpha { alpha: f32 },
51 Uniform { p: f32 },
53}
54
55#[derive(Debug, Clone, Copy)]
57pub enum GaReplacement {
58 Generational,
60 Elitist { elitism_k: usize },
62}
63
64#[derive(Debug, Clone)]
66pub struct GaConfig {
67 pub pop_size: usize,
69 pub genome_dim: usize,
71 pub bounds: (f32, f32),
73 pub mutation_sigma: f32,
75 pub selection: GaSelection,
77 pub crossover: GaCrossover,
79 pub replacement: GaReplacement,
81}
82
83impl GaConfig {
84 #[must_use]
86 pub fn default_for(pop_size: usize, genome_dim: usize) -> Self {
87 Self {
88 pop_size,
89 genome_dim,
90 bounds: (-5.12, 5.12),
91 mutation_sigma: 0.3,
92 selection: GaSelection::Tournament { size: 2 },
93 crossover: GaCrossover::BlxAlpha { alpha: 0.5 },
94 replacement: GaReplacement::Elitist { elitism_k: 1 },
95 }
96 }
97}
98
99#[derive(Debug, Clone)]
101pub struct GaState<B: Backend> {
102 pub population: Tensor<B, 2>,
104 pub fitness: Vec<f32>,
107 pub best_genome: Option<Tensor<B, 2>>,
109 pub best_fitness: f32,
111 pub generation: usize,
113}
114
115#[derive(Debug, Clone, Copy, Default)]
138pub struct GeneticAlgorithm<B: Backend> {
139 _backend: PhantomData<fn() -> B>,
140}
141
142impl<B: Backend> GeneticAlgorithm<B> {
143 #[must_use]
145 pub fn new() -> Self {
146 Self {
147 _backend: PhantomData,
148 }
149 }
150
151 fn sample_initial_population(
152 params: &GaConfig,
153 rng: &mut dyn Rng,
154 device: &<B as burn::tensor::backend::BackendTypes>::Device,
155 ) -> Tensor<B, 2> {
156 let (lo, hi) = params.bounds;
157 let pop = params.pop_size;
162 let genome_dim = params.genome_dim;
163 let mut stream = seed_stream(rng.next_u64(), 0, SeedPurpose::Init);
164 let mut rows = Vec::with_capacity(pop * genome_dim);
165 for _ in 0..pop * genome_dim {
166 rows.push(lo + (hi - lo) * stream.random::<f32>());
167 }
168 Tensor::<B, 2>::from_data(TensorData::new(rows, [pop, genome_dim]), device)
169 }
170}
171
172impl<B: Backend> Strategy<B> for GeneticAlgorithm<B>
173where
174 B::Device: Clone,
175{
176 type Params = GaConfig;
177 type State = GaState<B>;
178 type Genome = Tensor<B, 2>;
179
180 fn init(&self, params: &GaConfig, rng: &mut dyn Rng, device: &<B as burn::tensor::backend::BackendTypes>::Device) -> GaState<B> {
187 let population = Self::sample_initial_population(params, rng, device);
188 GaState {
189 population,
190 fitness: Vec::new(),
191 best_genome: None,
192 best_fitness: f32::INFINITY,
193 generation: 0,
194 }
195 }
196
197 fn ask(
215 &self,
216 params: &GaConfig,
217 state: &GaState<B>,
218 rng: &mut dyn Rng,
219 device: &<B as burn::tensor::backend::BackendTypes>::Device,
220 ) -> (Tensor<B, 2>, GaState<B>) {
221 if state.fitness.is_empty() {
224 return (state.population.clone(), state.clone());
225 }
226
227 let GaConfig {
228 pop_size,
229 mutation_sigma,
230 selection,
231 crossover,
232 ..
233 } = params;
234
235 let mut crossover_rng = seed_stream(
236 rng.next_u64(),
237 state.generation as u64,
238 SeedPurpose::Crossover,
239 );
240 let mut mutation_rng = seed_stream(
241 rng.next_u64(),
242 state.generation as u64,
243 SeedPurpose::Mutation,
244 );
245 let mut selection_rng = seed_stream(
246 rng.next_u64(),
247 state.generation as u64,
248 SeedPurpose::Selection,
249 );
250
251 let parents_a = match selection {
253 GaSelection::Tournament { size } => tournament_select(
254 &state.population,
255 &state.fitness,
256 *size,
257 *pop_size,
258 &mut selection_rng,
259 device,
260 ),
261 };
262 let parents_b = match selection {
263 GaSelection::Tournament { size } => tournament_select(
264 &state.population,
265 &state.fitness,
266 *size,
267 *pop_size,
268 &mut selection_rng,
269 device,
270 ),
271 };
272
273 let offspring = match crossover {
275 GaCrossover::BlxAlpha { alpha } => {
276 blx_alpha(parents_a, parents_b, *alpha, &mut crossover_rng, device)
277 }
278 GaCrossover::Uniform { p } => {
279 uniform_crossover(parents_a, parents_b, *p, &mut crossover_rng, device)
280 }
281 };
282
283 let offspring = gaussian_mutation(offspring, *mutation_sigma, &mut mutation_rng, device);
285
286 let (lo, hi) = params.bounds;
288 let offspring = offspring.clamp(lo, hi);
289
290 (offspring, state.clone())
291 }
292
293 fn tell(
309 &self,
310 params: &GaConfig,
311 population: Tensor<B, 2>,
312 fitness: Tensor<B, 1>,
313 mut state: GaState<B>,
314 _rng: &mut dyn Rng,
315 ) -> (GaState<B>, StrategyMetrics) {
316 let fitness_host = fitness.into_data().into_vec::<f32>().unwrap_or_default();
317
318 if state.fitness.is_empty() {
320 state.fitness.clone_from(&fitness_host);
321 state.generation += 1;
322 update_best(&mut state, &population, &fitness_host);
323 let m = StrategyMetrics::from_host_fitness(
324 state.generation,
325 &fitness_host,
326 state.best_fitness,
327 );
328 state.best_fitness = m.best_fitness_ever;
329 return (state, m);
330 }
331
332 let device = state.population.device();
333 let (next_pop, next_fitness) = match params.replacement {
334 GaReplacement::Generational => generational::<B>(
335 state.population.clone(),
336 &state.fitness,
337 population.clone(),
338 fitness_host.clone(),
339 ),
340 GaReplacement::Elitist { elitism_k } => elitist::<B>(
341 state.population.clone(),
342 &state.fitness,
343 population.clone(),
344 &fitness_host,
345 elitism_k,
346 &device,
347 ),
348 };
349
350 update_best(&mut state, &next_pop, &next_fitness);
351 state.population = next_pop;
352 state.fitness.clone_from(&next_fitness);
353 state.generation += 1;
354 let m =
355 StrategyMetrics::from_host_fitness(state.generation, &next_fitness, state.best_fitness);
356 state.best_fitness = m.best_fitness_ever;
357 (state, m)
358 }
359
360 fn best(&self, state: &GaState<B>) -> Option<(Tensor<B, 2>, f32)> {
365 state
366 .best_genome
367 .as_ref()
368 .map(|g| (g.clone(), state.best_fitness))
369 }
370}
371
372fn update_best<B: Backend>(state: &mut GaState<B>, pop: &Tensor<B, 2>, fitness: &[f32]) {
373 if fitness.is_empty() {
374 return;
375 }
376 let mut best_idx = 0_usize;
377 let mut best_f = fitness[0];
378 for (i, &f) in fitness.iter().enumerate().skip(1) {
379 if f < best_f {
380 best_f = f;
381 best_idx = i;
382 }
383 }
384 if best_f < state.best_fitness {
385 let device = pop.device();
386 #[allow(clippy::cast_possible_wrap)]
387 let idx = Tensor::<B, 1, burn::tensor::Int>::from_data(
388 TensorData::new(vec![best_idx as i64], [1]),
389 &device,
390 );
391 state.best_genome = Some(pop.clone().select(0, idx));
392 state.best_fitness = best_f;
393 }
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399 use crate::fitness::FromFitnessEvaluable;
400 use crate::strategy::EvolutionaryHarness;
401 use burn::backend::Flex;
402 use rlevo_core::fitness::FitnessEvaluable;
403
404 type TestBackend = Flex;
405
406 struct Sphere;
407 struct SphereFit;
408 impl FitnessEvaluable for SphereFit {
409 type Individual = Vec<f64>;
410 type Landscape = Sphere;
411 fn evaluate(&self, x: &Self::Individual, _: &Self::Landscape) -> f64 {
412 x.iter().map(|v| v * v).sum()
413 }
414 }
415
416 #[test]
417 fn ga_converges_on_sphere_d2() {
418 let device = Default::default();
419 let strategy = GeneticAlgorithm::<TestBackend>::new();
420 let params = GaConfig {
421 pop_size: 64,
422 genome_dim: 2,
423 bounds: (-5.0, 5.0),
424 mutation_sigma: 0.2,
425 selection: GaSelection::Tournament { size: 2 },
426 crossover: GaCrossover::BlxAlpha { alpha: 0.5 },
427 replacement: GaReplacement::Elitist { elitism_k: 1 },
428 };
429 let fitness_fn = FromFitnessEvaluable::new(SphereFit, Sphere);
430
431 let mut harness = EvolutionaryHarness::<TestBackend, _, _>::new(
432 strategy, params, fitness_fn, 42, device, 200,
433 );
434 harness.reset();
435 loop {
436 let step = harness.step(());
437 if step.done {
438 break;
439 }
440 }
441 let m = harness.latest_metrics().unwrap();
442 assert!(
443 m.best_fitness_ever < 1e-2,
444 "expected Sphere-D2 convergence, got best_fitness_ever={}",
445 m.best_fitness_ever
446 );
447 }
448}