feat(operators): add RealBounds initializer and GaussianMutation

`RealBounds` (Initializer<Vec<f64>>) samples each variable uniformly in
its inclusive (lo, hi) range; panics if any bound has lo > hi
(spec §11.1).

`GaussianMutation` (Variation<Vec<f64>>) clones the first parent and
adds Normal(0, sigma) noise to every element; panics on sigma <= 0.0;
does not enforce bounds in v1 (spec §11.2).
This commit is contained in:
2026-05-04 19:21:30 -06:00
parent 663ed0ae58
commit 113a7342f8
4 changed files with 141 additions and 0 deletions
+1
View File
@@ -3,6 +3,7 @@
//! full design.
pub mod core;
pub mod operators;
pub mod pareto;
pub mod prelude;
pub mod traits;
+5
View File
@@ -0,0 +1,5 @@
//! Built-in operators for common decision types.
pub mod real;
pub use real::*;
+133
View File
@@ -0,0 +1,133 @@
//! Operators for real-valued (`Vec<f64>`) decisions.
use rand::Rng as _;
use rand_distr::{Distribution, Normal};
use crate::core::rng::Rng;
use crate::traits::{Initializer, Variation};
/// Uniformly initialize `Vec<f64>` decisions within per-variable bounds.
///
/// Bounds are inclusive `(lo, hi)` ranges per dimension. Panics if any bound
/// has `lo > hi` (spec §11.1).
#[derive(Debug, Clone)]
pub struct RealBounds {
/// Per-variable inclusive bounds in decision order.
pub bounds: Vec<(f64, f64)>,
}
impl RealBounds {
/// Create a `RealBounds` initializer.
///
/// # Panics
/// If any `(lo, hi)` has `lo > hi`.
pub fn new(bounds: Vec<(f64, f64)>) -> Self {
for (i, &(lo, hi)) in bounds.iter().enumerate() {
assert!(
lo <= hi,
"RealBounds bound at index {i} has lo > hi: ({lo}, {hi})",
);
}
Self { bounds }
}
}
impl Initializer<Vec<f64>> for RealBounds {
fn initialize(&mut self, size: usize, rng: &mut Rng) -> Vec<Vec<f64>> {
let mut out = Vec::with_capacity(size);
for _ in 0..size {
let mut decision = Vec::with_capacity(self.bounds.len());
for &(lo, hi) in &self.bounds {
let v = if lo == hi { lo } else { rng.random_range(lo..=hi) };
decision.push(v);
}
out.push(decision);
}
out
}
}
/// Add `Normal(0, sigma)` noise to every variable of the first parent.
///
/// Always returns exactly one child. Does not enforce bounds in v1 (spec §11.2).
#[derive(Debug, Clone)]
pub struct GaussianMutation {
/// Standard deviation of the Gaussian noise. Must be positive.
pub sigma: f64,
}
impl Variation<Vec<f64>> for GaussianMutation {
fn vary(&mut self, parents: &[Vec<f64>], rng: &mut Rng) -> Vec<Vec<f64>> {
assert!(self.sigma > 0.0, "GaussianMutation sigma must be positive");
assert!(
!parents.is_empty(),
"GaussianMutation requires at least one parent",
);
let normal =
Normal::new(0.0, self.sigma).expect("Normal distribution rejected sigma");
let mut child = parents[0].clone();
for x in child.iter_mut() {
*x += normal.sample(rng);
}
vec![child]
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::rng::rng_from_seed;
#[test]
fn real_bounds_returns_correct_shape_and_range() {
let mut init = RealBounds::new(vec![(-1.0, 1.0), (0.0, 10.0)]);
let mut rng = rng_from_seed(7);
let decisions = init.initialize(5, &mut rng);
assert_eq!(decisions.len(), 5);
for d in &decisions {
assert_eq!(d.len(), 2);
assert!(d[0] >= -1.0 && d[0] <= 1.0);
assert!(d[1] >= 0.0 && d[1] <= 10.0);
}
}
#[test]
fn real_bounds_equal_bounds_yield_constant() {
let mut init = RealBounds::new(vec![(2.5, 2.5)]);
let mut rng = rng_from_seed(1);
let d = init.initialize(3, &mut rng);
assert!(d.iter().all(|v| v == &vec![2.5]));
}
#[test]
#[should_panic(expected = "lo > hi")]
fn real_bounds_invalid_panics() {
RealBounds::new(vec![(1.0, 0.0)]);
}
#[test]
fn gaussian_mutation_returns_one_child_same_length() {
let mut m = GaussianMutation { sigma: 0.1 };
let mut rng = rng_from_seed(99);
let parents = vec![vec![0.0_f64, 1.0, 2.0]];
let children = m.vary(&parents, &mut rng);
assert_eq!(children.len(), 1);
assert_eq!(children[0].len(), 3);
}
#[test]
#[should_panic(expected = "sigma must be positive")]
fn gaussian_mutation_zero_sigma_panics() {
let mut m = GaussianMutation { sigma: 0.0 };
let mut rng = rng_from_seed(1);
m.vary(&[vec![0.0]], &mut rng);
}
#[test]
#[should_panic(expected = "at least one parent")]
fn gaussian_mutation_empty_parents_panics() {
let mut m = GaussianMutation { sigma: 0.1 };
let mut rng = rng_from_seed(1);
m.vary(&[] as &[Vec<f64>], &mut rng);
}
}
+2
View File
@@ -15,3 +15,5 @@ pub use crate::pareto::{
Dominance, ParetoArchive, best_candidate, crowding_distance, non_dominated_sort,
pareto_compare, pareto_front,
};
pub use crate::operators::{GaussianMutation, RealBounds};