genetic_algorithms 3.0.0

Library for solving genetic algorithm problems
Documentation
/*!
# Rastrigin Continuous Optimization Example

This example demonstrates how to use the `genetic_algorithms` library to minimize the Rastrigin
function — a classic continuous optimization benchmark with many local minima and a global
minimum of 0.0 at the origin.

Features demonstrated:
- Range<f64> chromosomes (continuous representation)
- Gaussian mutation
- Tournament selection
- Uniform crossover
- Minimization mode
- Progress callback
- CompositeObserver combining LogObserver and optional MetricsObserver

Run with:
```sh
cargo run --example rastrigin
```
*/

use genetic_algorithms::chromosomes::Range as RangeChromosome;
use genetic_algorithms::genotypes::Range as RangeGenotype;
use genetic_algorithms::initializers::range_random_initialization;
use genetic_algorithms::prelude::*;
// Non-prelude items used only in this example:
use genetic_algorithms::ga::TerminationCause;
use genetic_algorithms::operations::GaussianParams;
use genetic_algorithms::population::Population;
use genetic_algorithms::stats::GenerationStats;
#[cfg(feature = "observer-metrics")]
use genetic_algorithms::MetricsObserver;
use genetic_algorithms::{rng, CompositeObserver};
use std::sync::Arc;

