#![allow(clippy::needless_range_loop)]
use super::Optimizer;
use crate::problem::Problem;
use crate::rng::Rng;
use crate::solution::{Report, Solution, StopReason};
use crate::termination::Termination;
#[derive(Debug, Clone, Copy)]
pub struct CmaEs {
pub population: Option<usize>,
pub sigma0: f64,
pub seed: u64,
}
impl Default for CmaEs {
fn default() -> Self {
CmaEs {
population: None,
sigma0: 0.3,
seed: 42,
}
}
}
impl Optimizer for CmaEs {
fn with_seed(&self, seed: u64) -> Self {
CmaEs { seed, ..*self }
}
fn optimize(&self, problem: &dyn Problem, term: &Termination) -> Report {
crate::problem::validate(problem).unwrap_or_else(|e| panic!("CmaEs: invalid problem: {e}"));
let run = cma_run(problem, term, self.seed, self.population, self.sigma0, 0);
let stop = term.reason(run.evaluations, run.best.value).unwrap_or(
StopReason::Converged,
);
Report {
solution: run.best,
stop,
evaluations: run.evaluations,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Restart {
Ipop,
Bipop,
}
#[derive(Debug, Clone, Copy)]
pub struct RestartCmaEs {
pub strategy: Restart,
pub sigma0: f64,
pub max_restarts: usize,
pub seed: u64,
}
impl Default for RestartCmaEs {
fn default() -> Self {
RestartCmaEs {
strategy: Restart::Bipop,
sigma0: 0.3,
max_restarts: 100,
seed: 42,
}
}
}
impl RestartCmaEs {
pub fn ipop() -> Self {
RestartCmaEs {
strategy: Restart::Ipop,
..RestartCmaEs::default()
}
}
pub fn bipop() -> Self {
RestartCmaEs::default()
}
}
impl Optimizer for RestartCmaEs {
fn with_seed(&self, seed: u64) -> Self {
RestartCmaEs { seed, ..*self }
}
fn optimize(&self, problem: &dyn Problem, term: &Termination) -> Report {
crate::problem::validate(problem)
.unwrap_or_else(|e| panic!("RestartCmaEs: invalid problem: {e}"));
let n = problem.bounds().len();
let lambda_def = (4 + (3.0 * (n as f64).ln()) as usize).max(4);
let mut ctl = Rng::split(self.seed, u64::MAX);
let mut best = Solution {
x: vec![0.0; n],
value: f64::INFINITY,
};
let mut evaluations = 0usize;
let mut lambda_large = lambda_def;
let mut budget_large = 0usize;
let mut budget_small = 0usize;
let mut last_large_run = 0usize;
let mut any_converged = false;
for run_idx in 0..self.max_restarts.max(1) as u64 {
if term.reason(evaluations, best.value).is_some() {
break;
}
let (lambda, sigma0, cap, is_large) = if run_idx == 0 {
(lambda_def, self.sigma0, None, true)
} else {
match self.strategy {
Restart::Ipop => {
lambda_large *= 2;
(lambda_large, self.sigma0, None, true)
}
Restart::Bipop => {
if budget_large <= budget_small {
lambda_large *= 2;
(lambda_large, self.sigma0, None, true)
} else {
let u1 = ctl.uniform();
let u2 = ctl.uniform();
let ratio = lambda_large as f64 / (2.0 * lambda_def as f64);
let lam = ((lambda_def as f64) * ratio.max(1.0).powf(u1 * u1)) as usize;
let sig = self.sigma0 * 10f64.powf(-2.0 * u2);
(
lam.max(4),
sig,
Some((last_large_run / 2).max(lam.max(4))),
false,
)
}
}
}
};
let inner_term = Termination {
max_evaluations: match cap {
Some(c) => (evaluations + c).min(term.max_evaluations),
None => term.max_evaluations,
},
target: term.target,
};
let out = cma_run(
problem,
&inner_term,
crate::rng::mix_seed(self.seed, run_idx),
Some(lambda),
sigma0,
evaluations,
);
let used = out.evaluations - evaluations;
evaluations = out.evaluations;
if is_large {
budget_large += used;
last_large_run = used;
} else {
budget_small += used;
}
if out.best.value < best.value {
best = out.best;
}
any_converged |= out.converged;
if !out.converged && term.reason(evaluations, best.value).is_some() {
break;
}
}
let stop = term
.reason(evaluations, best.value)
.unwrap_or(if any_converged {
StopReason::Converged
} else {
StopReason::BudgetExhausted
});
Report {
solution: best,
stop,
evaluations,
}
}
}
pub(crate) struct CmaRunOutcome {
pub best: Solution,
pub evaluations: usize,
pub converged: bool,
}
pub(crate) fn cma_run(
problem: &dyn Problem,
term: &Termination,
seed: u64,
population: Option<usize>,
sigma0: f64,
start_evals: usize,
) -> CmaRunOutcome {
let bounds = problem.bounds();
let n = bounds.len();
let mut rng = Rng::new(seed);
let lambda = population
.unwrap_or(4 + (3.0 * (n as f64).ln()) as usize)
.max(4);
let mu = lambda / 2;
let raw: Vec<f64> = (0..mu)
.map(|i| ((lambda as f64 + 1.0) / 2.0).ln() - ((i + 1) as f64).ln())
.collect();
let wsum: f64 = raw.iter().sum();
let w: Vec<f64> = raw.iter().map(|&v| v / wsum).collect();
let mu_eff = 1.0 / w.iter().map(|&v| v * v).sum::<f64>();
let nf = n as f64;
let c_sigma = (mu_eff + 2.0) / (nf + mu_eff + 5.0);
let d_sigma = 1.0 + 2.0 * (((mu_eff - 1.0) / (nf + 1.0)).sqrt() - 1.0).max(0.0) + c_sigma;
let c_c = (4.0 + mu_eff / nf) / (nf + 4.0 + 2.0 * mu_eff / nf);
let c_1 = 2.0 / ((nf + 1.3).powi(2) + mu_eff);
let c_mu = (1.0 - c_1).min(2.0 * (mu_eff - 2.0 + 1.0 / mu_eff) / ((nf + 2.0).powi(2) + mu_eff));
let e_n = nf.sqrt() * (1.0 - 1.0 / (4.0 * nf) + 1.0 / (21.0 * nf * nf));
let mut mean = vec![0.5; n];
let mut sigma = sigma0;
let mut cov = identity(n);
let mut p_sigma = vec![0.0; n];
let mut p_c = vec![0.0; n];
let mut generation = 0i32;
let hist_len = 10 + (30.0 * nf / lambda as f64).ceil() as usize;
let mut hist: Vec<f64> = Vec::with_capacity(hist_len + 1);
let tolx = 1e-12 * sigma0;
let mut converged = false;
let mut best = Solution {
x: denormalize(&mean, bounds),
value: f64::INFINITY,
};
let mut evaluations = start_evals;
'outer: while term.reason(evaluations, best.value).is_none() {
let (eigvals, b) = jacobi_eigen(&cov);
let max_eig = eigvals.iter().cloned().fold(f64::MIN_POSITIVE, f64::max);
let d: Vec<f64> = eigvals
.iter()
.map(|&v| v.max(max_eig * 1e-14).sqrt())
.collect();
let mut pop: Vec<(f64, Vec<f64>)> = Vec::with_capacity(lambda);
for _ in 0..lambda {
if term.reason(evaluations, best.value).is_some() {
break 'outer; }
let z: Vec<f64> = (0..n).map(|_| rng.normal()).collect();
let dz: Vec<f64> = (0..n).map(|j| d[j] * z[j]).collect();
let y = matvec(&b, &dz); let u_c: Vec<f64> = (0..n)
.map(|i| (mean[i] + sigma * y[i]).clamp(0.0, 1.0))
.collect();
let x = denormalize(&u_c, bounds);
let f = problem.objective(&x);
evaluations += 1;
let f = if f.is_finite() { f } else { f64::INFINITY };
if f < best.value {
best = Solution { x, value: f };
}
let y_eff: Vec<f64> = (0..n).map(|i| (u_c[i] - mean[i]) / sigma).collect();
pop.push((f, y_eff));
}
pop.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let mut y_w = vec![0.0; n];
for (i, wi) in w.iter().enumerate() {
for k in 0..n {
y_w[k] += wi * pop[i].1[k];
}
}
for k in 0..n {
mean[k] += sigma * y_w[k];
}
let c_inv_sqrt_yw = c_inv_sqrt_mul(&b, &d, &y_w);
let cs_factor = (c_sigma * (2.0 - c_sigma) * mu_eff).sqrt();
for k in 0..n {
p_sigma[k] = (1.0 - c_sigma) * p_sigma[k] + cs_factor * c_inv_sqrt_yw[k];
}
let ps_norm = norm(&p_sigma);
generation += 1;
let hsig = if ps_norm / (1.0 - (1.0 - c_sigma).powi(2 * generation)).sqrt()
< (1.4 + 2.0 / (nf + 1.0)) * e_n
{
1.0
} else {
0.0
};
let cc_factor = (c_c * (2.0 - c_c) * mu_eff).sqrt();
for k in 0..n {
p_c[k] = (1.0 - c_c) * p_c[k] + hsig * cc_factor * y_w[k];
}
let delta_hsig = (1.0 - hsig) * c_c * (2.0 - c_c);
for a in 0..n {
for bcol in a..n {
let mut rank_mu = 0.0;
for (i, wi) in w.iter().enumerate() {
rank_mu += wi * pop[i].1[a] * pop[i].1[bcol];
}
let rank_one = p_c[a] * p_c[bcol];
let val = (1.0 - c_1 - c_mu) * cov[a][bcol]
+ c_1 * (rank_one + delta_hsig * cov[a][bcol])
+ c_mu * rank_mu;
cov[a][bcol] = val;
cov[bcol][a] = val; }
}
sigma *= ((c_sigma / d_sigma) * (ps_norm / e_n - 1.0)).exp();
if !(1e-300..=1e6).contains(&sigma) {
converged = true; break;
}
hist.push(pop[0].0);
if hist.len() > hist_len {
hist.remove(0);
let (lo, hi) = hist
.iter()
.fold((f64::INFINITY, f64::NEG_INFINITY), |(l, h), &v| {
(l.min(v), h.max(v))
});
if hi - lo < 1e-12 {
converged = true; break;
}
}
let tolx_met = (0..n)
.all(|k| (sigma * p_c[k]).abs() < tolx && sigma * cov[k][k].max(0.0).sqrt() < tolx);
if tolx_met {
converged = true; break;
}
let min_eig = eigvals.iter().cloned().fold(f64::INFINITY, f64::min);
if max_eig / min_eig.max(f64::MIN_POSITIVE) > 1e14 {
converged = true; break;
}
}
CmaRunOutcome {
best,
evaluations,
converged,
}
}
fn denormalize(u: &[f64], bounds: &[(f64, f64)]) -> Vec<f64> {
u.iter()
.zip(bounds)
.map(|(&ui, &(lo, hi))| lo + ui * (hi - lo))
.collect()
}
fn identity(n: usize) -> Vec<Vec<f64>> {
let mut m = vec![vec![0.0; n]; n];
for (i, row) in m.iter_mut().enumerate() {
row[i] = 1.0;
}
m
}
fn matvec(m: &[Vec<f64>], v: &[f64]) -> Vec<f64> {
m.iter()
.map(|row| row.iter().zip(v).map(|(a, b)| a * b).sum())
.collect()
}
fn norm(v: &[f64]) -> f64 {
v.iter().map(|x| x * x).sum::<f64>().sqrt()
}
fn c_inv_sqrt_mul(b: &[Vec<f64>], d: &[f64], v: &[f64]) -> Vec<f64> {
let n = v.len();
let mut a = vec![0.0; n];
for (j, aj) in a.iter_mut().enumerate() {
for i in 0..n {
*aj += b[i][j] * v[i];
}
*aj /= d[j];
}
matvec(b, &a)
}
fn jacobi_eigen(input: &[Vec<f64>]) -> (Vec<f64>, Vec<Vec<f64>>) {
let n = input.len();
let mut a: Vec<Vec<f64>> = input.to_vec();
let mut v = identity(n);
if n == 1 {
return (vec![a[0][0]], v);
}
for _ in 0..100 {
let mut off = 0.0;
for p in 0..n {
for q in p + 1..n {
off += a[p][q] * a[p][q];
}
}
let scale = (0..n)
.map(|i| a[i][i].abs())
.fold(f64::MIN_POSITIVE, f64::max);
if off.sqrt() < 1e-14 * scale {
break;
}
for p in 0..n {
for q in p + 1..n {
if a[p][q].abs() < 1e-300 {
continue;
}
let theta = (a[q][q] - a[p][p]) / (2.0 * a[p][q]);
let t = theta.signum() / (theta.abs() + (theta * theta + 1.0).sqrt());
let c = 1.0 / (t * t + 1.0).sqrt();
let s = t * c;
for k in 0..n {
let akp = a[k][p];
let akq = a[k][q];
a[k][p] = c * akp - s * akq;
a[k][q] = s * akp + c * akq;
}
for k in 0..n {
let apk = a[p][k];
let aqk = a[q][k];
a[p][k] = c * apk - s * aqk;
a[q][k] = s * apk + c * aqk;
}
for k in 0..n {
let vkp = v[k][p];
let vkq = v[k][q];
v[k][p] = c * vkp - s * vkq;
v[k][q] = s * vkp + c * vkq;
}
}
}
}
let eigvals = (0..n).map(|i| a[i][i]).collect();
(eigvals, v)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn jacobi_recovers_known_eigenpairs() {
let (vals, vecs) = jacobi_eigen(&[vec![2.0, 1.0], vec![1.0, 2.0]]);
let mut sorted = vals.clone();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
assert!((sorted[0] - 1.0).abs() < 1e-9 && (sorted[1] - 3.0).abs() < 1e-9);
for j in 0..2 {
let col_norm = (0..2).map(|i| vecs[i][j] * vecs[i][j]).sum::<f64>().sqrt();
assert!((col_norm - 1.0).abs() < 1e-9);
}
}
}