use std::marker::PhantomData;
use burn::tensor::{Tensor, TensorData, backend::Backend};
use rand::Rng;
use rand::RngExt;
use crate::ops::{
crossover::{blx_alpha, uniform_crossover},
mutation::gaussian_mutation,
replacement::{elitist, generational},
selection::tournament_select,
};
use crate::rng::{SeedPurpose, seed_stream};
use crate::strategy::{Strategy, StrategyMetrics};
#[derive(Debug, Clone, Copy)]
pub enum GaSelection {
Tournament { size: usize },
}
#[derive(Debug, Clone, Copy)]
pub enum GaCrossover {
BlxAlpha { alpha: f32 },
Uniform { p: f32 },
}
#[derive(Debug, Clone, Copy)]
pub enum GaReplacement {
Generational,
Elitist { elitism_k: usize },
}
#[derive(Debug, Clone)]
pub struct GaConfig {
pub pop_size: usize,
pub genome_dim: usize,
pub bounds: (f32, f32),
pub mutation_sigma: f32,
pub selection: GaSelection,
pub crossover: GaCrossover,
pub replacement: GaReplacement,
}
impl GaConfig {
#[must_use]
pub fn default_for(pop_size: usize, genome_dim: usize) -> Self {
Self {
pop_size,
genome_dim,
bounds: (-5.12, 5.12),
mutation_sigma: 0.3,
selection: GaSelection::Tournament { size: 2 },
crossover: GaCrossover::BlxAlpha { alpha: 0.5 },
replacement: GaReplacement::Elitist { elitism_k: 1 },
}
}
}
#[derive(Debug, Clone)]
pub struct GaState<B: Backend> {
pub population: Tensor<B, 2>,
pub fitness: Vec<f32>,
pub best_genome: Option<Tensor<B, 2>>,
pub best_fitness: f32,
pub generation: usize,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct GeneticAlgorithm<B: Backend> {
_backend: PhantomData<fn() -> B>,
}
impl<B: Backend> GeneticAlgorithm<B> {
#[must_use]
pub fn new() -> Self {
Self {
_backend: PhantomData,
}
}
fn sample_initial_population(
params: &GaConfig,
rng: &mut dyn Rng,
device: &<B as burn::tensor::backend::BackendTypes>::Device,
) -> Tensor<B, 2> {
let (lo, hi) = params.bounds;
let pop = params.pop_size;
let genome_dim = params.genome_dim;
let mut stream = seed_stream(rng.next_u64(), 0, SeedPurpose::Init);
let mut rows = Vec::with_capacity(pop * genome_dim);
for _ in 0..pop * genome_dim {
rows.push(lo + (hi - lo) * stream.random::<f32>());
}
Tensor::<B, 2>::from_data(TensorData::new(rows, [pop, genome_dim]), device)
}
}
impl<B: Backend> Strategy<B> for GeneticAlgorithm<B>
where
B::Device: Clone,
{
type Params = GaConfig;
type State = GaState<B>;
type Genome = Tensor<B, 2>;
fn init(&self, params: &GaConfig, rng: &mut dyn Rng, device: &<B as burn::tensor::backend::BackendTypes>::Device) -> GaState<B> {
let population = Self::sample_initial_population(params, rng, device);
GaState {
population,
fitness: Vec::new(),
best_genome: None,
best_fitness: f32::INFINITY,
generation: 0,
}
}
fn ask(
&self,
params: &GaConfig,
state: &GaState<B>,
rng: &mut dyn Rng,
device: &<B as burn::tensor::backend::BackendTypes>::Device,
) -> (Tensor<B, 2>, GaState<B>) {
if state.fitness.is_empty() {
return (state.population.clone(), state.clone());
}
let GaConfig {
pop_size,
mutation_sigma,
selection,
crossover,
..
} = params;
let mut crossover_rng = seed_stream(
rng.next_u64(),
state.generation as u64,
SeedPurpose::Crossover,
);
let mut mutation_rng = seed_stream(
rng.next_u64(),
state.generation as u64,
SeedPurpose::Mutation,
);
let mut selection_rng = seed_stream(
rng.next_u64(),
state.generation as u64,
SeedPurpose::Selection,
);
let parents_a = match selection {
GaSelection::Tournament { size } => tournament_select(
&state.population,
&state.fitness,
*size,
*pop_size,
&mut selection_rng,
device,
),
};
let parents_b = match selection {
GaSelection::Tournament { size } => tournament_select(
&state.population,
&state.fitness,
*size,
*pop_size,
&mut selection_rng,
device,
),
};
let offspring = match crossover {
GaCrossover::BlxAlpha { alpha } => {
blx_alpha(parents_a, parents_b, *alpha, &mut crossover_rng, device)
}
GaCrossover::Uniform { p } => {
uniform_crossover(parents_a, parents_b, *p, &mut crossover_rng, device)
}
};
let offspring = gaussian_mutation(offspring, *mutation_sigma, &mut mutation_rng, device);
let (lo, hi) = params.bounds;
let offspring = offspring.clamp(lo, hi);
(offspring, state.clone())
}
fn tell(
&self,
params: &GaConfig,
population: Tensor<B, 2>,
fitness: Tensor<B, 1>,
mut state: GaState<B>,
_rng: &mut dyn Rng,
) -> (GaState<B>, StrategyMetrics) {
let fitness_host = fitness.into_data().into_vec::<f32>().unwrap_or_default();
if state.fitness.is_empty() {
state.fitness.clone_from(&fitness_host);
state.generation += 1;
update_best(&mut state, &population, &fitness_host);
let m = StrategyMetrics::from_host_fitness(
state.generation,
&fitness_host,
state.best_fitness,
);
state.best_fitness = m.best_fitness_ever;
return (state, m);
}
let device = state.population.device();
let (next_pop, next_fitness) = match params.replacement {
GaReplacement::Generational => generational::<B>(
state.population.clone(),
&state.fitness,
population.clone(),
fitness_host.clone(),
),
GaReplacement::Elitist { elitism_k } => elitist::<B>(
state.population.clone(),
&state.fitness,
population.clone(),
&fitness_host,
elitism_k,
&device,
),
};
update_best(&mut state, &next_pop, &next_fitness);
state.population = next_pop;
state.fitness.clone_from(&next_fitness);
state.generation += 1;
let m =
StrategyMetrics::from_host_fitness(state.generation, &next_fitness, state.best_fitness);
state.best_fitness = m.best_fitness_ever;
(state, m)
}
fn best(&self, state: &GaState<B>) -> Option<(Tensor<B, 2>, f32)> {
state
.best_genome
.as_ref()
.map(|g| (g.clone(), state.best_fitness))
}
}
fn update_best<B: Backend>(state: &mut GaState<B>, pop: &Tensor<B, 2>, fitness: &[f32]) {
if fitness.is_empty() {
return;
}
let mut best_idx = 0_usize;
let mut best_f = fitness[0];
for (i, &f) in fitness.iter().enumerate().skip(1) {
if f < best_f {
best_f = f;
best_idx = i;
}
}
if best_f < state.best_fitness {
let device = pop.device();
#[allow(clippy::cast_possible_wrap)]
let idx = Tensor::<B, 1, burn::tensor::Int>::from_data(
TensorData::new(vec![best_idx as i64], [1]),
&device,
);
state.best_genome = Some(pop.clone().select(0, idx));
state.best_fitness = best_f;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fitness::FromFitnessEvaluable;
use crate::strategy::EvolutionaryHarness;
use burn::backend::Flex;
use rlevo_core::fitness::FitnessEvaluable;
type TestBackend = Flex;
struct Sphere;
struct SphereFit;
impl FitnessEvaluable for SphereFit {
type Individual = Vec<f64>;
type Landscape = Sphere;
fn evaluate(&self, x: &Self::Individual, _: &Self::Landscape) -> f64 {
x.iter().map(|v| v * v).sum()
}
}
#[test]
fn ga_converges_on_sphere_d2() {
let device = Default::default();
let strategy = GeneticAlgorithm::<TestBackend>::new();
let params = GaConfig {
pop_size: 64,
genome_dim: 2,
bounds: (-5.0, 5.0),
mutation_sigma: 0.2,
selection: GaSelection::Tournament { size: 2 },
crossover: GaCrossover::BlxAlpha { alpha: 0.5 },
replacement: GaReplacement::Elitist { elitism_k: 1 },
};
let fitness_fn = FromFitnessEvaluable::new(SphereFit, Sphere);
let mut harness = EvolutionaryHarness::<TestBackend, _, _>::new(
strategy, params, fitness_fn, 42, device, 200,
);
harness.reset();
loop {
let step = harness.step(());
if step.done {
break;
}
}
let m = harness.latest_metrics().unwrap();
assert!(
m.best_fitness_ever < 1e-2,
"expected Sphere-D2 convergence, got best_fitness_ever={}",
m.best_fitness_ever
);
}
}