Skip to main content

fugue_evo/algorithms/
steady_state.rs

1//! Steady-State Genetic Algorithm
2//!
3//! This module implements a steady-state genetic algorithm where only a small
4//! number of individuals are replaced in each generation (typically 1 or 2),
5//! rather than replacing the entire population.
6
7use std::time::Instant;
8
9use rand::Rng;
10
11use crate::diagnostics::{EvolutionResult, EvolutionStats, GenerationStats, TimingStats};
12use crate::error::EvolutionError;
13use crate::fitness::traits::{Fitness, FitnessValue};
14use crate::genome::bounds::MultiBounds;
15use crate::genome::traits::EvolutionaryGenome;
16use crate::operators::traits::{
17    BoundedCrossoverOperator, BoundedMutationOperator, CrossoverOperator, MutationOperator,
18    SelectionOperator,
19};
20use crate::population::individual::Individual;
21use crate::population::population::Population;
22use crate::termination::{EvolutionState, MaxGenerations, TerminationCriterion};
23
24/// Replacement strategy for steady-state GA
25#[derive(Clone, Debug, Default)]
26pub enum ReplacementStrategy {
27    /// Replace the worst individual(s) in the population
28    #[default]
29    ReplaceWorst,
30    /// Replace a randomly selected individual
31    ReplaceRandom,
32    /// Replace the parent if offspring is better (generational replacement)
33    ReplaceParent,
34    /// Use tournament selection to choose individual to replace (inverse tournament)
35    TournamentWorst(usize),
36}
37
38/// Configuration for the Steady-State GA
39#[derive(Clone, Debug)]
40pub struct SteadyStateConfig {
41    /// Population size
42    pub population_size: usize,
43    /// Number of offspring to generate per step (typically 1 or 2)
44    pub offspring_count: usize,
45    /// Crossover probability
46    pub crossover_probability: f64,
47    /// Replacement strategy
48    pub replacement: ReplacementStrategy,
49    /// Whether to evaluate in parallel
50    pub parallel_evaluation: bool,
51    /// Number of steps per "generation" for statistics reporting
52    pub steps_per_generation: usize,
53}
54
55impl Default for SteadyStateConfig {
56    fn default() -> Self {
57        Self {
58            population_size: 100,
59            offspring_count: 2,
60            crossover_probability: 0.9,
61            replacement: ReplacementStrategy::ReplaceWorst,
62            parallel_evaluation: false,
63            steps_per_generation: 50, // Report stats every 50 replacements
64        }
65    }
66}
67
68/// Builder for SteadyStateGA
69pub struct SteadyStateBuilder<G, F, S, C, M, Fit, Term>
70where
71    G: EvolutionaryGenome,
72    F: FitnessValue,
73{
74    config: SteadyStateConfig,
75    bounds: Option<MultiBounds>,
76    selection: Option<S>,
77    crossover: Option<C>,
78    mutation: Option<M>,
79    fitness: Option<Fit>,
80    termination: Option<Term>,
81    _phantom: std::marker::PhantomData<(G, F)>,
82}
83
84impl<G, F> SteadyStateBuilder<G, F, (), (), (), (), ()>
85where
86    G: EvolutionaryGenome,
87    F: FitnessValue,
88{
89    /// Create a new builder with default configuration
90    pub fn new() -> Self {
91        Self {
92            config: SteadyStateConfig::default(),
93            bounds: None,
94            selection: None,
95            crossover: None,
96            mutation: None,
97            fitness: None,
98            termination: None,
99            _phantom: std::marker::PhantomData,
100        }
101    }
102}
103
104impl<G, F> Default for SteadyStateBuilder<G, F, (), (), (), (), ()>
105where
106    G: EvolutionaryGenome,
107    F: FitnessValue,
108{
109    fn default() -> Self {
110        Self::new()
111    }
112}
113
114impl<G, F, S, C, M, Fit, Term> SteadyStateBuilder<G, F, S, C, M, Fit, Term>
115where
116    G: EvolutionaryGenome,
117    F: FitnessValue,
118{
119    /// Set the population size
120    pub fn population_size(mut self, size: usize) -> Self {
121        self.config.population_size = size;
122        self
123    }
124
125    /// Set the number of offspring per step
126    pub fn offspring_count(mut self, count: usize) -> Self {
127        self.config.offspring_count = count;
128        self
129    }
130
131    /// Set the crossover probability
132    pub fn crossover_probability(mut self, probability: f64) -> Self {
133        self.config.crossover_probability = probability;
134        self
135    }
136
137    /// Set the replacement strategy
138    pub fn replacement(mut self, strategy: ReplacementStrategy) -> Self {
139        self.config.replacement = strategy;
140        self
141    }
142
143    /// Enable or disable parallel evaluation
144    pub fn parallel_evaluation(mut self, enabled: bool) -> Self {
145        self.config.parallel_evaluation = enabled;
146        self
147    }
148
149    /// Set the number of steps per generation (for statistics)
150    pub fn steps_per_generation(mut self, steps: usize) -> Self {
151        self.config.steps_per_generation = steps;
152        self
153    }
154
155    /// Set the search space bounds
156    pub fn bounds(mut self, bounds: MultiBounds) -> Self {
157        self.bounds = Some(bounds);
158        self
159    }
160
161    /// Set the selection operator
162    pub fn selection<NewS>(self, selection: NewS) -> SteadyStateBuilder<G, F, NewS, C, M, Fit, Term>
163    where
164        NewS: SelectionOperator<G>,
165    {
166        SteadyStateBuilder {
167            config: self.config,
168            bounds: self.bounds,
169            selection: Some(selection),
170            crossover: self.crossover,
171            mutation: self.mutation,
172            fitness: self.fitness,
173            termination: self.termination,
174            _phantom: std::marker::PhantomData,
175        }
176    }
177
178    /// Set the crossover operator
179    pub fn crossover<NewC>(self, crossover: NewC) -> SteadyStateBuilder<G, F, S, NewC, M, Fit, Term>
180    where
181        NewC: CrossoverOperator<G>,
182    {
183        SteadyStateBuilder {
184            config: self.config,
185            bounds: self.bounds,
186            selection: self.selection,
187            crossover: Some(crossover),
188            mutation: self.mutation,
189            fitness: self.fitness,
190            termination: self.termination,
191            _phantom: std::marker::PhantomData,
192        }
193    }
194
195    /// Set the mutation operator
196    pub fn mutation<NewM>(self, mutation: NewM) -> SteadyStateBuilder<G, F, S, C, NewM, Fit, Term>
197    where
198        NewM: MutationOperator<G>,
199    {
200        SteadyStateBuilder {
201            config: self.config,
202            bounds: self.bounds,
203            selection: self.selection,
204            crossover: self.crossover,
205            mutation: Some(mutation),
206            fitness: self.fitness,
207            termination: self.termination,
208            _phantom: std::marker::PhantomData,
209        }
210    }
211
212    /// Set the fitness function
213    pub fn fitness<NewFit>(self, fitness: NewFit) -> SteadyStateBuilder<G, F, S, C, M, NewFit, Term>
214    where
215        NewFit: Fitness<Genome = G, Value = F>,
216    {
217        SteadyStateBuilder {
218            config: self.config,
219            bounds: self.bounds,
220            selection: self.selection,
221            crossover: self.crossover,
222            mutation: self.mutation,
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>(
231        self,
232        termination: NewTerm,
233    ) -> SteadyStateBuilder<G, F, S, C, M, Fit, NewTerm>
234    where
235        NewTerm: TerminationCriterion<G, F>,
236    {
237        SteadyStateBuilder {
238            config: self.config,
239            bounds: self.bounds,
240            selection: self.selection,
241            crossover: self.crossover,
242            mutation: self.mutation,
243            fitness: self.fitness,
244            termination: Some(termination),
245            _phantom: std::marker::PhantomData,
246        }
247    }
248
249    /// Set max generations (convenience method)
250    pub fn max_generations(
251        self,
252        max: usize,
253    ) -> SteadyStateBuilder<G, F, S, C, M, Fit, MaxGenerations> {
254        SteadyStateBuilder {
255            config: self.config,
256            bounds: self.bounds,
257            selection: self.selection,
258            crossover: self.crossover,
259            mutation: self.mutation,
260            fitness: self.fitness,
261            termination: Some(MaxGenerations::new(max)),
262            _phantom: std::marker::PhantomData,
263        }
264    }
265}
266
267impl<G, F, S, C, M, Fit, Term> SteadyStateBuilder<G, F, S, C, M, Fit, Term>
268where
269    G: EvolutionaryGenome + Send + Sync,
270    F: FitnessValue + Send,
271    S: SelectionOperator<G>,
272    C: CrossoverOperator<G>,
273    M: MutationOperator<G>,
274    Fit: Fitness<Genome = G, Value = F> + Sync,
275    Term: TerminationCriterion<G, F>,
276{
277    /// Build the SteadyStateGA instance
278    #[allow(clippy::type_complexity)]
279    pub fn build(self) -> Result<SteadyStateGA<G, F, S, C, M, Fit, Term>, EvolutionError> {
280        let bounds = self
281            .bounds
282            .ok_or_else(|| EvolutionError::Configuration("Bounds must be specified".to_string()))?;
283
284        let selection = self.selection.ok_or_else(|| {
285            EvolutionError::Configuration("Selection operator must be specified".to_string())
286        })?;
287
288        let crossover = self.crossover.ok_or_else(|| {
289            EvolutionError::Configuration("Crossover operator must be specified".to_string())
290        })?;
291
292        let mutation = self.mutation.ok_or_else(|| {
293            EvolutionError::Configuration("Mutation operator must be specified".to_string())
294        })?;
295
296        let fitness = self.fitness.ok_or_else(|| {
297            EvolutionError::Configuration("Fitness function must be specified".to_string())
298        })?;
299
300        let termination = self.termination.ok_or_else(|| {
301            EvolutionError::Configuration("Termination criterion must be specified".to_string())
302        })?;
303
304        Ok(SteadyStateGA {
305            config: self.config,
306            bounds,
307            selection,
308            crossover,
309            mutation,
310            fitness,
311            termination,
312            _phantom: std::marker::PhantomData,
313        })
314    }
315}
316
317/// Steady-State Genetic Algorithm
318///
319/// A GA variant that replaces only a few individuals per generation step,
320/// maintaining a more continuous population transition.
321pub struct SteadyStateGA<G, F, S, C, M, Fit, Term>
322where
323    G: EvolutionaryGenome,
324    F: FitnessValue,
325{
326    config: SteadyStateConfig,
327    bounds: MultiBounds,
328    selection: S,
329    crossover: C,
330    mutation: M,
331    fitness: Fit,
332    termination: Term,
333    _phantom: std::marker::PhantomData<(G, F)>,
334}
335
336impl<G, F, S, C, M, Fit, Term> SteadyStateGA<G, F, S, C, M, Fit, Term>
337where
338    G: EvolutionaryGenome + Send + Sync,
339    F: FitnessValue + Send,
340    S: SelectionOperator<G>,
341    C: CrossoverOperator<G>,
342    M: MutationOperator<G>,
343    Fit: Fitness<Genome = G, Value = F> + Sync,
344    Term: TerminationCriterion<G, F>,
345{
346    /// Create a builder for SteadyStateGA
347    pub fn builder() -> SteadyStateBuilder<G, F, (), (), (), (), ()> {
348        SteadyStateBuilder::new()
349    }
350
351    /// Find the index of the worst individual to replace
352    fn find_replacement_index<R: Rng>(&self, population: &Population<G, F>, rng: &mut R) -> usize {
353        match &self.config.replacement {
354            ReplacementStrategy::ReplaceWorst => {
355                // Find index of worst fitness
356                population
357                    .iter()
358                    .enumerate()
359                    .min_by(|(_, a), (_, b)| {
360                        a.fitness_value()
361                            .partial_cmp(b.fitness_value())
362                            .unwrap_or(std::cmp::Ordering::Equal)
363                    })
364                    .map(|(i, _)| i)
365                    .unwrap_or(0)
366            }
367            ReplacementStrategy::ReplaceRandom => rng.gen_range(0..population.len()),
368            ReplacementStrategy::ReplaceParent => {
369                // This is handled specially in the main loop
370                0
371            }
372            ReplacementStrategy::TournamentWorst(tournament_size) => {
373                // Inverse tournament: select worst from tournament
374                let mut worst_idx = rng.gen_range(0..population.len());
375                let mut worst_fitness = population[worst_idx].fitness_value().to_f64();
376
377                for _ in 1..*tournament_size {
378                    let idx = rng.gen_range(0..population.len());
379                    let fitness = population[idx].fitness_value().to_f64();
380                    if fitness < worst_fitness {
381                        worst_idx = idx;
382                        worst_fitness = fitness;
383                    }
384                }
385                worst_idx
386            }
387        }
388    }
389
390    /// Run the steady-state genetic algorithm
391    pub fn run<R: Rng>(&self, rng: &mut R) -> Result<EvolutionResult<G, F>, EvolutionError> {
392        let start_time = Instant::now();
393
394        // Initialize population
395        let mut population: Population<G, F> =
396            Population::random(self.config.population_size, &self.bounds, rng);
397
398        // Evaluate initial population
399        if self.config.parallel_evaluation {
400            population.evaluate_parallel(&self.fitness);
401        } else {
402            population.evaluate(&self.fitness);
403        }
404
405        let mut stats = EvolutionStats::new();
406        let mut evaluations = population.len();
407        let mut fitness_history: Vec<f64> = Vec::new();
408        let mut step_count = 0usize;
409        let mut generation = 0usize;
410
411        // Track best individual
412        let mut best_individual = population
413            .best()
414            .ok_or(EvolutionError::EmptyPopulation)?
415            .clone();
416
417        // Record initial statistics
418        let gen_stats = GenerationStats::from_population(&population, 0, evaluations);
419        fitness_history.push(gen_stats.best_fitness);
420        stats.record(gen_stats);
421
422        // Main evolution loop
423        loop {
424            // Check termination
425            let state = EvolutionState {
426                generation,
427                evaluations,
428                best_fitness: best_individual.fitness_value().to_f64(),
429                population: &population,
430                fitness_history: &fitness_history,
431            };
432
433            if self.termination.should_terminate(&state) {
434                stats.set_termination_reason(self.termination.reason());
435                break;
436            }
437
438            let step_start = Instant::now();
439
440            // Selection pool
441            let selection_pool: Vec<(G, f64)> = population.as_fitness_pairs();
442
443            // Select parents
444            let parent1_idx = self.selection.select(&selection_pool, rng);
445            let parent2_idx = self.selection.select(&selection_pool, rng);
446            let parent1 = &selection_pool[parent1_idx].0;
447            let parent2 = &selection_pool[parent2_idx].0;
448
449            // Crossover
450            let (mut child1, mut child2) = if rng.gen::<f64>() < self.config.crossover_probability {
451                match self.crossover.crossover(parent1, parent2, rng).genome() {
452                    Some((c1, c2)) => (c1, c2),
453                    None => (parent1.clone(), parent2.clone()),
454                }
455            } else {
456                (parent1.clone(), parent2.clone())
457            };
458
459            // Mutation
460            self.mutation.mutate(&mut child1, rng);
461            self.mutation.mutate(&mut child2, rng);
462
463            // Evaluate offspring
464            let mut offspring: Vec<Individual<G, F>> =
465                vec![Individual::new(child1), Individual::new(child2)];
466
467            for ind in &mut offspring {
468                let fitness = self.fitness.evaluate(&ind.genome);
469                ind.set_fitness(fitness);
470            }
471            evaluations += offspring.len();
472
473            // Replace individuals in population
474            for child in offspring.into_iter().take(self.config.offspring_count) {
475                let replace_idx = match &self.config.replacement {
476                    ReplacementStrategy::ReplaceParent => {
477                        // Only replace if child is better than worst parent
478                        let p1_fit = selection_pool[parent1_idx].1;
479                        let p2_fit = selection_pool[parent2_idx].1;
480                        let child_fit = child.fitness_value().to_f64();
481
482                        if child_fit > p1_fit.min(p2_fit) {
483                            if p1_fit < p2_fit {
484                                parent1_idx
485                            } else {
486                                parent2_idx
487                            }
488                        } else {
489                            continue; // Don't replace
490                        }
491                    }
492                    _ => self.find_replacement_index(&population, rng),
493                };
494
495                // Only replace if new individual is better than the one being replaced
496                // (optional: could be configurable)
497                if child
498                    .fitness_value()
499                    .is_better_than(population[replace_idx].fitness_value())
500                {
501                    population[replace_idx] = child;
502                }
503            }
504
505            // Update best individual
506            if let Some(best) = population.best() {
507                if best.is_better_than(&best_individual) {
508                    best_individual = best.clone();
509                }
510            }
511
512            step_count += 1;
513
514            // Record statistics at generation boundaries
515            if step_count.is_multiple_of(self.config.steps_per_generation) {
516                generation += 1;
517                population.set_generation(generation);
518
519                let timing = TimingStats::new()
520                    .with_total(step_start.elapsed() * self.config.steps_per_generation as u32);
521
522                let gen_stats =
523                    GenerationStats::from_population(&population, generation, evaluations)
524                        .with_timing(timing);
525                fitness_history.push(gen_stats.best_fitness);
526                stats.record(gen_stats);
527            }
528        }
529
530        stats.set_runtime(start_time.elapsed());
531
532        Ok(EvolutionResult::new(
533            best_individual.genome,
534            best_individual.fitness.unwrap(),
535            generation,
536            evaluations,
537        )
538        .with_stats(stats))
539    }
540}
541
542// Implement bounded operators version
543impl<G, F, S, C, M, Fit, Term> SteadyStateGA<G, F, S, C, M, Fit, Term>
544where
545    G: EvolutionaryGenome + Send + Sync,
546    F: FitnessValue + Send,
547    S: SelectionOperator<G>,
548    C: BoundedCrossoverOperator<G>,
549    M: BoundedMutationOperator<G>,
550    Fit: Fitness<Genome = G, Value = F> + Sync,
551    Term: TerminationCriterion<G, F>,
552{
553    /// Run the steady-state genetic algorithm with bounded operators
554    pub fn run_bounded<R: Rng>(
555        &self,
556        rng: &mut R,
557    ) -> Result<EvolutionResult<G, F>, EvolutionError> {
558        let start_time = Instant::now();
559
560        // Initialize population
561        let mut population: Population<G, F> =
562            Population::random(self.config.population_size, &self.bounds, rng);
563
564        // Evaluate initial population
565        if self.config.parallel_evaluation {
566            population.evaluate_parallel(&self.fitness);
567        } else {
568            population.evaluate(&self.fitness);
569        }
570
571        let mut stats = EvolutionStats::new();
572        let mut evaluations = population.len();
573        let mut fitness_history: Vec<f64> = Vec::new();
574        let mut step_count = 0usize;
575        let mut generation = 0usize;
576
577        // Track best individual
578        let mut best_individual = population
579            .best()
580            .ok_or(EvolutionError::EmptyPopulation)?
581            .clone();
582
583        // Record initial statistics
584        let gen_stats = GenerationStats::from_population(&population, 0, evaluations);
585        fitness_history.push(gen_stats.best_fitness);
586        stats.record(gen_stats);
587
588        // Main evolution loop
589        loop {
590            // Check termination
591            let state = EvolutionState {
592                generation,
593                evaluations,
594                best_fitness: best_individual.fitness_value().to_f64(),
595                population: &population,
596                fitness_history: &fitness_history,
597            };
598
599            if self.termination.should_terminate(&state) {
600                stats.set_termination_reason(self.termination.reason());
601                break;
602            }
603
604            // Selection pool
605            let selection_pool: Vec<(G, f64)> = population.as_fitness_pairs();
606
607            // Select parents
608            let parent1_idx = self.selection.select(&selection_pool, rng);
609            let parent2_idx = self.selection.select(&selection_pool, rng);
610            let parent1 = &selection_pool[parent1_idx].0;
611            let parent2 = &selection_pool[parent2_idx].0;
612
613            // Crossover with bounds
614            let (mut child1, mut child2) = if rng.gen::<f64>() < self.config.crossover_probability {
615                match self
616                    .crossover
617                    .crossover_bounded(parent1, parent2, &self.bounds, rng)
618                    .genome()
619                {
620                    Some((c1, c2)) => (c1, c2),
621                    None => (parent1.clone(), parent2.clone()),
622                }
623            } else {
624                (parent1.clone(), parent2.clone())
625            };
626
627            // Mutation with bounds
628            self.mutation.mutate_bounded(&mut child1, &self.bounds, rng);
629            self.mutation.mutate_bounded(&mut child2, &self.bounds, rng);
630
631            // Evaluate offspring
632            let mut offspring: Vec<Individual<G, F>> =
633                vec![Individual::new(child1), Individual::new(child2)];
634
635            for ind in &mut offspring {
636                let fitness = self.fitness.evaluate(&ind.genome);
637                ind.set_fitness(fitness);
638            }
639            evaluations += offspring.len();
640
641            // Replace individuals in population
642            for child in offspring.into_iter().take(self.config.offspring_count) {
643                let replace_idx = match &self.config.replacement {
644                    ReplacementStrategy::ReplaceParent => {
645                        let p1_fit = selection_pool[parent1_idx].1;
646                        let p2_fit = selection_pool[parent2_idx].1;
647                        let child_fit = child.fitness_value().to_f64();
648
649                        if child_fit > p1_fit.min(p2_fit) {
650                            if p1_fit < p2_fit {
651                                parent1_idx
652                            } else {
653                                parent2_idx
654                            }
655                        } else {
656                            continue;
657                        }
658                    }
659                    _ => self.find_replacement_index(&population, rng),
660                };
661
662                if child
663                    .fitness_value()
664                    .is_better_than(population[replace_idx].fitness_value())
665                {
666                    population[replace_idx] = child;
667                }
668            }
669
670            // Update best individual
671            if let Some(best) = population.best() {
672                if best.is_better_than(&best_individual) {
673                    best_individual = best.clone();
674                }
675            }
676
677            step_count += 1;
678
679            // Record statistics at generation boundaries
680            if step_count.is_multiple_of(self.config.steps_per_generation) {
681                generation += 1;
682                population.set_generation(generation);
683
684                let gen_stats =
685                    GenerationStats::from_population(&population, generation, evaluations);
686                fitness_history.push(gen_stats.best_fitness);
687                stats.record(gen_stats);
688            }
689        }
690
691        stats.set_runtime(start_time.elapsed());
692
693        Ok(EvolutionResult::new(
694            best_individual.genome,
695            best_individual.fitness.unwrap(),
696            generation,
697            evaluations,
698        )
699        .with_stats(stats))
700    }
701}
702
703#[cfg(test)]
704mod tests {
705    use super::*;
706    use crate::fitness::benchmarks::{OneMax, Sphere};
707    use crate::genome::traits::RealValuedGenome;
708    use crate::operators::crossover::{SbxCrossover, UniformCrossover};
709    use crate::operators::mutation::{BitFlipMutation, PolynomialMutation};
710    use crate::operators::selection::TournamentSelection;
711    use crate::termination::MaxEvaluations;
712
713    #[test]
714    fn test_steady_state_builder() {
715        let bounds = MultiBounds::symmetric(5.0, 10);
716        let ga = SteadyStateBuilder::new()
717            .population_size(50)
718            .offspring_count(2)
719            .bounds(bounds)
720            .selection(TournamentSelection::new(3))
721            .crossover(SbxCrossover::new(20.0))
722            .mutation(PolynomialMutation::new(20.0))
723            .fitness(Sphere::new(10))
724            .max_generations(10)
725            .build();
726
727        assert!(ga.is_ok());
728    }
729
730    #[test]
731    fn test_steady_state_sphere() {
732        let mut rng = rand::thread_rng();
733        let bounds = MultiBounds::symmetric(5.12, 10);
734
735        let ga = SteadyStateBuilder::new()
736            .population_size(50)
737            .offspring_count(2)
738            .steps_per_generation(25)
739            .bounds(bounds)
740            .selection(TournamentSelection::new(3))
741            .crossover(SbxCrossover::new(20.0))
742            .mutation(PolynomialMutation::new(20.0))
743            .fitness(Sphere::new(10))
744            .termination(MaxEvaluations::new(2000))
745            .build()
746            .unwrap();
747
748        let result = ga.run(&mut rng).unwrap();
749
750        // Should find some improvement
751        assert!(
752            result.best_fitness > -200.0,
753            "Expected fitness > -200, got {}",
754            result.best_fitness
755        );
756        assert!(result.evaluations <= 2000);
757    }
758
759    #[test]
760    fn test_steady_state_onemax() {
761        let mut rng = rand::thread_rng();
762        let bounds = MultiBounds::uniform(crate::genome::bounds::Bounds::unit(), 20);
763
764        let ga = SteadyStateBuilder::new()
765            .population_size(50)
766            .offspring_count(2)
767            .steps_per_generation(25)
768            .bounds(bounds)
769            .selection(TournamentSelection::new(3))
770            .crossover(UniformCrossover::new())
771            .mutation(BitFlipMutation::new())
772            .fitness(OneMax::new(20))
773            .termination(MaxEvaluations::new(2000))
774            .build()
775            .unwrap();
776
777        let result = ga.run(&mut rng).unwrap();
778
779        // Should find good solution
780        assert!(result.best_fitness >= 15); // At least 75% ones
781    }
782
783    #[test]
784    fn test_replacement_strategies() {
785        let mut rng = rand::thread_rng();
786        let bounds = MultiBounds::symmetric(5.12, 5);
787
788        let strategies = vec![
789            ReplacementStrategy::ReplaceWorst,
790            ReplacementStrategy::ReplaceRandom,
791            ReplacementStrategy::TournamentWorst(3),
792        ];
793
794        for strategy in strategies {
795            let ga = SteadyStateBuilder::new()
796                .population_size(30)
797                .offspring_count(2)
798                .replacement(strategy)
799                .bounds(bounds.clone())
800                .selection(TournamentSelection::new(3))
801                .crossover(SbxCrossover::new(20.0))
802                .mutation(PolynomialMutation::new(20.0))
803                .fitness(Sphere::new(5))
804                .termination(MaxEvaluations::new(500))
805                .build()
806                .unwrap();
807
808            let result = ga.run(&mut rng);
809            assert!(result.is_ok());
810        }
811    }
812
813    #[test]
814    fn test_steady_state_bounded() {
815        let mut rng = rand::thread_rng();
816        let bounds = MultiBounds::symmetric(5.12, 10);
817
818        let ga = SteadyStateBuilder::new()
819            .population_size(50)
820            .bounds(bounds.clone())
821            .selection(TournamentSelection::new(3))
822            .crossover(SbxCrossover::new(20.0))
823            .mutation(PolynomialMutation::new(20.0))
824            .fitness(Sphere::new(10))
825            .termination(MaxEvaluations::new(1000))
826            .build()
827            .unwrap();
828
829        let result = ga.run_bounded(&mut rng).unwrap();
830
831        // All genes should be within bounds
832        for gene in result.best_genome.genes() {
833            assert!(*gene >= -5.12 && *gene <= 5.12);
834        }
835    }
836}