diff --git a/src/operators/composite.rs b/src/operators/composite.rs new file mode 100644 index 0000000..efdd494 --- /dev/null +++ b/src/operators/composite.rs @@ -0,0 +1,96 @@ +//! Compose two `Variation` operators into a pipeline (typically crossover → mutation). + +use crate::core::rng::Rng; +use crate::traits::Variation; + +/// A two-stage variation pipeline. +/// +/// On each call to `vary`: +/// +/// 1. The `crossover` operator is run on the input `parents`, producing one +/// or more children. +/// 2. For every child, the `mutation` operator is run with that child as its +/// sole parent, and the resulting children are concatenated into the +/// output. +/// +/// Use this to build the canonical NSGA-II operator stack — SBX followed by +/// polynomial mutation — out of the existing primitives: +/// +/// ```rust +/// use heuropt::prelude::*; +/// +/// let bounds = vec![(0.0, 1.0); 30]; +/// let variation = CompositeVariation { +/// crossover: SimulatedBinaryCrossover::new(bounds.clone(), 15.0, 0.5), +/// mutation: PolynomialMutation::new(bounds, 20.0, 1.0 / 30.0), +/// }; +/// let _ = variation; +/// ``` +#[derive(Debug, Clone)] +pub struct CompositeVariation { + /// First-stage operator; typically a crossover that consumes ≥ 2 parents. + pub crossover: C, + /// Second-stage operator; typically a mutation that consumes 1 parent. + pub mutation: M, +} + +impl Variation for CompositeVariation +where + D: Clone, + C: Variation, + M: Variation, +{ + fn vary(&mut self, parents: &[D], rng: &mut Rng) -> Vec { + let crossed = self.crossover.vary(parents, rng); + let mut out = Vec::with_capacity(crossed.len()); + for child in crossed { + let mutated = self.mutation.vary(std::slice::from_ref(&child), rng); + out.extend(mutated); + } + out + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::rng::rng_from_seed; + use crate::operators::real::{ + BoundedGaussianMutation, PolynomialMutation, SimulatedBinaryCrossover, + }; + + #[test] + fn pipes_sbx_into_polynomial_mutation() { + let bounds = vec![(-1.0, 1.0); 4]; + let mut variation = CompositeVariation { + crossover: SimulatedBinaryCrossover::new(bounds.clone(), 15.0, 1.0), + mutation: PolynomialMutation::new(bounds, 20.0, 0.25), + }; + let mut rng = rng_from_seed(123); + let p1 = vec![0.1, -0.2, 0.3, -0.4]; + let p2 = vec![-0.3, 0.4, -0.1, 0.2]; + let children = variation.vary(&[p1, p2], &mut rng); + // SBX produces 2 children; polynomial mutation produces 1 child each. + assert_eq!(children.len(), 2); + for c in &children { + assert_eq!(c.len(), 4); + for &x in c { + assert!(x >= -1.0 && x <= 1.0); + } + } + } + + #[test] + fn output_count_equals_inner_crossover_count_when_mutation_is_1to1() { + // BoundedGaussianMutation always returns 1 child. + let bounds = vec![(0.0, 1.0); 3]; + let mut variation = CompositeVariation { + crossover: SimulatedBinaryCrossover::new(bounds.clone(), 10.0, 0.5), + mutation: BoundedGaussianMutation::new(0.05, bounds), + }; + let mut rng = rng_from_seed(0); + let parents = vec![vec![0.5, 0.5, 0.5], vec![0.25, 0.75, 0.5]]; + let children = variation.vary(&parents, &mut rng); + assert_eq!(children.len(), 2); + } +} diff --git a/src/operators/mod.rs b/src/operators/mod.rs index a8fb0a2..f40dbda 100644 --- a/src/operators/mod.rs +++ b/src/operators/mod.rs @@ -1,9 +1,11 @@ //! Built-in operators for common decision types. pub mod binary; +pub mod composite; pub mod permutation; pub mod real; pub use binary::*; +pub use composite::*; pub use permutation::*; pub use real::*; diff --git a/src/prelude.rs b/src/prelude.rs index c3dd7a7..a924679 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -17,8 +17,8 @@ pub use crate::pareto::{ }; pub use crate::operators::{ - BitFlipMutation, BoundedGaussianMutation, GaussianMutation, PolynomialMutation, - RealBounds, SimulatedBinaryCrossover, SwapMutation, + BitFlipMutation, BoundedGaussianMutation, CompositeVariation, GaussianMutation, + PolynomialMutation, RealBounds, SimulatedBinaryCrossover, SwapMutation, }; pub use crate::algorithms::{