diff --git a/src/operators/permutation.rs b/src/operators/permutation.rs index 9b88c20..b8cbc5d 100644 --- a/src/operators/permutation.rs +++ b/src/operators/permutation.rs @@ -1,9 +1,134 @@ //! Operators for permutation (`Vec`) decisions. +//! +//! This module ships initializers, crossovers, and mutations that cover the +//! common permutation-encoded problem families: TSP and other strict-permutation +//! problems (cities labeled `0..n`), and JSS-style multiset / operation-string +//! encodings (each job id repeated `k` times). +//! +//! | Operator | Strict perm | Multiset / op-string | +//! |-----------------------------------|:-----------:|:--------------------:| +//! | `SwapMutation` | ✓ | ✓ | +//! | `InversionMutation` | ✓ | ✓ | +//! | `InsertionMutation` | ✓ | ✓ | +//! | `ScrambleMutation` | ✓ | ✓ | +//! | `OrderCrossover` (OX) | ✓ | ✗ | +//! | `PartiallyMappedCrossover` (PMX) | ✓ | ✗ | +//! | `CycleCrossover` (CX) | ✓ | ✗ | +//! | `EdgeRecombinationCrossover` | ✓ (`0..n`) | ✗ | +//! +//! The four crossovers all assume *strict* permutations — every value appears +//! exactly once. Combining them with multiset encodings (e.g., the +//! operation-based JSS encoding produced by [`ShuffledMultisetPermutation`]) +//! will break the multiset invariant. For multiset encodings, drive variation +//! with the mutation operators alone. use rand::Rng as _; +use rand::seq::SliceRandom; use crate::core::rng::Rng; -use crate::traits::Variation; +use crate::traits::{Initializer, Variation}; + +// --------------------------------------------------------------------------- +// Initializers +// --------------------------------------------------------------------------- + +/// Initializer that produces independent random shuffles of `[0..n)`. +/// +/// Use for any strict-permutation problem (TSP, single-machine scheduling, +/// QAP, …). +/// +/// # Example +/// +/// ``` +/// use heuropt::prelude::*; +/// +/// let mut rng = rng_from_seed(7); +/// let mut init = ShuffledPermutation { n: 5 }; +/// let pop = init.initialize(3, &mut rng); +/// assert_eq!(pop.len(), 3); +/// for p in &pop { +/// let mut sorted = p.clone(); +/// sorted.sort(); +/// assert_eq!(sorted, vec![0, 1, 2, 3, 4]); +/// } +/// ``` +#[derive(Debug, Clone, Copy)] +pub struct ShuffledPermutation { + /// Number of distinct elements; each produced shuffle is a permutation of `[0..n)`. + pub n: usize, +} + +impl Initializer> for ShuffledPermutation { + fn initialize(&mut self, size: usize, rng: &mut Rng) -> Vec> { + (0..size) + .map(|_| { + let mut p: Vec = (0..self.n).collect(); + p.shuffle(rng); + p + }) + .collect() + } +} + +/// Initializer that produces independent random shuffles of a multiset. +/// +/// The multiset is `[0]*r[0] ++ [1]*r[1] ++ ... ++ [k-1]*r[k-1]`, where +/// `r = repeats_per_id`. The canonical use is the operation-based encoding of +/// job-shop scheduling: for `n_jobs × n_machines` JSS, set +/// `repeats_per_id = vec![n_machines; n_jobs]` and each produced shuffle is a +/// valid operation order in which each job appears exactly `n_machines` times. +/// +/// # Example +/// +/// ``` +/// use heuropt::prelude::*; +/// +/// // 3 jobs × 2 machines: each job id 0, 1, 2 appears twice. +/// let mut rng = rng_from_seed(42); +/// let mut init = ShuffledMultisetPermutation::new(vec![2, 2, 2]); +/// let pop = init.initialize(4, &mut rng); +/// for p in &pop { +/// assert_eq!(p.len(), 6); +/// let mut counts = [0_usize; 3]; +/// for &v in p { counts[v] += 1; } +/// assert_eq!(counts, [2, 2, 2]); +/// } +/// ``` +#[derive(Debug, Clone)] +pub struct ShuffledMultisetPermutation { + /// Number of repetitions of each id; element `i` of the multiset appears + /// `repeats_per_id[i]` times. + pub repeats_per_id: Vec, +} + +impl ShuffledMultisetPermutation { + /// Construct a multiset initializer with the given per-id repetition counts. + pub fn new(repeats_per_id: Vec) -> Self { + Self { repeats_per_id } + } +} + +impl Initializer> for ShuffledMultisetPermutation { + fn initialize(&mut self, size: usize, rng: &mut Rng) -> Vec> { + let template: Vec = self + .repeats_per_id + .iter() + .enumerate() + .flat_map(|(id, &r)| std::iter::repeat_n(id, r)) + .collect(); + (0..size) + .map(|_| { + let mut v = template.clone(); + v.shuffle(rng); + v + }) + .collect() + } +} + +// --------------------------------------------------------------------------- +// Mutations +// --------------------------------------------------------------------------- /// Swap two distinct random indices in the first parent (spec §11.4). /// @@ -47,6 +172,519 @@ impl Variation> for SwapMutation { } } +/// Reverse a random sub-slice `[i, j]` of the first parent. +/// +/// Often called *2-opt-style* mutation in TSP literature. Preserves both strict +/// permutations and multiset encodings (since reversing a slice only permutes +/// the values within it). For TSP this is typically the most effective +/// mutation: it directly searches over edge-swap neighbors. +/// +/// If the parent has length `< 2` the child is returned unchanged. +/// +/// # Example +/// +/// ``` +/// use heuropt::prelude::*; +/// +/// let mut rng = rng_from_seed(0); +/// let mut m = InversionMutation; +/// let parent: Vec = (0..8).collect(); +/// let children = m.vary(std::slice::from_ref(&parent), &mut rng); +/// let mut sorted = children[0].clone(); +/// sorted.sort(); +/// assert_eq!(sorted, vec![0, 1, 2, 3, 4, 5, 6, 7]); +/// ``` +#[derive(Debug, Clone, Copy, Default)] +pub struct InversionMutation; + +impl Variation> for InversionMutation { + fn vary(&mut self, parents: &[Vec], rng: &mut Rng) -> Vec> { + assert!( + !parents.is_empty(), + "InversionMutation 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 j = rng.random_range(0..n); + let (lo, hi) = if i <= j { (i, j) } else { (j, i) }; + child[lo..=hi].reverse(); + } + vec![child] + } +} + +/// Remove a random element and re-insert it at a different random position +/// ("shift" mutation). +/// +/// Preserves both strict permutations and multisets. Particularly effective +/// for sequencing problems (flow shop, JSS) where moving a single +/// operation/job to a new position is a meaningful neighborhood move. +/// +/// If the parent has length `< 2` the child is returned unchanged. +/// +/// # Example +/// +/// ``` +/// use heuropt::prelude::*; +/// +/// let mut rng = rng_from_seed(5); +/// let mut m = InsertionMutation; +/// let parent: Vec = vec![0, 1, 2, 3, 4]; +/// let children = m.vary(std::slice::from_ref(&parent), &mut rng); +/// let mut sorted = children[0].clone(); +/// sorted.sort(); +/// assert_eq!(sorted, vec![0, 1, 2, 3, 4]); +/// ``` +#[derive(Debug, Clone, Copy, Default)] +pub struct InsertionMutation; + +impl Variation> for InsertionMutation { + fn vary(&mut self, parents: &[Vec], rng: &mut Rng) -> Vec> { + assert!( + !parents.is_empty(), + "InsertionMutation requires at least one parent", + ); + let mut child = parents[0].clone(); + let n = child.len(); + if n >= 2 { + let from = rng.random_range(0..n); + let v = child.remove(from); + let to = rng.random_range(0..=child.len()); + child.insert(to, v); + } + vec![child] + } +} + +/// Randomly permute the contents of a random sub-slice. +/// +/// Preserves both strict permutations and multisets. Provides a stronger +/// neighborhood than swap/inversion: a single application can rearrange up to +/// `n` positions at once, useful as a diversification operator. +/// +/// If the parent has length `< 2` the child is returned unchanged. +/// +/// # Example +/// +/// ``` +/// use heuropt::prelude::*; +/// +/// let mut rng = rng_from_seed(13); +/// let mut m = ScrambleMutation; +/// let parent: Vec = (0..10).collect(); +/// let children = m.vary(std::slice::from_ref(&parent), &mut rng); +/// let mut sorted = children[0].clone(); +/// sorted.sort(); +/// assert_eq!(sorted, vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); +/// ``` +#[derive(Debug, Clone, Copy, Default)] +pub struct ScrambleMutation; + +impl Variation> for ScrambleMutation { + fn vary(&mut self, parents: &[Vec], rng: &mut Rng) -> Vec> { + assert!( + !parents.is_empty(), + "ScrambleMutation 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 j = rng.random_range(0..n); + let (lo, hi) = if i <= j { (i, j) } else { (j, i) }; + child[lo..=hi].shuffle(rng); + } + vec![child] + } +} + +// --------------------------------------------------------------------------- +// Crossovers (strict permutations only) +// --------------------------------------------------------------------------- + +/// Order Crossover (OX) — strict-permutation crossover by Davis (1985). +/// +/// Pick a random segment `[lo, hi)` from parent A and copy it into the child +/// at those positions. Fill the remaining positions by walking parent B +/// (starting just after `hi`, wrapping), inserting each unused value in order. +/// +/// Returns two children: one with parents in the order `(A, B)`, one in the +/// order `(B, A)`. Both share the same crossover points. +/// +/// # Panics +/// - If fewer than two parents are supplied. +/// - If the two parents have different lengths. +/// +/// # Note +/// OX assumes *strict* permutations — every value appears exactly once. Using +/// it on multiset / operation-string encodings (e.g., JSS) will produce +/// invalid children. +/// +/// # Example +/// +/// ``` +/// use heuropt::prelude::*; +/// +/// let mut rng = rng_from_seed(1); +/// let mut ox = OrderCrossover; +/// let p1: Vec = vec![0, 1, 2, 3, 4, 5, 6, 7]; +/// let p2: Vec = vec![7, 6, 5, 4, 3, 2, 1, 0]; +/// let children = ox.vary(&[p1.clone(), p2.clone()], &mut rng); +/// assert_eq!(children.len(), 2); +/// for c in &children { +/// let mut sorted = c.clone(); +/// sorted.sort(); +/// assert_eq!(sorted, vec![0, 1, 2, 3, 4, 5, 6, 7]); +/// } +/// ``` +#[derive(Debug, Clone, Copy, Default)] +pub struct OrderCrossover; + +impl Variation> for OrderCrossover { + fn vary(&mut self, parents: &[Vec], rng: &mut Rng) -> Vec> { + assert!( + parents.len() >= 2, + "OrderCrossover requires at least 2 parents", + ); + let p1 = &parents[0]; + let p2 = &parents[1]; + assert_eq!( + p1.len(), + p2.len(), + "OrderCrossover requires equal-length parents", + ); + let n = p1.len(); + if n < 2 { + return vec![p1.clone(), p2.clone()]; + } + let i = rng.random_range(0..n); + let j = rng.random_range(0..n); + let (lo, hi_inclusive) = if i <= j { (i, j) } else { (j, i) }; + let hi = hi_inclusive + 1; // exclusive end + vec![ox_child(p1, p2, lo, hi), ox_child(p2, p1, lo, hi)] + } +} + +fn ox_child(donor: &[usize], filler: &[usize], lo: usize, hi: usize) -> Vec { + let n = donor.len(); + let mut child = vec![0_usize; n]; + let segment = &donor[lo..hi]; + child[lo..hi].copy_from_slice(segment); + let mut fill_pos = hi % n; + let mut filler_pos = hi % n; + let mut placed = hi - lo; + while placed < n { + let v = filler[filler_pos]; + if !segment.contains(&v) { + child[fill_pos] = v; + fill_pos = (fill_pos + 1) % n; + placed += 1; + } + filler_pos = (filler_pos + 1) % n; + } + child +} + +/// Partially Mapped Crossover (PMX) — Goldberg & Lingle (1985). +/// +/// Builds a child by starting from a copy of parent B, then sliding parent A's +/// segment `[lo, hi)` into place via swaps. The result has A's segment exactly +/// in the same positions and B's order outside the segment, with internal +/// swaps maintaining the permutation property. +/// +/// Returns two children: one built from `(A, B)`, one from `(B, A)`. +/// +/// # Panics +/// - If fewer than two parents are supplied. +/// - If the two parents have different lengths. +/// +/// # Note +/// PMX is strict-permutation only. +/// +/// # Example +/// +/// ``` +/// use heuropt::prelude::*; +/// +/// let mut rng = rng_from_seed(2); +/// let mut pmx = PartiallyMappedCrossover; +/// let p1: Vec = vec![0, 1, 2, 3, 4, 5, 6, 7]; +/// let p2: Vec = vec![3, 7, 5, 1, 6, 4, 2, 0]; +/// let children = pmx.vary(&[p1, p2], &mut rng); +/// assert_eq!(children.len(), 2); +/// for c in &children { +/// let mut sorted = c.clone(); +/// sorted.sort(); +/// assert_eq!(sorted, vec![0, 1, 2, 3, 4, 5, 6, 7]); +/// } +/// ``` +#[derive(Debug, Clone, Copy, Default)] +pub struct PartiallyMappedCrossover; + +impl Variation> for PartiallyMappedCrossover { + fn vary(&mut self, parents: &[Vec], rng: &mut Rng) -> Vec> { + assert!( + parents.len() >= 2, + "PartiallyMappedCrossover requires at least 2 parents", + ); + let p1 = &parents[0]; + let p2 = &parents[1]; + assert_eq!( + p1.len(), + p2.len(), + "PartiallyMappedCrossover requires equal-length parents", + ); + let n = p1.len(); + if n < 2 { + return vec![p1.clone(), p2.clone()]; + } + let i = rng.random_range(0..n); + let j = rng.random_range(0..n); + let (lo, hi_inclusive) = if i <= j { (i, j) } else { (j, i) }; + let hi = hi_inclusive + 1; + vec![pmx_child(p1, p2, lo, hi), pmx_child(p2, p1, lo, hi)] + } +} + +fn pmx_child(donor: &[usize], base: &[usize], lo: usize, hi: usize) -> Vec { + // Start from a copy of `base`; for each position k in [lo, hi), swap so + // that child[k] == donor[k]. Each swap preserves the permutation. + let mut child = base.to_vec(); + for k in lo..hi { + let v = donor[k]; + if child[k] == v { + continue; + } + let cur = child + .iter() + .position(|&x| x == v) + .expect("permutation invariant: every value must appear"); + child.swap(k, cur); + } + child +} + +/// Cycle Crossover (CX) — Oliver, Smith & Holland (1987). +/// +/// Partitions positions into cycles using the bijection `A[i] ↔ B[i]`. Cycles +/// alternate which parent supplies their values: cycle 1 from A, cycle 2 from +/// B, cycle 3 from A, … +/// +/// Returns two children, the second using the opposite cycle assignment. +/// +/// # Panics +/// - If fewer than two parents are supplied. +/// - If the two parents have different lengths. +/// +/// # Note +/// CX is strict-permutation only and additionally requires that both parents +/// contain exactly the same set of values (otherwise no cycle closes). +/// +/// # Example +/// +/// ``` +/// use heuropt::prelude::*; +/// +/// let mut rng = rng_from_seed(3); +/// let mut cx = CycleCrossover; +/// let p1: Vec = vec![0, 1, 2, 3, 4, 5, 6, 7]; +/// let p2: Vec = vec![7, 3, 1, 4, 2, 5, 6, 0]; +/// let children = cx.vary(&[p1, p2], &mut rng); +/// assert_eq!(children.len(), 2); +/// for c in &children { +/// let mut sorted = c.clone(); +/// sorted.sort(); +/// assert_eq!(sorted, vec![0, 1, 2, 3, 4, 5, 6, 7]); +/// } +/// ``` +#[derive(Debug, Clone, Copy, Default)] +pub struct CycleCrossover; + +impl Variation> for CycleCrossover { + fn vary(&mut self, parents: &[Vec], _rng: &mut Rng) -> Vec> { + assert!( + parents.len() >= 2, + "CycleCrossover requires at least 2 parents", + ); + let p1 = &parents[0]; + let p2 = &parents[1]; + assert_eq!( + p1.len(), + p2.len(), + "CycleCrossover requires equal-length parents", + ); + let n = p1.len(); + if n == 0 { + return vec![Vec::new(), Vec::new()]; + } + vec![cx_child(p1, p2), cx_child(p2, p1)] + } +} + +fn cx_child(start_parent: &[usize], other_parent: &[usize]) -> Vec { + let n = start_parent.len(); + let mut child = vec![0_usize; n]; + let mut visited = vec![false; n]; + let mut cycle_index = 0_usize; + for seed in 0..n { + if visited[seed] { + continue; + } + let (from, switch_through) = if cycle_index % 2 == 0 { + (start_parent, other_parent) + } else { + (other_parent, start_parent) + }; + let mut k = seed; + loop { + if visited[k] { + break; + } + visited[k] = true; + child[k] = from[k]; + let next_val = switch_through[k]; + let next_pos = from + .iter() + .position(|&x| x == next_val) + .expect("CycleCrossover: parents must share the same value multiset"); + k = next_pos; + } + cycle_index += 1; + } + child +} + +/// Edge Recombination Crossover (ERX) — Whitley, Starkweather & Fuquay (1989). +/// +/// The standard high-quality TSP crossover. Builds an adjacency table listing +/// each city's neighbors across both parent tours, then walks the table +/// greedily: at each step the next city is the unvisited neighbor of the +/// current city that has the fewest remaining edges (random tie-break). +/// Dead-ends are filled with any unvisited city. +/// +/// Two children are produced by starting at parents A's and parents B's first +/// city respectively. +/// +/// # Panics +/// - If fewer than two parents are supplied. +/// - If the two parents have different lengths. +/// +/// # Note +/// ERX assumes cities are labeled `0..n` (the standard TSP convention) for +/// O(1) adjacency-table indexing. Strict permutations only. +/// +/// # Example +/// +/// ``` +/// use heuropt::prelude::*; +/// +/// let mut rng = rng_from_seed(4); +/// let mut erx = EdgeRecombinationCrossover; +/// let p1: Vec = vec![0, 1, 2, 3, 4, 5]; +/// let p2: Vec = vec![5, 3, 1, 4, 2, 0]; +/// let children = erx.vary(&[p1, p2], &mut rng); +/// assert_eq!(children.len(), 2); +/// for c in &children { +/// let mut sorted = c.clone(); +/// sorted.sort(); +/// assert_eq!(sorted, vec![0, 1, 2, 3, 4, 5]); +/// } +/// ``` +#[derive(Debug, Clone, Copy, Default)] +pub struct EdgeRecombinationCrossover; + +impl Variation> for EdgeRecombinationCrossover { + fn vary(&mut self, parents: &[Vec], rng: &mut Rng) -> Vec> { + assert!( + parents.len() >= 2, + "EdgeRecombinationCrossover requires at least 2 parents", + ); + let p1 = &parents[0]; + let p2 = &parents[1]; + assert_eq!( + p1.len(), + p2.len(), + "EdgeRecombinationCrossover requires equal-length parents", + ); + let n = p1.len(); + if n == 0 { + return vec![Vec::new(), Vec::new()]; + } + vec![erx_child(p1, p2, p1[0], rng), erx_child(p1, p2, p2[0], rng)] + } +} + +fn erx_child(p1: &[usize], p2: &[usize], start: usize, rng: &mut Rng) -> Vec { + let n = p1.len(); + // adj[v] = neighbors of city v across both parent tours (no duplicates). + let mut adj: Vec> = vec![Vec::new(); n]; + for tour in [p1, p2] { + for i in 0..n { + let cur = tour[i]; + let prev = tour[(i + n - 1) % n]; + let next = tour[(i + 1) % n]; + debug_assert!( + cur < n && prev < n && next < n, + "ERX requires cities labeled 0..n", + ); + for nb in [prev, next] { + if !adj[cur].contains(&nb) { + adj[cur].push(nb); + } + } + } + } + let mut visited = vec![false; n]; + let mut child = Vec::with_capacity(n); + let mut current = start; + for _ in 0..n { + child.push(current); + visited[current] = true; + // Remove `current` from every adjacency list so it isn't picked again. + for list in adj.iter_mut() { + list.retain(|&x| x != current); + } + if child.len() == n { + break; + } + let neighbors: Vec = adj[current] + .iter() + .copied() + .filter(|&c| !visited[c]) + .collect(); + let next = if neighbors.is_empty() { + // Dead-end: pick any unvisited city. Iterating in index order gives + // a deterministic fallback; ERX is rarely sensitive to this choice. + (0..n) + .find(|&c| !visited[c]) + .expect("at least one unvisited city remains") + } else { + let min_deg = neighbors + .iter() + .map(|&c| adj[c].len()) + .min() + .expect("non-empty neighbors"); + let ties: Vec = neighbors + .into_iter() + .filter(|&c| adj[c].len() == min_deg) + .collect(); + if ties.len() == 1 { + ties[0] + } else { + ties[rng.random_range(0..ties.len())] + } + }; + current = next; + } + child +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + #[cfg(test)] mod tests { use super::*; @@ -57,8 +695,26 @@ mod tests { v } + fn is_strict_perm(v: &[usize]) -> bool { + let n = v.len(); + let mut seen = vec![false; n]; + for &x in v { + if x >= n || seen[x] { + return false; + } + seen[x] = true; + } + true + } + + fn multiset_eq(a: &[usize], b: &[usize]) -> bool { + sorted(a.to_vec()) == sorted(b.to_vec()) + } + + // -------- SwapMutation (kept) -------- + #[test] - fn preserves_multiset_contents() { + fn swap_preserves_multiset_contents() { let mut m = SwapMutation; let mut rng = rng_from_seed(11); let parent = vec![0_usize, 1, 2, 3, 4]; @@ -68,7 +724,7 @@ mod tests { } #[test] - fn single_element_unchanged() { + fn swap_single_element_unchanged() { let mut m = SwapMutation; let mut rng = rng_from_seed(0); let parent = vec![42_usize]; @@ -77,11 +733,243 @@ mod tests { } #[test] - fn two_elements_always_swapped() { + fn swap_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(std::slice::from_ref(&parent), &mut rng); assert_eq!(children[0], vec![2, 1]); } + + // -------- ShuffledPermutation -------- + + #[test] + fn shuffled_permutation_returns_requested_count() { + let mut init = ShuffledPermutation { n: 7 }; + let mut rng = rng_from_seed(1); + let pop = init.initialize(10, &mut rng); + assert_eq!(pop.len(), 10); + for p in &pop { + assert!(is_strict_perm(p)); + assert_eq!(p.len(), 7); + } + } + + #[test] + fn shuffled_permutation_n0_yields_empty_vecs() { + let mut init = ShuffledPermutation { n: 0 }; + let mut rng = rng_from_seed(1); + let pop = init.initialize(3, &mut rng); + assert_eq!(pop.len(), 3); + assert!(pop.iter().all(|p| p.is_empty())); + } + + // -------- ShuffledMultisetPermutation -------- + + #[test] + fn shuffled_multiset_preserves_counts() { + let repeats = vec![3_usize, 2, 4, 1]; // total 10 + let mut init = ShuffledMultisetPermutation::new(repeats.clone()); + let mut rng = rng_from_seed(2); + let pop = init.initialize(5, &mut rng); + for p in &pop { + assert_eq!(p.len(), 10); + let mut counts = [0_usize; 4]; + for &v in p { + counts[v] += 1; + } + assert_eq!(counts, [3, 2, 4, 1]); + } + } + + // -------- InversionMutation -------- + + #[test] + fn inversion_preserves_strict_permutation() { + let mut m = InversionMutation; + let parent: Vec = (0..9).collect(); + for seed in 0..50 { + let mut rng = rng_from_seed(seed); + let children = m.vary(std::slice::from_ref(&parent), &mut rng); + assert_eq!(children.len(), 1); + assert!(is_strict_perm(&children[0])); + } + } + + #[test] + fn inversion_preserves_multiset() { + let mut m = InversionMutation; + let parent = vec![0_usize, 0, 1, 1, 2, 2]; + for seed in 0..50 { + let mut rng = rng_from_seed(seed); + let children = m.vary(std::slice::from_ref(&parent), &mut rng); + assert!(multiset_eq(&children[0], &parent)); + } + } + + #[test] + fn inversion_single_element_unchanged() { + let mut m = InversionMutation; + let mut rng = rng_from_seed(0); + let parent = vec![42_usize]; + let children = m.vary(std::slice::from_ref(&parent), &mut rng); + assert_eq!(children[0], parent); + } + + // -------- InsertionMutation -------- + + #[test] + fn insertion_preserves_strict_permutation() { + let mut m = InsertionMutation; + let parent: Vec = (0..7).collect(); + for seed in 0..50 { + let mut rng = rng_from_seed(seed); + let children = m.vary(std::slice::from_ref(&parent), &mut rng); + assert_eq!(children[0].len(), 7); + assert!(is_strict_perm(&children[0])); + } + } + + #[test] + fn insertion_preserves_multiset() { + let mut m = InsertionMutation; + let parent = vec![0_usize, 1, 1, 2, 2, 2]; + for seed in 0..50 { + let mut rng = rng_from_seed(seed); + let children = m.vary(std::slice::from_ref(&parent), &mut rng); + assert!(multiset_eq(&children[0], &parent)); + } + } + + // -------- ScrambleMutation -------- + + #[test] + fn scramble_preserves_strict_permutation() { + let mut m = ScrambleMutation; + let parent: Vec = (0..8).collect(); + for seed in 0..50 { + let mut rng = rng_from_seed(seed); + let children = m.vary(std::slice::from_ref(&parent), &mut rng); + assert!(is_strict_perm(&children[0])); + } + } + + #[test] + fn scramble_preserves_multiset() { + let mut m = ScrambleMutation; + let parent = vec![0_usize, 1, 1, 2, 3, 3, 3]; + for seed in 0..50 { + let mut rng = rng_from_seed(seed); + let children = m.vary(std::slice::from_ref(&parent), &mut rng); + assert!(multiset_eq(&children[0], &parent)); + } + } + + // -------- OrderCrossover (OX) -------- + + #[test] + fn ox_returns_two_valid_permutations() { + let mut ox = OrderCrossover; + let p1: Vec = vec![0, 1, 2, 3, 4, 5, 6, 7]; + let p2: Vec = vec![3, 7, 5, 1, 6, 4, 2, 0]; + for seed in 0..30 { + let mut rng = rng_from_seed(seed); + let children = ox.vary(&[p1.clone(), p2.clone()], &mut rng); + assert_eq!(children.len(), 2); + for c in &children { + assert!(is_strict_perm(c), "child not a permutation: {:?}", c); + } + } + } + + #[test] + fn ox_with_identical_parents_reproduces_parent() { + let mut ox = OrderCrossover; + let p: Vec = vec![0, 1, 2, 3, 4, 5]; + let mut rng = rng_from_seed(0); + let children = ox.vary(&[p.clone(), p.clone()], &mut rng); + assert_eq!(children, vec![p.clone(), p]); + } + + // -------- PartiallyMappedCrossover (PMX) -------- + + #[test] + fn pmx_returns_two_valid_permutations() { + let mut pmx = PartiallyMappedCrossover; + let p1: Vec = vec![0, 1, 2, 3, 4, 5, 6, 7]; + let p2: Vec = vec![3, 7, 5, 1, 6, 4, 2, 0]; + for seed in 0..30 { + let mut rng = rng_from_seed(seed); + let children = pmx.vary(&[p1.clone(), p2.clone()], &mut rng); + assert_eq!(children.len(), 2); + for c in &children { + assert!(is_strict_perm(c), "child not a permutation: {:?}", c); + } + } + } + + #[test] + fn pmx_with_identical_parents_reproduces_parent() { + let mut pmx = PartiallyMappedCrossover; + let p: Vec = vec![0, 1, 2, 3, 4, 5]; + let mut rng = rng_from_seed(0); + let children = pmx.vary(&[p.clone(), p.clone()], &mut rng); + assert_eq!(children, vec![p.clone(), p]); + } + + // -------- CycleCrossover (CX) -------- + + #[test] + fn cx_returns_two_valid_permutations() { + let mut cx = CycleCrossover; + let p1: Vec = vec![0, 1, 2, 3, 4, 5, 6, 7]; + let p2: Vec = vec![7, 3, 1, 4, 2, 5, 6, 0]; + let mut rng = rng_from_seed(0); + let children = cx.vary(&[p1, p2], &mut rng); + assert_eq!(children.len(), 2); + for c in &children { + assert!(is_strict_perm(c)); + } + } + + #[test] + fn cx_with_identical_parents_reproduces_parent() { + let mut cx = CycleCrossover; + let p: Vec = vec![0, 1, 2, 3, 4]; + let mut rng = rng_from_seed(0); + let children = cx.vary(&[p.clone(), p.clone()], &mut rng); + assert_eq!(children, vec![p.clone(), p]); + } + + // -------- EdgeRecombinationCrossover (ERX) -------- + + #[test] + fn erx_returns_two_valid_permutations() { + let mut erx = EdgeRecombinationCrossover; + let p1: Vec = vec![0, 1, 2, 3, 4, 5, 6, 7]; + let p2: Vec = vec![3, 5, 7, 1, 6, 4, 2, 0]; + for seed in 0..30 { + let mut rng = rng_from_seed(seed); + let children = erx.vary(&[p1.clone(), p2.clone()], &mut rng); + assert_eq!(children.len(), 2); + for c in &children { + assert!(is_strict_perm(c), "child not a permutation: {:?}", c); + } + } + } + + #[test] + fn erx_with_identical_parents_walks_the_same_tour() { + // When both parents are identical, the adjacency table reproduces + // the parent's edge set; the only reachable tour is the parent + // (possibly reversed). Either way, the multiset is preserved. + let mut erx = EdgeRecombinationCrossover; + let p: Vec = vec![0, 1, 2, 3, 4, 5]; + let mut rng = rng_from_seed(0); + let children = erx.vary(&[p.clone(), p.clone()], &mut rng); + for c in &children { + assert!(is_strict_perm(c)); + assert_eq!(c.len(), p.len()); + } + } } diff --git a/src/prelude.rs b/src/prelude.rs index 9b54d69..8f9762b 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -19,9 +19,11 @@ pub use crate::pareto::{ }; pub use crate::operators::{ - BitFlipMutation, BoundedGaussianMutation, ClampToBounds, CompositeVariation, GaussianMutation, - LevyMutation, PolynomialMutation, ProjectToSimplex, RealBounds, SimulatedBinaryCrossover, - SwapMutation, + BitFlipMutation, BoundedGaussianMutation, ClampToBounds, CompositeVariation, CycleCrossover, + EdgeRecombinationCrossover, GaussianMutation, InsertionMutation, InversionMutation, + LevyMutation, OrderCrossover, PartiallyMappedCrossover, PolynomialMutation, ProjectToSimplex, + RealBounds, ScrambleMutation, ShuffledMultisetPermutation, ShuffledPermutation, + SimulatedBinaryCrossover, SwapMutation, }; pub use crate::algorithms::{