Skip to main content

forge_core/
rng.rs

1//! Deterministic RNG (SplitMix64), no external dependencies.
2//!
3//! Reproducibility is a design goal of the whole engine family: the same seed
4//! must yield the same result, on any platform, with or without parallelism.
5//! This is the exact generator already used by `anvil-core` and
6//! `rainflow-core`, lifted here so every optimizer in the ecosystem shares one
7//! certified source of randomness.
8//!
9//! For parallel runs (island models, ensemble restarts) derive an independent
10//! stream per worker with [`Rng::split`]; distinct stream ids give
11//! statistically independent, fully reproducible sub-sequences.
12
13/// Derives a deterministic stream seed from a base `seed` and a stream `id`.
14///
15/// Mixes the id into the seed through the SplitMix64 finalizer so that even
16/// adjacent ids (0, 1, 2, …) produce well-separated seeds. Used to give each
17/// island/ensemble worker an independent, reproducible sub-stream.
18pub fn mix_seed(seed: u64, id: u64) -> u64 {
19    let mut z = seed ^ id.wrapping_mul(0x9E37_79B9_7F4A_7C15);
20    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
21    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
22    z ^ (z >> 31)
23}
24
25/// A SplitMix64 generator. Fast, good statistical quality, single-word state.
26#[derive(Clone, Debug)]
27pub struct Rng {
28    state: u64,
29}
30
31impl Rng {
32    /// Creates an RNG from a seed.
33    pub fn new(seed: u64) -> Self {
34        Rng { state: seed }
35    }
36
37    /// Derives an independent generator for parallel stream `id`.
38    ///
39    /// The child seed is [`mix_seed`]`(seed, id)`, so different `(seed, id)`
40    /// pairs give non-overlapping sub-streams deterministically — the basis for
41    /// reproducible island/ensemble parallelism.
42    pub fn split(seed: u64, id: u64) -> Self {
43        Rng::new(mix_seed(seed, id))
44    }
45
46    #[inline]
47    fn next_u64(&mut self) -> u64 {
48        self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
49        let mut z = self.state;
50        z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
51        z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
52        z ^ (z >> 31)
53    }
54
55    /// Uniform `f64` in `[0, 1)` with 53 bits of mantissa.
56    #[inline]
57    pub fn uniform(&mut self) -> f64 {
58        (self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
59    }
60
61    /// Uniform `f64` in `[lo, hi)`.
62    #[inline]
63    pub fn uniform_in(&mut self, lo: f64, hi: f64) -> f64 {
64        lo + (hi - lo) * self.uniform()
65    }
66
67    /// Standard normal sample via the Box–Muller transform.
68    #[inline]
69    pub fn normal(&mut self) -> f64 {
70        let u1 = (1.0 - self.uniform()).max(f64::MIN_POSITIVE); // avoid ln(0)
71        let u2 = self.uniform();
72        (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
73    }
74
75    /// `usize` uniform in `[0, bound)`.
76    #[inline]
77    pub fn index(&mut self, bound: usize) -> usize {
78        debug_assert!(bound > 0);
79        (self.next_u64() % bound as u64) as usize
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn deterministic() {
89        let mut a = Rng::new(123);
90        let mut b = Rng::new(123);
91        for _ in 0..1000 {
92            assert_eq!(a.next_u64(), b.next_u64());
93        }
94    }
95
96    #[test]
97    fn uniform_in_range() {
98        let mut r = Rng::new(7);
99        for _ in 0..10_000 {
100            let x = r.uniform();
101            assert!((0.0..1.0).contains(&x));
102        }
103    }
104
105    #[test]
106    fn index_in_range() {
107        let mut r = Rng::new(9);
108        for _ in 0..10_000 {
109            assert!(r.index(13) < 13);
110        }
111    }
112
113    /// Split streams are reproducible and diverge from each other.
114    #[test]
115    fn split_streams_are_independent_and_reproducible() {
116        let mut s0a = Rng::split(42, 0);
117        let mut s0b = Rng::split(42, 0);
118        let mut s1 = Rng::split(42, 1);
119        // Same (seed, id) reproduces exactly.
120        assert_eq!(s0a.next_u64(), s0b.next_u64());
121        // Different ids diverge (overwhelmingly likely; fixed seeds make it sure).
122        let a: Vec<u64> = (0..8).map(|_| s0a.next_u64()).collect();
123        let b: Vec<u64> = (0..8).map(|_| s1.next_u64()).collect();
124        assert_ne!(a, b);
125    }
126
127    /// The normal sampler is centered near zero with ~unit spread.
128    #[test]
129    fn normal_is_roughly_standard() {
130        let mut r = Rng::new(2024);
131        let n = 100_000;
132        let mut mean = 0.0;
133        let mut m2 = 0.0;
134        for k in 1..=n {
135            let x = r.normal();
136            let d = x - mean;
137            mean += d / k as f64;
138            m2 += d * (x - mean);
139        }
140        let var = m2 / (n as f64 - 1.0);
141        assert!(mean.abs() < 0.02, "mean {mean}");
142        assert!((var - 1.0).abs() < 0.05, "var {var}");
143    }
144}