use std::collections::VecDeque;
use super::cma_es::workspace::{BoundPenalty, Distribution};
use crate::core::constraint::evaluation::BoxProblem;
use crate::core::math::{
ClampInPlace, ComponentMulAssign, MatDiagonal, 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;
use super::cma_es::{
CmaConstants, apply_permutation, compute_constants, nan_last_cmp,
};
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BoundedCmaEs<V, M, F: Scalar = f64> {
distribution_tolerance: Option<F>,
lambda_override: Option<usize>,
constants: Option<BoundedCmaConstants<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 BoundedCmaConstants<F = f64> {
cma: CmaConstants<F>,
damp: F,
edist_threshold: F,
hist_cap: usize,
}
impl<V, M, F: Scalar> BoundedCmaEs<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
}
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,
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<super::cma_es::diagnostics::DistributionRef<'_, V, M, F>> {
self.work.as_ref().map(|work| {
super::cma_es::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 genotypes(&self) -> &[V] {
self.work.as_ref().map_or(&[], |work| &work.candidates)
}
pub fn penalized_costs(&self) -> &[F] {
self.work.as_ref().map_or(&[], |work| &work.costs)
}
pub fn with_lambda(mut self, lambda: usize) -> Self {
assert!(
lambda >= 4,
"BoundedCmaEs 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
}
}
fn compute_bounded_constants<F: Scalar>(
n: usize,
lambda: usize,
) -> BoundedCmaConstants<F> {
let cma = compute_constants::<F>(n, lambda);
let one = F::one();
let n_f = F::from_usize(n).unwrap();
let three = F::from_f64(3.0).unwrap();
let ten = F::from_f64(10.0).unwrap();
let damp = (cma.mu_eff / (ten * n_f)).min(one);
let edist_threshold = three * n_f.sqrt().max(one)
/ cma.mu_eff.max(F::from_f64(f64::MIN_POSITIVE).unwrap());
let hist_cap = 20 + (3 * n) / lambda;
BoundedCmaConstants {
cma,
damp,
edist_threshold,
hist_cap,
}
}
pub(crate) fn evaluate_with_penalty<P, V, F>(
problem: &mut Problem<P>,
x: &V,
lower: &V,
upper: &V,
gamma: &V,
n: usize,
) -> Result<(F, F), P::Error>
where
F: Scalar,
P: CostFunction<Param = V, Output = F>,
V: Clone + ClampInPlace + std::ops::Index<usize, Output = F>,
{
let mut x_rep = x.clone();
x_rep.clamp_in_place(lower, upper);
let raw = problem.cost(&x_rep)?;
let mut penalty = F::zero();
for i in 0..n {
let dx = x[i] - x_rep[i];
penalty = penalty + gamma[i] * dx * dx;
}
penalty = penalty / F::from_usize(n).unwrap();
Ok((raw, raw + penalty))
}
fn update_gamma<P, V, M, F>(
state: &mut Distribution<V, M, F>,
k: &BoundedCmaConstants<F>,
problem: &P,
) where
F: Scalar,
P: BoxProblem<Param = V>,
V: Clone
+ ClampInPlace
+ std::ops::Index<usize, Output = F>
+ std::ops::IndexMut<usize, Output = F>,
M: MatDiagonal<V>,
{
let n = k.cma.n;
let pen = state
.penalty
.as_mut()
.expect("BoundedCmaEs::init installs the penalty before next_iter");
if pen.raw_costs.is_empty() {
return;
}
let zero = F::zero();
let two = F::from_f64(2.0).unwrap();
let three = F::from_f64(3.0).unwrap();
let five = F::from_f64(5.0).unwrap();
let n_f = F::from_usize(n).unwrap();
let diag_c = state.c.diagonal();
let mut mean_varis = zero;
for i in 0..n {
mean_varis = mean_varis + state.sigma * state.sigma * diag_c[i];
}
mean_varis = mean_varis / n_f;
let mut m_rep = state.m.clone();
m_rep.clamp_in_place(problem.lower(), problem.upper());
let mut dmean: Vec<F> = Vec::with_capacity(n);
let mut any_violation = false;
for i in 0..n {
let var_i = state.sigma * state.sigma * diag_c[i];
let d = (state.m[i] - m_rep[i]) / var_i.sqrt();
if d != zero {
any_violation = true;
}
dmean.push(d);
}
let mut sorted = pen.raw_costs.clone();
sorted
.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let l = 1 + sorted.len();
let val = (sorted[3 * l / 4] - sorted[l / 4]) / mean_varis;
if val.is_finite() && val > zero {
pen.hist.push_front(val);
} else if val == F::infinity() && !pen.hist.is_empty() {
let max_hist = pen
.hist
.iter()
.copied()
.fold(F::neg_infinity(), |a, b| if b > a { b } else { a });
pen.hist.push_front(max_hist);
}
while pen.hist.len() > k.hist_cap {
pen.hist.pop_back();
}
if pen.hist.is_empty() {
return;
}
let mut hsorted: Vec<F> = pen.hist.iter().copied().collect();
hsorted
.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let dfit = hsorted[hsorted.len() / 2];
if any_violation && !pen.weights_initialized {
let init_val = two * dfit;
for i in 0..n {
pen.gamma[i] = init_val;
}
pen.weights_initialized = true;
}
if pen.weights_initialized {
for (i, dmean_i) in dmean.iter().enumerate() {
let edist_i = dmean_i.abs() - k.edist_threshold;
if edist_i > zero {
let factor = ((edist_i / three).tanh() / two * k.damp).exp();
pen.gamma[i] = pen.gamma[i] * factor;
}
}
let cap = five * dfit;
let decay = (-k.damp / three).exp();
for i in 0..n {
if pen.gamma[i] > cap {
pen.gamma[i] = pen.gamma[i] * decay;
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn sample_and_penalize<P, V, M, F>(
state: &mut Distribution<V, M, F>,
lambda: usize,
n: usize,
rng: &mut ChaCha8Rng,
problem: &mut Problem<P>,
lo: &V,
hi: &V,
) -> Result<(), P::Error>
where
F: Scalar,
P: CostFunction<Param = V, Output = F>,
V: VectorLen
+ Clone
+ ScaledAdd<F>
+ ComponentMulAssign
+ ClampInPlace
+ SampleStandardNormal
+ std::ops::Index<usize, Output = F>,
M: MatVec<V>,
{
let Distribution {
candidates,
costs,
objective_costs,
m,
d,
b,
sigma,
penalty,
..
} = state;
let pen = penalty
.as_mut()
.expect("BoundedCmaEs::init installs the penalty before sampling");
candidates.clear();
costs.clear();
objective_costs.clear();
pen.raw_costs.clear();
for _ in 0..lambda {
let z_k = V::sample_standard_normal(m, rng);
let mut bd_z = z_k;
bd_z.component_mul_assign(d);
let bd_z = b.matvec(&bd_z);
let mut x_k = m.clone();
x_k.scaled_add(*sigma, &bd_z);
let (raw, p) =
evaluate_with_penalty(problem, &x_k, lo, hi, &pen.gamma, n)?;
candidates.push(x_k);
costs.push(p);
objective_costs.push(raw);
pen.raw_costs.push(raw);
}
Ok(())
}
impl<P, V, M, F> Solver<P, PopulationProgress<V, F>> for BoundedCmaEs<V, M, F>
where
F: Scalar,
P: CostFunction<Param = V, Output = F> + BoxProblem<Param = V>,
V: VectorLen
+ Clone
+ ScaledAdd<F>
+ ScaleInPlace<F>
+ ComponentMulAssign
+ ClampInPlace
+ NormSquared<F>
+ SampleStandardNormal
+ std::ops::Index<usize, Output = F>
+ std::ops::IndexMut<usize, Output = F>,
M: MatrixIdentity
+ MatrixFromDiagonal<V>
+ MatVec<V>
+ MatTransposeVec<V>
+ MatDiagonal<V>
+ ScaleInPlace<F>
+ RankOneUpdate<V, F>
+ SymmetricEigen<V>
+ Clone,
V: crate::VectorLen + std::ops::Index<usize, Output = F>,
{
type Error = crate::ConstraintSolveError<P::Error>;
fn init(
&mut self,
problem: &mut Problem<P>,
mut state: PopulationProgress<V, F>,
) -> Result<PopulationProgress<V, F>, Self::Error> {
crate::core::constraint::evaluation::validate_box_problem(
problem.inner(),
None,
false,
)
.map_err(crate::core::constraint::evaluation::invalid)?;
let mut mean = state
.seed()
.expect("BoundedCmaEs 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_bounded_constants::<F>(n, lambda));
let lo = problem.inner().lower().clone();
let hi = problem.inner().upper().clone();
assert_eq!(lo.vec_len(), n, "lower bounds must match the mean");
assert_eq!(hi.vec_len(), n, "upper bounds must match the mean");
mean.clamp_in_place(&lo, &hi);
let mut gamma = mean.clone();
for i in 0..n {
gamma[i] = F::one();
}
let mut work = Distribution::new(mean, self.initial_sigma);
if let Some(stds) = &self.initial_stds {
work = work.with_stds(stds.clone());
}
work.penalty = Some(BoundPenalty {
gamma,
weights_initialized: false,
hist: VecDeque::new(),
raw_costs: Vec::with_capacity(lambda),
});
sample_and_penalize(
&mut work,
lambda,
n,
&mut self.rng,
problem,
&lo,
&hi,
)?;
sort_bounded_population(&mut work);
let gamma = &work.penalty.as_ref().unwrap().gamma;
let (raw, _) =
evaluate_with_penalty(problem, &work.m, &lo, &hi, gamma, n)?;
work.m_cost = Some(raw);
publish(&work, &mut state, &lo, &hi);
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("BoundedCmaEs::init must run before next_iter");
let kc = &k.cma;
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(kc.mu) {
y_w.scaled_add(kc.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 - kc.c_sigma);
let coef_sigma = (kc.c_sigma * (two - kc.c_sigma) * kc.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 =
(kc.c_sigma / kc.d_sigma) * (p_sigma_norm / kc.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 - kc.c_sigma).powi(exponent)).sqrt();
let h_sigma = if p_sigma_norm / denom < kc.h_sigma_threshold {
one
} else {
zero
};
work.p_c.scale_in_place(one - kc.c_c);
let coef_c = h_sigma * (kc.c_c * (two - kc.c_c) * kc.mu_eff).sqrt();
work.p_c.scaled_add(coef_c, &y_w);
let delta_h = (one - h_sigma) * kc.c_c * (two - kc.c_c);
let c_scale = one + kc.c_1 * delta_h - kc.c_1 - kc.c_mu * kc.sum_w;
work.c.scale_in_place(c_scale);
work.c.rank_one_update(kc.c_1, &work.p_c);
let n_f = F::from_usize(kc.n).unwrap();
for (i, y_i) in y_sorted.iter().enumerate() {
let w_i = kc.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(kc.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(_) => {
return Ok(crate::SolverStep::from((
state,
Some(Termination::numerical_failure(
"Bounded CMA-ES covariance eigendecomposition failed.",
)),
)));
}
};
work.b = b_new;
let eig_floor = F::from_f64(1e-30).unwrap();
for i in 0..kc.n {
let lam = eigs[i].max(eig_floor);
let s = lam.sqrt();
work.d[i] = s;
work.d_inv[i] = one / s;
}
update_gamma(work, k, problem.inner());
let n = kc.n;
let lambda = kc.lambda;
let lo = problem.inner().lower().clone();
let hi = problem.inner().upper().clone();
sample_and_penalize(work, lambda, n, &mut self.rng, problem, &lo, &hi)?;
sort_bounded_population(work);
let gamma = &work.penalty.as_ref().unwrap().gamma;
let (raw, _pen_m) =
evaluate_with_penalty(problem, &work.m, &lo, &hi, gamma, n)?;
work.m_cost = Some(raw);
publish(work, &mut state, &lo, &hi);
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,
)
})
}
}
pub(crate) fn sort_bounded_population<V, M, F: Scalar>(
work: &mut Distribution<V, M, F>,
) {
let mut indices: Vec<_> = (0..work.costs.len()).collect();
indices.sort_by(|&i, &j| nan_last_cmp(&work.costs[i], &work.costs[j]));
apply_permutation(&mut work.candidates, &indices);
apply_permutation(&mut work.costs, &indices);
apply_permutation(&mut work.objective_costs, &indices);
}
pub(crate) fn publish<V: VectorLen + Clone + ClampInPlace, M, F: Scalar>(
work: &Distribution<V, M, F>,
state: &mut PopulationProgress<V, F>,
lo: &V,
hi: &V,
) {
let candidates = work
.candidates
.iter()
.map(|x| {
let mut x = x.clone();
x.clamp_in_place(lo, hi);
x
})
.collect();
state
.replace(candidates, work.objective_costs.clone())
.expect("CMA generation has matching shapes");
let mut mean = work.m.clone();
mean.clamp_in_place(lo, hi);
state
.publish_representative(
mean,
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 BoundedCmaEs<V, M, F>
{
type State = PopulationProgress<V, F>;
fn seed(&self, x: &V) -> Self::State {
PopulationProgress::from_point(x.clone())
}
}