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.
|
//! Built-in operators for common decision types.
|
||||||
|
|
||||||
pub mod binary;
|
pub mod binary;
|
||||||
|
pub mod composite;
|
||||||
pub mod permutation;
|
pub mod permutation;
|
||||||
pub mod real;
|
pub mod real;
|
||||||
|
|
||||||
pub use binary::*;
|
pub use binary::*;
|
||||||
|
pub use composite::*;
|
||||||
pub use permutation::*;
|
pub use permutation::*;
|
||||||
pub use real::*;
|
pub use real::*;
|
||||||
|
|||||||
+2
-2
@@ -17,8 +17,8 @@ pub use crate::pareto::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub use crate::operators::{
|
pub use crate::operators::{
|
||||||
BitFlipMutation, BoundedGaussianMutation, GaussianMutation, PolynomialMutation,
|
BitFlipMutation, BoundedGaussianMutation, CompositeVariation, GaussianMutation,
|
||||||
RealBounds, SimulatedBinaryCrossover, SwapMutation,
|
PolynomialMutation, RealBounds, SimulatedBinaryCrossover, SwapMutation,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use crate::algorithms::{
|
pub use crate::algorithms::{
|
||||||
|
|||||||
Reference in New Issue
Block a user