use std::time::Instant;
use rand::Rng;
use rand_distr::StandardNormal;
use crate::diagnostics::{EvolutionResult, EvolutionStats, GenerationStats, TimingStats};
use crate::error::EvolutionError;
use crate::fitness::traits::{Fitness, FitnessValue};
use crate::genome::bounds::MultiBounds;
use crate::genome::traits::{EvolutionaryGenome, RealValuedGenome};
use crate::hyperparameter::self_adaptive::{AdaptiveGenome, StrategyParams};
use crate::population::individual::Individual;
use crate::population::population::Population;
use crate::termination::{EvolutionState, MaxGenerations, TerminationCriterion};
#[derive(Clone, Debug, Default)]
pub enum ESSelectionStrategy {
MuPlusLambda,
#[default]
MuCommaLambda,
}
#[derive(Clone, Debug)]
pub struct ESConfig {
pub mu: usize,
pub lambda: usize,
pub selection: ESSelectionStrategy,
pub initial_sigma: f64,
pub self_adaptive: bool,
pub recombination: RecombinationType,
pub min_sigma: Option<f64>,
}
impl Default for ESConfig {
fn default() -> Self {
Self {
mu: 15,
lambda: 100,
selection: ESSelectionStrategy::MuCommaLambda,
initial_sigma: 1.0,
self_adaptive: true,
recombination: RecombinationType::Intermediate,
min_sigma: None,
}
}
}
impl ESConfig {
pub fn resolved_min_sigma(&self) -> f64 {
self.min_sigma.unwrap_or(1e-8 * self.initial_sigma)
}
pub fn mu_plus_lambda(mu: usize, lambda: usize) -> Self {
Self {
mu,
lambda,
selection: ESSelectionStrategy::MuPlusLambda,
..Default::default()
}
}
pub fn mu_comma_lambda(mu: usize, lambda: usize) -> Result<Self, EvolutionError> {
if lambda < mu {
return Err(EvolutionError::Configuration(format!(
"For (μ,λ)-ES, λ ({}) must be >= μ ({})",
lambda, mu
)));
}
Ok(Self {
mu,
lambda,
selection: ESSelectionStrategy::MuCommaLambda,
..Default::default()
})
}
}
#[derive(Clone, Debug, Default)]
pub enum RecombinationType {
None,
Discrete,
#[default]
Intermediate,
GlobalIntermediate,
}
pub struct ESBuilder<G, F, Fit, Term>
where
G: EvolutionaryGenome,
F: FitnessValue,
{
config: ESConfig,
bounds: Option<MultiBounds>,
fitness: Option<Fit>,
termination: Option<Term>,
_phantom: std::marker::PhantomData<(G, F)>,
}
impl<G, F> ESBuilder<G, F, (), ()>
where
G: EvolutionaryGenome,
F: FitnessValue,
{
pub fn new() -> Self {
Self {
config: ESConfig::default(),
bounds: None,
fitness: None,
termination: None,
_phantom: std::marker::PhantomData,
}
}
pub fn mu_plus_lambda(mu: usize, lambda: usize) -> Self {
Self {
config: ESConfig::mu_plus_lambda(mu, lambda),
bounds: None,
fitness: None,
termination: None,
_phantom: std::marker::PhantomData,
}
}
pub fn mu_comma_lambda(mu: usize, lambda: usize) -> Result<Self, EvolutionError> {
Ok(Self {
config: ESConfig::mu_comma_lambda(mu, lambda)?,
bounds: None,
fitness: None,
termination: None,
_phantom: std::marker::PhantomData,
})
}
}
impl<G, F> Default for ESBuilder<G, F, (), ()>
where
G: EvolutionaryGenome,
F: FitnessValue,
{
fn default() -> Self {
Self::new()
}
}
impl<G, F, Fit, Term> ESBuilder<G, F, Fit, Term>
where
G: EvolutionaryGenome,
F: FitnessValue,
{
pub fn mu(mut self, mu: usize) -> Self {
self.config.mu = mu;
self
}
pub fn lambda(mut self, lambda: usize) -> Self {
self.config.lambda = lambda;
self
}
pub fn selection_strategy(mut self, strategy: ESSelectionStrategy) -> Self {
self.config.selection = strategy;
self
}
pub fn initial_sigma(mut self, sigma: f64) -> Self {
self.config.initial_sigma = sigma;
self
}
pub fn min_sigma(mut self, sigma: f64) -> Self {
self.config.min_sigma = Some(sigma);
self
}
pub fn self_adaptive(mut self, enabled: bool) -> Self {
self.config.self_adaptive = enabled;
self
}
pub fn recombination(mut self, recomb: RecombinationType) -> Self {
self.config.recombination = recomb;
self
}
pub fn bounds(mut self, bounds: MultiBounds) -> Self {
self.bounds = Some(bounds);
self
}
pub fn fitness<NewFit>(self, fitness: NewFit) -> ESBuilder<G, F, NewFit, Term>
where
NewFit: Fitness<Genome = G, Value = F>,
{
ESBuilder {
config: self.config,
bounds: self.bounds,
fitness: Some(fitness),
termination: self.termination,
_phantom: std::marker::PhantomData,
}
}
pub fn termination<NewTerm>(self, termination: NewTerm) -> ESBuilder<G, F, Fit, NewTerm>
where
NewTerm: TerminationCriterion<G, F>,
{
ESBuilder {
config: self.config,
bounds: self.bounds,
fitness: self.fitness,
termination: Some(termination),
_phantom: std::marker::PhantomData,
}
}
pub fn max_generations(self, max: usize) -> ESBuilder<G, F, Fit, MaxGenerations> {
ESBuilder {
config: self.config,
bounds: self.bounds,
fitness: self.fitness,
termination: Some(MaxGenerations::new(max)),
_phantom: std::marker::PhantomData,
}
}
}
#[cfg(feature = "parallel")]
impl<G, F, Fit, Term> ESBuilder<G, F, Fit, Term>
where
G: EvolutionaryGenome + RealValuedGenome + Send + Sync,
F: FitnessValue + Send,
Fit: Fitness<Genome = G, Value = F> + Sync,
Term: TerminationCriterion<G, F>,
{
pub fn build(self) -> Result<EvolutionStrategy<G, F, Fit, Term>, EvolutionError> {
let bounds = self
.bounds
.ok_or_else(|| EvolutionError::Configuration("Bounds must be specified".to_string()))?;
let fitness = self.fitness.ok_or_else(|| {
EvolutionError::Configuration("Fitness function must be specified".to_string())
})?;
let termination = self.termination.ok_or_else(|| {
EvolutionError::Configuration("Termination criterion must be specified".to_string())
})?;
if matches!(self.config.selection, ESSelectionStrategy::MuCommaLambda)
&& self.config.lambda < self.config.mu
{
return Err(EvolutionError::Configuration(format!(
"For (μ,λ)-ES, λ ({}) must be >= μ ({})",
self.config.lambda, self.config.mu
)));
}
Ok(EvolutionStrategy {
config: self.config,
bounds,
fitness,
termination,
_phantom: std::marker::PhantomData,
})
}
}
#[cfg(not(feature = "parallel"))]
impl<G, F, Fit, Term> ESBuilder<G, F, Fit, Term>
where
G: EvolutionaryGenome + RealValuedGenome,
F: FitnessValue,
Fit: Fitness<Genome = G, Value = F>,
Term: TerminationCriterion<G, F>,
{
pub fn build(self) -> Result<EvolutionStrategy<G, F, Fit, Term>, EvolutionError> {
let bounds = self
.bounds
.ok_or_else(|| EvolutionError::Configuration("Bounds must be specified".to_string()))?;
let fitness = self.fitness.ok_or_else(|| {
EvolutionError::Configuration("Fitness function must be specified".to_string())
})?;
let termination = self.termination.ok_or_else(|| {
EvolutionError::Configuration("Termination criterion must be specified".to_string())
})?;
if matches!(self.config.selection, ESSelectionStrategy::MuCommaLambda)
&& self.config.lambda < self.config.mu
{
return Err(EvolutionError::Configuration(format!(
"For (μ,λ)-ES, λ ({}) must be >= μ ({})",
self.config.lambda, self.config.mu
)));
}
Ok(EvolutionStrategy {
config: self.config,
bounds,
fitness,
termination,
_phantom: std::marker::PhantomData,
})
}
}
pub struct EvolutionStrategy<G, F, Fit, Term>
where
G: EvolutionaryGenome,
F: FitnessValue,
{
config: ESConfig,
bounds: MultiBounds,
fitness: Fit,
termination: Term,
_phantom: std::marker::PhantomData<(G, F)>,
}
#[cfg(feature = "parallel")]
impl<G, F, Fit, Term> EvolutionStrategy<G, F, Fit, Term>
where
G: EvolutionaryGenome + RealValuedGenome + Send + Sync,
F: FitnessValue + Send,
Fit: Fitness<Genome = G, Value = F> + Sync,
Term: TerminationCriterion<G, F>,
{
pub fn builder() -> ESBuilder<G, F, (), ()> {
ESBuilder::new()
}
pub fn run<R: Rng>(&self, rng: &mut R) -> Result<EvolutionResult<G, F>, EvolutionError> {
self.run_with_callback(rng, |_generation, _best_fitness| true)
}
pub fn run_with_callback<R: Rng, Cb: FnMut(usize, f64) -> bool>(
&self,
rng: &mut R,
mut on_generation: Cb,
) -> Result<EvolutionResult<G, F>, EvolutionError> {
let start_time = Instant::now();
let mut population: Vec<(AdaptiveGenome<G>, F)> = (0..self.config.mu)
.map(|_| {
let genome = G::generate(rng, &self.bounds);
let adaptive = if self.config.self_adaptive {
AdaptiveGenome::new_non_isotropic(
genome,
vec![self.config.initial_sigma; self.bounds.dimension()],
)
} else {
AdaptiveGenome::new_isotropic(genome, self.config.initial_sigma)
};
let fitness = self.fitness.evaluate(adaptive.inner());
(adaptive, fitness)
})
.collect();
population.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let mut stats = EvolutionStats::new();
let mut evaluations = self.config.mu;
let mut fitness_history: Vec<f64> = Vec::new();
let mut generation = 0usize;
let mut best = population[0].clone();
let mut tracking_population: Population<G, F> = Population::with_capacity(self.config.mu);
for (adaptive, fit) in &population {
let mut ind = Individual::new(adaptive.inner().clone());
ind.set_fitness(fit.clone());
tracking_population.push(ind);
}
let gen_stats = GenerationStats::from_population(&tracking_population, 0, evaluations);
fitness_history.push(gen_stats.best_fitness);
stats.record(gen_stats);
loop {
let state = EvolutionState {
generation,
evaluations,
best_fitness: best.1.to_f64(),
population: &tracking_population,
fitness_history: &fitness_history,
};
if self.termination.should_terminate(&state) {
stats.set_termination_reason(self.termination.reason());
break;
}
if !on_generation(generation, best.1.to_f64()) {
break;
}
let gen_start = Instant::now();
let mut offspring: Vec<(AdaptiveGenome<G>, F)> = Vec::with_capacity(self.config.lambda);
for _ in 0..self.config.lambda {
let child = match &self.config.recombination {
RecombinationType::None => {
let parent_idx = rng.gen_range(0..self.config.mu);
population[parent_idx].0.clone()
}
RecombinationType::Discrete => {
let p1_idx = rng.gen_range(0..self.config.mu);
let p2_idx = rng.gen_range(0..self.config.mu);
self.discrete_recombination(
&population[p1_idx].0,
&population[p2_idx].0,
rng,
)
}
RecombinationType::Intermediate => {
let p1_idx = rng.gen_range(0..self.config.mu);
let p2_idx = rng.gen_range(0..self.config.mu);
self.intermediate_recombination(
&population[p1_idx].0,
&population[p2_idx].0,
)
}
RecombinationType::GlobalIntermediate => {
self.global_intermediate_recombination(&population)
}
};
let mutated = self.mutate(child, rng);
let fitness = self.fitness.evaluate(mutated.inner());
offspring.push((mutated, fitness));
}
evaluations += self.config.lambda;
match self.config.selection {
ESSelectionStrategy::MuPlusLambda => {
let mut combined = population;
combined.extend(offspring);
combined
.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
population = combined.into_iter().take(self.config.mu).collect();
}
ESSelectionStrategy::MuCommaLambda => {
offspring
.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
population = offspring.into_iter().take(self.config.mu).collect();
}
}
if population[0].1.is_better_than(&best.1) {
best = population[0].clone();
}
generation += 1;
tracking_population.clear();
for (adaptive, fit) in &population {
let mut ind = Individual::new(adaptive.inner().clone());
ind.set_fitness(fit.clone());
tracking_population.push(ind);
}
tracking_population.set_generation(generation);
let timing = TimingStats::new().with_total(gen_start.elapsed());
let gen_stats =
GenerationStats::from_population(&tracking_population, generation, evaluations)
.with_timing(timing);
fitness_history.push(gen_stats.best_fitness);
stats.record(gen_stats);
}
stats.set_runtime(start_time.elapsed());
Ok(
EvolutionResult::new(best.0.into_inner(), best.1, generation, evaluations)
.with_stats(stats),
)
}
fn discrete_recombination<R: Rng>(
&self,
p1: &AdaptiveGenome<G>,
p2: &AdaptiveGenome<G>,
rng: &mut R,
) -> AdaptiveGenome<G> {
let genes1 = p1.inner().genes();
let genes2 = p2.inner().genes();
let child_genes: Vec<f64> = genes1
.iter()
.zip(genes2.iter())
.map(|(g1, g2)| if rng.gen_bool(0.5) { *g1 } else { *g2 })
.collect();
let child_genome = G::from_genes(child_genes).expect("Failed to create genome from genes");
match (&p1.strategy, &p2.strategy) {
(StrategyParams::Isotropic(s1), StrategyParams::Isotropic(s2)) => {
AdaptiveGenome::new_isotropic(child_genome, (s1 * s2).sqrt())
}
(StrategyParams::NonIsotropic(s1), StrategyParams::NonIsotropic(s2)) => {
let sigmas: Vec<f64> = s1
.iter()
.zip(s2.iter())
.map(|(a, b)| if rng.gen_bool(0.5) { *a } else { *b })
.collect();
AdaptiveGenome::new_non_isotropic(child_genome, sigmas)
}
_ => AdaptiveGenome::new_isotropic(child_genome, self.config.initial_sigma),
}
}
fn intermediate_recombination(
&self,
p1: &AdaptiveGenome<G>,
p2: &AdaptiveGenome<G>,
) -> AdaptiveGenome<G> {
let genes1 = p1.inner().genes();
let genes2 = p2.inner().genes();
let child_genes: Vec<f64> = genes1
.iter()
.zip(genes2.iter())
.map(|(g1, g2)| (g1 + g2) / 2.0)
.collect();
let child_genome = G::from_genes(child_genes).expect("Failed to create genome from genes");
match (&p1.strategy, &p2.strategy) {
(StrategyParams::Isotropic(s1), StrategyParams::Isotropic(s2)) => {
AdaptiveGenome::new_isotropic(child_genome, (s1 * s2).sqrt())
}
(StrategyParams::NonIsotropic(s1), StrategyParams::NonIsotropic(s2)) => {
let sigmas: Vec<f64> = s1
.iter()
.zip(s2.iter())
.map(|(a, b)| (a * b).sqrt())
.collect();
AdaptiveGenome::new_non_isotropic(child_genome, sigmas)
}
_ => AdaptiveGenome::new_isotropic(child_genome, self.config.initial_sigma),
}
}
fn global_intermediate_recombination(
&self,
population: &[(AdaptiveGenome<G>, F)],
) -> AdaptiveGenome<G> {
let n = population.len();
let dim = population[0].0.inner().genes().len();
let mut child_genes = vec![0.0; dim];
for (adaptive, _) in population {
for (i, gene) in adaptive.inner().genes().iter().enumerate() {
child_genes[i] += gene / n as f64;
}
}
let child_genome = G::from_genes(child_genes).expect("Failed to create genome from genes");
let avg_sigma = if self.config.self_adaptive {
let sigmas: Vec<f64> = (0..dim)
.map(|i| {
let sum: f64 = population
.iter()
.map(|(a, _)| a.strategy.get_sigma(i))
.sum();
sum / n as f64
})
.collect();
AdaptiveGenome::new_non_isotropic(child_genome, sigmas)
} else {
AdaptiveGenome::new_isotropic(child_genome, self.config.initial_sigma)
};
avg_sigma
}
fn mutate<R: Rng>(&self, mut genome: AdaptiveGenome<G>, rng: &mut R) -> AdaptiveGenome<G> {
let n = genome.inner().genes().len();
if self.config.self_adaptive {
genome
.strategy
.mutate(n, self.config.resolved_min_sigma(), rng);
}
let sigmas: Vec<f64> = (0..n).map(|i| genome.strategy.get_sigma(i)).collect();
let genes = genome.inner_mut().genes_mut();
for i in 0..genes.len() {
let perturbation: f64 = rng.sample(StandardNormal);
genes[i] += sigmas[i] * perturbation;
if let Some(b) = self.bounds.get(i) {
genes[i] = genes[i].clamp(b.min, b.max);
}
}
genome
}
}
#[cfg(not(feature = "parallel"))]
impl<G, F, Fit, Term> EvolutionStrategy<G, F, Fit, Term>
where
G: EvolutionaryGenome + RealValuedGenome,
F: FitnessValue,
Fit: Fitness<Genome = G, Value = F>,
Term: TerminationCriterion<G, F>,
{
pub fn builder() -> ESBuilder<G, F, (), ()> {
ESBuilder::new()
}
pub fn run<R: Rng>(&self, rng: &mut R) -> Result<EvolutionResult<G, F>, EvolutionError> {
self.run_with_callback(rng, |_generation, _best_fitness| true)
}
pub fn run_with_callback<R: Rng, Cb: FnMut(usize, f64) -> bool>(
&self,
rng: &mut R,
mut on_generation: Cb,
) -> Result<EvolutionResult<G, F>, EvolutionError> {
let start_time = Instant::now();
let mut population: Vec<(AdaptiveGenome<G>, F)> = (0..self.config.mu)
.map(|_| {
let genome = G::generate(rng, &self.bounds);
let adaptive = if self.config.self_adaptive {
AdaptiveGenome::new_non_isotropic(
genome,
vec![self.config.initial_sigma; self.bounds.dimension()],
)
} else {
AdaptiveGenome::new_isotropic(genome, self.config.initial_sigma)
};
let fitness = self.fitness.evaluate(adaptive.inner());
(adaptive, fitness)
})
.collect();
population.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let mut stats = EvolutionStats::new();
let mut evaluations = self.config.mu;
let mut fitness_history: Vec<f64> = Vec::new();
let mut generation = 0usize;
let mut best = population[0].clone();
let mut tracking_population: Population<G, F> = Population::with_capacity(self.config.mu);
for (adaptive, fit) in &population {
let mut ind = Individual::new(adaptive.inner().clone());
ind.set_fitness(fit.clone());
tracking_population.push(ind);
}
let gen_stats = GenerationStats::from_population(&tracking_population, 0, evaluations);
fitness_history.push(gen_stats.best_fitness);
stats.record(gen_stats);
loop {
let state = EvolutionState {
generation,
evaluations,
best_fitness: best.1.to_f64(),
population: &tracking_population,
fitness_history: &fitness_history,
};
if self.termination.should_terminate(&state) {
stats.set_termination_reason(self.termination.reason());
break;
}
if !on_generation(generation, best.1.to_f64()) {
break;
}
let gen_start = Instant::now();
let mut offspring: Vec<(AdaptiveGenome<G>, F)> = Vec::with_capacity(self.config.lambda);
for _ in 0..self.config.lambda {
let child = match &self.config.recombination {
RecombinationType::None => {
let parent_idx = rng.gen_range(0..self.config.mu);
population[parent_idx].0.clone()
}
RecombinationType::Discrete => {
let p1_idx = rng.gen_range(0..self.config.mu);
let p2_idx = rng.gen_range(0..self.config.mu);
self.discrete_recombination(
&population[p1_idx].0,
&population[p2_idx].0,
rng,
)
}
RecombinationType::Intermediate => {
let p1_idx = rng.gen_range(0..self.config.mu);
let p2_idx = rng.gen_range(0..self.config.mu);
self.intermediate_recombination(
&population[p1_idx].0,
&population[p2_idx].0,
)
}
RecombinationType::GlobalIntermediate => {
self.global_intermediate_recombination(&population)
}
};
let mutated = self.mutate(child, rng);
let fitness = self.fitness.evaluate(mutated.inner());
offspring.push((mutated, fitness));
}
evaluations += self.config.lambda;
match self.config.selection {
ESSelectionStrategy::MuPlusLambda => {
let mut combined = population;
combined.extend(offspring);
combined
.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
population = combined.into_iter().take(self.config.mu).collect();
}
ESSelectionStrategy::MuCommaLambda => {
offspring
.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
population = offspring.into_iter().take(self.config.mu).collect();
}
}
if population[0].1.is_better_than(&best.1) {
best = population[0].clone();
}
generation += 1;
tracking_population.clear();
for (adaptive, fit) in &population {
let mut ind = Individual::new(adaptive.inner().clone());
ind.set_fitness(fit.clone());
tracking_population.push(ind);
}
tracking_population.set_generation(generation);
let timing = TimingStats::new().with_total(gen_start.elapsed());
let gen_stats =
GenerationStats::from_population(&tracking_population, generation, evaluations)
.with_timing(timing);
fitness_history.push(gen_stats.best_fitness);
stats.record(gen_stats);
}
stats.set_runtime(start_time.elapsed());
Ok(
EvolutionResult::new(best.0.into_inner(), best.1, generation, evaluations)
.with_stats(stats),
)
}
fn discrete_recombination<R: Rng>(
&self,
p1: &AdaptiveGenome<G>,
p2: &AdaptiveGenome<G>,
rng: &mut R,
) -> AdaptiveGenome<G> {
let genes1 = p1.inner().genes();
let genes2 = p2.inner().genes();
let child_genes: Vec<f64> = genes1
.iter()
.zip(genes2.iter())
.map(|(g1, g2)| if rng.gen_bool(0.5) { *g1 } else { *g2 })
.collect();
let child_genome = G::from_genes(child_genes).expect("Failed to create genome from genes");
match (&p1.strategy, &p2.strategy) {
(StrategyParams::Isotropic(s1), StrategyParams::Isotropic(s2)) => {
AdaptiveGenome::new_isotropic(child_genome, (s1 * s2).sqrt())
}
(StrategyParams::NonIsotropic(s1), StrategyParams::NonIsotropic(s2)) => {
let sigmas: Vec<f64> = s1
.iter()
.zip(s2.iter())
.map(|(a, b)| if rng.gen_bool(0.5) { *a } else { *b })
.collect();
AdaptiveGenome::new_non_isotropic(child_genome, sigmas)
}
_ => AdaptiveGenome::new_isotropic(child_genome, self.config.initial_sigma),
}
}
fn intermediate_recombination(
&self,
p1: &AdaptiveGenome<G>,
p2: &AdaptiveGenome<G>,
) -> AdaptiveGenome<G> {
let genes1 = p1.inner().genes();
let genes2 = p2.inner().genes();
let child_genes: Vec<f64> = genes1
.iter()
.zip(genes2.iter())
.map(|(g1, g2)| (g1 + g2) / 2.0)
.collect();
let child_genome = G::from_genes(child_genes).expect("Failed to create genome from genes");
match (&p1.strategy, &p2.strategy) {
(StrategyParams::Isotropic(s1), StrategyParams::Isotropic(s2)) => {
AdaptiveGenome::new_isotropic(child_genome, (s1 * s2).sqrt())
}
(StrategyParams::NonIsotropic(s1), StrategyParams::NonIsotropic(s2)) => {
let sigmas: Vec<f64> = s1
.iter()
.zip(s2.iter())
.map(|(a, b)| (a * b).sqrt())
.collect();
AdaptiveGenome::new_non_isotropic(child_genome, sigmas)
}
_ => AdaptiveGenome::new_isotropic(child_genome, self.config.initial_sigma),
}
}
fn global_intermediate_recombination(
&self,
population: &[(AdaptiveGenome<G>, F)],
) -> AdaptiveGenome<G> {
let n = population.len();
let dim = population[0].0.inner().genes().len();
let mut child_genes = vec![0.0; dim];
for (adaptive, _) in population {
for (i, gene) in adaptive.inner().genes().iter().enumerate() {
child_genes[i] += gene / n as f64;
}
}
let child_genome = G::from_genes(child_genes).expect("Failed to create genome from genes");
let avg_sigma = if self.config.self_adaptive {
let sigmas: Vec<f64> = (0..dim)
.map(|i| {
let sum: f64 = population
.iter()
.map(|(a, _)| a.strategy.get_sigma(i))
.sum();
sum / n as f64
})
.collect();
AdaptiveGenome::new_non_isotropic(child_genome, sigmas)
} else {
AdaptiveGenome::new_isotropic(child_genome, self.config.initial_sigma)
};
avg_sigma
}
fn mutate<R: Rng>(&self, mut genome: AdaptiveGenome<G>, rng: &mut R) -> AdaptiveGenome<G> {
let n = genome.inner().genes().len();
if self.config.self_adaptive {
genome
.strategy
.mutate(n, self.config.resolved_min_sigma(), rng);
}
let sigmas: Vec<f64> = (0..n).map(|i| genome.strategy.get_sigma(i)).collect();
let genes = genome.inner_mut().genes_mut();
for i in 0..genes.len() {
let perturbation: f64 = rng.sample(StandardNormal);
genes[i] += sigmas[i] * perturbation;
if let Some(b) = self.bounds.get(i) {
genes[i] = genes[i].clamp(b.min, b.max);
}
}
genome
}
}
pub type MuPlusLambdaES<G, F, Fit, Term> = EvolutionStrategy<G, F, Fit, Term>;
pub type MuCommaLambdaES<G, F, Fit, Term> = EvolutionStrategy<G, F, Fit, Term>;
#[cfg(test)]
mod tests {
use super::*;
use crate::fitness::benchmarks::Sphere;
use crate::genome::real_vector::RealVector;
use crate::termination::MaxEvaluations;
use rand::SeedableRng;
#[test]
fn test_es_builder() {
let bounds = MultiBounds::symmetric(5.0, 10);
let es: Result<EvolutionStrategy<RealVector, f64, _, _>, _> = ESBuilder::new()
.mu(15)
.lambda(100)
.bounds(bounds)
.fitness(Sphere::new(10))
.max_generations(10)
.build();
assert!(es.is_ok());
}
#[test]
fn test_mu_plus_lambda_es() {
let mut rng = rand::thread_rng();
let bounds = MultiBounds::symmetric(5.12, 10);
let es: EvolutionStrategy<RealVector, f64, _, _> = ESBuilder::mu_plus_lambda(10, 70)
.initial_sigma(1.0)
.self_adaptive(true)
.bounds(bounds)
.fitness(Sphere::new(10))
.termination(MaxEvaluations::new(3000))
.build()
.unwrap();
let result = es.run(&mut rng).unwrap();
assert!(
result.best_fitness > -50.0,
"Expected fitness > -50, got {}",
result.best_fitness
);
}
#[test]
fn test_mu_comma_lambda_es() {
let mut rng = rand::thread_rng();
let bounds = MultiBounds::symmetric(5.12, 10);
let es: EvolutionStrategy<RealVector, f64, _, _> = ESBuilder::mu_comma_lambda(10, 70)
.unwrap()
.initial_sigma(1.0)
.self_adaptive(true)
.bounds(bounds)
.fitness(Sphere::new(10))
.termination(MaxEvaluations::new(3000))
.build()
.unwrap();
let result = es.run(&mut rng).unwrap();
assert!(
result.best_fitness > -100.0,
"Expected fitness > -100, got {}",
result.best_fitness
);
}
#[test]
fn test_mu_comma_lambda_constraint() {
let result = ESConfig::mu_comma_lambda(50, 30);
assert!(result.is_err());
}
#[test]
fn test_recombination_types() {
let mut rng = rand::thread_rng();
let bounds = MultiBounds::symmetric(5.12, 5);
let recomb_types = vec![
RecombinationType::None,
RecombinationType::Discrete,
RecombinationType::Intermediate,
RecombinationType::GlobalIntermediate,
];
for recomb in recomb_types {
let es: EvolutionStrategy<RealVector, f64, _, _> = ESBuilder::new()
.mu(10)
.lambda(50)
.recombination(recomb)
.bounds(bounds.clone())
.fitness(Sphere::new(5))
.termination(MaxEvaluations::new(500))
.build()
.unwrap();
let result = es.run(&mut rng);
assert!(result.is_ok());
}
}
#[test]
fn test_es_self_adaptive_disabled() {
let mut rng = rand::thread_rng();
let bounds = MultiBounds::symmetric(5.12, 5);
let es: EvolutionStrategy<RealVector, f64, _, _> = ESBuilder::new()
.mu(10)
.lambda(50)
.self_adaptive(false)
.initial_sigma(0.5)
.bounds(bounds)
.fitness(Sphere::new(5))
.termination(MaxEvaluations::new(500))
.build()
.unwrap();
let result = es.run(&mut rng);
assert!(result.is_ok());
}
#[test]
fn test_es_run_with_callback_reports_progress() {
let mut rng = rand::rngs::StdRng::seed_from_u64(11);
let bounds = MultiBounds::symmetric(5.12, 4);
let es: EvolutionStrategy<RealVector, f64, _, _> = ESBuilder::new()
.mu(6)
.lambda(24)
.initial_sigma(0.5)
.bounds(bounds)
.fitness(Sphere::new(4))
.max_generations(15)
.build()
.unwrap();
let mut seen: Vec<usize> = Vec::new();
let result = es
.run_with_callback(&mut rng, |generation, best| {
assert!(best.is_finite());
seen.push(generation);
true
})
.unwrap();
assert_eq!(seen, (0..result.generations).collect::<Vec<_>>());
assert!(!seen.is_empty());
}
#[test]
fn test_es_run_with_callback_cancels_early() {
let mut rng = rand::rngs::StdRng::seed_from_u64(12);
let bounds = MultiBounds::symmetric(5.12, 4);
let es: EvolutionStrategy<RealVector, f64, _, _> = ESBuilder::new()
.mu(6)
.lambda(24)
.initial_sigma(0.5)
.bounds(bounds)
.fitness(Sphere::new(4))
.max_generations(10_000)
.build()
.unwrap();
let mut calls = 0usize;
let result = es
.run_with_callback(&mut rng, |_generation, _best| {
calls += 1;
calls < 5
})
.unwrap();
assert!(calls <= 5, "callback should stop being called after cancel");
assert!(
result.generations < 10,
"run must stop far short of the 10k budget, got {}",
result.generations
);
}
#[test]
fn test_default_selection_is_comma() {
let config = ESConfig::default();
assert!(config.self_adaptive);
assert!(
matches!(config.selection, ESSelectionStrategy::MuCommaLambda),
"self-adaptive default must use (μ,λ) comma selection"
);
assert!(config.lambda >= config.mu);
}
#[test]
fn test_resolved_min_sigma() {
let mut config = ESConfig {
initial_sigma: 2.0,
..Default::default()
};
assert!((config.resolved_min_sigma() - 2e-8).abs() < 1e-18);
config.min_sigma = Some(0.01);
assert!((config.resolved_min_sigma() - 0.01).abs() < 1e-18);
}
#[test]
fn test_min_sigma_builder_threads_through() {
let bounds = MultiBounds::symmetric(5.12, 5);
let es: EvolutionStrategy<RealVector, f64, _, _> = ESBuilder::new()
.mu(5)
.lambda(35)
.self_adaptive(true)
.initial_sigma(1.0)
.min_sigma(0.05)
.bounds(bounds)
.fitness(Sphere::new(5))
.termination(MaxEvaluations::new(700))
.build()
.unwrap();
let mut rng = rand::thread_rng();
assert!(es.run(&mut rng).is_ok());
}
#[test]
fn test_es_bounds_respected() {
let mut rng = rand::thread_rng();
let bounds = MultiBounds::symmetric(2.0, 5);
let es: EvolutionStrategy<RealVector, f64, _, _> = ESBuilder::new()
.mu(10)
.lambda(50)
.initial_sigma(5.0) .bounds(bounds.clone())
.fitness(Sphere::new(5))
.termination(MaxEvaluations::new(500))
.build()
.unwrap();
let result = es.run(&mut rng).unwrap();
for gene in result.best_genome.genes() {
assert!(
*gene >= -2.0 && *gene <= 2.0,
"Gene {} outside bounds [-2.0, 2.0]",
gene
);
}
}
}