use crate::{OptimizerError, OptimizerResult};
use scirs2_core::RngExt;
use std::collections::HashMap;
use std::sync::Arc;
use torsh_core::device::CpuDevice;
pub trait FitnessFunction: Send + Sync {
fn evaluate(&self, individual: &[f32]) -> OptimizerResult<f32>;
fn dimension(&self) -> usize;
fn bounds(&self) -> Option<(Vec<f32>, Vec<f32>)> {
None
}
fn maximize(&self) -> bool {
true
}
fn name(&self) -> &str {
"Unknown"
}
}
#[derive(Debug, Clone)]
pub struct EvolutionaryConfig {
pub population_size: usize,
pub num_parents: usize,
pub num_offspring: usize,
pub max_generations: usize,
pub tolerance: f32,
pub max_stagnation: usize,
pub seed: Option<u64>,
pub device: Arc<CpuDevice>,
pub verbose: bool,
}
impl Default for EvolutionaryConfig {
fn default() -> Self {
Self {
population_size: 50,
num_parents: 25,
num_offspring: 50,
max_generations: 1000,
tolerance: 1e-6,
max_stagnation: 50,
seed: None,
device: Arc::new(CpuDevice::new()),
verbose: false,
}
}
}
#[derive(Debug, Clone)]
pub struct Individual {
pub genome: Vec<f32>,
pub fitness: f32,
pub strategy_params: Vec<f32>,
}
impl Individual {
pub fn new(genome: Vec<f32>) -> Self {
Self {
genome,
fitness: f32::NEG_INFINITY,
strategy_params: Vec::new(),
}
}
pub fn with_strategy_params(genome: Vec<f32>, strategy_params: Vec<f32>) -> Self {
Self {
genome,
fitness: f32::NEG_INFINITY,
strategy_params,
}
}
}
#[derive(Debug, Clone)]
pub struct EvolutionResult {
pub best_individual: Individual,
pub generations: usize,
pub evaluations: usize,
pub converged: bool,
pub convergence_reason: String,
pub history: Vec<(usize, f32, f32)>,
}
#[derive(Debug)]
pub struct EvolutionStrategy {
config: EvolutionaryConfig,
sigma: f32,
tau: f32,
plus_strategy: bool,
}
impl EvolutionStrategy {
pub fn new(config: EvolutionaryConfig) -> Self {
let dimension = 1.0; Self {
config,
sigma: 1.0,
tau: 1.0 / (2.0 * dimension as f32).sqrt(),
plus_strategy: true,
}
}
pub fn with_parameters(mut self, sigma: f32, tau: f32, plus_strategy: bool) -> Self {
self.sigma = sigma;
self.tau = tau;
self.plus_strategy = plus_strategy;
self
}
pub fn evolve<F: FitnessFunction>(
&mut self,
fitness_fn: &F,
bounds: &[(f32, f32)],
) -> OptimizerResult<EvolutionResult> {
let dimension = fitness_fn.dimension();
if bounds.len() != dimension {
return Err(OptimizerError::InvalidInput(
"Bounds dimension doesn't match fitness function dimension".to_string(),
));
}
self.tau = 1.0 / (2.0 * dimension as f32).sqrt();
use scirs2_core::random::{Random, Rng};
let mut rng = if let Some(seed) = self.config.seed {
Random::seed(seed)
} else {
Random::seed(0)
};
let mut population = self.initialize_population(&mut rng, bounds)?;
let mut evaluations = 0;
for individual in &mut population {
individual.fitness = fitness_fn.evaluate(&individual.genome)?;
evaluations += 1;
}
population.sort_by(|a, b| {
b.fitness
.partial_cmp(&a.fitness)
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut generations = 0;
let mut history = Vec::new();
let mut best_fitness = population[0].fitness;
let mut stagnation_count = 0;
while generations < self.config.max_generations
&& stagnation_count < self.config.max_stagnation
{
let old_best = best_fitness;
let mean_fitness: f32 =
population.iter().map(|ind| ind.fitness).sum::<f32>() / population.len() as f32;
history.push((generations, population[0].fitness, mean_fitness));
let parents = self.select_parents(&population);
let mut offspring = Vec::with_capacity(self.config.num_offspring);
for _ in 0..self.config.num_offspring {
let parent_indices: Vec<usize> = (0..parents.len()).collect();
let parent1_idx = parent_indices[rng.gen_range(0..parent_indices.len())];
let parent2_idx = parent_indices[rng.gen_range(0..parent_indices.len())];
let child = self.recombine_and_mutate(
&parents[parent1_idx],
&parents[parent2_idx],
&mut rng,
bounds,
)?;
offspring.push(child);
}
for individual in &mut offspring {
individual.fitness = fitness_fn.evaluate(&individual.genome)?;
evaluations += 1;
}
population = if self.plus_strategy {
let mut combined = parents;
combined.extend(offspring);
combined.sort_by(|a, b| {
b.fitness
.partial_cmp(&a.fitness)
.unwrap_or(std::cmp::Ordering::Equal)
});
combined
.into_iter()
.take(self.config.population_size)
.collect()
} else {
offspring.sort_by(|a, b| {
b.fitness
.partial_cmp(&a.fitness)
.unwrap_or(std::cmp::Ordering::Equal)
});
offspring
.into_iter()
.take(self.config.population_size)
.collect()
};
best_fitness = population[0].fitness;
if (best_fitness - old_best).abs() < self.config.tolerance {
stagnation_count += 1;
} else {
stagnation_count = 0;
}
generations += 1;
if self.config.verbose && generations % 10 == 0 {
println!(
"Generation {}: Best fitness = {:.6e}, Mean fitness = {:.6e}",
generations, best_fitness, mean_fitness
);
}
}
let converged = stagnation_count < self.config.max_stagnation;
let reason = if converged {
"Maximum generations reached".to_string()
} else {
"Stagnation limit reached".to_string()
};
Ok(EvolutionResult {
best_individual: population[0].clone(),
generations,
evaluations,
converged,
convergence_reason: reason,
history,
})
}
fn initialize_population<R: scirs2_core::random::Rng>(
&self,
rng: &mut R,
bounds: &[(f32, f32)],
) -> OptimizerResult<Vec<Individual>> {
let dimension = bounds.len();
let mut population = Vec::with_capacity(self.config.population_size);
for _ in 0..self.config.population_size {
let mut genome = Vec::with_capacity(dimension);
let mut strategy_params = vec![self.sigma; dimension];
for i in 0..dimension {
let (min_bound, max_bound) = bounds[i];
genome.push(rng.random::<f32>() * (max_bound - min_bound) + min_bound);
}
population.push(Individual::with_strategy_params(genome, strategy_params));
}
Ok(population)
}
fn select_parents(&self, population: &[Individual]) -> Vec<Individual> {
population
.iter()
.take(self.config.num_parents)
.cloned()
.collect()
}
fn recombine_and_mutate<R: scirs2_core::random::Rng>(
&self,
parent1: &Individual,
parent2: &Individual,
rng: &mut R,
bounds: &[(f32, f32)],
) -> OptimizerResult<Individual> {
let dimension = parent1.genome.len();
let mut child_genome = Vec::with_capacity(dimension);
let mut child_strategy = Vec::with_capacity(dimension);
for i in 0..dimension {
let alpha = rng.random::<f32>();
child_genome.push(alpha * parent1.genome[i] + (1.0 - alpha) * parent2.genome[i]);
child_strategy.push((parent1.strategy_params[i] + parent2.strategy_params[i]) / 2.0);
}
let global_factor = (self.tau * rng.random::<f32>().ln()).exp();
let individual_tau = self.tau / (2.0 * dimension as f32).sqrt();
for i in 0..dimension {
let individual_factor = (individual_tau * rng.random::<f32>().ln()).exp();
child_strategy[i] *= global_factor * individual_factor;
child_strategy[i] = child_strategy[i].max(1e-6); }
for i in 0..dimension {
let mutation = rng.random::<f32>() * child_strategy[i];
child_genome[i] += mutation;
let (min_bound, max_bound) = bounds[i];
child_genome[i] = child_genome[i].max(min_bound).min(max_bound);
}
Ok(Individual::with_strategy_params(
child_genome,
child_strategy,
))
}
}
#[derive(Debug)]
pub struct CMAES {
config: EvolutionaryConfig,
sigma: f32,
dimension: usize,
mean: Vec<f32>,
covariance: Vec<Vec<f32>>,
pc: Vec<f32>,
ps: Vec<f32>,
}
impl CMAES {
pub fn new(config: EvolutionaryConfig, dimension: usize) -> Self {
let mean = vec![0.0; dimension];
let mut covariance = vec![vec![0.0; dimension]; dimension];
for i in 0..dimension {
covariance[i][i] = 1.0;
}
Self {
config,
sigma: 1.0,
dimension,
mean,
covariance,
pc: vec![0.0; dimension],
ps: vec![0.0; dimension],
}
}
pub fn evolve<F: FitnessFunction>(
&mut self,
fitness_fn: &F,
initial_mean: &[f32],
bounds: &[(f32, f32)],
) -> OptimizerResult<EvolutionResult> {
if initial_mean.len() != self.dimension || bounds.len() != self.dimension {
return Err(OptimizerError::InvalidInput(
"Dimension mismatch".to_string(),
));
}
self.mean = initial_mean.to_vec();
use scirs2_core::random::{Random, Rng};
let mut rng = if let Some(seed) = self.config.seed {
Random::seed(seed)
} else {
Random::seed(0)
};
let lambda = self.config.population_size;
let mu = lambda / 2;
let cc = 4.0 / (self.dimension as f32 + 4.0);
let cs = (mu as f32 + 2.0) / (self.dimension as f32 + mu as f32 + 3.0);
let c1 = 2.0 / ((self.dimension as f32 + 1.3) * (self.dimension as f32 + 1.3) + mu as f32);
let cmu = (2.0 * (mu as f32 - 2.0 + 1.0 / mu as f32))
/ ((self.dimension as f32 + 2.0) * (self.dimension as f32 + 2.0) + mu as f32);
let damps =
1.0 + 2.0 * (0.0_f32).max((mu as f32 - 1.0) / (self.dimension as f32 + 1.0) - 1.0) + cs;
let mut generations = 0;
let mut evaluations = 0;
let mut history = Vec::new();
let mut best_individual = Individual::new(self.mean.clone());
best_individual.fitness = f32::NEG_INFINITY;
let mut stagnation_count = 0;
while generations < self.config.max_generations
&& stagnation_count < self.config.max_stagnation
{
let old_best = best_individual.fitness;
let mut population = Vec::with_capacity(lambda);
for _ in 0..lambda {
let individual = self.sample_individual(&mut rng, bounds)?;
population.push(individual);
}
for individual in &mut population {
individual.fitness = fitness_fn.evaluate(&individual.genome)?;
evaluations += 1;
if individual.fitness > best_individual.fitness {
best_individual = individual.clone();
}
}
population.sort_by(|a, b| {
b.fitness
.partial_cmp(&a.fitness)
.unwrap_or(std::cmp::Ordering::Equal)
});
let old_mean = self.mean.clone();
for i in 0..self.dimension {
self.mean[i] = 0.0;
for j in 0..mu {
self.mean[i] += population[j].genome[i];
}
self.mean[i] /= mu as f32;
}
let mean_diff: Vec<f32> = self
.mean
.iter()
.zip(old_mean.iter())
.map(|(new, old)| (new - old) / self.sigma)
.collect();
for i in 0..self.dimension {
self.ps[i] =
(1.0 - cs) * self.ps[i] + (cs * (2.0 - cs) * mu as f32).sqrt() * mean_diff[i];
}
for i in 0..self.dimension {
self.pc[i] = (1.0 - cc) * self.pc[i] + cc * (2.0 - cc).sqrt() * mean_diff[i];
}
for i in 0..self.dimension {
for j in 0..self.dimension {
self.covariance[i][j] =
(1.0 - c1 - cmu) * self.covariance[i][j] + c1 * self.pc[i] * self.pc[j];
}
}
let ps_norm: f32 = self.ps.iter().map(|x| x * x).sum::<f32>().sqrt();
let expected_norm = (2.0 / std::f32::consts::PI).sqrt()
* (1.0 - 1.0 / (4.0 * self.dimension as f32)
+ 1.0 / (21.0 * self.dimension as f32 * self.dimension as f32));
self.sigma *= (cs / damps * (ps_norm / expected_norm - 1.0)).exp();
let mean_fitness: f32 =
population.iter().map(|ind| ind.fitness).sum::<f32>() / population.len() as f32;
history.push((generations, population[0].fitness, mean_fitness));
if (best_individual.fitness - old_best).abs() < self.config.tolerance {
stagnation_count += 1;
} else {
stagnation_count = 0;
}
generations += 1;
if self.config.verbose && generations % 10 == 0 {
println!(
"Generation {}: Best fitness = {:.6e}, Sigma = {:.6e}",
generations, best_individual.fitness, self.sigma
);
}
}
let converged = stagnation_count < self.config.max_stagnation;
let reason = if converged {
"Maximum generations reached".to_string()
} else {
"Stagnation limit reached".to_string()
};
Ok(EvolutionResult {
best_individual,
generations,
evaluations,
converged,
convergence_reason: reason,
history,
})
}
fn sample_individual<R: scirs2_core::random::Rng>(
&self,
rng: &mut R,
bounds: &[(f32, f32)],
) -> OptimizerResult<Individual> {
let mut genome = Vec::with_capacity(self.dimension);
for i in 0..self.dimension {
let noise = rng.random::<f32>(); let sample = self.mean[i] + self.sigma * self.covariance[i][i].sqrt() * noise;
let (min_bound, max_bound) = bounds[i];
genome.push(sample.max(min_bound).min(max_bound));
}
Ok(Individual::new(genome))
}
}
#[derive(Debug)]
pub struct OpenAIES {
config: EvolutionaryConfig,
sigma: f32,
alpha: f32,
}
impl OpenAIES {
pub fn new(config: EvolutionaryConfig) -> Self {
Self {
config,
sigma: 0.1,
alpha: 0.01,
}
}
pub fn with_parameters(mut self, sigma: f32, alpha: f32) -> Self {
self.sigma = sigma;
self.alpha = alpha;
self
}
pub fn evolve<F: FitnessFunction>(
&mut self,
fitness_fn: &F,
initial_params: &[f32],
) -> OptimizerResult<EvolutionResult> {
let dimension = fitness_fn.dimension();
if initial_params.len() != dimension {
return Err(OptimizerError::InvalidInput(
"Initial parameters dimension doesn't match fitness function dimension".to_string(),
));
}
use scirs2_core::random::{Random, Rng};
let mut rng = if let Some(seed) = self.config.seed {
Random::seed(seed)
} else {
Random::seed(0)
};
let mut params = initial_params.to_vec();
let mut generations = 0;
let mut evaluations = 0;
let mut history = Vec::new();
let mut best_fitness = f32::NEG_INFINITY;
let mut stagnation_count = 0;
while generations < self.config.max_generations
&& stagnation_count < self.config.max_stagnation
{
let old_best = best_fitness;
let mut noise_vectors = Vec::with_capacity(self.config.population_size);
let mut fitness_values = Vec::with_capacity(self.config.population_size);
for _ in 0..self.config.population_size {
let mut noise = Vec::with_capacity(dimension);
for _ in 0..dimension {
noise.push(rng.random::<f32>() * 2.0 - 1.0); }
let mut perturbed_params = Vec::with_capacity(dimension);
for i in 0..dimension {
perturbed_params.push(params[i] + self.sigma * noise[i]);
}
let fitness = fitness_fn.evaluate(&perturbed_params)?;
evaluations += 1;
noise_vectors.push(noise);
fitness_values.push(fitness);
if fitness > best_fitness {
best_fitness = fitness;
}
}
let mean_fitness: f32 =
fitness_values.iter().sum::<f32>() / fitness_values.len() as f32;
let mut gradient = vec![0.0; dimension];
for (noise, fitness) in noise_vectors.iter().zip(fitness_values.iter()) {
let weight = fitness - mean_fitness;
for i in 0..dimension {
gradient[i] += weight * noise[i];
}
}
for i in 0..dimension {
gradient[i] /= self.config.population_size as f32 * self.sigma;
}
for i in 0..dimension {
params[i] += self.alpha * gradient[i];
}
history.push((generations, best_fitness, mean_fitness));
if (best_fitness - old_best).abs() < self.config.tolerance {
stagnation_count += 1;
} else {
stagnation_count = 0;
}
generations += 1;
if self.config.verbose && generations % 10 == 0 {
println!(
"Generation {}: Best fitness = {:.6e}, Mean fitness = {:.6e}",
generations, best_fitness, mean_fitness
);
}
}
let converged = stagnation_count < self.config.max_stagnation;
let reason = if converged {
"Maximum generations reached".to_string()
} else {
"Stagnation limit reached".to_string()
};
Ok(EvolutionResult {
best_individual: Individual::new(params),
generations,
evaluations,
converged,
convergence_reason: reason,
history,
})
}
}
pub mod test_functions {
use super::*;
pub struct SphereFitness {
pub dimension: usize,
}
impl FitnessFunction for SphereFitness {
fn evaluate(&self, individual: &[f32]) -> OptimizerResult<f32> {
Ok(-individual.iter().map(|&x| x * x).sum::<f32>())
}
fn dimension(&self) -> usize {
self.dimension
}
fn bounds(&self) -> Option<(Vec<f32>, Vec<f32>)> {
Some((vec![-5.0; self.dimension], vec![5.0; self.dimension]))
}
fn name(&self) -> &str {
"Sphere Fitness"
}
}
pub struct RosenbrockFitness {
pub dimension: usize,
}
impl FitnessFunction for RosenbrockFitness {
fn evaluate(&self, individual: &[f32]) -> OptimizerResult<f32> {
let mut sum = 0.0;
for i in 0..individual.len() - 1 {
let term1 = individual[i + 1] - individual[i] * individual[i];
let term2 = 1.0 - individual[i];
sum += 100.0 * term1 * term1 + term2 * term2;
}
Ok(-sum)
}
fn dimension(&self) -> usize {
self.dimension
}
fn bounds(&self) -> Option<(Vec<f32>, Vec<f32>)> {
Some((vec![-2.0; self.dimension], vec![2.0; self.dimension]))
}
fn name(&self) -> &str {
"Rosenbrock Fitness"
}
}
}
#[cfg(test)]
mod tests {
use super::test_functions::*;
use super::*;
#[test]
fn test_evolution_strategy_sphere() {
let config = EvolutionaryConfig {
population_size: 30,
num_parents: 15,
num_offspring: 30,
max_generations: 100,
tolerance: 1e-6,
seed: Some(42),
..Default::default()
};
let mut optimizer = EvolutionStrategy::new(config);
let fitness_fn = SphereFitness { dimension: 2 };
let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
let result = optimizer.evolve(&fitness_fn, &bounds).unwrap();
assert!(result.best_individual.fitness > -1e-2);
assert!(result.best_individual.genome.iter().all(|&x| x.abs() < 0.1));
}
#[test]
fn test_cmaes_sphere() {
let config = EvolutionaryConfig {
population_size: 30,
max_generations: 100,
tolerance: 1e-6,
seed: Some(42),
..Default::default()
};
let mut optimizer = CMAES::new(config, 2);
let fitness_fn = SphereFitness { dimension: 2 };
let initial_mean = vec![1.0, 1.0];
let bounds = vec![(-5.0, 5.0), (-5.0, 5.0)];
let result = optimizer
.evolve(&fitness_fn, &initial_mean, &bounds)
.unwrap();
assert!(result.best_individual.fitness <= 0.0); assert!(result.evaluations > 0);
assert!(!result.best_individual.genome.is_empty());
}
#[test]
fn test_openai_es() {
let config = EvolutionaryConfig {
population_size: 50,
max_generations: 100,
tolerance: 1e-6,
seed: Some(42),
..Default::default()
};
let mut optimizer = OpenAIES::new(config).with_parameters(0.1, 0.01);
let fitness_fn = SphereFitness { dimension: 2 };
let initial_params = vec![1.0, 1.0];
let result = optimizer.evolve(&fitness_fn, &initial_params).unwrap();
assert!(result.best_individual.fitness <= 0.0); assert!(result.evaluations > 0);
assert!(!result.best_individual.genome.is_empty());
}
}