Skip to main content

ferray_random/bitgen/
xoshiro256.rs

1// ferray-random: Xoshiro256** BitGenerator implementation
2//
3// Reference: David Blackman and Sebastiano Vigna, "Scrambled Linear
4// Pseudorandom Number Generators", ACM TOMS, 2021.
5// Period: 2^256 - 1. Jump: 2^128.
6
7// Jump-table constants are 64-bit hex magic numbers from the Vigna paper;
8// underscore separators would diverge from the canonical published form.
9#![allow(clippy::unreadable_literal)]
10
11use super::BitGenerator;
12
13/// Xoshiro256** pseudo-random number generator.
14///
15/// This is the default `BitGenerator` for ferray-random. It has a period of
16/// 2^256 - 1 and supports `jump()` for parallel generation (each jump
17/// advances the state by 2^128 steps).
18///
19/// # Example
20/// ```
21/// use ferray_random::bitgen::Xoshiro256StarStar;
22/// use ferray_random::bitgen::BitGenerator;
23///
24/// let mut rng = Xoshiro256StarStar::seed_from_u64(42);
25/// let val = rng.next_u64();
26/// ```
27pub struct Xoshiro256StarStar {
28    s: [u64; 4],
29}
30
31impl Xoshiro256StarStar {
32    /// Create from an explicit 4-element state. The state must not be all zeros.
33    fn from_state(s: [u64; 4]) -> Self {
34        debug_assert!(
35            s != [0, 0, 0, 0],
36            "Xoshiro256** state must not be all zeros"
37        );
38        Self { s }
39    }
40}
41
42impl BitGenerator for Xoshiro256StarStar {
43    fn state_bytes(&self) -> Result<Vec<u8>, ferray_core::FerrayError> {
44        let mut out = Vec::with_capacity(32);
45        for &w in &self.s {
46            out.extend_from_slice(&w.to_le_bytes());
47        }
48        Ok(out)
49    }
50
51    fn set_state_bytes(&mut self, bytes: &[u8]) -> Result<(), ferray_core::FerrayError> {
52        if bytes.len() != 32 {
53            return Err(ferray_core::FerrayError::invalid_value(format!(
54                "Xoshiro256** state must be 32 bytes, got {}",
55                bytes.len()
56            )));
57        }
58        let mut s = [0u64; 4];
59        for (i, chunk) in bytes.chunks_exact(8).enumerate() {
60            s[i] = u64::from_le_bytes(chunk.try_into().unwrap());
61        }
62        if s == [0, 0, 0, 0] {
63            return Err(ferray_core::FerrayError::invalid_value(
64                "Xoshiro256** state must not be all zeros",
65            ));
66        }
67        self.s = s;
68        Ok(())
69    }
70
71    fn next_u64(&mut self) -> u64 {
72        let result = (self.s[1].wrapping_mul(5)).rotate_left(7).wrapping_mul(9);
73        let t = self.s[1] << 17;
74
75        self.s[2] ^= self.s[0];
76        self.s[3] ^= self.s[1];
77        self.s[1] ^= self.s[2];
78        self.s[0] ^= self.s[3];
79
80        self.s[2] ^= t;
81        self.s[3] = self.s[3].rotate_left(45);
82
83        result
84    }
85
86    fn seed_from_u64(seed: u64) -> Self {
87        // Use the shared splitmix64 helper (#259).
88        let mut sm = seed;
89        let s0 = super::splitmix64(&mut sm);
90        let s1 = super::splitmix64(&mut sm);
91        let s2 = super::splitmix64(&mut sm);
92        let s3 = super::splitmix64(&mut sm);
93        // Ensure state is not all zeros
94        if s0 | s1 | s2 | s3 == 0 {
95            Self::from_state([1, 0, 0, 0])
96        } else {
97            Self::from_state([s0, s1, s2, s3])
98        }
99    }
100
101    fn jump(&mut self) -> Option<()> {
102        // Jump polynomial for 2^128 steps
103        const JUMP: [u64; 4] = [
104            0x180ec6d33cfd0aba,
105            0xd5a61266f0c9392c,
106            0xa9582618e03fc9aa,
107            0x39abdc4529b1661c,
108        ];
109
110        let mut s0: u64 = 0;
111        let mut s1: u64 = 0;
112        let mut s2: u64 = 0;
113        let mut s3: u64 = 0;
114
115        for &jmp in &JUMP {
116            for b in 0..64 {
117                if (jmp >> b) & 1 != 0 {
118                    s0 ^= self.s[0];
119                    s1 ^= self.s[1];
120                    s2 ^= self.s[2];
121                    s3 ^= self.s[3];
122                }
123                self.next_u64();
124            }
125        }
126
127        self.s[0] = s0;
128        self.s[1] = s1;
129        self.s[2] = s2;
130        self.s[3] = s3;
131
132        Some(())
133    }
134
135    fn stream(_seed: u64, _stream_id: u64) -> Option<Self> {
136        // Xoshiro256** does not support stream IDs; use jump() instead
137        None
138    }
139}
140
141impl Clone for Xoshiro256StarStar {
142    fn clone(&self) -> Self {
143        Self { s: self.s }
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn deterministic_output() {
153        let mut rng1 = Xoshiro256StarStar::seed_from_u64(42);
154        let mut rng2 = Xoshiro256StarStar::seed_from_u64(42);
155        for _ in 0..1000 {
156            assert_eq!(rng1.next_u64(), rng2.next_u64());
157        }
158    }
159
160    #[test]
161    fn different_seeds_differ() {
162        let mut rng1 = Xoshiro256StarStar::seed_from_u64(42);
163        let mut rng2 = Xoshiro256StarStar::seed_from_u64(43);
164        let mut same = true;
165        for _ in 0..100 {
166            if rng1.next_u64() != rng2.next_u64() {
167                same = false;
168                break;
169            }
170        }
171        assert!(!same);
172    }
173
174    #[test]
175    fn jump_advances_state() {
176        let mut rng = Xoshiro256StarStar::seed_from_u64(1234);
177        let before = rng.next_u64();
178        let mut rng2 = Xoshiro256StarStar::seed_from_u64(1234);
179        let _ = rng2.next_u64();
180        rng2.jump();
181        let after = rng2.next_u64();
182        // After jump, output should differ
183        assert_ne!(before, after);
184    }
185
186    #[test]
187    fn stream_not_supported() {
188        assert!(Xoshiro256StarStar::stream(42, 0).is_none());
189    }
190
191    #[test]
192    fn next_f64_range() {
193        let mut rng = Xoshiro256StarStar::seed_from_u64(999);
194        for _ in 0..10_000 {
195            let v = rng.next_f64();
196            assert!((0.0..1.0).contains(&v));
197        }
198    }
199}