Skip to main content

wafrift_evolution/evolution/crossover/
selection.rs

1use crate::evolution::Chromosome;
2use rand::Rng;
3
4/// Selection strategy for tournament selection.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum SelectionStrategy {
7    Standard,
8    Adaptive,
9    Roulette,
10}
11
12/// Tournament selection with configurable tournament size.
13#[must_use]
14pub fn tournament_select<'a>(population: &'a [Chromosome], rng: &mut impl Rng) -> &'a Chromosome {
15    let tournament_size = 3_usize.min(population.len());
16    tournament_select_with_size(population, tournament_size, rng)
17}
18
19/// Tournament selection with explicit tournament size.
20///
21/// # Panics
22/// Panics with a clear contract message if `population` is empty —
23/// silently returning a "default" Chromosome would mask the caller's
24/// state-machine bug. Callers that may have an empty population
25/// should guard before invoking this helper.
26#[must_use]
27pub fn tournament_select_with_size<'a>(
28    population: &'a [Chromosome],
29    tournament_size: usize,
30    rng: &mut impl Rng,
31) -> &'a Chromosome {
32    assert!(
33        !population.is_empty(),
34        "tournament_select_with_size called with empty population — caller bug"
35    );
36    let size = tournament_size.min(population.len());
37    let mut best_idx = rng.gen_range(0..population.len());
38    for _ in 1..size {
39        let candidate_idx = rng.gen_range(0..population.len());
40        if population[candidate_idx].fitness > population[best_idx].fitness {
41            best_idx = candidate_idx;
42        }
43    }
44    &population[best_idx]
45}
46
47/// Roulette wheel selection (fitness proportionate selection).
48///
49/// # Panics
50/// Panics if `population` is empty (see `tournament_select_with_size`).
51#[must_use]
52pub fn roulette_select<'a>(population: &'a [Chromosome], rng: &mut impl Rng) -> &'a Chromosome {
53    assert!(
54        !population.is_empty(),
55        "roulette_select called with empty population — caller bug"
56    );
57    if population.len() == 1 {
58        return &population[0];
59    }
60    let total_fitness: f64 = population.iter().map(|c| c.fitness.max(0.0)).sum();
61    if total_fitness <= 0.0 {
62        return &population[rng.gen_range(0..population.len())];
63    }
64    let mut spin = rng.gen_range(0.0..total_fitness);
65    for chromosome in population {
66        spin -= chromosome.fitness.max(0.0);
67        if spin <= 0.0 {
68            return chromosome;
69        }
70    }
71    &population[population.len() - 1]
72}
73
74/// Adaptive selection that adjusts tournament size based on population diversity.
75#[must_use]
76pub fn adaptive_select<'a>(
77    population: &'a [Chromosome],
78    diversity: f64,
79    rng: &mut impl Rng,
80) -> &'a Chromosome {
81    let base_size = 3_usize;
82    let max_size = (population.len() / 3).max(base_size);
83    let adjusted_size = base_size + ((max_size - base_size) as f64 * (1.0 - diversity)) as usize;
84    tournament_select_with_size(population, adjusted_size, rng)
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use rand::{SeedableRng, rngs::StdRng};
91
92    #[test]
93    fn tournament_select_single_chromosome_returns_it() {
94        let population = vec![Chromosome::new(vec![("encoding".into(), "None".into())])];
95        let mut rng = StdRng::seed_from_u64(29);
96        let selected = tournament_select(&population, &mut rng);
97        assert_eq!(selected.genes, population[0].genes);
98    }
99}