rigidity_scenes/rng.rs
1//! A deterministic pseudo-random generator.
2//!
3//! Home-grown rather than `rand`: the scenes are a measuring standard, and
4//! reproducibility matters more here than distribution quality. An
5//! external generator may change algorithm in a minor release, and every
6//! number recorded in the tests would stop matching.
7//!
8//! The algorithm is splitmix64: ten lines, good bit quality, period 2⁶⁴.
9
10/// A generator with explicit state.
11#[derive(Debug, Clone)]
12pub struct Rng {
13 state: u64,
14}
15
16impl Rng {
17 /// Creates a generator from a seed.
18 pub fn new(seed: u64) -> Self {
19 Self { state: seed }
20 }
21
22 fn next_u64(&mut self) -> u64 {
23 self.state = self.state.wrapping_add(0x9E37_79B9_7F4A_7C15);
24 let mut z = self.state;
25 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
26 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
27 z ^ (z >> 31)
28 }
29
30 /// A uniform number in `[0, 1)`.
31 pub fn unit(&mut self) -> f64 {
32 // 53 bits of mantissa: no more fits into an f64.
33 (self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
34 }
35
36 /// A uniform number in `[-1, 1)`.
37 pub fn symmetric(&mut self) -> f64 {
38 self.unit() * 2.0 - 1.0
39 }
40
41 /// A normal number with standard deviation `sigma`.
42 ///
43 /// The Box–Muller transform. The second value of the pair is thrown
44 /// away: keeping it would mean state that depends on call history, and
45 /// the order of calls would start to affect the result.
46 pub fn normal(&mut self, sigma: f64) -> f64 {
47 let u1 = self.unit().max(f64::MIN_POSITIVE);
48 let u2 = self.unit();
49 sigma * (-2.0 * u1.ln()).sqrt() * (std::f64::consts::TAU * u2).cos()
50 }
51}