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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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::{
|
||||
|
||||
Reference in New Issue
Block a user