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)]
26pub enum ESSelectionStrategy {
27 #[default]
29 MuPlusLambda,
30 MuCommaLambda,
32}
33
34#[derive(Clone, Debug)]
36pub struct ESConfig {
37 pub mu: usize,
39 pub lambda: usize,
41 pub selection: ESSelectionStrategy,
43 pub initial_sigma: f64,
45 pub self_adaptive: bool,
47 pub recombination: RecombinationType,
49}
50
51impl Default for ESConfig {
52 fn default() -> Self {
53 Self {
54 mu: 15,
55 lambda: 100,
56 selection: ESSelectionStrategy::MuPlusLambda,
57 initial_sigma: 1.0,
58 self_adaptive: true,
59 recombination: RecombinationType::Intermediate,
60 }
61 }
62}
63
64impl ESConfig {
65 pub fn mu_plus_lambda(mu: usize, lambda: usize) -> Self {
67 Self {
68 mu,
69 lambda,
70 selection: ESSelectionStrategy::MuPlusLambda,
71 ..Default::default()
72 }
73 }
74
75 pub fn mu_comma_lambda(mu: usize, lambda: usize) -> Result<Self, EvolutionError> {
77 if lambda < mu {
78 return Err(EvolutionError::Configuration(format!(
79 "For (μ,λ)-ES, λ ({}) must be >= μ ({})",
80 lambda, mu
81 )));
82 }
83 Ok(Self {
84 mu,
85 lambda,
86 selection: ESSelectionStrategy::MuCommaLambda,
87 ..Default::default()
88 })
89 }
90}
91
92#[derive(Clone, Debug, Default)]
94pub enum RecombinationType {
95 None,
97 Discrete,
99 #[default]
101 Intermediate,
102 GlobalIntermediate,
104}
105
106pub struct ESBuilder<G, F, Fit, Term>
108where
109 G: EvolutionaryGenome,
110 F: FitnessValue,
111{
112 config: ESConfig,
113 bounds: Option<MultiBounds>,
114 fitness: Option<Fit>,
115 termination: Option<Term>,
116 _phantom: std::marker::PhantomData<(G, F)>,
117}
118
119impl<G, F> ESBuilder<G, F, (), ()>
120where
121 G: EvolutionaryGenome,
122 F: FitnessValue,
123{
124 pub fn new() -> Self {
126 Self {
127 config: ESConfig::default(),
128 bounds: None,
129 fitness: None,
130 termination: None,
131 _phantom: std::marker::PhantomData,
132 }
133 }
134
135 pub fn mu_plus_lambda(mu: usize, lambda: usize) -> Self {
137 Self {
138 config: ESConfig::mu_plus_lambda(mu, lambda),
139 bounds: None,
140 fitness: None,
141 termination: None,
142 _phantom: std::marker::PhantomData,
143 }
144 }
145
146 pub fn mu_comma_lambda(mu: usize, lambda: usize) -> Result<Self, EvolutionError> {
148 Ok(Self {
149 config: ESConfig::mu_comma_lambda(mu, lambda)?,
150 bounds: None,
151 fitness: None,
152 termination: None,
153 _phantom: std::marker::PhantomData,
154 })
155 }
156}
157
158impl<G, F> Default for ESBuilder<G, F, (), ()>
159where
160 G: EvolutionaryGenome,
161 F: FitnessValue,
162{
163 fn default() -> Self {
164 Self::new()
165 }
166}
167
168impl<G, F, Fit, Term> ESBuilder<G, F, Fit, Term>
169where
170 G: EvolutionaryGenome,
171 F: FitnessValue,
172{
173 pub fn mu(mut self, mu: usize) -> Self {
175 self.config.mu = mu;
176 self
177 }
178
179 pub fn lambda(mut self, lambda: usize) -> Self {
181 self.config.lambda = lambda;
182 self
183 }
184
185 pub fn selection_strategy(mut self, strategy: ESSelectionStrategy) -> Self {
187 self.config.selection = strategy;
188 self
189 }
190
191 pub fn initial_sigma(mut self, sigma: f64) -> Self {
193 self.config.initial_sigma = sigma;
194 self
195 }
196
197 pub fn self_adaptive(mut self, enabled: bool) -> Self {
199 self.config.self_adaptive = enabled;
200 self
201 }
202
203 pub fn recombination(mut self, recomb: RecombinationType) -> Self {
205 self.config.recombination = recomb;
206 self
207 }
208
209 pub fn bounds(mut self, bounds: MultiBounds) -> Self {
211 self.bounds = Some(bounds);
212 self
213 }
214
215 pub fn fitness<NewFit>(self, fitness: NewFit) -> ESBuilder<G, F, NewFit, Term>
217 where
218 NewFit: Fitness<Genome = G, Value = F>,
219 {
220 ESBuilder {
221 config: self.config,
222 bounds: self.bounds,
223 fitness: Some(fitness),
224 termination: self.termination,
225 _phantom: std::marker::PhantomData,
226 }
227 }
228
229 pub fn termination<NewTerm>(self, termination: NewTerm) -> ESBuilder<G, F, Fit, NewTerm>
231 where
232 NewTerm: TerminationCriterion<G, F>,
233 {
234 ESBuilder {
235 config: self.config,
236 bounds: self.bounds,
237 fitness: self.fitness,
238 termination: Some(termination),
239 _phantom: std::marker::PhantomData,
240 }
241 }
242
243 pub fn max_generations(self, max: usize) -> ESBuilder<G, F, Fit, MaxGenerations> {
245 ESBuilder {
246 config: self.config,
247 bounds: self.bounds,
248 fitness: self.fitness,
249 termination: Some(MaxGenerations::new(max)),
250 _phantom: std::marker::PhantomData,
251 }
252 }
253}
254
255#[cfg(feature = "parallel")]
257impl<G, F, Fit, Term> ESBuilder<G, F, Fit, Term>
258where
259 G: EvolutionaryGenome + RealValuedGenome + Send + Sync,
260 F: FitnessValue + Send,
261 Fit: Fitness<Genome = G, Value = F> + Sync,
262 Term: TerminationCriterion<G, F>,
263{
264 pub fn build(self) -> Result<EvolutionStrategy<G, F, Fit, Term>, EvolutionError> {
266 let bounds = self
267 .bounds
268 .ok_or_else(|| EvolutionError::Configuration("Bounds must be specified".to_string()))?;
269
270 let fitness = self.fitness.ok_or_else(|| {
271 EvolutionError::Configuration("Fitness function must be specified".to_string())
272 })?;
273
274 let termination = self.termination.ok_or_else(|| {
275 EvolutionError::Configuration("Termination criterion must be specified".to_string())
276 })?;
277
278 if matches!(self.config.selection, ESSelectionStrategy::MuCommaLambda)
280 && self.config.lambda < self.config.mu
281 {
282 return Err(EvolutionError::Configuration(format!(
283 "For (μ,λ)-ES, λ ({}) must be >= μ ({})",
284 self.config.lambda, self.config.mu
285 )));
286 }
287
288 Ok(EvolutionStrategy {
289 config: self.config,
290 bounds,
291 fitness,
292 termination,
293 _phantom: std::marker::PhantomData,
294 })
295 }
296}
297
298#[cfg(not(feature = "parallel"))]
300impl<G, F, Fit, Term> ESBuilder<G, F, Fit, Term>
301where
302 G: EvolutionaryGenome + RealValuedGenome,
303 F: FitnessValue,
304 Fit: Fitness<Genome = G, Value = F>,
305 Term: TerminationCriterion<G, F>,
306{
307 pub fn build(self) -> Result<EvolutionStrategy<G, F, Fit, Term>, EvolutionError> {
309 let bounds = self
310 .bounds
311 .ok_or_else(|| EvolutionError::Configuration("Bounds must be specified".to_string()))?;
312
313 let fitness = self.fitness.ok_or_else(|| {
314 EvolutionError::Configuration("Fitness function must be specified".to_string())
315 })?;
316
317 let termination = self.termination.ok_or_else(|| {
318 EvolutionError::Configuration("Termination criterion must be specified".to_string())
319 })?;
320
321 if matches!(self.config.selection, ESSelectionStrategy::MuCommaLambda)
323 && self.config.lambda < self.config.mu
324 {
325 return Err(EvolutionError::Configuration(format!(
326 "For (μ,λ)-ES, λ ({}) must be >= μ ({})",
327 self.config.lambda, self.config.mu
328 )));
329 }
330
331 Ok(EvolutionStrategy {
332 config: self.config,
333 bounds,
334 fitness,
335 termination,
336 _phantom: std::marker::PhantomData,
337 })
338 }
339}
340
341pub struct EvolutionStrategy<G, F, Fit, Term>
346where
347 G: EvolutionaryGenome,
348 F: FitnessValue,
349{
350 config: ESConfig,
351 bounds: MultiBounds,
352 fitness: Fit,
353 termination: Term,
354 _phantom: std::marker::PhantomData<(G, F)>,
355}
356
357#[cfg(feature = "parallel")]
359impl<G, F, Fit, Term> EvolutionStrategy<G, F, Fit, Term>
360where
361 G: EvolutionaryGenome + RealValuedGenome + Send + Sync,
362 F: FitnessValue + Send,
363 Fit: Fitness<Genome = G, Value = F> + Sync,
364 Term: TerminationCriterion<G, F>,
365{
366 pub fn builder() -> ESBuilder<G, F, (), ()> {
368 ESBuilder::new()
369 }
370
371 pub fn run<R: Rng>(&self, rng: &mut R) -> Result<EvolutionResult<G, F>, EvolutionError> {
373 let start_time = Instant::now();
374
375 let mut population: Vec<(AdaptiveGenome<G>, F)> = (0..self.config.mu)
377 .map(|_| {
378 let genome = G::generate(rng, &self.bounds);
379 let adaptive = if self.config.self_adaptive {
380 AdaptiveGenome::new_non_isotropic(
381 genome,
382 vec![self.config.initial_sigma; self.bounds.dimension()],
383 )
384 } else {
385 AdaptiveGenome::new_isotropic(genome, self.config.initial_sigma)
386 };
387 let fitness = self.fitness.evaluate(adaptive.inner());
388 (adaptive, fitness)
389 })
390 .collect();
391
392 population.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
394
395 let mut stats = EvolutionStats::new();
396 let mut evaluations = self.config.mu;
397 let mut fitness_history: Vec<f64> = Vec::new();
398 let mut generation = 0usize;
399
400 let mut best = population[0].clone();
402
403 let mut tracking_population: Population<G, F> = Population::with_capacity(self.config.mu);
405 for (adaptive, fit) in &population {
406 let mut ind = Individual::new(adaptive.inner().clone());
407 ind.set_fitness(fit.clone());
408 tracking_population.push(ind);
409 }
410
411 let gen_stats = GenerationStats::from_population(&tracking_population, 0, evaluations);
413 fitness_history.push(gen_stats.best_fitness);
414 stats.record(gen_stats);
415
416 loop {
418 let state = EvolutionState {
420 generation,
421 evaluations,
422 best_fitness: best.1.to_f64(),
423 population: &tracking_population,
424 fitness_history: &fitness_history,
425 };
426
427 if self.termination.should_terminate(&state) {
428 stats.set_termination_reason(self.termination.reason());
429 break;
430 }
431
432 let gen_start = Instant::now();
433
434 let mut offspring: Vec<(AdaptiveGenome<G>, F)> = Vec::with_capacity(self.config.lambda);
436
437 for _ in 0..self.config.lambda {
438 let child = match &self.config.recombination {
440 RecombinationType::None => {
441 let parent_idx = rng.gen_range(0..self.config.mu);
443 population[parent_idx].0.clone()
444 }
445 RecombinationType::Discrete => {
446 let p1_idx = rng.gen_range(0..self.config.mu);
448 let p2_idx = rng.gen_range(0..self.config.mu);
449 self.discrete_recombination(
450 &population[p1_idx].0,
451 &population[p2_idx].0,
452 rng,
453 )
454 }
455 RecombinationType::Intermediate => {
456 let p1_idx = rng.gen_range(0..self.config.mu);
458 let p2_idx = rng.gen_range(0..self.config.mu);
459 self.intermediate_recombination(
460 &population[p1_idx].0,
461 &population[p2_idx].0,
462 )
463 }
464 RecombinationType::GlobalIntermediate => {
465 self.global_intermediate_recombination(&population)
467 }
468 };
469
470 let mutated = self.mutate(child, rng);
472
473 let fitness = self.fitness.evaluate(mutated.inner());
475 offspring.push((mutated, fitness));
476 }
477 evaluations += self.config.lambda;
478
479 match self.config.selection {
481 ESSelectionStrategy::MuPlusLambda => {
482 let mut combined = population;
484 combined.extend(offspring);
485 combined
486 .sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
487 population = combined.into_iter().take(self.config.mu).collect();
488 }
489 ESSelectionStrategy::MuCommaLambda => {
490 offspring
492 .sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
493 population = offspring.into_iter().take(self.config.mu).collect();
494 }
495 }
496
497 if population[0].1.is_better_than(&best.1) {
499 best = population[0].clone();
500 }
501
502 generation += 1;
503
504 tracking_population.clear();
506 for (adaptive, fit) in &population {
507 let mut ind = Individual::new(adaptive.inner().clone());
508 ind.set_fitness(fit.clone());
509 tracking_population.push(ind);
510 }
511 tracking_population.set_generation(generation);
512
513 let timing = TimingStats::new().with_total(gen_start.elapsed());
515 let gen_stats =
516 GenerationStats::from_population(&tracking_population, generation, evaluations)
517 .with_timing(timing);
518 fitness_history.push(gen_stats.best_fitness);
519 stats.record(gen_stats);
520 }
521
522 stats.set_runtime(start_time.elapsed());
523
524 Ok(
525 EvolutionResult::new(best.0.into_inner(), best.1, generation, evaluations)
526 .with_stats(stats),
527 )
528 }
529
530 fn discrete_recombination<R: Rng>(
532 &self,
533 p1: &AdaptiveGenome<G>,
534 p2: &AdaptiveGenome<G>,
535 rng: &mut R,
536 ) -> AdaptiveGenome<G> {
537 let genes1 = p1.inner().genes();
538 let genes2 = p2.inner().genes();
539
540 let child_genes: Vec<f64> = genes1
541 .iter()
542 .zip(genes2.iter())
543 .map(|(g1, g2)| if rng.gen_bool(0.5) { *g1 } else { *g2 })
544 .collect();
545
546 let child_genome = G::from_genes(child_genes).expect("Failed to create genome from genes");
547
548 match (&p1.strategy, &p2.strategy) {
550 (StrategyParams::Isotropic(s1), StrategyParams::Isotropic(s2)) => {
551 AdaptiveGenome::new_isotropic(child_genome, (s1 * s2).sqrt())
552 }
553 (StrategyParams::NonIsotropic(s1), StrategyParams::NonIsotropic(s2)) => {
554 let sigmas: Vec<f64> = s1
555 .iter()
556 .zip(s2.iter())
557 .map(|(a, b)| if rng.gen_bool(0.5) { *a } else { *b })
558 .collect();
559 AdaptiveGenome::new_non_isotropic(child_genome, sigmas)
560 }
561 _ => AdaptiveGenome::new_isotropic(child_genome, self.config.initial_sigma),
562 }
563 }
564
565 fn intermediate_recombination(
567 &self,
568 p1: &AdaptiveGenome<G>,
569 p2: &AdaptiveGenome<G>,
570 ) -> AdaptiveGenome<G> {
571 let genes1 = p1.inner().genes();
572 let genes2 = p2.inner().genes();
573
574 let child_genes: Vec<f64> = genes1
575 .iter()
576 .zip(genes2.iter())
577 .map(|(g1, g2)| (g1 + g2) / 2.0)
578 .collect();
579
580 let child_genome = G::from_genes(child_genes).expect("Failed to create genome from genes");
581
582 match (&p1.strategy, &p2.strategy) {
584 (StrategyParams::Isotropic(s1), StrategyParams::Isotropic(s2)) => {
585 AdaptiveGenome::new_isotropic(child_genome, (s1 * s2).sqrt())
586 }
587 (StrategyParams::NonIsotropic(s1), StrategyParams::NonIsotropic(s2)) => {
588 let sigmas: Vec<f64> = s1
589 .iter()
590 .zip(s2.iter())
591 .map(|(a, b)| (a * b).sqrt())
592 .collect();
593 AdaptiveGenome::new_non_isotropic(child_genome, sigmas)
594 }
595 _ => AdaptiveGenome::new_isotropic(child_genome, self.config.initial_sigma),
596 }
597 }
598
599 fn global_intermediate_recombination(
601 &self,
602 population: &[(AdaptiveGenome<G>, F)],
603 ) -> AdaptiveGenome<G> {
604 let n = population.len();
605 let dim = population[0].0.inner().genes().len();
606
607 let mut child_genes = vec![0.0; dim];
608 for (adaptive, _) in population {
609 for (i, gene) in adaptive.inner().genes().iter().enumerate() {
610 child_genes[i] += gene / n as f64;
611 }
612 }
613
614 let child_genome = G::from_genes(child_genes).expect("Failed to create genome from genes");
615
616 let avg_sigma = if self.config.self_adaptive {
618 let sigmas: Vec<f64> = (0..dim)
620 .map(|i| {
621 let sum: f64 = population
622 .iter()
623 .map(|(a, _)| a.strategy.get_sigma(i))
624 .sum();
625 sum / n as f64
626 })
627 .collect();
628 AdaptiveGenome::new_non_isotropic(child_genome, sigmas)
629 } else {
630 AdaptiveGenome::new_isotropic(child_genome, self.config.initial_sigma)
631 };
632
633 avg_sigma
634 }
635
636 fn mutate<R: Rng>(&self, mut genome: AdaptiveGenome<G>, rng: &mut R) -> AdaptiveGenome<G> {
638 let n = genome.inner().genes().len();
639
640 if self.config.self_adaptive {
642 genome.strategy.mutate(n, rng);
643 }
644
645 let sigmas: Vec<f64> = (0..n).map(|i| genome.strategy.get_sigma(i)).collect();
647
648 let genes = genome.inner_mut().genes_mut();
650 for i in 0..genes.len() {
651 let perturbation: f64 = rng.sample(StandardNormal);
652 genes[i] += sigmas[i] * perturbation;
653
654 if let Some(b) = self.bounds.get(i) {
656 genes[i] = genes[i].clamp(b.min, b.max);
657 }
658 }
659
660 genome
661 }
662}
663
664#[cfg(not(feature = "parallel"))]
666impl<G, F, Fit, Term> EvolutionStrategy<G, F, Fit, Term>
667where
668 G: EvolutionaryGenome + RealValuedGenome,
669 F: FitnessValue,
670 Fit: Fitness<Genome = G, Value = F>,
671 Term: TerminationCriterion<G, F>,
672{
673 pub fn builder() -> ESBuilder<G, F, (), ()> {
675 ESBuilder::new()
676 }
677
678 pub fn run<R: Rng>(&self, rng: &mut R) -> Result<EvolutionResult<G, F>, EvolutionError> {
680 let start_time = Instant::now();
681
682 let mut population: Vec<(AdaptiveGenome<G>, F)> = (0..self.config.mu)
684 .map(|_| {
685 let genome = G::generate(rng, &self.bounds);
686 let adaptive = if self.config.self_adaptive {
687 AdaptiveGenome::new_non_isotropic(
688 genome,
689 vec![self.config.initial_sigma; self.bounds.dimension()],
690 )
691 } else {
692 AdaptiveGenome::new_isotropic(genome, self.config.initial_sigma)
693 };
694 let fitness = self.fitness.evaluate(adaptive.inner());
695 (adaptive, fitness)
696 })
697 .collect();
698
699 population.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
701
702 let mut stats = EvolutionStats::new();
703 let mut evaluations = self.config.mu;
704 let mut fitness_history: Vec<f64> = Vec::new();
705 let mut generation = 0usize;
706
707 let mut best = population[0].clone();
709
710 let mut tracking_population: Population<G, F> = Population::with_capacity(self.config.mu);
712 for (adaptive, fit) in &population {
713 let mut ind = Individual::new(adaptive.inner().clone());
714 ind.set_fitness(fit.clone());
715 tracking_population.push(ind);
716 }
717
718 let gen_stats = GenerationStats::from_population(&tracking_population, 0, evaluations);
720 fitness_history.push(gen_stats.best_fitness);
721 stats.record(gen_stats);
722
723 loop {
725 let state = EvolutionState {
727 generation,
728 evaluations,
729 best_fitness: best.1.to_f64(),
730 population: &tracking_population,
731 fitness_history: &fitness_history,
732 };
733
734 if self.termination.should_terminate(&state) {
735 stats.set_termination_reason(self.termination.reason());
736 break;
737 }
738
739 let gen_start = Instant::now();
740
741 let mut offspring: Vec<(AdaptiveGenome<G>, F)> = Vec::with_capacity(self.config.lambda);
743
744 for _ in 0..self.config.lambda {
745 let child = match &self.config.recombination {
747 RecombinationType::None => {
748 let parent_idx = rng.gen_range(0..self.config.mu);
750 population[parent_idx].0.clone()
751 }
752 RecombinationType::Discrete => {
753 let p1_idx = rng.gen_range(0..self.config.mu);
755 let p2_idx = rng.gen_range(0..self.config.mu);
756 self.discrete_recombination(
757 &population[p1_idx].0,
758 &population[p2_idx].0,
759 rng,
760 )
761 }
762 RecombinationType::Intermediate => {
763 let p1_idx = rng.gen_range(0..self.config.mu);
765 let p2_idx = rng.gen_range(0..self.config.mu);
766 self.intermediate_recombination(
767 &population[p1_idx].0,
768 &population[p2_idx].0,
769 )
770 }
771 RecombinationType::GlobalIntermediate => {
772 self.global_intermediate_recombination(&population)
774 }
775 };
776
777 let mutated = self.mutate(child, rng);
779
780 let fitness = self.fitness.evaluate(mutated.inner());
782 offspring.push((mutated, fitness));
783 }
784 evaluations += self.config.lambda;
785
786 match self.config.selection {
788 ESSelectionStrategy::MuPlusLambda => {
789 let mut combined = population;
791 combined.extend(offspring);
792 combined
793 .sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
794 population = combined.into_iter().take(self.config.mu).collect();
795 }
796 ESSelectionStrategy::MuCommaLambda => {
797 offspring
799 .sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
800 population = offspring.into_iter().take(self.config.mu).collect();
801 }
802 }
803
804 if population[0].1.is_better_than(&best.1) {
806 best = population[0].clone();
807 }
808
809 generation += 1;
810
811 tracking_population.clear();
813 for (adaptive, fit) in &population {
814 let mut ind = Individual::new(adaptive.inner().clone());
815 ind.set_fitness(fit.clone());
816 tracking_population.push(ind);
817 }
818 tracking_population.set_generation(generation);
819
820 let timing = TimingStats::new().with_total(gen_start.elapsed());
822 let gen_stats =
823 GenerationStats::from_population(&tracking_population, generation, evaluations)
824 .with_timing(timing);
825 fitness_history.push(gen_stats.best_fitness);
826 stats.record(gen_stats);
827 }
828
829 stats.set_runtime(start_time.elapsed());
830
831 Ok(
832 EvolutionResult::new(best.0.into_inner(), best.1, generation, evaluations)
833 .with_stats(stats),
834 )
835 }
836
837 fn discrete_recombination<R: Rng>(
839 &self,
840 p1: &AdaptiveGenome<G>,
841 p2: &AdaptiveGenome<G>,
842 rng: &mut R,
843 ) -> AdaptiveGenome<G> {
844 let genes1 = p1.inner().genes();
845 let genes2 = p2.inner().genes();
846
847 let child_genes: Vec<f64> = genes1
848 .iter()
849 .zip(genes2.iter())
850 .map(|(g1, g2)| if rng.gen_bool(0.5) { *g1 } else { *g2 })
851 .collect();
852
853 let child_genome = G::from_genes(child_genes).expect("Failed to create genome from genes");
854
855 match (&p1.strategy, &p2.strategy) {
857 (StrategyParams::Isotropic(s1), StrategyParams::Isotropic(s2)) => {
858 AdaptiveGenome::new_isotropic(child_genome, (s1 * s2).sqrt())
859 }
860 (StrategyParams::NonIsotropic(s1), StrategyParams::NonIsotropic(s2)) => {
861 let sigmas: Vec<f64> = s1
862 .iter()
863 .zip(s2.iter())
864 .map(|(a, b)| if rng.gen_bool(0.5) { *a } else { *b })
865 .collect();
866 AdaptiveGenome::new_non_isotropic(child_genome, sigmas)
867 }
868 _ => AdaptiveGenome::new_isotropic(child_genome, self.config.initial_sigma),
869 }
870 }
871
872 fn intermediate_recombination(
874 &self,
875 p1: &AdaptiveGenome<G>,
876 p2: &AdaptiveGenome<G>,
877 ) -> AdaptiveGenome<G> {
878 let genes1 = p1.inner().genes();
879 let genes2 = p2.inner().genes();
880
881 let child_genes: Vec<f64> = genes1
882 .iter()
883 .zip(genes2.iter())
884 .map(|(g1, g2)| (g1 + g2) / 2.0)
885 .collect();
886
887 let child_genome = G::from_genes(child_genes).expect("Failed to create genome from genes");
888
889 match (&p1.strategy, &p2.strategy) {
891 (StrategyParams::Isotropic(s1), StrategyParams::Isotropic(s2)) => {
892 AdaptiveGenome::new_isotropic(child_genome, (s1 * s2).sqrt())
893 }
894 (StrategyParams::NonIsotropic(s1), StrategyParams::NonIsotropic(s2)) => {
895 let sigmas: Vec<f64> = s1
896 .iter()
897 .zip(s2.iter())
898 .map(|(a, b)| (a * b).sqrt())
899 .collect();
900 AdaptiveGenome::new_non_isotropic(child_genome, sigmas)
901 }
902 _ => AdaptiveGenome::new_isotropic(child_genome, self.config.initial_sigma),
903 }
904 }
905
906 fn global_intermediate_recombination(
908 &self,
909 population: &[(AdaptiveGenome<G>, F)],
910 ) -> AdaptiveGenome<G> {
911 let n = population.len();
912 let dim = population[0].0.inner().genes().len();
913
914 let mut child_genes = vec![0.0; dim];
915 for (adaptive, _) in population {
916 for (i, gene) in adaptive.inner().genes().iter().enumerate() {
917 child_genes[i] += gene / n as f64;
918 }
919 }
920
921 let child_genome = G::from_genes(child_genes).expect("Failed to create genome from genes");
922
923 let avg_sigma = if self.config.self_adaptive {
925 let sigmas: Vec<f64> = (0..dim)
927 .map(|i| {
928 let sum: f64 = population
929 .iter()
930 .map(|(a, _)| a.strategy.get_sigma(i))
931 .sum();
932 sum / n as f64
933 })
934 .collect();
935 AdaptiveGenome::new_non_isotropic(child_genome, sigmas)
936 } else {
937 AdaptiveGenome::new_isotropic(child_genome, self.config.initial_sigma)
938 };
939
940 avg_sigma
941 }
942
943 fn mutate<R: Rng>(&self, mut genome: AdaptiveGenome<G>, rng: &mut R) -> AdaptiveGenome<G> {
945 let n = genome.inner().genes().len();
946
947 if self.config.self_adaptive {
949 genome.strategy.mutate(n, rng);
950 }
951
952 let sigmas: Vec<f64> = (0..n).map(|i| genome.strategy.get_sigma(i)).collect();
954
955 let genes = genome.inner_mut().genes_mut();
957 for i in 0..genes.len() {
958 let perturbation: f64 = rng.sample(StandardNormal);
959 genes[i] += sigmas[i] * perturbation;
960
961 if let Some(b) = self.bounds.get(i) {
963 genes[i] = genes[i].clamp(b.min, b.max);
964 }
965 }
966
967 genome
968 }
969}
970
971pub type MuPlusLambdaES<G, F, Fit, Term> = EvolutionStrategy<G, F, Fit, Term>;
973
974pub type MuCommaLambdaES<G, F, Fit, Term> = EvolutionStrategy<G, F, Fit, Term>;
976
977#[cfg(test)]
978mod tests {
979 use super::*;
980 use crate::fitness::benchmarks::Sphere;
981 use crate::genome::real_vector::RealVector;
982 use crate::termination::MaxEvaluations;
983
984 #[test]
985 fn test_es_builder() {
986 let bounds = MultiBounds::symmetric(5.0, 10);
987 let es: Result<EvolutionStrategy<RealVector, f64, _, _>, _> = ESBuilder::new()
988 .mu(15)
989 .lambda(100)
990 .bounds(bounds)
991 .fitness(Sphere::new(10))
992 .max_generations(10)
993 .build();
994
995 assert!(es.is_ok());
996 }
997
998 #[test]
999 fn test_mu_plus_lambda_es() {
1000 let mut rng = rand::thread_rng();
1001 let bounds = MultiBounds::symmetric(5.12, 10);
1002
1003 let es: EvolutionStrategy<RealVector, f64, _, _> = ESBuilder::mu_plus_lambda(10, 70)
1004 .initial_sigma(1.0)
1005 .self_adaptive(true)
1006 .bounds(bounds)
1007 .fitness(Sphere::new(10))
1008 .termination(MaxEvaluations::new(3000))
1009 .build()
1010 .unwrap();
1011
1012 let result = es.run(&mut rng).unwrap();
1013
1014 assert!(
1016 result.best_fitness > -50.0,
1017 "Expected fitness > -50, got {}",
1018 result.best_fitness
1019 );
1020 }
1021
1022 #[test]
1023 fn test_mu_comma_lambda_es() {
1024 let mut rng = rand::thread_rng();
1025 let bounds = MultiBounds::symmetric(5.12, 10);
1026
1027 let es: EvolutionStrategy<RealVector, f64, _, _> = ESBuilder::mu_comma_lambda(10, 70)
1028 .unwrap()
1029 .initial_sigma(1.0)
1030 .self_adaptive(true)
1031 .bounds(bounds)
1032 .fitness(Sphere::new(10))
1033 .termination(MaxEvaluations::new(3000))
1034 .build()
1035 .unwrap();
1036
1037 let result = es.run(&mut rng).unwrap();
1038
1039 assert!(
1041 result.best_fitness > -100.0,
1042 "Expected fitness > -100, got {}",
1043 result.best_fitness
1044 );
1045 }
1046
1047 #[test]
1048 fn test_mu_comma_lambda_constraint() {
1049 let result = ESConfig::mu_comma_lambda(50, 30);
1051 assert!(result.is_err());
1052 }
1053
1054 #[test]
1055 fn test_recombination_types() {
1056 let mut rng = rand::thread_rng();
1057 let bounds = MultiBounds::symmetric(5.12, 5);
1058
1059 let recomb_types = vec![
1060 RecombinationType::None,
1061 RecombinationType::Discrete,
1062 RecombinationType::Intermediate,
1063 RecombinationType::GlobalIntermediate,
1064 ];
1065
1066 for recomb in recomb_types {
1067 let es: EvolutionStrategy<RealVector, f64, _, _> = ESBuilder::new()
1068 .mu(10)
1069 .lambda(50)
1070 .recombination(recomb)
1071 .bounds(bounds.clone())
1072 .fitness(Sphere::new(5))
1073 .termination(MaxEvaluations::new(500))
1074 .build()
1075 .unwrap();
1076
1077 let result = es.run(&mut rng);
1078 assert!(result.is_ok());
1079 }
1080 }
1081
1082 #[test]
1083 fn test_es_self_adaptive_disabled() {
1084 let mut rng = rand::thread_rng();
1085 let bounds = MultiBounds::symmetric(5.12, 5);
1086
1087 let es: EvolutionStrategy<RealVector, f64, _, _> = ESBuilder::new()
1088 .mu(10)
1089 .lambda(50)
1090 .self_adaptive(false)
1091 .initial_sigma(0.5)
1092 .bounds(bounds)
1093 .fitness(Sphere::new(5))
1094 .termination(MaxEvaluations::new(500))
1095 .build()
1096 .unwrap();
1097
1098 let result = es.run(&mut rng);
1099 assert!(result.is_ok());
1100 }
1101
1102 #[test]
1103 fn test_es_bounds_respected() {
1104 let mut rng = rand::thread_rng();
1105 let bounds = MultiBounds::symmetric(2.0, 5);
1106
1107 let es: EvolutionStrategy<RealVector, f64, _, _> = ESBuilder::new()
1108 .mu(10)
1109 .lambda(50)
1110 .initial_sigma(5.0) .bounds(bounds.clone())
1112 .fitness(Sphere::new(5))
1113 .termination(MaxEvaluations::new(500))
1114 .build()
1115 .unwrap();
1116
1117 let result = es.run(&mut rng).unwrap();
1118
1119 for gene in result.best_genome.genes() {
1121 assert!(
1122 *gene >= -2.0 && *gene <= 2.0,
1123 "Gene {} outside bounds [-2.0, 2.0]",
1124 gene
1125 );
1126 }
1127 }
1128}