Skip to main content

rlevo_evolution/
rng.rs

1//! Deterministic seed derivation for strategies.
2//!
3//! Callers derive sub-seeds by mixing a `base`, a `generation` index, and
4//! a [`SeedPurpose`] so parallel streams (selection, mutation, crossover)
5//! do not alias. The mixer is splitmix64, matching the algorithm used by
6//! [`rlevo_core::util::seed::SeedStream`] for trial-level seed fan-out.
7
8use rand::rngs::StdRng;
9use rand::SeedableRng;
10
11/// Tag identifying which evolutionary operation a sub-stream is for.
12///
13/// Mixing the purpose into the seed means that, within a single
14/// generation, selection and mutation draw from non-overlapping PRNG
15/// streams — critical for reproducing trial-level determinism across
16/// refactors that reorder operator calls.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18#[repr(u8)]
19pub enum SeedPurpose {
20    /// Initial population sampling.
21    Init = 0,
22    /// Parent selection.
23    Selection = 1,
24    /// Recombination / crossover.
25    Crossover = 2,
26    /// Mutation.
27    Mutation = 3,
28    /// Replacement / survivor selection.
29    Replacement = 4,
30    /// Differential-evolution trial-vector construction.
31    Trial = 5,
32    /// Catch-all for strategy-specific stochastic steps.
33    Other = 6,
34}
35
36impl SeedPurpose {
37    const fn constant(self) -> u64 {
38        match self {
39            SeedPurpose::Init => 0xA5A5_A5A5_A5A5_A5A5,
40            SeedPurpose::Selection => 0x1234_5678_9ABC_DEF0,
41            SeedPurpose::Crossover => 0xDEAD_BEEF_CAFE_F00D,
42            SeedPurpose::Mutation => 0xFEED_FACE_0BAD_F00D,
43            SeedPurpose::Replacement => 0x0123_4567_89AB_CDEF,
44            SeedPurpose::Trial => 0xBAAD_F00D_DEAD_C0DE,
45            SeedPurpose::Other => 0x9E37_79B9_7F4A_7C15,
46        }
47    }
48}
49
50/// Derives a seeded PRNG from a base seed, generation counter, and purpose.
51///
52/// Each combination produces an independent stream; repeated calls with
53/// the same arguments return bit-identical sequences.
54#[must_use]
55pub fn seed_stream(base: u64, generation: u64, purpose: SeedPurpose) -> StdRng {
56    let mut x = base
57        .wrapping_add(generation.wrapping_mul(0x9E37_79B9_7F4A_7C15))
58        .wrapping_add(purpose.constant());
59    x = splitmix64(x);
60    x = splitmix64(x);
61    StdRng::seed_from_u64(x)
62}
63
64const fn splitmix64(mut x: u64) -> u64 {
65    x = x.wrapping_add(0x9E37_79B9_7F4A_7C15);
66    x = (x ^ (x >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
67    x = (x ^ (x >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
68    x ^ (x >> 31)
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use rand::{Rng, RngExt};
75
76    #[test]
77    fn seed_stream_is_deterministic() {
78        let mut a = seed_stream(42, 0, SeedPurpose::Init);
79        let mut b = seed_stream(42, 0, SeedPurpose::Init);
80        for _ in 0..8 {
81            assert_eq!(a.next_u64(), b.next_u64());
82        }
83    }
84
85    #[test]
86    fn different_purposes_produce_different_streams() {
87        let a = seed_stream(42, 0, SeedPurpose::Init).next_u64();
88        let b = seed_stream(42, 0, SeedPurpose::Selection).next_u64();
89        let c = seed_stream(42, 0, SeedPurpose::Mutation).next_u64();
90        assert_ne!(a, b);
91        assert_ne!(a, c);
92        assert_ne!(b, c);
93    }
94
95    #[test]
96    fn different_generations_produce_different_streams() {
97        let a = seed_stream(42, 0, SeedPurpose::Mutation).next_u64();
98        let b = seed_stream(42, 1, SeedPurpose::Mutation).next_u64();
99        assert_ne!(a, b);
100    }
101
102    #[test]
103    fn different_bases_produce_different_streams() {
104        let a = seed_stream(1, 0, SeedPurpose::Init).next_u64();
105        let b = seed_stream(2, 0, SeedPurpose::Init).next_u64();
106        assert_ne!(a, b);
107    }
108
109    #[test]
110    fn rng_generates_bounded_values() {
111        let mut rng = seed_stream(7, 0, SeedPurpose::Init);
112        let x: u32 = rng.random_range(0..100);
113        assert!(x < 100);
114    }
115}