From 0cbef6be1bce754c3b5864a1c4a4b24d99e22e2c Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Mon, 4 May 2026 19:21:55 -0600 Subject: [PATCH] feat(operators): add SwapMutation for permutations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Variation that clones the first parent (a Vec permutation) and swaps two distinct random indices when len >= 2 (spec §11.4). Tests confirm the multiset of contents is preserved. --- src/operators/mod.rs | 2 + src/operators/permutation.rs | 71 ++++++++++++++++++++++++++++++++++++ src/prelude.rs | 2 +- 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 src/operators/permutation.rs diff --git a/src/operators/mod.rs b/src/operators/mod.rs index 36849f7..a8fb0a2 100644 --- a/src/operators/mod.rs +++ b/src/operators/mod.rs @@ -1,7 +1,9 @@ //! Built-in operators for common decision types. pub mod binary; +pub mod permutation; pub mod real; pub use binary::*; +pub use permutation::*; pub use real::*; diff --git a/src/operators/permutation.rs b/src/operators/permutation.rs new file mode 100644 index 0000000..987648e --- /dev/null +++ b/src/operators/permutation.rs @@ -0,0 +1,71 @@ +//! Operators for permutation (`Vec`) decisions. + +use rand::Rng as _; + +use crate::core::rng::Rng; +use crate::traits::Variation; + +/// Swap two distinct random indices in the first parent (spec §11.4). +/// +/// If the parent has length `< 2` the child is returned unchanged. +#[derive(Debug, Clone, Copy, Default)] +pub struct SwapMutation; + +impl Variation> for SwapMutation { + fn vary(&mut self, parents: &[Vec], rng: &mut Rng) -> Vec> { + assert!( + !parents.is_empty(), + "SwapMutation requires at least one parent", + ); + let mut child = parents[0].clone(); + let n = child.len(); + if n >= 2 { + let i = rng.random_range(0..n); + let mut j = rng.random_range(0..n); + while j == i { + j = rng.random_range(0..n); + } + child.swap(i, j); + } + vec![child] + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::rng::rng_from_seed; + + fn sorted(mut v: Vec) -> Vec { + v.sort(); + v + } + + #[test] + fn preserves_multiset_contents() { + let mut m = SwapMutation; + let mut rng = rng_from_seed(11); + let parent = vec![0_usize, 1, 2, 3, 4]; + let children = m.vary(&[parent.clone()], &mut rng); + assert_eq!(children.len(), 1); + assert_eq!(sorted(children[0].clone()), sorted(parent)); + } + + #[test] + fn single_element_unchanged() { + let mut m = SwapMutation; + let mut rng = rng_from_seed(0); + let parent = vec![42_usize]; + let children = m.vary(&[parent.clone()], &mut rng); + assert_eq!(children[0], parent); + } + + #[test] + fn two_elements_always_swapped() { + let mut m = SwapMutation; + let mut rng = rng_from_seed(0); + let parent = vec![1_usize, 2]; + let children = m.vary(&[parent.clone()], &mut rng); + assert_eq!(children[0], vec![2, 1]); + } +} diff --git a/src/prelude.rs b/src/prelude.rs index aa439a4..1be8d1f 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::{BitFlipMutation, GaussianMutation, RealBounds}; +pub use crate::operators::{BitFlipMutation, GaussianMutation, RealBounds, SwapMutation};