From b97ec4f7ab263c6a827802e278ca9bcab7fcfd7f Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Mon, 4 May 2026 19:21:36 -0600 Subject: [PATCH] feat(operators): add BitFlipMutation for Vec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Variation that clones the first parent and flips each bit independently with probability `probability`. Panics if probability is outside [0, 1] (spec §11.3). Tests verify that probability=0 produces an unchanged child and probability=1 flips every bit (spec §18.3). --- src/operators/binary.rs | 70 +++++++++++++++++++++++++++++++++++++++++ src/operators/mod.rs | 2 ++ src/prelude.rs | 2 +- 3 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 src/operators/binary.rs diff --git a/src/operators/binary.rs b/src/operators/binary.rs new file mode 100644 index 0000000..1bd83d0 --- /dev/null +++ b/src/operators/binary.rs @@ -0,0 +1,70 @@ +//! Operators for binary (`Vec`) decisions. + +use rand::Rng as _; + +use crate::core::rng::Rng; +use crate::traits::Variation; + +/// Flip each bit of the first parent independently with probability `p`. +/// +/// Always returns exactly one child (spec §11.3). Panics if `probability` is +/// outside `[0.0, 1.0]` or if no parents are provided. +#[derive(Debug, Clone)] +pub struct BitFlipMutation { + /// Per-bit flip probability. Must lie in `[0.0, 1.0]`. + pub probability: f64, +} + +impl Variation> for BitFlipMutation { + fn vary(&mut self, parents: &[Vec], rng: &mut Rng) -> Vec> { + assert!( + (0.0..=1.0).contains(&self.probability), + "BitFlipMutation probability must be in [0.0, 1.0]", + ); + assert!( + !parents.is_empty(), + "BitFlipMutation requires at least one parent", + ); + let mut child = parents[0].clone(); + for bit in child.iter_mut() { + if rng.random_bool(self.probability) { + *bit = !*bit; + } + } + vec![child] + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::rng::rng_from_seed; + + #[test] + fn probability_zero_changes_nothing() { + let mut m = BitFlipMutation { probability: 0.0 }; + let mut rng = rng_from_seed(0); + let parent = vec![false, true, false, true, true]; + let children = m.vary(&[parent.clone()], &mut rng); + assert_eq!(children.len(), 1); + assert_eq!(children[0], parent); + } + + #[test] + fn probability_one_flips_everything() { + let mut m = BitFlipMutation { probability: 1.0 }; + let mut rng = rng_from_seed(0); + let parent = vec![false, true, false, true, true]; + let children = m.vary(&[parent.clone()], &mut rng); + let expected: Vec = parent.iter().map(|b| !b).collect(); + assert_eq!(children[0], expected); + } + + #[test] + #[should_panic(expected = "must be in [0.0, 1.0]")] + fn probability_above_one_panics() { + let mut m = BitFlipMutation { probability: 1.5 }; + let mut rng = rng_from_seed(0); + m.vary(&[vec![true]], &mut rng); + } +} diff --git a/src/operators/mod.rs b/src/operators/mod.rs index c5fde37..36849f7 100644 --- a/src/operators/mod.rs +++ b/src/operators/mod.rs @@ -1,5 +1,7 @@ //! Built-in operators for common decision types. +pub mod binary; pub mod real; +pub use binary::*; pub use real::*; diff --git a/src/prelude.rs b/src/prelude.rs index 25311a0..aa439a4 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -16,4 +16,4 @@ pub use crate::pareto::{ pareto_compare, pareto_front, }; -pub use crate::operators::{GaussianMutation, RealBounds}; +pub use crate::operators::{BitFlipMutation, GaussianMutation, RealBounds};