//! `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). /// /// # Example /// /// ``` /// use heuropt::prelude::*; /// /// struct Sphere; /// impl Problem for Sphere { /// type Decision = Vec; /// fn objectives(&self) -> ObjectiveSpace { /// ObjectiveSpace::new(vec![Objective::minimize("f")]) /// } /// fn evaluate(&self, x: &Vec) -> Evaluation { /// Evaluation::new(vec![x.iter().map(|v| v * v).sum::()]) /// } /// } /// /// let bounds = vec![(-5.0_f64, 5.0_f64); 3]; /// let mut opt = GeneticAlgorithm::new( /// GeneticAlgorithmConfig { /// population_size: 30, /// generations: 50, /// tournament_size: 2, /// elitism: 2, /// seed: 42, /// }, /// RealBounds::new(bounds.clone()), /// CompositeVariation { /// crossover: SimulatedBinaryCrossover::new(bounds.clone(), 15.0, 0.5), /// mutation: PolynomialMutation::new(bounds, 20.0, 1.0), /// }, /// ); /// let r = opt.run(&Sphere); /// assert!(r.best.is_some()); /// ``` #[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, ) } } #[cfg(feature = "async")] impl GeneticAlgorithm { /// Async version of [`Optimizer::run`] — drives evaluations through /// the user-chosen async runtime. Available only with the `async` /// feature. /// /// `concurrency` bounds in-flight evaluations per batch (initial /// population and per-generation offspring). pub async fn run_async

( &mut self, problem: &P, concurrency: usize, ) -> OptimizationResult where P: crate::core::async_problem::AsyncProblem, I: Initializer, V: Variation, { use crate::algorithms::parallel_eval_async::evaluate_batch_async; 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_async(problem, initial_decisions, concurrency).await; let mut evaluations = population.len(); for _ in 0..self.config.generations { 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); } } let offspring = evaluate_batch_async(problem, offspring_decisions, concurrency).await; evaluations += offspring.len(); 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), }, } } impl crate::traits::AlgorithmInfo for GeneticAlgorithm { fn name(&self) -> &'static str { "GA" } fn full_name(&self) -> &'static str { "Genetic Algorithm" } fn seed(&self) -> Option { Some(self.config.seed) } } #[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< RealBounds, CompositeVariation, > { 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); } // ---- Mutation-test pinned helpers -------------------------------------- use crate::core::candidate::Candidate; use crate::core::evaluation::Evaluation; fn fc(obj: f64) -> Candidate { Candidate::new(0, Evaluation::new(vec![obj])) } fn fc_cv(obj: f64, cv: f64) -> Candidate { Candidate::new(0, Evaluation::constrained(vec![obj], cv)) } #[test] fn compare_for_fitness_feasibility_first() { let feasible = fc(100.0); let infeasible = fc_cv(0.0, 1.0); assert_eq!( compare_for_fitness(&feasible, &infeasible, Direction::Minimize), std::cmp::Ordering::Less, ); assert_eq!( compare_for_fitness(&infeasible, &feasible, Direction::Minimize), std::cmp::Ordering::Greater, ); } #[test] fn compare_for_fitness_two_feasible_min_and_max() { let lo = fc(1.0); let hi = fc(2.0); assert_eq!( compare_for_fitness(&lo, &hi, Direction::Minimize), std::cmp::Ordering::Less ); assert_eq!( compare_for_fitness(&lo, &hi, Direction::Maximize), std::cmp::Ordering::Greater ); } #[test] fn compare_for_fitness_two_infeasible_lower_violation_wins() { let low = fc_cv(0.0, 0.3); let high = fc_cv(0.0, 0.9); assert_eq!( compare_for_fitness(&low, &high, Direction::Minimize), std::cmp::Ordering::Less ); } /// `survival_selection` carries `elitism` parents and `n - elitism` /// offspring, each set sorted best-first. Pin the exact composition. #[test] fn survival_selection_keeps_elites_and_best_offspring() { // Parents: objectives 5, 1, 9 → best is 1. let parents = vec![fc(5.0), fc(1.0), fc(9.0)]; // Offspring: objectives 4, 2, 8 → best two are 2, 4. let offspring = vec![fc(4.0), fc(2.0), fc(8.0)]; let next = survival_selection(&parents, offspring, Direction::Minimize, 3, 1); assert_eq!(next.len(), 3); // 1 elite (best parent = 1.0) + 2 best offspring (2.0, 4.0). assert_eq!(next[0].evaluation.objectives[0], 1.0); assert_eq!(next[1].evaluation.objectives[0], 2.0); assert_eq!(next[2].evaluation.objectives[0], 4.0); } #[test] fn survival_selection_zero_elitism_is_all_offspring() { let parents = vec![fc(1.0)]; let offspring = vec![fc(9.0), fc(3.0)]; let next = survival_selection(&parents, offspring, Direction::Minimize, 2, 0); assert_eq!(next.len(), 2); // No elites — both slots come from offspring, best-first. assert_eq!(next[0].evaluation.objectives[0], 3.0); assert_eq!(next[1].evaluation.objectives[0], 9.0); } }