1use std::marker::PhantomData;
10
11use nalgebra::{DMatrix, SymmetricEigen};
12use rand::Rng;
13use rand_distr::{Distribution, StandardNormal};
14
15use crate::error::{EvoResult, EvolutionError};
16use crate::genome::bounds::MultiBounds;
17use crate::genome::real_vector::RealVector;
18use crate::genome::traits::RealValuedGenome;
19use crate::population::individual::Individual;
20
21#[cfg(feature = "parallel")]
25pub trait CmaEsFitness: Send + Sync {
26 fn evaluate(&self, x: &RealVector) -> f64;
28}
29
30#[cfg(not(feature = "parallel"))]
34pub trait CmaEsFitness {
35 fn evaluate(&self, x: &RealVector) -> f64;
37}
38
39#[derive(Clone, Debug)]
41pub struct CmaEsState {
42 pub mean: Vec<f64>,
44
45 pub sigma: f64,
47
48 pub covariance: Vec<Vec<f64>>,
50
51 pub path_sigma: Vec<f64>,
53
54 pub path_c: Vec<f64>,
56
57 pub eigenvalues: Vec<f64>,
59
60 pub eigenvectors: Vec<Vec<f64>>,
62
63 pub eigen_eval: usize,
65
66 pub dimension: usize,
68
69 pub lambda: usize,
71
72 pub mu: usize,
74
75 pub weights: Vec<f64>,
77
78 pub mu_eff: f64,
80
81 pub c_1: f64,
83
84 pub c_mu: f64,
86
87 pub c_sigma: f64,
89
90 pub d_sigma: f64,
92
93 pub c_c: f64,
95
96 pub chi_n: f64,
98
99 pub generation: usize,
101
102 pub evaluations: usize,
104
105 pub best_fitness: f64,
107
108 pub best_solution: Vec<f64>,
110}
111
112impl CmaEsState {
113 pub fn new(initial_mean: Vec<f64>, initial_sigma: f64, lambda: Option<usize>) -> Self {
115 let n = initial_mean.len();
116
117 let lambda = lambda.unwrap_or((4.0 + (3.0 * (n as f64).ln()).floor()) as usize);
119 let lambda = lambda.max(4); let mu = lambda / 2;
123
124 let mut weights: Vec<f64> = (0..mu)
126 .map(|i| ((lambda as f64 + 1.0) / 2.0).ln() - ((i + 1) as f64).ln())
127 .collect();
128
129 let weight_sum: f64 = weights.iter().sum();
131 for w in &mut weights {
132 *w /= weight_sum;
133 }
134
135 let mu_eff = 1.0 / weights.iter().map(|w| w * w).sum::<f64>();
137
138 let c_sigma = (mu_eff + 2.0) / (n as f64 + mu_eff + 5.0);
141 let c_c = (4.0 + mu_eff / n as f64) / (n as f64 + 4.0 + 2.0 * mu_eff / n as f64);
142
143 let c_1 = 2.0 / ((n as f64 + 1.3).powi(2) + mu_eff);
145 let alpha_mu = 2.0;
146 let c_mu = (alpha_mu * (mu_eff - 2.0 + 1.0 / mu_eff))
147 / ((n as f64 + 2.0).powi(2) + alpha_mu * mu_eff / 2.0);
148 let c_mu = c_mu.min(1.0 - c_1); let d_sigma =
152 1.0 + 2.0 * (0.0_f64.max(((mu_eff - 1.0) / (n as f64 + 1.0)).sqrt() - 1.0)) + c_sigma;
153
154 let chi_n =
156 (n as f64).sqrt() * (1.0 - 1.0 / (4.0 * n as f64) + 1.0 / (21.0 * (n as f64).powi(2)));
157
158 let covariance: Vec<Vec<f64>> = (0..n)
160 .map(|i| {
161 let mut row = vec![0.0; n];
162 row[i] = 1.0;
163 row
164 })
165 .collect();
166
167 let eigenvalues = vec![1.0; n];
169 let eigenvectors: Vec<Vec<f64>> = (0..n)
170 .map(|i| {
171 let mut row = vec![0.0; n];
172 row[i] = 1.0;
173 row
174 })
175 .collect();
176
177 Self {
178 mean: initial_mean.clone(),
179 sigma: initial_sigma,
180 covariance,
181 path_sigma: vec![0.0; n],
182 path_c: vec![0.0; n],
183 eigenvalues,
184 eigenvectors,
185 eigen_eval: 0,
186 dimension: n,
187 lambda,
188 mu,
189 weights,
190 mu_eff,
191 c_1,
192 c_mu,
193 c_sigma,
194 d_sigma,
195 c_c,
196 chi_n,
197 generation: 0,
198 evaluations: 0,
199 best_fitness: f64::INFINITY,
200 best_solution: initial_mean,
201 }
202 }
203
204 pub fn sample_population<R: Rng>(&self, rng: &mut R) -> Vec<RealVector> {
206 let n = self.dimension;
207 let normal = StandardNormal;
208
209 (0..self.lambda)
210 .map(|_| {
211 let z: Vec<f64> = (0..n).map(|_| normal.sample(rng)).collect();
213
214 let y: Vec<f64> = self.transform_sample(&z);
216
217 let genes: Vec<f64> = self
219 .mean
220 .iter()
221 .zip(y.iter())
222 .map(|(&m, &yi)| m + self.sigma * yi)
223 .collect();
224
225 RealVector::new(genes)
226 })
227 .collect()
228 }
229
230 fn transform_sample(&self, z: &[f64]) -> Vec<f64> {
232 let n = self.dimension;
233 let mut y = vec![0.0; n];
234
235 for i in 0..n {
237 for j in 0..n {
238 y[i] += self.eigenvectors[i][j] * self.eigenvalues[j].sqrt() * z[j];
239 }
240 }
241
242 y
243 }
244
245 pub fn update(&mut self, offspring: &[(RealVector, f64)]) {
249 let n = self.dimension;
250
251 let selected: Vec<&(RealVector, f64)> = offspring.iter().take(self.mu).collect();
253
254 let mut y_w = vec![0.0; n];
257 for (i, (genome, _fitness)) in selected.iter().enumerate() {
258 let genes = genome.genes();
259 for j in 0..n {
260 y_w[j] += self.weights[i] * (genes[j] - self.mean[j]) / self.sigma;
261 }
262 }
263
264 let mut bd_inv_yw = vec![0.0; n];
266 {
267 let mut temp = vec![0.0; n];
269 for i in 0..n {
270 for j in 0..n {
271 temp[i] += self.eigenvectors[j][i] * y_w[j];
272 }
273 temp[i] /= self.eigenvalues[i].sqrt().max(1e-16);
274 }
275 for i in 0..n {
277 for j in 0..n {
278 bd_inv_yw[i] += self.eigenvectors[i][j] * temp[j];
279 }
280 }
281 }
282
283 let c_sigma_factor = (self.c_sigma * (2.0 - self.c_sigma) * self.mu_eff).sqrt();
285 for i in 0..n {
286 self.path_sigma[i] =
287 (1.0 - self.c_sigma) * self.path_sigma[i] + c_sigma_factor * bd_inv_yw[i];
288 }
289
290 let path_sigma_norm_sq: f64 = self.path_sigma.iter().map(|x| x * x).sum();
292 let path_sigma_norm = path_sigma_norm_sq.sqrt();
293
294 let h_sigma = if path_sigma_norm
296 / (1.0 - (1.0 - self.c_sigma).powi((2 * (self.generation + 1)) as i32)).sqrt()
297 / self.chi_n
298 < 1.4 + 2.0 / (n as f64 + 1.0)
299 {
300 1.0
301 } else {
302 0.0
303 };
304
305 let c_c_factor = (self.c_c * (2.0 - self.c_c) * self.mu_eff).sqrt();
307 for i in 0..n {
308 self.path_c[i] = (1.0 - self.c_c) * self.path_c[i] + h_sigma * c_c_factor * y_w[i];
309 }
310
311 let delta_h = (1.0 - h_sigma) * self.c_c * (2.0 - self.c_c);
313
314 for i in 0..n {
315 for j in 0..=i {
316 self.covariance[i][j] *= 1.0 - self.c_1 - self.c_mu + delta_h * self.c_1;
318
319 self.covariance[i][j] += self.c_1 * self.path_c[i] * self.path_c[j];
321
322 for k in 0..self.mu {
324 let y_k: Vec<f64> = selected[k]
325 .0
326 .genes()
327 .iter()
328 .zip(self.mean.iter())
329 .map(|(&x, &m)| (x - m) / self.sigma)
330 .collect();
331 self.covariance[i][j] += self.c_mu * self.weights[k] * y_k[i] * y_k[j];
332 }
333
334 if i != j {
336 self.covariance[j][i] = self.covariance[i][j];
337 }
338 }
339 }
340
341 for i in 0..n {
343 self.mean[i] += self.sigma * y_w[i];
344 }
345
346 self.sigma *= ((self.c_sigma / self.d_sigma) * (path_sigma_norm / self.chi_n - 1.0)).exp();
348
349 self.generation += 1;
351
352 if self.generation - self.eigen_eval >= self.eigen_update_interval() {
357 self.update_eigensystem();
358 self.eigen_eval = self.generation;
359 }
360 }
361
362 pub fn eigen_update_interval(&self) -> usize {
372 let n = self.dimension as f64;
373 ((1.0 / (10.0 * n * (self.c_1 + self.c_mu))).floor() as usize).max(1)
374 }
375
376 fn update_eigensystem(&mut self) {
378 let n = self.dimension;
379
380 for i in 0..n {
382 for j in 0..i {
383 self.covariance[i][j] = (self.covariance[i][j] + self.covariance[j][i]) / 2.0;
384 self.covariance[j][i] = self.covariance[i][j];
385 }
386 }
387
388 let (eigenvalues, eigenvectors) = symmetric_eigendecomposition(&self.covariance);
392
393 self.eigenvalues = eigenvalues;
394 self.eigenvectors = eigenvectors;
395
396 for ev in &mut self.eigenvalues {
398 *ev = ev.max(1e-16);
399 }
400 }
401
402 pub fn has_converged(&self) -> bool {
404 let max_eigenvalue = self
406 .eigenvalues
407 .iter()
408 .cloned()
409 .fold(f64::NEG_INFINITY, f64::max);
410 let min_eigenvalue = self
411 .eigenvalues
412 .iter()
413 .cloned()
414 .fold(f64::INFINITY, f64::min);
415
416 if max_eigenvalue / min_eigenvalue.max(1e-16) > 1e14 {
418 return true;
419 }
420
421 if self.sigma < 1e-16 {
423 return true;
424 }
425
426 if self.sigma * max_eigenvalue.sqrt() < 1e-16 {
428 return true;
429 }
430
431 false
432 }
433}
434
435fn symmetric_eigendecomposition(a: &[Vec<f64>]) -> (Vec<f64>, Vec<Vec<f64>>) {
449 let n = a.len();
450
451 let flat: Vec<f64> = (0..n).flat_map(|i| (0..n).map(move |j| a[i][j])).collect();
453 let matrix = DMatrix::from_row_slice(n, n, &flat);
454
455 let eig = SymmetricEigen::new(matrix);
456
457 let eigenvalues: Vec<f64> = eig.eigenvalues.iter().copied().collect();
458 let eigenvectors: Vec<Vec<f64>> = (0..n)
459 .map(|i| (0..n).map(|j| eig.eigenvectors[(i, j)]).collect())
460 .collect();
461
462 (eigenvalues, eigenvectors)
463}
464
465#[cfg(feature = "parallel")]
467impl<F> CmaEsFitness for F
468where
469 F: Fn(&RealVector) -> f64 + Send + Sync,
470{
471 fn evaluate(&self, x: &RealVector) -> f64 {
472 self(x)
473 }
474}
475
476#[cfg(not(feature = "parallel"))]
478impl<F> CmaEsFitness for F
479where
480 F: Fn(&RealVector) -> f64,
481{
482 fn evaluate(&self, x: &RealVector) -> f64 {
483 self(x)
484 }
485}
486
487#[derive(Clone)]
489pub struct CmaEs<F> {
490 pub state: CmaEsState,
492 pub bounds: Option<MultiBounds>,
494 pub boundary_penalty: f64,
501 _phantom: PhantomData<F>,
503}
504
505impl<F: CmaEsFitness> CmaEs<F> {
506 pub fn new(initial_mean: Vec<f64>, initial_sigma: f64) -> Self {
508 Self {
509 state: CmaEsState::new(initial_mean, initial_sigma, None),
510 bounds: None,
511 boundary_penalty: 0.0,
512 _phantom: PhantomData,
513 }
514 }
515
516 pub fn with_lambda(initial_mean: Vec<f64>, initial_sigma: f64, lambda: usize) -> Self {
518 Self {
519 state: CmaEsState::new(initial_mean, initial_sigma, Some(lambda)),
520 bounds: None,
521 boundary_penalty: 0.0,
522 _phantom: PhantomData,
523 }
524 }
525
526 pub fn with_bounds(mut self, bounds: MultiBounds) -> Self {
528 self.bounds = Some(bounds);
529 self
530 }
531
532 pub fn with_boundary_penalty(mut self, weight: f64) -> Self {
534 self.boundary_penalty = weight;
535 self
536 }
537
538 pub fn step<R: Rng>(
548 &mut self,
549 fitness: &F,
550 rng: &mut R,
551 ) -> EvoResult<Vec<Individual<RealVector>>> {
552 let unrepaired = self.state.sample_population(rng);
554
555 let mut evaluated: Vec<(RealVector, RealVector, f64)> = unrepaired
559 .into_iter()
560 .map(|raw| {
561 let feasible = match self.bounds {
562 Some(ref bounds) => {
563 let mut repaired = raw.clone();
564 repaired.apply_bounds(bounds);
565 repaired
566 }
567 None => raw.clone(),
568 };
569
570 let mut f = fitness.evaluate(&feasible);
571 if self.boundary_penalty > 0.0 {
572 let penalty: f64 = raw
573 .genes()
574 .iter()
575 .zip(feasible.genes().iter())
576 .map(|(&x, &c)| {
577 let d = x - c;
578 d * d
579 })
580 .sum();
581 f += self.boundary_penalty * penalty;
582 }
583
584 (raw, feasible, f)
585 })
586 .collect();
587
588 self.state.evaluations += evaluated.len();
589
590 evaluated.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal));
592
593 let update_input: Vec<(RealVector, f64)> = evaluated
595 .iter()
596 .map(|(raw, _feasible, f)| (raw.clone(), *f))
597 .collect();
598 self.state.update(&update_input);
599
600 if let Some((_, feasible, f)) = evaluated.first() {
602 if *f < self.state.best_fitness {
603 self.state.best_fitness = *f;
604 self.state.best_solution = feasible.genes().to_vec();
605 }
606 }
607
608 let individuals: Vec<Individual<RealVector>> = evaluated
610 .into_iter()
611 .map(|(_raw, feasible, f)| Individual::with_fitness(feasible, f))
612 .collect();
613
614 Ok(individuals)
615 }
616
617 pub fn run_generations<R: Rng>(
619 &mut self,
620 fitness: &F,
621 max_generations: usize,
622 rng: &mut R,
623 ) -> EvoResult<Individual<RealVector>> {
624 let mut best: Option<Individual<RealVector>> = None;
625
626 for _ in 0..max_generations {
627 let population = self.step(fitness, rng)?;
628
629 if let Some(current_best) = population.first() {
631 match &best {
632 None => best = Some(current_best.clone()),
633 Some(existing) => {
634 if current_best.fitness_f64() < existing.fitness_f64() {
635 best = Some(current_best.clone());
636 }
637 }
638 }
639 }
640
641 if self.state.has_converged() {
643 break;
644 }
645 }
646
647 best.ok_or(EvolutionError::EmptyPopulation)
648 }
649
650 pub fn run_until<R: Rng>(
652 &mut self,
653 fitness: &F,
654 target_fitness: f64,
655 max_generations: usize,
656 rng: &mut R,
657 ) -> EvoResult<Individual<RealVector>> {
658 let mut best: Option<Individual<RealVector>> = None;
659
660 for _ in 0..max_generations {
661 let population = self.step(fitness, rng)?;
662
663 if let Some(current_best) = population.first() {
665 match &best {
666 None => best = Some(current_best.clone()),
667 Some(existing) => {
668 if current_best.fitness_f64() < existing.fitness_f64() {
669 best = Some(current_best.clone());
670 }
671 }
672 }
673 }
674
675 if let Some(ref b) = best {
677 if b.fitness_f64() <= target_fitness {
678 break;
679 }
680 }
681
682 if self.state.has_converged() {
684 break;
685 }
686 }
687
688 best.ok_or(EvolutionError::EmptyPopulation)
689 }
690
691 pub fn generation(&self) -> usize {
693 self.state.generation
694 }
695
696 pub fn evaluations(&self) -> usize {
698 self.state.evaluations
699 }
700
701 pub fn mean(&self) -> &[f64] {
703 &self.state.mean
704 }
705
706 pub fn sigma(&self) -> f64 {
708 self.state.sigma
709 }
710
711 pub fn best_solution(&self) -> &[f64] {
713 &self.state.best_solution
714 }
715
716 pub fn best_fitness(&self) -> f64 {
718 self.state.best_fitness
719 }
720}
721
722pub struct CmaEsBuilder {
724 initial_mean: Option<Vec<f64>>,
725 initial_sigma: f64,
726 lambda: Option<usize>,
727 bounds: Option<MultiBounds>,
728 boundary_penalty: f64,
729}
730
731impl CmaEsBuilder {
732 pub fn new() -> Self {
734 Self {
735 initial_mean: None,
736 initial_sigma: 1.0,
737 lambda: None,
738 bounds: None,
739 boundary_penalty: 0.0,
740 }
741 }
742
743 pub fn boundary_penalty(mut self, weight: f64) -> Self {
745 self.boundary_penalty = weight;
746 self
747 }
748
749 pub fn mean(mut self, mean: Vec<f64>) -> Self {
751 self.initial_mean = Some(mean);
752 self
753 }
754
755 pub fn sigma(mut self, sigma: f64) -> Self {
757 self.initial_sigma = sigma;
758 self
759 }
760
761 pub fn lambda(mut self, lambda: usize) -> Self {
763 self.lambda = Some(lambda);
764 self
765 }
766
767 pub fn bounds(mut self, bounds: MultiBounds) -> Self {
769 self.bounds = Some(bounds);
770 self
771 }
772
773 pub fn build<F: CmaEsFitness>(self) -> EvoResult<CmaEs<F>> {
775 let mean = self
776 .initial_mean
777 .ok_or_else(|| EvolutionError::Configuration("Initial mean not set".to_string()))?;
778
779 let mut cmaes = match self.lambda {
780 Some(l) => CmaEs::with_lambda(mean, self.initial_sigma, l),
781 None => CmaEs::new(mean, self.initial_sigma),
782 };
783
784 if let Some(bounds) = self.bounds {
785 cmaes = cmaes.with_bounds(bounds);
786 }
787
788 cmaes.boundary_penalty = self.boundary_penalty;
789
790 Ok(cmaes)
791 }
792}
793
794impl Default for CmaEsBuilder {
795 fn default() -> Self {
796 Self::new()
797 }
798}
799
800#[cfg(test)]
801mod tests {
802 use super::*;
803 use crate::genome::traits::EvolutionaryGenome;
804 use approx::assert_relative_eq;
805
806 struct Sphere;
808
809 impl CmaEsFitness for Sphere {
810 fn evaluate(&self, genome: &RealVector) -> f64 {
811 genome.genes().iter().map(|x| x * x).sum()
812 }
813 }
814
815 #[test]
816 fn test_cmaes_state_initialization() {
817 let mean = vec![0.0, 0.0, 0.0];
818 let state = CmaEsState::new(mean.clone(), 1.0, None);
819
820 assert_eq!(state.dimension, 3);
821 assert_eq!(state.mean, mean);
822 assert_eq!(state.sigma, 1.0);
823 assert!(state.lambda >= 4);
824 assert!(state.mu > 0);
825 assert_eq!(state.weights.len(), state.mu);
826 }
827
828 #[test]
829 fn test_cmaes_weights_sum_to_one() {
830 let state = CmaEsState::new(vec![0.0; 10], 1.0, None);
831 let sum: f64 = state.weights.iter().sum();
832 assert_relative_eq!(sum, 1.0, epsilon = 1e-10);
833 }
834
835 #[test]
836 fn test_cmaes_sampling() {
837 let mut rng = rand::thread_rng();
838 let state = CmaEsState::new(vec![0.0; 5], 1.0, Some(10));
839
840 let samples = state.sample_population(&mut rng);
841
842 assert_eq!(samples.len(), 10);
843 for sample in &samples {
844 assert_eq!(sample.dimension(), 5);
845 }
846 }
847
848 #[test]
849 fn test_cmaes_step() {
850 let mut rng = rand::thread_rng();
851 let fitness = Sphere;
852 let mut cmaes: CmaEs<Sphere> = CmaEs::new(vec![5.0, 5.0, 5.0], 2.0);
853
854 let population = cmaes.step(&fitness, &mut rng).unwrap();
855
856 assert_eq!(population.len(), cmaes.state.lambda);
857 assert_eq!(cmaes.state.generation, 1);
858 }
859
860 #[test]
861 fn test_cmaes_optimization() {
862 use rand::SeedableRng;
863 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
864 let fitness = Sphere;
865 let mut cmaes: CmaEs<Sphere> = CmaEs::new(vec![5.0, 5.0], 2.0);
866
867 let result = cmaes.run_generations(&fitness, 150, &mut rng).unwrap();
869
870 let final_fitness = result.fitness_f64();
873 let initial_fitness = 50.0; assert!(
875 final_fitness < initial_fitness * 0.7,
876 "Final fitness {} should be significantly better than initial {}",
877 final_fitness,
878 initial_fitness
879 );
880 }
881
882 #[test]
883 fn test_cmaes_with_bounds() {
884 let mut rng = rand::thread_rng();
885 let fitness = Sphere;
886 let bounds = MultiBounds::symmetric(10.0, 3);
887
888 let mut cmaes: CmaEs<Sphere> = CmaEs::new(vec![5.0, 5.0, 5.0], 2.0).with_bounds(bounds);
889
890 let result = cmaes.run_generations(&fitness, 30, &mut rng).unwrap();
891
892 for gene in result.genome().genes() {
894 assert!(*gene >= -10.0 && *gene <= 10.0);
895 }
896 }
897
898 #[test]
899 fn test_cmaes_builder() {
900 let cmaes: CmaEs<Sphere> = CmaEsBuilder::new()
901 .mean(vec![0.0, 0.0, 0.0])
902 .sigma(0.5)
903 .lambda(20)
904 .bounds(MultiBounds::symmetric(5.0, 3))
905 .build()
906 .unwrap();
907
908 assert_eq!(cmaes.state.lambda, 20);
909 assert_eq!(cmaes.state.sigma, 0.5);
910 assert!(cmaes.bounds.is_some());
911 }
912
913 #[test]
914 fn test_cmaes_convergence_detection() {
915 let mut state = CmaEsState::new(vec![0.0, 0.0], 1e-20, None);
916 assert!(state.has_converged());
917
918 state.sigma = 1.0;
919 state.eigenvalues = vec![1e15, 1.0];
920 assert!(state.has_converged());
921 }
922
923 fn rosenbrock(x: &RealVector) -> f64 {
927 x.genes()
928 .windows(2)
929 .map(|w| 100.0 * (w[1] - w[0] * w[0]).powi(2) + (1.0 - w[0]).powi(2))
930 .sum()
931 }
932
933 fn random_spd(n: usize, rng: &mut impl Rng) -> Vec<Vec<f64>> {
935 let a: Vec<Vec<f64>> = (0..n)
936 .map(|_| {
937 (0..n)
938 .map(|_| rng.gen_range(-1.0..1.0))
939 .collect::<Vec<f64>>()
940 })
941 .collect();
942 let mut c = vec![vec![0.0; n]; n];
943 for (i, ci) in c.iter_mut().enumerate() {
944 for (j, cij) in ci.iter_mut().enumerate() {
945 let mut s = 0.0;
946 for k in 0..n {
947 s += a[i][k] * a[j][k];
948 }
949 *cij = s;
950 }
951 }
952 for (i, ci) in c.iter_mut().enumerate() {
953 ci[i] += n as f64;
954 }
955 c
956 }
957
958 #[test]
965 fn test_eigendecomposition_known_matrix() {
966 let a = vec![vec![4.0, 1.0], vec![1.0, 3.0]];
967 let (eigenvalues, eigenvectors) = symmetric_eigendecomposition(&a);
968
969 let mut sorted = eigenvalues.clone();
971 sorted.sort_by(|x, y| x.partial_cmp(y).unwrap());
972 assert_relative_eq!(sorted[0], (7.0 - 5.0_f64.sqrt()) / 2.0, epsilon = 1e-9);
973 assert_relative_eq!(sorted[1], (7.0 + 5.0_f64.sqrt()) / 2.0, epsilon = 1e-9);
974
975 assert!(eigenvalues.iter().all(|&l| l > 0.0));
977
978 for j in 0..2 {
980 let v: Vec<f64> = (0..2).map(|i| eigenvectors[i][j]).collect();
981 for i in 0..2 {
982 let cv: f64 = (0..2).map(|k| a[i][k] * v[k]).sum();
983 assert_relative_eq!(cv, eigenvalues[j] * v[i], epsilon = 1e-9);
984 }
985 }
986 }
987
988 #[test]
992 fn test_eigendecomposition_random_spd() {
993 use rand::SeedableRng;
994 let mut rng = rand::rngs::StdRng::seed_from_u64(2024);
995
996 for &n in &[2usize, 3, 5, 8] {
997 let c = random_spd(n, &mut rng);
998 let (eigenvalues, eigenvectors) = symmetric_eigendecomposition(&c);
999
1000 assert!(
1002 eigenvalues.iter().all(|&l| l > 0.0),
1003 "SPD matrix must have all-positive eigenvalues, got {:?}",
1004 eigenvalues
1005 );
1006
1007 for j in 0..n {
1009 let v: Vec<f64> = (0..n).map(|i| eigenvectors[i][j]).collect();
1010 for i in 0..n {
1011 let cv: f64 = (0..n).map(|k| c[i][k] * v[k]).sum();
1012 assert!(
1013 (cv - eigenvalues[j] * v[i]).abs() < 1e-9,
1014 "n={}: C·v_{} component {} mismatch",
1015 n,
1016 j,
1017 i
1018 );
1019 }
1020 }
1021
1022 for i in 0..n {
1024 for j in 0..n {
1025 let recon: f64 = (0..n)
1026 .map(|k| eigenvectors[i][k] * eigenvalues[k] * eigenvectors[j][k])
1027 .sum();
1028 assert!(
1029 (recon - c[i][j]).abs() < 1e-9,
1030 "n={}: reconstruction mismatch at ({},{})",
1031 n,
1032 i,
1033 j
1034 );
1035 }
1036 }
1037 }
1038 }
1039
1040 #[test]
1045 fn test_cmaes_rosenbrock_convergence() {
1046 use rand::SeedableRng;
1047 let mut rng = rand::rngs::StdRng::seed_from_u64(7);
1048
1049 let mut cmaes: CmaEs<_> = CmaEs::with_lambda(vec![0.0; 5], 0.5, 20);
1051 let best = cmaes.run_until(&rosenbrock, 1e-6, 4000, &mut rng).unwrap();
1052
1053 assert!(
1054 best.fitness_f64() < 1e-6,
1055 "CMA-ES should reach f < 1e-6 on 5-D Rosenbrock, got {}",
1056 best.fitness_f64()
1057 );
1058 }
1059
1060 #[test]
1064 fn test_eigen_recompute_cadence() {
1065 use rand::SeedableRng;
1066 let mut rng = rand::rngs::StdRng::seed_from_u64(3);
1067 let fitness = Sphere;
1068 let n = 100;
1069 let mut cmaes: CmaEs<Sphere> = CmaEs::new(vec![1.0; n], 1.0);
1070
1071 assert_eq!(cmaes.state.eigen_update_interval(), 1);
1073
1074 let gens = 30;
1075 let mut recomputes = 0;
1076 let mut last = cmaes.state.eigen_eval;
1077 for _ in 0..gens {
1078 cmaes.step(&fitness, &mut rng).unwrap();
1079 if cmaes.state.eigen_eval != last {
1080 recomputes += 1;
1081 last = cmaes.state.eigen_eval;
1082 }
1083 }
1084
1085 assert!(
1086 recomputes >= gens - 1,
1087 "expected ~every-generation eigen recompute for n=100, got {} in {} gens",
1088 recomputes,
1089 gens
1090 );
1091 }
1092
1093 #[test]
1099 fn test_cmaes_update_uses_unrepaired_samples() {
1100 use crate::genome::bounds::Bounds;
1101 use rand::SeedableRng;
1102 let mut rng = rand::rngs::StdRng::seed_from_u64(11);
1103 let fitness = Sphere;
1104
1105 let bounds = MultiBounds::uniform(Bounds::new(2.9, 3.1), 2);
1106 let mut cmaes: CmaEs<Sphere> = CmaEs::new(vec![0.0, 0.0], 1.0).with_bounds(bounds);
1107
1108 cmaes.step(&fitness, &mut rng).unwrap();
1109
1110 for &m in &cmaes.state.mean {
1111 assert!(
1112 m.abs() < 2.0,
1113 "mean component {} was pulled onto the clamped boundary (~2.9); \
1114 the distribution update must use unrepaired samples",
1115 m
1116 );
1117 }
1118
1119 for &g in &cmaes.state.best_solution {
1121 assert!((2.9..=3.1).contains(&g));
1122 }
1123 }
1124
1125 #[test]
1128 fn test_cmaes_boundary_penalty_option() {
1129 use crate::genome::bounds::Bounds;
1130 use rand::SeedableRng;
1131 let mut rng = rand::rngs::StdRng::seed_from_u64(5);
1132 let fitness = Sphere;
1133
1134 let bounds = MultiBounds::uniform(Bounds::new(-1.0, 1.0), 3);
1135 let mut cmaes: CmaEs<Sphere> = CmaEs::new(vec![0.0; 3], 2.0)
1136 .with_bounds(bounds)
1137 .with_boundary_penalty(10.0);
1138 assert_eq!(cmaes.boundary_penalty, 10.0);
1139
1140 cmaes.step(&fitness, &mut rng).unwrap();
1142 for &g in &cmaes.state.best_solution {
1143 assert!((-1.0..=1.0).contains(&g));
1144 }
1145 }
1146}