use crate::core::math::{
ComponentMulAssign, MatTransposeVec, MatVec, MatrixFromDiagonal,
MatrixIdentity, NormSquared, RankOneUpdate, SampleStandardNormal, Scalar,
ScaleInPlace, ScaledAdd, SymmetricEigen, VectorLen,
};
use crate::core::problem::{CostFunction, Problem};
use crate::core::rng::{ChaCha8Rng, SeedableRng};
use crate::core::solver::Solver;
use crate::core::state::PopulationProgress;
use crate::core::termination::Termination;
pub mod diagnostics;
pub(crate) mod workspace;
use workspace::Distribution;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CmaEs<V, M, F: Scalar = f64> {
distribution_tolerance: Option<F>,
distribution_tolerance_configured: bool,
lambda_override: Option<usize>,
constants: Option<CmaConstants<F>>,
rng: ChaCha8Rng,
initial_sigma: F,
initial_stds: Option<V>,
seed: u64,
pub(crate) work: Option<Distribution<V, M, F>>,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub(crate) struct CmaConstants<F = f64> {
pub(crate) n: usize,
pub(crate) lambda: usize,
pub(crate) mu: usize,
pub(crate) weights: Vec<F>,
pub(crate) mu_eff: F,
pub(crate) sum_w: F,
pub(crate) c_sigma: F,
pub(crate) d_sigma: F,
pub(crate) c_c: F,
pub(crate) c_1: F,
pub(crate) c_mu: F,
pub(crate) expected_norm: F,
pub(crate) h_sigma_threshold: F,
}
impl<V, M, F: Scalar> CmaEs<V, M, F> {
pub fn with_absolute_distribution_size_tolerance(
mut self,
value: impl Into<Option<F>>,
) -> Self {
self.distribution_tolerance =
crate::core::convergence::optional_tolerance(value);
self.distribution_tolerance_configured = true;
self
}
pub fn new(seed: u64, sigma: F) -> Self {
assert!(
sigma.is_finite() && sigma > F::zero(),
"initial sigma must be finite and positive"
);
Self {
lambda_override: None,
distribution_tolerance: None,
distribution_tolerance_configured: false,
constants: None,
rng: ChaCha8Rng::seed_from_u64(seed),
initial_sigma: sigma,
initial_stds: None,
seed,
work: None,
}
}
pub fn with_stds(mut self, stds: impl Into<Option<V>>) -> Self {
self.initial_stds = stds.into();
self
}
pub fn mean(&self) -> Option<&V> {
self.work.as_ref().map(|work| &work.m)
}
pub fn sigma(&self) -> Option<F> {
self.work.as_ref().map(|work| work.sigma)
}
pub fn distribution(
&self,
) -> Option<diagnostics::DistributionRef<'_, V, M, F>> {
self.work.as_ref().map(|work| diagnostics::DistributionRef {
mean: &work.m,
sigma: work.sigma,
distribution_tolerance: self.distribution_tolerance,
covariance: &work.c,
principal_axis_stds: &work.d,
covariance_path: &work.p_c,
step_size_path: &work.p_sigma,
generation: work.generation,
})
}
pub fn with_lambda(mut self, lambda: usize) -> Self {
assert!(
lambda >= 4,
"CmaEs requires lambda >= 4, got {} (Hansen 2016 footnote 30: \
smaller populations have strong adverse effects on performance)",
lambda
);
self.lambda_override = Some(lambda);
self
}
pub fn default_lambda(n: usize) -> usize {
4 + (3.0 * (n as f64).ln()).floor() as usize
}
}
pub(crate) fn expected_norm_n01<F: Scalar>(n: usize) -> F {
let n_f = F::from_usize(n).unwrap();
let one = F::one();
let four = F::from_f64(4.0).unwrap();
let twenty_one = F::from_f64(21.0).unwrap();
n_f.sqrt() * (one - one / (four * n_f) + one / (twenty_one * n_f * n_f))
}
pub(crate) fn compute_weights<F: Scalar>(
n: usize,
lambda: usize,
c_1: F,
c_mu: F,
) -> (Vec<F>, F, F) {
let mu = lambda / 2;
let one = F::one();
let two = F::from_f64(2.0).unwrap();
let zero = F::zero();
let lambda_f = F::from_usize(lambda).unwrap();
let raw: Vec<F> = (1..=lambda)
.map(|i| ((lambda_f + one) / two).ln() - F::from_usize(i).unwrap().ln())
.collect();
let sum_pos: F = raw[..mu].iter().copied().sum();
let raw_pos_norm_sq: F = raw[..mu].iter().map(|w| *w * *w).sum();
let mu_eff = sum_pos * sum_pos / raw_pos_norm_sq;
let sum_neg: F = raw[mu..].iter().copied().sum();
let raw_neg_norm_sq: F = raw[mu..].iter().map(|w| *w * *w).sum();
let mu_eff_neg = if raw_neg_norm_sq > zero {
sum_neg * sum_neg / raw_neg_norm_sq
} else {
zero
};
let alpha_mu_minus = one + c_1 / c_mu;
let alpha_mu_eff_minus = one + two * mu_eff_neg / (mu_eff + two);
let alpha_pos_def_minus =
(one - c_1 - c_mu) / (F::from_usize(n).unwrap() * c_mu);
let alpha_neg = alpha_mu_minus
.min(alpha_mu_eff_minus)
.min(alpha_pos_def_minus);
let sum_abs_neg: F = raw[mu..].iter().map(|w| -*w).sum();
let mut weights = Vec::with_capacity(lambda);
for (i, &raw_i) in raw.iter().enumerate() {
let w = if i < mu {
raw_i / sum_pos
} else if sum_abs_neg > zero {
alpha_neg * raw_i / sum_abs_neg
} else {
zero
};
weights.push(w);
}
let sum_w: F = weights.iter().copied().sum();
(weights, mu_eff, sum_w)
}
pub(crate) fn compute_constants<F: Scalar>(
n: usize,
lambda: usize,
) -> CmaConstants<F> {
let mu = lambda / 2;
let one = F::one();
let two = F::from_f64(2.0).unwrap();
let zero = F::zero();
let n_f = F::from_usize(n).unwrap();
let lambda_f = F::from_usize(lambda).unwrap();
let alpha_cov = two;
let raw: Vec<F> = (1..=lambda)
.map(|i| ((lambda_f + one) / two).ln() - F::from_usize(i).unwrap().ln())
.collect();
let sum_pos: F = raw[..mu].iter().copied().sum();
let mu_eff_provisional =
sum_pos * sum_pos / raw[..mu].iter().map(|w| *w * *w).sum::<F>();
let c_1 = alpha_cov
/ ((n_f + F::from_f64(1.3).unwrap())
* (n_f + F::from_f64(1.3).unwrap())
+ mu_eff_provisional);
let c_mu_unbounded = alpha_cov
* (mu_eff_provisional - two + one / mu_eff_provisional)
/ ((n_f + two) * (n_f + two) + alpha_cov * mu_eff_provisional / two);
let c_mu = (one - c_1).min(c_mu_unbounded);
let (weights, mu_eff, sum_w) = compute_weights::<F>(n, lambda, c_1, c_mu);
let c_sigma = (mu_eff + two) / (n_f + mu_eff + F::from_f64(5.0).unwrap());
let d_sigma = {
let inner = ((mu_eff - one) / (n_f + one)).sqrt() - one;
one + two * inner.max(zero) + c_sigma
};
let c_c = (F::from_f64(4.0).unwrap() + mu_eff / n_f)
/ (n_f + F::from_f64(4.0).unwrap() + two * mu_eff / n_f);
let expected_norm = expected_norm_n01::<F>(n);
let h_sigma_threshold =
(F::from_f64(1.4).unwrap() + two / (n_f + one)) * expected_norm;
CmaConstants {
n,
lambda,
mu,
weights,
mu_eff,
sum_w,
c_sigma,
d_sigma,
c_c,
c_1,
c_mu,
expected_norm,
h_sigma_threshold,
}
}
pub(crate) fn sample_generation<V, M, F>(
state: &mut Distribution<V, M, F>,
lambda: usize,
rng: &mut ChaCha8Rng,
) where
F: Scalar,
V: VectorLen
+ Clone
+ ScaledAdd<F>
+ ComponentMulAssign
+ SampleStandardNormal,
M: MatVec<V>,
{
state.candidates.clear();
for _ in 0..lambda {
let z_k = V::sample_standard_normal(&state.m, rng);
let mut bd_z = z_k;
bd_z.component_mul_assign(&state.d);
let bd_z = state.b.matvec(&bd_z);
let mut x_k = state.m.clone();
x_k.scaled_add(state.sigma, &bd_z);
state.candidates.push(x_k);
}
}
pub(crate) fn sort_population_ascending<V, F: PartialOrd>(
candidates: &mut [V],
costs: &mut [F],
) {
let n = candidates.len();
debug_assert_eq!(n, costs.len());
let mut idx: Vec<usize> = (0..n).collect();
idx.sort_by(|&i, &j| nan_last_cmp(&costs[i], &costs[j]));
apply_permutation(candidates, &idx);
apply_permutation(costs, &idx);
}
pub(crate) fn nan_last_cmp<F: PartialOrd>(a: &F, b: &F) -> std::cmp::Ordering {
a.partial_cmp(b).unwrap_or_else(|| {
let a_nan = a.partial_cmp(a).is_none();
let b_nan = b.partial_cmp(b).is_none();
match (a_nan, b_nan) {
(true, false) => std::cmp::Ordering::Greater,
(false, true) => std::cmp::Ordering::Less,
_ => std::cmp::Ordering::Equal,
}
})
}
pub(crate) fn apply_permutation<T>(slice: &mut [T], idx: &[usize]) {
let mut visited = vec![false; slice.len()];
for start in 0..slice.len() {
if visited[start] || idx[start] == start {
visited[start] = true;
continue;
}
let mut current = start;
loop {
let next = idx[current];
visited[current] = true;
if next == start {
break;
}
slice.swap(current, next);
current = next;
}
}
}
impl<P, V, M, F> Solver<P, PopulationProgress<V, F>> for CmaEs<V, M, F>
where
F: Scalar + crate::core::parallel::MaybeSend,
P: CostFunction<Param = V, Output = F> + crate::core::parallel::MaybeSync,
P::Error: crate::core::parallel::MaybeSend,
V: VectorLen
+ Clone
+ ScaledAdd<F>
+ ScaleInPlace<F>
+ ComponentMulAssign
+ NormSquared<F>
+ SampleStandardNormal
+ crate::core::parallel::MaybeSync
+ std::ops::Index<usize, Output = F>
+ std::ops::IndexMut<usize, Output = F>,
M: MatrixIdentity
+ MatrixFromDiagonal<V>
+ MatVec<V>
+ MatTransposeVec<V>
+ ScaleInPlace<F>
+ RankOneUpdate<V, F>
+ SymmetricEigen<V>
+ Clone,
{
type Error = P::Error;
fn init(
&mut self,
problem: &mut Problem<P>,
mut state: PopulationProgress<V, F>,
) -> Result<PopulationProgress<V, F>, Self::Error> {
let mean = state.seed().expect("CmaEs requires a mean seed").clone();
state.reset();
self.rng = ChaCha8Rng::seed_from_u64(self.seed);
self.work = None;
let n = mean.vec_len();
let lambda = self
.lambda_override
.unwrap_or_else(|| Self::default_lambda(n));
self.constants = Some(compute_constants::<F>(n, lambda));
let mut work = Distribution::new(mean, self.initial_sigma);
if let Some(stds) = &self.initial_stds {
work = work.with_stds(stds.clone());
}
sample_generation(&mut work, lambda, &mut self.rng);
work.costs = problem.cost_batch(&work.candidates)?;
sort_population_ascending(&mut work.candidates, &mut work.costs);
work.m_cost = Some(problem.cost(&work.m)?);
publish(&mut work, &mut state);
self.work = Some(work);
Ok(state)
}
fn next_iter(
&mut self,
problem: &mut Problem<P>,
mut state: PopulationProgress<V, F>,
) -> Result<crate::SolverStep<PopulationProgress<V, F>>, Self::Error> {
let work = self.work.as_mut().expect("CMA init must precede iteration");
let k = self
.constants
.as_ref()
.expect("CmaEs::init must run before next_iter");
work.candidates = std::mem::take(&mut state.candidates);
work.costs = std::mem::take(&mut state.costs);
work.generation += 1;
let one = F::one();
let two = F::from_f64(2.0).unwrap();
let zero = F::zero();
let mut y_sorted: Vec<V> = work
.candidates
.iter()
.map(|x| {
let mut y = x.clone();
y.scaled_add(-one, &work.m);
y.scale_in_place(one / work.sigma);
y
})
.collect();
let mut y_w = work.m.clone();
y_w.scale_in_place(zero);
for (i, y_i) in y_sorted.iter().enumerate().take(k.mu) {
y_w.scaled_add(k.weights[i], y_i);
}
work.m.scaled_add(work.sigma, &y_w);
let mut bt_y_w = work.b.mat_transpose_vec(&y_w);
bt_y_w.component_mul_assign(&work.d_inv);
let c_invsqrt_y_w = work.b.matvec(&bt_y_w);
work.p_sigma.scale_in_place(one - k.c_sigma);
let coef_sigma = (k.c_sigma * (two - k.c_sigma) * k.mu_eff).sqrt();
work.p_sigma.scaled_add(coef_sigma, &c_invsqrt_y_w);
let p_sigma_norm = work.p_sigma.norm_squared().sqrt();
let log_factor =
(k.c_sigma / k.d_sigma) * (p_sigma_norm / k.expected_norm - one);
work.sigma = work.sigma * log_factor.exp();
let g_for_h = (work.generation + 1) as i32;
let exponent = 2 * g_for_h;
let denom = (one - (one - k.c_sigma).powi(exponent)).sqrt();
let h_sigma = if p_sigma_norm / denom < k.h_sigma_threshold {
one
} else {
zero
};
work.p_c.scale_in_place(one - k.c_c);
let coef_c = h_sigma * (k.c_c * (two - k.c_c) * k.mu_eff).sqrt();
work.p_c.scaled_add(coef_c, &y_w);
let delta_h = (one - h_sigma) * k.c_c * (two - k.c_c);
let c_scale = one + k.c_1 * delta_h - k.c_1 - k.c_mu * k.sum_w;
work.c.scale_in_place(c_scale);
work.c.rank_one_update(k.c_1, &work.p_c);
let n_f = F::from_usize(k.n).unwrap();
for (i, y_i) in y_sorted.iter().enumerate() {
let w_i = k.weights[i];
let w_i_o = if w_i >= zero {
w_i
} else {
let mut bt_y = work.b.mat_transpose_vec(y_i);
bt_y.component_mul_assign(&work.d_inv);
let cinv_norm_sq = bt_y.norm_squared();
if cinv_norm_sq > zero {
w_i * n_f / cinv_norm_sq
} else {
zero
}
};
if w_i_o != zero {
work.c.rank_one_update(k.c_mu * w_i_o, y_i);
}
}
drop(std::mem::take(&mut y_sorted));
let (b_new, eigs) = match work.c.try_eigh() {
Ok(pair) => pair,
Err(_) => {
state.candidates = std::mem::take(&mut work.candidates);
state.costs = std::mem::take(&mut work.costs);
return Ok(crate::SolverStep::from((
state,
Some(Termination::numerical_failure(
"CMA-ES covariance eigendecomposition failed.",
)),
)));
}
};
work.b = b_new;
let eig_floor = F::from_f64(1e-30).unwrap();
for i in 0..k.n {
let lam = eigs[i].max(eig_floor);
let s = lam.sqrt();
work.d[i] = s;
work.d_inv[i] = one / s;
}
let lambda = k.lambda;
sample_generation(work, lambda, &mut self.rng);
work.costs = problem.cost_batch(&work.candidates)?;
sort_population_ascending(&mut work.candidates, &mut work.costs);
let m_cost = problem.cost(&work.m)?;
work.m_cost = Some(m_cost);
publish(work, &mut state);
Ok(crate::SolverStep::from((state, None)))
}
fn terminate(
&self,
_state: &PopulationProgress<V, F>,
) -> Option<Termination<F>> {
let tolerance = self.distribution_tolerance?;
let work = self.work.as_ref()?;
let metric = work.sigma * work.max_axis_std();
(metric.is_finite()
&& (metric < tolerance
|| (tolerance == F::zero() && metric == F::zero())))
.then(|| {
Termination::upper_bound(
crate::ConvergenceTest::DistributionSize,
metric,
tolerance,
tolerance,
None,
)
})
}
}
impl<V, M, F> crate::core::inner::ResumableInner<V, F> for CmaEs<V, M, F>
where
F: Scalar,
V: VectorLen
+ Clone
+ ScaleInPlace<F>
+ std::ops::IndexMut<usize, Output = F>,
M: MatrixIdentity,
{
type State = PopulationProgress<V, F>;
fn seed_chain(
&self,
x: &V,
_fx: F,
scale: F,
seed: u64,
) -> (Self, Self::State) {
let cma = Self {
lambda_override: self.lambda_override,
distribution_tolerance: self.distribution_tolerance,
distribution_tolerance_configured: self
.distribution_tolerance_configured,
constants: None,
rng: ChaCha8Rng::seed_from_u64(seed),
initial_sigma: scale,
initial_stds: self.initial_stds.clone(),
seed,
work: None,
};
(cma, PopulationProgress::from_point(x.clone()))
}
fn prepare_resume(&self, state: &mut Self::State) {
state.reset_segment_iter();
}
fn configure_segment(
&mut self,
_state: &Self::State,
_control: &mut crate::RunControl<Self::State>,
) {
if !self.distribution_tolerance_configured {
self.distribution_tolerance = Some(
F::from_f64(1e-12).unwrap()
* self.sigma().unwrap_or(self.initial_sigma),
);
}
}
}
pub(crate) fn publish<V: VectorLen + Clone, M, F: Scalar>(
work: &mut Distribution<V, M, F>,
state: &mut PopulationProgress<V, F>,
) {
state
.replace(
std::mem::take(&mut work.candidates),
std::mem::take(&mut work.costs),
)
.expect("CMA generation has matching shapes");
state
.publish_representative_from(
&work.m,
work.m_cost.expect("CMA mean is evaluated"),
)
.expect("CMA mean matches the population");
}
impl<V: VectorLen + Clone, M, F: Scalar> crate::core::inner::InitialState<V>
for CmaEs<V, M, F>
{
type State = PopulationProgress<V, F>;
fn seed(&self, x: &V) -> Self::State {
PopulationProgress::from_point(x.clone())
}
}
#[cfg(test)]
mod zero_tolerance_tests {
use super::*;
use crate::{BoundedCmaEs, DenseMatrix, PopulationProgress};
use std::convert::Infallible;
struct FlatBox<F>(Vec<F>, Vec<F>);
impl<F: Scalar> crate::Constraints for FlatBox<F> {
type Set = crate::ConstraintSet<crate::BoxBounds<Vec<F>>>;
fn constraints(
&self,
) -> <Self::Set as crate::ConstraintDescription>::Borrowed<'_> {
crate::ConstraintSet::new().with_bounds(crate::BoxBounds::new(
Some(&self.0),
Some(&self.1),
))
}
}
impl<F: Scalar> CostFunction for FlatBox<F> {
type Param = Vec<F>;
type Output = F;
type Error = Infallible;
fn cost(&self, _: &Vec<F>) -> Result<F, Infallible> {
Ok(F::zero())
}
}
fn check<F: Scalar + Send + Sync>()
where
Vec<F>: SampleStandardNormal,
{
let mut problem = Problem::new(crate::SamplingDomain::new(FlatBox(
vec![-F::one()],
vec![F::one()],
)));
let mut solver: CmaEs<Vec<F>, DenseMatrix<F>, F> =
CmaEs::new(42, F::one())
.with_absolute_distribution_size_tolerance(F::zero());
let state = solver
.init(
&mut problem,
PopulationProgress::from_point(vec![F::zero()]),
)
.unwrap();
assert!(
<CmaEs<_, _, F> as Solver<crate::SamplingDomain<FlatBox<F>>, _>>::terminate(
&solver, &state
)
.is_none()
);
solver.work.as_mut().unwrap().sigma = F::zero();
assert!(
<CmaEs<_, _, F> as Solver<crate::SamplingDomain<FlatBox<F>>, _>>::terminate(
&solver, &state
)
.unwrap()
.is_converged()
);
let mut problem =
Problem::new(FlatBox(vec![-F::one()], vec![F::one()]));
let mut bounded: BoundedCmaEs<Vec<F>, DenseMatrix<F>, F> =
BoundedCmaEs::new(42, F::one())
.with_absolute_distribution_size_tolerance(F::zero());
let state = bounded
.init(
&mut problem,
PopulationProgress::from_point(vec![F::zero()]),
)
.unwrap();
assert!(
<BoundedCmaEs<_, _, F> as Solver<FlatBox<F>, _>>::terminate(
&bounded, &state
)
.is_none()
);
bounded.work.as_mut().unwrap().sigma = F::zero();
assert!(
<BoundedCmaEs<_, _, F> as Solver<FlatBox<F>, _>>::terminate(
&bounded, &state
)
.unwrap()
.is_converged()
);
}
#[test]
fn zero_tolx_observes_exact_distribution_collapse() {
check::<f64>();
check::<f32>();
}
}