Skip to main content

wafrift_evolution/evolution/crossover/
selection.rs

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