feat(operators): add BoundedGaussianMutation
A bounded variant of GaussianMutation: same Gaussian noise applied to the first parent, but every variable is clamped to its per-dimension inclusive bound. Useful as a drop-in for problems that need feasibility maintained across generations rather than relying on clamp-inside-evaluate. Panics on `sigma <= 0.0`, on no parents, and on construction if any `(lo, hi)` has `lo > hi`. Decision length must match the bounds length when called.
This commit is contained in:
@@ -73,6 +73,59 @@ impl Variation<Vec<f64>> for GaussianMutation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bounded variant of [`GaussianMutation`]: add `Normal(0, sigma)` noise to
|
||||||
|
/// every variable of the first parent, then clamp each variable to its
|
||||||
|
/// per-dimension inclusive bound.
|
||||||
|
///
|
||||||
|
/// Always returns exactly one child. Use this when you want feasibility
|
||||||
|
/// maintained across generations without leaning on
|
||||||
|
/// clamp-inside-`Problem::evaluate`.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BoundedGaussianMutation {
|
||||||
|
/// Standard deviation of the Gaussian noise. Must be positive.
|
||||||
|
pub sigma: f64,
|
||||||
|
/// Per-variable inclusive bounds. Length must match the parent decision.
|
||||||
|
pub bounds: Vec<(f64, f64)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BoundedGaussianMutation {
|
||||||
|
/// Construct a `BoundedGaussianMutation`.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// If `sigma <= 0.0` or any bound has `lo > hi`.
|
||||||
|
pub fn new(sigma: f64, bounds: Vec<(f64, f64)>) -> Self {
|
||||||
|
assert!(sigma > 0.0, "BoundedGaussianMutation sigma must be positive");
|
||||||
|
for (i, &(lo, hi)) in bounds.iter().enumerate() {
|
||||||
|
assert!(
|
||||||
|
lo <= hi,
|
||||||
|
"BoundedGaussianMutation bound at index {i} has lo > hi: ({lo}, {hi})",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Self { sigma, bounds }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Variation<Vec<f64>> for BoundedGaussianMutation {
|
||||||
|
fn vary(&mut self, parents: &[Vec<f64>], rng: &mut Rng) -> Vec<Vec<f64>> {
|
||||||
|
assert!(
|
||||||
|
!parents.is_empty(),
|
||||||
|
"BoundedGaussianMutation requires at least one parent",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parents[0].len(),
|
||||||
|
self.bounds.len(),
|
||||||
|
"BoundedGaussianMutation parent length must match bounds length",
|
||||||
|
);
|
||||||
|
let normal =
|
||||||
|
Normal::new(0.0, self.sigma).expect("Normal distribution rejected sigma");
|
||||||
|
let mut child = parents[0].clone();
|
||||||
|
for (x, &(lo, hi)) in child.iter_mut().zip(self.bounds.iter()) {
|
||||||
|
*x = (*x + normal.sample(rng)).clamp(lo, hi);
|
||||||
|
}
|
||||||
|
vec![child]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -130,4 +183,40 @@ mod tests {
|
|||||||
let mut rng = rng_from_seed(1);
|
let mut rng = rng_from_seed(1);
|
||||||
m.vary(&[] as &[Vec<f64>], &mut rng);
|
m.vary(&[] as &[Vec<f64>], &mut rng);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bounded_gaussian_keeps_child_in_bounds() {
|
||||||
|
let mut m = BoundedGaussianMutation::new(5.0, vec![(-1.0, 1.0); 4]);
|
||||||
|
let mut rng = rng_from_seed(0);
|
||||||
|
let parent = vec![0.0_f64; 4];
|
||||||
|
// sigma=5 against bounds [-1, 1] guarantees clamping fires.
|
||||||
|
for _ in 0..100 {
|
||||||
|
let children = m.vary(std::slice::from_ref(&parent), &mut rng);
|
||||||
|
assert_eq!(children.len(), 1);
|
||||||
|
assert_eq!(children[0].len(), 4);
|
||||||
|
for &x in &children[0] {
|
||||||
|
assert!(x >= -1.0 && x <= 1.0, "out of bounds: {x}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "sigma must be positive")]
|
||||||
|
fn bounded_gaussian_zero_sigma_panics() {
|
||||||
|
let _ = BoundedGaussianMutation::new(0.0, vec![(0.0, 1.0)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "lo > hi")]
|
||||||
|
fn bounded_gaussian_invalid_bounds_panics() {
|
||||||
|
let _ = BoundedGaussianMutation::new(0.1, vec![(1.0, 0.0)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "must match bounds length")]
|
||||||
|
fn bounded_gaussian_mismatched_length_panics() {
|
||||||
|
let mut m = BoundedGaussianMutation::new(0.1, vec![(0.0, 1.0); 3]);
|
||||||
|
let mut rng = rng_from_seed(0);
|
||||||
|
m.vary(&[vec![0.0; 2]], &mut rng);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -16,7 +16,9 @@ pub use crate::pareto::{
|
|||||||
pareto_compare, pareto_front,
|
pareto_compare, pareto_front,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use crate::operators::{BitFlipMutation, GaussianMutation, RealBounds, SwapMutation};
|
pub use crate::operators::{
|
||||||
|
BitFlipMutation, BoundedGaussianMutation, GaussianMutation, RealBounds, SwapMutation,
|
||||||
|
};
|
||||||
|
|
||||||
pub use crate::algorithms::{
|
pub use crate::algorithms::{
|
||||||
DifferentialEvolution, DifferentialEvolutionConfig, Nsga2, Nsga2Config, Paes, PaesConfig,
|
DifferentialEvolution, DifferentialEvolutionConfig, Nsga2, Nsga2Config, Paes, PaesConfig,
|
||||||
|
|||||||
Reference in New Issue
Block a user