diff --git a/src/lib.rs b/src/lib.rs index 94dccd0..483060d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ //! full design. pub mod core; +pub mod operators; pub mod pareto; pub mod prelude; pub mod traits; diff --git a/src/operators/mod.rs b/src/operators/mod.rs new file mode 100644 index 0000000..c5fde37 --- /dev/null +++ b/src/operators/mod.rs @@ -0,0 +1,5 @@ +//! Built-in operators for common decision types. + +pub mod real; + +pub use real::*; diff --git a/src/operators/real.rs b/src/operators/real.rs new file mode 100644 index 0000000..5e3fbcf --- /dev/null +++ b/src/operators/real.rs @@ -0,0 +1,133 @@ +//! Operators for real-valued (`Vec`) decisions. + +use rand::Rng as _; +use rand_distr::{Distribution, Normal}; + +use crate::core::rng::Rng; +use crate::traits::{Initializer, Variation}; + +/// Uniformly initialize `Vec` 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> for RealBounds { + fn initialize(&mut self, size: usize, rng: &mut Rng) -> Vec> { + 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> for GaussianMutation { + fn vary(&mut self, parents: &[Vec], rng: &mut Rng) -> Vec> { + 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], &mut rng); + } +} diff --git a/src/prelude.rs b/src/prelude.rs index 148677b..25311a0 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -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};