use std::borrow::Cow;
use std::fmt::Debug;
use std::sync::{Arc, Mutex};
use rand::Rng;
use crate::configuration::ProblemSolving;
use crate::error::GaError;
use crate::fitness::BatchFitnessEvaluator;
use crate::ga::TerminationCause;
use crate::observer::GaObserver;
use crate::rng::make_rng;
use crate::stats::GenerationStats;
use crate::traits::{FitnessFn, LinearChromosome, RealGene};
use super::configuration::CmaConfiguration;
use super::restart::{RestartEvent, RestartKind, RestartStrategy};
fn jacobi_eigendecompose(c: &[f64], n: usize) -> (Vec<f64>, Vec<f64>) {
let mut a: Vec<f64> = c.to_vec();
let mut b: Vec<f64> = (0..n * n)
.map(|k| if k / n == k % n { 1.0 } else { 0.0 })
.collect();
for _ in 0..50 {
let mut max_off = 0.0_f64;
let mut p = 0;
let mut q = 1;
for i in 0..n {
for j in (i + 1)..n {
let abs_aij = a[i * n + j].abs();
if abs_aij > max_off {
max_off = abs_aij;
p = i;
q = j;
}
}
}
if max_off < 1e-12 {
break;
}
let theta = if (a[p * n + p] - a[q * n + q]).abs() < 1e-300 {
std::f64::consts::FRAC_PI_4
} else {
0.5 * ((2.0 * a[p * n + q]) / (a[p * n + p] - a[q * n + q])).atan()
};
let cos_t = theta.cos();
let sin_t = theta.sin();
let mut a_new = a.clone();
for k in 0..n {
if k != p && k != q {
let a_kp = a[k * n + p];
let a_kq = a[k * n + q];
a_new[k * n + p] = cos_t * a_kp + sin_t * a_kq;
a_new[p * n + k] = a_new[k * n + p];
a_new[k * n + q] = -sin_t * a_kp + cos_t * a_kq;
a_new[q * n + k] = a_new[k * n + q];
}
}
let app = a[p * n + p];
let aqq = a[q * n + q];
let apq = a[p * n + q];
a_new[p * n + p] = cos_t * cos_t * app + 2.0 * sin_t * cos_t * apq + sin_t * sin_t * aqq;
a_new[q * n + q] = sin_t * sin_t * app - 2.0 * sin_t * cos_t * apq + cos_t * cos_t * aqq;
a_new[p * n + q] = 0.0;
a_new[q * n + p] = 0.0;
a = a_new;
let mut b_new = b.clone();
for k in 0..n {
let b_kp = b[k * n + p];
let b_kq = b[k * n + q];
b_new[k * n + p] = cos_t * b_kp + sin_t * b_kq;
b_new[k * n + q] = -sin_t * b_kp + cos_t * b_kq;
}
b = b_new;
}
let mut d: Vec<f64> = (0..n).map(|i| a[i * n + i]).collect();
let max_d = d.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let floor = 1e-10 * max_d.max(1e-300); for di in &mut d {
if *di < floor {
*di = floor;
}
*di = di.sqrt();
}
(b, d)
}
fn compute_invsqrtc(b: &[f64], d: &[f64], n: usize) -> Vec<f64> {
let mut result = vec![0.0_f64; n * n];
for i in 0..n {
for j in 0..n {
let mut sum = 0.0;
for k in 0..n {
sum += b[i * n + k] * b[j * n + k] / d[k];
}
result[i * n + j] = sum;
}
}
result
}
fn standard_normal_pair<R: Rng>(rng: &mut R) -> (f64, f64) {
let u1: f64 = rng.random::<f64>().max(f64::MIN_POSITIVE);
let u2: f64 = rng.random::<f64>();
let mag = (-2.0 * u1.ln()).sqrt();
let angle = 2.0 * std::f64::consts::PI * u2;
(mag * angle.cos(), mag * angle.sin())
}
fn standard_normal<R: Rng>(rng: &mut R) -> f64 {
standard_normal_pair(rng).0
}
fn matvec(a: &[f64], x: &[f64], n: usize) -> Vec<f64> {
let mut y = vec![0.0_f64; n];
for i in 0..n {
let mut s = 0.0;
for j in 0..n {
s += a[i * n + j] * x[j];
}
y[i] = s;
}
y
}
struct CmaState {
n: usize,
lambda: usize,
mu: usize,
weights: Vec<f64>,
mu_eff: f64,
cs: f64,
ds: f64,
chi_n: f64,
cc: f64,
c1: f64,
cmu: f64,
t_eigen: usize,
mean: Vec<f64>,
sigma: f64,
pc: Vec<f64>,
ps: Vec<f64>,
c_mat: Vec<f64>,
b_mat: Vec<f64>,
d_vec: Vec<f64>,
invsqrtc: Vec<f64>,
eigeneval: usize,
}
impl CmaState {
fn new(n: usize, lambda: usize, config: &CmaConfiguration, initial_mean: Vec<f64>) -> Self {
let mu = lambda / 2;
let w_raw: Vec<f64> = (0..mu)
.map(|i| ((lambda as f64 + 1.0) / 2.0).ln() - ((i + 1) as f64).ln())
.collect();
let w_sum: f64 = w_raw.iter().sum();
let weights: Vec<f64> = w_raw.iter().map(|w| w / w_sum).collect();
let mu_eff = 1.0 / weights.iter().map(|w| w * w).sum::<f64>();
let nf = n as f64;
let cs = config.cs.unwrap_or((mu_eff + 2.0) / (nf + mu_eff + 5.0));
let ds = 1.0 + 2.0 * (((mu_eff - 1.0) / (nf + 1.0)).max(0.0)).sqrt() + cs;
let chi_n = nf.sqrt() * (1.0 - 1.0 / (4.0 * nf) + 1.0 / (21.0 * nf * nf));
let cc = config
.cc
.unwrap_or((4.0 + mu_eff / nf) / (nf + 4.0 + 2.0 * mu_eff / nf));
let c1 = config.c1.unwrap_or(2.0 / ((nf + 1.3).powi(2) + mu_eff));
let cmu = config.cmu.unwrap_or(
((2.0 * (mu_eff - 2.0 + 1.0 / mu_eff)) / ((nf + 2.0).powi(2) + mu_eff)).min(1.0 - c1),
);
let t_eigen = (((n as f64).powf(1.5) * 10.0 / lambda as f64).floor() as usize).max(1);
let identity: Vec<f64> = (0..n * n)
.map(|k| if k / n == k % n { 1.0 } else { 0.0 })
.collect();
let c_mat = identity.clone();
let b_mat = identity.clone();
let invsqrtc = identity;
let d_vec = vec![1.0_f64; n];
let pc = vec![0.0_f64; n];
let ps = vec![0.0_f64; n];
CmaState {
n,
lambda,
mu,
weights,
mu_eff,
cs,
ds,
chi_n,
cc,
c1,
cmu,
t_eigen,
mean: initial_mean,
sigma: config.sigma0,
pc,
ps,
c_mat,
b_mat,
d_vec,
invsqrtc,
eigeneval: 0,
}
}
}
pub struct CmaResult<U: LinearChromosome> {
pub population: Vec<U>,
pub best: U,
pub best_fitness: f64,
pub generations: usize,
pub total_restarts: usize,
}
pub struct CmaEngine<U: LinearChromosome>
where
U::Gene: RealGene,
{
config: CmaConfiguration,
init_fn: Arc<dyn Fn(usize) -> Vec<U> + Send + Sync>,
fitness_fn: Arc<FitnessFn<U::Gene>>,
observer: Option<Arc<dyn GaObserver<U> + Send + Sync>>,
batch_evaluator: Option<Arc<dyn BatchFitnessEvaluator<U> + Send + Sync>>,
fitness_cache: Option<Arc<Mutex<crate::fitness::cache::FitnessCache>>>,
}
impl<U: LinearChromosome + Clone> CmaEngine<U>
where
U::Gene: RealGene,
{
pub fn new(
config: CmaConfiguration,
init_fn: impl Fn(usize) -> Vec<U> + Send + Sync + 'static,
fitness_fn: impl Fn(&[U::Gene]) -> f64 + Send + Sync + 'static,
) -> Self {
Self {
config,
init_fn: Arc::new(init_fn),
fitness_fn: Arc::new(fitness_fn),
observer: None,
batch_evaluator: None,
fitness_cache: None,
}
}
pub fn with_observer(mut self, obs: Arc<dyn GaObserver<U> + Send + Sync>) -> Self {
self.observer = Some(obs);
self
}
pub fn with_batch_evaluator(
mut self,
evaluator: Arc<dyn BatchFitnessEvaluator<U> + Send + Sync>,
) -> Self {
self.batch_evaluator = Some(evaluator);
self
}
fn batch_evaluate_pop(&self, pop: &mut [U]) -> Result<(), GaError>
where
U::Gene: Debug,
{
let evaluator = match self.batch_evaluator.as_ref() {
Some(e) => Arc::clone(e),
None => return Ok(()),
};
if pop.is_empty() {
return Ok(());
}
match self.fitness_cache.as_ref() {
None => {
let values = evaluator.evaluate_batch(pop);
debug_assert_eq!(
values.len(),
pop.len(),
"evaluate_batch returned {} values for {} chromosomes (T-60-01)",
values.len(),
pop.len()
);
for (i, chromosome) in pop.iter_mut().enumerate() {
chromosome.set_fitness(values[i]);
}
}
Some(cache_handle) => {
let mut fitness_values: Vec<f64> = vec![0.0; pop.len()];
let mut miss_indices: Vec<usize> = Vec::new();
{
let mut cache = cache_handle.lock().map_err(|_| {
GaError::InternalError("fitness cache mutex poisoned".to_string())
})?;
for (i, chromosome) in pop.iter().enumerate() {
let key = crate::fitness::cache::hash_dna(chromosome.dna());
match cache.get(key) {
Some(f) => fitness_values[i] = f,
None => miss_indices.push(i),
}
}
}
if !miss_indices.is_empty() {
let miss_chromosomes: Vec<U> =
miss_indices.iter().map(|&i| pop[i].clone()).collect();
let miss_values = evaluator.evaluate_batch(&miss_chromosomes);
debug_assert_eq!(
miss_values.len(),
miss_indices.len(),
"evaluate_batch returned {} values for {} miss chromosomes (T-60-01)",
miss_values.len(),
miss_indices.len()
);
let mut cache = cache_handle.lock().map_err(|_| {
GaError::InternalError("fitness cache mutex poisoned".to_string())
})?;
for (pos, &orig_i) in miss_indices.iter().enumerate() {
let f = miss_values[pos];
fitness_values[orig_i] = f;
let key = crate::fitness::cache::hash_dna(pop[orig_i].dna());
cache.put(key, f);
}
}
for (i, chromosome) in pop.iter_mut().enumerate() {
chromosome.set_fitness(fitness_values[i]);
}
}
}
Ok(())
}
#[inline]
fn notify<F: FnOnce(&dyn GaObserver<U>)>(&self, f: F) {
if let Some(ref obs) = self.observer {
f(obs.as_ref());
}
}
#[inline]
fn is_better(&self, candidate: f64, current: f64) -> bool {
match self.config.problem_solving {
ProblemSolving::Minimization => candidate < current,
ProblemSolving::Maximization => candidate > current,
ProblemSolving::FixedFitness => {
if let Some(t) = self.config.fitness_target {
(candidate - t).abs() < (current - t).abs()
} else {
candidate < current
}
}
}
}
fn reached_target(&self, fitness: f64, target: f64) -> bool {
match self.config.problem_solving {
ProblemSolving::Minimization => fitness <= target,
ProblemSolving::Maximization => fitness >= target,
ProblemSolving::FixedFitness => (fitness - target).abs() < 1e-6,
}
}
fn find_best(&self, pop: &[U]) -> (usize, f64) {
let mut best_idx = 0;
let mut best_fit = pop[0].fitness();
for (i, ind) in pop.iter().enumerate().skip(1) {
if self.is_better(ind.fitness(), best_fit) {
best_fit = ind.fitness();
best_idx = i;
}
}
(best_idx, best_fit)
}
fn compute_next_lambda(
strategy: &RestartStrategy,
current_lambda: usize,
default_lambda: usize,
restart_count: usize,
) -> usize {
let raw = match strategy {
RestartStrategy::Ipop {
population_scale, ..
} => ((current_lambda as f64) * population_scale).floor() as usize,
RestartStrategy::Bipop {
population_scale,
small_population_size,
..
} => {
let next_restart_number = restart_count + 1;
if next_restart_number % 2 == 1 {
((current_lambda as f64) * population_scale).floor() as usize
} else if *small_population_size == 0 {
(default_lambda / 5).max(1)
} else {
*small_population_size
}
}
};
raw.max(2)
}
fn restart_kind(strategy: &RestartStrategy, restart_count: usize) -> RestartKind {
let next_restart_number = restart_count + 1;
match strategy {
RestartStrategy::Ipop { .. } => RestartKind::Ipop,
RestartStrategy::Bipop { .. } => {
if next_restart_number % 2 == 1 {
RestartKind::BipopLarge
} else {
RestartKind::BipopSmall
}
}
}
}
pub fn run(&mut self) -> Result<CmaResult<U>, GaError>
where
U::Gene: Debug,
{
let mut rng = make_rng();
let is_maximization = matches!(self.config.problem_solving, ProblemSolving::Maximization);
if let Some(size) = self.config.fitness_cache_size {
if self.fitness_cache.is_none() {
if self.batch_evaluator.is_none() {
let (wrapped_fn, cache_handle) =
crate::fitness::cache::wrap_with_cache(Arc::clone(&self.fitness_fn), size);
self.fitness_fn = wrapped_fn;
self.fitness_cache = Some(cache_handle);
} else {
self.fitness_cache = Some(Arc::new(Mutex::new(
crate::fitness::cache::FitnessCache::new(size),
)));
}
}
}
self.notify(|obs| obs.on_run_start());
let peek_size = self.config.population_size.max(1);
let peek_pop: Vec<U> = (self.init_fn)(peek_size);
if peek_pop.is_empty() {
crate::log_warn!(
target: "cma_events",
"CmaEngine: init_fn returned an empty population; returning empty result"
);
self.notify(|obs| obs.on_run_end(TerminationCause::GenerationLimitReached, &[]));
return Err(GaError::InitializationError(
"CmaEngine: init_fn returned an empty population".to_string(),
));
}
let n = peek_pop[0].dna().len();
assert!(
n > 0,
"CmaEngine: chromosomes must have non-zero DNA length"
);
let default_lambda = if self.config.population_size == 0 {
4 + (3.0 * (n as f64).ln()).floor() as usize
} else {
self.config.population_size
};
let mut total_restarts: usize = 0;
let mut current_lambda = default_lambda;
let mut global_best_fitness = if is_maximization {
f64::NEG_INFINITY
} else {
f64::INFINITY
};
let mut global_best: Option<U> = None;
let mut termination_cause = TerminationCause::GenerationLimitReached;
let mut all_stats: Vec<GenerationStats> = Vec::with_capacity(self.config.max_generations);
let mut pop: Vec<U> = (self.init_fn)(current_lambda);
if pop.is_empty() {
return Err(GaError::InitializationError(
"CmaEngine: init_fn returned an empty population (first init)".to_string(),
));
}
'restart_loop: loop {
if total_restarts > 0 {
pop = (self.init_fn)(current_lambda);
if pop.is_empty() {
return Err(GaError::InitializationError(
"CmaEngine: init_fn returned an empty population (restart)".to_string(),
));
}
}
let template = pop[0].clone();
if self.batch_evaluator.is_some() {
self.batch_evaluate_pop(&mut pop)?;
} else {
for ind in &mut pop {
let f = (self.fitness_fn)(ind.dna());
ind.set_fitness(f);
}
}
let mut restart_mean = vec![0.0_f64; n];
for chr in &pop {
for (j, g) in chr.dna().iter().enumerate() {
restart_mean[j] += g.real_value();
}
}
let pop_len = pop.len() as f64;
for v in &mut restart_mean {
*v /= pop_len;
}
let mut state = CmaState::new(n, current_lambda, &self.config, restart_mean);
debug_assert_eq!(state.n, n, "CmaState dimension mismatch");
debug_assert_eq!(state.lambda, current_lambda, "CmaState lambda mismatch");
let (b_init, d_init) = jacobi_eigendecompose(&state.c_mat, n);
state.b_mat = b_init;
state.d_vec = d_init;
state.invsqrtc = compute_invsqrtc(&state.b_mat, &state.d_vec, n);
let (restart_init_idx, restart_init_fitness) = self.find_best(&pop);
let mut restart_best_fitness = restart_init_fitness;
let mut stagnation_count: usize = 0;
if self.is_better(restart_init_fitness, global_best_fitness) {
global_best_fitness = restart_init_fitness;
global_best = Some(pop[restart_init_idx].clone());
self.notify(|obs| obs.on_new_best(0, global_best.as_ref().unwrap()));
}
for gen in 0..self.config.max_generations {
let (prev_cache_hits, prev_cache_misses) = match &self.fitness_cache {
Some(ch) => {
let c = ch.lock().map_err(|_| {
GaError::InternalError("fitness cache mutex poisoned".to_string())
})?;
(c.hits(), c.misses())
}
None => (0, 0),
};
self.notify(|obs| obs.on_generation_start(gen));
let mut offspring: Vec<U> = Vec::with_capacity(current_lambda);
for _ in 0..current_lambda {
let z_k: Vec<f64> = (0..n).map(|_| standard_normal(&mut rng)).collect();
let dz: Vec<f64> = (0..n).map(|i| state.d_vec[i] * z_k[i]).collect();
let y_k = matvec(&state.b_mat, &dz, n);
let x_k: Vec<f64> = (0..n)
.map(|j| state.mean[j] + state.sigma * y_k[j])
.collect();
let new_dna: Vec<U::Gene> = template
.dna()
.iter()
.enumerate()
.map(|(j, g)| g.with_real_value(x_k[j]))
.collect();
let mut child = template.clone();
child.set_dna(Cow::Owned(new_dna));
if self.batch_evaluator.is_none() {
let f = (self.fitness_fn)(child.dna());
child.set_fitness(f);
}
offspring.push(child);
}
if self.batch_evaluator.is_some() {
self.batch_evaluate_pop(&mut offspring)?;
}
pop = offspring;
let mut indices: Vec<usize> = (0..pop.len()).collect();
if is_maximization {
indices.sort_unstable_by(|&a, &b| {
pop[b]
.fitness()
.partial_cmp(&pop[a].fitness())
.unwrap_or(std::cmp::Ordering::Equal)
});
} else {
indices.sort_unstable_by(|&a, &b| {
pop[a]
.fitness()
.partial_cmp(&pop[b].fitness())
.unwrap_or(std::cmp::Ordering::Equal)
});
}
let mu = state.mu;
let selected_indices = &indices[..mu];
let old_mean = state.mean.clone();
let mut new_mean = vec![0.0_f64; n];
for (k, &idx) in selected_indices.iter().enumerate() {
for (j, nm) in new_mean.iter_mut().enumerate() {
*nm += state.weights[k] * pop[idx].dna()[j].real_value();
}
}
if !new_mean.iter().all(|v| v.is_finite()) {
crate::log_warn!(
target: "cma_events",
"CmaEngine generation {}: new_mean contains NaN/Inf — stopping early",
gen
);
break;
}
let step: Vec<f64> = (0..n)
.map(|i| (new_mean[i] - old_mean[i]) / state.sigma)
.collect();
let invsqrtc_step = matvec(&state.invsqrtc, &step, n);
let sqrt_cs_factor = (state.cs * (2.0 - state.cs) * state.mu_eff).sqrt();
let ps_new: Vec<f64> = (0..n)
.map(|i| (1.0 - state.cs) * state.ps[i] + sqrt_cs_factor * invsqrtc_step[i])
.collect();
state.ps = ps_new;
let ps_norm = state.ps.iter().map(|x| x * x).sum::<f64>().sqrt();
let denom = (1.0 - (1.0 - state.cs).powi(2 * (gen + 1) as i32)).sqrt();
let h_sigma = if ps_norm / denom / state.chi_n < 1.4 + 2.0 / (n as f64 + 1.0) {
1.0_f64
} else {
0.0_f64
};
let sqrt_cc_factor = (state.cc * (2.0 - state.cc) * state.mu_eff).sqrt();
let pc_new: Vec<f64> = (0..n)
.map(|i| (1.0 - state.cc) * state.pc[i] + h_sigma * sqrt_cc_factor * step[i])
.collect();
state.pc = pc_new;
let mut c_new: Vec<f64> = state
.c_mat
.iter()
.map(|&v| (1.0 - state.c1 - state.cmu) * v)
.collect();
for i in 0..n {
for j in 0..n {
c_new[i * n + j] += state.c1
* (state.pc[i] * state.pc[j]
+ (1.0 - h_sigma)
* state.cc
* (2.0 - state.cc)
* state.c_mat[i * n + j]);
}
}
for (k, &idx) in selected_indices.iter().enumerate() {
let y_k: Vec<f64> = (0..n)
.map(|j| (pop[idx].dna()[j].real_value() - old_mean[j]) / state.sigma)
.collect();
for i in 0..n {
for j in 0..n {
c_new[i * n + j] += state.cmu * state.weights[k] * y_k[i] * y_k[j];
}
}
}
for i in 0..n {
for j in (i + 1)..n {
let avg = (c_new[i * n + j] + c_new[j * n + i]) / 2.0;
c_new[i * n + j] = avg;
c_new[j * n + i] = avg;
}
}
state.c_mat = c_new;
state.sigma *= ((state.cs / state.ds) * (ps_norm / state.chi_n - 1.0)).exp();
state.sigma = state.sigma.clamp(1e-20, 1e20);
state.mean = new_mean;
if gen >= state.eigeneval + state.t_eigen {
let (b_new, d_new) = jacobi_eigendecompose(&state.c_mat, n);
state.b_mat = b_new;
state.d_vec = d_new;
state.invsqrtc = compute_invsqrtc(&state.b_mat, &state.d_vec, n);
state.eigeneval = gen;
}
let (bi, bf) = self.find_best(&pop);
if self.is_better(bf, restart_best_fitness) {
restart_best_fitness = bf;
stagnation_count = 0;
} else {
stagnation_count += 1;
}
if self.is_better(bf, global_best_fitness) {
global_best_fitness = bf;
global_best = Some(pop[bi].clone());
self.notify(|obs| obs.on_new_best(gen, global_best.as_ref().unwrap()));
}
let fitness_values: Vec<f64> = pop.iter().map(|c| c.fitness()).collect();
let mut stats =
GenerationStats::from_fitness_values(gen, &fitness_values, is_maximization);
if let Some(ref ch) = self.fitness_cache {
let c = ch.lock().map_err(|_| {
GaError::InternalError("fitness cache mutex poisoned".to_string())
})?;
stats.cache_hits = Some(c.hits().saturating_sub(prev_cache_hits));
stats.cache_misses = Some(c.misses().saturating_sub(prev_cache_misses));
}
self.notify(|obs| obs.on_generation_end(&stats));
all_stats.push(stats);
if let Some(ref strategy) = self.config.restart_strategy {
let (threshold, max_r) = match strategy {
RestartStrategy::Ipop {
stagnation_threshold,
max_restarts,
..
} => (*stagnation_threshold, *max_restarts),
RestartStrategy::Bipop {
stagnation_threshold,
max_restarts,
..
} => (*stagnation_threshold, *max_restarts),
};
if stagnation_count >= threshold {
if total_restarts >= max_r {
break 'restart_loop;
}
let pop_before = current_lambda;
current_lambda = Self::compute_next_lambda(
strategy,
current_lambda,
default_lambda,
total_restarts,
);
let kind = Self::restart_kind(strategy, total_restarts);
total_restarts += 1;
let event = RestartEvent {
restart_number: total_restarts,
generation: gen,
population_size_before: pop_before,
population_size_after: current_lambda,
kind,
};
self.notify(|obs| obs.on_restart(&event));
break; }
}
if let Some(target) = self.config.fitness_target {
if self.reached_target(global_best_fitness, target) {
termination_cause = TerminationCause::FitnessTargetReached;
break 'restart_loop;
}
}
}
if self.config.restart_strategy.is_none() {
break 'restart_loop;
}
let max_r = match &self.config.restart_strategy {
Some(RestartStrategy::Ipop { max_restarts, .. }) => *max_restarts,
Some(RestartStrategy::Bipop { max_restarts, .. }) => *max_restarts,
None => 0,
};
if total_restarts >= max_r {
break 'restart_loop;
}
if let Some(ref strategy) = self.config.restart_strategy {
let pop_before = current_lambda;
current_lambda = Self::compute_next_lambda(
strategy,
current_lambda,
default_lambda,
total_restarts,
);
let kind = Self::restart_kind(strategy, total_restarts);
total_restarts += 1;
let event = RestartEvent {
restart_number: total_restarts,
generation: self.config.max_generations.saturating_sub(1),
population_size_before: pop_before,
population_size_after: current_lambda,
kind,
};
self.notify(|obs| obs.on_restart(&event));
}
}
let final_best = global_best.ok_or_else(|| {
GaError::InternalError("CmaEngine: no best chromosome found (empty run)".to_string())
})?;
let generations = all_stats.len();
let all_stats_ref = all_stats.as_slice();
self.notify(|obs| obs.on_run_end(termination_cause, all_stats_ref));
Ok(CmaResult {
population: pop,
best: final_best,
best_fitness: global_best_fitness,
generations,
total_restarts,
})
}
}