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