1use std::time::Instant;
10
11use rand::Rng;
12use rand_distr::StandardNormal;
13
14use crate::diagnostics::{EvolutionResult, EvolutionStats, GenerationStats, TimingStats};
15use crate::error::EvolutionError;
16use crate::fitness::traits::{Fitness, FitnessValue};
17use crate::genome::bounds::MultiBounds;
18use crate::genome::traits::{EvolutionaryGenome, RealValuedGenome};
19use crate::hyperparameter::self_adaptive::{AdaptiveGenome, StrategyParams};
20use crate::population::individual::Individual;
21use crate::population::population::Population;
22use crate::termination::{EvolutionState, MaxGenerations, TerminationCriterion};
23
24#[derive(Clone, Debug, Default)]
38pub enum ESSelectionStrategy {
39 MuPlusLambda,
45 #[default]
49 MuCommaLambda,
50}
51
52#[derive(Clone, Debug)]
54pub struct ESConfig {
55 pub mu: usize,
57 pub lambda: usize,
59 pub selection: ESSelectionStrategy,
61 pub initial_sigma: f64,
63 pub self_adaptive: bool,
65 pub recombination: RecombinationType,
67 pub min_sigma: Option<f64>,
75}
76
77impl Default for ESConfig {
78 fn default() -> Self {
84 Self {
85 mu: 15,
86 lambda: 100,
87 selection: ESSelectionStrategy::MuCommaLambda,
88 initial_sigma: 1.0,
89 self_adaptive: true,
90 recombination: RecombinationType::Intermediate,
91 min_sigma: None,
92 }
93 }
94}
95
96impl ESConfig {
97 pub fn resolved_min_sigma(&self) -> f64 {
102 self.min_sigma.unwrap_or(1e-8 * self.initial_sigma)
103 }
104
105 pub fn mu_plus_lambda(mu: usize, lambda: usize) -> Self {
107 Self {
108 mu,
109 lambda,
110 selection: ESSelectionStrategy::MuPlusLambda,
111 ..Default::default()
112 }
113 }
114
115 pub fn mu_comma_lambda(mu: usize, lambda: usize) -> Result<Self, EvolutionError> {
117 if lambda < mu {
118 return Err(EvolutionError::Configuration(format!(
119 "For (μ,λ)-ES, λ ({}) must be >= μ ({})",
120 lambda, mu
121 )));
122 }
123 Ok(Self {
124 mu,
125 lambda,
126 selection: ESSelectionStrategy::MuCommaLambda,
127 ..Default::default()
128 })
129 }
130}
131
132#[derive(Clone, Debug, Default)]
134pub enum RecombinationType {
135 None,
137 Discrete,
139 #[default]
141 Intermediate,
142 GlobalIntermediate,
144}
145
146pub struct ESBuilder<G, F, Fit, Term>
148where
149 G: EvolutionaryGenome,
150 F: FitnessValue,
151{
152 config: ESConfig,
153 bounds: Option<MultiBounds>,
154 fitness: Option<Fit>,
155 termination: Option<Term>,
156 _phantom: std::marker::PhantomData<(G, F)>,
157}
158
159impl<G, F> ESBuilder<G, F, (), ()>
160where
161 G: EvolutionaryGenome,
162 F: FitnessValue,
163{
164 pub fn new() -> Self {
166 Self {
167 config: ESConfig::default(),
168 bounds: None,
169 fitness: None,
170 termination: None,
171 _phantom: std::marker::PhantomData,
172 }
173 }
174
175 pub fn mu_plus_lambda(mu: usize, lambda: usize) -> Self {
177 Self {
178 config: ESConfig::mu_plus_lambda(mu, lambda),
179 bounds: None,
180 fitness: None,
181 termination: None,
182 _phantom: std::marker::PhantomData,
183 }
184 }
185
186 pub fn mu_comma_lambda(mu: usize, lambda: usize) -> Result<Self, EvolutionError> {
188 Ok(Self {
189 config: ESConfig::mu_comma_lambda(mu, lambda)?,
190 bounds: None,
191 fitness: None,
192 termination: None,
193 _phantom: std::marker::PhantomData,
194 })
195 }
196}
197
198impl<G, F> Default for ESBuilder<G, F, (), ()>
199where
200 G: EvolutionaryGenome,
201 F: FitnessValue,
202{
203 fn default() -> Self {
204 Self::new()
205 }
206}
207
208impl<G, F, Fit, Term> ESBuilder<G, F, Fit, Term>
209where
210 G: EvolutionaryGenome,
211 F: FitnessValue,
212{
213 pub fn mu(mut self, mu: usize) -> Self {
215 self.config.mu = mu;
216 self
217 }
218
219 pub fn lambda(mut self, lambda: usize) -> Self {
221 self.config.lambda = lambda;
222 self
223 }
224
225 pub fn selection_strategy(mut self, strategy: ESSelectionStrategy) -> Self {
227 self.config.selection = strategy;
228 self
229 }
230
231 pub fn initial_sigma(mut self, sigma: f64) -> Self {
233 self.config.initial_sigma = sigma;
234 self
235 }
236
237 pub fn min_sigma(mut self, sigma: f64) -> Self {
242 self.config.min_sigma = Some(sigma);
243 self
244 }
245
246 pub fn self_adaptive(mut self, enabled: bool) -> Self {
248 self.config.self_adaptive = enabled;
249 self
250 }
251
252 pub fn recombination(mut self, recomb: RecombinationType) -> Self {
254 self.config.recombination = recomb;
255 self
256 }
257
258 pub fn bounds(mut self, bounds: MultiBounds) -> Self {
260 self.bounds = Some(bounds);
261 self
262 }
263
264 pub fn fitness<NewFit>(self, fitness: NewFit) -> ESBuilder<G, F, NewFit, Term>
266 where
267 NewFit: Fitness<Genome = G, Value = F>,
268 {
269 ESBuilder {
270 config: self.config,
271 bounds: self.bounds,
272 fitness: Some(fitness),
273 termination: self.termination,
274 _phantom: std::marker::PhantomData,
275 }
276 }
277
278 pub fn termination<NewTerm>(self, termination: NewTerm) -> ESBuilder<G, F, Fit, NewTerm>
280 where
281 NewTerm: TerminationCriterion<G, F>,
282 {
283 ESBuilder {
284 config: self.config,
285 bounds: self.bounds,
286 fitness: self.fitness,
287 termination: Some(termination),
288 _phantom: std::marker::PhantomData,
289 }
290 }
291
292 pub fn max_generations(self, max: usize) -> ESBuilder<G, F, Fit, MaxGenerations> {
294 ESBuilder {
295 config: self.config,
296 bounds: self.bounds,
297 fitness: self.fitness,
298 termination: Some(MaxGenerations::new(max)),
299 _phantom: std::marker::PhantomData,
300 }
301 }
302}
303
304#[cfg(feature = "parallel")]
306impl<G, F, Fit, Term> ESBuilder<G, F, Fit, Term>
307where
308 G: EvolutionaryGenome + RealValuedGenome + Send + Sync,
309 F: FitnessValue + Send,
310 Fit: Fitness<Genome = G, Value = F> + Sync,
311 Term: TerminationCriterion<G, F>,
312{
313 pub fn build(self) -> Result<EvolutionStrategy<G, F, Fit, Term>, EvolutionError> {
315 let bounds = self
316 .bounds
317 .ok_or_else(|| EvolutionError::Configuration("Bounds must be specified".to_string()))?;
318
319 let fitness = self.fitness.ok_or_else(|| {
320 EvolutionError::Configuration("Fitness function must be specified".to_string())
321 })?;
322
323 let termination = self.termination.ok_or_else(|| {
324 EvolutionError::Configuration("Termination criterion must be specified".to_string())
325 })?;
326
327 if matches!(self.config.selection, ESSelectionStrategy::MuCommaLambda)
329 && self.config.lambda < self.config.mu
330 {
331 return Err(EvolutionError::Configuration(format!(
332 "For (μ,λ)-ES, λ ({}) must be >= μ ({})",
333 self.config.lambda, self.config.mu
334 )));
335 }
336
337 Ok(EvolutionStrategy {
338 config: self.config,
339 bounds,
340 fitness,
341 termination,
342 _phantom: std::marker::PhantomData,
343 })
344 }
345}
346
347#[cfg(not(feature = "parallel"))]
349impl<G, F, Fit, Term> ESBuilder<G, F, Fit, Term>
350where
351 G: EvolutionaryGenome + RealValuedGenome,
352 F: FitnessValue,
353 Fit: Fitness<Genome = G, Value = F>,
354 Term: TerminationCriterion<G, F>,
355{
356 pub fn build(self) -> Result<EvolutionStrategy<G, F, Fit, Term>, EvolutionError> {
358 let bounds = self
359 .bounds
360 .ok_or_else(|| EvolutionError::Configuration("Bounds must be specified".to_string()))?;
361
362 let fitness = self.fitness.ok_or_else(|| {
363 EvolutionError::Configuration("Fitness function must be specified".to_string())
364 })?;
365
366 let termination = self.termination.ok_or_else(|| {
367 EvolutionError::Configuration("Termination criterion must be specified".to_string())
368 })?;
369
370 if matches!(self.config.selection, ESSelectionStrategy::MuCommaLambda)
372 && self.config.lambda < self.config.mu
373 {
374 return Err(EvolutionError::Configuration(format!(
375 "For (μ,λ)-ES, λ ({}) must be >= μ ({})",
376 self.config.lambda, self.config.mu
377 )));
378 }
379
380 Ok(EvolutionStrategy {
381 config: self.config,
382 bounds,
383 fitness,
384 termination,
385 _phantom: std::marker::PhantomData,
386 })
387 }
388}
389
390pub struct EvolutionStrategy<G, F, Fit, Term>
395where
396 G: EvolutionaryGenome,
397 F: FitnessValue,
398{
399 config: ESConfig,
400 bounds: MultiBounds,
401 fitness: Fit,
402 termination: Term,
403 _phantom: std::marker::PhantomData<(G, F)>,
404}
405
406#[cfg(feature = "parallel")]
408impl<G, F, Fit, Term> EvolutionStrategy<G, F, Fit, Term>
409where
410 G: EvolutionaryGenome + RealValuedGenome + Send + Sync,
411 F: FitnessValue + Send,
412 Fit: Fitness<Genome = G, Value = F> + Sync,
413 Term: TerminationCriterion<G, F>,
414{
415 pub fn builder() -> ESBuilder<G, F, (), ()> {
417 ESBuilder::new()
418 }
419
420 pub fn run<R: Rng>(&self, rng: &mut R) -> Result<EvolutionResult<G, F>, EvolutionError> {
422 self.run_with_callback(rng, |_generation, _best_fitness| true)
425 }
426
427 pub fn run_with_callback<R: Rng, Cb: FnMut(usize, f64) -> bool>(
434 &self,
435 rng: &mut R,
436 mut on_generation: Cb,
437 ) -> Result<EvolutionResult<G, F>, EvolutionError> {
438 let start_time = Instant::now();
439
440 let mut population: Vec<(AdaptiveGenome<G>, F)> = (0..self.config.mu)
442 .map(|_| {
443 let genome = G::generate(rng, &self.bounds);
444 let adaptive = if self.config.self_adaptive {
445 AdaptiveGenome::new_non_isotropic(
446 genome,
447 vec![self.config.initial_sigma; self.bounds.dimension()],
448 )
449 } else {
450 AdaptiveGenome::new_isotropic(genome, self.config.initial_sigma)
451 };
452 let fitness = self.fitness.evaluate(adaptive.inner());
453 (adaptive, fitness)
454 })
455 .collect();
456
457 population.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
459
460 let mut stats = EvolutionStats::new();
461 let mut evaluations = self.config.mu;
462 let mut fitness_history: Vec<f64> = Vec::new();
463 let mut generation = 0usize;
464
465 let mut best = population[0].clone();
467
468 let mut tracking_population: Population<G, F> = Population::with_capacity(self.config.mu);
470 for (adaptive, fit) in &population {
471 let mut ind = Individual::new(adaptive.inner().clone());
472 ind.set_fitness(fit.clone());
473 tracking_population.push(ind);
474 }
475
476 let gen_stats = GenerationStats::from_population(&tracking_population, 0, evaluations);
478 fitness_history.push(gen_stats.best_fitness);
479 stats.record(gen_stats);
480
481 loop {
483 let state = EvolutionState {
485 generation,
486 evaluations,
487 best_fitness: best.1.to_f64(),
488 population: &tracking_population,
489 fitness_history: &fitness_history,
490 };
491
492 if self.termination.should_terminate(&state) {
493 stats.set_termination_reason(self.termination.reason());
494 break;
495 }
496
497 if !on_generation(generation, best.1.to_f64()) {
500 break;
501 }
502
503 let gen_start = Instant::now();
504
505 let mut offspring: Vec<(AdaptiveGenome<G>, F)> = Vec::with_capacity(self.config.lambda);
507
508 for _ in 0..self.config.lambda {
509 let child = match &self.config.recombination {
511 RecombinationType::None => {
512 let parent_idx = rng.gen_range(0..self.config.mu);
514 population[parent_idx].0.clone()
515 }
516 RecombinationType::Discrete => {
517 let p1_idx = rng.gen_range(0..self.config.mu);
519 let p2_idx = rng.gen_range(0..self.config.mu);
520 self.discrete_recombination(
521 &population[p1_idx].0,
522 &population[p2_idx].0,
523 rng,
524 )
525 }
526 RecombinationType::Intermediate => {
527 let p1_idx = rng.gen_range(0..self.config.mu);
529 let p2_idx = rng.gen_range(0..self.config.mu);
530 self.intermediate_recombination(
531 &population[p1_idx].0,
532 &population[p2_idx].0,
533 )
534 }
535 RecombinationType::GlobalIntermediate => {
536 self.global_intermediate_recombination(&population)
538 }
539 };
540
541 let mutated = self.mutate(child, rng);
543
544 let fitness = self.fitness.evaluate(mutated.inner());
546 offspring.push((mutated, fitness));
547 }
548 evaluations += self.config.lambda;
549
550 match self.config.selection {
552 ESSelectionStrategy::MuPlusLambda => {
553 let mut combined = population;
555 combined.extend(offspring);
556 combined
557 .sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
558 population = combined.into_iter().take(self.config.mu).collect();
559 }
560 ESSelectionStrategy::MuCommaLambda => {
561 offspring
563 .sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
564 population = offspring.into_iter().take(self.config.mu).collect();
565 }
566 }
567
568 if population[0].1.is_better_than(&best.1) {
570 best = population[0].clone();
571 }
572
573 generation += 1;
574
575 tracking_population.clear();
577 for (adaptive, fit) in &population {
578 let mut ind = Individual::new(adaptive.inner().clone());
579 ind.set_fitness(fit.clone());
580 tracking_population.push(ind);
581 }
582 tracking_population.set_generation(generation);
583
584 let timing = TimingStats::new().with_total(gen_start.elapsed());
586 let gen_stats =
587 GenerationStats::from_population(&tracking_population, generation, evaluations)
588 .with_timing(timing);
589 fitness_history.push(gen_stats.best_fitness);
590 stats.record(gen_stats);
591 }
592
593 stats.set_runtime(start_time.elapsed());
594
595 Ok(
596 EvolutionResult::new(best.0.into_inner(), best.1, generation, evaluations)
597 .with_stats(stats),
598 )
599 }
600
601 fn discrete_recombination<R: Rng>(
603 &self,
604 p1: &AdaptiveGenome<G>,
605 p2: &AdaptiveGenome<G>,
606 rng: &mut R,
607 ) -> AdaptiveGenome<G> {
608 let genes1 = p1.inner().genes();
609 let genes2 = p2.inner().genes();
610
611 let child_genes: Vec<f64> = genes1
612 .iter()
613 .zip(genes2.iter())
614 .map(|(g1, g2)| if rng.gen_bool(0.5) { *g1 } else { *g2 })
615 .collect();
616
617 let child_genome = G::from_genes(child_genes).expect("Failed to create genome from genes");
618
619 match (&p1.strategy, &p2.strategy) {
621 (StrategyParams::Isotropic(s1), StrategyParams::Isotropic(s2)) => {
622 AdaptiveGenome::new_isotropic(child_genome, (s1 * s2).sqrt())
623 }
624 (StrategyParams::NonIsotropic(s1), StrategyParams::NonIsotropic(s2)) => {
625 let sigmas: Vec<f64> = s1
626 .iter()
627 .zip(s2.iter())
628 .map(|(a, b)| if rng.gen_bool(0.5) { *a } else { *b })
629 .collect();
630 AdaptiveGenome::new_non_isotropic(child_genome, sigmas)
631 }
632 _ => AdaptiveGenome::new_isotropic(child_genome, self.config.initial_sigma),
633 }
634 }
635
636 fn intermediate_recombination(
638 &self,
639 p1: &AdaptiveGenome<G>,
640 p2: &AdaptiveGenome<G>,
641 ) -> AdaptiveGenome<G> {
642 let genes1 = p1.inner().genes();
643 let genes2 = p2.inner().genes();
644
645 let child_genes: Vec<f64> = genes1
646 .iter()
647 .zip(genes2.iter())
648 .map(|(g1, g2)| (g1 + g2) / 2.0)
649 .collect();
650
651 let child_genome = G::from_genes(child_genes).expect("Failed to create genome from genes");
652
653 match (&p1.strategy, &p2.strategy) {
655 (StrategyParams::Isotropic(s1), StrategyParams::Isotropic(s2)) => {
656 AdaptiveGenome::new_isotropic(child_genome, (s1 * s2).sqrt())
657 }
658 (StrategyParams::NonIsotropic(s1), StrategyParams::NonIsotropic(s2)) => {
659 let sigmas: Vec<f64> = s1
660 .iter()
661 .zip(s2.iter())
662 .map(|(a, b)| (a * b).sqrt())
663 .collect();
664 AdaptiveGenome::new_non_isotropic(child_genome, sigmas)
665 }
666 _ => AdaptiveGenome::new_isotropic(child_genome, self.config.initial_sigma),
667 }
668 }
669
670 fn global_intermediate_recombination(
672 &self,
673 population: &[(AdaptiveGenome<G>, F)],
674 ) -> AdaptiveGenome<G> {
675 let n = population.len();
676 let dim = population[0].0.inner().genes().len();
677
678 let mut child_genes = vec![0.0; dim];
679 for (adaptive, _) in population {
680 for (i, gene) in adaptive.inner().genes().iter().enumerate() {
681 child_genes[i] += gene / n as f64;
682 }
683 }
684
685 let child_genome = G::from_genes(child_genes).expect("Failed to create genome from genes");
686
687 let avg_sigma = if self.config.self_adaptive {
689 let sigmas: Vec<f64> = (0..dim)
691 .map(|i| {
692 let sum: f64 = population
693 .iter()
694 .map(|(a, _)| a.strategy.get_sigma(i))
695 .sum();
696 sum / n as f64
697 })
698 .collect();
699 AdaptiveGenome::new_non_isotropic(child_genome, sigmas)
700 } else {
701 AdaptiveGenome::new_isotropic(child_genome, self.config.initial_sigma)
702 };
703
704 avg_sigma
705 }
706
707 fn mutate<R: Rng>(&self, mut genome: AdaptiveGenome<G>, rng: &mut R) -> AdaptiveGenome<G> {
709 let n = genome.inner().genes().len();
710
711 if self.config.self_adaptive {
713 genome
714 .strategy
715 .mutate(n, self.config.resolved_min_sigma(), rng);
716 }
717
718 let sigmas: Vec<f64> = (0..n).map(|i| genome.strategy.get_sigma(i)).collect();
720
721 let genes = genome.inner_mut().genes_mut();
723 for i in 0..genes.len() {
724 let perturbation: f64 = rng.sample(StandardNormal);
725 genes[i] += sigmas[i] * perturbation;
726
727 if let Some(b) = self.bounds.get(i) {
729 genes[i] = genes[i].clamp(b.min, b.max);
730 }
731 }
732
733 genome
734 }
735}
736
737#[cfg(not(feature = "parallel"))]
739impl<G, F, Fit, Term> EvolutionStrategy<G, F, Fit, Term>
740where
741 G: EvolutionaryGenome + RealValuedGenome,
742 F: FitnessValue,
743 Fit: Fitness<Genome = G, Value = F>,
744 Term: TerminationCriterion<G, F>,
745{
746 pub fn builder() -> ESBuilder<G, F, (), ()> {
748 ESBuilder::new()
749 }
750
751 pub fn run<R: Rng>(&self, rng: &mut R) -> Result<EvolutionResult<G, F>, EvolutionError> {
753 self.run_with_callback(rng, |_generation, _best_fitness| true)
756 }
757
758 pub fn run_with_callback<R: Rng, Cb: FnMut(usize, f64) -> bool>(
765 &self,
766 rng: &mut R,
767 mut on_generation: Cb,
768 ) -> Result<EvolutionResult<G, F>, EvolutionError> {
769 let start_time = Instant::now();
770
771 let mut population: Vec<(AdaptiveGenome<G>, F)> = (0..self.config.mu)
773 .map(|_| {
774 let genome = G::generate(rng, &self.bounds);
775 let adaptive = if self.config.self_adaptive {
776 AdaptiveGenome::new_non_isotropic(
777 genome,
778 vec![self.config.initial_sigma; self.bounds.dimension()],
779 )
780 } else {
781 AdaptiveGenome::new_isotropic(genome, self.config.initial_sigma)
782 };
783 let fitness = self.fitness.evaluate(adaptive.inner());
784 (adaptive, fitness)
785 })
786 .collect();
787
788 population.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
790
791 let mut stats = EvolutionStats::new();
792 let mut evaluations = self.config.mu;
793 let mut fitness_history: Vec<f64> = Vec::new();
794 let mut generation = 0usize;
795
796 let mut best = population[0].clone();
798
799 let mut tracking_population: Population<G, F> = Population::with_capacity(self.config.mu);
801 for (adaptive, fit) in &population {
802 let mut ind = Individual::new(adaptive.inner().clone());
803 ind.set_fitness(fit.clone());
804 tracking_population.push(ind);
805 }
806
807 let gen_stats = GenerationStats::from_population(&tracking_population, 0, evaluations);
809 fitness_history.push(gen_stats.best_fitness);
810 stats.record(gen_stats);
811
812 loop {
814 let state = EvolutionState {
816 generation,
817 evaluations,
818 best_fitness: best.1.to_f64(),
819 population: &tracking_population,
820 fitness_history: &fitness_history,
821 };
822
823 if self.termination.should_terminate(&state) {
824 stats.set_termination_reason(self.termination.reason());
825 break;
826 }
827
828 if !on_generation(generation, best.1.to_f64()) {
831 break;
832 }
833
834 let gen_start = Instant::now();
835
836 let mut offspring: Vec<(AdaptiveGenome<G>, F)> = Vec::with_capacity(self.config.lambda);
838
839 for _ in 0..self.config.lambda {
840 let child = match &self.config.recombination {
842 RecombinationType::None => {
843 let parent_idx = rng.gen_range(0..self.config.mu);
845 population[parent_idx].0.clone()
846 }
847 RecombinationType::Discrete => {
848 let p1_idx = rng.gen_range(0..self.config.mu);
850 let p2_idx = rng.gen_range(0..self.config.mu);
851 self.discrete_recombination(
852 &population[p1_idx].0,
853 &population[p2_idx].0,
854 rng,
855 )
856 }
857 RecombinationType::Intermediate => {
858 let p1_idx = rng.gen_range(0..self.config.mu);
860 let p2_idx = rng.gen_range(0..self.config.mu);
861 self.intermediate_recombination(
862 &population[p1_idx].0,
863 &population[p2_idx].0,
864 )
865 }
866 RecombinationType::GlobalIntermediate => {
867 self.global_intermediate_recombination(&population)
869 }
870 };
871
872 let mutated = self.mutate(child, rng);
874
875 let fitness = self.fitness.evaluate(mutated.inner());
877 offspring.push((mutated, fitness));
878 }
879 evaluations += self.config.lambda;
880
881 match self.config.selection {
883 ESSelectionStrategy::MuPlusLambda => {
884 let mut combined = population;
886 combined.extend(offspring);
887 combined
888 .sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
889 population = combined.into_iter().take(self.config.mu).collect();
890 }
891 ESSelectionStrategy::MuCommaLambda => {
892 offspring
894 .sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
895 population = offspring.into_iter().take(self.config.mu).collect();
896 }
897 }
898
899 if population[0].1.is_better_than(&best.1) {
901 best = population[0].clone();
902 }
903
904 generation += 1;
905
906 tracking_population.clear();
908 for (adaptive, fit) in &population {
909 let mut ind = Individual::new(adaptive.inner().clone());
910 ind.set_fitness(fit.clone());
911 tracking_population.push(ind);
912 }
913 tracking_population.set_generation(generation);
914
915 let timing = TimingStats::new().with_total(gen_start.elapsed());
917 let gen_stats =
918 GenerationStats::from_population(&tracking_population, generation, evaluations)
919 .with_timing(timing);
920 fitness_history.push(gen_stats.best_fitness);
921 stats.record(gen_stats);
922 }
923
924 stats.set_runtime(start_time.elapsed());
925
926 Ok(
927 EvolutionResult::new(best.0.into_inner(), best.1, generation, evaluations)
928 .with_stats(stats),
929 )
930 }
931
932 fn discrete_recombination<R: Rng>(
934 &self,
935 p1: &AdaptiveGenome<G>,
936 p2: &AdaptiveGenome<G>,
937 rng: &mut R,
938 ) -> AdaptiveGenome<G> {
939 let genes1 = p1.inner().genes();
940 let genes2 = p2.inner().genes();
941
942 let child_genes: Vec<f64> = genes1
943 .iter()
944 .zip(genes2.iter())
945 .map(|(g1, g2)| if rng.gen_bool(0.5) { *g1 } else { *g2 })
946 .collect();
947
948 let child_genome = G::from_genes(child_genes).expect("Failed to create genome from genes");
949
950 match (&p1.strategy, &p2.strategy) {
952 (StrategyParams::Isotropic(s1), StrategyParams::Isotropic(s2)) => {
953 AdaptiveGenome::new_isotropic(child_genome, (s1 * s2).sqrt())
954 }
955 (StrategyParams::NonIsotropic(s1), StrategyParams::NonIsotropic(s2)) => {
956 let sigmas: Vec<f64> = s1
957 .iter()
958 .zip(s2.iter())
959 .map(|(a, b)| if rng.gen_bool(0.5) { *a } else { *b })
960 .collect();
961 AdaptiveGenome::new_non_isotropic(child_genome, sigmas)
962 }
963 _ => AdaptiveGenome::new_isotropic(child_genome, self.config.initial_sigma),
964 }
965 }
966
967 fn intermediate_recombination(
969 &self,
970 p1: &AdaptiveGenome<G>,
971 p2: &AdaptiveGenome<G>,
972 ) -> AdaptiveGenome<G> {
973 let genes1 = p1.inner().genes();
974 let genes2 = p2.inner().genes();
975
976 let child_genes: Vec<f64> = genes1
977 .iter()
978 .zip(genes2.iter())
979 .map(|(g1, g2)| (g1 + g2) / 2.0)
980 .collect();
981
982 let child_genome = G::from_genes(child_genes).expect("Failed to create genome from genes");
983
984 match (&p1.strategy, &p2.strategy) {
986 (StrategyParams::Isotropic(s1), StrategyParams::Isotropic(s2)) => {
987 AdaptiveGenome::new_isotropic(child_genome, (s1 * s2).sqrt())
988 }
989 (StrategyParams::NonIsotropic(s1), StrategyParams::NonIsotropic(s2)) => {
990 let sigmas: Vec<f64> = s1
991 .iter()
992 .zip(s2.iter())
993 .map(|(a, b)| (a * b).sqrt())
994 .collect();
995 AdaptiveGenome::new_non_isotropic(child_genome, sigmas)
996 }
997 _ => AdaptiveGenome::new_isotropic(child_genome, self.config.initial_sigma),
998 }
999 }
1000
1001 fn global_intermediate_recombination(
1003 &self,
1004 population: &[(AdaptiveGenome<G>, F)],
1005 ) -> AdaptiveGenome<G> {
1006 let n = population.len();
1007 let dim = population[0].0.inner().genes().len();
1008
1009 let mut child_genes = vec![0.0; dim];
1010 for (adaptive, _) in population {
1011 for (i, gene) in adaptive.inner().genes().iter().enumerate() {
1012 child_genes[i] += gene / n as f64;
1013 }
1014 }
1015
1016 let child_genome = G::from_genes(child_genes).expect("Failed to create genome from genes");
1017
1018 let avg_sigma = if self.config.self_adaptive {
1020 let sigmas: Vec<f64> = (0..dim)
1022 .map(|i| {
1023 let sum: f64 = population
1024 .iter()
1025 .map(|(a, _)| a.strategy.get_sigma(i))
1026 .sum();
1027 sum / n as f64
1028 })
1029 .collect();
1030 AdaptiveGenome::new_non_isotropic(child_genome, sigmas)
1031 } else {
1032 AdaptiveGenome::new_isotropic(child_genome, self.config.initial_sigma)
1033 };
1034
1035 avg_sigma
1036 }
1037
1038 fn mutate<R: Rng>(&self, mut genome: AdaptiveGenome<G>, rng: &mut R) -> AdaptiveGenome<G> {
1040 let n = genome.inner().genes().len();
1041
1042 if self.config.self_adaptive {
1044 genome
1045 .strategy
1046 .mutate(n, self.config.resolved_min_sigma(), rng);
1047 }
1048
1049 let sigmas: Vec<f64> = (0..n).map(|i| genome.strategy.get_sigma(i)).collect();
1051
1052 let genes = genome.inner_mut().genes_mut();
1054 for i in 0..genes.len() {
1055 let perturbation: f64 = rng.sample(StandardNormal);
1056 genes[i] += sigmas[i] * perturbation;
1057
1058 if let Some(b) = self.bounds.get(i) {
1060 genes[i] = genes[i].clamp(b.min, b.max);
1061 }
1062 }
1063
1064 genome
1065 }
1066}
1067
1068pub type MuPlusLambdaES<G, F, Fit, Term> = EvolutionStrategy<G, F, Fit, Term>;
1070
1071pub type MuCommaLambdaES<G, F, Fit, Term> = EvolutionStrategy<G, F, Fit, Term>;
1073
1074#[cfg(test)]
1075mod tests {
1076 use super::*;
1077 use crate::fitness::benchmarks::Sphere;
1078 use crate::genome::real_vector::RealVector;
1079 use crate::termination::MaxEvaluations;
1080 use rand::SeedableRng;
1081
1082 #[test]
1083 fn test_es_builder() {
1084 let bounds = MultiBounds::symmetric(5.0, 10);
1085 let es: Result<EvolutionStrategy<RealVector, f64, _, _>, _> = ESBuilder::new()
1086 .mu(15)
1087 .lambda(100)
1088 .bounds(bounds)
1089 .fitness(Sphere::new(10))
1090 .max_generations(10)
1091 .build();
1092
1093 assert!(es.is_ok());
1094 }
1095
1096 #[test]
1097 fn test_mu_plus_lambda_es() {
1098 let mut rng = rand::thread_rng();
1099 let bounds = MultiBounds::symmetric(5.12, 10);
1100
1101 let es: EvolutionStrategy<RealVector, f64, _, _> = ESBuilder::mu_plus_lambda(10, 70)
1102 .initial_sigma(1.0)
1103 .self_adaptive(true)
1104 .bounds(bounds)
1105 .fitness(Sphere::new(10))
1106 .termination(MaxEvaluations::new(3000))
1107 .build()
1108 .unwrap();
1109
1110 let result = es.run(&mut rng).unwrap();
1111
1112 assert!(
1114 result.best_fitness > -50.0,
1115 "Expected fitness > -50, got {}",
1116 result.best_fitness
1117 );
1118 }
1119
1120 #[test]
1121 fn test_mu_comma_lambda_es() {
1122 let mut rng = rand::thread_rng();
1123 let bounds = MultiBounds::symmetric(5.12, 10);
1124
1125 let es: EvolutionStrategy<RealVector, f64, _, _> = ESBuilder::mu_comma_lambda(10, 70)
1126 .unwrap()
1127 .initial_sigma(1.0)
1128 .self_adaptive(true)
1129 .bounds(bounds)
1130 .fitness(Sphere::new(10))
1131 .termination(MaxEvaluations::new(3000))
1132 .build()
1133 .unwrap();
1134
1135 let result = es.run(&mut rng).unwrap();
1136
1137 assert!(
1139 result.best_fitness > -100.0,
1140 "Expected fitness > -100, got {}",
1141 result.best_fitness
1142 );
1143 }
1144
1145 #[test]
1146 fn test_mu_comma_lambda_constraint() {
1147 let result = ESConfig::mu_comma_lambda(50, 30);
1149 assert!(result.is_err());
1150 }
1151
1152 #[test]
1153 fn test_recombination_types() {
1154 let mut rng = rand::thread_rng();
1155 let bounds = MultiBounds::symmetric(5.12, 5);
1156
1157 let recomb_types = vec![
1158 RecombinationType::None,
1159 RecombinationType::Discrete,
1160 RecombinationType::Intermediate,
1161 RecombinationType::GlobalIntermediate,
1162 ];
1163
1164 for recomb in recomb_types {
1165 let es: EvolutionStrategy<RealVector, f64, _, _> = ESBuilder::new()
1166 .mu(10)
1167 .lambda(50)
1168 .recombination(recomb)
1169 .bounds(bounds.clone())
1170 .fitness(Sphere::new(5))
1171 .termination(MaxEvaluations::new(500))
1172 .build()
1173 .unwrap();
1174
1175 let result = es.run(&mut rng);
1176 assert!(result.is_ok());
1177 }
1178 }
1179
1180 #[test]
1181 fn test_es_self_adaptive_disabled() {
1182 let mut rng = rand::thread_rng();
1183 let bounds = MultiBounds::symmetric(5.12, 5);
1184
1185 let es: EvolutionStrategy<RealVector, f64, _, _> = ESBuilder::new()
1186 .mu(10)
1187 .lambda(50)
1188 .self_adaptive(false)
1189 .initial_sigma(0.5)
1190 .bounds(bounds)
1191 .fitness(Sphere::new(5))
1192 .termination(MaxEvaluations::new(500))
1193 .build()
1194 .unwrap();
1195
1196 let result = es.run(&mut rng);
1197 assert!(result.is_ok());
1198 }
1199
1200 #[test]
1203 fn test_es_run_with_callback_reports_progress() {
1204 let mut rng = rand::rngs::StdRng::seed_from_u64(11);
1205 let bounds = MultiBounds::symmetric(5.12, 4);
1206 let es: EvolutionStrategy<RealVector, f64, _, _> = ESBuilder::new()
1207 .mu(6)
1208 .lambda(24)
1209 .initial_sigma(0.5)
1210 .bounds(bounds)
1211 .fitness(Sphere::new(4))
1212 .max_generations(15)
1213 .build()
1214 .unwrap();
1215
1216 let mut seen: Vec<usize> = Vec::new();
1217 let result = es
1218 .run_with_callback(&mut rng, |generation, best| {
1219 assert!(best.is_finite());
1220 seen.push(generation);
1221 true
1222 })
1223 .unwrap();
1224
1225 assert_eq!(seen, (0..result.generations).collect::<Vec<_>>());
1227 assert!(!seen.is_empty());
1228 }
1229
1230 #[test]
1231 fn test_es_run_with_callback_cancels_early() {
1232 let mut rng = rand::rngs::StdRng::seed_from_u64(12);
1233 let bounds = MultiBounds::symmetric(5.12, 4);
1234 let es: EvolutionStrategy<RealVector, f64, _, _> = ESBuilder::new()
1236 .mu(6)
1237 .lambda(24)
1238 .initial_sigma(0.5)
1239 .bounds(bounds)
1240 .fitness(Sphere::new(4))
1241 .max_generations(10_000)
1242 .build()
1243 .unwrap();
1244
1245 let mut calls = 0usize;
1246 let result = es
1247 .run_with_callback(&mut rng, |_generation, _best| {
1248 calls += 1;
1249 calls < 5
1251 })
1252 .unwrap();
1253
1254 assert!(calls <= 5, "callback should stop being called after cancel");
1257 assert!(
1258 result.generations < 10,
1259 "run must stop far short of the 10k budget, got {}",
1260 result.generations
1261 );
1262 }
1263
1264 #[test]
1269 fn test_default_selection_is_comma() {
1270 let config = ESConfig::default();
1271 assert!(config.self_adaptive);
1272 assert!(
1273 matches!(config.selection, ESSelectionStrategy::MuCommaLambda),
1274 "self-adaptive default must use (μ,λ) comma selection"
1275 );
1276 assert!(config.lambda >= config.mu);
1278 }
1279
1280 #[test]
1284 fn test_resolved_min_sigma() {
1285 let mut config = ESConfig {
1286 initial_sigma: 2.0,
1287 ..Default::default()
1288 };
1289 assert!((config.resolved_min_sigma() - 2e-8).abs() < 1e-18);
1290
1291 config.min_sigma = Some(0.01);
1292 assert!((config.resolved_min_sigma() - 0.01).abs() < 1e-18);
1293 }
1294
1295 #[test]
1298 fn test_min_sigma_builder_threads_through() {
1299 let bounds = MultiBounds::symmetric(5.12, 5);
1300 let es: EvolutionStrategy<RealVector, f64, _, _> = ESBuilder::new()
1301 .mu(5)
1302 .lambda(35)
1303 .self_adaptive(true)
1304 .initial_sigma(1.0)
1305 .min_sigma(0.05)
1306 .bounds(bounds)
1307 .fitness(Sphere::new(5))
1308 .termination(MaxEvaluations::new(700))
1309 .build()
1310 .unwrap();
1311 let mut rng = rand::thread_rng();
1314 assert!(es.run(&mut rng).is_ok());
1315 }
1316
1317 #[test]
1318 fn test_es_bounds_respected() {
1319 let mut rng = rand::thread_rng();
1320 let bounds = MultiBounds::symmetric(2.0, 5);
1321
1322 let es: EvolutionStrategy<RealVector, f64, _, _> = ESBuilder::new()
1323 .mu(10)
1324 .lambda(50)
1325 .initial_sigma(5.0) .bounds(bounds.clone())
1327 .fitness(Sphere::new(5))
1328 .termination(MaxEvaluations::new(500))
1329 .build()
1330 .unwrap();
1331
1332 let result = es.run(&mut rng).unwrap();
1333
1334 for gene in result.best_genome.genes() {
1336 assert!(
1337 *gene >= -2.0 && *gene <= 2.0,
1338 "Gene {} outside bounds [-2.0, 2.0]",
1339 gene
1340 );
1341 }
1342 }
1343}