use std::marker::PhantomData;
use burn::tensor::{Tensor, TensorData, backend::Backend};
use rand::Rng;
use rand::RngExt;
use rlevo_core::bounds::Bounds;
use rlevo_core::config::{self, ConfigError, ConstraintKind, Validate, Violations};
use crate::ops::linalg::{SymEigen, jacobi_eigen, matvec, symmetrize};
use crate::rng::{SeedPurpose, seed_stream};
use crate::strategy::{Strategy, StrategyMetrics};
const EIGENVALUE_FLOOR: f32 = 1e-20;
const CONDITION_FLOOR: f32 = 1e-14;
fn eigenvalue_floor(eigvals: &[f32]) -> f32 {
let lmax: f32 = eigvals.iter().copied().fold(0.0_f32, f32::max);
(lmax * CONDITION_FLOOR).max(EIGENVALUE_FLOOR)
}
#[derive(Debug, Clone)]
pub struct CmaEsConfig {
pub pop_size: usize,
pub genome_dim: usize,
pub bounds: Bounds,
pub initial_sigma: f32,
pub mu: usize,
pub weights: Vec<f32>,
pub mu_eff: f32,
pub c_sigma: f32,
pub d_sigma: f32,
pub c_c: f32,
pub c_1: f32,
pub c_mu: f32,
pub chi_n: f32,
}
impl CmaEsConfig {
#[must_use]
pub fn default_for(genome_dim: usize) -> Self {
#[allow(clippy::cast_precision_loss)]
let d = genome_dim as f32;
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let lambda = 4 + (3.0 * d.ln()).floor() as usize;
Self::with_pop_size(lambda, genome_dim)
}
#[must_use]
pub fn with_pop_size(pop_size: usize, genome_dim: usize) -> Self {
#[allow(clippy::cast_precision_loss)]
let d = genome_dim as f32;
let mu: usize = pop_size / 2;
let raw: Vec<f32> = (1..=mu)
.map(|i| {
#[allow(clippy::cast_precision_loss)]
let fi = i as f32;
#[allow(clippy::cast_precision_loss)]
let mu_f = mu as f32;
(mu_f + 0.5).ln() - fi.ln()
})
.collect();
let sum: f32 = raw.iter().sum();
let weights: Vec<f32> = raw.iter().map(|w| w / sum).collect();
let sum_sq: f32 = weights.iter().map(|w| w * w).sum();
let mu_eff: f32 = 1.0 / sum_sq;
let c_sigma: f32 = (mu_eff + 2.0) / (d + mu_eff + 5.0);
let d_sigma: f32 =
1.0 + 2.0 * (((mu_eff - 1.0) / (d + 1.0)).sqrt() - 1.0).max(0.0) + c_sigma;
let c_c: f32 = (4.0 + mu_eff / d) / (d + 4.0 + 2.0 * mu_eff / d);
let c_1: f32 = 2.0 / ((d + 1.3) * (d + 1.3) + mu_eff);
let c_mu: f32 =
(1.0 - c_1).min(2.0 * (mu_eff - 2.0 + 1.0 / mu_eff) / ((d + 2.0) * (d + 2.0) + mu_eff));
let chi_n: f32 = d.sqrt() * (1.0 - 1.0 / (4.0 * d) + 1.0 / (21.0 * d * d));
Self {
pop_size,
genome_dim,
bounds: Bounds::new(-5.12, 5.12),
initial_sigma: 1.0,
mu,
weights,
mu_eff,
c_sigma,
d_sigma,
c_c,
c_1,
c_mu,
chi_n,
}
}
}
impl Validate for CmaEsConfig {
fn validate(&self) -> Result<(), ConfigError> {
self.validate_all().map_err(|mut errs| errs.remove(0))
}
fn validate_all(&self) -> Result<(), Vec<ConfigError>> {
const C: &str = "CmaEsConfig";
let mut v = Violations::new();
v.check(config::at_least(C, "pop_size", self.pop_size, 2));
v.check(config::nonzero(C, "genome_dim", self.genome_dim));
v.check(config::positive(
C,
"initial_sigma",
f64::from(self.initial_sigma),
));
v.check(config::at_least(C, "mu", self.mu, 1));
if self.mu > self.pop_size {
v.check(Err(ConfigError {
config: C,
field: "mu",
kind: ConstraintKind::Custom("mu must not exceed pop_size"),
}));
}
if self.weights.len() != self.mu {
v.check(Err(ConfigError {
config: C,
field: "weights",
kind: ConstraintKind::Custom("weights length must equal mu"),
}));
}
if !self.weights.iter().all(|w| *w > 0.0) {
v.check(Err(ConfigError {
config: C,
field: "weights",
kind: ConstraintKind::Custom("recombination weights must all be positive"),
}));
}
let weight_sum = f64::from(self.weights.iter().sum::<f32>());
v.check(config::in_range(
C,
"weights",
1.0 - 1e-3,
1.0 + 1e-3,
weight_sum,
));
v.check(config::in_range(
C,
"mu_eff",
1.0,
f64::INFINITY,
f64::from(self.mu_eff),
));
v.check(config::positive(C, "d_sigma", f64::from(self.d_sigma)));
v.check(config::positive(C, "chi_n", f64::from(self.chi_n)));
v.check(config::in_range(
C,
"c_sigma",
0.0,
1.0,
f64::from(self.c_sigma),
));
v.check(config::in_range(C, "c_c", 0.0, 1.0, f64::from(self.c_c)));
v.check(config::in_range(C, "c_1", 0.0, 1.0, f64::from(self.c_1)));
v.check(config::in_range(C, "c_mu", 0.0, 1.0, f64::from(self.c_mu)));
v.check(config::in_range(
C,
"c_1_plus_c_mu",
0.0,
1.0,
f64::from(self.c_1) + f64::from(self.c_mu),
));
v.into_result()
}
}
#[derive(Debug, Clone)]
pub struct CmaEsState<B: Backend> {
mean: Vec<f32>,
cov: Vec<f32>,
p_sigma: Vec<f32>,
p_c: Vec<f32>,
sigma: f32,
generation: usize,
best_genome: Option<Tensor<B, 2>>,
best_fitness: f32,
eig: Option<SymEigen>,
}
impl<B: Backend> CmaEsState<B> {
#[allow(clippy::too_many_arguments)]
pub fn try_new(
mean: Vec<f32>,
mut cov: Vec<f32>,
p_sigma: Vec<f32>,
p_c: Vec<f32>,
sigma: f32,
generation: usize,
best_genome: Option<Tensor<B, 2>>,
best_fitness: f32,
) -> Result<Self, ConfigError> {
let d = mean.len();
config::nonzero("CmaEsState", "mean", d)?;
if cov.len() != d * d {
return Err(ConfigError {
config: "CmaEsState",
field: "cov",
kind: ConstraintKind::Custom("covariance must be a row-major D × D matrix"),
});
}
if p_sigma.len() != d {
return Err(ConfigError {
config: "CmaEsState",
field: "p_sigma",
kind: ConstraintKind::Custom("evolution path length must equal D"),
});
}
if p_c.len() != d {
return Err(ConfigError {
config: "CmaEsState",
field: "p_c",
kind: ConstraintKind::Custom("evolution path length must equal D"),
});
}
config::positive("CmaEsState", "sigma", f64::from(sigma))?;
symmetrize(&mut cov, d);
Ok(Self {
mean,
cov,
p_sigma,
p_c,
sigma,
generation,
best_genome,
best_fitness,
eig: None,
})
}
#[must_use]
pub fn mean(&self) -> &[f32] {
&self.mean
}
#[must_use]
pub fn cov(&self) -> &[f32] {
&self.cov
}
#[must_use]
pub fn p_sigma(&self) -> &[f32] {
&self.p_sigma
}
#[must_use]
pub fn p_c(&self) -> &[f32] {
&self.p_c
}
#[must_use]
pub fn sigma(&self) -> f32 {
self.sigma
}
#[must_use]
pub fn generation(&self) -> usize {
self.generation
}
#[must_use]
pub fn best_genome(&self) -> Option<&Tensor<B, 2>> {
self.best_genome.as_ref()
}
#[must_use]
pub fn best_fitness(&self) -> f32 {
self.best_fitness
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CmaEs<B: Backend> {
_backend: PhantomData<fn() -> B>,
}
impl<B: Backend> CmaEs<B> {
#[must_use]
pub fn new() -> Self {
Self {
_backend: PhantomData,
}
}
}
impl<B: Backend> Strategy<B> for CmaEs<B>
where
B::Device: Clone,
{
type Params = CmaEsConfig;
type State = CmaEsState<B>;
type Genome = Tensor<B, 2>;
fn init(
&self,
params: &CmaEsConfig,
rng: &mut dyn Rng,
_device: &<B as burn::tensor::backend::BackendTypes>::Device,
) -> CmaEsState<B> {
debug_assert!(
params.validate().is_ok(),
"invalid CmaEsConfig reached init: {params:?}"
);
let d = params.genome_dim;
let (lo, hi): (f32, f32) = params.bounds.into();
let mut stream = seed_stream(rng.next_u64(), 0, SeedPurpose::Init);
let mean: Vec<f32> = (0..d)
.map(|_| lo + (hi - lo) * stream.random::<f32>())
.collect();
let mut cov: Vec<f32> = vec![0.0; d * d];
for i in 0..d {
cov[i * d + i] = 1.0;
}
CmaEsState {
mean,
cov,
p_sigma: vec![0.0; d],
p_c: vec![0.0; d],
sigma: params.initial_sigma,
generation: 0,
best_genome: None,
best_fitness: f32::NEG_INFINITY,
eig: None,
}
}
fn ask(
&self,
params: &CmaEsConfig,
state: &CmaEsState<B>,
rng: &mut dyn Rng,
device: &<B as burn::tensor::backend::BackendTypes>::Device,
) -> (Tensor<B, 2>, CmaEsState<B>) {
let d = params.genome_dim;
let lambda = params.pop_size;
let eig: SymEigen = jacobi_eigen(&state.cov, d);
let floor: f32 = eigenvalue_floor(&eig.values);
let mut bd: Vec<f32> = vec![0.0; d * d];
for i in 0..d {
for k in 0..d {
bd[i * d + k] = eig.vectors[i * d + k] * eig.values[k].max(floor).sqrt();
}
}
let mut stream = seed_stream(
rng.next_u64(),
state.generation as u64,
SeedPurpose::CmaSampling,
);
let mut rows: Vec<f32> = Vec::with_capacity(lambda * d);
for _ in 0..lambda {
let z: Vec<f32> = (0..d)
.map(|_| crate::sampling::standard_normal(&mut stream))
.collect();
let bdz: Vec<f32> = matvec(&bd, &z, d);
for (mean_i, bdz_i) in state.mean.iter().zip(bdz.iter()) {
rows.push(mean_i + state.sigma * bdz_i);
}
}
let population = Tensor::<B, 2>::from_data(TensorData::new(rows, [lambda, d]), device);
let mut next = state.clone();
next.eig = Some(eig);
(population, next)
}
#[allow(clippy::too_many_lines, clippy::cast_precision_loss)]
fn tell(
&self,
params: &CmaEsConfig,
population: Tensor<B, 2>,
fitness: Tensor<B, 1>,
mut state: CmaEsState<B>,
_rng: &mut dyn Rng,
) -> (CmaEsState<B>, StrategyMetrics) {
let d = params.genome_dim;
let lambda = params.pop_size;
let mu = params.mu;
let fitness_host: Vec<f32> = fitness
.into_data()
.into_vec::<f32>()
.expect("fitness tensor must be readable as f32");
let pop_host: Vec<f32> = population
.clone()
.into_data()
.into_vec::<f32>()
.expect("population tensor must be readable as f32");
let mut ranked: Vec<usize> = (0..lambda).collect();
let sane: Vec<f32> = fitness_host
.iter()
.map(|&f| crate::fitness::sanitize_fitness(f))
.collect();
let n_finite: usize = sane.iter().filter(|f| f.is_finite()).count();
if n_finite < mu {
update_best(&mut state, &population, &fitness_host);
state.generation += 1;
let metrics = StrategyMetrics::from_host_fitness(
state.generation,
&fitness_host,
state.best_fitness,
);
state.best_fitness = metrics.best_fitness_ever();
return (state, metrics);
}
ranked.sort_by(|&a, &b| sane[b].total_cmp(&sane[a]));
let m_old: Vec<f32> = state.mean.clone();
let sigma_old: f32 = state.sigma;
let mut y_sel: Vec<Vec<f32>> = Vec::with_capacity(mu);
let mut y_w: Vec<f32> = vec![0.0; d];
for (&idx, &w) in ranked.iter().take(mu).zip(params.weights.iter()) {
let mut yi: Vec<f32> = vec![0.0; d];
for i in 0..d {
yi[i] = (pop_host[idx * d + i] - m_old[i]) / sigma_old;
y_w[i] += w * yi[i];
}
y_sel.push(yi);
}
let mut mean_new: Vec<f32> = vec![0.0; d];
for i in 0..d {
mean_new[i] = m_old[i] + sigma_old * y_w[i];
}
let SymEigen {
values: eigvals,
vectors: eigvecs,
} = state
.eig
.take()
.unwrap_or_else(|| jacobi_eigen(&state.cov, d));
let floor: f32 = eigenvalue_floor(&eigvals);
let inv_sqrt: Vec<f32> = eigvals.iter().map(|&l| 1.0 / l.max(floor).sqrt()).collect();
let mut c_inv_sqrt: Vec<f32> = vec![0.0; d * d];
for i in 0..d {
for j in 0..d {
let mut acc: f32 = 0.0;
for k in 0..d {
acc += eigvecs[i * d + k] * inv_sqrt[k] * eigvecs[j * d + k];
}
c_inv_sqrt[i * d + j] = acc;
}
}
let cs_factor: f32 = (params.c_sigma * (2.0 - params.c_sigma) * params.mu_eff).sqrt();
let c_inv_yw: Vec<f32> = matvec(&c_inv_sqrt, &y_w, d);
let mut p_sigma: Vec<f32> = vec![0.0; d];
for i in 0..d {
p_sigma[i] = (1.0 - params.c_sigma) * state.p_sigma[i] + cs_factor * c_inv_yw[i];
}
let p_sigma_norm: f32 = p_sigma.iter().map(|v| v * v).sum::<f32>().sqrt();
let sigma_new: f32 = (sigma_old
* ((params.c_sigma / params.d_sigma) * (p_sigma_norm / params.chi_n - 1.0)).exp())
.max(f32::MIN_POSITIVE);
let gen_count: f32 = state.generation as f32 + 1.0;
let denom: f32 = (1.0 - (1.0 - params.c_sigma).powf(2.0 * gen_count)).sqrt();
let h_sigma: f32 = if p_sigma_norm / denom
< (1.4 + 2.0 / (params.genome_dim as f32 + 1.0)) * params.chi_n
{
1.0
} else {
0.0
};
let pc_factor: f32 = (params.c_c * (2.0 - params.c_c) * params.mu_eff).sqrt();
let mut p_c: Vec<f32> = vec![0.0; d];
for i in 0..d {
p_c[i] = (1.0 - params.c_c) * state.p_c[i] + h_sigma * pc_factor * y_w[i];
}
let delta_h: f32 = (1.0 - h_sigma) * params.c_c * (2.0 - params.c_c);
let c_old: Vec<f32> = state.cov.clone();
let mut cov_new: Vec<f32> = vec![0.0; d * d];
for i in 0..d {
for j in 0..d {
let decay: f32 = 1.0 - params.c_1 - params.c_mu;
let rank1: f32 = params.c_1 * (p_c[i] * p_c[j] + delta_h * c_old[i * d + j]);
let mut rankmu: f32 = 0.0;
for (rank, yi) in y_sel.iter().enumerate() {
rankmu += params.weights[rank] * (yi[i] * yi[j]);
}
rankmu *= params.c_mu;
cov_new[i * d + j] = decay * c_old[i * d + j] + rank1 + rankmu;
}
}
symmetrize(&mut cov_new, d);
update_best(&mut state, &population, &fitness_host);
state.generation += 1;
let metrics =
StrategyMetrics::from_host_fitness(state.generation, &fitness_host, state.best_fitness);
state.best_fitness = metrics.best_fitness_ever();
state.mean = mean_new;
state.cov = cov_new;
state.p_sigma = p_sigma;
state.p_c = p_c;
state.sigma = sigma_new;
(state, metrics)
}
fn best(&self, state: &CmaEsState<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 CmaEsState<B>, pop: &Tensor<B, 2>, fitness: &[f32]) {
if fitness.is_empty() {
return;
}
let mut best_idx: usize = 0;
let mut best: f32 = f32::NEG_INFINITY;
for (i, &f) in fitness.iter().enumerate() {
if f > best {
best = f;
best_idx = i;
}
}
if best > 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;
}
}
#[cfg(test)]
mod tests {
use super::*;
use burn::backend::Flex;
use proptest::prelude::*;
use rand::SeedableRng;
use rand::rngs::StdRng;
#[test]
fn try_new_checks_dimensions() {
assert!(
CmaEsState::<Flex>::try_new(
vec![0.0, 0.0],
vec![1.0, 0.0, 0.0, 1.0],
vec![0.0, 0.0],
vec![0.0, 0.0],
0.5,
0,
None,
f32::MIN,
)
.is_ok()
);
assert!(
CmaEsState::<Flex>::try_new(
vec![0.0, 0.0],
vec![1.0, 0.0, 0.0],
vec![0.0, 0.0],
vec![0.0, 0.0],
0.5,
0,
None,
f32::MIN,
)
.is_err()
);
assert!(
CmaEsState::<Flex>::try_new(
vec![0.0, 0.0],
vec![1.0, 0.0, 0.0, 1.0],
vec![0.0, 0.0],
vec![0.0, 0.0],
0.0,
0,
None,
f32::MIN,
)
.is_err()
);
}
#[test]
fn default_config_validates() {
assert!(CmaEsConfig::default_for(10).validate().is_ok());
}
#[test]
fn rejects_pop_size_below_two() {
let mut cfg = CmaEsConfig::default_for(10);
cfg.pop_size = 1;
assert_eq!(cfg.validate().unwrap_err().field, "pop_size");
}
#[test]
fn default_config_validates_all() {
assert!(CmaEsConfig::default_for(10).validate_all().is_ok());
}
#[test]
fn rejects_desynced_weights() {
let mut cfg = CmaEsConfig::default_for(10);
cfg.weights.pop();
let err = cfg.validate().unwrap_err();
assert_eq!(err.field, "weights");
}
#[test]
fn rejects_diverging_covariance_rates() {
let mut cfg = CmaEsConfig::default_for(10);
cfg.c_1 = 0.7;
cfg.c_mu = 0.7;
let err = cfg.validate().unwrap_err();
assert_eq!(err.field, "c_1_plus_c_mu");
}
#[test]
fn validate_all_reports_every_violation() {
let mut cfg = CmaEsConfig::default_for(10);
cfg.weights.pop(); cfg.d_sigma = -1.0; cfg.c_1 = 0.7;
cfg.c_mu = 0.7; let errs = cfg.validate_all().unwrap_err();
let fields: Vec<&str> = errs.iter().map(|e| e.field).collect();
assert!(fields.contains(&"weights"));
assert!(fields.contains(&"d_sigma"));
assert!(fields.contains(&"c_1_plus_c_mu"));
assert!(errs.len() >= 3, "expected all violations, got {fields:?}");
assert_eq!(cfg.validate().unwrap_err(), errs[0]);
}
#[test]
fn default_for_d10_constants() {
let cfg = CmaEsConfig::default_for(10);
assert_eq!(cfg.pop_size, 10);
assert_eq!(cfg.mu, 5);
assert_eq!(cfg.weights.len(), 5);
let sum: f32 = cfg.weights.iter().sum();
approx::assert_relative_eq!(sum, 1.0, epsilon = 1e-5);
for pair in cfg.weights.windows(2) {
assert!(pair[0] >= pair[1], "weights must be descending");
}
assert!(
cfg.mu_eff > 1.0 && cfg.mu_eff <= 5.0,
"mu_eff = {}",
cfg.mu_eff
);
assert!(cfg.c_sigma > 0.0 && cfg.c_sigma < 1.0);
assert!(cfg.d_sigma >= 1.0);
assert!(cfg.c_c > 0.0 && cfg.c_c < 1.0);
assert!(cfg.c_1 > 0.0 && cfg.c_1 < 1.0);
assert!(cfg.c_mu > 0.0);
assert!(cfg.c_1 + cfg.c_mu <= 1.0, "c_1 + c_mu must not exceed 1");
approx::assert_relative_eq!(cfg.chi_n, 3.084_7_f32, epsilon = 1e-3);
}
#[test]
fn with_pop_size_scales_mu() {
let cfg = CmaEsConfig::with_pop_size(50, 10);
assert_eq!(cfg.pop_size, 50);
assert_eq!(cfg.mu, 25);
let sum: f32 = cfg.weights.iter().sum();
approx::assert_relative_eq!(sum, 1.0, epsilon = 1e-5);
}
#[test]
fn tell_freezes_distribution_on_too_few_finite() {
let strategy = CmaEs::<Flex>::new();
let params = CmaEsConfig::with_pop_size(6, 2); assert_eq!(params.mu, 3);
let device = Default::default();
let mut rng = StdRng::seed_from_u64(0xF10E);
let state = strategy.init(¶ms, &mut rng, &device);
let (population, asked) = strategy.ask(¶ms, &state, &mut rng, &device);
let mean0: Vec<f32> = asked.mean().to_vec();
let cov0: Vec<f32> = asked.cov().to_vec();
let p_sigma0: Vec<f32> = asked.p_sigma().to_vec();
let p_c0: Vec<f32> = asked.p_c().to_vec();
let sigma0: f32 = asked.sigma();
let gen0: usize = asked.generation();
let fitness = Tensor::<Flex, 1>::from_data(
TensorData::new(
vec![1.0f32, f32::NAN, f32::NAN, f32::NAN, f32::NAN, f32::NAN],
[6],
),
&device,
);
let (told, _metrics) = strategy.tell(¶ms, population, fitness, asked, &mut rng);
assert_eq!(told.mean(), mean0.as_slice());
assert_eq!(told.cov(), cov0.as_slice());
assert_eq!(told.p_sigma(), p_sigma0.as_slice());
assert_eq!(told.p_c(), p_c0.as_slice());
assert_eq!(told.sigma().to_bits(), sigma0.to_bits());
assert_eq!(told.generation(), gen0 + 1);
assert_eq!(told.best_fitness().to_bits(), 1.0f32.to_bits());
}
#[test]
fn tell_cache_reuse_matches_recompute() {
let strategy = CmaEs::<Flex>::new();
let params = CmaEsConfig::with_pop_size(6, 2);
let device = Default::default();
let mut rng = StdRng::seed_from_u64(0x00CA_C4E5);
let state = strategy.init(¶ms, &mut rng, &device);
let (population, asked) = strategy.ask(¶ms, &state, &mut rng, &device);
let rebuilt = CmaEsState::<Flex>::try_new(
asked.mean().to_vec(),
asked.cov().to_vec(),
asked.p_sigma().to_vec(),
asked.p_c().to_vec(),
asked.sigma(),
asked.generation(),
asked.best_genome().cloned(),
asked.best_fitness(),
)
.expect("valid state");
let fitness_vals: Vec<f32> = vec![6.0, 5.0, 4.0, 3.0, 2.0, 1.0];
let f_cached =
Tensor::<Flex, 1>::from_data(TensorData::new(fitness_vals.clone(), [6]), &device);
let f_recomp = Tensor::<Flex, 1>::from_data(TensorData::new(fitness_vals, [6]), &device);
let mut rng_a = StdRng::seed_from_u64(1);
let mut rng_b = StdRng::seed_from_u64(2);
let (told_cached, _) =
strategy.tell(¶ms, population.clone(), f_cached, asked, &mut rng_a);
let (told_recomp, _) = strategy.tell(¶ms, population, f_recomp, rebuilt, &mut rng_b);
assert_eq!(told_cached.mean(), told_recomp.mean());
assert_eq!(told_cached.cov(), told_recomp.cov());
assert_eq!(told_cached.p_sigma(), told_recomp.p_sigma());
assert_eq!(told_cached.p_c(), told_recomp.p_c());
assert_eq!(told_cached.sigma().to_bits(), told_recomp.sigma().to_bits());
}
#[test]
fn try_new_symmetrizes_covariance() {
let state = CmaEsState::<Flex>::try_new(
vec![0.0, 0.0],
vec![1.0, 0.4, 0.2, 1.0],
vec![0.0, 0.0],
vec![0.0, 0.0],
0.5,
0,
None,
f32::NEG_INFINITY,
)
.expect("valid state");
let cov: &[f32] = state.cov();
approx::assert_relative_eq!(cov[1], 0.3, epsilon = 1e-6);
approx::assert_relative_eq!(cov[2], 0.3, epsilon = 1e-6);
}
#[test]
fn tell_clears_eig_memo_after_cov_update() {
let strategy = CmaEs::<Flex>::new();
let params = CmaEsConfig::with_pop_size(6, 2);
let device = Default::default();
let mut rng = StdRng::seed_from_u64(0x00EE_6011);
let state = strategy.init(¶ms, &mut rng, &device);
let (population, asked) = strategy.ask(¶ms, &state, &mut rng, &device);
assert!(asked.eig.is_some(), "ask must populate the eig memo");
let fitness = Tensor::<Flex, 1>::from_data(
TensorData::new(vec![6.0f32, 5.0, 4.0, 3.0, 2.0, 1.0], [6]),
&device,
);
let (told, _metrics) = strategy.tell(¶ms, population, fitness, asked, &mut rng);
assert!(
told.eig.is_none(),
"a cov-mutating tell must leave the eig memo empty"
);
}
#[test]
fn two_generation_sequence_refreshes_memo() {
let strategy = CmaEs::<Flex>::new();
let params = CmaEsConfig::with_pop_size(6, 2);
let device = Default::default();
let mut rng = StdRng::seed_from_u64(0x00A2_9E11);
let fitness = |dev: &_| {
Tensor::<Flex, 1>::from_data(
TensorData::new(vec![6.0f32, 5.0, 4.0, 3.0, 2.0, 1.0], [6]),
dev,
)
};
let s0 = strategy.init(¶ms, &mut rng, &device);
let (pop0, asked0) = strategy.ask(¶ms, &s0, &mut rng, &device);
assert!(asked0.eig.is_some(), "first ask must populate the memo");
let (told0, _m0) = strategy.tell(¶ms, pop0, fitness(&device), asked0, &mut rng);
assert!(told0.eig.is_none(), "first tell must clear the memo");
let (pop1, asked1) = strategy.ask(¶ms, &told0, &mut rng, &device);
assert!(
asked1.eig.is_some(),
"second ask must build a fresh memo off the updated cov"
);
let (told1, _m1) = strategy.tell(¶ms, pop1, fitness(&device), asked1, &mut rng);
assert!(told1.eig.is_none(), "second tell must clear the memo");
assert!(told1.mean().iter().all(|v| v.is_finite()), "mean finite");
assert!(told1.cov().iter().all(|v| v.is_finite()), "cov finite");
assert!(told1.sigma().is_finite(), "sigma finite");
}
#[test]
fn tell_keeps_covariance_symmetric_and_positive_definite() {
let strategy = CmaEs::<Flex>::new();
let params = CmaEsConfig::with_pop_size(6, 3);
let d: usize = params.genome_dim;
let device = Default::default();
let mut rng = StdRng::seed_from_u64(0x5EED_C0DE);
let state = strategy.init(¶ms, &mut rng, &device);
let (population, asked) = strategy.ask(¶ms, &state, &mut rng, &device);
let fitness = Tensor::<Flex, 1>::from_data(
TensorData::new(vec![6.0f32, 5.0, 4.0, 3.0, 2.0, 1.0], [6]),
&device,
);
let (told, _metrics) = strategy.tell(¶ms, population, fitness, asked, &mut rng);
let cov: &[f32] = told.cov();
for i in 0..d {
for j in 0..d {
assert_eq!(
cov[i * d + j].to_bits(),
cov[j * d + i].to_bits(),
"asymmetry at ({i}, {j})"
);
}
}
let eig: SymEigen = jacobi_eigen(cov, d);
assert!(
eig.values.iter().all(|&l| l > 0.0),
"covariance not positive-definite: eigenvalues {:?}",
eig.values
);
for i in 0..d {
assert!(cov[i * d + i] > 0.0, "non-positive variance at {i}");
}
}
#[test]
#[allow(clippy::cast_precision_loss)]
fn long_run_tell_never_drifts_off_symmetric_manifold() {
let strategy = CmaEs::<Flex>::new();
let lambda: usize = 16;
let params = CmaEsConfig::with_pop_size(lambda, 5);
let d: usize = params.genome_dim;
let device = Default::default();
let mut rng = StdRng::seed_from_u64(0x0241_D21F7);
let fitness_vals: Vec<f32> = (0..lambda).map(|i| (lambda - i) as f32).collect();
let mut state = strategy.init(¶ms, &mut rng, &device);
for generation in 0..400 {
let (population, asked) = strategy.ask(¶ms, &state, &mut rng, &device);
let fitness = Tensor::<Flex, 1>::from_data(
TensorData::new(fitness_vals.clone(), [lambda]),
&device,
);
let (told, _metrics) = strategy.tell(¶ms, population, fitness, asked, &mut rng);
let cov: &[f32] = told.cov();
assert!(
cov.iter().all(|v| v.is_finite()),
"cov non-finite at generation {generation}"
);
for i in 0..d {
for j in 0..d {
assert_eq!(
cov[i * d + j].to_bits(),
cov[j * d + i].to_bits(),
"asymmetry at ({i}, {j}) in generation {generation}"
);
}
}
state = told;
}
}
#[test]
fn rankmu_accumulation_is_symmetric_by_construction() {
let d: usize = 3;
let weights: Vec<f32> = vec![0.3, 0.7];
let y_sel: Vec<Vec<f32>> = vec![vec![1.1, 3.3, 7.7], vec![2.2, 5.5, 9.9]];
let mut fixed: Vec<f32> = vec![0.0; d * d];
for i in 0..d {
for j in 0..d {
let mut acc: f32 = 0.0;
for (rank, yi) in y_sel.iter().enumerate() {
acc += weights[rank] * (yi[i] * yi[j]);
}
fixed[i * d + j] = acc;
}
}
for i in 0..d {
for j in 0..d {
assert_eq!(
fixed[i * d + j].to_bits(),
fixed[j * d + i].to_bits(),
"fixed grouping asymmetric at ({i}, {j})"
);
}
}
let mut old: Vec<f32> = vec![0.0; d * d];
for i in 0..d {
for j in 0..d {
let mut acc: f32 = 0.0;
for (rank, yi) in y_sel.iter().enumerate() {
acc += weights[rank] * yi[i] * yi[j];
}
old[i * d + j] = acc;
}
}
let mut old_diverges: bool = false;
for i in 0..d {
for j in 0..d {
if old[i * d + j].to_bits() != old[j * d + i].to_bits() {
old_diverges = true;
}
}
}
assert!(
old_diverges,
"old grouping did not diverge — contrast values no longer exercise \
float non-associativity"
);
}
#[test]
fn best_is_none_before_tell_and_some_after() {
let strategy = CmaEs::<Flex>::new();
let params = CmaEsConfig::with_pop_size(6, 2);
let device = Default::default();
let mut rng = StdRng::seed_from_u64(0xB357_7E57);
let state = strategy.init(¶ms, &mut rng, &device);
assert!(
strategy.best(&state).is_none(),
"best must be None before the first tell"
);
let (population, asked) = strategy.ask(¶ms, &state, &mut rng, &device);
let fitness = Tensor::<Flex, 1>::from_data(
TensorData::new(vec![6.0f32, 5.0, 4.0, 3.0, 2.0, 1.0], [6]),
&device,
);
let (told, _metrics) = strategy.tell(¶ms, population, fitness, asked, &mut rng);
let best = strategy.best(&told).expect("best is Some after a tell");
let (genome, fit): (Tensor<Flex, 2>, f32) = best;
approx::assert_relative_eq!(fit, 6.0, epsilon = 1e-6);
assert_eq!(genome.dims(), [1, 2]);
}
#[test]
fn eigenvalue_floor_clamps_degenerate_eigenvalue() {
let eigvals: Vec<f32> = vec![1.0, 0.0];
let floor: f32 = eigenvalue_floor(&eigvals);
assert_eq!(floor.to_bits(), CONDITION_FLOOR.to_bits());
assert!(floor > EIGENVALUE_FLOOR);
let clamped: f32 = eigvals[1].max(floor);
assert!(clamped > 0.0, "floored eigenvalue must be positive");
assert!(clamped.sqrt().is_finite(), "√Λ must be finite");
assert!((1.0 / clamped.sqrt()).is_finite(), "1/√Λ must be finite");
assert!(
!(1.0f32 / eigvals[1].sqrt()).is_finite(),
"un-floored 1/√0 must diverge — proves the floor is load-bearing"
);
}
#[test]
fn update_best_empty_population_is_noop() {
let strategy = CmaEs::<Flex>::new();
let params = CmaEsConfig::with_pop_size(6, 2);
let device = Default::default();
let mut rng = StdRng::seed_from_u64(0x0E11_0E11);
let mut state = strategy.init(¶ms, &mut rng, &device);
let pop = Tensor::<Flex, 2>::from_data(TensorData::new(vec![0.0f32, 0.0], [1, 2]), &device);
update_best(&mut state, &pop, &[]);
assert!(
state.best_genome().is_none(),
"empty population must not set a best genome"
);
assert_eq!(
state.best_fitness().to_bits(),
f32::NEG_INFINITY.to_bits(),
"empty population must not move best fitness off its sentinel"
);
}
proptest! {
#![proptest_config(ProptestConfig {
cases: 16,
max_shrink_iters: 256,
..ProptestConfig::default()
})]
#[test]
fn cma_es_drive_preserves_invariants(
lambda in 2usize..=64,
d in 1usize..=20,
seed in any::<u64>(),
) {
let strategy = CmaEs::<Flex>::new();
let params = CmaEsConfig::with_pop_size(lambda, d);
prop_assume!(params.validate().is_ok());
let device = Default::default();
let mut rng = StdRng::seed_from_u64(seed);
#[allow(clippy::cast_precision_loss)]
let fitness_vals: Vec<f32> = (0..lambda).map(|i| (lambda - i) as f32).collect();
let mut state = strategy.init(¶ms, &mut rng, &device);
prop_assert!(
strategy.best(&state).is_none(),
"best must be None before the first tell"
);
for _generation in 0..4 {
let (population, asked) = strategy.ask(¶ms, &state, &mut rng, &device);
prop_assert_eq!(population.dims(), [lambda, d], "ask output shape");
let fitness = Tensor::<Flex, 1>::from_data(
TensorData::new(fitness_vals.clone(), [lambda]),
&device,
);
let (told, _metrics) =
strategy.tell(¶ms, population, fitness, asked, &mut rng);
let cov: &[f32] = told.cov();
for i in 0..d {
for j in 0..d {
prop_assert_eq!(
cov[i * d + j].to_bits(),
cov[j * d + i].to_bits(),
"asymmetry at ({}, {})",
i,
j
);
}
}
let eig: SymEigen = jacobi_eigen(cov, d);
prop_assert!(
eig.values.iter().all(|&l| l > 0.0),
"covariance not positive-definite: eigenvalues {:?}",
eig.values
);
for i in 0..d {
prop_assert!(cov[i * d + i] > 0.0, "non-positive variance at {}", i);
}
prop_assert!(told.mean().iter().all(|v| v.is_finite()), "mean finite");
prop_assert!(told.cov().iter().all(|v| v.is_finite()), "cov finite");
prop_assert!(told.sigma().is_finite(), "sigma finite");
let best = strategy.best(&told);
prop_assert!(best.is_some(), "best must be Some after a tell");
let (genome, fit): (Tensor<Flex, 2>, f32) =
best.expect("best is Some after a tell");
prop_assert!(fit.is_finite(), "best fitness finite");
prop_assert_eq!(genome.dims(), [1, d], "best genome shape");
state = told;
}
}
}
}