From f77e163ac44efc8389f23abe4fe6b7969b313f93 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Tue, 5 May 2026 07:53:39 -0600 Subject: [PATCH] =?UTF-8?q?feat(algorithms):=20add=20GeneticAlgorithm=20?= =?UTF-8?q?=E2=80=94=20single-objective=20generational=20GA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Canonical generational GA with elitism: each generation runs binary tournament selection (using `tournament_select_single_objective`) on the current population, applies the variation operator pair-wise to produce offspring, evaluates them, then replaces the population while preserving the top `elitism` members from the previous generation (elitism prevents fitness regression on a single seed). Single-objective only. Generic over decision type — pair with `SimulatedBinaryCrossover + PolynomialMutation` for real-valued, single-point crossover + bit-flip for binary, etc. Tests: convergence on Sphere1D, deterministic reruns, panic on multi-objective, panic on `population_size < 2`, panic on `elitism > population_size`. --- src/algorithms/genetic_algorithm.rs | 279 ++++++++++++++++++++++++++++ src/algorithms/mod.rs | 2 + src/prelude.rs | 8 +- 3 files changed, 285 insertions(+), 4 deletions(-) create mode 100644 src/algorithms/genetic_algorithm.rs diff --git a/src/algorithms/genetic_algorithm.rs b/src/algorithms/genetic_algorithm.rs new file mode 100644 index 0000000..ed649bb --- /dev/null +++ b/src/algorithms/genetic_algorithm.rs @@ -0,0 +1,279 @@ +//! `GeneticAlgorithm` — single-objective generational GA with elitism. + +use crate::algorithms::parallel_eval::evaluate_batch; +use crate::core::candidate::Candidate; +use crate::core::objective::Direction; +use crate::core::population::Population; +use crate::core::problem::Problem; +use crate::core::result::OptimizationResult; +use crate::core::rng::rng_from_seed; +use crate::pareto::front::best_candidate; +use crate::selection::tournament::tournament_select_single_objective; +use crate::traits::{Initializer, Optimizer, Variation}; + +/// Configuration for [`GeneticAlgorithm`]. +#[derive(Debug, Clone)] +pub struct GeneticAlgorithmConfig { + /// Constant population size. + pub population_size: usize, + /// Number of generations. + pub generations: usize, + /// Tournament size for parent selection (typical: 2). + pub tournament_size: usize, + /// Number of elite members to carry over each generation (must be + /// `< population_size`). + pub elitism: usize, + /// Seed for the deterministic RNG. + pub seed: u64, +} + +impl Default for GeneticAlgorithmConfig { + fn default() -> Self { + Self { + population_size: 100, + generations: 200, + tournament_size: 2, + elitism: 2, + seed: 42, + } + } +} + +/// Single-objective generational genetic algorithm with elitism. +/// +/// Each generation: binary tournament selection (on the configured +/// `tournament_size`) chooses parent pairs, the variation operator +/// produces offspring, those are evaluated, and the next population is +/// the top `elitism` from the previous generation plus the best +/// `population_size - elitism` offspring (by fitness). +#[derive(Debug, Clone)] +pub struct GeneticAlgorithm { + /// Algorithm configuration. + pub config: GeneticAlgorithmConfig, + /// Initial-decision sampler. + pub initializer: I, + /// Offspring-producing variation operator. + pub variation: V, +} + +impl GeneticAlgorithm { + /// Construct a `GeneticAlgorithm`. + pub fn new(config: GeneticAlgorithmConfig, initializer: I, variation: V) -> Self { + Self { config, initializer, variation } + } +} + +impl Optimizer

