feat(operators): add CompositeVariation pipeline (crossover → mutation)

Generic two-stage Variation operator: runs an inner crossover-style
operator on the parents, then applies an inner mutation-style operator
to each resulting child. Lets users build the canonical NSGA-II
operator stack — `SimulatedBinaryCrossover` followed by
`PolynomialMutation` — by composing the existing primitives instead
of bundling a one-off SbxPolyMut struct.

Lives in src/operators/composite.rs to keep type-specific operator
files unchanged. Generic over decision type and over both inner
operators.
This commit is contained in:
2026-05-04 19:44:58 -06:00
parent 464617b1f8
commit cc1b44b34e
3 changed files with 100 additions and 2 deletions
+96
View File
@@ -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<C, M> {
/// 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<D, C, M> Variation<D> for CompositeVariation<C, M>
where
D: Clone,
C: Variation<D>,
M: Variation<D>,
{
fn vary(&mut self, parents: &[D], rng: &mut Rng) -> Vec<D> {
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);
}
}
+2
View File
@@ -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::*;
+2 -2
View File
@@ -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::{