Skip to main content

dupehell_core/
rng.rs

1// DupeHell -- MIT License
2//
3// Synthetic multi-domain dataset generator for record linkage benchmarking.
4// No liability for misuse.
5
6use rand::RngCore;
7use rand::SeedableRng;
8use rand_pcg::Pcg64Mcg;
9
10/// A thin wrapper around PCG64 for deterministic generation.
11///
12/// Python passes a master seed; this RNG can produce sub-seeds for
13/// parallel batches via `fork()`.
14#[derive(Clone, Debug)]
15pub struct Rng {
16    inner: Pcg64Mcg,
17}
18
19impl Rng {
20    /// Create a new RNG from a 64-bit seed.
21    pub fn new(seed: u64) -> Self {
22        Self {
23            inner: Pcg64Mcg::seed_from_u64(seed),
24        }
25    }
26
27    /// Return a random `u64`.
28    pub fn next_u64(&mut self) -> u64 {
29        self.inner.next_u64()
30    }
31
32    /// Return a random `usize` in `0..bound`.
33    ///
34    /// Uses Lemire's multiply-shift method instead of `% bound`, which would
35    /// otherwise skew low values for any `bound` that doesn't divide 2^64.
36    pub fn next_usize(&mut self, bound: usize) -> usize {
37        if bound <= 1 {
38            return 0;
39        }
40        ((self.next_u64() as u128 * bound as u128) >> 64) as usize
41    }
42
43    /// Return a random `f64` in `[0, 1)`.
44    pub fn next_f64(&mut self) -> f64 {
45        // convert to [0, 1) — 53 bits of precision
46        (self.next_u64() >> 11) as f64 * (1.0 / 9007199254740992.0)
47    }
48
49    /// Fork: create a fresh RNG seeded with a value from this one.
50    /// Useful for spawning per-batch or per-column RNGs.
51    pub fn fork(&mut self) -> Self {
52        Self::new(self.next_u64())
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn test_deterministic() {
62        let mut a = Rng::new(42);
63        let mut b = Rng::new(42);
64        for _ in 0..100 {
65            assert_eq!(a.next_u64(), b.next_u64());
66        }
67    }
68
69    #[test]
70    fn test_fork_differs() {
71        let mut parent = Rng::new(42);
72        let mut child = parent.fork();
73        // parent and child should produce different sequences
74        assert_ne!(parent.next_u64(), child.next_u64());
75    }
76
77    #[test]
78    fn test_next_usize_bounded() {
79        let mut rng = Rng::new(0);
80        for _ in 0..1000 {
81            let v = rng.next_usize(10);
82            assert!(v < 10, "value {v} out of range");
83        }
84    }
85
86    #[test]
87    fn test_next_f64_range() {
88        let mut rng = Rng::new(1);
89        for _ in 0..1000 {
90            let v = rng.next_f64();
91            assert!((0.0..1.0).contains(&v), "value {v} out of [0,1)");
92        }
93    }
94}