Skip to main content

ferray_random/bitgen/
philox.rs

1// ferray-random: Philox 4x32 counter-based BitGenerator
2//
3// Reference: Salmon et al., "Parallel Random Numbers: As Easy as 1, 2, 3",
4// SC '11: Proceedings of the 2011 International Conference for High
5// Performance Computing.
6//
7// Philox 4x32-10: 4 words of 32 bits, 10 rounds.
8// Supports stream IDs natively (the key encodes the stream).
9
10use super::BitGenerator;
11
12/// Philox 4x32-10 counter-based pseudo-random number generator.
13///
14/// This generator natively supports stream IDs, making it ideal for
15/// parallel generation. Each (seed, stream_id) pair produces an
16/// independent, non-overlapping output sequence.
17///
18/// # Example
19/// ```
20/// use ferray_random::bitgen::Philox;
21/// use ferray_random::bitgen::BitGenerator;
22///
23/// let mut rng = Philox::seed_from_u64(42);
24/// let val = rng.next_u64();
25/// ```
26pub struct Philox {
27    /// 4x32-bit counter
28    counter: [u32; 4],
29    /// 2x32-bit key (derived from seed and stream)
30    key: [u32; 2],
31    /// Buffered output (4 x u32 = 2 x u64)
32    buffer: [u32; 4],
33    /// Index into buffer (0..4)
34    buf_idx: usize,
35}
36
37// Philox 4x32 round constants
38const PHILOX_M0: u32 = 0xD251_1F53;
39const PHILOX_M1: u32 = 0xCD9E_8D57;
40const PHILOX_W0: u32 = 0x9E37_79B9;
41const PHILOX_W1: u32 = 0xBB67_AE85;
42
43/// Single Philox round: two multiplications with xor-folding.
44#[inline]
45fn philox_round(ctr: &mut [u32; 4], key: &[u32; 2]) {
46    let lo0 = ctr[0] as u64 * PHILOX_M0 as u64;
47    let lo1 = ctr[2] as u64 * PHILOX_M1 as u64;
48    let hi0 = (lo0 >> 32) as u32;
49    let lo0 = lo0 as u32;
50    let hi1 = (lo1 >> 32) as u32;
51    let lo1 = lo1 as u32;
52    let new0 = hi1 ^ ctr[1] ^ key[0];
53    let new1 = lo1;
54    let new2 = hi0 ^ ctr[3] ^ key[1];
55    let new3 = lo0;
56    ctr[0] = new0;
57    ctr[1] = new1;
58    ctr[2] = new2;
59    ctr[3] = new3;
60}
61
62/// Bump key between rounds.
63#[inline]
64fn philox_bump_key(key: &mut [u32; 2]) {
65    key[0] = key[0].wrapping_add(PHILOX_W0);
66    key[1] = key[1].wrapping_add(PHILOX_W1);
67}
68
69/// Full Philox 4x32-10 bijection: 10 rounds.
70fn philox4x32_10(counter: [u32; 4], key: [u32; 2]) -> [u32; 4] {
71    let mut ctr = counter;
72    let mut k = key;
73    // 10 rounds with key bumps between rounds
74    philox_round(&mut ctr, &k);
75    philox_bump_key(&mut k);
76    philox_round(&mut ctr, &k);
77    philox_bump_key(&mut k);
78    philox_round(&mut ctr, &k);
79    philox_bump_key(&mut k);
80    philox_round(&mut ctr, &k);
81    philox_bump_key(&mut k);
82    philox_round(&mut ctr, &k);
83    philox_bump_key(&mut k);
84    philox_round(&mut ctr, &k);
85    philox_bump_key(&mut k);
86    philox_round(&mut ctr, &k);
87    philox_bump_key(&mut k);
88    philox_round(&mut ctr, &k);
89    philox_bump_key(&mut k);
90    philox_round(&mut ctr, &k);
91    philox_bump_key(&mut k);
92    philox_round(&mut ctr, &k);
93    ctr
94}
95
96impl Philox {
97    /// Increment the 128-bit counter (4 x u32, little-endian).
98    fn increment_counter(&mut self) {
99        for word in &mut self.counter {
100            *word = word.wrapping_add(1);
101            if *word != 0 {
102                return;
103            }
104        }
105    }
106
107    /// Generate the next block of 4 random u32 values.
108    fn generate_block(&mut self) {
109        self.buffer = philox4x32_10(self.counter, self.key);
110        self.increment_counter();
111        self.buf_idx = 0;
112    }
113
114    /// Create a Philox generator with explicit key and starting counter.
115    fn new_with_key(key: [u32; 2], counter: [u32; 4]) -> Self {
116        let mut rng = Philox {
117            counter,
118            key,
119            buffer: [0; 4],
120            buf_idx: 4, // Force generation on first call
121        };
122        rng.generate_block();
123        rng
124    }
125}
126
127impl BitGenerator for Philox {
128    fn next_u64(&mut self) -> u64 {
129        if self.buf_idx >= 4 {
130            self.generate_block();
131        }
132        let lo = self.buffer[self.buf_idx] as u64;
133        self.buf_idx += 1;
134        if self.buf_idx >= 4 {
135            self.generate_block();
136        }
137        let hi = self.buffer[self.buf_idx] as u64;
138        self.buf_idx += 1;
139        lo | (hi << 32)
140    }
141
142    fn seed_from_u64(seed: u64) -> Self {
143        let key = [seed as u32, (seed >> 32) as u32];
144        Self::new_with_key(key, [0; 4])
145    }
146
147    fn jump(&mut self) -> Option<()> {
148        // Philox supports jump by advancing the counter by 2^64
149        // (each counter value produces 4 u32 = 2 u64, so this is 2^65 u64 outputs)
150        self.counter[2] = self.counter[2].wrapping_add(1);
151        if self.counter[2] == 0 {
152            self.counter[3] = self.counter[3].wrapping_add(1);
153        }
154        self.buf_idx = 4; // Force re-generation
155        Some(())
156    }
157
158    fn stream(seed: u64, stream_id: u64) -> Option<Self> {
159        // Encode stream_id into the counter's upper 64 bits
160        let key = [seed as u32, (seed >> 32) as u32];
161        let counter = [0u32, 0u32, stream_id as u32, (stream_id >> 32) as u32];
162        Some(Self::new_with_key(key, counter))
163    }
164}
165
166impl Clone for Philox {
167    fn clone(&self) -> Self {
168        Self {
169            counter: self.counter,
170            key: self.key,
171            buffer: self.buffer,
172            buf_idx: self.buf_idx,
173        }
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn deterministic_output() {
183        let mut rng1 = Philox::seed_from_u64(42);
184        let mut rng2 = Philox::seed_from_u64(42);
185        for _ in 0..1000 {
186            assert_eq!(rng1.next_u64(), rng2.next_u64());
187        }
188    }
189
190    #[test]
191    fn different_seeds_differ() {
192        let mut rng1 = Philox::seed_from_u64(42);
193        let mut rng2 = Philox::seed_from_u64(43);
194        let mut same = true;
195        for _ in 0..100 {
196            if rng1.next_u64() != rng2.next_u64() {
197                same = false;
198                break;
199            }
200        }
201        assert!(!same);
202    }
203
204    #[test]
205    fn stream_support() {
206        let rng0 = Philox::stream(42, 0);
207        let rng1 = Philox::stream(42, 1);
208        assert!(rng0.is_some());
209        assert!(rng1.is_some());
210
211        let mut rng0 = rng0.unwrap();
212        let mut rng1 = rng1.unwrap();
213        // Different streams should produce different output
214        let v0 = rng0.next_u64();
215        let v1 = rng1.next_u64();
216        assert_ne!(v0, v1);
217    }
218
219    #[test]
220    fn jump_support() {
221        let mut rng = Philox::seed_from_u64(42);
222        assert!(rng.jump().is_some());
223    }
224
225    #[test]
226    fn stream_deterministic() {
227        let mut rng1 = Philox::stream(42, 7).unwrap();
228        let mut rng2 = Philox::stream(42, 7).unwrap();
229        for _ in 0..1000 {
230            assert_eq!(rng1.next_u64(), rng2.next_u64());
231        }
232    }
233}