Skip to main content

fugue_evo/algorithms/
simple_ga.rs

1//! Simple Genetic Algorithm
2//!
3//! This module implements a standard generational genetic algorithm.
4
5use std::time::Instant;
6
7use rand::Rng;
8
9use crate::diagnostics::{EvolutionResult, EvolutionStats, GenerationStats, TimingStats};
10use crate::error::EvolutionError;
11use crate::fitness::traits::{Fitness, FitnessValue};
12use crate::genome::bit_string::BitString;
13use crate::genome::bounds::MultiBounds;
14use crate::genome::permutation::Permutation;
15use crate::genome::real_vector::RealVector;
16use crate::genome::traits::EvolutionaryGenome;
17use crate::hyperparameter::bayesian::{
18    ThompsonConfig, ThompsonSamplingTuner, TunableMutation, PARAM_CROSSOVER_PROB,
19    PARAM_MUTATION_RATE,
20};
21use crate::operators::crossover::{OxCrossover, SbxCrossover, UniformCrossover};
22use crate::operators::mutation::{BitFlipMutation, PermutationSwapMutation, PolynomialMutation};
23use crate::operators::selection::TournamentSelection;
24use crate::operators::traits::{
25    BoundedCrossoverOperator, BoundedMutationOperator, CrossoverOperator, MutationOperator,
26    SelectionOperator,
27};
28use crate::population::individual::Individual;
29use crate::population::population::Population;
30use crate::termination::{EvolutionState, MaxGenerations, TerminationCriterion};
31
32/// Validate a builder's configuration and bounds, producing a clear typed error.
33///
34/// Every operator/fitness/termination slot is enforced at compile time by the
35/// type-state builder, so this only needs to cover the runtime-configurable
36/// fields (population sizing, probabilities, and bounds).
37fn validate_config(config: &SimpleGAConfig, bounds: &MultiBounds) -> Result<(), EvolutionError> {
38    if config.population_size == 0 {
39        return Err(EvolutionError::Configuration(
40            "population_size must be at least 1".to_string(),
41        ));
42    }
43    if bounds.dimension() == 0 {
44        return Err(EvolutionError::Configuration(
45            "bounds must have at least one dimension".to_string(),
46        ));
47    }
48    if config.elitism && config.elite_count > config.population_size {
49        return Err(EvolutionError::Configuration(format!(
50            "elite_count ({}) cannot exceed population_size ({})",
51            config.elite_count, config.population_size
52        )));
53    }
54    if !(0.0..=1.0).contains(&config.crossover_probability) {
55        return Err(EvolutionError::Configuration(format!(
56            "crossover_probability must be in [0, 1], got {}",
57            config.crossover_probability
58        )));
59    }
60    Ok(())
61}
62
63/// Configuration for the Simple GA
64#[derive(Clone, Debug)]
65pub struct SimpleGAConfig {
66    /// Population size
67    pub population_size: usize,
68    /// Whether to use elitism (preserve best individual)
69    pub elitism: bool,
70    /// Number of elite individuals to preserve
71    pub elite_count: usize,
72    /// Crossover probability
73    pub crossover_probability: f64,
74    /// Whether to evaluate in parallel
75    pub parallel_evaluation: bool,
76}
77
78impl Default for SimpleGAConfig {
79    fn default() -> Self {
80        Self {
81            population_size: 100,
82            elitism: true,
83            elite_count: 1,
84            crossover_probability: 0.9,
85            parallel_evaluation: true,
86        }
87    }
88}
89
90/// Builder for SimpleGA
91pub struct SimpleGABuilder<G, F, S, C, M, Fit, Term>
92where
93    G: EvolutionaryGenome,
94    F: FitnessValue,
95{
96    config: SimpleGAConfig,
97    bounds: Option<MultiBounds>,
98    selection: Option<S>,
99    crossover: Option<C>,
100    mutation: Option<M>,
101    fitness: Option<Fit>,
102    termination: Option<Term>,
103    adaptive: Option<ThompsonConfig>,
104    _phantom: std::marker::PhantomData<(G, F)>,
105}
106
107impl<G, F> SimpleGABuilder<G, F, (), (), (), (), ()>
108where
109    G: EvolutionaryGenome,
110    F: FitnessValue,
111{
112    /// Create a new builder with default configuration
113    pub fn new() -> Self {
114        Self {
115            config: SimpleGAConfig::default(),
116            bounds: None,
117            selection: None,
118            crossover: None,
119            mutation: None,
120            fitness: None,
121            termination: None,
122            adaptive: None,
123            _phantom: std::marker::PhantomData,
124        }
125    }
126}
127
128impl
129    SimpleGABuilder<RealVector, f64, TournamentSelection, SbxCrossover, PolynomialMutation, (), ()>
130{
131    /// Ergonomic entry point for real-valued optimization — **no turbofish needed**.
132    ///
133    /// Pins the genome to [`RealVector`] and the fitness value to `f64`, and
134    /// pre-installs sensible operator defaults (tournament selection, SBX
135    /// crossover, polynomial mutation). Any default is overridable by calling the
136    /// corresponding `.selection()` / `.crossover()` / `.mutation()` method.
137    ///
138    /// ```no_run
139    /// use fugue_evo::prelude::*;
140    /// use rand::{rngs::StdRng, SeedableRng};
141    ///
142    /// let mut rng = StdRng::seed_from_u64(42);
143    /// let result = SimpleGABuilder::real_valued()
144    ///     .population_size(100)
145    ///     .bounds(MultiBounds::symmetric(5.12, 10))
146    ///     .fitness(Sphere::new(10))
147    ///     .max_generations(200)
148    ///     .build()
149    ///     .unwrap()
150    ///     .run(&mut rng)
151    ///     .unwrap();
152    /// println!("best = {:.6}", result.best_fitness);
153    /// ```
154    pub fn real_valued() -> Self {
155        SimpleGABuilder::new()
156            .selection(TournamentSelection::new(3))
157            .crossover(SbxCrossover::new(20.0))
158            .mutation(PolynomialMutation::new(20.0))
159    }
160}
161
162impl
163    SimpleGABuilder<
164        BitString,
165        usize,
166        TournamentSelection,
167        UniformCrossover,
168        BitFlipMutation,
169        (),
170        (),
171    >
172{
173    /// Ergonomic entry point for bit-string optimization — **no turbofish needed**.
174    ///
175    /// Pins the genome to [`BitString`] and the fitness value to `usize`, with
176    /// tournament selection, uniform crossover, and bit-flip mutation as
177    /// overridable defaults.
178    pub fn bit_string() -> Self {
179        SimpleGABuilder::new()
180            .selection(TournamentSelection::new(3))
181            .crossover(UniformCrossover::new())
182            .mutation(BitFlipMutation::new())
183    }
184}
185
186impl
187    SimpleGABuilder<
188        Permutation,
189        f64,
190        TournamentSelection,
191        OxCrossover,
192        PermutationSwapMutation,
193        (),
194        (),
195    >
196{
197    /// Ergonomic entry point for permutation optimization — **no turbofish needed**.
198    ///
199    /// Pins the genome to [`Permutation`] and the fitness value to `f64`, with
200    /// tournament selection, order crossover (OX), and swap mutation as
201    /// overridable defaults.
202    pub fn permutation() -> Self {
203        SimpleGABuilder::new()
204            .selection(TournamentSelection::new(3))
205            .crossover(OxCrossover)
206            .mutation(PermutationSwapMutation::default())
207    }
208}
209
210impl<G, F> Default for SimpleGABuilder<G, F, (), (), (), (), ()>
211where
212    G: EvolutionaryGenome,
213    F: FitnessValue,
214{
215    fn default() -> Self {
216        Self::new()
217    }
218}
219
220impl<G, F, S, C, M, Fit, Term> SimpleGABuilder<G, F, S, C, M, Fit, Term>
221where
222    G: EvolutionaryGenome,
223    F: FitnessValue,
224{
225    /// Set the population size
226    pub fn population_size(mut self, size: usize) -> Self {
227        self.config.population_size = size;
228        self
229    }
230
231    /// Enable or disable elitism
232    pub fn elitism(mut self, enabled: bool) -> Self {
233        self.config.elitism = enabled;
234        self
235    }
236
237    /// Set the number of elite individuals to preserve
238    pub fn elite_count(mut self, count: usize) -> Self {
239        self.config.elite_count = count;
240        self
241    }
242
243    /// Set the crossover probability
244    pub fn crossover_probability(mut self, probability: f64) -> Self {
245        self.config.crossover_probability = probability;
246        self
247    }
248
249    /// Enable or disable parallel evaluation
250    pub fn parallel_evaluation(mut self, enabled: bool) -> Self {
251        self.config.parallel_evaluation = enabled;
252        self
253    }
254
255    /// Set the search space bounds
256    pub fn bounds(mut self, bounds: MultiBounds) -> Self {
257        self.bounds = Some(bounds);
258        self
259    }
260
261    /// Opt in to online Thompson-sampling tuning of operator parameters.
262    ///
263    /// When enabled, [`SimpleGA::run_adaptive`] consults a
264    /// [`ThompsonSamplingTuner`] each generation for the per-gene mutation
265    /// probability and the whole-genome crossover probability, applies the
266    /// sampled arm values, and credits the arms with the observed
267    /// parent-vs-offspring improvement events. The mutation operator must
268    /// implement [`TunableMutation`].
269    pub fn adaptive_operators(mut self, config: ThompsonConfig) -> Self {
270        self.adaptive = Some(config);
271        self
272    }
273
274    /// Set the selection operator
275    pub fn selection<NewS>(self, selection: NewS) -> SimpleGABuilder<G, F, NewS, C, M, Fit, Term>
276    where
277        NewS: SelectionOperator<G>,
278    {
279        SimpleGABuilder {
280            config: self.config,
281            bounds: self.bounds,
282            selection: Some(selection),
283            adaptive: self.adaptive,
284            crossover: self.crossover,
285            mutation: self.mutation,
286            fitness: self.fitness,
287            termination: self.termination,
288            _phantom: std::marker::PhantomData,
289        }
290    }
291
292    /// Set the crossover operator
293    pub fn crossover<NewC>(self, crossover: NewC) -> SimpleGABuilder<G, F, S, NewC, M, Fit, Term>
294    where
295        NewC: CrossoverOperator<G>,
296    {
297        SimpleGABuilder {
298            config: self.config,
299            bounds: self.bounds,
300            selection: self.selection,
301            crossover: Some(crossover),
302            adaptive: self.adaptive,
303            mutation: self.mutation,
304            fitness: self.fitness,
305            termination: self.termination,
306            _phantom: std::marker::PhantomData,
307        }
308    }
309
310    /// Set the mutation operator
311    pub fn mutation<NewM>(self, mutation: NewM) -> SimpleGABuilder<G, F, S, C, NewM, Fit, Term>
312    where
313        NewM: MutationOperator<G>,
314    {
315        SimpleGABuilder {
316            config: self.config,
317            bounds: self.bounds,
318            selection: self.selection,
319            crossover: self.crossover,
320            mutation: Some(mutation),
321            adaptive: self.adaptive,
322            fitness: self.fitness,
323            termination: self.termination,
324            _phantom: std::marker::PhantomData,
325        }
326    }
327
328    /// Set the fitness function
329    pub fn fitness<NewFit>(self, fitness: NewFit) -> SimpleGABuilder<G, F, S, C, M, NewFit, Term>
330    where
331        NewFit: Fitness<Genome = G, Value = F>,
332    {
333        SimpleGABuilder {
334            config: self.config,
335            bounds: self.bounds,
336            selection: self.selection,
337            crossover: self.crossover,
338            mutation: self.mutation,
339            fitness: Some(fitness),
340            adaptive: self.adaptive,
341            termination: self.termination,
342            _phantom: std::marker::PhantomData,
343        }
344    }
345
346    /// Set the termination criterion
347    pub fn termination<NewTerm>(
348        self,
349        termination: NewTerm,
350    ) -> SimpleGABuilder<G, F, S, C, M, Fit, NewTerm>
351    where
352        NewTerm: TerminationCriterion<G, F>,
353    {
354        SimpleGABuilder {
355            config: self.config,
356            bounds: self.bounds,
357            selection: self.selection,
358            crossover: self.crossover,
359            mutation: self.mutation,
360            fitness: self.fitness,
361            termination: Some(termination),
362            adaptive: self.adaptive,
363            _phantom: std::marker::PhantomData,
364        }
365    }
366
367    /// Set max generations (convenience method)
368    pub fn max_generations(
369        self,
370        max: usize,
371    ) -> SimpleGABuilder<G, F, S, C, M, Fit, MaxGenerations> {
372        SimpleGABuilder {
373            config: self.config,
374            bounds: self.bounds,
375            selection: self.selection,
376            crossover: self.crossover,
377            mutation: self.mutation,
378            fitness: self.fitness,
379            termination: Some(MaxGenerations::new(max)),
380            adaptive: self.adaptive,
381            _phantom: std::marker::PhantomData,
382        }
383    }
384}
385
386// Builder build() method - parallel version with Send + Sync bounds
387#[cfg(feature = "parallel")]
388impl<G, F, S, C, M, Fit, Term> SimpleGABuilder<G, F, S, C, M, Fit, Term>
389where
390    G: EvolutionaryGenome + Send + Sync,
391    F: FitnessValue + Send,
392    S: SelectionOperator<G>,
393    C: CrossoverOperator<G>,
394    M: MutationOperator<G>,
395    Fit: Fitness<Genome = G, Value = F> + Sync,
396    Term: TerminationCriterion<G, F>,
397{
398    /// Build the SimpleGA instance
399    #[allow(clippy::type_complexity)]
400    pub fn build(self) -> Result<SimpleGA<G, F, S, C, M, Fit, Term>, EvolutionError> {
401        let bounds = self
402            .bounds
403            .ok_or_else(|| EvolutionError::Configuration("Bounds must be specified".to_string()))?;
404
405        let selection = self.selection.ok_or_else(|| {
406            EvolutionError::Configuration("Selection operator must be specified".to_string())
407        })?;
408
409        let crossover = self.crossover.ok_or_else(|| {
410            EvolutionError::Configuration("Crossover operator must be specified".to_string())
411        })?;
412
413        let mutation = self.mutation.ok_or_else(|| {
414            EvolutionError::Configuration("Mutation operator must be specified".to_string())
415        })?;
416
417        let fitness = self.fitness.ok_or_else(|| {
418            EvolutionError::Configuration("Fitness function must be specified".to_string())
419        })?;
420
421        let termination = self.termination.ok_or_else(|| {
422            EvolutionError::Configuration("Termination criterion must be specified".to_string())
423        })?;
424
425        // Validate runtime-configurable fields (operators/fitness/termination are
426        // already enforced at compile time by the type-state builder).
427        validate_config(&self.config, &bounds)?;
428
429        let tuner = self.adaptive.map(|cfg| cfg.build_tuner());
430
431        Ok(SimpleGA {
432            config: self.config,
433            bounds,
434            selection,
435            crossover,
436            mutation,
437            fitness,
438            termination,
439            tuner,
440            _phantom: std::marker::PhantomData,
441        })
442    }
443}
444
445// Builder build() method - non-parallel version without Send + Sync bounds
446#[cfg(not(feature = "parallel"))]
447impl<G, F, S, C, M, Fit, Term> SimpleGABuilder<G, F, S, C, M, Fit, Term>
448where
449    G: EvolutionaryGenome,
450    F: FitnessValue,
451    S: SelectionOperator<G>,
452    C: CrossoverOperator<G>,
453    M: MutationOperator<G>,
454    Fit: Fitness<Genome = G, Value = F>,
455    Term: TerminationCriterion<G, F>,
456{
457    /// Build the SimpleGA instance
458    #[allow(clippy::type_complexity)]
459    pub fn build(self) -> Result<SimpleGA<G, F, S, C, M, Fit, Term>, EvolutionError> {
460        let bounds = self
461            .bounds
462            .ok_or_else(|| EvolutionError::Configuration("Bounds must be specified".to_string()))?;
463
464        let selection = self.selection.ok_or_else(|| {
465            EvolutionError::Configuration("Selection operator must be specified".to_string())
466        })?;
467
468        let crossover = self.crossover.ok_or_else(|| {
469            EvolutionError::Configuration("Crossover operator must be specified".to_string())
470        })?;
471
472        let mutation = self.mutation.ok_or_else(|| {
473            EvolutionError::Configuration("Mutation operator must be specified".to_string())
474        })?;
475
476        let fitness = self.fitness.ok_or_else(|| {
477            EvolutionError::Configuration("Fitness function must be specified".to_string())
478        })?;
479
480        let termination = self.termination.ok_or_else(|| {
481            EvolutionError::Configuration("Termination criterion must be specified".to_string())
482        })?;
483
484        // Validate runtime-configurable fields (operators/fitness/termination are
485        // already enforced at compile time by the type-state builder).
486        validate_config(&self.config, &bounds)?;
487
488        let tuner = self.adaptive.map(|cfg| cfg.build_tuner());
489
490        Ok(SimpleGA {
491            config: self.config,
492            bounds,
493            selection,
494            crossover,
495            mutation,
496            fitness,
497            termination,
498            tuner,
499            _phantom: std::marker::PhantomData,
500        })
501    }
502}
503
504/// Simple Genetic Algorithm
505///
506/// A standard generational GA with configurable operators.
507pub struct SimpleGA<G, F, S, C, M, Fit, Term>
508where
509    G: EvolutionaryGenome,
510    F: FitnessValue,
511{
512    config: SimpleGAConfig,
513    bounds: MultiBounds,
514    selection: S,
515    crossover: C,
516    mutation: M,
517    fitness: Fit,
518    termination: Term,
519    tuner: Option<ThompsonSamplingTuner>,
520    _phantom: std::marker::PhantomData<(G, F)>,
521}
522
523// Adaptive (Thompson-sampling) run loop.
524//
525// Available for any tunable mutation operator regardless of the `parallel`
526// feature: it evaluates offspring sequentially so it can read each
527// parent-vs-offspring improvement event and feed it back to the tuner.
528impl<G, F, S, C, M, Fit, Term> SimpleGA<G, F, S, C, M, Fit, Term>
529where
530    G: EvolutionaryGenome,
531    F: FitnessValue,
532    S: SelectionOperator<G>,
533    C: CrossoverOperator<G>,
534    M: MutationOperator<G> + TunableMutation + Clone,
535    Fit: Fitness<Genome = G, Value = F>,
536    Term: TerminationCriterion<G, F>,
537{
538    /// Access the online tuner.
539    ///
540    /// Populated once [`run_adaptive`](Self::run_adaptive) has run, or immediately
541    /// if the builder opted in via
542    /// [`SimpleGABuilder::adaptive_operators`](SimpleGABuilder::adaptive_operators).
543    pub fn tuner(&self) -> Option<&ThompsonSamplingTuner> {
544        self.tuner.as_ref()
545    }
546
547    /// Run the GA with online Thompson-sampling tuning of operator parameters.
548    ///
549    /// Each generation the tuner Thompson-samples a per-gene mutation probability
550    /// and a whole-genome crossover probability; those values drive that
551    /// generation's operators, and each offspring's improvement over its parents
552    /// is credited back to the arm that produced it. If the builder did not opt in
553    /// via [`SimpleGABuilder::adaptive_operators`](SimpleGABuilder::adaptive_operators),
554    /// a default [`ThompsonConfig`] tuner is created on first use.
555    pub fn run_adaptive<R: Rng>(
556        &mut self,
557        rng: &mut R,
558    ) -> Result<EvolutionResult<G, F>, EvolutionError> {
559        let start_time = Instant::now();
560
561        // Own the tuner locally so the loop can freely borrow `self`'s fields.
562        let mut tuner = self
563            .tuner
564            .take()
565            .unwrap_or_else(|| ThompsonConfig::default().build_tuner());
566
567        let mut population: Population<G, F> =
568            Population::random(self.config.population_size, &self.bounds, rng);
569        population.evaluate(&self.fitness);
570
571        let mut stats = EvolutionStats::new();
572        let mut evaluations = population.len();
573        let mut fitness_history: Vec<f64> = Vec::new();
574
575        let mut best_individual = population
576            .best()
577            .ok_or(EvolutionError::EmptyPopulation)?
578            .clone();
579
580        let gen_stats = GenerationStats::from_population(&population, 0, evaluations);
581        fitness_history.push(gen_stats.best_fitness);
582        stats.record(gen_stats);
583        tuner.snapshot(0);
584
585        loop {
586            let state = EvolutionState {
587                generation: population.generation(),
588                evaluations,
589                best_fitness: best_individual.fitness_value().to_f64(),
590                population: &population,
591                fitness_history: &fitness_history,
592            };
593            if self.termination.should_terminate(&state) {
594                stats.set_termination_reason(self.termination.reason());
595                break;
596            }
597
598            let next_generation = population.generation() + 1;
599
600            // Thompson-sample this generation's operator parameters.
601            tuner.select_all(rng);
602            let crossover_prob = tuner
603                .selected(PARAM_CROSSOVER_PROB)
604                .unwrap_or(self.config.crossover_probability);
605            let mut mutation = self.mutation.clone();
606            if let Some(mutation_prob) = tuner.selected(PARAM_MUTATION_RATE) {
607                mutation.set_mutation_probability(mutation_prob);
608            }
609
610            let mut new_population: Population<G, F> =
611                Population::with_capacity(self.config.population_size);
612
613            // Elitism: carry the best (already-evaluated) individuals forward.
614            if self.config.elitism {
615                let mut sorted = population.clone();
616                sorted.sort_by_fitness();
617                for i in 0..self.config.elite_count.min(sorted.len()) {
618                    new_population.push(sorted[i].clone());
619                }
620            }
621
622            let selection_pool: Vec<(G, f64)> = population.as_fitness_pairs();
623
624            while new_population.len() < self.config.population_size {
625                let p1 = self.selection.select(&selection_pool, rng);
626                let p2 = self.selection.select(&selection_pool, rng);
627                let parent1 = &selection_pool[p1].0;
628                let parent2 = &selection_pool[p2].0;
629                // Improvement is measured against the better of the two parents.
630                let parent_best = selection_pool[p1].1.max(selection_pool[p2].1);
631
632                let (mut child1, mut child2) = if rng.gen::<f64>() < crossover_prob {
633                    match self.crossover.crossover(parent1, parent2, rng).genome() {
634                        Some((c1, c2)) => (c1, c2),
635                        None => (parent1.clone(), parent2.clone()),
636                    }
637                } else {
638                    (parent1.clone(), parent2.clone())
639                };
640
641                mutation.mutate(&mut child1, rng);
642                mutation.mutate(&mut child2, rng);
643
644                // Evaluate immediately so each improvement event credits the arms.
645                let f1 = self.fitness.evaluate(&child1);
646                tuner.observe(f1.to_f64() > parent_best);
647                evaluations += 1;
648                let mut ind1 = Individual::with_fitness(child1, f1);
649                ind1.birth_generation = next_generation;
650                new_population.push(ind1);
651
652                if new_population.len() < self.config.population_size {
653                    let f2 = self.fitness.evaluate(&child2);
654                    tuner.observe(f2.to_f64() > parent_best);
655                    evaluations += 1;
656                    let mut ind2 = Individual::with_fitness(child2, f2);
657                    ind2.birth_generation = next_generation;
658                    new_population.push(ind2);
659                }
660            }
661
662            while new_population.len() > self.config.population_size {
663                new_population.pop();
664            }
665
666            new_population.set_generation(next_generation);
667            population = new_population;
668
669            if let Some(best) = population.best() {
670                if best.is_better_than(&best_individual) {
671                    best_individual = best.clone();
672                }
673            }
674
675            let gen_stats =
676                GenerationStats::from_population(&population, population.generation(), evaluations);
677            fitness_history.push(gen_stats.best_fitness);
678            stats.record(gen_stats);
679            tuner.snapshot(next_generation);
680        }
681
682        stats.set_runtime(start_time.elapsed());
683
684        // Store the tuner back so callers can inspect the learned posteriors.
685        self.tuner = Some(tuner);
686
687        Ok(EvolutionResult::new(
688            best_individual.genome,
689            best_individual.fitness.unwrap(),
690            population.generation(),
691            evaluations,
692        )
693        .with_stats(stats))
694    }
695}
696
697// Parallel version with Send + Sync bounds
698#[cfg(feature = "parallel")]
699impl<G, F, S, C, M, Fit, Term> SimpleGA<G, F, S, C, M, Fit, Term>
700where
701    G: EvolutionaryGenome + Send + Sync,
702    F: FitnessValue + Send,
703    S: SelectionOperator<G>,
704    C: CrossoverOperator<G>,
705    M: MutationOperator<G>,
706    Fit: Fitness<Genome = G, Value = F> + Sync,
707    Term: TerminationCriterion<G, F>,
708{
709    /// Create a builder for SimpleGA
710    pub fn builder() -> SimpleGABuilder<G, F, (), (), (), (), ()> {
711        SimpleGABuilder::new()
712    }
713
714    /// Run the genetic algorithm
715    pub fn run<R: Rng>(&self, rng: &mut R) -> Result<EvolutionResult<G, F>, EvolutionError> {
716        let start_time = Instant::now();
717
718        // Initialize population
719        let mut population: Population<G, F> =
720            Population::random(self.config.population_size, &self.bounds, rng);
721
722        // Evaluate initial population
723        let eval_start = Instant::now();
724        if self.config.parallel_evaluation {
725            population.evaluate_parallel(&self.fitness);
726        } else {
727            population.evaluate(&self.fitness);
728        }
729        let eval_time = eval_start.elapsed();
730
731        let mut stats = EvolutionStats::new();
732        let mut evaluations = population.len();
733        let mut fitness_history: Vec<f64> = Vec::new();
734
735        // Track best individual
736        let mut best_individual = population
737            .best()
738            .ok_or(EvolutionError::EmptyPopulation)?
739            .clone();
740
741        // Record initial statistics
742        let gen_stats = GenerationStats::from_population(&population, 0, evaluations)
743            .with_timing(TimingStats::new().with_evaluation(eval_time));
744        fitness_history.push(gen_stats.best_fitness);
745        stats.record(gen_stats);
746
747        // Main evolution loop
748        loop {
749            // Check termination
750            let state = EvolutionState {
751                generation: population.generation(),
752                evaluations,
753                best_fitness: best_individual.fitness_value().to_f64(),
754                population: &population,
755                fitness_history: &fitness_history,
756            };
757
758            if self.termination.should_terminate(&state) {
759                stats.set_termination_reason(self.termination.reason());
760                break;
761            }
762
763            let gen_start = Instant::now();
764
765            // Create new generation
766            let mut new_population: Population<G, F> =
767                Population::with_capacity(self.config.population_size);
768
769            // Elitism: copy best individuals
770            if self.config.elitism {
771                let mut sorted = population.clone();
772                sorted.sort_by_fitness();
773                for i in 0..self.config.elite_count.min(sorted.len()) {
774                    new_population.push(sorted[i].clone());
775                }
776            }
777
778            // Selection pool
779            let selection_pool: Vec<(G, f64)> = population.as_fitness_pairs();
780
781            // Generate offspring
782            let _selection_start = Instant::now();
783            let mut selection_time = std::time::Duration::ZERO;
784            let mut crossover_time = std::time::Duration::ZERO;
785            let mut mutation_time = std::time::Duration::ZERO;
786
787            while new_population.len() < self.config.population_size {
788                // Selection
789                let sel_start = Instant::now();
790                let parent1_idx = self.selection.select(&selection_pool, rng);
791                let parent2_idx = self.selection.select(&selection_pool, rng);
792                selection_time += sel_start.elapsed();
793
794                let parent1 = &selection_pool[parent1_idx].0;
795                let parent2 = &selection_pool[parent2_idx].0;
796
797                // Crossover
798                let cross_start = Instant::now();
799                let (mut child1, mut child2) =
800                    if rng.gen::<f64>() < self.config.crossover_probability {
801                        match self.crossover.crossover(parent1, parent2, rng).genome() {
802                            Some((c1, c2)) => (c1, c2),
803                            None => (parent1.clone(), parent2.clone()),
804                        }
805                    } else {
806                        (parent1.clone(), parent2.clone())
807                    };
808                crossover_time += cross_start.elapsed();
809
810                // Mutation
811                let mut_start = Instant::now();
812                self.mutation.mutate(&mut child1, rng);
813                self.mutation.mutate(&mut child2, rng);
814                mutation_time += mut_start.elapsed();
815
816                // Add to new population
817                new_population.push(Individual::with_generation(
818                    child1,
819                    population.generation() + 1,
820                ));
821                if new_population.len() < self.config.population_size {
822                    new_population.push(Individual::with_generation(
823                        child2,
824                        population.generation() + 1,
825                    ));
826                }
827            }
828
829            // Truncate to exact size
830            while new_population.len() > self.config.population_size {
831                new_population.pop();
832            }
833
834            // Evaluate new population
835            let eval_start = Instant::now();
836            if self.config.parallel_evaluation {
837                new_population.evaluate_parallel(&self.fitness);
838            } else {
839                new_population.evaluate(&self.fitness);
840            }
841            let eval_time = eval_start.elapsed();
842            evaluations += new_population.len()
843                - (if self.config.elitism {
844                    self.config.elite_count
845                } else {
846                    0
847                });
848
849            // Update generation counter
850            new_population.set_generation(population.generation() + 1);
851            population = new_population;
852
853            // Update best individual
854            if let Some(best) = population.best() {
855                if best.is_better_than(&best_individual) {
856                    best_individual = best.clone();
857                }
858            }
859
860            // Record statistics
861            let timing = TimingStats::new()
862                .with_selection(selection_time)
863                .with_crossover(crossover_time)
864                .with_mutation(mutation_time)
865                .with_evaluation(eval_time)
866                .with_total(gen_start.elapsed());
867
868            let gen_stats =
869                GenerationStats::from_population(&population, population.generation(), evaluations)
870                    .with_timing(timing);
871            fitness_history.push(gen_stats.best_fitness);
872            stats.record(gen_stats);
873        }
874
875        stats.set_runtime(start_time.elapsed());
876
877        Ok(EvolutionResult::new(
878            best_individual.genome,
879            best_individual.fitness.unwrap(),
880            population.generation(),
881            evaluations,
882        )
883        .with_stats(stats))
884    }
885}
886
887// Bounded operators version - parallel with Send + Sync
888#[cfg(feature = "parallel")]
889impl<G, F, S, C, M, Fit, Term> SimpleGA<G, F, S, C, M, Fit, Term>
890where
891    G: EvolutionaryGenome + Send + Sync,
892    F: FitnessValue + Send,
893    S: SelectionOperator<G>,
894    C: BoundedCrossoverOperator<G>,
895    M: BoundedMutationOperator<G>,
896    Fit: Fitness<Genome = G, Value = F> + Sync,
897    Term: TerminationCriterion<G, F>,
898{
899    /// Run the genetic algorithm with bounded operators
900    pub fn run_bounded<R: Rng>(
901        &self,
902        rng: &mut R,
903    ) -> Result<EvolutionResult<G, F>, EvolutionError> {
904        let start_time = Instant::now();
905
906        // Initialize population
907        let mut population: Population<G, F> =
908            Population::random(self.config.population_size, &self.bounds, rng);
909
910        // Evaluate initial population
911        if self.config.parallel_evaluation {
912            population.evaluate_parallel(&self.fitness);
913        } else {
914            population.evaluate(&self.fitness);
915        }
916
917        let mut stats = EvolutionStats::new();
918        let mut evaluations = population.len();
919        let mut fitness_history: Vec<f64> = Vec::new();
920
921        // Track best individual
922        let mut best_individual = population
923            .best()
924            .ok_or(EvolutionError::EmptyPopulation)?
925            .clone();
926
927        // Record initial statistics
928        let gen_stats = GenerationStats::from_population(&population, 0, evaluations);
929        fitness_history.push(gen_stats.best_fitness);
930        stats.record(gen_stats);
931
932        // Main evolution loop
933        loop {
934            // Check termination
935            let state = EvolutionState {
936                generation: population.generation(),
937                evaluations,
938                best_fitness: best_individual.fitness_value().to_f64(),
939                population: &population,
940                fitness_history: &fitness_history,
941            };
942
943            if self.termination.should_terminate(&state) {
944                stats.set_termination_reason(self.termination.reason());
945                break;
946            }
947
948            // Create new generation
949            let mut new_population: Population<G, F> =
950                Population::with_capacity(self.config.population_size);
951
952            // Elitism
953            if self.config.elitism {
954                let mut sorted = population.clone();
955                sorted.sort_by_fitness();
956                for i in 0..self.config.elite_count.min(sorted.len()) {
957                    new_population.push(sorted[i].clone());
958                }
959            }
960
961            let selection_pool: Vec<(G, f64)> = population.as_fitness_pairs();
962
963            while new_population.len() < self.config.population_size {
964                let parent1_idx = self.selection.select(&selection_pool, rng);
965                let parent2_idx = self.selection.select(&selection_pool, rng);
966
967                let parent1 = &selection_pool[parent1_idx].0;
968                let parent2 = &selection_pool[parent2_idx].0;
969
970                let (mut child1, mut child2) =
971                    if rng.gen::<f64>() < self.config.crossover_probability {
972                        match self
973                            .crossover
974                            .crossover_bounded(parent1, parent2, &self.bounds, rng)
975                            .genome()
976                        {
977                            Some((c1, c2)) => (c1, c2),
978                            None => (parent1.clone(), parent2.clone()),
979                        }
980                    } else {
981                        (parent1.clone(), parent2.clone())
982                    };
983
984                self.mutation.mutate_bounded(&mut child1, &self.bounds, rng);
985                self.mutation.mutate_bounded(&mut child2, &self.bounds, rng);
986
987                new_population.push(Individual::with_generation(
988                    child1,
989                    population.generation() + 1,
990                ));
991                if new_population.len() < self.config.population_size {
992                    new_population.push(Individual::with_generation(
993                        child2,
994                        population.generation() + 1,
995                    ));
996                }
997            }
998
999            while new_population.len() > self.config.population_size {
1000                new_population.pop();
1001            }
1002
1003            if self.config.parallel_evaluation {
1004                new_population.evaluate_parallel(&self.fitness);
1005            } else {
1006                new_population.evaluate(&self.fitness);
1007            }
1008            evaluations += new_population.len()
1009                - (if self.config.elitism {
1010                    self.config.elite_count
1011                } else {
1012                    0
1013                });
1014
1015            new_population.set_generation(population.generation() + 1);
1016            population = new_population;
1017
1018            if let Some(best) = population.best() {
1019                if best.is_better_than(&best_individual) {
1020                    best_individual = best.clone();
1021                }
1022            }
1023
1024            let gen_stats =
1025                GenerationStats::from_population(&population, population.generation(), evaluations);
1026            fitness_history.push(gen_stats.best_fitness);
1027            stats.record(gen_stats);
1028        }
1029
1030        stats.set_runtime(start_time.elapsed());
1031
1032        Ok(EvolutionResult::new(
1033            best_individual.genome,
1034            best_individual.fitness.unwrap(),
1035            population.generation(),
1036            evaluations,
1037        )
1038        .with_stats(stats))
1039    }
1040}
1041
1042// Non-parallel version without Send + Sync bounds
1043#[cfg(not(feature = "parallel"))]
1044impl<G, F, S, C, M, Fit, Term> SimpleGA<G, F, S, C, M, Fit, Term>
1045where
1046    G: EvolutionaryGenome,
1047    F: FitnessValue,
1048    S: SelectionOperator<G>,
1049    C: CrossoverOperator<G>,
1050    M: MutationOperator<G>,
1051    Fit: Fitness<Genome = G, Value = F>,
1052    Term: TerminationCriterion<G, F>,
1053{
1054    /// Create a builder for SimpleGA
1055    pub fn builder() -> SimpleGABuilder<G, F, (), (), (), (), ()> {
1056        SimpleGABuilder::new()
1057    }
1058
1059    /// Run the genetic algorithm
1060    ///
1061    /// This is a thin driver over the incremental stepping API
1062    /// ([`SimpleGA::init_run`], [`SimpleGA::step_generation`],
1063    /// [`SimpleGA::finish_run`]); it is guaranteed to produce the same result as
1064    /// driving those methods manually (AUDIT EV-34).
1065    pub fn run<R: Rng>(&self, rng: &mut R) -> Result<EvolutionResult<G, F>, EvolutionError> {
1066        let mut state = self.init_run(rng)?;
1067        while self.step_generation(&mut state, rng)? {}
1068        Ok(self.finish_run(state))
1069    }
1070}
1071
1072// The incremental stepping API (SimpleGaRun + init_run/step_generation/
1073// finish_run/inject_migrants) is available in ALL builds — it uses sequential
1074// evaluation and needs no Send+Sync bounds, so it works whether or not the
1075// `parallel` feature is enabled. This lets the checkpoint resume API (EV-02) and
1076// the WASM stepping bindings (EV-34) build on it regardless of feature flags.
1077// (The parallel `run` above still uses rayon; `run` is intentionally kept per-cfg
1078// so the default build stays parallel.)
1079impl<G, F, S, C, M, Fit, Term> SimpleGA<G, F, S, C, M, Fit, Term>
1080where
1081    G: EvolutionaryGenome,
1082    F: FitnessValue,
1083    S: SelectionOperator<G>,
1084    C: CrossoverOperator<G>,
1085    M: MutationOperator<G>,
1086    Fit: Fitness<Genome = G, Value = F>,
1087    Term: TerminationCriterion<G, F>,
1088{
1089    /// Initialize an incremental run: build and evaluate the initial population
1090    /// and record generation-0 statistics.
1091    ///
1092    /// The returned [`SimpleGaRun`] can then be advanced one generation at a time
1093    /// with [`SimpleGA::step_generation`] and consumed with
1094    /// [`SimpleGA::finish_run`]. This is the incremental counterpart to
1095    /// [`SimpleGA::run`], letting callers (e.g. the WASM bindings) report
1096    /// progress and cancel early (AUDIT EV-34).
1097    pub fn init_run<R: Rng>(&self, rng: &mut R) -> Result<SimpleGaRun<G, F>, EvolutionError> {
1098        let start_time = Instant::now();
1099
1100        // Initialize population
1101        let mut population: Population<G, F> =
1102            Population::random(self.config.population_size, &self.bounds, rng);
1103
1104        // Evaluate initial population (sequential only)
1105        let eval_start = Instant::now();
1106        population.evaluate(&self.fitness);
1107        let eval_time = eval_start.elapsed();
1108
1109        let mut stats = EvolutionStats::new();
1110        let evaluations = population.len();
1111        let mut fitness_history: Vec<f64> = Vec::new();
1112
1113        // Track best individual
1114        let best_individual = population
1115            .best()
1116            .ok_or(EvolutionError::EmptyPopulation)?
1117            .clone();
1118
1119        // Record initial statistics
1120        let gen_stats = GenerationStats::from_population(&population, 0, evaluations)
1121            .with_timing(TimingStats::new().with_evaluation(eval_time));
1122        fitness_history.push(gen_stats.best_fitness);
1123        stats.record(gen_stats);
1124
1125        Ok(SimpleGaRun {
1126            population,
1127            best_individual,
1128            evaluations,
1129            fitness_history,
1130            stats,
1131            start_time,
1132            terminated: false,
1133        })
1134    }
1135
1136    /// Advance an incremental run by a single generation.
1137    ///
1138    /// Returns `Ok(true)` if a generation was executed, or `Ok(false)` if the
1139    /// termination criterion fired (in which case `state` is left otherwise
1140    /// unchanged and marked terminated). This mirrors exactly one iteration of
1141    /// [`SimpleGA::run`]'s main loop, including the termination check performed
1142    /// before the generation body.
1143    pub fn step_generation<R: Rng>(
1144        &self,
1145        state: &mut SimpleGaRun<G, F>,
1146        rng: &mut R,
1147    ) -> Result<bool, EvolutionError> {
1148        if state.terminated {
1149            return Ok(false);
1150        }
1151
1152        // Check termination
1153        let evo_state = EvolutionState {
1154            generation: state.population.generation(),
1155            evaluations: state.evaluations,
1156            best_fitness: state.best_individual.fitness_value().to_f64(),
1157            population: &state.population,
1158            fitness_history: &state.fitness_history,
1159        };
1160
1161        if self.termination.should_terminate(&evo_state) {
1162            state
1163                .stats
1164                .set_termination_reason(self.termination.reason());
1165            state.terminated = true;
1166            return Ok(false);
1167        }
1168
1169        let gen_start = Instant::now();
1170
1171        // Create new generation
1172        let mut new_population: Population<G, F> =
1173            Population::with_capacity(self.config.population_size);
1174
1175        // Elitism: copy best individuals
1176        if self.config.elitism {
1177            let mut sorted = state.population.clone();
1178            sorted.sort_by_fitness();
1179            for i in 0..self.config.elite_count.min(sorted.len()) {
1180                new_population.push(sorted[i].clone());
1181            }
1182        }
1183
1184        // Selection pool
1185        let selection_pool: Vec<(G, f64)> = state.population.as_fitness_pairs();
1186
1187        // Generate offspring
1188        let _selection_start = Instant::now();
1189        let mut selection_time = std::time::Duration::ZERO;
1190        let mut crossover_time = std::time::Duration::ZERO;
1191        let mut mutation_time = std::time::Duration::ZERO;
1192
1193        while new_population.len() < self.config.population_size {
1194            // Selection
1195            let sel_start = Instant::now();
1196            let parent1_idx = self.selection.select(&selection_pool, rng);
1197            let parent2_idx = self.selection.select(&selection_pool, rng);
1198            selection_time += sel_start.elapsed();
1199
1200            let parent1 = &selection_pool[parent1_idx].0;
1201            let parent2 = &selection_pool[parent2_idx].0;
1202
1203            // Crossover
1204            let cross_start = Instant::now();
1205            let (mut child1, mut child2) = if rng.gen::<f64>() < self.config.crossover_probability {
1206                match self.crossover.crossover(parent1, parent2, rng).genome() {
1207                    Some((c1, c2)) => (c1, c2),
1208                    None => (parent1.clone(), parent2.clone()),
1209                }
1210            } else {
1211                (parent1.clone(), parent2.clone())
1212            };
1213            crossover_time += cross_start.elapsed();
1214
1215            // Mutation
1216            let mut_start = Instant::now();
1217            self.mutation.mutate(&mut child1, rng);
1218            self.mutation.mutate(&mut child2, rng);
1219            mutation_time += mut_start.elapsed();
1220
1221            // Add to new population
1222            new_population.push(Individual::with_generation(
1223                child1,
1224                state.population.generation() + 1,
1225            ));
1226            if new_population.len() < self.config.population_size {
1227                new_population.push(Individual::with_generation(
1228                    child2,
1229                    state.population.generation() + 1,
1230                ));
1231            }
1232        }
1233
1234        // Truncate to exact size
1235        while new_population.len() > self.config.population_size {
1236            new_population.pop();
1237        }
1238
1239        // Evaluate new population (sequential only)
1240        let eval_start = Instant::now();
1241        new_population.evaluate(&self.fitness);
1242        let eval_time = eval_start.elapsed();
1243        state.evaluations += new_population.len()
1244            - (if self.config.elitism {
1245                self.config.elite_count
1246            } else {
1247                0
1248            });
1249
1250        // Update generation counter
1251        new_population.set_generation(state.population.generation() + 1);
1252        state.population = new_population;
1253
1254        // Update best individual
1255        if let Some(best) = state.population.best() {
1256            if best.is_better_than(&state.best_individual) {
1257                state.best_individual = best.clone();
1258            }
1259        }
1260
1261        // Record statistics
1262        let timing = TimingStats::new()
1263            .with_selection(selection_time)
1264            .with_crossover(crossover_time)
1265            .with_mutation(mutation_time)
1266            .with_evaluation(eval_time)
1267            .with_total(gen_start.elapsed());
1268
1269        let gen_stats = GenerationStats::from_population(
1270            &state.population,
1271            state.population.generation(),
1272            state.evaluations,
1273        )
1274        .with_timing(timing);
1275        state.fitness_history.push(gen_stats.best_fitness);
1276        state.stats.record(gen_stats);
1277
1278        Ok(true)
1279    }
1280
1281    /// Consume an incremental run and produce the final [`EvolutionResult`],
1282    /// mirroring the tail of [`SimpleGA::run`].
1283    pub fn finish_run(&self, mut state: SimpleGaRun<G, F>) -> EvolutionResult<G, F> {
1284        state.stats.set_runtime(state.start_time.elapsed());
1285
1286        EvolutionResult::new(
1287            state.best_individual.genome,
1288            state
1289                .best_individual
1290                .fitness
1291                .expect("best individual is always evaluated"),
1292            state.population.generation(),
1293            state.evaluations,
1294        )
1295        .with_stats(state.stats)
1296    }
1297
1298    /// Inject migrant genomes into an in-progress run, replacing the current
1299    /// worst individuals.
1300    ///
1301    /// Each migrant is evaluated with this GA's own fitness function (so a genome
1302    /// that emigrated from another island is scored under the receiving island's
1303    /// objective) and overwrites one of the worst individuals in the population.
1304    /// The best-so-far individual is refreshed and the evaluation counter is
1305    /// advanced by the number of migrants accepted. Used to build island-model
1306    /// migration on top of the incremental stepping API (AUDIT EV-77).
1307    pub fn inject_migrants(&self, state: &mut SimpleGaRun<G, F>, migrants: Vec<G>) {
1308        if migrants.is_empty() {
1309            return;
1310        }
1311
1312        // Sort best-first so the worst individuals sit at the tail.
1313        state.population.sort_by_fitness();
1314        let n = state.population.len();
1315        if n == 0 {
1316            return;
1317        }
1318        let k = migrants.len().min(n);
1319
1320        let evaluated: Vec<Individual<G, F>> = migrants
1321            .into_iter()
1322            .take(k)
1323            .map(|genome| {
1324                let value = self.fitness.evaluate(&genome);
1325                Individual::with_fitness(genome, value)
1326            })
1327            .collect();
1328
1329        {
1330            let individuals = state.population.individuals_mut();
1331            for (i, individual) in evaluated.into_iter().enumerate() {
1332                let idx = n - 1 - i; // replace worst-first
1333                individuals[idx] = individual;
1334            }
1335        }
1336        state.evaluations += k;
1337
1338        // Refresh the tracked best individual.
1339        if let Some(best) = state.population.best() {
1340            if best.is_better_than(&state.best_individual) {
1341                state.best_individual = best.clone();
1342            }
1343        }
1344    }
1345}
1346
1347/// Algorithm-level checkpoint/resume for bit-identical continuation (EV-02).
1348///
1349/// These build the library-provided resume path the checkpoint primitives were
1350/// missing: instead of hand-rolling the generation loop (as the example and
1351/// integration test previously had to), a caller drives an incremental run with
1352/// [`SimpleGA::init_run`]/[`SimpleGA::step_generation`], snapshots it with
1353/// [`SimpleGA::checkpoint_run`] (capturing a [`SnapshotRng`](crate::checkpoint::SnapshotRng)),
1354/// and later restores it with [`SimpleGA::resume`] / [`SimpleGA::run_from_checkpoint`].
1355/// Because the ChaCha RNG state is captured and restored, resuming is
1356/// bit-identical to an uninterrupted run.
1357///
1358/// Constrained to `f64` fitness because [`Checkpoint`](crate::checkpoint::Checkpoint)
1359/// serializes `Individual<G>` (fitness value `f64`).
1360impl<G, S, C, M, Fit, Term> SimpleGA<G, f64, S, C, M, Fit, Term>
1361where
1362    G: EvolutionaryGenome,
1363    S: SelectionOperator<G>,
1364    C: CrossoverOperator<G>,
1365    M: MutationOperator<G>,
1366    Fit: Fitness<Genome = G, Value = f64>,
1367    Term: TerminationCriterion<G, f64>,
1368{
1369    /// Capture an in-progress incremental run into a [`Checkpoint`](crate::checkpoint::Checkpoint)
1370    /// for bit-identical resume (EV-02).
1371    ///
1372    /// Serializes the population (with its generation counter), the tracked best
1373    /// individual, the evaluation count and the per-generation statistics, and
1374    /// captures the complete state of a [`SnapshotRng`](crate::checkpoint::SnapshotRng)
1375    /// (the ChaCha family). Restoring the checkpoint via [`SimpleGA::resume`] and
1376    /// continuing with [`SimpleGA::step_generation`] reproduces the exact
1377    /// trajectory an uninterrupted run would have taken.
1378    pub fn checkpoint_run<R>(
1379        &self,
1380        state: &SimpleGaRun<G, f64>,
1381        rng: &R,
1382    ) -> Result<crate::checkpoint::Checkpoint<G>, crate::error::CheckpointError>
1383    where
1384        R: crate::checkpoint::SnapshotRng,
1385    {
1386        let individuals: Vec<Individual<G>> = state.population.iter().cloned().collect();
1387        crate::checkpoint::Checkpoint::new(state.generation(), individuals)
1388            .with_evaluations(state.evaluations())
1389            .with_best(state.best_individual.clone())
1390            .with_statistics(state.stats.generations.clone())
1391            .with_rng(rng)
1392    }
1393
1394    /// Resume an incremental run from a [`Checkpoint`](crate::checkpoint::Checkpoint),
1395    /// restoring the population, best individual, evaluation count, statistics AND
1396    /// the captured [`SnapshotRng`](crate::checkpoint::SnapshotRng) (EV-02).
1397    ///
1398    /// Returns the reconstructed [`SimpleGaRun`] and the restored RNG; drive it
1399    /// forward with [`SimpleGA::step_generation`]/[`SimpleGA::finish_run`] (or use
1400    /// [`SimpleGA::run_from_checkpoint`] to continue straight to termination). The
1401    /// checkpoint MUST have been created with a captured RNG (via
1402    /// [`SimpleGA::checkpoint_run`] / [`Checkpoint::with_rng`](crate::checkpoint::Checkpoint::with_rng));
1403    /// otherwise this returns [`CheckpointError::Corrupted`](crate::error::CheckpointError::Corrupted),
1404    /// because bit-identical resume is impossible without the RNG state.
1405    pub fn resume<R>(
1406        &self,
1407        checkpoint: &crate::checkpoint::Checkpoint<G>,
1408    ) -> Result<(SimpleGaRun<G, f64>, R), crate::error::CheckpointError>
1409    where
1410        R: crate::checkpoint::SnapshotRng,
1411    {
1412        let rng = checkpoint.restore_rng::<R>()?.ok_or_else(|| {
1413            crate::error::CheckpointError::Corrupted(
1414                "checkpoint has no captured RNG state; bit-identical resume requires a \
1415                 SnapshotRng captured via SimpleGA::checkpoint_run / Checkpoint::with_rng"
1416                    .to_string(),
1417            )
1418        })?;
1419
1420        // Reconstruct the population and restore its generation counter.
1421        let mut population: Population<G, f64> =
1422            Population::with_capacity(checkpoint.population.len());
1423        for ind in &checkpoint.population {
1424            population.push(ind.clone());
1425        }
1426        population.set_generation(checkpoint.generation);
1427
1428        // Restore the tracked best (fall back to the population's best if absent).
1429        let best_individual = match &checkpoint.best {
1430            Some(best) => best.clone(),
1431            None => population.best().cloned().ok_or_else(|| {
1432                crate::error::CheckpointError::Corrupted("empty population".to_string())
1433            })?,
1434        };
1435
1436        // Rebuild statistics and the best-fitness history from the recorded
1437        // per-generation stats, so termination criteria that read history (e.g.
1438        // stagnation) behave identically to an uninterrupted run.
1439        let mut stats = EvolutionStats::new();
1440        stats.generations = checkpoint.statistics.clone();
1441        let fitness_history: Vec<f64> = checkpoint
1442            .statistics
1443            .iter()
1444            .map(|g| g.best_fitness)
1445            .collect();
1446
1447        let state = SimpleGaRun {
1448            population,
1449            best_individual,
1450            evaluations: checkpoint.evaluations,
1451            fitness_history,
1452            stats,
1453            start_time: Instant::now(),
1454            terminated: false,
1455        };
1456        Ok((state, rng))
1457    }
1458
1459    /// Resume from a [`Checkpoint`](crate::checkpoint::Checkpoint) and run to
1460    /// termination, returning the final [`EvolutionResult`] (EV-02).
1461    ///
1462    /// A convenience over [`SimpleGA::resume`] followed by repeated
1463    /// [`SimpleGA::step_generation`] and [`SimpleGA::finish_run`]. Because the
1464    /// captured [`SnapshotRng`](crate::checkpoint::SnapshotRng) is restored,
1465    /// this yields the bit-identical result of an uninterrupted `run` for the same
1466    /// seed and configuration.
1467    pub fn run_from_checkpoint<R>(
1468        &self,
1469        checkpoint: &crate::checkpoint::Checkpoint<G>,
1470    ) -> Result<EvolutionResult<G, f64>, EvolutionError>
1471    where
1472        R: crate::checkpoint::SnapshotRng + Rng,
1473    {
1474        let (mut state, mut rng) = self.resume::<R>(checkpoint)?;
1475        while self.step_generation(&mut state, &mut rng)? {}
1476        Ok(self.finish_run(state))
1477    }
1478}
1479
1480/// Mutable state for driving a [`SimpleGA`] one generation at a time.
1481///
1482/// Produced by [`SimpleGA::init_run`], advanced by
1483/// [`SimpleGA::step_generation`], and consumed by [`SimpleGA::finish_run`]. This
1484/// is the incremental counterpart to [`SimpleGA::run`] (AUDIT EV-34): callers can
1485/// drive the generation loop, read progress via the getters below, and cancel
1486/// early — all without changing `run`'s behavior, since `run` is implemented in
1487/// terms of these methods.
1488///
1489/// Available in all builds (not gated on the `parallel` feature) so the
1490/// checkpoint resume API (EV-02) and WASM stepping bindings (EV-34) work
1491/// regardless of feature flags.
1492pub struct SimpleGaRun<G, F = f64>
1493where
1494    G: EvolutionaryGenome,
1495    F: FitnessValue,
1496{
1497    population: Population<G, F>,
1498    best_individual: Individual<G, F>,
1499    evaluations: usize,
1500    fitness_history: Vec<f64>,
1501    stats: EvolutionStats,
1502    start_time: Instant,
1503    terminated: bool,
1504}
1505
1506impl<G, F> SimpleGaRun<G, F>
1507where
1508    G: EvolutionaryGenome,
1509    F: FitnessValue,
1510{
1511    /// Number of generations completed so far.
1512    pub fn generation(&self) -> usize {
1513        self.population.generation()
1514    }
1515
1516    /// Total fitness evaluations performed so far.
1517    pub fn evaluations(&self) -> usize {
1518        self.evaluations
1519    }
1520
1521    /// Best fitness found so far (as `f64`).
1522    pub fn best_fitness(&self) -> f64 {
1523        self.best_individual.fitness_value().to_f64()
1524    }
1525
1526    /// The best genome found so far.
1527    pub fn best_genome(&self) -> &G {
1528        &self.best_individual.genome
1529    }
1530
1531    /// Per-generation best-so-far fitness trajectory (index 0 is generation 0).
1532    pub fn fitness_history(&self) -> &[f64] {
1533        &self.fitness_history
1534    }
1535
1536    /// `true` once the termination criterion has fired.
1537    pub fn is_terminated(&self) -> bool {
1538        self.terminated
1539    }
1540
1541    /// Clone the best `k` genomes in the current population (best first).
1542    ///
1543    /// Used to select emigrants for island-model migration (AUDIT EV-77).
1544    pub fn best_genomes(&self, k: usize) -> Vec<G> {
1545        let mut sorted = self.population.clone();
1546        sorted.sort_by_fitness();
1547        sorted
1548            .iter()
1549            .take(k)
1550            .map(|ind| ind.genome.clone())
1551            .collect()
1552    }
1553}
1554
1555// Bounded operators version - non-parallel without Send + Sync
1556#[cfg(not(feature = "parallel"))]
1557impl<G, F, S, C, M, Fit, Term> SimpleGA<G, F, S, C, M, Fit, Term>
1558where
1559    G: EvolutionaryGenome,
1560    F: FitnessValue,
1561    S: SelectionOperator<G>,
1562    C: BoundedCrossoverOperator<G>,
1563    M: BoundedMutationOperator<G>,
1564    Fit: Fitness<Genome = G, Value = F>,
1565    Term: TerminationCriterion<G, F>,
1566{
1567    /// Run the genetic algorithm with bounded operators
1568    pub fn run_bounded<R: Rng>(
1569        &self,
1570        rng: &mut R,
1571    ) -> Result<EvolutionResult<G, F>, EvolutionError> {
1572        let start_time = Instant::now();
1573
1574        // Initialize population
1575        let mut population: Population<G, F> =
1576            Population::random(self.config.population_size, &self.bounds, rng);
1577
1578        // Evaluate initial population (sequential only)
1579        population.evaluate(&self.fitness);
1580
1581        let mut stats = EvolutionStats::new();
1582        let mut evaluations = population.len();
1583        let mut fitness_history: Vec<f64> = Vec::new();
1584
1585        // Track best individual
1586        let mut best_individual = population
1587            .best()
1588            .ok_or(EvolutionError::EmptyPopulation)?
1589            .clone();
1590
1591        // Record initial statistics
1592        let gen_stats = GenerationStats::from_population(&population, 0, evaluations);
1593        fitness_history.push(gen_stats.best_fitness);
1594        stats.record(gen_stats);
1595
1596        // Main evolution loop
1597        loop {
1598            // Check termination
1599            let state = EvolutionState {
1600                generation: population.generation(),
1601                evaluations,
1602                best_fitness: best_individual.fitness_value().to_f64(),
1603                population: &population,
1604                fitness_history: &fitness_history,
1605            };
1606
1607            if self.termination.should_terminate(&state) {
1608                stats.set_termination_reason(self.termination.reason());
1609                break;
1610            }
1611
1612            // Create new generation
1613            let mut new_population: Population<G, F> =
1614                Population::with_capacity(self.config.population_size);
1615
1616            // Elitism
1617            if self.config.elitism {
1618                let mut sorted = population.clone();
1619                sorted.sort_by_fitness();
1620                for i in 0..self.config.elite_count.min(sorted.len()) {
1621                    new_population.push(sorted[i].clone());
1622                }
1623            }
1624
1625            let selection_pool: Vec<(G, f64)> = population.as_fitness_pairs();
1626
1627            while new_population.len() < self.config.population_size {
1628                let parent1_idx = self.selection.select(&selection_pool, rng);
1629                let parent2_idx = self.selection.select(&selection_pool, rng);
1630
1631                let parent1 = &selection_pool[parent1_idx].0;
1632                let parent2 = &selection_pool[parent2_idx].0;
1633
1634                let (mut child1, mut child2) =
1635                    if rng.gen::<f64>() < self.config.crossover_probability {
1636                        match self
1637                            .crossover
1638                            .crossover_bounded(parent1, parent2, &self.bounds, rng)
1639                            .genome()
1640                        {
1641                            Some((c1, c2)) => (c1, c2),
1642                            None => (parent1.clone(), parent2.clone()),
1643                        }
1644                    } else {
1645                        (parent1.clone(), parent2.clone())
1646                    };
1647
1648                self.mutation.mutate_bounded(&mut child1, &self.bounds, rng);
1649                self.mutation.mutate_bounded(&mut child2, &self.bounds, rng);
1650
1651                new_population.push(Individual::with_generation(
1652                    child1,
1653                    population.generation() + 1,
1654                ));
1655                if new_population.len() < self.config.population_size {
1656                    new_population.push(Individual::with_generation(
1657                        child2,
1658                        population.generation() + 1,
1659                    ));
1660                }
1661            }
1662
1663            while new_population.len() > self.config.population_size {
1664                new_population.pop();
1665            }
1666
1667            // Evaluate (sequential only)
1668            new_population.evaluate(&self.fitness);
1669            evaluations += new_population.len()
1670                - (if self.config.elitism {
1671                    self.config.elite_count
1672                } else {
1673                    0
1674                });
1675
1676            new_population.set_generation(population.generation() + 1);
1677            population = new_population;
1678
1679            if let Some(best) = population.best() {
1680                if best.is_better_than(&best_individual) {
1681                    best_individual = best.clone();
1682                }
1683            }
1684
1685            let gen_stats =
1686                GenerationStats::from_population(&population, population.generation(), evaluations);
1687            fitness_history.push(gen_stats.best_fitness);
1688            stats.record(gen_stats);
1689        }
1690
1691        stats.set_runtime(start_time.elapsed());
1692
1693        Ok(EvolutionResult::new(
1694            best_individual.genome,
1695            best_individual.fitness.unwrap(),
1696            population.generation(),
1697            evaluations,
1698        )
1699        .with_stats(stats))
1700    }
1701}
1702
1703#[cfg(test)]
1704mod tests {
1705    use super::*;
1706    use crate::fitness::benchmarks::{OneMax, Sphere};
1707    use crate::genome::traits::RealValuedGenome;
1708    use crate::operators::crossover::{SbxCrossover, UniformCrossover};
1709    use crate::operators::mutation::{BitFlipMutation, GaussianMutation, PolynomialMutation};
1710    use crate::operators::selection::TournamentSelection;
1711    use crate::termination::TargetFitness;
1712
1713    #[test]
1714    fn test_simple_ga_builder() {
1715        let bounds = MultiBounds::symmetric(5.0, 10);
1716        let ga = SimpleGABuilder::new()
1717            .population_size(50)
1718            .bounds(bounds)
1719            .selection(TournamentSelection::new(3))
1720            .crossover(SbxCrossover::new(20.0))
1721            .mutation(PolynomialMutation::new(20.0))
1722            .fitness(Sphere::new(10))
1723            .max_generations(10)
1724            .build();
1725
1726        assert!(ga.is_ok());
1727    }
1728
1729    #[test]
1730    fn test_simple_ga_missing_bounds() {
1731        // Test that build() returns an error when bounds are missing
1732        // Note: With the type-safe builder, operators must be provided to call build()
1733        // but bounds can be missing (they're an Option in the builder)
1734        let ga = SimpleGABuilder::new()
1735            .population_size(50)
1736            // bounds are missing
1737            .selection(TournamentSelection::new(3))
1738            .crossover(SbxCrossover::new(20.0))
1739            .mutation(PolynomialMutation::new(20.0))
1740            .fitness(Sphere::new(10))
1741            .max_generations(10)
1742            .build();
1743
1744        assert!(ga.is_err());
1745        if let Err(e) = ga {
1746            assert!(e.to_string().contains("Bounds"));
1747        }
1748    }
1749
1750    #[test]
1751    fn test_simple_ga_sphere() {
1752        let mut rng = rand::thread_rng();
1753        let bounds = MultiBounds::symmetric(5.12, 10);
1754
1755        let ga = SimpleGABuilder::new()
1756            .population_size(50)
1757            .bounds(bounds)
1758            .selection(TournamentSelection::new(3))
1759            .crossover(SbxCrossover::new(20.0))
1760            .mutation(PolynomialMutation::new(20.0))
1761            .fitness(Sphere::new(10))
1762            .max_generations(100)
1763            .build()
1764            .unwrap();
1765
1766        let result = ga.run(&mut rng).unwrap();
1767
1768        // Should find some improvement from random initialization
1769        // Initial random values in [-5.12, 5.12] have expected fitness around -52 per dimension
1770        // so total ~-520. Even modest improvement should get to -200 or better.
1771        assert!(
1772            result.best_fitness > -200.0,
1773            "Expected fitness > -200, got {}",
1774            result.best_fitness
1775        ); // Sphere is negated, so closer to 0 is better
1776        assert!(result.generations <= 100);
1777        assert!(result.evaluations > 0);
1778    }
1779
1780    // regression: EV-34 — the incremental stepping API (init_run / step_generation
1781    // / finish_run) must reproduce run() exactly, since run() is implemented in
1782    // terms of it. Only compiled in the non-parallel build where the API exists.
1783    #[cfg(not(feature = "parallel"))]
1784    #[test]
1785    fn test_step_api_matches_run() {
1786        use rand::SeedableRng;
1787
1788        let build = || {
1789            SimpleGABuilder::new()
1790                .population_size(30)
1791                .bounds(MultiBounds::symmetric(5.12, 6))
1792                .selection(TournamentSelection::new(3))
1793                .crossover(SbxCrossover::new(20.0))
1794                .mutation(PolynomialMutation::new(20.0))
1795                .fitness(Sphere::new(6))
1796                .max_generations(25)
1797                .build()
1798                .unwrap()
1799        };
1800
1801        // One-shot run().
1802        let ga_a = build();
1803        let mut rng_a = rand::rngs::StdRng::seed_from_u64(2024);
1804        let run_result = ga_a.run(&mut rng_a).unwrap();
1805
1806        // Manual stepping with the same seed.
1807        let ga_b = build();
1808        let mut rng_b = rand::rngs::StdRng::seed_from_u64(2024);
1809        let mut state = ga_b.init_run(&mut rng_b).unwrap();
1810        let mut steps = 0;
1811        while ga_b.step_generation(&mut state, &mut rng_b).unwrap() {
1812            steps += 1;
1813        }
1814        assert!(state.is_terminated());
1815        assert_eq!(steps, 25, "should take exactly max_generations steps");
1816        let step_result = ga_b.finish_run(state);
1817
1818        assert_eq!(run_result.generations, step_result.generations);
1819        assert_eq!(run_result.evaluations, step_result.evaluations);
1820        assert_eq!(run_result.best_fitness, step_result.best_fitness);
1821        assert_eq!(
1822            run_result.best_genome.genes(),
1823            step_result.best_genome.genes()
1824        );
1825    }
1826
1827    // regression: EV-77 — migration helpers used by the WASM island model.
1828    #[cfg(not(feature = "parallel"))]
1829    #[test]
1830    fn test_inject_migrants_replaces_worst() {
1831        use rand::SeedableRng;
1832
1833        let ga = SimpleGABuilder::new()
1834            .population_size(20)
1835            .bounds(MultiBounds::symmetric(5.12, 4))
1836            .selection(TournamentSelection::new(3))
1837            .crossover(SbxCrossover::new(20.0))
1838            .mutation(PolynomialMutation::new(20.0))
1839            .fitness(Sphere::new(4))
1840            .max_generations(10)
1841            .build()
1842            .unwrap();
1843
1844        let mut rng = rand::rngs::StdRng::seed_from_u64(7);
1845        let mut state = ga.init_run(&mut rng).unwrap();
1846        for _ in 0..3 {
1847            ga.step_generation(&mut state, &mut rng).unwrap();
1848        }
1849
1850        let evals_before = state.evaluations();
1851        // The optimum for the (negated) Sphere is the origin; inject it and the
1852        // best-so-far must improve to ~0 and the evaluation count must rise.
1853        let optimum = RealVector::new(vec![0.0; 4]);
1854        ga.inject_migrants(&mut state, vec![optimum]);
1855
1856        assert_eq!(state.evaluations(), evals_before + 1);
1857        assert!(
1858            state.best_fitness() > -1e-9,
1859            "injected optimum should become the best (got {})",
1860            state.best_fitness()
1861        );
1862
1863        // best_genomes returns clones of the top individuals.
1864        let top = state.best_genomes(2);
1865        assert_eq!(top.len(), 2);
1866    }
1867
1868    #[test]
1869    fn test_simple_ga_onemax() {
1870        let mut rng = rand::thread_rng();
1871        let bounds = MultiBounds::uniform(crate::genome::bounds::Bounds::unit(), 20);
1872
1873        let ga = SimpleGABuilder::new()
1874            .population_size(50)
1875            .bounds(bounds)
1876            .selection(TournamentSelection::new(3))
1877            .crossover(UniformCrossover::new())
1878            .mutation(BitFlipMutation::new())
1879            .fitness(OneMax::new(20))
1880            .max_generations(50)
1881            .build()
1882            .unwrap();
1883
1884        let result = ga.run(&mut rng).unwrap();
1885
1886        // Should find perfect or near-perfect solution
1887        assert!(result.best_fitness >= 15); // At least 75% ones
1888    }
1889
1890    #[test]
1891    fn test_simple_ga_target_fitness() {
1892        let mut rng = rand::thread_rng();
1893        let bounds = MultiBounds::symmetric(5.12, 5);
1894
1895        let ga = SimpleGABuilder::new()
1896            .population_size(100)
1897            .bounds(bounds)
1898            .selection(TournamentSelection::new(5))
1899            .crossover(SbxCrossover::new(20.0))
1900            .mutation(PolynomialMutation::new(20.0))
1901            .fitness(Sphere::new(5))
1902            .termination(TargetFitness::with_tolerance(0.0, 0.1)) // Near 0 (optimal)
1903            .build()
1904            .unwrap();
1905
1906        let result = ga.run(&mut rng).unwrap();
1907
1908        // Should reach target
1909        assert!(result.best_fitness >= -0.1);
1910        assert_eq!(
1911            result.stats.termination_reason.as_deref(),
1912            Some("Target fitness reached")
1913        );
1914    }
1915
1916    #[test]
1917    fn test_simple_ga_bounded() {
1918        let mut rng = rand::thread_rng();
1919        let bounds = MultiBounds::symmetric(5.12, 10);
1920
1921        let ga = SimpleGABuilder::new()
1922            .population_size(50)
1923            .bounds(bounds.clone())
1924            .selection(TournamentSelection::new(3))
1925            .crossover(SbxCrossover::new(20.0))
1926            .mutation(PolynomialMutation::new(20.0))
1927            .fitness(Sphere::new(10))
1928            .max_generations(50)
1929            .build()
1930            .unwrap();
1931
1932        let result = ga.run_bounded(&mut rng).unwrap();
1933
1934        // All genes should be within bounds
1935        for gene in result.best_genome.genes() {
1936            assert!(*gene >= -5.12 && *gene <= 5.12);
1937        }
1938    }
1939
1940    #[test]
1941    fn test_simple_ga_elitism() {
1942        let mut rng = rand::thread_rng();
1943        let bounds = MultiBounds::symmetric(5.12, 5);
1944
1945        // Run without elitism
1946        let ga_no_elite = SimpleGABuilder::new()
1947            .population_size(20)
1948            .elitism(false)
1949            .bounds(bounds.clone())
1950            .selection(TournamentSelection::new(2))
1951            .crossover(SbxCrossover::new(20.0))
1952            .mutation(PolynomialMutation::new(20.0))
1953            .fitness(Sphere::new(5))
1954            .max_generations(20)
1955            .build()
1956            .unwrap();
1957
1958        // Run with elitism
1959        let ga_elite = SimpleGABuilder::new()
1960            .population_size(20)
1961            .elitism(true)
1962            .elite_count(2)
1963            .bounds(bounds)
1964            .selection(TournamentSelection::new(2))
1965            .crossover(SbxCrossover::new(20.0))
1966            .mutation(PolynomialMutation::new(20.0))
1967            .fitness(Sphere::new(5))
1968            .max_generations(20)
1969            .build()
1970            .unwrap();
1971
1972        // Both should run without error
1973        let result_no_elite = ga_no_elite.run(&mut rng);
1974        let result_elite = ga_elite.run(&mut rng);
1975
1976        assert!(result_no_elite.is_ok());
1977        assert!(result_elite.is_ok());
1978    }
1979
1980    #[test]
1981    fn test_simple_ga_statistics() {
1982        let mut rng = rand::thread_rng();
1983        let bounds = MultiBounds::symmetric(5.12, 5);
1984
1985        let ga = SimpleGABuilder::new()
1986            .population_size(20)
1987            .bounds(bounds)
1988            .selection(TournamentSelection::new(2))
1989            .crossover(SbxCrossover::new(20.0))
1990            .mutation(PolynomialMutation::new(20.0))
1991            .fitness(Sphere::new(5))
1992            .max_generations(10)
1993            .build()
1994            .unwrap();
1995
1996        let result = ga.run(&mut rng).unwrap();
1997
1998        // Check statistics were collected
1999        assert_eq!(result.stats.num_generations(), 11); // Initial + 10 generations
2000        assert!(result.stats.total_runtime_ms > 0.0);
2001
2002        // Best fitness should improve or stay the same
2003        let history = result.stats.best_fitness_history();
2004        for i in 1..history.len() {
2005            assert!(history[i] >= history[i - 1] - 0.001); // Allow small numerical error
2006        }
2007    }
2008
2009    /// regression: EV-35 — the quickstart must work with ZERO turbofish via the
2010    /// `real_valued()` entry point (pre-fix, the only path was the 7-parameter
2011    /// `SimpleGABuilder::<RealVector, f64, _, _, _, _, _>::new()` turbofish form).
2012    #[test]
2013    fn test_real_valued_constructor_no_turbofish() {
2014        let mut rng = rand::thread_rng();
2015        let ga = SimpleGABuilder::real_valued()
2016            .population_size(40)
2017            .bounds(MultiBounds::symmetric(5.12, 10))
2018            .fitness(Sphere::new(10))
2019            .max_generations(20)
2020            .build()
2021            .unwrap();
2022        let result = ga.run(&mut rng).unwrap();
2023        assert!(result.evaluations > 0);
2024    }
2025
2026    /// regression: EV-35 — a default operator installed by `real_valued()` remains
2027    /// overridable (calling `.mutation(...)` swaps it and still builds).
2028    #[test]
2029    fn test_real_valued_constructor_override_operator() {
2030        let mut rng = rand::thread_rng();
2031        let ga = SimpleGABuilder::real_valued()
2032            .mutation(GaussianMutation::new(0.1))
2033            .population_size(30)
2034            .bounds(MultiBounds::symmetric(5.12, 5))
2035            .fitness(Sphere::new(5))
2036            .max_generations(10)
2037            .build()
2038            .unwrap();
2039        assert!(ga.run(&mut rng).is_ok());
2040    }
2041
2042    /// regression: EV-35 — bit-string quickstart with zero turbofish.
2043    #[test]
2044    fn test_bit_string_constructor_no_turbofish() {
2045        let mut rng = rand::thread_rng();
2046        let ga = SimpleGABuilder::bit_string()
2047            .population_size(40)
2048            .bounds(MultiBounds::uniform(
2049                crate::genome::bounds::Bounds::unit(),
2050                20,
2051            ))
2052            .fitness(OneMax::new(20))
2053            .max_generations(30)
2054            .build()
2055            .unwrap();
2056        let result = ga.run(&mut rng).unwrap();
2057        assert!(result.best_fitness >= 10);
2058    }
2059
2060    /// regression: EV-35 — permutation quickstart with zero turbofish.
2061    #[test]
2062    fn test_permutation_constructor_no_turbofish() {
2063        use crate::fitness::traits::FnFitness;
2064        use crate::genome::permutation::Permutation;
2065        use crate::genome::traits::PermutationGenome;
2066
2067        let mut rng = rand::thread_rng();
2068        // Maximized when the permutation equals the identity (sum of |v - i| = 0).
2069        let fitness = FnFitness::new(|p: &Permutation| -> f64 {
2070            -(p.permutation()
2071                .iter()
2072                .enumerate()
2073                .map(|(i, &v)| (v as f64 - i as f64).abs())
2074                .sum::<f64>())
2075        });
2076        let ga = SimpleGABuilder::permutation()
2077            .population_size(30)
2078            .bounds(MultiBounds::symmetric(1.0, 8))
2079            .fitness(fitness)
2080            .max_generations(10)
2081            .build()
2082            .unwrap();
2083        assert!(ga.run(&mut rng).is_ok());
2084    }
2085
2086    /// regression: EV-86 — build() must reject an invalid config with a clear
2087    /// typed `Configuration` error rather than proceeding or panicking.
2088    #[test]
2089    fn test_build_rejects_zero_population() {
2090        let ga = SimpleGABuilder::real_valued()
2091            .population_size(0)
2092            .bounds(MultiBounds::symmetric(5.12, 10))
2093            .fitness(Sphere::new(10))
2094            .max_generations(10)
2095            .build();
2096        assert!(ga.is_err());
2097        let msg = ga.err().unwrap().to_string();
2098        assert!(msg.contains("population_size"), "got: {msg}");
2099    }
2100
2101    /// regression: EV-86 — build() validates the crossover probability range.
2102    #[test]
2103    fn test_build_rejects_out_of_range_crossover_probability() {
2104        let ga = SimpleGABuilder::real_valued()
2105            .crossover_probability(1.5)
2106            .population_size(20)
2107            .bounds(MultiBounds::symmetric(5.12, 5))
2108            .fitness(Sphere::new(5))
2109            .max_generations(5)
2110            .build();
2111        assert!(ga.is_err());
2112        assert!(ga
2113            .err()
2114            .unwrap()
2115            .to_string()
2116            .contains("crossover_probability"));
2117    }
2118
2119    /// regression: EV-86 — elite_count exceeding population_size is rejected.
2120    #[test]
2121    fn test_build_rejects_elite_count_exceeding_population() {
2122        let ga = SimpleGABuilder::real_valued()
2123            .population_size(10)
2124            .elite_count(50)
2125            .bounds(MultiBounds::symmetric(5.12, 5))
2126            .fitness(Sphere::new(5))
2127            .max_generations(5)
2128            .build();
2129        assert!(ga.is_err());
2130        assert!(ga.err().unwrap().to_string().contains("elite_count"));
2131    }
2132
2133    /// regression: EV-21 — the adaptive tuner is actually wired into the GA: after
2134    /// `run_adaptive` the tuner has received improvement feedback (pre-fix, no
2135    /// algorithm consumed the learner at all).
2136    #[test]
2137    fn test_run_adaptive_feeds_tuner() {
2138        use crate::hyperparameter::bayesian::{ThompsonConfig, PARAM_MUTATION_RATE};
2139
2140        let mut rng = rand::thread_rng();
2141        let mut ga = SimpleGABuilder::real_valued()
2142            .population_size(30)
2143            .bounds(MultiBounds::symmetric(5.12, 8))
2144            .fitness(Sphere::new(8))
2145            .max_generations(15)
2146            .adaptive_operators(ThompsonConfig::default())
2147            .build()
2148            .unwrap();
2149
2150        let result = ga.run_adaptive(&mut rng).unwrap();
2151        assert!(result.evaluations > 0);
2152
2153        let tuner = ga
2154            .tuner()
2155            .expect("tuner should be present after run_adaptive");
2156        assert!(
2157            tuner.total_observations() > 0,
2158            "tuner must receive improvement feedback"
2159        );
2160        let mr = tuner.parameter(PARAM_MUTATION_RATE).unwrap();
2161        assert!(
2162            mr.total_observations() > 0.0,
2163            "mutation-rate arms must accumulate observations"
2164        );
2165    }
2166}