Skip to main content

ferray_random/bitgen/
pcg64.rs

1// ferray-random: PCG64 BitGenerator implementation
2//
3// PCG-XSL-RR 128/64 (LCG) from Melissa O'Neill's PCG paper.
4// Period: 2^128. No jump support.
5
6// Hex magic constants are 64-bit nothing-up-my-sleeve seeds reproduced
7// verbatim from the PCG and SplitMix64 references; underscore separators
8// would diverge from the canonical published form.
9#![allow(clippy::unreadable_literal)]
10
11use super::BitGenerator;
12
13/// PCG64 (PCG-XSL-RR 128/64) pseudo-random number generator.
14///
15/// Uses a 128-bit linear congruential generator with a permutation-based
16/// output function. Period is 2^128. Does not support `jump()` or `stream()`.
17///
18/// # Example
19/// ```
20/// use ferray_random::bitgen::Pcg64;
21/// use ferray_random::bitgen::BitGenerator;
22///
23/// let mut rng = Pcg64::seed_from_u64(42);
24/// let val = rng.next_u64();
25/// ```
26pub struct Pcg64 {
27    state: u128,
28    inc: u128,
29}
30
31/// Default multiplier for the LCG (from PCG paper).
32const PCG_DEFAULT_MULTIPLIER: u128 = 0x2360_ED05_1FC6_5DA4_4385_DF64_9FCC_F645;
33
34impl Pcg64 {
35    /// Internal step function: advance the LCG state.
36    #[inline]
37    const fn step(&mut self) {
38        self.state = self
39            .state
40            .wrapping_mul(PCG_DEFAULT_MULTIPLIER)
41            .wrapping_add(self.inc);
42    }
43
44    /// Output function: XSL-RR permutation of the 128-bit state to 64-bit output.
45    #[inline]
46    const fn output(state: u128) -> u64 {
47        let xsl = ((state >> 64) ^ state) as u64;
48        let rot = (state >> 122) as u32;
49        xsl.rotate_right(rot)
50    }
51}
52
53impl BitGenerator for Pcg64 {
54    fn next_u64(&mut self) -> u64 {
55        let old_state = self.state;
56        self.step();
57        Self::output(old_state)
58    }
59
60    fn seed_from_u64(seed: u64) -> Self {
61        // Use SplitMix64 expansion for seeding (#259 — shared helper).
62        let seed128 = {
63            let mut s = seed;
64            let a = super::splitmix64(&mut s);
65            let b = super::splitmix64(&mut s);
66            ((a as u128) << 64) | (b as u128)
67        };
68        // inc must be odd
69        let inc = {
70            let mut s = seed.wrapping_add(0xda3e39cb94b95bdb);
71            let a = super::splitmix64(&mut s);
72            let b = super::splitmix64(&mut s);
73            (((a as u128) << 64) | (b as u128)) | 1
74        };
75
76        let mut rng = Self { state: 0, inc };
77        rng.step();
78        rng.state = rng.state.wrapping_add(seed128);
79        rng.step();
80        rng
81    }
82
83    fn jump(&mut self) -> Option<()> {
84        // PCG64 does not support jump-ahead
85        None
86    }
87
88    fn stream(_seed: u64, _stream_id: u64) -> Option<Self> {
89        // PCG64 does not support stream IDs in this implementation
90        None
91    }
92}
93
94impl Clone for Pcg64 {
95    fn clone(&self) -> Self {
96        Self {
97            state: self.state,
98            inc: self.inc,
99        }
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn deterministic_output() {
109        let mut rng1 = Pcg64::seed_from_u64(42);
110        let mut rng2 = Pcg64::seed_from_u64(42);
111        for _ in 0..1000 {
112            assert_eq!(rng1.next_u64(), rng2.next_u64());
113        }
114    }
115
116    #[test]
117    fn different_seeds_differ() {
118        let mut rng1 = Pcg64::seed_from_u64(42);
119        let mut rng2 = Pcg64::seed_from_u64(43);
120        let mut same = true;
121        for _ in 0..100 {
122            if rng1.next_u64() != rng2.next_u64() {
123                same = false;
124                break;
125            }
126        }
127        assert!(!same);
128    }
129
130    #[test]
131    fn jump_not_supported() {
132        let mut rng = Pcg64::seed_from_u64(42);
133        assert!(rng.jump().is_none());
134    }
135
136    #[test]
137    fn stream_not_supported() {
138        assert!(Pcg64::stream(42, 0).is_none());
139    }
140
141    #[test]
142    fn output_covers_full_range() {
143        let mut rng = Pcg64::seed_from_u64(12345);
144        let mut seen_high = false;
145        let mut seen_low = false;
146        for _ in 0..10_000 {
147            let v = rng.next_u64();
148            if v > (u64::MAX / 2) {
149                seen_high = true;
150            } else {
151                seen_low = true;
152            }
153            if seen_high && seen_low {
154                break;
155            }
156        }
157        assert!(seen_high && seen_low);
158    }
159}