Skip to main content

evolution_ternary/
engine.rs

1//! Evolution engine — orchestrates selection, mutation, and generational loops.
2
3use crate::population::{TernaryPopulation, SimpleRng};
4use crate::fitness::FitnessFunction;
5use crate::selection::{SelectionMethod, Selection};
6use crate::mutation::Mutation;
7use crate::species::SpeciesTracker;
8use crate::convergence::ConvergenceDetector;
9
10/// Configuration for the evolution engine.
11#[derive(Clone, Debug)]
12pub struct EvolutionConfig {
13    pub selection: SelectionMethod,
14    pub mutation: Mutation,
15    pub max_generations: usize,
16    pub elitism: usize, // Number of top agents carried over unchanged
17    pub seed: u64,
18}
19
20impl EvolutionConfig {
21    pub fn default_config() -> Self {
22        Self {
23            selection: SelectionMethod::tournament(),
24            mutation: Mutation::default_rates(),
25            max_generations: 100,
26            elitism: 2,
27            seed: 42,
28        }
29    }
30}
31
32/// Summary of a completed evolution run.
33#[derive(Clone, Debug)]
34pub struct EvolutionResult {
35    pub generations_run: usize,
36    pub best_fitness: f64,
37    pub best_strategy: Vec<u8>,
38    pub fitness_history: Vec<f64>,
39    pub converged: bool,
40    pub convergence_reason: Option<String>,
41    pub final_species: (usize, usize, usize, usize),
42}
43
44/// The main evolution engine.
45pub struct EvolutionEngine {
46    pub config: EvolutionConfig,
47    pub fitness_fn: FitnessFunction,
48    pub species_tracker: SpeciesTracker,
49    pub convergence_detector: ConvergenceDetector,
50}
51
52impl EvolutionEngine {
53    pub fn new(config: EvolutionConfig, fitness_fn: FitnessFunction) -> Self {
54        Self {
55            config,
56            fitness_fn,
57            species_tracker: SpeciesTracker::new(),
58            convergence_detector: ConvergenceDetector::default_detector(),
59        }
60    }
61
62    /// Evaluate all agents in the population.
63    fn evaluate(&self, pop: &mut TernaryPopulation) {
64        for agent in &mut pop.agents {
65            agent.fitness = self.fitness_fn.evaluate(&agent.strategy);
66        }
67    }
68
69    /// Run evolution on the given initial population.
70    pub fn run(&mut self, initial_pop: TernaryPopulation) -> EvolutionResult {
71        let mut rng = SimpleRng::from_seed(self.config.seed);
72        let mut pop = initial_pop;
73        let pop_size = pop.size();
74
75        // Evaluate initial population
76        self.evaluate(&mut pop);
77        pop.sort_by_fitness();
78
79        let mut fitness_history = Vec::new();
80
81        for gen in 0..self.config.max_generations {
82            let avg = pop.avg_fitness();
83            fitness_history.push(avg);
84            self.convergence_detector.record(avg);
85            self.species_tracker.record(&pop.agents);
86
87            // Check convergence
88            let status = self.convergence_detector.check(&self.species_tracker);
89            if status.converged && gen > 5 {
90                let best = pop.best().clone();
91                let final_species = *self.species_tracker.history.last().unwrap();
92                return EvolutionResult {
93                    generations_run: gen + 1,
94                    best_fitness: best.fitness,
95                    best_strategy: best.strategy,
96                    fitness_history,
97                    converged: true,
98                    convergence_reason: status.reason,
99                    final_species,
100                };
101            }
102
103            // Selection
104            let parents = self.config.selection.select(&pop, pop_size, &mut rng);
105
106            // Create next generation with elitism
107            let mut next_agents: Vec<_> = pop.agents[..self.config.elitism.min(pop_size)].to_vec();
108
109            // Crossover and mutation
110            let mut i = 0;
111            while next_agents.len() < pop_size {
112                let p1 = &parents[i % parents.len()];
113                let p2 = &parents[(i + 1) % parents.len()];
114                let (mut c1, mut c2) = self.config.mutation.crossover(p1, p2, &mut rng);
115                self.config.mutation.mutate(&mut c1, &mut rng);
116                self.config.mutation.mutate(&mut c2, &mut rng);
117                next_agents.push(c1);
118                if next_agents.len() < pop_size {
119                    next_agents.push(c2);
120                }
121                i += 2;
122            }
123
124            // Replace population
125            pop.agents = next_agents;
126            self.evaluate(&mut pop);
127            pop.sort_by_fitness();
128        }
129
130        // Record final generation
131        fitness_history.push(pop.avg_fitness());
132        self.convergence_detector.record(pop.avg_fitness());
133        self.species_tracker.record(&pop.agents);
134
135        let best = pop.best().clone();
136        let final_species = *self.species_tracker.history.last().unwrap();
137        let status = self.convergence_detector.check(&self.species_tracker);
138
139        EvolutionResult {
140            generations_run: self.config.max_generations,
141            best_fitness: best.fitness,
142            best_strategy: best.strategy,
143            fitness_history,
144            converged: status.converged,
145            convergence_reason: status.reason,
146            final_species,
147        }
148    }
149}