Skip to main content

comnum_core/
rng.rs

1//! Reproducible random numbers.
2//!
3//! The generator is **xoshiro256\*\*** (Blackman & Vigna, 2018) seeded through
4//! **SplitMix64**: fast, statistically excellent for simulation, and identical across
5//! platforms, so that a stream drawn here is the same as one drawn from the same seed by
6//! another implementation of the generator (a learned receiver trained in Python, say).
7//!
8//! Monte Carlo campaigns never share a generator between trials: [`stream`] derives an
9//! independent generator from `(base_seed, experiment, condition, trial)`, so a trial is
10//! reproducible on its own, whatever the number of threads and the order in which the
11//! trials run (see the crate `comnum-sim`).
12//!
13//! # Mathematics
14//!
15//! - [`Rng::uniform`] returns `(next_u64() >> 11) · 2⁻⁵³ ∈ [0, 1)`;
16//! - [`Rng::gaussian`] is the Box–Muller transform `√(-2 ln(1-u₁)) cos(2π u₂)` (the sine
17//!   branch is dropped, so that one call consumes exactly two uniforms);
18//! - [`Rng::complex_gaussian`] is circular with `E|z|² = 1`;
19//! - [`Rng::gamma`] is the method of Marsaglia & Tsang (2000), a rejection from a
20//!   transformed Gaussian;
21//! - [`stream`]: `seed = sm(sm(sm(base ⊕ fnv1a(experiment)) ⊕ condition) ⊕ trial)` with
22//!   `sm` one SplitMix64 step; [`member`]: `seed ⊕ index · φ`, the members of a family of
23//!   deterministic sequences shared by a transmitter and its receiver.
24//!
25//! # Examples
26//! ```
27//! use comnum_core::rng::{stream, Rng};
28//! let mut a = Rng::new(42);
29//! let mut b = Rng::new(42);
30//! assert_eq!(a.next_u64(), b.next_u64());
31//! // two trials of the same condition get independent streams
32//! let x = stream(1, "ber", 3, 0).uniform();
33//! let y = stream(1, "ber", 3, 1).uniform();
34//! assert_ne!(x, y);
35//! assert_eq!(x, stream(1, "ber", 3, 0).uniform());
36//! ```
37//!
38//! # References
39//!
40//! 1. D. Blackman and S. Vigna, "Scrambled linear pseudorandom number generators,"
41//!    *ACM Trans. Math. Softw.*, 47(4), 2021 (arXiv:1805.01407, 2018).
42//! 2. G. Marsaglia and W. W. Tsang, "A simple method for generating gamma variables,"
43//!    *ACM Trans. Math. Softw.*, 26(3), 363–372, 2000.
44
45use crate::C64;
46
47/// Advance a SplitMix64 state and return its output.
48#[inline]
49pub(crate) fn splitmix64(state: &mut u64) -> u64 {
50    *state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
51    let mut z = *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/// FNV-1a 64-bit hash of a byte string (hashes the experiment names of [`stream`]).
58pub fn fnv1a64(bytes: &[u8]) -> u64 {
59    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
60    for &b in bytes {
61        h ^= b as u64;
62        h = h.wrapping_mul(0x0000_0100_0000_01B3);
63    }
64    h
65}
66
67/// The xoshiro256\*\* generator (256 bits of state).
68#[derive(Clone, Debug)]
69pub struct Rng {
70    s: [u64; 4],
71}
72
73impl Rng {
74    /// A generator seeded from one `u64` through SplitMix64.
75    pub fn new(seed: u64) -> Rng {
76        let mut st = seed;
77        let s = [splitmix64(&mut st), splitmix64(&mut st), splitmix64(&mut st), splitmix64(&mut st)];
78        Rng { s }
79    }
80
81    /// The 256 bits of the state, to save a generator and resume it with
82    /// [`Rng::from_state`] (the pickling of the Python generator).
83    pub fn state(&self) -> [u64; 4] {
84        self.s
85    }
86
87    /// The generator of a state saved by [`Rng::state`]: it continues the draws of the
88    /// saved generator.
89    ///
90    /// # Errors
91    /// [`crate::Error::Config`] for the all-zero state, a fixed point of xoshiro256\*\*
92    /// that no seed gives.
93    pub fn from_state(s: [u64; 4]) -> crate::Result<Rng> {
94        crate::ensure(s != [0; 4], || "the all-zero state is not a state of xoshiro256**".into())?;
95        Ok(Rng { s })
96    }
97
98    /// Next 64 random bits.
99    #[inline]
100    pub fn next_u64(&mut self) -> u64 {
101        let s = &mut self.s;
102        let result = s[1].wrapping_mul(5).rotate_left(7).wrapping_mul(9);
103        let t = s[1] << 17;
104        s[2] ^= s[0];
105        s[3] ^= s[1];
106        s[1] ^= s[2];
107        s[0] ^= s[3];
108        s[2] ^= t;
109        s[3] = s[3].rotate_left(45);
110        result
111    }
112
113    /// Uniform on `[0, 1)` with 53 random bits.
114    #[inline]
115    pub fn uniform(&mut self) -> f64 {
116        (self.next_u64() >> 11) as f64 * (1.0 / 9_007_199_254_740_992.0)
117    }
118
119    /// Uniform integer in `0..n` (`n ≥ 1`).
120    ///
121    /// # Panics
122    /// In debug builds, for `n = 0` (an empty range has no element: release builds
123    /// return 0 rather than check it in the inner loops).
124    #[inline]
125    pub fn below(&mut self, n: usize) -> usize {
126        debug_assert!(n >= 1, "Rng::below(0): the range 0..0 is empty");
127        ((self.uniform() * n as f64) as usize).min(n.saturating_sub(1))
128    }
129
130    /// A phase uniform on $[-\pi, \pi)$: $2\pi U - \pi$, $U$ of [`Rng::uniform`] (one draw;
131    /// the initial phases of the fading generators).
132    ///
133    /// # Examples
134    /// ```
135    /// use comnum_core::rng::Rng;
136    /// let mut r = Rng::new(1);
137    /// assert!((0..1000).map(|_| r.phase()).all(|p| (-std::f64::consts::PI..std::f64::consts::PI).contains(&p)));
138    /// ```
139    #[inline]
140    pub fn phase(&mut self) -> f64 {
141        self.uniform() * 2.0 * std::f64::consts::PI - std::f64::consts::PI
142    }
143
144    /// One fair random bit.
145    #[inline]
146    pub fn bit(&mut self) -> u8 {
147        (self.next_u64() >> 63) as u8
148    }
149
150    /// Standard Gaussian `N(0, 1)`.
151    #[inline]
152    pub fn gaussian(&mut self) -> f64 {
153        let u1 = self.uniform();
154        let u2 = self.uniform();
155        let r = (-2.0 * (1.0 - u1).ln()).sqrt();
156        r * (2.0 * std::f64::consts::PI * u2).cos()
157    }
158
159    /// Circular complex Gaussian with `E|z|² = 1` (each component of variance ½).
160    #[inline]
161    pub fn complex_gaussian(&mut self) -> C64 {
162        let u1 = self.uniform();
163        let u2 = self.uniform();
164        let r = (-2.0 * (1.0 - u1).ln()).sqrt() * std::f64::consts::FRAC_1_SQRT_2;
165        let (s, c) = (2.0 * std::f64::consts::PI * u2).sin_cos();
166        C64::new(r * c, r * s)
167    }
168
169    /// A gamma variable of shape `shape` and unit scale (mean `shape`), by the method of
170    /// Marsaglia & Tsang (2000): for a shape below 1, the draw of shape `shape + 1` times
171    /// $U^{1/\text{shape}}$. A non-positive or non-finite shape returns `NaN`.
172    ///
173    /// # Examples
174    /// ```
175    /// use comnum_core::rng::Rng;
176    /// let mut r = Rng::new(1);
177    /// let n = 100_000;
178    /// let mean = (0..n).map(|_| r.gamma(2.5)).sum::<f64>() / n as f64;
179    /// assert!((mean - 2.5).abs() < 0.03);
180    /// ```
181    pub fn gamma(&mut self, shape: f64) -> f64 {
182        if !(shape > 0.0 && shape.is_finite()) {
183            return f64::NAN;
184        }
185        if shape < 1.0 {
186            let u = self.uniform();
187            return self.gamma(shape + 1.0) * (1.0 - u).powf(1.0 / shape);
188        }
189        let d = shape - 1.0 / 3.0;
190        let c = 1.0 / (9.0 * d).sqrt();
191        loop {
192            let x = self.gaussian();
193            let v = 1.0 + c * x;
194            if v <= 0.0 {
195                continue;
196            }
197            let v = v * v * v;
198            let u = 1.0 - self.uniform();
199            if u < 1.0 - 0.0331 * x * x * x * x || u.ln() < 0.5 * x * x + d * (1.0 - v + v.ln()) {
200                return d * v;
201            }
202        }
203    }
204
205    /// A uniform random permutation of `v` in place (Fisher–Yates, Durstenfeld's
206    /// version): for $i = n - 1, \dots, 1$, swap $v_{i}$ and $v_{j}$ with $j$ uniform in
207    /// $\\{0, \dots, i\\}$ ([`Rng::below`]), $n - 1$ draws in all.
208    ///
209    /// # Examples
210    /// ```
211    /// use comnum_core::rng::Rng;
212    /// let mut v: Vec<usize> = (0..10).collect();
213    /// Rng::new(3).shuffle(&mut v);
214    /// let mut sorted = v.clone();
215    /// sorted.sort_unstable();
216    /// assert_eq!(sorted, (0..10).collect::<Vec<_>>());
217    /// ```
218    pub fn shuffle<T>(&mut self, v: &mut [T]) {
219        for i in (1..v.len()).rev() {
220            v.swap(i, self.below(i + 1));
221        }
222    }
223
224    /// `n` circular complex Gaussian samples of variance `noise_var` (`E|z|² = σ²`).
225    pub fn complex_noise(&mut self, n: usize, noise_var: f64) -> Vec<C64> {
226        let s = noise_var.max(0.0).sqrt();
227        (0..n).map(|_| self.complex_gaussian() * s).collect()
228    }
229}
230
231/// The generator every random block receives (`&mut SimRng`).
232///
233/// A concrete type rather than a generic parameter, so that the block traits stay
234/// object-safe and every call is monomorphic. It is [`Rng`] (xoshiro256\*\*), see
235/// `docs/adr/0002-keep-xoshiro-rng.md`.
236pub type SimRng = Rng;
237
238/// An independent, reproducible generator for one batch of frames.
239///
240/// Keyed by the batch index, never by the worker, so that results do not depend on the
241/// number of threads. It is the stream `(master_seed, "", 0, batch)` of [`stream`].
242///
243/// # Examples
244/// ```
245/// use comnum_core::rng::batch_rng;
246/// assert_eq!(batch_rng(42, 3).next_u64(), batch_rng(42, 3).next_u64());
247/// assert_ne!(batch_rng(42, 3).next_u64(), batch_rng(42, 4).next_u64());
248/// ```
249pub fn batch_rng(master_seed: u64, batch: u64) -> SimRng {
250    stream(master_seed, "", 0, batch)
251}
252
253/// The generator of the member `index` of a family of deterministic sequences drawn from
254/// `seed` (the pilots of the REs of a grid, the phase sequences of SLM):
255/// `Rng::new(seed ⊕ index · φ)`, $\varphi$ the 64-bit golden ratio of SplitMix64. Unlike
256/// [`stream`] it is a plain function of the two numbers, which the transmitter and the
257/// receiver share.
258///
259/// # Examples
260/// ```
261/// use comnum_core::rng::member;
262/// assert_eq!(member(7, 3).next_u64(), member(7, 3).next_u64());
263/// assert_ne!(member(7, 3).next_u64(), member(7, 4).next_u64());
264/// ```
265pub fn member(seed: u64, index: u64) -> Rng {
266    Rng::new(seed ^ index.wrapping_mul(0x9E37_79B9_7F4A_7C15))
267}
268
269/// An independent generator for `(base_seed, experiment, condition, trial)`.
270pub fn stream(base_seed: u64, experiment: &str, condition: u64, trial: u64) -> Rng {
271    let mut st = base_seed ^ fnv1a64(experiment.as_bytes());
272    let a = splitmix64(&mut st);
273    let mut st = a ^ condition;
274    let b = splitmix64(&mut st);
275    let mut st = b ^ trial;
276    let c = splitmix64(&mut st);
277    Rng::new(c)
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    #[cfg(debug_assertions)]
286    #[should_panic(expected = "is empty")]
287    fn below_zero_is_a_broken_invariant_in_debug_builds() {
288        let _ = Rng::new(1).below(0);
289    }
290
291    #[test]
292    fn a_saved_state_resumes_the_draws() {
293        let mut a = Rng::new(42);
294        a.next_u64();
295        let mut b = Rng::from_state(a.state()).unwrap();
296        assert_eq!((0..5).map(|_| a.next_u64()).collect::<Vec<_>>(), (0..5).map(|_| b.next_u64()).collect::<Vec<_>>());
297        assert!(Rng::from_state([0; 4]).is_err());
298    }
299
300    #[test]
301    fn reference_values() {
302        // xoshiro256** seeded by SplitMix64 from 0 (reference C implementation)
303        assert_eq!(Rng::new(0).next_u64(), 0x99EC_5F36_CB75_F2B4);
304        assert_eq!(fnv1a64(b"a"), 0xAF63_DC4C_8601_EC8C);
305    }
306
307    #[test]
308    fn a_phase_is_the_uniform_draw_scaled_to_minus_pi_pi() {
309        // the expression the fading generators wrote inline, bit for bit
310        let (mut a, mut b) = (Rng::new(3), Rng::new(3));
311        for _ in 0..1000 {
312            let pi = std::f64::consts::PI;
313            assert_eq!(a.phase().to_bits(), (b.uniform() * 2.0 * pi - pi).to_bits());
314        }
315    }
316
317    #[test]
318    fn moments() {
319        let mut r = Rng::new(7);
320        let n = 200_000;
321        let (mut m, mut v, mut b) = (0.0, 0.0, 0u64);
322        for _ in 0..n {
323            let z = r.complex_gaussian();
324            m += z.re;
325            v += z.norm_sqr();
326            b += r.bit() as u64;
327        }
328        assert!((m / n as f64).abs() < 0.01);
329        assert!((v / n as f64 - 1.0).abs() < 0.01);
330        assert!((b as f64 / n as f64 - 0.5).abs() < 0.01);
331    }
332}