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 next_u64(&mut self) -> u64 {
44        let result = (self.s[1].wrapping_mul(5)).rotate_left(7).wrapping_mul(9);
45        let t = self.s[1] << 17;
46
47        self.s[2] ^= self.s[0];
48        self.s[3] ^= self.s[1];
49        self.s[1] ^= self.s[2];
50        self.s[0] ^= self.s[3];
51
52        self.s[2] ^= t;
53        self.s[3] = self.s[3].rotate_left(45);
54
55        result
56    }
57
58    fn seed_from_u64(seed: u64) -> Self {
59        // Use the shared splitmix64 helper (#259).
60        let mut sm = seed;
61        let s0 = super::splitmix64(&mut sm);
62        let s1 = super::splitmix64(&mut sm);
63        let s2 = super::splitmix64(&mut sm);
64        let s3 = super::splitmix64(&mut sm);
65        // Ensure state is not all zeros
66        if s0 | s1 | s2 | s3 == 0 {
67            Self::from_state([1, 0, 0, 0])
68        } else {
69            Self::from_state([s0, s1, s2, s3])
70        }
71    }
72
73    fn jump(&mut self) -> Option<()> {
74        // Jump polynomial for 2^128 steps
75        const JUMP: [u64; 4] = [
76            0x180ec6d33cfd0aba,
77            0xd5a61266f0c9392c,
78            0xa9582618e03fc9aa,
79            0x39abdc4529b1661c,
80        ];
81
82        let mut s0: u64 = 0;
83        let mut s1: u64 = 0;
84        let mut s2: u64 = 0;
85        let mut s3: u64 = 0;
86
87        for &jmp in &JUMP {
88            for b in 0..64 {
89                if (jmp >> b) & 1 != 0 {
90                    s0 ^= self.s[0];
91                    s1 ^= self.s[1];
92                    s2 ^= self.s[2];
93                    s3 ^= self.s[3];
94                }
95                self.next_u64();
96            }
97        }
98
99        self.s[0] = s0;
100        self.s[1] = s1;
101        self.s[2] = s2;
102        self.s[3] = s3;
103
104        Some(())
105    }
106
107    fn stream(_seed: u64, _stream_id: u64) -> Option<Self> {
108        // Xoshiro256** does not support stream IDs; use jump() instead
109        None
110    }
111}
112
113impl Clone for Xoshiro256StarStar {
114    fn clone(&self) -> Self {
115        Self { s: self.s }
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn deterministic_output() {
125        let mut rng1 = Xoshiro256StarStar::seed_from_u64(42);
126        let mut rng2 = Xoshiro256StarStar::seed_from_u64(42);
127        for _ in 0..1000 {
128            assert_eq!(rng1.next_u64(), rng2.next_u64());
129        }
130    }
131
132    #[test]
133    fn different_seeds_differ() {
134        let mut rng1 = Xoshiro256StarStar::seed_from_u64(42);
135        let mut rng2 = Xoshiro256StarStar::seed_from_u64(43);
136        let mut same = true;
137        for _ in 0..100 {
138            if rng1.next_u64() != rng2.next_u64() {
139                same = false;
140                break;
141            }
142        }
143        assert!(!same);
144    }
145
146    #[test]
147    fn jump_advances_state() {
148        let mut rng = Xoshiro256StarStar::seed_from_u64(1234);
149        let before = rng.next_u64();
150        let mut rng2 = Xoshiro256StarStar::seed_from_u64(1234);
151        let _ = rng2.next_u64();
152        rng2.jump();
153        let after = rng2.next_u64();
154        // After jump, output should differ
155        assert_ne!(before, after);
156    }
157
158    #[test]
159    fn stream_not_supported() {
160        assert!(Xoshiro256StarStar::stream(42, 0).is_none());
161    }
162
163    #[test]
164    fn next_f64_range() {
165        let mut rng = Xoshiro256StarStar::seed_from_u64(999);
166        for _ in 0..10_000 {
167            let v = rng.next_f64();
168            assert!((0.0..1.0).contains(&v));
169        }
170    }
171}