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. (Formally the
12//! derived states live on the same SplitMix64 orbit, so overlap is not
13//! impossible — merely astronomically unlikely, ~`streams²·draws/2⁶⁴`.)
14
15/// Derives a deterministic stream seed from a base `seed` and a stream `id`.
16///
17/// Mixes the id into the seed through the SplitMix64 finalizer so that even
18/// adjacent ids (0, 1, 2, …) produce well-separated seeds. Used to give each
19/// island/ensemble worker an independent, reproducible sub-stream.
20pub fn mix_seed(seed: u64, id: u64) -> u64 {
21 let mut z = seed ^ id.wrapping_mul(0x9E37_79B9_7F4A_7C15);
22 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
23 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
24 z ^ (z >> 31)
25}
26
27/// A SplitMix64 generator. Fast, good statistical quality, single-word state.
28#[derive(Clone, Debug)]
29pub struct Rng {
30 state: u64,
31}
32
33impl Rng {
34 /// Creates an RNG from a seed.
35 pub fn new(seed: u64) -> Self {
36 Rng { state: seed }
37 }
38
39 /// Derives an independent generator for parallel stream `id`.
40 ///
41 /// The child seed is [`mix_seed`]`(seed, id)`, so different `(seed, id)`
42 /// pairs give non-overlapping sub-streams deterministically — the basis for
43 /// reproducible island/ensemble parallelism.
44 pub fn split(seed: u64, id: u64) -> Self {
45 Rng::new(mix_seed(seed, id))
46 }
47
48 #[inline]
49 fn next_u64(&mut self) -> u64 {
50 self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
51 let mut z = self.state;
52 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
53 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
54 z ^ (z >> 31)
55 }
56
57 /// Uniform `f64` in `[0, 1)` with 53 bits of mantissa.
58 #[inline]
59 pub fn uniform(&mut self) -> f64 {
60 (self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
61 }
62
63 /// Uniform `f64` in `[lo, hi]` (the top end is reachable: `lo + (hi−lo)·u`
64 /// with `u` half an ulp below 1 can round up to `hi`, which is fine for
65 /// the inclusive bounds used throughout the crate).
66 #[inline]
67 pub fn uniform_in(&mut self, lo: f64, hi: f64) -> f64 {
68 lo + (hi - lo) * self.uniform()
69 }
70
71 /// Standard normal sample via the Box–Muller transform.
72 #[inline]
73 pub fn normal(&mut self) -> f64 {
74 let u1 = (1.0 - self.uniform()).max(f64::MIN_POSITIVE); // avoid ln(0)
75 let u2 = self.uniform();
76 (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
77 }
78
79 /// `usize` uniform in `[0, bound)`.
80 ///
81 /// Uses a plain modulo reduction: the bias is ~`bound/2⁶⁴` (immeasurable
82 /// for the population/dimension sizes used here) and, unlike rejection
83 /// sampling, it consumes exactly one draw — which keeps RNG consumption,
84 /// and therefore byte-level reproducibility, independent of `bound`.
85 ///
86 /// # Panics
87 /// If `bound == 0`.
88 #[inline]
89 pub fn index(&mut self, bound: usize) -> usize {
90 assert!(bound > 0, "Rng::index: bound must be > 0");
91 (self.next_u64() % bound as u64) as usize
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn deterministic() {
101 let mut a = Rng::new(123);
102 let mut b = Rng::new(123);
103 for _ in 0..1000 {
104 assert_eq!(a.next_u64(), b.next_u64());
105 }
106 }
107
108 #[test]
109 fn uniform_in_range() {
110 let mut r = Rng::new(7);
111 for _ in 0..10_000 {
112 let x = r.uniform();
113 assert!((0.0..1.0).contains(&x));
114 }
115 }
116
117 #[test]
118 fn index_in_range() {
119 let mut r = Rng::new(9);
120 for _ in 0..10_000 {
121 assert!(r.index(13) < 13);
122 }
123 }
124
125 /// Split streams are reproducible and diverge from each other.
126 #[test]
127 fn split_streams_are_independent_and_reproducible() {
128 let mut s0a = Rng::split(42, 0);
129 let mut s0b = Rng::split(42, 0);
130 let mut s1 = Rng::split(42, 1);
131 // Same (seed, id) reproduces exactly.
132 assert_eq!(s0a.next_u64(), s0b.next_u64());
133 // Different ids diverge (overwhelmingly likely; fixed seeds make it sure).
134 let a: Vec<u64> = (0..8).map(|_| s0a.next_u64()).collect();
135 let b: Vec<u64> = (0..8).map(|_| s1.next_u64()).collect();
136 assert_ne!(a, b);
137 }
138
139 /// The normal sampler is centered near zero with ~unit spread.
140 #[test]
141 fn normal_is_roughly_standard() {
142 let mut r = Rng::new(2024);
143 let n = 100_000;
144 let mut mean = 0.0;
145 let mut m2 = 0.0;
146 for k in 1..=n {
147 let x = r.normal();
148 let d = x - mean;
149 mean += d / k as f64;
150 m2 += d * (x - mean);
151 }
152 let var = m2 / (n as f64 - 1.0);
153 assert!(mean.abs() < 0.02, "mean {mean}");
154 assert!((var - 1.0).abs() < 0.05, "var {var}");
155 }
156}