genetic_algorithms 2.2.0

Library for solving genetic algorithm problems
Documentation
//! Per-generation statistics.
//!
//! The [`GenerationStats`] struct captures fitness metrics (best, worst,
//! average, standard deviation) at the end of each generation. These
//! statistics are used internally for convergence-based stopping criteria and
//! are also passed to user callbacks during `run_with_callback`.

/// Per-generation statistics for tracking GA convergence and behavior.
///
/// Collected at the end of each generation and optionally passed to callbacks.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GenerationStats {
    /// Generation number (0-based).
    pub generation: usize,
    /// Best (minimum or maximum depending on problem) fitness in this generation.
    pub best_fitness: f64,
    /// Worst fitness in this generation.
    pub worst_fitness: f64,
    /// Average fitness across the population.
    pub avg_fitness: f64,
    /// Standard deviation of fitness values.
    pub fitness_std_dev: f64,
    /// Population size at the end of this generation.
    pub population_size: usize,
    /// Population diversity: standard deviation of fitness values.
    /// Equal to `fitness_std_dev` in v2.2. Higher values = more diverse.
    #[cfg_attr(feature = "serde", serde(default))]
    pub diversity: f64,
    /// Current dynamic mutation probability, if dynamic mutation is enabled.
    /// `None` when dynamic mutation is disabled.
    #[cfg_attr(feature = "serde", serde(default))]
    pub dynamic_mutation_probability: Option<f64>,
}

impl GenerationStats {
    /// Computes statistics from a slice of fitness values.
    ///
    /// `is_maximization` controls which value is "best" vs "worst".
    pub fn from_fitness_values(
        generation: usize,
        fitness_values: &[f64],
        is_maximization: bool,
    ) -> Self {
        let n = fitness_values.len();
        if n == 0 {
            return GenerationStats {
                generation,
                best_fitness: 0.0,
                worst_fitness: 0.0,
                avg_fitness: 0.0,
                fitness_std_dev: 0.0,
                population_size: 0,
                diversity: 0.0,
                dynamic_mutation_probability: None,
            };
        }

        let sum: f64 = fitness_values.iter().sum();
        let avg = sum / n as f64;

        let variance = fitness_values
            .iter()
            .map(|f| (f - avg).powi(2))
            .sum::<f64>()
            / n as f64;
        let std_dev = variance.sqrt();

        let mut min = f64::INFINITY;
        let mut max = f64::NEG_INFINITY;
        for &f in fitness_values {
            if f < min {
                min = f;
            }
            if f > max {
                max = f;
            }
        }

        let (best, worst) = if is_maximization {
            (max, min)
        } else {
            (min, max)
        };

        GenerationStats {
            generation,
            best_fitness: best,
            worst_fitness: worst,
            avg_fitness: avg,
            fitness_std_dev: std_dev,
            population_size: n,
            diversity: std_dev,
            dynamic_mutation_probability: None,
        }
    }
}