1use std::marker::PhantomData;
10
11use rand::Rng;
12use rand_distr::{Distribution, StandardNormal};
13
14use crate::error::{EvoResult, EvolutionError};
15use crate::genome::bounds::MultiBounds;
16use crate::genome::real_vector::RealVector;
17use crate::genome::traits::RealValuedGenome;
18use crate::population::individual::Individual;
19
20#[cfg(feature = "parallel")]
24pub trait CmaEsFitness: Send + Sync {
25 fn evaluate(&self, x: &RealVector) -> f64;
27}
28
29#[cfg(not(feature = "parallel"))]
33pub trait CmaEsFitness {
34 fn evaluate(&self, x: &RealVector) -> f64;
36}
37
38#[derive(Clone, Debug)]
40pub struct CmaEsState {
41 pub mean: Vec<f64>,
43
44 pub sigma: f64,
46
47 pub covariance: Vec<Vec<f64>>,
49
50 pub path_sigma: Vec<f64>,
52
53 pub path_c: Vec<f64>,
55
56 pub eigenvalues: Vec<f64>,
58
59 pub eigenvectors: Vec<Vec<f64>>,
61
62 pub eigen_eval: usize,
64
65 pub dimension: usize,
67
68 pub lambda: usize,
70
71 pub mu: usize,
73
74 pub weights: Vec<f64>,
76
77 pub mu_eff: f64,
79
80 pub c_1: f64,
82
83 pub c_mu: f64,
85
86 pub c_sigma: f64,
88
89 pub d_sigma: f64,
91
92 pub c_c: f64,
94
95 pub chi_n: f64,
97
98 pub generation: usize,
100
101 pub evaluations: usize,
103
104 pub best_fitness: f64,
106
107 pub best_solution: Vec<f64>,
109}
110
111impl CmaEsState {
112 pub fn new(initial_mean: Vec<f64>, initial_sigma: f64, lambda: Option<usize>) -> Self {
114 let n = initial_mean.len();
115
116 let lambda = lambda.unwrap_or((4.0 + (3.0 * (n as f64).ln()).floor()) as usize);
118 let lambda = lambda.max(4); let mu = lambda / 2;
122
123 let mut weights: Vec<f64> = (0..mu)
125 .map(|i| ((lambda as f64 + 1.0) / 2.0).ln() - ((i + 1) as f64).ln())
126 .collect();
127
128 let weight_sum: f64 = weights.iter().sum();
130 for w in &mut weights {
131 *w /= weight_sum;
132 }
133
134 let mu_eff = 1.0 / weights.iter().map(|w| w * w).sum::<f64>();
136
137 let c_sigma = (mu_eff + 2.0) / (n as f64 + mu_eff + 5.0);
140 let c_c = (4.0 + mu_eff / n as f64) / (n as f64 + 4.0 + 2.0 * mu_eff / n as f64);
141
142 let c_1 = 2.0 / ((n as f64 + 1.3).powi(2) + mu_eff);
144 let alpha_mu = 2.0;
145 let c_mu = (alpha_mu * (mu_eff - 2.0 + 1.0 / mu_eff))
146 / ((n as f64 + 2.0).powi(2) + alpha_mu * mu_eff / 2.0);
147 let c_mu = c_mu.min(1.0 - c_1); let d_sigma =
151 1.0 + 2.0 * (0.0_f64.max(((mu_eff - 1.0) / (n as f64 + 1.0)).sqrt() - 1.0)) + c_sigma;
152
153 let chi_n =
155 (n as f64).sqrt() * (1.0 - 1.0 / (4.0 * n as f64) + 1.0 / (21.0 * (n as f64).powi(2)));
156
157 let covariance: Vec<Vec<f64>> = (0..n)
159 .map(|i| {
160 let mut row = vec![0.0; n];
161 row[i] = 1.0;
162 row
163 })
164 .collect();
165
166 let eigenvalues = vec![1.0; n];
168 let eigenvectors: Vec<Vec<f64>> = (0..n)
169 .map(|i| {
170 let mut row = vec![0.0; n];
171 row[i] = 1.0;
172 row
173 })
174 .collect();
175
176 Self {
177 mean: initial_mean.clone(),
178 sigma: initial_sigma,
179 covariance,
180 path_sigma: vec![0.0; n],
181 path_c: vec![0.0; n],
182 eigenvalues,
183 eigenvectors,
184 eigen_eval: 0,
185 dimension: n,
186 lambda,
187 mu,
188 weights,
189 mu_eff,
190 c_1,
191 c_mu,
192 c_sigma,
193 d_sigma,
194 c_c,
195 chi_n,
196 generation: 0,
197 evaluations: 0,
198 best_fitness: f64::INFINITY,
199 best_solution: initial_mean,
200 }
201 }
202
203 pub fn sample_population<R: Rng>(&self, rng: &mut R) -> Vec<RealVector> {
205 let n = self.dimension;
206 let normal = StandardNormal;
207
208 (0..self.lambda)
209 .map(|_| {
210 let z: Vec<f64> = (0..n).map(|_| normal.sample(rng)).collect();
212
213 let y: Vec<f64> = self.transform_sample(&z);
215
216 let genes: Vec<f64> = self
218 .mean
219 .iter()
220 .zip(y.iter())
221 .map(|(&m, &yi)| m + self.sigma * yi)
222 .collect();
223
224 RealVector::new(genes)
225 })
226 .collect()
227 }
228
229 fn transform_sample(&self, z: &[f64]) -> Vec<f64> {
231 let n = self.dimension;
232 let mut y = vec![0.0; n];
233
234 for i in 0..n {
236 for j in 0..n {
237 y[i] += self.eigenvectors[i][j] * self.eigenvalues[j].sqrt() * z[j];
238 }
239 }
240
241 y
242 }
243
244 pub fn update(&mut self, offspring: &[(RealVector, f64)]) {
248 let n = self.dimension;
249
250 let selected: Vec<&(RealVector, f64)> = offspring.iter().take(self.mu).collect();
252
253 let mut y_w = vec![0.0; n];
256 for (i, (genome, _fitness)) in selected.iter().enumerate() {
257 let genes = genome.genes();
258 for j in 0..n {
259 y_w[j] += self.weights[i] * (genes[j] - self.mean[j]) / self.sigma;
260 }
261 }
262
263 let mut bd_inv_yw = vec![0.0; n];
265 {
266 let mut temp = vec![0.0; n];
268 for i in 0..n {
269 for j in 0..n {
270 temp[i] += self.eigenvectors[j][i] * y_w[j];
271 }
272 temp[i] /= self.eigenvalues[i].sqrt().max(1e-16);
273 }
274 for i in 0..n {
276 for j in 0..n {
277 bd_inv_yw[i] += self.eigenvectors[i][j] * temp[j];
278 }
279 }
280 }
281
282 let c_sigma_factor = (self.c_sigma * (2.0 - self.c_sigma) * self.mu_eff).sqrt();
284 for i in 0..n {
285 self.path_sigma[i] =
286 (1.0 - self.c_sigma) * self.path_sigma[i] + c_sigma_factor * bd_inv_yw[i];
287 }
288
289 let path_sigma_norm_sq: f64 = self.path_sigma.iter().map(|x| x * x).sum();
291 let path_sigma_norm = path_sigma_norm_sq.sqrt();
292
293 let h_sigma = if path_sigma_norm
295 / (1.0 - (1.0 - self.c_sigma).powi((2 * (self.generation + 1)) as i32)).sqrt()
296 / self.chi_n
297 < 1.4 + 2.0 / (n as f64 + 1.0)
298 {
299 1.0
300 } else {
301 0.0
302 };
303
304 let c_c_factor = (self.c_c * (2.0 - self.c_c) * self.mu_eff).sqrt();
306 for i in 0..n {
307 self.path_c[i] = (1.0 - self.c_c) * self.path_c[i] + h_sigma * c_c_factor * y_w[i];
308 }
309
310 let delta_h = (1.0 - h_sigma) * self.c_c * (2.0 - self.c_c);
312
313 for i in 0..n {
314 for j in 0..=i {
315 self.covariance[i][j] *= 1.0 - self.c_1 - self.c_mu + delta_h * self.c_1;
317
318 self.covariance[i][j] += self.c_1 * self.path_c[i] * self.path_c[j];
320
321 for k in 0..self.mu {
323 let y_k: Vec<f64> = selected[k]
324 .0
325 .genes()
326 .iter()
327 .zip(self.mean.iter())
328 .map(|(&x, &m)| (x - m) / self.sigma)
329 .collect();
330 self.covariance[i][j] += self.c_mu * self.weights[k] * y_k[i] * y_k[j];
331 }
332
333 if i != j {
335 self.covariance[j][i] = self.covariance[i][j];
336 }
337 }
338 }
339
340 for i in 0..n {
342 self.mean[i] += self.sigma * y_w[i];
343 }
344
345 self.sigma *= ((self.c_sigma / self.d_sigma) * (path_sigma_norm / self.chi_n - 1.0)).exp();
347
348 self.generation += 1;
350
351 let update_eigendecomp = self.generation - self.eigen_eval
354 > (self.lambda as f64 / (self.c_1 + self.c_mu) / n as f64 / 10.0) as usize;
355
356 if update_eigendecomp {
357 self.update_eigensystem();
358 self.eigen_eval = self.generation;
359 }
360
361 if let Some((best_genome, best_fit)) = offspring.first() {
363 if *best_fit < self.best_fitness {
364 self.best_fitness = *best_fit;
365 self.best_solution = best_genome.genes().to_vec();
366 }
367 }
368 }
369
370 fn update_eigensystem(&mut self) {
372 let n = self.dimension;
373
374 for i in 0..n {
376 for j in 0..i {
377 self.covariance[i][j] = (self.covariance[i][j] + self.covariance[j][i]) / 2.0;
378 self.covariance[j][i] = self.covariance[i][j];
379 }
380 }
381
382 let (eigenvalues, eigenvectors) = jacobi_eigendecomposition(&self.covariance);
385
386 self.eigenvalues = eigenvalues;
387 self.eigenvectors = eigenvectors;
388
389 for ev in &mut self.eigenvalues {
391 *ev = ev.max(1e-16);
392 }
393 }
394
395 pub fn has_converged(&self) -> bool {
397 let max_eigenvalue = self
399 .eigenvalues
400 .iter()
401 .cloned()
402 .fold(f64::NEG_INFINITY, f64::max);
403 let min_eigenvalue = self
404 .eigenvalues
405 .iter()
406 .cloned()
407 .fold(f64::INFINITY, f64::min);
408
409 if max_eigenvalue / min_eigenvalue.max(1e-16) > 1e14 {
411 return true;
412 }
413
414 if self.sigma < 1e-16 {
416 return true;
417 }
418
419 if self.sigma * max_eigenvalue.sqrt() < 1e-16 {
421 return true;
422 }
423
424 false
425 }
426}
427
428fn jacobi_eigendecomposition(a: &[Vec<f64>]) -> (Vec<f64>, Vec<Vec<f64>>) {
430 let n = a.len();
431 let mut d: Vec<f64> = (0..n).map(|i| a[i][i]).collect();
432 let mut v: Vec<Vec<f64>> = (0..n)
433 .map(|i| {
434 let mut row = vec![0.0; n];
435 row[i] = 1.0;
436 row
437 })
438 .collect();
439 let mut b = d.clone();
440 let mut z = vec![0.0; n];
441
442 let max_sweeps = 50;
443
444 for _ in 0..max_sweeps {
445 let mut sm = 0.0;
446 for p in 0..n - 1 {
447 for q in p + 1..n {
448 sm += a[p][q].abs();
449 }
450 }
451
452 if sm < 1e-16 {
453 break;
454 }
455
456 for p in 0..n - 1 {
457 for q in p + 1..n {
458 let h = d[q] - d[p];
459 let mut t: f64;
460
461 if a[p][q].abs() < 1e-100 {
462 t = 0.0;
463 } else if h.abs() < 1e-100 {
464 t = a[p][q].signum();
465 } else {
466 let theta = 0.5 * h / a[p][q];
467 t = 1.0 / (theta.abs() + (1.0 + theta * theta).sqrt());
468 if theta < 0.0 {
469 t = -t;
470 }
471 }
472
473 let c = 1.0 / (1.0 + t * t).sqrt();
474 let s = t * c;
475 let tau = s / (1.0 + c);
476
477 let h = t * a[p][q];
478 z[p] -= h;
479 z[q] += h;
480 d[p] -= h;
481 d[q] += h;
482
483 for j in 0..p {
485 let g = a[j][p];
486 let h = a[j][q];
487 let apj = g - s * (h + g * tau);
488 let aqj = h + s * (g - h * tau);
489 let _ = (apj, aqj);
491 }
492
493 for j in 0..n {
495 let g = v[j][p];
496 let h = v[j][q];
497 v[j][p] = g - s * (h + g * tau);
498 v[j][q] = h + s * (g - h * tau);
499 }
500 }
501 }
502
503 for i in 0..n {
504 b[i] += z[i];
505 d[i] = b[i];
506 z[i] = 0.0;
507 }
508 }
509
510 (d, v)
511}
512
513#[cfg(feature = "parallel")]
515impl<F> CmaEsFitness for F
516where
517 F: Fn(&RealVector) -> f64 + Send + Sync,
518{
519 fn evaluate(&self, x: &RealVector) -> f64 {
520 self(x)
521 }
522}
523
524#[cfg(not(feature = "parallel"))]
526impl<F> CmaEsFitness for F
527where
528 F: Fn(&RealVector) -> f64,
529{
530 fn evaluate(&self, x: &RealVector) -> f64 {
531 self(x)
532 }
533}
534
535#[derive(Clone)]
537pub struct CmaEs<F> {
538 pub state: CmaEsState,
540 pub bounds: Option<MultiBounds>,
542 _phantom: PhantomData<F>,
544}
545
546impl<F: CmaEsFitness> CmaEs<F> {
547 pub fn new(initial_mean: Vec<f64>, initial_sigma: f64) -> Self {
549 Self {
550 state: CmaEsState::new(initial_mean, initial_sigma, None),
551 bounds: None,
552 _phantom: PhantomData,
553 }
554 }
555
556 pub fn with_lambda(initial_mean: Vec<f64>, initial_sigma: f64, lambda: usize) -> Self {
558 Self {
559 state: CmaEsState::new(initial_mean, initial_sigma, Some(lambda)),
560 bounds: None,
561 _phantom: PhantomData,
562 }
563 }
564
565 pub fn with_bounds(mut self, bounds: MultiBounds) -> Self {
567 self.bounds = Some(bounds);
568 self
569 }
570
571 pub fn step<R: Rng>(
573 &mut self,
574 fitness: &F,
575 rng: &mut R,
576 ) -> EvoResult<Vec<Individual<RealVector>>> {
577 let mut offspring = self.state.sample_population(rng);
579
580 if let Some(ref bounds) = self.bounds {
582 for genome in &mut offspring {
583 genome.apply_bounds(bounds);
584 }
585 }
586
587 let mut evaluated: Vec<(RealVector, f64)> = offspring
589 .into_iter()
590 .map(|g| {
591 let f = fitness.evaluate(&g);
592 (g, f)
593 })
594 .collect();
595
596 self.state.evaluations += evaluated.len();
597
598 evaluated.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
600
601 self.state.update(&evaluated);
603
604 let individuals: Vec<Individual<RealVector>> = evaluated
606 .into_iter()
607 .map(|(g, f)| Individual::with_fitness(g, f))
608 .collect();
609
610 Ok(individuals)
611 }
612
613 pub fn run_generations<R: Rng>(
615 &mut self,
616 fitness: &F,
617 max_generations: usize,
618 rng: &mut R,
619 ) -> EvoResult<Individual<RealVector>> {
620 let mut best: Option<Individual<RealVector>> = None;
621
622 for _ in 0..max_generations {
623 let population = self.step(fitness, rng)?;
624
625 if let Some(current_best) = population.first() {
627 match &best {
628 None => best = Some(current_best.clone()),
629 Some(existing) => {
630 if current_best.fitness_f64() < existing.fitness_f64() {
631 best = Some(current_best.clone());
632 }
633 }
634 }
635 }
636
637 if self.state.has_converged() {
639 break;
640 }
641 }
642
643 best.ok_or(EvolutionError::EmptyPopulation)
644 }
645
646 pub fn run_until<R: Rng>(
648 &mut self,
649 fitness: &F,
650 target_fitness: f64,
651 max_generations: usize,
652 rng: &mut R,
653 ) -> EvoResult<Individual<RealVector>> {
654 let mut best: Option<Individual<RealVector>> = None;
655
656 for _ in 0..max_generations {
657 let population = self.step(fitness, rng)?;
658
659 if let Some(current_best) = population.first() {
661 match &best {
662 None => best = Some(current_best.clone()),
663 Some(existing) => {
664 if current_best.fitness_f64() < existing.fitness_f64() {
665 best = Some(current_best.clone());
666 }
667 }
668 }
669 }
670
671 if let Some(ref b) = best {
673 if b.fitness_f64() <= target_fitness {
674 break;
675 }
676 }
677
678 if self.state.has_converged() {
680 break;
681 }
682 }
683
684 best.ok_or(EvolutionError::EmptyPopulation)
685 }
686
687 pub fn generation(&self) -> usize {
689 self.state.generation
690 }
691
692 pub fn evaluations(&self) -> usize {
694 self.state.evaluations
695 }
696
697 pub fn mean(&self) -> &[f64] {
699 &self.state.mean
700 }
701
702 pub fn sigma(&self) -> f64 {
704 self.state.sigma
705 }
706
707 pub fn best_solution(&self) -> &[f64] {
709 &self.state.best_solution
710 }
711
712 pub fn best_fitness(&self) -> f64 {
714 self.state.best_fitness
715 }
716}
717
718pub struct CmaEsBuilder {
720 initial_mean: Option<Vec<f64>>,
721 initial_sigma: f64,
722 lambda: Option<usize>,
723 bounds: Option<MultiBounds>,
724}
725
726impl CmaEsBuilder {
727 pub fn new() -> Self {
729 Self {
730 initial_mean: None,
731 initial_sigma: 1.0,
732 lambda: None,
733 bounds: None,
734 }
735 }
736
737 pub fn mean(mut self, mean: Vec<f64>) -> Self {
739 self.initial_mean = Some(mean);
740 self
741 }
742
743 pub fn sigma(mut self, sigma: f64) -> Self {
745 self.initial_sigma = sigma;
746 self
747 }
748
749 pub fn lambda(mut self, lambda: usize) -> Self {
751 self.lambda = Some(lambda);
752 self
753 }
754
755 pub fn bounds(mut self, bounds: MultiBounds) -> Self {
757 self.bounds = Some(bounds);
758 self
759 }
760
761 pub fn build<F: CmaEsFitness>(self) -> EvoResult<CmaEs<F>> {
763 let mean = self
764 .initial_mean
765 .ok_or_else(|| EvolutionError::Configuration("Initial mean not set".to_string()))?;
766
767 let mut cmaes = match self.lambda {
768 Some(l) => CmaEs::with_lambda(mean, self.initial_sigma, l),
769 None => CmaEs::new(mean, self.initial_sigma),
770 };
771
772 if let Some(bounds) = self.bounds {
773 cmaes = cmaes.with_bounds(bounds);
774 }
775
776 Ok(cmaes)
777 }
778}
779
780impl Default for CmaEsBuilder {
781 fn default() -> Self {
782 Self::new()
783 }
784}
785
786#[cfg(test)]
787mod tests {
788 use super::*;
789 use crate::genome::traits::EvolutionaryGenome;
790 use approx::assert_relative_eq;
791
792 struct Sphere;
794
795 impl CmaEsFitness for Sphere {
796 fn evaluate(&self, genome: &RealVector) -> f64 {
797 genome.genes().iter().map(|x| x * x).sum()
798 }
799 }
800
801 #[test]
802 fn test_cmaes_state_initialization() {
803 let mean = vec![0.0, 0.0, 0.0];
804 let state = CmaEsState::new(mean.clone(), 1.0, None);
805
806 assert_eq!(state.dimension, 3);
807 assert_eq!(state.mean, mean);
808 assert_eq!(state.sigma, 1.0);
809 assert!(state.lambda >= 4);
810 assert!(state.mu > 0);
811 assert_eq!(state.weights.len(), state.mu);
812 }
813
814 #[test]
815 fn test_cmaes_weights_sum_to_one() {
816 let state = CmaEsState::new(vec![0.0; 10], 1.0, None);
817 let sum: f64 = state.weights.iter().sum();
818 assert_relative_eq!(sum, 1.0, epsilon = 1e-10);
819 }
820
821 #[test]
822 fn test_cmaes_sampling() {
823 let mut rng = rand::thread_rng();
824 let state = CmaEsState::new(vec![0.0; 5], 1.0, Some(10));
825
826 let samples = state.sample_population(&mut rng);
827
828 assert_eq!(samples.len(), 10);
829 for sample in &samples {
830 assert_eq!(sample.dimension(), 5);
831 }
832 }
833
834 #[test]
835 fn test_cmaes_step() {
836 let mut rng = rand::thread_rng();
837 let fitness = Sphere;
838 let mut cmaes: CmaEs<Sphere> = CmaEs::new(vec![5.0, 5.0, 5.0], 2.0);
839
840 let population = cmaes.step(&fitness, &mut rng).unwrap();
841
842 assert_eq!(population.len(), cmaes.state.lambda);
843 assert_eq!(cmaes.state.generation, 1);
844 }
845
846 #[test]
847 fn test_cmaes_optimization() {
848 use rand::SeedableRng;
849 let mut rng = rand::rngs::StdRng::seed_from_u64(42);
850 let fitness = Sphere;
851 let mut cmaes: CmaEs<Sphere> = CmaEs::new(vec![5.0, 5.0], 2.0);
852
853 let result = cmaes.run_generations(&fitness, 150, &mut rng).unwrap();
855
856 let final_fitness = result.fitness_f64();
859 let initial_fitness = 50.0; assert!(
861 final_fitness < initial_fitness * 0.7,
862 "Final fitness {} should be significantly better than initial {}",
863 final_fitness,
864 initial_fitness
865 );
866 }
867
868 #[test]
869 fn test_cmaes_with_bounds() {
870 let mut rng = rand::thread_rng();
871 let fitness = Sphere;
872 let bounds = MultiBounds::symmetric(10.0, 3);
873
874 let mut cmaes: CmaEs<Sphere> = CmaEs::new(vec![5.0, 5.0, 5.0], 2.0).with_bounds(bounds);
875
876 let result = cmaes.run_generations(&fitness, 30, &mut rng).unwrap();
877
878 for gene in result.genome().genes() {
880 assert!(*gene >= -10.0 && *gene <= 10.0);
881 }
882 }
883
884 #[test]
885 fn test_cmaes_builder() {
886 let cmaes: CmaEs<Sphere> = CmaEsBuilder::new()
887 .mean(vec![0.0, 0.0, 0.0])
888 .sigma(0.5)
889 .lambda(20)
890 .bounds(MultiBounds::symmetric(5.0, 3))
891 .build()
892 .unwrap();
893
894 assert_eq!(cmaes.state.lambda, 20);
895 assert_eq!(cmaes.state.sigma, 0.5);
896 assert!(cmaes.bounds.is_some());
897 }
898
899 #[test]
900 fn test_cmaes_convergence_detection() {
901 let mut state = CmaEsState::new(vec![0.0, 0.0], 1e-20, None);
902 assert!(state.has_converged());
903
904 state.sigma = 1.0;
905 state.eigenvalues = vec![1e15, 1.0];
906 assert!(state.has_converged());
907 }
908
909 #[test]
910 fn test_jacobi_eigendecomposition() {
911 let a = vec![vec![4.0, 1.0], vec![1.0, 3.0]];
913
914 let (eigenvalues, _eigenvectors) = jacobi_eigendecomposition(&a);
915
916 let expected_sum = 7.0; let actual_sum: f64 = eigenvalues.iter().sum();
919 assert_relative_eq!(actual_sum, expected_sum, epsilon = 1e-6);
920 }
921}