Skip to main content

qfall_math/utils/sample/
uniform.rs

1// Copyright 2025 Jan Niklas Siemer
2//
3// This file is part of qFALL-math.
4//
5// qFALL-math is free software: you can redistribute it and/or modify it under
6// the terms of the Mozilla Public License Version 2.0 as published by the
7// Mozilla Foundation. See <https://mozilla.org/en-US/MPL/2.0/>.
8
9//! This module includes core functionality to sample according to the
10//! uniform random distribution.
11
12use crate::{error::MathError, integer::Z};
13use flint3_sys::{fmpz_addmul_ui, fmpz_set_ui};
14use rand::{
15    Rng, RngExt, SeedableRng, TryCryptoRng, TryRng,
16    rngs::{StdRng, ThreadRng},
17};
18use std::{cell::RefCell, convert::Infallible, rc::Rc};
19
20/// Defines the source of randomness used by the samplers in this module.
21///
22/// A [`ThreadRng`] can not be seeded explicitly, as it is a handle to a
23/// thread-local generator that is seeded and periodically reseeded by the OS.
24/// Hence, seeded samplers use a [`StdRng`] instead, which uses the same
25/// algorithm (ChaCha with 12 rounds) as [`ThreadRng`], but it's never reseeded.
26///
27/// Cloning a [`SamplerRng`] returns a handle to the same generator for both variants,
28/// i.e. a clone and its original draw from one shared stream of randomness.
29///
30/// Variants:
31/// - `Thread`: a handle to the [`ThreadRng`] of the current thread
32/// - `Seeded`: a shared [`StdRng`] that was explicitly seeded
33#[derive(Debug, Clone)]
34pub enum SamplerRng {
35    Thread(ThreadRng),
36    Seeded(Rc<RefCell<StdRng>>),
37}
38
39impl SamplerRng {
40    /// Returns a [`SamplerRng`] containing a [`StdRng`] seeded with `seed`
41    /// if a seed is provided, and the [`ThreadRng`] of the current thread otherwise.
42    ///
43    /// Parameters:
44    /// - `seed`: specifies the optional 256-bit seed for the [`StdRng`]
45    pub(crate) fn new(seed: Option<[u8; 32]>) -> Self {
46        match seed {
47            Some(seed) => Self::Seeded(Rc::new(RefCell::new(StdRng::from_seed(seed)))),
48            None => Self::Thread(rand::rng()),
49        }
50    }
51
52    /// Returns a fresh seed drawn from this [`SamplerRng`] if it is seeded, and `None` otherwise.
53    /// This allows to derive independent seeds for several subroutines of one seeded function.
54    pub(crate) fn derive_seed(&mut self) -> Option<[u8; 32]> {
55        match self {
56            Self::Thread(_) => None,
57            Self::Seeded(_) => Some(self.random()),
58        }
59    }
60}
61
62impl Default for SamplerRng {
63    /// Returns a [`SamplerRng`] containing the [`ThreadRng`] of the current thread.
64    fn default() -> Self {
65        Self::new(None)
66    }
67}
68
69impl TryRng for SamplerRng {
70    type Error = Infallible;
71
72    fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
73        match self {
74            Self::Thread(rng) => rng.try_next_u32(),
75            Self::Seeded(rng) => rng.borrow_mut().try_next_u32(),
76        }
77    }
78
79    fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
80        match self {
81            Self::Thread(rng) => rng.try_next_u64(),
82            Self::Seeded(rng) => rng.borrow_mut().try_next_u64(),
83        }
84    }
85
86    fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> {
87        match self {
88            Self::Thread(rng) => rng.try_fill_bytes(dst),
89            Self::Seeded(rng) => rng.borrow_mut().try_fill_bytes(dst),
90        }
91    }
92}
93
94// Both [`ThreadRng`] and [`StdRng`] are claimed to be cryptographically secure.
95impl TryCryptoRng for SamplerRng {}
96
97/// Enables uniformly random sampling a [`Z`] in `[0, interval_size)`.
98///
99/// Attributes:
100/// - `interval_size`: defines the interval [0, interval_size), which we sample from
101/// - `two_pow_32`: is a helper to shift bits by 32-bits left by multiplication
102/// - `nr_iterations`: defines how many full samples of u32 are required
103/// - `upper_modulo`: is a power of two to remove superfluously sampled bits to increase
104///   the probability of accepting a sample to at least 1/2
105/// - `rng`: defines the [`ThreadRng`] or seeded [`StdRng`] that's used to sample uniform [u32] integers
106///
107/// # Examples
108/// ```
109/// use qfall_math::{utils::sample::uniform::UniformIntegerSampler, integer::Z};
110/// let interval_size = Z::from(20);
111///
112/// let mut uis = UniformIntegerSampler::init(&interval_size, None).unwrap();
113///
114/// let sample = uis.sample();
115///
116/// assert!(Z::ZERO <= sample);
117/// assert!(sample < interval_size);
118/// ```
119pub struct UniformIntegerSampler {
120    pub interval_size: Z,
121    pub two_pow_32: u64,
122    pub nr_iterations: u32,
123    pub upper_modulo: u32,
124    pub rng: SamplerRng,
125}
126
127impl UniformIntegerSampler {
128    /// Initializes the [`UniformIntegerSampler`] with
129    /// - `interval_size` as `interval_size`,
130    /// - `two_pow_32` as a [u64] containing 2^32
131    /// - `nr_iterations` as `(interval_size - 1).bits() / 32` floored
132    /// - `upper_modulo` as 2^{(interval_size - 1).bits() mod 32}
133    /// - `rng` as a [`StdRng`] seeded with `seed` if `seed` is provided,
134    ///   and as a fresh [`ThreadRng`] otherwise
135    ///
136    /// Parameters:
137    /// - `interval_size`: specifies the interval `[0, interval_size)`
138    ///   from which the samples are drawn
139    /// - `seed`: specifies an optional 256-bit seed for the internal [`StdRng`].
140    ///   If `None` is provided, a fresh [`ThreadRng`] is used instead.
141    ///
142    /// Returns a [`UniformIntegerSampler`] or a [`MathError`],
143    /// if the interval size is chosen smaller than or equal to `1`.
144    ///
145    /// # Examples
146    /// ```
147    /// use qfall_math::{utils::sample::uniform::UniformIntegerSampler, integer::Z};
148    /// let interval_size = Z::from(20);
149    ///
150    /// let uis = UniformIntegerSampler::init(&interval_size, None).unwrap();
151    ///
152    /// let mut uis_seeded_0 = UniformIntegerSampler::init(&interval_size, Some([42; 32])).unwrap();
153    /// let mut uis_seeded_1 = UniformIntegerSampler::init(&interval_size, Some([42; 32])).unwrap();
154    /// assert_eq!(uis_seeded_0.sample(), uis_seeded_1.sample());
155    /// ```
156    ///
157    /// # Errors and Failures
158    /// - Returns a [`MathError`] of type [`InvalidInterval`](MathError::InvalidInterval)
159    ///   if the interval is chosen smaller than `1`.
160    pub fn init(interval_size: &Z, seed: Option<[u8; 32]>) -> Result<Self, MathError> {
161        Self::init_with_rng(interval_size, SamplerRng::new(seed))
162    }
163
164    /// Initializes the [`UniformIntegerSampler`] as described in [`UniformIntegerSampler::init`]
165    /// with `rng` as the source of randomness.
166    ///
167    /// Parameters:
168    /// - `interval_size`: specifies the interval `[0, interval_size)`
169    ///   from which the samples are drawn
170    /// - `rng`: specifies the random number generator used as the source of randomness
171    ///
172    /// Returns a [`UniformIntegerSampler`] or a [`MathError`],
173    /// if the interval size is chosen smaller than or equal to `1`.
174    ///
175    /// # Errors and Failures
176    /// - Returns a [`MathError`] of type [`InvalidInterval`](MathError::InvalidInterval)
177    ///   if the interval is chosen smaller than `1`.
178    pub(crate) fn init_with_rng(interval_size: &Z, rng: SamplerRng) -> Result<Self, MathError> {
179        if interval_size < &Z::ONE {
180            return Err(MathError::InvalidInterval(format!(
181                "An invalid interval size {interval_size} was provided."
182            )));
183        }
184
185        // Compute 2^32 to be able to shift bits to the left
186        // by 32 bits using multiplication
187        let two_pow_32 = u32::MAX as u64 + 1;
188
189        let bit_size = (interval_size - Z::ONE).bits() as u32;
190
191        // div rounds towards 0, i.e. div_floor in this case, i.e. this is
192        // perfect for sampling the top one first and then iterating
193        // nr_iterations-many times
194        let nr_iterations = bit_size / 32;
195
196        // Set upper_modulo to 2^{bit_size mod 32}
197        // defines how many bits will be discarded / have been sampled too much
198        let upper_modulo = 2_u32.pow(bit_size % 32);
199
200        Ok(Self {
201            interval_size: interval_size.clone(),
202            two_pow_32,
203            nr_iterations,
204            upper_modulo,
205            rng,
206        })
207    }
208
209    /// Computes a uniformly chosen [`Z`] sample in `[0, interval_size)`
210    /// using rejection sampling that accepts samples with probability at least 1/2.
211    ///
212    /// # Examples
213    /// ```
214    /// use qfall_math::{utils::sample::uniform::UniformIntegerSampler, integer::Z};
215    /// let interval_size = Z::from(20);
216    ///
217    /// let mut uis = UniformIntegerSampler::init(&interval_size, None).unwrap();
218    ///
219    /// let sample = uis.sample();
220    ///
221    /// assert!(Z::ZERO <= sample);
222    /// assert!(sample < interval_size);
223    /// ```
224    pub fn sample(&mut self) -> Z {
225        if self.interval_size.is_one() {
226            return Z::ZERO;
227        }
228
229        let mut sample = self.sample_bits_uniform();
230        while sample >= self.interval_size {
231            sample = self.sample_bits_uniform();
232        }
233
234        sample
235    }
236
237    /// Computes `self.nr_iterations * 32 + upper_modulo` many uniformly chosen bits.
238    ///
239    /// Returns a [`Z`] containing `self.nr_iterations * 32 + upper_modulo`-many uniformly
240    /// chosen bits.
241    ///
242    /// # Examples
243    /// ```
244    /// use qfall_math::{utils::sample::uniform::UniformIntegerSampler, integer::Z};
245    /// let interval = Z::from(u16::MAX) + 1;
246    ///
247    /// let mut uis = UniformIntegerSampler::init(&interval, None).unwrap();
248    ///
249    /// let sample = uis.sample_bits_uniform();
250    ///
251    /// assert!(Z::ZERO <= sample);
252    /// assert!(sample < interval);
253    /// ```
254    pub fn sample_bits_uniform(&mut self) -> Z {
255        // remove superfluously sampled bits to increase chance of acception to at lest 1/2
256        let mut value = Z::from(self.rng.next_u32() % self.upper_modulo);
257
258        for _ in 0..self.nr_iterations {
259            let sample = self.rng.next_u32();
260
261            let mut res = Z::default();
262            unsafe {
263                fmpz_set_ui(&mut res.value, sample as u64);
264                // Sets res = res + value * 2^32 reusing the memory allocated of res
265                // could be optimized by shifting bits left by 32 bits once lshift is part of flint-sys
266                fmpz_addmul_ui(&mut res.value, &value.value, self.two_pow_32);
267            };
268            value = res;
269        }
270
271        value
272    }
273}
274
275#[cfg(test)]
276mod test_uis {
277    use super::{UniformIntegerSampler, Z};
278    use crate::utils::sample::test_seed;
279    use std::collections::HashSet;
280
281    /// Checks whether sampling works fine for small interval sizes.
282    #[test]
283    fn small_interval() {
284        for seeded in [false, true] {
285            let size_2 = Z::from(2);
286            let size_7 = Z::from(7);
287
288            let mut uis_2 = UniformIntegerSampler::init(&size_2, seeded.then(test_seed)).unwrap();
289            let mut uis_7 = UniformIntegerSampler::init(&size_7, seeded.then(test_seed)).unwrap();
290
291            for _ in 0..3 {
292                let sample_2 = uis_2.sample();
293                let sample_7 = uis_7.sample();
294
295                assert!(Z::ZERO <= sample_2);
296                assert!(sample_2 < size_2);
297                assert!(Z::ZERO <= sample_7);
298                assert!(sample_7 < size_7)
299            }
300        }
301    }
302
303    /// Checks whether sampling works fine for large interval sizes.
304    #[test]
305    fn large_interval() {
306        for seeded in [false, true] {
307            let size_0 = Z::from(u64::MAX);
308            let size_1 = Z::from(u64::MAX) * 2 + 1;
309
310            let mut uis_0 = UniformIntegerSampler::init(&size_0, seeded.then(test_seed)).unwrap();
311            let mut uis_1 = UniformIntegerSampler::init(&size_1, seeded.then(test_seed)).unwrap();
312
313            for _i in 0..u8::MAX {
314                let sample_0 = uis_0.sample();
315                let sample_1 = uis_1.sample();
316
317                assert!(Z::ZERO <= sample_0);
318                assert!(sample_0 < size_0);
319                assert!(Z::ZERO <= sample_1);
320                assert!(sample_1 < size_1);
321            }
322        }
323    }
324
325    /// Checks whether it samples from the entire interval.
326    #[test]
327    fn entire_interval() {
328        for seeded in [false, true] {
329            let interval_sizes = vec![6, 7, 16];
330
331            for interval_size in interval_sizes {
332                let interval = Z::from(interval_size);
333
334                let mut uis =
335                    UniformIntegerSampler::init(&interval, seeded.then(test_seed)).unwrap();
336
337                let mut samples = HashSet::new();
338                for _ in 0..2_u32.pow(interval_size) {
339                    samples.insert(uis.sample());
340                }
341                // if len(samples) == interval_size, then every element in [0, interval_size)
342                // needs to be represented in samples
343                assert_eq!(
344                    interval_size,
345                    samples.len() as u32,
346                    "This test may fail with low probability."
347                );
348            }
349        }
350    }
351
352    /// Checks whether interval sizes smaller than 2 result in an error.
353    #[test]
354    fn invalid_interval() {
355        for seeded in [false, true] {
356            assert!(UniformIntegerSampler::init(&Z::ZERO, seeded.then(test_seed)).is_err());
357            assert!(UniformIntegerSampler::init(&Z::MINUS_ONE, seeded.then(test_seed)).is_err());
358        }
359    }
360
361    /// Checks whether random bit sampling doesn't fill more bits than required.
362    #[test]
363    fn sample_bits_uniform_necessary_nr_bytes() {
364        for seeded in [false, true] {
365            let size_0 = Z::from(8);
366            let size_1 = Z::from(256);
367            let size_2 = Z::from(u32::MAX) + Z::ONE;
368
369            let mut uis_0 = UniformIntegerSampler::init(&size_0, seeded.then(test_seed)).unwrap();
370            let mut uis_1 = UniformIntegerSampler::init(&size_1, seeded.then(test_seed)).unwrap();
371            let mut uis_2 = UniformIntegerSampler::init(&size_2, seeded.then(test_seed)).unwrap();
372
373            for _ in 0..u8::MAX {
374                let sample_0 = uis_0.sample_bits_uniform();
375                let sample_1 = uis_1.sample_bits_uniform();
376                let sample_2 = uis_2.sample_bits_uniform();
377
378                assert!(Z::ZERO <= sample_0);
379                assert!(sample_0 < size_0);
380                assert!(Z::ZERO <= sample_1);
381                assert!(sample_1 < size_1);
382                assert!(Z::ZERO <= sample_2);
383                assert!(sample_2 < size_2);
384            }
385        }
386    }
387
388    /// Checks whether two samplers with the same seed output the same samples
389    /// for small and large interval sizes.
390    #[test]
391    fn same_seed_same_samples() {
392        let interval_sizes = [Z::from(2), Z::from(7), Z::from(u64::MAX) * 2 + 1];
393
394        for interval_size in interval_sizes {
395            let mut uis_0 = UniformIntegerSampler::init(&interval_size, Some([42; 32])).unwrap();
396            let mut uis_1 = UniformIntegerSampler::init(&interval_size, Some([42; 32])).unwrap();
397
398            for _ in 0..u8::MAX {
399                assert_eq!(uis_0.sample(), uis_1.sample());
400            }
401        }
402    }
403
404    /// Checks whether the seeded sampler outputs the known samples for a fixed seed.
405    #[test]
406    fn known_answer() {
407        let interval_size = Z::from(u64::MAX) * 2 + 1;
408        let mut uis = UniformIntegerSampler::init(&interval_size, Some([42; 32])).unwrap();
409
410        let samples: Vec<String> = (0..4).map(|_| uis.sample().to_string()).collect();
411
412        assert_eq!(
413            "22821106978489223123, 9739806954439942819, 36580328303367661063, 5398446775404852435",
414            samples.join(", ")
415        );
416    }
417}