1pub mod convergence;
6
7use std::time::Duration;
8
9use serde::{Deserialize, Serialize};
10
11use crate::fitness::traits::FitnessValue;
12use crate::genome::traits::EvolutionaryGenome;
13use crate::population::population::Population;
14
15#[derive(Clone, Debug, Serialize, Deserialize)]
17pub struct GenerationStats {
18 pub generation: usize,
20 pub evaluations: usize,
22 pub best_fitness: f64,
24 pub worst_fitness: f64,
26 pub mean_fitness: f64,
28 pub median_fitness: f64,
30 pub fitness_std: f64,
32 pub diversity: f64,
34 pub timing: TimingStats,
36}
37
38#[derive(Clone, Debug, Default, Serialize, Deserialize)]
40pub struct TimingStats {
41 pub evaluation_ms: f64,
43 pub selection_ms: f64,
45 pub crossover_ms: f64,
47 pub mutation_ms: f64,
49 pub total_ms: f64,
51}
52
53impl TimingStats {
54 pub fn new() -> Self {
56 Self::default()
57 }
58
59 pub fn with_evaluation(mut self, duration: Duration) -> Self {
61 self.evaluation_ms = duration.as_secs_f64() * 1000.0;
62 self
63 }
64
65 pub fn with_selection(mut self, duration: Duration) -> Self {
67 self.selection_ms = duration.as_secs_f64() * 1000.0;
68 self
69 }
70
71 pub fn with_crossover(mut self, duration: Duration) -> Self {
73 self.crossover_ms = duration.as_secs_f64() * 1000.0;
74 self
75 }
76
77 pub fn with_mutation(mut self, duration: Duration) -> Self {
79 self.mutation_ms = duration.as_secs_f64() * 1000.0;
80 self
81 }
82
83 pub fn with_total(mut self, duration: Duration) -> Self {
85 self.total_ms = duration.as_secs_f64() * 1000.0;
86 self
87 }
88}
89
90impl GenerationStats {
91 pub fn from_population<G, F>(
93 population: &Population<G, F>,
94 generation: usize,
95 evaluations: usize,
96 ) -> Self
97 where
98 G: EvolutionaryGenome,
99 F: FitnessValue,
100 {
101 let mut fitnesses: Vec<f64> = population
102 .iter()
103 .filter_map(|i| i.fitness.as_ref().map(|f| f.to_f64()))
104 .collect();
105
106 if fitnesses.is_empty() {
107 return Self {
108 generation,
109 evaluations,
110 best_fitness: f64::NEG_INFINITY,
111 worst_fitness: f64::INFINITY,
112 mean_fitness: 0.0,
113 median_fitness: 0.0,
114 fitness_std: 0.0,
115 diversity: 0.0,
116 timing: TimingStats::default(),
117 };
118 }
119
120 fitnesses.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
121
122 let best = fitnesses.last().copied().unwrap_or(f64::NEG_INFINITY);
123 let worst = fitnesses.first().copied().unwrap_or(f64::INFINITY);
124 let mean = fitnesses.iter().sum::<f64>() / fitnesses.len() as f64;
125 let median = if fitnesses.len().is_multiple_of(2) {
126 (fitnesses[fitnesses.len() / 2 - 1] + fitnesses[fitnesses.len() / 2]) / 2.0
127 } else {
128 fitnesses[fitnesses.len() / 2]
129 };
130
131 let variance = if fitnesses.len() > 1 {
132 fitnesses.iter().map(|f| (f - mean).powi(2)).sum::<f64>() / (fitnesses.len() - 1) as f64
133 } else {
134 0.0
135 };
136 let std = variance.sqrt();
137
138 let diversity = population.diversity();
139
140 Self {
141 generation,
142 evaluations,
143 best_fitness: best,
144 worst_fitness: worst,
145 mean_fitness: mean,
146 median_fitness: median,
147 fitness_std: std,
148 diversity,
149 timing: TimingStats::default(),
150 }
151 }
152
153 pub fn with_timing(mut self, timing: TimingStats) -> Self {
155 self.timing = timing;
156 self
157 }
158}
159
160#[derive(Clone, Debug, Default, Serialize, Deserialize)]
162pub struct EvolutionStats {
163 pub generations: Vec<GenerationStats>,
165 pub total_runtime_ms: f64,
167 pub termination_reason: Option<String>,
169}
170
171impl EvolutionStats {
172 pub fn new() -> Self {
174 Self::default()
175 }
176
177 pub fn record(&mut self, stats: GenerationStats) {
179 self.generations.push(stats);
180 }
181
182 pub fn num_generations(&self) -> usize {
184 self.generations.len()
185 }
186
187 pub fn best_fitness(&self) -> Option<f64> {
189 self.generations
190 .iter()
191 .map(|g| g.best_fitness)
192 .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
193 }
194
195 pub fn final_best_fitness(&self) -> Option<f64> {
197 self.generations.last().map(|g| g.best_fitness)
198 }
199
200 pub fn best_fitness_history(&self) -> Vec<f64> {
202 self.generations.iter().map(|g| g.best_fitness).collect()
203 }
204
205 pub fn mean_fitness_history(&self) -> Vec<f64> {
207 self.generations.iter().map(|g| g.mean_fitness).collect()
208 }
209
210 pub fn diversity_history(&self) -> Vec<f64> {
212 self.generations.iter().map(|g| g.diversity).collect()
213 }
214
215 pub fn set_termination_reason(&mut self, reason: &str) {
217 self.termination_reason = Some(reason.to_string());
218 }
219
220 pub fn set_runtime(&mut self, duration: Duration) {
222 self.total_runtime_ms = duration.as_secs_f64() * 1000.0;
223 }
224
225 pub fn summary(&self) -> String {
227 let best = self.best_fitness().unwrap_or(f64::NEG_INFINITY);
228 let final_best = self.final_best_fitness().unwrap_or(f64::NEG_INFINITY);
229 let generations = self.num_generations();
230 let runtime = self.total_runtime_ms;
231
232 format!(
233 "Evolution Summary:\n\
234 - Generations: {}\n\
235 - Best fitness: {:.6}\n\
236 - Final best: {:.6}\n\
237 - Runtime: {:.2}ms\n\
238 - Termination: {}",
239 generations,
240 best,
241 final_best,
242 runtime,
243 self.termination_reason.as_deref().unwrap_or("unknown")
244 )
245 }
246}
247
248#[derive(Clone, Debug)]
250pub struct EvolutionResult<G, F = f64>
251where
252 G: EvolutionaryGenome,
253 F: FitnessValue,
254{
255 pub best_genome: G,
257 pub best_fitness: F,
259 pub generations: usize,
261 pub evaluations: usize,
263 pub stats: EvolutionStats,
265}
266
267impl<G, F> EvolutionResult<G, F>
268where
269 G: EvolutionaryGenome,
270 F: FitnessValue,
271{
272 pub fn new(best_genome: G, best_fitness: F, generations: usize, evaluations: usize) -> Self {
274 Self {
275 best_genome,
276 best_fitness,
277 generations,
278 evaluations,
279 stats: EvolutionStats::new(),
280 }
281 }
282
283 pub fn with_stats(mut self, stats: EvolutionStats) -> Self {
285 self.stats = stats;
286 self
287 }
288}
289
290pub mod prelude {
291 pub use super::convergence::{
292 detect_stagnation, evolutionary_ess, evolutionary_ess_log, evolutionary_rhat,
293 fitness_convergence, ConvergenceConfig, ConvergenceDetector, ConvergenceReason,
294 ConvergenceStatus,
295 };
296 pub use super::{EvolutionResult, EvolutionStats, GenerationStats, TimingStats};
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302 use crate::genome::real_vector::RealVector;
303 use crate::population::individual::Individual;
304
305 fn create_test_population() -> Population<RealVector> {
306 let individuals = vec![
307 Individual::with_fitness(RealVector::new(vec![1.0]), 10.0),
308 Individual::with_fitness(RealVector::new(vec![2.0]), 20.0),
309 Individual::with_fitness(RealVector::new(vec![3.0]), 30.0),
310 Individual::with_fitness(RealVector::new(vec![4.0]), 40.0),
311 Individual::with_fitness(RealVector::new(vec![5.0]), 50.0),
312 ];
313 Population::from_individuals(individuals)
314 }
315
316 #[test]
317 fn test_generation_stats_from_population() {
318 let pop = create_test_population();
319 let stats = GenerationStats::from_population(&pop, 10, 100);
320
321 assert_eq!(stats.generation, 10);
322 assert_eq!(stats.evaluations, 100);
323 assert_eq!(stats.best_fitness, 50.0);
324 assert_eq!(stats.worst_fitness, 10.0);
325 assert_eq!(stats.mean_fitness, 30.0);
326 assert_eq!(stats.median_fitness, 30.0);
327 assert!(stats.fitness_std > 15.0 && stats.fitness_std < 16.0);
328 }
329
330 #[test]
331 fn test_generation_stats_empty_population() {
332 let pop: Population<RealVector> = Population::new();
333 let stats = GenerationStats::from_population(&pop, 0, 0);
334
335 assert_eq!(stats.best_fitness, f64::NEG_INFINITY);
336 assert_eq!(stats.worst_fitness, f64::INFINITY);
337 }
338
339 #[test]
340 fn test_evolution_stats_record() {
341 let mut stats = EvolutionStats::new();
342 let pop = create_test_population();
343
344 for i in 0..5 {
345 let gen_stats = GenerationStats::from_population(&pop, i, i * 10);
346 stats.record(gen_stats);
347 }
348
349 assert_eq!(stats.num_generations(), 5);
350 assert_eq!(stats.best_fitness(), Some(50.0));
351 }
352
353 #[test]
354 fn test_evolution_stats_history() {
355 let mut stats = EvolutionStats::new();
356
357 for i in 0..5 {
358 stats.record(GenerationStats {
359 generation: i,
360 evaluations: i * 10,
361 best_fitness: (i + 1) as f64 * 10.0,
362 worst_fitness: 0.0,
363 mean_fitness: (i + 1) as f64 * 5.0,
364 median_fitness: 0.0,
365 fitness_std: 0.0,
366 diversity: 1.0 / (i + 1) as f64,
367 timing: TimingStats::default(),
368 });
369 }
370
371 let best_history = stats.best_fitness_history();
372 assert_eq!(best_history, vec![10.0, 20.0, 30.0, 40.0, 50.0]);
373
374 let mean_history = stats.mean_fitness_history();
375 assert_eq!(mean_history, vec![5.0, 10.0, 15.0, 20.0, 25.0]);
376 }
377
378 #[test]
379 fn test_evolution_stats_summary() {
380 let mut stats = EvolutionStats::new();
381 stats.record(GenerationStats {
382 generation: 0,
383 evaluations: 100,
384 best_fitness: 50.0,
385 worst_fitness: 10.0,
386 mean_fitness: 30.0,
387 median_fitness: 30.0,
388 fitness_std: 15.0,
389 diversity: 0.5,
390 timing: TimingStats::default(),
391 });
392 stats.set_termination_reason("Target reached");
393 stats.set_runtime(Duration::from_millis(1234));
394
395 let summary = stats.summary();
396 assert!(summary.contains("Generations: 1"));
397 assert!(summary.contains("Best fitness: 50"));
398 assert!(summary.contains("Target reached"));
399 }
400
401 #[test]
402 fn test_timing_stats() {
403 let timing = TimingStats::new()
404 .with_evaluation(Duration::from_millis(100))
405 .with_selection(Duration::from_millis(20))
406 .with_crossover(Duration::from_millis(30))
407 .with_mutation(Duration::from_millis(10))
408 .with_total(Duration::from_millis(160));
409
410 assert!((timing.evaluation_ms - 100.0).abs() < 0.1);
411 assert!((timing.selection_ms - 20.0).abs() < 0.1);
412 assert!((timing.crossover_ms - 30.0).abs() < 0.1);
413 assert!((timing.mutation_ms - 10.0).abs() < 0.1);
414 assert!((timing.total_ms - 160.0).abs() < 0.1);
415 }
416
417 #[test]
418 fn test_evolution_result() {
419 let genome = RealVector::new(vec![1.0, 2.0, 3.0]);
420 let result = EvolutionResult::new(genome, 42.0, 100, 1000);
421
422 assert_eq!(result.best_fitness, 42.0);
423 assert_eq!(result.generations, 100);
424 assert_eq!(result.evaluations, 1000);
425 }
426}