Skip to main content

fugue_evo/algorithms/
evolution_strategy.rs

1//! Evolution Strategies: (μ+λ)-ES and (μ,λ)-ES
2//!
3//! This module implements the classic Evolution Strategy algorithms:
4//! - (μ+λ)-ES: Parents compete with offspring for survival
5//! - (μ,λ)-ES: Only offspring compete, parents are discarded
6//!
7//! Both support self-adaptive mutation through the `AdaptiveGenome` wrapper.
8
9use 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/// Selection strategy for Evolution Strategies
25#[derive(Clone, Debug, Default)]
26pub enum ESSelectionStrategy {
27    /// (μ+λ): Select best μ from parents + offspring combined
28    #[default]
29    MuPlusLambda,
30    /// (μ,λ): Select best μ from offspring only (requires λ ≥ μ)
31    MuCommaLambda,
32}
33
34/// Configuration for Evolution Strategies
35#[derive(Clone, Debug)]
36pub struct ESConfig {
37    /// Number of parents (μ)
38    pub mu: usize,
39    /// Number of offspring (λ)
40    pub lambda: usize,
41    /// Selection strategy
42    pub selection: ESSelectionStrategy,
43    /// Initial step size (σ)
44    pub initial_sigma: f64,
45    /// Use self-adaptive mutation (evolve σ)
46    pub self_adaptive: bool,
47    /// Recombination type
48    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    /// Create a (μ+λ)-ES configuration
66    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    /// Create a (μ,λ)-ES configuration
76    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/// Recombination type for ES
93#[derive(Clone, Debug, Default)]
94pub enum RecombinationType {
95    /// No recombination (asexual)
96    None,
97    /// Discrete recombination (randomly select genes from parents)
98    Discrete,
99    /// Intermediate recombination (average of parents)
100    #[default]
101    Intermediate,
102    /// Global intermediate (average of all parents)
103    GlobalIntermediate,
104}
105
106/// Builder for Evolution Strategy
107pub 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    /// Create a new builder with default configuration
125    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    /// Create a (μ+λ)-ES builder
136    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    /// Create a (μ,λ)-ES builder
147    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    /// Set μ (number of parents)
174    pub fn mu(mut self, mu: usize) -> Self {
175        self.config.mu = mu;
176        self
177    }
178
179    /// Set λ (number of offspring)
180    pub fn lambda(mut self, lambda: usize) -> Self {
181        self.config.lambda = lambda;
182        self
183    }
184
185    /// Set the selection strategy
186    pub fn selection_strategy(mut self, strategy: ESSelectionStrategy) -> Self {
187        self.config.selection = strategy;
188        self
189    }
190
191    /// Set the initial step size
192    pub fn initial_sigma(mut self, sigma: f64) -> Self {
193        self.config.initial_sigma = sigma;
194        self
195    }
196
197    /// Enable or disable self-adaptive mutation
198    pub fn self_adaptive(mut self, enabled: bool) -> Self {
199        self.config.self_adaptive = enabled;
200        self
201    }
202
203    /// Set the recombination type
204    pub fn recombination(mut self, recomb: RecombinationType) -> Self {
205        self.config.recombination = recomb;
206        self
207    }
208
209    /// Set the search space bounds
210    pub fn bounds(mut self, bounds: MultiBounds) -> Self {
211        self.bounds = Some(bounds);
212        self
213    }
214
215    /// Set the fitness function
216    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    /// Set the termination criterion
230    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    /// Set max generations (convenience method)
244    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// Parallel version with Send + Sync bounds
256#[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    /// Build the Evolution Strategy instance
265    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        // Validate (μ,λ) constraint
279        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// Non-parallel version of build()
299#[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    /// Build the Evolution Strategy instance
308    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        // Validate (μ,λ) constraint
322        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
341/// Evolution Strategy (μ+λ)-ES or (μ,λ)-ES
342///
343/// A classic evolutionary algorithm using Gaussian mutation and optional
344/// self-adaptive step size control.
345pub 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// Parallel version with Send + Sync bounds
358#[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    /// Create a builder for Evolution Strategy
367    pub fn builder() -> ESBuilder<G, F, (), ()> {
368        ESBuilder::new()
369    }
370
371    /// Run the evolution strategy
372    pub fn run<R: Rng>(&self, rng: &mut R) -> Result<EvolutionResult<G, F>, EvolutionError> {
373        let start_time = Instant::now();
374
375        // Initialize population with adaptive genomes
376        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        // Sort by fitness (descending for maximization)
393        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        // Track best individual
401        let mut best = population[0].clone();
402
403        // Create a tracking population for termination checks
404        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        // Record initial statistics
412        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        // Main evolution loop
417        loop {
418            // Check termination
419            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            // Generate offspring
435            let mut offspring: Vec<(AdaptiveGenome<G>, F)> = Vec::with_capacity(self.config.lambda);
436
437            for _ in 0..self.config.lambda {
438                // Select parent(s) for recombination
439                let child = match &self.config.recombination {
440                    RecombinationType::None => {
441                        // Clone a random parent
442                        let parent_idx = rng.gen_range(0..self.config.mu);
443                        population[parent_idx].0.clone()
444                    }
445                    RecombinationType::Discrete => {
446                        // Select two parents, randomly pick genes
447                        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                        // Average of two parents
457                        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                        // Average of all parents
466                        self.global_intermediate_recombination(&population)
467                    }
468                };
469
470                // Mutate
471                let mutated = self.mutate(child, rng);
472
473                // Evaluate
474                let fitness = self.fitness.evaluate(mutated.inner());
475                offspring.push((mutated, fitness));
476            }
477            evaluations += self.config.lambda;
478
479            // Selection
480            match self.config.selection {
481                ESSelectionStrategy::MuPlusLambda => {
482                    // Combine parents and offspring
483                    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                    // Select only from offspring
491                    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            // Update best
498            if population[0].1.is_better_than(&best.1) {
499                best = population[0].clone();
500            }
501
502            generation += 1;
503
504            // Update tracking population for statistics
505            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            // Record statistics
514            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    /// Discrete recombination: randomly select genes from parents
531    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        // Recombine strategy parameters
549        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    /// Intermediate recombination: average of two parents
566    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        // Average strategy parameters
583        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    /// Global intermediate recombination: average of all parents
600    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        // Average all strategy parameters
617        let avg_sigma = if self.config.self_adaptive {
618            // Average the sigmas from all parents
619            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    /// Mutate an adaptive genome
637    fn mutate<R: Rng>(&self, mut genome: AdaptiveGenome<G>, rng: &mut R) -> AdaptiveGenome<G> {
638        let n = genome.inner().genes().len();
639
640        // Self-adaptive: mutate strategy parameters first
641        if self.config.self_adaptive {
642            genome.strategy.mutate(n, rng);
643        }
644
645        // Collect sigmas first to avoid borrow conflicts
646        let sigmas: Vec<f64> = (0..n).map(|i| genome.strategy.get_sigma(i)).collect();
647
648        // Then mutate the genome using the (possibly updated) strategy
649        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            // Clamp to bounds
655            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// Non-parallel version without Send + Sync bounds
665#[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    /// Create a builder for Evolution Strategy
674    pub fn builder() -> ESBuilder<G, F, (), ()> {
675        ESBuilder::new()
676    }
677
678    /// Run the evolution strategy
679    pub fn run<R: Rng>(&self, rng: &mut R) -> Result<EvolutionResult<G, F>, EvolutionError> {
680        let start_time = Instant::now();
681
682        // Initialize population with adaptive genomes
683        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        // Sort by fitness (descending for maximization)
700        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        // Track best individual
708        let mut best = population[0].clone();
709
710        // Create a tracking population for termination checks
711        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        // Record initial statistics
719        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        // Main evolution loop
724        loop {
725            // Check termination
726            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            // Generate offspring
742            let mut offspring: Vec<(AdaptiveGenome<G>, F)> = Vec::with_capacity(self.config.lambda);
743
744            for _ in 0..self.config.lambda {
745                // Select parent(s) for recombination
746                let child = match &self.config.recombination {
747                    RecombinationType::None => {
748                        // Clone a random parent
749                        let parent_idx = rng.gen_range(0..self.config.mu);
750                        population[parent_idx].0.clone()
751                    }
752                    RecombinationType::Discrete => {
753                        // Select two parents, randomly pick genes
754                        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                        // Average of two parents
764                        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                        // Average of all parents
773                        self.global_intermediate_recombination(&population)
774                    }
775                };
776
777                // Mutate
778                let mutated = self.mutate(child, rng);
779
780                // Evaluate
781                let fitness = self.fitness.evaluate(mutated.inner());
782                offspring.push((mutated, fitness));
783            }
784            evaluations += self.config.lambda;
785
786            // Selection
787            match self.config.selection {
788                ESSelectionStrategy::MuPlusLambda => {
789                    // Combine parents and offspring
790                    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                    // Select only from offspring
798                    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            // Update best
805            if population[0].1.is_better_than(&best.1) {
806                best = population[0].clone();
807            }
808
809            generation += 1;
810
811            // Update tracking population for statistics
812            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            // Record statistics
821            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    /// Discrete recombination: randomly select genes from parents
838    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        // Recombine strategy parameters
856        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    /// Intermediate recombination: average of two parents
873    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        // Average strategy parameters
890        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    /// Global intermediate recombination: average of all parents
907    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        // Average all strategy parameters
924        let avg_sigma = if self.config.self_adaptive {
925            // Average the sigmas from all parents
926            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    /// Mutate an adaptive genome
944    fn mutate<R: Rng>(&self, mut genome: AdaptiveGenome<G>, rng: &mut R) -> AdaptiveGenome<G> {
945        let n = genome.inner().genes().len();
946
947        // Self-adaptive: mutate strategy parameters first
948        if self.config.self_adaptive {
949            genome.strategy.mutate(n, rng);
950        }
951
952        // Collect sigmas first to avoid borrow conflicts
953        let sigmas: Vec<f64> = (0..n).map(|i| genome.strategy.get_sigma(i)).collect();
954
955        // Then mutate the genome using the (possibly updated) strategy
956        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            // Clamp to bounds
962            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
971/// Type alias for (μ+λ)-ES
972pub type MuPlusLambdaES<G, F, Fit, Term> = EvolutionStrategy<G, F, Fit, Term>;
973
974/// Type alias for (μ,λ)-ES
975pub 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        // Should find improvement
1015        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        // Should find improvement (may be less effective than μ+λ without elitism)
1040        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        // λ < μ should fail
1050        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) // Large sigma to test bounds
1111            .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        // All genes should be within bounds
1120        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}