Skip to main content

evolution_ternary/
mutation.rs

1//! Mutation operators: point mutation, crossover (single-point, uniform).
2
3use crate::population::{TernaryAgent, SimpleRng};
4
5/// How crossover is performed.
6#[derive(Clone, Debug)]
7pub enum CrossoverMethod {
8    /// Single-point crossover at a random cut.
9    SinglePoint,
10    /// Uniform crossover — each position picked randomly from either parent.
11    Uniform,
12}
13
14/// Mutation configuration.
15#[derive(Clone, Debug)]
16pub struct Mutation {
17    /// Probability of point mutation per position.
18    pub point_rate: f64,
19    /// Crossover method.
20    pub crossover: CrossoverMethod,
21    /// Crossover probability (if not crossed over, child = clone of parent).
22    pub crossover_rate: f64,
23}
24
25impl Mutation {
26    /// Default mutation: 1% point mutation rate, single-point crossover at 70%.
27    pub fn default_rates() -> Self {
28        Self {
29            point_rate: 0.01,
30            crossover: CrossoverMethod::SinglePoint,
31            crossover_rate: 0.7,
32        }
33    }
34
35    /// Apply point mutation to a single agent (modifies in place).
36    /// Each position has `point_rate` chance of being randomly changed.
37    pub fn point_mutate(&self, agent: &mut TernaryAgent, rng: &mut SimpleRng) {
38        for v in &mut agent.strategy {
39            if rng.next_f64() < self.point_rate {
40                // Flip to a random different ternary value
41                let current = *v;
42                let new_val = (rng.next_u32() % 2) as u8;
43                *v = if new_val < current { new_val } else { new_val + 1 };
44                if *v > 2 { *v = 0; }
45                debug_assert!(*v <= 2);
46            }
47        }
48    }
49
50    /// Crossover two parents to produce two children.
51    pub fn crossover(&self, parent_a: &TernaryAgent, parent_b: &TernaryAgent, rng: &mut SimpleRng) -> (TernaryAgent, TernaryAgent) {
52        if rng.next_f64() > self.crossover_rate {
53            return (parent_a.clone(), parent_b.clone());
54        }
55        match &self.crossover {
56            CrossoverMethod::SinglePoint => {
57                let point = if parent_a.strategy.is_empty() {
58                    0
59                } else {
60                    ((rng.next_u64() as usize) % parent_a.strategy.len()).max(1)
61                };
62                let mut child_a = parent_a.clone();
63                let mut child_b = parent_b.clone();
64                for i in point..parent_a.strategy.len() {
65                    child_a.strategy[i] = parent_b.strategy[i];
66                    child_b.strategy[i] = parent_a.strategy[i];
67                }
68                (child_a, child_b)
69            }
70            CrossoverMethod::Uniform => {
71                let mut child_a = parent_a.clone();
72                let mut child_b = parent_b.clone();
73                for i in 0..parent_a.strategy.len() {
74                    if rng.next_f64() < 0.5 {
75                        child_a.strategy[i] = parent_b.strategy[i];
76                        child_b.strategy[i] = parent_a.strategy[i];
77                    }
78                }
79                (child_a, child_b)
80            }
81        }
82    }
83
84    /// Apply mutation (point mutation) to an agent.
85    pub fn mutate(&self, agent: &mut TernaryAgent, rng: &mut SimpleRng) {
86        self.point_mutate(agent, rng);
87    }
88}
89
90/// Autofill mutation: analogous to spreadsheet autofill — extrapolate patterns
91/// in the strategy vector to fill or replace positions.
92pub fn autofill_mutate(agent: &mut TernaryAgent, start: usize, rng: &mut SimpleRng) {
93    if agent.strategy.len() < 2 || start >= agent.strategy.len() {
94        return;
95    }
96    // Find the nearest two preceding values to extrapolate a pattern
97    let len = agent.strategy.len();
98    if start >= 2 {
99        let diff = (agent.strategy[start - 1] as i32 - agent.strategy[start - 2] as i32).rem_euclid(3);
100        for i in start..len {
101            agent.strategy[i] = (agent.strategy[i - 1] as i32 + diff) as u8 % 3;
102        }
103    } else {
104        // Random fill if not enough context
105        for i in start..len {
106            agent.strategy[i] = (rng.next_u32() % 3) as u8;
107        }
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn test_autofill_constant_pattern() {
117        let mut agent = TernaryAgent::new_unchecked(vec![1, 1, 1, 0, 0, 0]);
118        let mut rng = SimpleRng::from_seed(99);
119        autofill_mutate(&mut agent, 3, &mut rng);
120        assert_eq!(agent.strategy[3], 1);
121        assert_eq!(agent.strategy[4], 1);
122        assert_eq!(agent.strategy[5], 1);
123    }
124
125    #[test]
126    fn test_autofill_ascending_pattern() {
127        let mut agent = TernaryAgent::new_unchecked(vec![0, 1, 0, 0, 0, 0]);
128        let mut rng = SimpleRng::from_seed(99);
129        autofill_mutate(&mut agent, 2, &mut rng);
130        assert_eq!(agent.strategy[2], 2); // 0,1 -> diff=1 -> next=2
131        assert_eq!(agent.strategy[3], 0); // 2+1=3%3=0
132        assert_eq!(agent.strategy[4], 1);
133        assert_eq!(agent.strategy[5], 2);
134    }
135}