use std::marker::PhantomData;
use nalgebra::{DMatrix, SymmetricEigen};
use rand::Rng;
use rand_distr::{Distribution, StandardNormal};
use crate::error::{EvoResult, EvolutionError};
use crate::genome::bounds::MultiBounds;
use crate::genome::real_vector::RealVector;
use crate::genome::traits::RealValuedGenome;
use crate::population::individual::Individual;
#[cfg(feature = "parallel")]
pub trait CmaEsFitness: Send + Sync {
fn evaluate(&self, x: &RealVector) -> f64;
}
#[cfg(not(feature = "parallel"))]
pub trait CmaEsFitness {
fn evaluate(&self, x: &RealVector) -> f64;
}
#[derive(Clone, Debug)]
pub struct CmaEsState {
pub mean: Vec<f64>,
pub sigma: f64,
pub covariance: Vec<Vec<f64>>,
pub path_sigma: Vec<f64>,
pub path_c: Vec<f64>,
pub eigenvalues: Vec<f64>,
pub eigenvectors: Vec<Vec<f64>>,
pub eigen_eval: usize,
pub dimension: usize,
pub lambda: usize,
pub mu: usize,
pub weights: Vec<f64>,
pub mu_eff: f64,
pub c_1: f64,
pub c_mu: f64,
pub c_sigma: f64,
pub d_sigma: f64,
pub c_c: f64,
pub chi_n: f64,
pub generation: usize,
pub evaluations: usize,
pub best_fitness: f64,
pub best_solution: Vec<f64>,
}
impl CmaEsState {
pub fn new(initial_mean: Vec<f64>, initial_sigma: f64, lambda: Option<usize>) -> Self {
let n = initial_mean.len();
let lambda = lambda.unwrap_or((4.0 + (3.0 * (n as f64).ln()).floor()) as usize);
let lambda = lambda.max(4);
let mu = lambda / 2;
let mut weights: Vec<f64> = (0..mu)
.map(|i| ((lambda as f64 + 1.0) / 2.0).ln() - ((i + 1) as f64).ln())
.collect();
let weight_sum: f64 = weights.iter().sum();
for w in &mut weights {
*w /= weight_sum;
}
let mu_eff = 1.0 / weights.iter().map(|w| w * w).sum::<f64>();
let c_sigma = (mu_eff + 2.0) / (n as f64 + mu_eff + 5.0);
let c_c = (4.0 + mu_eff / n as f64) / (n as f64 + 4.0 + 2.0 * mu_eff / n as f64);
let c_1 = 2.0 / ((n as f64 + 1.3).powi(2) + mu_eff);
let alpha_mu = 2.0;
let c_mu = (alpha_mu * (mu_eff - 2.0 + 1.0 / mu_eff))
/ ((n as f64 + 2.0).powi(2) + alpha_mu * mu_eff / 2.0);
let c_mu = c_mu.min(1.0 - c_1);
let d_sigma =
1.0 + 2.0 * (0.0_f64.max(((mu_eff - 1.0) / (n as f64 + 1.0)).sqrt() - 1.0)) + c_sigma;
let chi_n =
(n as f64).sqrt() * (1.0 - 1.0 / (4.0 * n as f64) + 1.0 / (21.0 * (n as f64).powi(2)));
let covariance: Vec<Vec<f64>> = (0..n)
.map(|i| {
let mut row = vec![0.0; n];
row[i] = 1.0;
row
})
.collect();
let eigenvalues = vec![1.0; n];
let eigenvectors: Vec<Vec<f64>> = (0..n)
.map(|i| {
let mut row = vec![0.0; n];
row[i] = 1.0;
row
})
.collect();
Self {
mean: initial_mean.clone(),
sigma: initial_sigma,
covariance,
path_sigma: vec![0.0; n],
path_c: vec![0.0; n],
eigenvalues,
eigenvectors,
eigen_eval: 0,
dimension: n,
lambda,
mu,
weights,
mu_eff,
c_1,
c_mu,
c_sigma,
d_sigma,
c_c,
chi_n,
generation: 0,
evaluations: 0,
best_fitness: f64::INFINITY,
best_solution: initial_mean,
}
}
pub fn sample_population<R: Rng>(&self, rng: &mut R) -> Vec<RealVector> {
let n = self.dimension;
let normal = StandardNormal;
(0..self.lambda)
.map(|_| {
let z: Vec<f64> = (0..n).map(|_| normal.sample(rng)).collect();
let y: Vec<f64> = self.transform_sample(&z);
let genes: Vec<f64> = self
.mean
.iter()
.zip(y.iter())
.map(|(&m, &yi)| m + self.sigma * yi)
.collect();
RealVector::new(genes)
})
.collect()
}
fn transform_sample(&self, z: &[f64]) -> Vec<f64> {
let n = self.dimension;
let mut y = vec![0.0; n];
for i in 0..n {
for j in 0..n {
y[i] += self.eigenvectors[i][j] * self.eigenvalues[j].sqrt() * z[j];
}
}
y
}
pub fn update(&mut self, offspring: &[(RealVector, f64)]) {
let n = self.dimension;
let selected: Vec<&(RealVector, f64)> = offspring.iter().take(self.mu).collect();
let mut y_w = vec![0.0; n];
for (i, (genome, _fitness)) in selected.iter().enumerate() {
let genes = genome.genes();
for j in 0..n {
y_w[j] += self.weights[i] * (genes[j] - self.mean[j]) / self.sigma;
}
}
let mut bd_inv_yw = vec![0.0; n];
{
let mut temp = vec![0.0; n];
for i in 0..n {
for j in 0..n {
temp[i] += self.eigenvectors[j][i] * y_w[j];
}
temp[i] /= self.eigenvalues[i].sqrt().max(1e-16);
}
for i in 0..n {
for j in 0..n {
bd_inv_yw[i] += self.eigenvectors[i][j] * temp[j];
}
}
}
let c_sigma_factor = (self.c_sigma * (2.0 - self.c_sigma) * self.mu_eff).sqrt();
for i in 0..n {
self.path_sigma[i] =
(1.0 - self.c_sigma) * self.path_sigma[i] + c_sigma_factor * bd_inv_yw[i];
}
let path_sigma_norm_sq: f64 = self.path_sigma.iter().map(|x| x * x).sum();
let path_sigma_norm = path_sigma_norm_sq.sqrt();
let h_sigma = if path_sigma_norm
/ (1.0 - (1.0 - self.c_sigma).powi((2 * (self.generation + 1)) as i32)).sqrt()
/ self.chi_n
< 1.4 + 2.0 / (n as f64 + 1.0)
{
1.0
} else {
0.0
};
let c_c_factor = (self.c_c * (2.0 - self.c_c) * self.mu_eff).sqrt();
for i in 0..n {
self.path_c[i] = (1.0 - self.c_c) * self.path_c[i] + h_sigma * c_c_factor * y_w[i];
}
let delta_h = (1.0 - h_sigma) * self.c_c * (2.0 - self.c_c);
for i in 0..n {
for j in 0..=i {
self.covariance[i][j] *= 1.0 - self.c_1 - self.c_mu + delta_h * self.c_1;
self.covariance[i][j] += self.c_1 * self.path_c[i] * self.path_c[j];
for k in 0..self.mu {
let y_k: Vec<f64> = selected[k]
.0
.genes()
.iter()
.zip(self.mean.iter())
.map(|(&x, &m)| (x - m) / self.sigma)
.collect();
self.covariance[i][j] += self.c_mu * self.weights[k] * y_k[i] * y_k[j];
}
if i != j {
self.covariance[j][i] = self.covariance[i][j];
}
}
}
for i in 0..n {
self.mean[i] += self.sigma * y_w[i];
}
self.sigma *= ((self.c_sigma / self.d_sigma) * (path_sigma_norm / self.chi_n - 1.0)).exp();
self.generation += 1;
if self.generation - self.eigen_eval >= self.eigen_update_interval() {
self.update_eigensystem();
self.eigen_eval = self.generation;
}
}
pub fn eigen_update_interval(&self) -> usize {
let n = self.dimension as f64;
((1.0 / (10.0 * n * (self.c_1 + self.c_mu))).floor() as usize).max(1)
}
fn update_eigensystem(&mut self) {
let n = self.dimension;
for i in 0..n {
for j in 0..i {
self.covariance[i][j] = (self.covariance[i][j] + self.covariance[j][i]) / 2.0;
self.covariance[j][i] = self.covariance[i][j];
}
}
let (eigenvalues, eigenvectors) = symmetric_eigendecomposition(&self.covariance);
self.eigenvalues = eigenvalues;
self.eigenvectors = eigenvectors;
for ev in &mut self.eigenvalues {
*ev = ev.max(1e-16);
}
}
pub fn has_converged(&self) -> bool {
let max_eigenvalue = self
.eigenvalues
.iter()
.cloned()
.fold(f64::NEG_INFINITY, f64::max);
let min_eigenvalue = self
.eigenvalues
.iter()
.cloned()
.fold(f64::INFINITY, f64::min);
if max_eigenvalue / min_eigenvalue.max(1e-16) > 1e14 {
return true;
}
if self.sigma < 1e-16 {
return true;
}
if self.sigma * max_eigenvalue.sqrt() < 1e-16 {
return true;
}
false
}
}
fn symmetric_eigendecomposition(a: &[Vec<f64>]) -> (Vec<f64>, Vec<Vec<f64>>) {
let n = a.len();
let flat: Vec<f64> = (0..n).flat_map(|i| (0..n).map(move |j| a[i][j])).collect();
let matrix = DMatrix::from_row_slice(n, n, &flat);
let eig = SymmetricEigen::new(matrix);
let eigenvalues: Vec<f64> = eig.eigenvalues.iter().copied().collect();
let eigenvectors: Vec<Vec<f64>> = (0..n)
.map(|i| (0..n).map(|j| eig.eigenvectors[(i, j)]).collect())
.collect();
(eigenvalues, eigenvectors)
}
#[cfg(feature = "parallel")]
impl<F> CmaEsFitness for F
where
F: Fn(&RealVector) -> f64 + Send + Sync,
{
fn evaluate(&self, x: &RealVector) -> f64 {
self(x)
}
}
#[cfg(not(feature = "parallel"))]
impl<F> CmaEsFitness for F
where
F: Fn(&RealVector) -> f64,
{
fn evaluate(&self, x: &RealVector) -> f64 {
self(x)
}
}
#[derive(Clone)]
pub struct CmaEs<F> {
pub state: CmaEsState,
pub bounds: Option<MultiBounds>,
pub boundary_penalty: f64,
_phantom: PhantomData<F>,
}
impl<F: CmaEsFitness> CmaEs<F> {
pub fn new(initial_mean: Vec<f64>, initial_sigma: f64) -> Self {
Self {
state: CmaEsState::new(initial_mean, initial_sigma, None),
bounds: None,
boundary_penalty: 0.0,
_phantom: PhantomData,
}
}
pub fn with_lambda(initial_mean: Vec<f64>, initial_sigma: f64, lambda: usize) -> Self {
Self {
state: CmaEsState::new(initial_mean, initial_sigma, Some(lambda)),
bounds: None,
boundary_penalty: 0.0,
_phantom: PhantomData,
}
}
pub fn with_bounds(mut self, bounds: MultiBounds) -> Self {
self.bounds = Some(bounds);
self
}
pub fn with_boundary_penalty(mut self, weight: f64) -> Self {
self.boundary_penalty = weight;
self
}
pub fn step<R: Rng>(
&mut self,
fitness: &F,
rng: &mut R,
) -> EvoResult<Vec<Individual<RealVector>>> {
let unrepaired = self.state.sample_population(rng);
let mut evaluated: Vec<(RealVector, RealVector, f64)> = unrepaired
.into_iter()
.map(|raw| {
let feasible = match self.bounds {
Some(ref bounds) => {
let mut repaired = raw.clone();
repaired.apply_bounds(bounds);
repaired
}
None => raw.clone(),
};
let mut f = fitness.evaluate(&feasible);
if self.boundary_penalty > 0.0 {
let penalty: f64 = raw
.genes()
.iter()
.zip(feasible.genes().iter())
.map(|(&x, &c)| {
let d = x - c;
d * d
})
.sum();
f += self.boundary_penalty * penalty;
}
(raw, feasible, f)
})
.collect();
self.state.evaluations += evaluated.len();
evaluated.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal));
let update_input: Vec<(RealVector, f64)> = evaluated
.iter()
.map(|(raw, _feasible, f)| (raw.clone(), *f))
.collect();
self.state.update(&update_input);
if let Some((_, feasible, f)) = evaluated.first() {
if *f < self.state.best_fitness {
self.state.best_fitness = *f;
self.state.best_solution = feasible.genes().to_vec();
}
}
let individuals: Vec<Individual<RealVector>> = evaluated
.into_iter()
.map(|(_raw, feasible, f)| Individual::with_fitness(feasible, f))
.collect();
Ok(individuals)
}
pub fn run_generations<R: Rng>(
&mut self,
fitness: &F,
max_generations: usize,
rng: &mut R,
) -> EvoResult<Individual<RealVector>> {
let mut best: Option<Individual<RealVector>> = None;
for _ in 0..max_generations {
let population = self.step(fitness, rng)?;
if let Some(current_best) = population.first() {
match &best {
None => best = Some(current_best.clone()),
Some(existing) => {
if current_best.fitness_f64() < existing.fitness_f64() {
best = Some(current_best.clone());
}
}
}
}
if self.state.has_converged() {
break;
}
}
best.ok_or(EvolutionError::EmptyPopulation)
}
pub fn run_until<R: Rng>(
&mut self,
fitness: &F,
target_fitness: f64,
max_generations: usize,
rng: &mut R,
) -> EvoResult<Individual<RealVector>> {
let mut best: Option<Individual<RealVector>> = None;
for _ in 0..max_generations {
let population = self.step(fitness, rng)?;
if let Some(current_best) = population.first() {
match &best {
None => best = Some(current_best.clone()),
Some(existing) => {
if current_best.fitness_f64() < existing.fitness_f64() {
best = Some(current_best.clone());
}
}
}
}
if let Some(ref b) = best {
if b.fitness_f64() <= target_fitness {
break;
}
}
if self.state.has_converged() {
break;
}
}
best.ok_or(EvolutionError::EmptyPopulation)
}
pub fn generation(&self) -> usize {
self.state.generation
}
pub fn evaluations(&self) -> usize {
self.state.evaluations
}
pub fn mean(&self) -> &[f64] {
&self.state.mean
}
pub fn sigma(&self) -> f64 {
self.state.sigma
}
pub fn best_solution(&self) -> &[f64] {
&self.state.best_solution
}
pub fn best_fitness(&self) -> f64 {
self.state.best_fitness
}
}
pub struct CmaEsBuilder {
initial_mean: Option<Vec<f64>>,
initial_sigma: f64,
lambda: Option<usize>,
bounds: Option<MultiBounds>,
boundary_penalty: f64,
}
impl CmaEsBuilder {
pub fn new() -> Self {
Self {
initial_mean: None,
initial_sigma: 1.0,
lambda: None,
bounds: None,
boundary_penalty: 0.0,
}
}
pub fn boundary_penalty(mut self, weight: f64) -> Self {
self.boundary_penalty = weight;
self
}
pub fn mean(mut self, mean: Vec<f64>) -> Self {
self.initial_mean = Some(mean);
self
}
pub fn sigma(mut self, sigma: f64) -> Self {
self.initial_sigma = sigma;
self
}
pub fn lambda(mut self, lambda: usize) -> Self {
self.lambda = Some(lambda);
self
}
pub fn bounds(mut self, bounds: MultiBounds) -> Self {
self.bounds = Some(bounds);
self
}
pub fn build<F: CmaEsFitness>(self) -> EvoResult<CmaEs<F>> {
let mean = self
.initial_mean
.ok_or_else(|| EvolutionError::Configuration("Initial mean not set".to_string()))?;
let mut cmaes = match self.lambda {
Some(l) => CmaEs::with_lambda(mean, self.initial_sigma, l),
None => CmaEs::new(mean, self.initial_sigma),
};
if let Some(bounds) = self.bounds {
cmaes = cmaes.with_bounds(bounds);
}
cmaes.boundary_penalty = self.boundary_penalty;
Ok(cmaes)
}
}
impl Default for CmaEsBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::genome::traits::EvolutionaryGenome;
use approx::assert_relative_eq;
struct Sphere;
impl CmaEsFitness for Sphere {
fn evaluate(&self, genome: &RealVector) -> f64 {
genome.genes().iter().map(|x| x * x).sum()
}
}
#[test]
fn test_cmaes_state_initialization() {
let mean = vec![0.0, 0.0, 0.0];
let state = CmaEsState::new(mean.clone(), 1.0, None);
assert_eq!(state.dimension, 3);
assert_eq!(state.mean, mean);
assert_eq!(state.sigma, 1.0);
assert!(state.lambda >= 4);
assert!(state.mu > 0);
assert_eq!(state.weights.len(), state.mu);
}
#[test]
fn test_cmaes_weights_sum_to_one() {
let state = CmaEsState::new(vec![0.0; 10], 1.0, None);
let sum: f64 = state.weights.iter().sum();
assert_relative_eq!(sum, 1.0, epsilon = 1e-10);
}
#[test]
fn test_cmaes_sampling() {
let mut rng = rand::thread_rng();
let state = CmaEsState::new(vec![0.0; 5], 1.0, Some(10));
let samples = state.sample_population(&mut rng);
assert_eq!(samples.len(), 10);
for sample in &samples {
assert_eq!(sample.dimension(), 5);
}
}
#[test]
fn test_cmaes_step() {
let mut rng = rand::thread_rng();
let fitness = Sphere;
let mut cmaes: CmaEs<Sphere> = CmaEs::new(vec![5.0, 5.0, 5.0], 2.0);
let population = cmaes.step(&fitness, &mut rng).unwrap();
assert_eq!(population.len(), cmaes.state.lambda);
assert_eq!(cmaes.state.generation, 1);
}
#[test]
fn test_cmaes_optimization() {
use rand::SeedableRng;
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
let fitness = Sphere;
let mut cmaes: CmaEs<Sphere> = CmaEs::new(vec![5.0, 5.0], 2.0);
let result = cmaes.run_generations(&fitness, 150, &mut rng).unwrap();
let final_fitness = result.fitness_f64();
let initial_fitness = 50.0; assert!(
final_fitness < initial_fitness * 0.7,
"Final fitness {} should be significantly better than initial {}",
final_fitness,
initial_fitness
);
}
#[test]
fn test_cmaes_with_bounds() {
let mut rng = rand::thread_rng();
let fitness = Sphere;
let bounds = MultiBounds::symmetric(10.0, 3);
let mut cmaes: CmaEs<Sphere> = CmaEs::new(vec![5.0, 5.0, 5.0], 2.0).with_bounds(bounds);
let result = cmaes.run_generations(&fitness, 30, &mut rng).unwrap();
for gene in result.genome().genes() {
assert!(*gene >= -10.0 && *gene <= 10.0);
}
}
#[test]
fn test_cmaes_builder() {
let cmaes: CmaEs<Sphere> = CmaEsBuilder::new()
.mean(vec![0.0, 0.0, 0.0])
.sigma(0.5)
.lambda(20)
.bounds(MultiBounds::symmetric(5.0, 3))
.build()
.unwrap();
assert_eq!(cmaes.state.lambda, 20);
assert_eq!(cmaes.state.sigma, 0.5);
assert!(cmaes.bounds.is_some());
}
#[test]
fn test_cmaes_convergence_detection() {
let mut state = CmaEsState::new(vec![0.0, 0.0], 1e-20, None);
assert!(state.has_converged());
state.sigma = 1.0;
state.eigenvalues = vec![1e15, 1.0];
assert!(state.has_converged());
}
fn rosenbrock(x: &RealVector) -> f64 {
x.genes()
.windows(2)
.map(|w| 100.0 * (w[1] - w[0] * w[0]).powi(2) + (1.0 - w[0]).powi(2))
.sum()
}
fn random_spd(n: usize, rng: &mut impl Rng) -> Vec<Vec<f64>> {
let a: Vec<Vec<f64>> = (0..n)
.map(|_| {
(0..n)
.map(|_| rng.gen_range(-1.0..1.0))
.collect::<Vec<f64>>()
})
.collect();
let mut c = vec![vec![0.0; n]; n];
for (i, ci) in c.iter_mut().enumerate() {
for (j, cij) in ci.iter_mut().enumerate() {
let mut s = 0.0;
for k in 0..n {
s += a[i][k] * a[j][k];
}
*cij = s;
}
}
for (i, ci) in c.iter_mut().enumerate() {
ci[i] += n as f64;
}
c
}
#[test]
fn test_eigendecomposition_known_matrix() {
let a = vec![vec![4.0, 1.0], vec![1.0, 3.0]];
let (eigenvalues, eigenvectors) = symmetric_eigendecomposition(&a);
let mut sorted = eigenvalues.clone();
sorted.sort_by(|x, y| x.partial_cmp(y).unwrap());
assert_relative_eq!(sorted[0], (7.0 - 5.0_f64.sqrt()) / 2.0, epsilon = 1e-9);
assert_relative_eq!(sorted[1], (7.0 + 5.0_f64.sqrt()) / 2.0, epsilon = 1e-9);
assert!(eigenvalues.iter().all(|&l| l > 0.0));
for j in 0..2 {
let v: Vec<f64> = (0..2).map(|i| eigenvectors[i][j]).collect();
for i in 0..2 {
let cv: f64 = (0..2).map(|k| a[i][k] * v[k]).sum();
assert_relative_eq!(cv, eigenvalues[j] * v[i], epsilon = 1e-9);
}
}
}
#[test]
fn test_eigendecomposition_random_spd() {
use rand::SeedableRng;
let mut rng = rand::rngs::StdRng::seed_from_u64(2024);
for &n in &[2usize, 3, 5, 8] {
let c = random_spd(n, &mut rng);
let (eigenvalues, eigenvectors) = symmetric_eigendecomposition(&c);
assert!(
eigenvalues.iter().all(|&l| l > 0.0),
"SPD matrix must have all-positive eigenvalues, got {:?}",
eigenvalues
);
for j in 0..n {
let v: Vec<f64> = (0..n).map(|i| eigenvectors[i][j]).collect();
for i in 0..n {
let cv: f64 = (0..n).map(|k| c[i][k] * v[k]).sum();
assert!(
(cv - eigenvalues[j] * v[i]).abs() < 1e-9,
"n={}: C·v_{} component {} mismatch",
n,
j,
i
);
}
}
for i in 0..n {
for j in 0..n {
let recon: f64 = (0..n)
.map(|k| eigenvectors[i][k] * eigenvalues[k] * eigenvectors[j][k])
.sum();
assert!(
(recon - c[i][j]).abs() < 1e-9,
"n={}: reconstruction mismatch at ({},{})",
n,
i,
j
);
}
}
}
}
#[test]
fn test_cmaes_rosenbrock_convergence() {
use rand::SeedableRng;
let mut rng = rand::rngs::StdRng::seed_from_u64(7);
let mut cmaes: CmaEs<_> = CmaEs::with_lambda(vec![0.0; 5], 0.5, 20);
let best = cmaes.run_until(&rosenbrock, 1e-6, 4000, &mut rng).unwrap();
assert!(
best.fitness_f64() < 1e-6,
"CMA-ES should reach f < 1e-6 on 5-D Rosenbrock, got {}",
best.fitness_f64()
);
}
#[test]
fn test_eigen_recompute_cadence() {
use rand::SeedableRng;
let mut rng = rand::rngs::StdRng::seed_from_u64(3);
let fitness = Sphere;
let n = 100;
let mut cmaes: CmaEs<Sphere> = CmaEs::new(vec![1.0; n], 1.0);
assert_eq!(cmaes.state.eigen_update_interval(), 1);
let gens = 30;
let mut recomputes = 0;
let mut last = cmaes.state.eigen_eval;
for _ in 0..gens {
cmaes.step(&fitness, &mut rng).unwrap();
if cmaes.state.eigen_eval != last {
recomputes += 1;
last = cmaes.state.eigen_eval;
}
}
assert!(
recomputes >= gens - 1,
"expected ~every-generation eigen recompute for n=100, got {} in {} gens",
recomputes,
gens
);
}
#[test]
fn test_cmaes_update_uses_unrepaired_samples() {
use crate::genome::bounds::Bounds;
use rand::SeedableRng;
let mut rng = rand::rngs::StdRng::seed_from_u64(11);
let fitness = Sphere;
let bounds = MultiBounds::uniform(Bounds::new(2.9, 3.1), 2);
let mut cmaes: CmaEs<Sphere> = CmaEs::new(vec![0.0, 0.0], 1.0).with_bounds(bounds);
cmaes.step(&fitness, &mut rng).unwrap();
for &m in &cmaes.state.mean {
assert!(
m.abs() < 2.0,
"mean component {} was pulled onto the clamped boundary (~2.9); \
the distribution update must use unrepaired samples",
m
);
}
for &g in &cmaes.state.best_solution {
assert!((2.9..=3.1).contains(&g));
}
}
#[test]
fn test_cmaes_boundary_penalty_option() {
use crate::genome::bounds::Bounds;
use rand::SeedableRng;
let mut rng = rand::rngs::StdRng::seed_from_u64(5);
let fitness = Sphere;
let bounds = MultiBounds::uniform(Bounds::new(-1.0, 1.0), 3);
let mut cmaes: CmaEs<Sphere> = CmaEs::new(vec![0.0; 3], 2.0)
.with_bounds(bounds)
.with_boundary_penalty(10.0);
assert_eq!(cmaes.boundary_penalty, 10.0);
cmaes.step(&fitness, &mut rng).unwrap();
for &g in &cmaes.state.best_solution {
assert!((-1.0..=1.0).contains(&g));
}
}
}