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}