use crate::error::{OptimizeError, OptimizeResult};
use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
use scirs2_core::random::rngs::StdRng;
use scirs2_core::random::{Rng, SeedableRng};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BoundaryHandling {
None,
Projection,
Reflection,
Penalty {
weight: f64,
},
Resampling {
max_attempts: usize,
},
}
impl Default for BoundaryHandling {
fn default() -> Self {
BoundaryHandling::Reflection
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum RestartStrategy {
NoRestart,
Ipop {
max_restarts: usize,
},
Bipop {
max_restarts: usize,
},
}
impl Default for RestartStrategy {
fn default() -> Self {
RestartStrategy::Ipop { max_restarts: 9 }
}
}
#[derive(Debug, Clone)]
pub struct CmaEsOptions {
pub sigma0: f64,
pub population_size: Option<usize>,
pub max_fevals: usize,
pub max_iterations: usize,
pub ftol: f64,
pub xtol: f64,
pub boundary_handling: BoundaryHandling,
pub restart_strategy: RestartStrategy,
pub lower_bounds: Option<Vec<f64>>,
pub upper_bounds: Option<Vec<f64>>,
pub seed: Option<u64>,
pub verbosity: usize,
}
impl Default for CmaEsOptions {
fn default() -> Self {
Self {
sigma0: 0.3,
population_size: None,
max_fevals: 100_000,
max_iterations: 100_000,
ftol: 1e-12,
xtol: 1e-12,
boundary_handling: BoundaryHandling::default(),
restart_strategy: RestartStrategy::default(),
lower_bounds: None,
upper_bounds: None,
seed: None,
verbosity: 0,
}
}
}
#[derive(Debug, Clone)]
pub struct CmaEsResult {
pub x: Array1<f64>,
pub fun: f64,
pub nfev: usize,
pub nit: usize,
pub n_restarts: usize,
pub success: bool,
pub message: String,
pub sigma_final: f64,
pub cond_final: f64,
}
#[derive(Debug, Clone)]
pub struct CmaEsState {
n: usize,
mean: Array1<f64>,
sigma: f64,
lambda: usize,
mu: usize,
weights: Vec<f64>,
mu_eff: f64,
p_sigma: Array1<f64>,
p_c: Array1<f64>,
cov: Array2<f64>,
eigenvalues: Array1<f64>,
eigenvectors: Array2<f64>,
inv_sqrt_cov: Array2<f64>,
c_sigma: f64,
d_sigma: f64,
c_c: f64,
c_1: f64,
c_mu: f64,
chi_n: f64,
generation: usize,
fevals: usize,
best_x: Array1<f64>,
best_f: f64,
eigen_update_counter: usize,
rng: StdRng,
}
impl CmaEsState {
pub fn new(x0: &[f64], sigma0: f64, lambda: Option<usize>, seed: u64) -> OptimizeResult<Self> {
let n = x0.len();
if n == 0 {
return Err(OptimizeError::InvalidInput(
"Dimension must be at least 1".to_string(),
));
}
if sigma0 <= 0.0 || !sigma0.is_finite() {
return Err(OptimizeError::InvalidInput(format!(
"sigma0 must be positive and finite, got {}",
sigma0
)));
}
let lambda = lambda.unwrap_or_else(|| 4 + (3.0 * (n as f64).ln()).floor() as usize);
let lambda = lambda.max(4); let mu = lambda / 2;
let raw_weights: Vec<f64> = (0..mu)
.map(|i| ((mu as f64 + 0.5).ln() - ((i + 1) as f64).ln()).max(0.0))
.collect();
let w_sum: f64 = raw_weights.iter().sum();
let weights: Vec<f64> = raw_weights.iter().map(|w| w / w_sum).collect();
let w_sq_sum: f64 = weights.iter().map(|w| w * w).sum();
let mu_eff = 1.0 / w_sq_sum;
let c_sigma = (mu_eff + 2.0) / (n as f64 + mu_eff + 5.0);
let d_sigma =
1.0 + 2.0 * (((mu_eff - 1.0) / (n as f64 + 1.0)).sqrt() - 1.0).max(0.0) + c_sigma;
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 c_mu_candidate = (1.0 - c_1)
.min(2.0 * (mu_eff - 2.0 + 1.0 / mu_eff) / ((n as f64 + 2.0).powi(2) + mu_eff));
let c_mu = c_mu_candidate.max(0.0);
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 mean = Array1::from_vec(x0.to_vec());
let cov = Array2::eye(n);
let eigenvalues = Array1::ones(n);
let eigenvectors = Array2::eye(n);
let inv_sqrt_cov = Array2::eye(n);
let p_sigma = Array1::zeros(n);
let p_c = Array1::zeros(n);
let rng = StdRng::seed_from_u64(seed);
Ok(Self {
n,
mean: mean.clone(),
sigma: sigma0,
lambda,
mu,
weights,
mu_eff,
p_sigma,
p_c,
cov,
eigenvalues,
eigenvectors,
inv_sqrt_cov,
c_sigma,
d_sigma,
c_c,
c_1,
c_mu,
chi_n,
generation: 0,
fevals: 0,
best_x: mean,
best_f: f64::INFINITY,
eigen_update_counter: 0,
rng,
})
}
fn sample_population(&mut self) -> Vec<Array1<f64>> {
let mut population = Vec::with_capacity(self.lambda);
for _ in 0..self.lambda {
let z: Array1<f64> = Array1::from_vec(
(0..self.n)
.map(|_| sample_standard_normal(&mut self.rng))
.collect(),
);
let d_z = &z * &self.eigenvalues.mapv(f64::sqrt);
let y = self.eigenvectors.dot(&d_z);
let x = &self.mean + &(y * self.sigma);
population.push(x);
}
population
}
fn apply_boundary_handling(
&mut self,
x: &Array1<f64>,
lower: &Option<Vec<f64>>,
upper: &Option<Vec<f64>>,
handling: BoundaryHandling,
) -> (Array1<f64>, f64) {
let mut x_fixed = x.clone();
let mut penalty = 0.0;
let lb = lower.as_deref();
let ub = upper.as_deref();
if lb.is_none() && ub.is_none() {
return (x_fixed, 0.0);
}
match handling {
BoundaryHandling::None => {}
BoundaryHandling::Projection => {
for i in 0..self.n {
if let Some(lb_vals) = lb {
if x_fixed[i] < lb_vals[i] {
x_fixed[i] = lb_vals[i];
}
}
if let Some(ub_vals) = ub {
if x_fixed[i] > ub_vals[i] {
x_fixed[i] = ub_vals[i];
}
}
}
}
BoundaryHandling::Reflection => {
for i in 0..self.n {
let lo = lb.map_or(f64::NEG_INFINITY, |v| v[i]);
let hi = ub.map_or(f64::INFINITY, |v| v[i]);
if lo.is_finite() && hi.is_finite() {
let range = hi - lo;
if range > 0.0 {
let mut val = x_fixed[i];
for _ in 0..10 {
if val < lo {
val = lo + (lo - val);
} else if val > hi {
val = hi - (val - hi);
} else {
break;
}
}
x_fixed[i] = val.clamp(lo, hi);
}
} else {
if lo.is_finite() && x_fixed[i] < lo {
x_fixed[i] = lo;
}
if hi.is_finite() && x_fixed[i] > hi {
x_fixed[i] = hi;
}
}
}
}
BoundaryHandling::Penalty { weight } => {
for i in 0..self.n {
if let Some(lb_vals) = lb {
if x_fixed[i] < lb_vals[i] {
let diff = lb_vals[i] - x_fixed[i];
penalty += weight * diff * diff;
x_fixed[i] = lb_vals[i];
}
}
if let Some(ub_vals) = ub {
if x_fixed[i] > ub_vals[i] {
let diff = x_fixed[i] - ub_vals[i];
penalty += weight * diff * diff;
x_fixed[i] = ub_vals[i];
}
}
}
}
BoundaryHandling::Resampling { max_attempts } => {
let mut feasible = true;
for i in 0..self.n {
let lo = lb.map_or(f64::NEG_INFINITY, |v| v[i]);
let hi = ub.map_or(f64::INFINITY, |v| v[i]);
if x_fixed[i] < lo || x_fixed[i] > hi {
feasible = false;
break;
}
}
if !feasible {
for _ in 0..max_attempts {
let z: Array1<f64> = Array1::from_vec(
(0..self.n)
.map(|_| sample_standard_normal(&mut self.rng))
.collect(),
);
let d_z = &z * &self.eigenvalues.mapv(f64::sqrt);
let y = self.eigenvectors.dot(&d_z);
x_fixed = &self.mean + &(y * self.sigma);
let mut all_feasible = true;
for i in 0..self.n {
let lo = lb.map_or(f64::NEG_INFINITY, |v| v[i]);
let hi = ub.map_or(f64::INFINITY, |v| v[i]);
if x_fixed[i] < lo || x_fixed[i] > hi {
all_feasible = false;
break;
}
}
if all_feasible {
return (x_fixed, 0.0);
}
}
for i in 0..self.n {
if let Some(lb_vals) = lb {
if x_fixed[i] < lb_vals[i] {
x_fixed[i] = lb_vals[i];
}
}
if let Some(ub_vals) = ub {
if x_fixed[i] > ub_vals[i] {
x_fixed[i] = ub_vals[i];
}
}
}
}
}
}
(x_fixed, penalty)
}
fn update_eigen(&mut self) {
let n = self.n;
for i in 0..n {
for j in (i + 1)..n {
let avg = 0.5 * (self.cov[[i, j]] + self.cov[[j, i]]);
self.cov[[i, j]] = avg;
self.cov[[j, i]] = avg;
}
}
let (eigenvalues, eigenvectors) = jacobi_eigen(&self.cov, n);
let min_eigenval = 1e-20;
self.eigenvalues = eigenvalues.mapv(|v| v.max(min_eigenval));
self.eigenvectors = eigenvectors;
let d_inv = self.eigenvalues.mapv(|v| 1.0 / v.sqrt());
let mut inv_sqrt = Array2::zeros((n, n));
for i in 0..n {
for j in 0..n {
let mut sum = 0.0;
for k in 0..n {
sum += self.eigenvectors[[i, k]] * d_inv[k] * self.eigenvectors[[j, k]];
}
inv_sqrt[[i, j]] = sum;
}
}
self.inv_sqrt_cov = inv_sqrt;
}
fn update(&mut self, population: &[Array1<f64>], fitness: &[f64]) {
let mut indices: Vec<usize> = (0..self.lambda).collect();
indices.sort_by(|&a, &b| {
fitness[a]
.partial_cmp(&fitness[b])
.unwrap_or(std::cmp::Ordering::Equal)
});
if fitness[indices[0]] < self.best_f {
self.best_f = fitness[indices[0]];
self.best_x = population[indices[0]].clone();
}
let old_mean = self.mean.clone();
self.mean = Array1::zeros(self.n);
for i in 0..self.mu {
let idx = indices[i];
self.mean = &self.mean + &(&population[idx] * self.weights[i]);
}
let mean_diff = &self.mean - &old_mean;
let inv_sqrt_cov_mean_diff = self.inv_sqrt_cov.dot(&mean_diff);
let c_sigma_factor = (self.c_sigma * (2.0 - self.c_sigma) * self.mu_eff).sqrt();
self.p_sigma = &self.p_sigma * (1.0 - self.c_sigma)
+ &(&inv_sqrt_cov_mean_diff * (c_sigma_factor / self.sigma));
let p_sigma_norm = self.p_sigma.dot(&self.p_sigma).sqrt();
let threshold = (1.0 - (1.0 - self.c_sigma).powi(2 * (self.generation as i32 + 1))).sqrt()
* (1.4 + 2.0 / (self.n as f64 + 1.0))
* self.chi_n;
let h_sigma: f64 = if p_sigma_norm < threshold { 1.0 } else { 0.0 };
let c_c_factor = (self.c_c * (2.0 - self.c_c) * self.mu_eff).sqrt();
self.p_c =
&self.p_c * (1.0 - self.c_c) + &(&mean_diff * (h_sigma * c_c_factor / self.sigma));
let pc_outer = outer_product(&self.p_c, &self.p_c);
let delta_h = (1.0 - h_sigma) * self.c_c * (2.0 - self.c_c);
let mut rank_mu_update: Array2<f64> = Array2::zeros((self.n, self.n));
for i in 0..self.mu {
let idx = indices[i];
let y_i = (&population[idx] - &old_mean) / self.sigma;
let y_outer = outer_product(&y_i, &y_i);
rank_mu_update = rank_mu_update + &(&y_outer * self.weights[i]);
}
let scale = 1.0 - self.c_1 - self.c_mu + delta_h * self.c_1;
self.cov = &self.cov * scale + &(&pc_outer * self.c_1) + &(&rank_mu_update * self.c_mu);
let sigma_exp = (self.c_sigma / self.d_sigma) * (p_sigma_norm / self.chi_n - 1.0);
self.sigma *= sigma_exp.exp();
self.sigma = self.sigma.clamp(1e-20, 1e20);
self.eigen_update_counter += 1;
let update_freq = (self.lambda as f64 / ((self.c_1 + self.c_mu) * self.n as f64 * 10.0))
.max(1.0) as usize;
if self.eigen_update_counter >= update_freq {
self.update_eigen();
self.eigen_update_counter = 0;
}
self.generation += 1;
}
fn check_termination(&self, recent_fitness: &[f64], options: &CmaEsOptions) -> Option<String> {
if self.fevals >= options.max_fevals {
return Some(format!(
"Maximum function evaluations ({}) reached",
options.max_fevals
));
}
if self.generation >= options.max_iterations {
return Some(format!(
"Maximum iterations ({}) reached",
options.max_iterations
));
}
if recent_fitness.len() >= self.lambda {
let f_best = recent_fitness.iter().copied().fold(f64::INFINITY, f64::min);
let f_worst = recent_fitness
.iter()
.copied()
.fold(f64::NEG_INFINITY, f64::max);
if (f_worst - f_best).abs() < options.ftol {
return Some("Function tolerance reached (flat fitness)".to_string());
}
}
let max_eigenval = self.eigenvalues.iter().copied().fold(0.0_f64, f64::max);
if self.sigma * max_eigenval.sqrt() < options.xtol {
return Some("Solution tolerance reached".to_string());
}
let min_eigenval = self
.eigenvalues
.iter()
.copied()
.fold(f64::INFINITY, f64::min);
if min_eigenval > 0.0 {
let cond = max_eigenval / min_eigenval;
if cond > 1e14 {
return Some(format!("Condition number too large: {:.2e}", cond));
}
}
if self.sigma < 1e-20 {
return Some("Step size sigma below minimum threshold".to_string());
}
None
}
pub fn condition_number(&self) -> f64 {
let max_ev = self.eigenvalues.iter().copied().fold(0.0_f64, f64::max);
let min_ev = self
.eigenvalues
.iter()
.copied()
.fold(f64::INFINITY, f64::min);
if min_ev > 0.0 {
max_ev / min_ev
} else {
f64::INFINITY
}
}
}
pub struct IpopCmaEs<F>
where
F: Fn(&ArrayView1<f64>) -> f64,
{
func: F,
x0: Vec<f64>,
options: CmaEsOptions,
}
impl<F> IpopCmaEs<F>
where
F: Fn(&ArrayView1<f64>) -> f64,
{
pub fn new(func: F, x0: &[f64], options: CmaEsOptions) -> Self {
Self {
func,
x0: x0.to_vec(),
options,
}
}
pub fn run(&self) -> OptimizeResult<CmaEsResult> {
let max_restarts = match self.options.restart_strategy {
RestartStrategy::NoRestart => 0,
RestartStrategy::Ipop { max_restarts } => max_restarts,
RestartStrategy::Bipop { max_restarts } => max_restarts,
};
let seed = self
.options
.seed
.unwrap_or_else(|| scirs2_core::random::rng().random_range(0..u64::MAX));
let mut overall_best_x = Array1::from_vec(self.x0.clone());
let mut overall_best_f = f64::INFINITY;
let mut total_fevals = 0_usize;
let mut total_iterations = 0_usize;
let mut final_message = String::new();
let mut final_sigma = self.options.sigma0;
let mut final_cond = 1.0;
let mut any_success = false;
let base_lambda = self
.options
.population_size
.unwrap_or_else(|| 4 + (3.0 * (self.x0.len() as f64).ln()).floor() as usize);
let mut rng = StdRng::seed_from_u64(seed);
for restart_idx in 0..=max_restarts {
let current_lambda = if restart_idx == 0 {
base_lambda
} else {
match self.options.restart_strategy {
RestartStrategy::Ipop { .. } => base_lambda * (1 << restart_idx.min(10)),
RestartStrategy::Bipop { .. } => {
if restart_idx % 2 == 1 {
base_lambda * (1 << ((restart_idx / 2 + 1).min(10)))
} else {
(base_lambda / 2).max(4)
}
}
RestartStrategy::NoRestart => base_lambda,
}
};
let x0_restart = if restart_idx == 0 {
self.x0.clone()
} else {
let mut x0_new = self.x0.clone();
for i in 0..x0_new.len() {
let lo = self.options.lower_bounds.as_ref().map_or(-10.0, |lb| lb[i]);
let hi = self.options.upper_bounds.as_ref().map_or(10.0, |ub| ub[i]);
x0_new[i] = rng.random_range(lo..hi);
}
x0_new
};
let run_seed = seed.wrapping_add(restart_idx as u64 * 1_000_000);
let result = self.run_single(
&x0_restart,
current_lambda,
run_seed,
self.options.max_fevals.saturating_sub(total_fevals),
)?;
total_fevals += result.nfev;
total_iterations += result.nit;
if result.fun < overall_best_f {
overall_best_f = result.fun;
overall_best_x = result.x.clone();
final_sigma = result.sigma_final;
final_cond = result.cond_final;
}
if result.success {
any_success = true;
}
final_message = result.message.clone();
if total_fevals >= self.options.max_fevals {
final_message = "Budget exhausted across restarts".to_string();
break;
}
if result.success && result.fun < self.options.ftol {
break;
}
}
let n_restarts = if any_success { 0 } else { max_restarts };
Ok(CmaEsResult {
x: overall_best_x,
fun: overall_best_f,
nfev: total_fevals,
nit: total_iterations,
n_restarts,
success: any_success || overall_best_f < f64::INFINITY,
message: final_message,
sigma_final: final_sigma,
cond_final: final_cond,
})
}
fn run_single(
&self,
x0: &[f64],
lambda: usize,
seed: u64,
max_fevals: usize,
) -> OptimizeResult<CmaEsResult> {
let mut state = CmaEsState::new(x0, self.options.sigma0, Some(lambda), seed)?;
let x0_arr = Array1::from_vec(x0.to_vec());
let f0 = (self.func)(&x0_arr.view());
state.fevals += 1;
if f0 < state.best_f {
state.best_f = f0;
state.best_x = x0_arr;
}
let mut termination_msg = String::new();
let mut converged = false;
loop {
let mut population = state.sample_population();
let mut penalties = vec![0.0; state.lambda];
for i in 0..state.lambda {
let (x_fixed, pen) = state.apply_boundary_handling(
&population[i],
&self.options.lower_bounds,
&self.options.upper_bounds,
self.options.boundary_handling,
);
population[i] = x_fixed;
penalties[i] = pen;
}
let mut fitness: Vec<f64> = population.iter().map(|x| (self.func)(&x.view())).collect();
state.fevals += state.lambda;
for (f, p) in fitness.iter_mut().zip(penalties.iter()) {
*f += *p;
}
if let Some(msg) = state.check_termination(&fitness, &self.options) {
termination_msg = msg;
converged = termination_msg.contains("tolerance reached");
break;
}
if state.fevals >= max_fevals {
termination_msg = "Local budget exhausted".to_string();
break;
}
state.update(&population, &fitness);
}
let cond_final = state.condition_number();
Ok(CmaEsResult {
x: state.best_x,
fun: state.best_f,
nfev: state.fevals,
nit: state.generation,
n_restarts: 0,
success: converged,
message: termination_msg,
sigma_final: state.sigma,
cond_final,
})
}
}
pub fn cma_es_minimize<F>(
func: F,
x0: &[f64],
options: Option<CmaEsOptions>,
) -> OptimizeResult<CmaEsResult>
where
F: Fn(&ArrayView1<f64>) -> f64,
{
let options = options.unwrap_or_default();
let optimizer = IpopCmaEs::new(func, x0, options);
optimizer.run()
}
fn sample_standard_normal(rng: &mut StdRng) -> f64 {
let u1: f64 = rng.random_range(1e-10..1.0);
let u2: f64 = rng.random_range(0.0..std::f64::consts::TAU);
(-2.0 * u1.ln()).sqrt() * u2.cos()
}
fn outer_product(a: &Array1<f64>, b: &Array1<f64>) -> Array2<f64> {
let n = a.len();
let m = b.len();
let mut result = Array2::zeros((n, m));
for i in 0..n {
for j in 0..m {
result[[i, j]] = a[i] * b[j];
}
}
result
}
fn jacobi_eigen(mat: &Array2<f64>, n: usize) -> (Array1<f64>, Array2<f64>) {
let mut a = mat.clone();
let mut v = Array2::eye(n);
let max_iter = 100 * n * n;
let tol = 1e-15;
for _ in 0..max_iter {
let mut max_val = 0.0_f64;
let mut p = 0;
let mut q = 1;
for i in 0..n {
for j in (i + 1)..n {
if a[[i, j]].abs() > max_val {
max_val = a[[i, j]].abs();
p = i;
q = j;
}
}
}
if max_val < tol {
break;
}
let app = a[[p, p]];
let aqq = a[[q, q]];
let apq = a[[p, q]];
let theta = if (app - aqq).abs() < tol {
std::f64::consts::FRAC_PI_4
} else {
0.5 * (2.0 * apq / (app - aqq)).atan()
};
let cos_t = theta.cos();
let sin_t = theta.sin();
let mut new_a = a.clone();
for i in 0..n {
if i != p && i != q {
new_a[[i, p]] = cos_t * a[[i, p]] + sin_t * a[[i, q]];
new_a[[p, i]] = new_a[[i, p]];
new_a[[i, q]] = -sin_t * a[[i, p]] + cos_t * a[[i, q]];
new_a[[q, i]] = new_a[[i, q]];
}
}
new_a[[p, p]] = cos_t * cos_t * app + 2.0 * sin_t * cos_t * apq + sin_t * sin_t * aqq;
new_a[[q, q]] = sin_t * sin_t * app - 2.0 * sin_t * cos_t * apq + cos_t * cos_t * aqq;
new_a[[p, q]] = 0.0;
new_a[[q, p]] = 0.0;
a = new_a;
let mut new_v = v.clone();
for i in 0..n {
new_v[[i, p]] = cos_t * v[[i, p]] + sin_t * v[[i, q]];
new_v[[i, q]] = -sin_t * v[[i, p]] + cos_t * v[[i, q]];
}
v = new_v;
}
let eigenvalues = Array1::from_vec((0..n).map(|i| a[[i, i]]).collect());
(eigenvalues, v)
}
#[cfg(test)]
mod tests {
use super::*;
fn sphere(x: &ArrayView1<f64>) -> f64 {
x.iter().map(|xi| xi * xi).sum()
}
fn rosenbrock(x: &ArrayView1<f64>) -> f64 {
let mut sum = 0.0;
for i in 0..x.len() - 1 {
sum += 100.0 * (x[i + 1] - x[i] * x[i]).powi(2) + (1.0 - x[i]).powi(2);
}
sum
}
fn rastrigin(x: &ArrayView1<f64>) -> f64 {
let n = x.len() as f64;
let mut sum = 10.0 * n;
for &xi in x.iter() {
sum += xi * xi - 10.0 * (2.0 * std::f64::consts::PI * xi).cos();
}
sum
}
#[test]
fn test_cma_es_sphere_2d() {
let options = CmaEsOptions {
sigma0: 0.5,
max_fevals: 10_000,
restart_strategy: RestartStrategy::NoRestart,
seed: Some(42),
..Default::default()
};
let result = cma_es_minimize(sphere, &[3.0, -2.0], Some(options));
assert!(result.is_ok());
let res = result.expect("CMA-ES sphere 2D failed");
assert!(res.fun < 1e-6, "Sphere function value: {}", res.fun);
for &xi in res.x.iter() {
assert!(xi.abs() < 1e-3, "Solution component: {}", xi);
}
}
#[test]
fn test_cma_es_sphere_5d() {
let options = CmaEsOptions {
sigma0: 1.0,
max_fevals: 50_000,
restart_strategy: RestartStrategy::NoRestart,
seed: Some(123),
..Default::default()
};
let result = cma_es_minimize(sphere, &[5.0, -3.0, 2.0, -1.0, 4.0], Some(options));
assert!(result.is_ok());
let res = result.expect("CMA-ES sphere 5D failed");
assert!(res.fun < 1e-4, "Sphere 5D value: {}", res.fun);
}
#[test]
fn test_cma_es_rosenbrock_2d() {
let options = CmaEsOptions {
sigma0: 0.5,
max_fevals: 50_000,
restart_strategy: RestartStrategy::NoRestart,
seed: Some(99),
..Default::default()
};
let result = cma_es_minimize(rosenbrock, &[0.0, 0.0], Some(options));
assert!(result.is_ok());
let res = result.expect("CMA-ES Rosenbrock 2D failed");
assert!(res.fun < 1e-2, "Rosenbrock value: {}", res.fun);
}
#[test]
fn test_cma_es_with_bounds() {
let options = CmaEsOptions {
sigma0: 0.3,
max_fevals: 10_000,
restart_strategy: RestartStrategy::NoRestart,
lower_bounds: Some(vec![0.0, 0.0]),
upper_bounds: Some(vec![5.0, 5.0]),
boundary_handling: BoundaryHandling::Reflection,
seed: Some(77),
..Default::default()
};
let result = cma_es_minimize(sphere, &[2.5, 2.5], Some(options));
assert!(result.is_ok());
let res = result.expect("CMA-ES bounded failed");
assert!(res.fun < 0.1, "Bounded sphere value: {}", res.fun);
for &xi in res.x.iter() {
assert!(xi >= -0.01, "Lower bound violated: {}", xi);
assert!(xi <= 5.01, "Upper bound violated: {}", xi);
}
}
#[test]
fn test_cma_es_ipop_restart() {
let options = CmaEsOptions {
sigma0: 2.0,
max_fevals: 50_000,
restart_strategy: RestartStrategy::Ipop { max_restarts: 3 },
lower_bounds: Some(vec![-5.12, -5.12]),
upper_bounds: Some(vec![5.12, 5.12]),
seed: Some(55),
..Default::default()
};
let result = cma_es_minimize(rastrigin, &[3.0, -2.0], Some(options));
assert!(result.is_ok());
let res = result.expect("IPOP CMA-ES Rastrigin failed");
assert!(res.fun < 5.0, "Rastrigin IPOP value: {}", res.fun);
}
#[test]
fn test_cma_es_penalty_boundary() {
let options = CmaEsOptions {
sigma0: 0.5,
max_fevals: 10_000,
restart_strategy: RestartStrategy::NoRestart,
lower_bounds: Some(vec![-1.0, -1.0]),
upper_bounds: Some(vec![1.0, 1.0]),
boundary_handling: BoundaryHandling::Penalty { weight: 100.0 },
seed: Some(42),
..Default::default()
};
let result = cma_es_minimize(sphere, &[0.5, 0.5], Some(options));
assert!(result.is_ok());
}
#[test]
fn test_cma_es_projection_boundary() {
let options = CmaEsOptions {
sigma0: 0.5,
max_fevals: 10_000,
restart_strategy: RestartStrategy::NoRestart,
lower_bounds: Some(vec![-2.0, -2.0]),
upper_bounds: Some(vec![2.0, 2.0]),
boundary_handling: BoundaryHandling::Projection,
seed: Some(42),
..Default::default()
};
let result = cma_es_minimize(sphere, &[1.0, 1.0], Some(options));
assert!(result.is_ok());
let res = result.expect("CMA-ES projection failed");
assert!(res.fun < 0.01, "Projection sphere value: {}", res.fun);
}
#[test]
fn test_cma_es_resampling_boundary() {
let options = CmaEsOptions {
sigma0: 0.3,
max_fevals: 10_000,
restart_strategy: RestartStrategy::NoRestart,
lower_bounds: Some(vec![-2.0, -2.0]),
upper_bounds: Some(vec![2.0, 2.0]),
boundary_handling: BoundaryHandling::Resampling { max_attempts: 100 },
seed: Some(42),
..Default::default()
};
let result = cma_es_minimize(sphere, &[1.0, 1.0], Some(options));
assert!(result.is_ok());
}
#[test]
fn test_state_creation() {
let state = CmaEsState::new(&[0.0, 0.0, 0.0], 0.5, None, 42);
assert!(state.is_ok());
let s = state.expect("State creation failed");
assert_eq!(s.n, 3);
assert_eq!(s.sigma, 0.5);
assert!(s.lambda >= 4);
assert!(s.mu >= 2);
}
#[test]
fn test_state_creation_invalid() {
let state = CmaEsState::new(&[], 0.5, None, 42);
assert!(state.is_err());
let state = CmaEsState::new(&[0.0], -1.0, None, 42);
assert!(state.is_err());
let state = CmaEsState::new(&[0.0], f64::NAN, None, 42);
assert!(state.is_err());
}
#[test]
fn test_jacobi_eigen_identity() {
let mat = Array2::eye(3);
let (eigenvalues, eigenvectors) = jacobi_eigen(&mat, 3);
for &ev in eigenvalues.iter() {
assert!((ev - 1.0).abs() < 1e-10);
}
let prod = eigenvectors.t().dot(&eigenvectors);
for i in 0..3 {
for j in 0..3 {
let expected = if i == j { 1.0 } else { 0.0 };
assert!(
(prod[[i, j]] - expected).abs() < 1e-10,
"Orthogonality check failed at ({}, {}): {}",
i,
j,
prod[[i, j]]
);
}
}
}
#[test]
fn test_bipop_restart() {
let options = CmaEsOptions {
sigma0: 1.0,
max_fevals: 20_000,
restart_strategy: RestartStrategy::Bipop { max_restarts: 2 },
seed: Some(42),
..Default::default()
};
let result = cma_es_minimize(sphere, &[3.0, -2.0], Some(options));
assert!(result.is_ok());
}
}