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,
};