for GeneticAlgorithm +where + P: Problem + Sync, + P::Decision: Send, + I: Initializer, + V: Variation, +{ + fn run(&mut self, problem: &P) -> OptimizationResult { + assert!( + self.config.population_size >= 2, + "GeneticAlgorithm population_size must be >= 2", + ); + assert!( + self.config.tournament_size >= 1, + "GeneticAlgorithm tournament_size must be >= 1", + ); + assert!( + self.config.elitism < self.config.population_size, + "GeneticAlgorithm elitism must be < population_size", + ); + let n = self.config.population_size; + let objectives = problem.objectives(); + assert!( + objectives.is_single_objective(), + "GeneticAlgorithm requires exactly one objective", + ); + let direction = objectives.objectives[0].direction; + let mut rng = rng_from_seed(self.config.seed); + + let initial_decisions = self.initializer.initialize(n, &mut rng); + let mut population: Vec> = + evaluate_batch(problem, initial_decisions); + let mut evaluations = population.len(); + + for _ in 0..self.config.generations { + // --- Phase 1: parent selection + variation (serial RNG) --- + let mut offspring_decisions: Vec = Vec::with_capacity(n); + while offspring_decisions.len() < n { + let parents_decisions = tournament_select_single_objective( + &population, + &objectives, + self.config.tournament_size, + 2, + &mut rng, + ); + let children = self.variation.vary(&parents_decisions, &mut rng); + assert!(!children.is_empty(), "GeneticAlgorithm variation returned no children"); + for child in children { + if offspring_decisions.len() >= n { + break; + } + offspring_decisions.push(child); + } + } + + // --- Phase 2: parallel-friendly batch evaluation --- + let offspring = evaluate_batch(problem, offspring_decisions); + evaluations += offspring.len(); + + // --- Phase 3: survival = elites + best offspring --- + population = survival_selection( + &population, + offspring, + direction, + n, + self.config.elitism, + ); + } + + let best = best_candidate(&population, &objectives); + let front: Vec> = best.iter().cloned().collect(); + OptimizationResult::new( + Population::new(population), + front, + best, + evaluations, + self.config.generations, + ) + } +} + +fn survival_selection( + parents: &[Candidate], + offspring: Vec>, + direction: Direction, + n: usize, + elitism: usize, +) -> Vec> { + // Sort the parents by fitness descending (best first). + let mut sorted_parents: Vec> = parents.to_vec(); + sorted_parents.sort_by(|a, b| compare_for_fitness(a, b, direction)); + + // Sort the offspring the same way. + let mut sorted_offspring = offspring; + sorted_offspring.sort_by(|a, b| compare_for_fitness(a, b, direction)); + + let mut next: Vec> = Vec::with_capacity(n); + next.extend(sorted_parents.into_iter().take(elitism)); + next.extend(sorted_offspring.into_iter().take(n - elitism)); + next +} + +/// Order such that "best" comes first. Feasible beats infeasible; among +/// infeasibles, lower violation wins; among feasibles, direction-aware +/// objective comparison. +fn compare_for_fitness( + a: &Candidate, + b: &Candidate, + direction: Direction, +) -> std::cmp::Ordering { + match (a.evaluation.is_feasible(), b.evaluation.is_feasible()) { + (true, false) => std::cmp::Ordering::Less, + (false, true) => std::cmp::Ordering::Greater, + (false, false) => a + .evaluation + .constraint_violation + .partial_cmp(&b.evaluation.constraint_violation) + .unwrap_or(std::cmp::Ordering::Equal), + (true, true) => match direction { + Direction::Minimize => a.evaluation.objectives[0] + .partial_cmp(&b.evaluation.objectives[0]) + .unwrap_or(std::cmp::Ordering::Equal), + Direction::Maximize => b.evaluation.objectives[0] + .partial_cmp(&a.evaluation.objectives[0]) + .unwrap_or(std::cmp::Ordering::Equal), + }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::operators::{ + CompositeVariation, PolynomialMutation, RealBounds, SimulatedBinaryCrossover, + }; + use crate::tests_support::{SchafferN1, Sphere1D}; + + fn make_optimizer( + seed: u64, + ) -> GeneticAlgorithm> + { + let bounds = vec![(-5.0, 5.0)]; + let initializer = RealBounds::new(bounds.clone()); + let variation = CompositeVariation { + crossover: SimulatedBinaryCrossover::new(bounds.clone(), 15.0, 0.5), + mutation: PolynomialMutation::new(bounds, 20.0, 1.0), + }; + GeneticAlgorithm::new( + GeneticAlgorithmConfig { + population_size: 30, + generations: 50, + tournament_size: 2, + elitism: 2, + seed, + }, + initializer, + variation, + ) + } + + #[test] + fn finds_minimum_of_sphere() { + let mut opt = make_optimizer(1); + let r = opt.run(&Sphere1D); + let best = r.best.unwrap(); + assert!( + best.evaluation.objectives[0] < 1e-2, + "got f = {}", + best.evaluation.objectives[0], + ); + } + + #[test] + fn deterministic_with_same_seed() { + let mut a = make_optimizer(99); + let mut b = make_optimizer(99); + let ra = a.run(&Sphere1D); + let rb = b.run(&Sphere1D); + assert_eq!( + ra.best.unwrap().evaluation.objectives, + rb.best.unwrap().evaluation.objectives, + ); + } + + #[test] + #[should_panic(expected = "exactly one objective")] + fn multi_objective_panics() { + let mut opt = make_optimizer(0); + let _ = opt.run(&SchafferN1); + } + + #[test] + #[should_panic(expected = "elitism must be < population_size")] + fn elitism_too_large_panics() { + let bounds = vec![(-1.0, 1.0)]; + let initializer = RealBounds::new(bounds.clone()); + let variation = CompositeVariation { + crossover: SimulatedBinaryCrossover::new(bounds.clone(), 15.0, 0.5), + mutation: PolynomialMutation::new(bounds, 20.0, 1.0), + }; + let mut opt = GeneticAlgorithm::new( + GeneticAlgorithmConfig { + population_size: 4, + generations: 1, + tournament_size: 2, + elitism: 4, + seed: 0, + }, + initializer, + variation, + ); + let _ = opt.run(&Sphere1D); + } +} diff --git a/src/algorithms/mod.rs b/src/algorithms/mod.rs index 2875d8e..37234e5 100644 --- a/src/algorithms/mod.rs +++ b/src/algorithms/mod.rs @@ -1,6 +1,7 @@ //! Built-in reference optimizers. pub mod differential_evolution; +pub mod genetic_algorithm; pub mod hill_climber; pub mod moead; pub mod nsga2; @@ -12,6 +13,7 @@ pub mod simulated_annealing; pub mod spea2; pub use differential_evolution::*; +pub use genetic_algorithm::*; pub use hill_climber::*; pub use moead::*; pub use nsga2::*; diff --git a/src/prelude.rs b/src/prelude.rs index 72388c1..b13f469 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -22,8 +22,8 @@ pub use crate::operators::{ }; pub use crate::algorithms::{ - DifferentialEvolution, DifferentialEvolutionConfig, HillClimber, HillClimberConfig, - Moead, MoeadConfig, Nsga2, Nsga2Config, Nsga3, Nsga3Config, Paes, PaesConfig, - RandomSearch, RandomSearchConfig, SimulatedAnnealing, SimulatedAnnealingConfig, - Spea2, Spea2Config, + DifferentialEvolution, DifferentialEvolutionConfig, GeneticAlgorithm, + GeneticAlgorithmConfig, HillClimber, HillClimberConfig, Moead, MoeadConfig, Nsga2, + Nsga2Config, Nsga3, Nsga3Config, Paes, PaesConfig, RandomSearch, RandomSearchConfig, + SimulatedAnnealing, SimulatedAnnealingConfig, Spea2, Spea2Config, };