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}