fn main() {
    let _ = env_logger::try_init();
    // Parse optional --seed <N> argument for reproducible runs (used by build_perf.sh golden capture).
    // When --seed is provided, rayon is fixed to 1 thread so the counter-based RNG in
    // rng::make_rng() is called in a deterministic order. The seed is also passed via
    // with_rng_seed() so the GA engine re-applies it at run() time (the engine resets the seed
    // internally, overriding any set_seed() call made before build()).
    let args: Vec<String> = std::env::args().collect();
    let seed_opt: Option<u64> = args
        .iter()
        .position(|a| a == "--seed")
        .and_then(|pos| args.get(pos + 1))
        .and_then(|v| v.parse::<u64>().ok());

    if let Some(s) = seed_opt {
        #[cfg(all(not(target_arch = "wasm32"), feature = "parallel"))]
        {
            // Fix rayon to single thread for reproducible RNG counter ordering.
            rayon::ThreadPoolBuilder::new()
                .num_threads(1)
                .build_global()
                .ok(); // ignore error if pool already initialized
        }
        // Pre-set seed for initialization function (runs before ga.run()).
        rng::set_seed(Some(s));
    }

    // --- Problem parameters ---
    const DIMENSIONS: usize = 5;
    const POP_SIZE: usize = 100;
    const MAX_GENERATIONS: usize = 500;

    // --- Fitness function: Rastrigin function (minimize toward 0.0 at origin) ---
    // f(x) = A*n + sum(x_i^2 - A*cos(2*pi*x_i))  where A = 10
    let fitness_fn = |dna: &[RangeGenotype<f64>]| -> f64 {
        let a = 10.0;
        let n = dna.len() as f64;
        a * n
            + dna
                .iter()
                .map(|g| g.value.powi(2) - a * (2.0 * std::f64::consts::PI * g.value).cos())
                .sum::<f64>()
    };

    // --- Allele range: each dimension in [-5.12, 5.12] ---
    let alleles = vec![RangeGenotype::new(0, vec![(-5.12, 5.12)], 0.0_f64)];
    let alleles_clone = alleles.clone();

    // --- Build composite observer (LogObserver always active; MetricsObserver when feature flag set) ---
    let composite = CompositeObserver::new().register(Arc::new(LogObserver));
    #[cfg(feature = "observer-metrics")]
    let composite = composite.register(Arc::new(MetricsObserver::new("rastrigin")));

    // --- Build the GA configuration ---
    let mut ga_builder = Ga::new()
        // Chromosome: DIMENSIONS genes, each a continuous value in [-5.12, 5.12]
        .with_chromosome_length(ChromosomeLength::Fixed(DIMENSIONS))
        .with_population_size(POP_SIZE)
        // Random initialization within allele bounds
        .with_initialization_fn(move |genes_per_chromosome, _| {
            range_random_initialization(genes_per_chromosome, Some(&alleles_clone))
        })
        .with_fitness_fn(fitness_fn)
        // Selection: Tournament (pressure-based, good for continuous landscapes)
        .with_selection_method(Selection::Tournament)
        // Crossover: Uniform blend
        .with_crossover_method(Crossover::Uniform)
        // Mutation: Gaussian perturbation (appropriate for continuous optimization)
        .with_mutation_method(Mutation::Gaussian(GaussianParams { sigma: None }))
        // Survivor selection: Fitness-based
        .with_survivor_method(Survivor::Fitness)
        // Problem: minimize fitness toward 0.0
        .with_problem_solving(ProblemSolving::Minimization)
        .with_max_generations(MAX_GENERATIONS)
        // Observer: CompositeObserver fans out to LogObserver (and MetricsObserver if feature enabled)
        .with_observer(Arc::new(composite));

    // Wire seed into GA engine so run() re-applies it (engine resets seed internally at run time).
    if let Some(s) = seed_opt {
        ga_builder = ga_builder.with_rng_seed(s);
    }

    let mut ga = ga_builder
        .build()
        .expect("Failed to build GA configuration");

    println!("== Rastrigin Continuous Optimization ==");
    println!(
        "Chromosome: {} dimensions, Population: {}, Max generations: {}",
        DIMENSIONS, POP_SIZE, MAX_GENERATIONS
    );
    println!("Operators: Selection=Tournament, Crossover=Uniform, Mutation=Gaussian");
    println!("-------------------------------------------------------");

    // --- Run the GA with a callback to report progress ---
    let report_interval = 50;

    // Stats accumulator for --plot (visualization feature); move added for accumulator capture
    #[cfg(feature = "visualization")]
    let plot_stats: std::sync::Arc<
        std::sync::Mutex<Vec<genetic_algorithms::stats::GenerationStats>>,
    > = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
    #[cfg(feature = "visualization")]
    let plot_stats_clone = plot_stats.clone();

    let result = ga.run_with_callback(
        Some(
            move |gen: &usize,
                  pop: &Population<RangeChromosome<f64>>,
                  _stats: &GenerationStats,
                  _cause: &TerminationCause|
                  -> std::ops::ControlFlow<()> {
                let avg_fitness =
                    pop.chromosomes.iter().map(|c| c.fitness()).sum::<f64>() / pop.size() as f64;
                println!(
                    "Generation {:4}: best = {:8.4}, avg = {:8.4}",
                    gen, pop.best_chromosome.fitness, avg_fitness
                );
                #[cfg(feature = "visualization")]
                if let Ok(mut guard) = plot_stats_clone.lock() {
                    guard.push(_stats.clone());
                }
                std::ops::ControlFlow::Continue(())
            },
        ),
        report_interval,
    );

    // --- Show the final result ---
    match result {
        Ok(population) => {
            println!("-------------------------------------------------------");
            println!(
                "Finished. Best fitness: {:.6}",
                population.best_chromosome.fitness
            );
            if population.best_chromosome.fitness < 1.0 {
                println!("Near-optimal solution found!");
            } else {
                println!("Did not reach optimum. Try increasing generations or population size.");
            }

            #[cfg(feature = "visualization")]
            if std::env::args().any(|a| a == "--plot") {
                // requires --features visualization
                let stats_guard = plot_stats.lock().unwrap_or_else(|e| e.into_inner());
                let stats = &*stats_guard;
                std::fs::create_dir_all("docs/images").expect("failed to create docs/images");
                genetic_algorithms::visualization::plot_fitness(stats, "docs/images/rastrigin.png")
                    .expect("plot failed");
                println!("Fitness plot saved to docs/images/rastrigin.png");
            }
        }
        Err(e) => {
            println!("GA failed: {:?}", e);
        }
    }
}