Skip to main content

qfall_math/rational/q/sample/
gauss.rs

1// Copyright 2024 Marvin Beckmann
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 contains sampling algorithms for gaussian distributions over [`Q`].
10
11use crate::{
12    error::MathError, macros::seeded::seedable_function, rational::Q,
13    utils::sample::uniform::SamplerRng,
14};
15use probability::{
16    distribution::{Gaussian, Sample},
17    source,
18};
19use rand::Rng;
20
21impl Q {
22    seedable_function!(
23        /// Chooses a [`Q`] instance according to the continuous Gaussian distribution.
24        ///
25        /// Parameters:
26        /// - `center`: specifies the position of the center
27        /// - `sigma`: specifies the standard deviation
28        #[seeded]
29        /// - `seed`: specifies the 256-bit seed of the PRNG used for sampling
30        #[optionally_seeded]
31        /// - `seed`: specifies an optional 256-bit seed of the PRNG used for sampling.
32        #[optionally_seeded]
33        ///   If `None` is provided, a fresh [`ThreadRng`](rand::rngs::ThreadRng) is used instead.
34        ///
35        /// Returns new [`Q`] sample chosen according to the specified continuous Gaussian
36        /// distribution or a [`MathError`] if the specified parameters were not chosen
37        /// appropriately (`sigma > 0`).
38        ///
39        /// # Examples
40        /// ```
41        /// use qfall_math::rational::Q;
42        ///
43        #[unseeded]
44        /// let sample = Q::sample_gauss(0, 1).unwrap();
45        #[seeded]
46        /// let sample = Q::sample_gauss_seeded(0, 1, [42; 32]).unwrap();
47        /// ```
48        ///
49        /// # Errors and Failures
50        /// - Returns a [`MathError`] of type [`NonPositive`](MathError::NonPositive)
51        ///   if `sigma <= 0`.
52        pub(crate) fn sample_gauss(
53            center: impl Into<Q>,
54            sigma: impl Into<f64>,
55            seed: Option<[u8; 32]>,
56        ) -> Result<Q, MathError> {
57            let center = center.into();
58            let sigma = sigma.into();
59            if sigma <= 0.0 {
60                return Err(MathError::NonPositive(format!(
61                    "The sigma has to be positive and not zero, but the provided value is {sigma}."
62                )));
63            }
64            let mut rng = SamplerRng::new(seed);
65            let mut source = source::default(rng.next_u64());
66
67            // Instead of sampling with a center of c, we sample with center 0 and add the
68            // center later. These are equivalent and this way we can sample in larger ranges
69            let sampler = Gaussian::new(0.0, sigma);
70            let sample = center + Q::from(sampler.sample(&mut source));
71
72            Ok(sample)
73        }
74    );
75}
76
77#[cfg(test)]
78mod test_sample_gauss {
79    use crate::rational::Q;
80    use crate::utils::sample::test_seed;
81
82    /// Test correct distribution with a confidence level of 99.7% -> 3 standard
83    /// deviations.
84    #[test]
85    fn in_concentration_bound() {
86        for seeded in [false, true] {
87            let range = 3;
88            for (mu, sigma) in [(i64::MAX, 1), (0, 20), (i64::MIN, 100)] {
89                assert!(
90                    range * sigma
91                        >= (Q::from(mu)
92                            - Q::sample_gauss_optionally_seeded(mu, sigma, seeded.then(test_seed))
93                                .unwrap())
94                        .abs()
95                )
96            }
97        }
98    }
99
100    /// Ensure that an error is returned if `sigma` is not positive
101    #[test]
102    fn non_positive_sigma() {
103        for seeded in [false, true] {
104            for (mu, sigma) in [(0, 0), (0, -1)] {
105                assert!(
106                    Q::sample_gauss_optionally_seeded(mu, sigma, seeded.then(test_seed)).is_err()
107                )
108            }
109        }
110    }
111
112    /// Checks whether the same seed results in the same sample.
113    #[test]
114    fn same_seed_same_sample() {
115        use crate::rational::Q;
116        let sample_0 = Q::sample_gauss_seeded(0, 1, [42; 32]).unwrap();
117        let sample_1 = Q::sample_gauss_seeded(0, 1, [42; 32]).unwrap();
118
119        assert_eq!(sample_0, sample_1);
120    }
121
122    /// Checks whether the seeded variant outputs the known sample for a fixed seed.
123    #[test]
124    fn known_answer() {
125        use crate::rational::Q;
126        let sample = Q::sample_gauss_seeded(0, 1, [42; 32]).unwrap();
127
128        assert_eq!("2896371026169371/2251799813685248", sample.to_string());
129    }
130
131    /// Checks whether the unseeded and the seeded function are available.
132    #[test]
133    fn availability() {
134        use crate::rational::Q;
135
136        // explicitly checks the existence of the unseeded and the seeded function
137        let _ = Q::sample_gauss(0, 1);
138        let _ = Q::sample_gauss_seeded(0, 1, test_seed());
139    }
140}