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 state_bytes(&self) -> Result<Vec<u8>, ferray_core::FerrayError> {
55        let mut out = Vec::with_capacity(32);
56        out.extend_from_slice(&self.state.to_le_bytes());
57        out.extend_from_slice(&self.inc.to_le_bytes());
58        Ok(out)
59    }
60
61    fn set_state_bytes(&mut self, bytes: &[u8]) -> Result<(), ferray_core::FerrayError> {
62        if bytes.len() != 32 {
63            return Err(ferray_core::FerrayError::invalid_value(format!(
64                "Pcg64 state must be 32 bytes, got {}",
65                bytes.len()
66            )));
67        }
68        self.state = u128::from_le_bytes(bytes[0..16].try_into().unwrap());
69        let inc = u128::from_le_bytes(bytes[16..32].try_into().unwrap());
70        if inc & 1 == 0 {
71            return Err(ferray_core::FerrayError::invalid_value(
72                "Pcg64 inc must be odd",
73            ));
74        }
75        self.inc = inc;
76        Ok(())
77    }
78
79    fn next_u64(&mut self) -> u64 {
80        let old_state = self.state;
81        self.step();
82        Self::output(old_state)
83    }
84
85    fn seed_from_u64(seed: u64) -> Self {
86        // Use SplitMix64 expansion for seeding (#259 — shared helper).
87        let seed128 = {
88            let mut s = seed;
89            let a = super::splitmix64(&mut s);
90            let b = super::splitmix64(&mut s);
91            ((a as u128) << 64) | (b as u128)
92        };
93        // inc must be odd
94        let inc = {
95            let mut s = seed.wrapping_add(0xda3e39cb94b95bdb);
96            let a = super::splitmix64(&mut s);
97            let b = super::splitmix64(&mut s);
98            (((a as u128) << 64) | (b as u128)) | 1
99        };
100
101        let mut rng = Self { state: 0, inc };
102        rng.step();
103        rng.state = rng.state.wrapping_add(seed128);
104        rng.step();
105        rng
106    }
107
108    fn jump(&mut self) -> Option<()> {
109        // PCG64 does not support jump-ahead
110        None
111    }
112
113    fn stream(_seed: u64, _stream_id: u64) -> Option<Self> {
114        // PCG64 does not support stream IDs in this implementation
115        None
116    }
117}
118
119impl Clone for Pcg64 {
120    fn clone(&self) -> Self {
121        Self {
122            state: self.state,
123            inc: self.inc,
124        }
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn deterministic_output() {
134        let mut rng1 = Pcg64::seed_from_u64(42);
135        let mut rng2 = Pcg64::seed_from_u64(42);
136        for _ in 0..1000 {
137            assert_eq!(rng1.next_u64(), rng2.next_u64());
138        }
139    }
140
141    #[test]
142    fn different_seeds_differ() {
143        let mut rng1 = Pcg64::seed_from_u64(42);
144        let mut rng2 = Pcg64::seed_from_u64(43);
145        let mut same = true;
146        for _ in 0..100 {
147            if rng1.next_u64() != rng2.next_u64() {
148                same = false;
149                break;
150            }
151        }
152        assert!(!same);
153    }
154
155    #[test]
156    fn jump_not_supported() {
157        let mut rng = Pcg64::seed_from_u64(42);
158        assert!(rng.jump().is_none());
159    }
160
161    #[test]
162    fn stream_not_supported() {
163        assert!(Pcg64::stream(42, 0).is_none());
164    }
165
166    #[test]
167    fn output_covers_full_range() {
168        let mut rng = Pcg64::seed_from_u64(12345);
169        let mut seen_high = false;
170        let mut seen_low = false;
171        for _ in 0..10_000 {
172            let v = rng.next_u64();
173            if v > (u64::MAX / 2) {
174                seen_high = true;
175            } else {
176                seen_low = true;
177            }
178            if seen_high && seen_low {
179                break;
180            }
181        }
182        assert!(seen_high && seen_low);
183    }
184}