use std::marker::PhantomData;
use burn::tensor::{Int, Tensor, TensorData, backend::Backend};
use rand::Rng;
use crate::ops::mutation::gaussian_mutation_per_row;
use crate::rng::{SeedPurpose, seed_stream};
use crate::strategy::{Strategy, StrategyMetrics};
#[derive(Debug, Clone)]
pub struct EpConfig {
pub mu: usize,
pub genome_dim: usize,
pub bounds: (f32, f32),
pub initial_sigma: f32,
pub tau: f32,
pub tournament_q: usize,
}
impl EpConfig {
#[must_use]
pub fn default_for(mu: usize, genome_dim: usize) -> Self {
#[allow(clippy::cast_precision_loss)]
let d = genome_dim as f32;
let tau = 1.0 / (2.0 * d.sqrt()).sqrt();
Self {
mu,
genome_dim,
bounds: (-5.12, 5.12),
initial_sigma: 1.0,
tau,
tournament_q: 10,
}
}
}
#[derive(Debug, Clone)]
pub struct EpState<B: Backend> {
pub parents: Tensor<B, 2>,
pub sigmas: Tensor<B, 1>,
pub parent_fitness: Vec<f32>,
pub best_genome: Option<Tensor<B, 2>>,
pub best_fitness: f32,
pub generation: usize,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct EvolutionaryProgramming<B: Backend> {
_backend: PhantomData<fn() -> B>,
}
impl<B: Backend> EvolutionaryProgramming<B> {
#[must_use]
pub fn new() -> Self {
Self {
_backend: PhantomData,
}
}
}
impl<B: Backend> Strategy<B> for EvolutionaryProgramming<B>
where
B::Device: Clone,
{
type Params = EpConfig;
type State = EpState<B>;
type Genome = Tensor<B, 2>;
fn init(&self, params: &EpConfig, rng: &mut dyn Rng, device: &B::Device) -> EpState<B> {
let (lo, hi) = params.bounds;
B::seed(device, rng.next_u64());
let parents = Tensor::<B, 2>::random(
[params.mu, params.genome_dim],
burn::tensor::Distribution::Uniform(f64::from(lo), f64::from(hi)),
device,
);
let sigmas = Tensor::<B, 1>::from_data(
TensorData::new(vec![params.initial_sigma; params.mu], [params.mu]),
device,
);
EpState {
parents,
sigmas,
parent_fitness: Vec::new(),
best_genome: None,
best_fitness: f32::INFINITY,
generation: 0,
}
}
fn ask(
&self,
params: &EpConfig,
state: &EpState<B>,
rng: &mut dyn Rng,
device: &B::Device,
) -> (Tensor<B, 2>, EpState<B>) {
if state.parent_fitness.is_empty() {
return (state.parents.clone(), state.clone());
}
let mu = params.mu;
let mut sigma_rng =
seed_stream(rng.next_u64(), state.generation as u64, SeedPurpose::Other);
let mut mutation_rng = seed_stream(
rng.next_u64(),
state.generation as u64,
SeedPurpose::Mutation,
);
B::seed(device, sigma_rng.next_u64());
let noise =
Tensor::<B, 1>::random([mu], burn::tensor::Distribution::Normal(0.0, 1.0), device);
let offspring_sigmas = state.sigmas.clone() * noise.mul_scalar(params.tau).exp();
B::seed(device, mutation_rng.next_u64());
let offspring =
gaussian_mutation_per_row(state.parents.clone(), offspring_sigmas.clone(), device);
let (lo, hi) = params.bounds;
let offspring = offspring.clamp(lo, hi);
let mut state = state.clone();
state.sigmas = Tensor::cat(vec![state.sigmas.clone(), offspring_sigmas], 0);
(offspring, state)
}
fn tell(
&self,
params: &EpConfig,
offspring: Tensor<B, 2>,
fitness: Tensor<B, 1>,
mut state: EpState<B>,
rng: &mut dyn Rng,
) -> (EpState<B>, StrategyMetrics) {
let fitness_host = fitness.into_data().into_vec::<f32>().unwrap_or_default();
let device = offspring.device();
if state.parent_fitness.is_empty() {
state.parent_fitness.clone_from(&fitness_host);
state.generation += 1;
update_best(&mut state, &offspring, &fitness_host);
let m = StrategyMetrics::from_host_fitness(
state.generation,
&fitness_host,
state.best_fitness,
);
state.best_fitness = m.best_fitness_ever;
state.parents = offspring;
state.sigmas = Tensor::<B, 1>::from_data(
TensorData::new(vec![params.initial_sigma; params.mu], [params.mu]),
&device,
);
return (state, m);
}
let mu = params.mu;
let combined_pop = Tensor::cat(vec![state.parents.clone(), offspring.clone()], 0);
let combined_fit: Vec<f32> = state
.parent_fitness
.iter()
.chain(fitness_host.iter())
.copied()
.collect();
let combined_sigmas = state.sigmas.clone();
let mut selection_rng = seed_stream(
rng.next_u64(),
state.generation as u64,
SeedPurpose::Selection,
);
let n = combined_fit.len();
let mut win_counts: Vec<u32> = vec![0; n];
for (i, &my_fit) in combined_fit.iter().enumerate() {
for _ in 0..params.tournament_q {
use rand::RngExt;
let opp = selection_rng.random_range(0..n);
if my_fit < combined_fit[opp] {
win_counts[i] += 1;
}
}
}
let mut indexed: Vec<usize> = (0..n).collect();
indexed.sort_by(|&a, &b| {
win_counts[b].cmp(&win_counts[a]).then_with(|| {
combined_fit[a]
.partial_cmp(&combined_fit[b])
.unwrap_or(std::cmp::Ordering::Equal)
})
});
indexed.truncate(mu);
#[allow(clippy::cast_possible_wrap)]
let survivor_idx: Vec<i64> = indexed.iter().map(|&i| i as i64).collect();
let idx_tensor =
Tensor::<B, 1, Int>::from_data(TensorData::new(survivor_idx.clone(), [mu]), &device);
let next_parents = combined_pop.select(0, idx_tensor.clone());
let next_sigmas = combined_sigmas.select(0, idx_tensor);
let next_fitness: Vec<f32> = survivor_idx
.iter()
.map(|&i| {
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
combined_fit[i as usize]
})
.collect();
state.parents = next_parents;
state.sigmas = next_sigmas;
state.parent_fitness = next_fitness;
state.generation += 1;
update_best(&mut state, &offspring, &fitness_host);
let m =
StrategyMetrics::from_host_fitness(state.generation, &fitness_host, state.best_fitness);
state.best_fitness = m.best_fitness_ever;
(state, m)
}
fn best(&self, state: &EpState<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 EpState<B>, pop: &Tensor<B, 2>, fitness: &[f32]) {
if fitness.is_empty() {
return;
}
let mut best_idx = 0usize;
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, 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::NdArray;
use rlevo_core::fitness::FitnessEvaluable;
type TestBackend = NdArray;
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 ep_converges_on_sphere_d2() {
let device = Default::default();
let params = EpConfig::default_for(10, 2);
let fitness_fn = FromFitnessEvaluable::new(SphereFit, Sphere);
let mut harness = EvolutionaryHarness::<TestBackend, _, _>::new(
EvolutionaryProgramming::<TestBackend>::new(),
params,
fitness_fn,
3,
device,
300,
);
harness.reset();
loop {
if harness.step(()).done {
break;
}
}
let best = harness.latest_metrics().unwrap().best_fitness_ever;
assert!(best < 1e-2, "EP Sphere-D2 best={best}");
}
#[test]
fn ep_converges_on_sphere_d10() {
let device = Default::default();
let params = EpConfig::default_for(20, 10);
let fitness_fn = FromFitnessEvaluable::new(SphereFit, Sphere);
let mut harness = EvolutionaryHarness::<TestBackend, _, _>::new(
EvolutionaryProgramming::<TestBackend>::new(),
params,
fitness_fn,
5,
device,
2000,
);
harness.reset();
loop {
if harness.step(()).done {
break;
}
}
let best = harness.latest_metrics().unwrap().best_fitness_ever;
assert!(best < 1e-4, "EP Sphere-D10 best={best}");
}
}