Skip to main content

qfall_math/rational/poly_over_q/sample/
gauss.rs

1// Copyright 2024 Marcel Luca Schmidt
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 algorithms for sampling according
10//! to the Gaussian distribution.
11
12use crate::{
13    error::MathError,
14    macros::seeded::seedable_function,
15    rational::{PolyOverQ, Q},
16    traits::SetCoefficient,
17    utils::index::evaluate_index,
18    utils::sample::uniform::SamplerRng,
19};
20use probability::{
21    prelude::{Gaussian, Sample},
22    source,
23};
24use rand::Rng;
25use std::fmt::Display;
26
27impl PolyOverQ {
28    seedable_function!(
29        /// Initializes a new [`PolyOverQ`] with maximum degree `max_degree`
30        /// and with each coefficient sampled independently according to the
31        /// Gaussian distribution, using [`Q::sample_gauss`].
32        ///
33        /// Parameters:
34        /// - `max_degree`: specifies the included maximal degree the created [`PolyOverQ`] should have
35        /// - `center`: specifies the center for each coefficient of the polynomial
36        /// - `sigma`: specifies the standard deviation
37        #[seeded]
38        /// - `seed`: specifies the 256-bit seed of the PRNG used for sampling
39        #[optionally_seeded]
40        /// - `seed`: specifies an optional 256-bit seed of the PRNG used for sampling.
41        #[optionally_seeded]
42        ///   If `None` is provided, a fresh [`ThreadRng`](rand::rngs::ThreadRng) is used instead.
43        ///
44        /// Returns a fresh [`PolyOverQ`] instance of maximum degree `max_degree`
45        /// with coefficients chosen independently according the Gaussian
46        /// distribution or a [`MathError`] if the specified parameters were not chosen
47        /// appropriately (`sigma > 0`).
48        ///
49        /// # Examples
50        /// ```
51        /// use qfall_math::rational::PolyOverQ;
52        ///
53        #[unseeded]
54        /// let sample = PolyOverQ::sample_gauss(2, 0, 1).unwrap();
55        #[seeded]
56        /// let sample = PolyOverQ::sample_gauss_seeded(2, 0, 1, [42; 32]).unwrap();
57        /// ```
58        ///
59        /// # Errors and Failures
60        /// - Returns a [`MathError`] of type [`NonPositive`](MathError::NonPositive)
61        ///   if `sigma <= 0`.
62        ///
63        /// # Panics ...
64        /// - if `max_degree` is negative, or does not fit into an [`i64`].
65        pub(crate) fn sample_gauss(
66            max_degree: impl TryInto<i64> + Display,
67            center: impl Into<Q>,
68            sigma: impl Into<f64>,
69            seed: Option<[u8; 32]>,
70        ) -> Result<Self, MathError> {
71            let max_degree = evaluate_index(max_degree).unwrap();
72
73            let center = center.into();
74            let sigma: f64 = sigma.into();
75            let mut poly = PolyOverQ::default();
76
77            if sigma <= 0.0 {
78                return Err(MathError::NonPositive(format!(
79                    "The sigma has to be positive and not zero, but the provided value is {sigma}."
80                )));
81            }
82            let mut rng = SamplerRng::new(seed);
83            let mut source = source::default(rng.next_u64());
84
85            // Instead of sampling with a center of c, we sample with center 0 and add the
86            // center later. These are equivalent and this way we can sample in larger ranges
87            let sampler = Gaussian::new(0.0, sigma);
88
89            for index in 0..=max_degree {
90                let mut sample = Q::from(sampler.sample(&mut source));
91                sample += &center;
92                unsafe { poly.set_coeff_unchecked(index, &sample) };
93            }
94            Ok(poly)
95        }
96    );
97}
98
99#[cfg(test)]
100mod test_sample_gauss {
101    use crate::utils::sample::test_seed;
102    use crate::{
103        integer::Z,
104        rational::{PolyOverQ, Q},
105    };
106
107    /// Checks whether `sample_gauss` is available for all types
108    /// implementing [`Into<Z>`], i.e. u8, u16, u32, u64, i8, ...
109    /// or [`Into<Q>`], i.e. u8, i16, f32, Z, Q, ...
110    #[test]
111    fn availability() {
112        for seeded in [false, true] {
113            let center_q = Q::ZERO;
114            let center_z = Z::ZERO;
115
116            let _ =
117                PolyOverQ::sample_gauss_optionally_seeded(1u8, 16u8, 0f32, seeded.then(test_seed));
118            let _ = PolyOverQ::sample_gauss_optionally_seeded(
119                1u16,
120                16u16,
121                0f64,
122                seeded.then(test_seed),
123            );
124            let _ = PolyOverQ::sample_gauss_optionally_seeded(
125                1u32,
126                16u32,
127                0f32,
128                seeded.then(test_seed),
129            );
130            let _ = PolyOverQ::sample_gauss_optionally_seeded(
131                1u64,
132                16u64,
133                0f64,
134                seeded.then(test_seed),
135            );
136            let _ =
137                PolyOverQ::sample_gauss_optionally_seeded(1i8, 16u8, 0f32, seeded.then(test_seed));
138            let _ = PolyOverQ::sample_gauss_optionally_seeded(
139                1i16,
140                16i16,
141                0f32,
142                seeded.then(test_seed),
143            );
144            let _ = PolyOverQ::sample_gauss_optionally_seeded(
145                1i32,
146                16i32,
147                0f32,
148                seeded.then(test_seed),
149            );
150            let _ = PolyOverQ::sample_gauss_optionally_seeded(
151                1i64,
152                16i64,
153                0f64,
154                seeded.then(test_seed),
155            );
156            let _ = PolyOverQ::sample_gauss_optionally_seeded(
157                1u8,
158                center_q,
159                0f32,
160                seeded.then(test_seed),
161            );
162            let _ = PolyOverQ::sample_gauss_optionally_seeded(
163                1u8,
164                center_z,
165                0f64,
166                seeded.then(test_seed),
167            );
168
169            // explicitly checks the existence of the unseeded and the seeded function
170            if seeded {
171                let _ = PolyOverQ::sample_gauss_seeded(1u8, 16u8, 0f32, test_seed());
172            } else {
173                let _ = PolyOverQ::sample_gauss(1u8, 16u8, 0f32);
174            }
175        }
176    }
177
178    /// Checks whether the number of coefficients is correct.
179    #[test]
180    fn nr_coeffs() {
181        for seeded in [false, true] {
182            let degrees = [1, 3, 7, 15, 32, 120];
183            for degree in degrees {
184                let res = PolyOverQ::sample_gauss_optionally_seeded(
185                    degree,
186                    i64::MAX,
187                    1,
188                    seeded.then(test_seed),
189                )
190                .unwrap();
191
192                assert_eq!(
193                    res.get_degree(),
194                    degree,
195                    "Could fail with negligible probability."
196                );
197            }
198        }
199    }
200
201    /// Checks is as error is returned, if sigma is less than 1.
202    #[test]
203    fn invalid_sigma() {
204        for seeded in [false, true] {
205            assert!(
206                PolyOverQ::sample_gauss_optionally_seeded(1, 0, 0, seeded.then(test_seed)).is_err()
207            );
208            assert!(
209                PolyOverQ::sample_gauss_optionally_seeded(1, 0, -1, seeded.then(test_seed))
210                    .is_err()
211            );
212        }
213    }
214
215    /// Checks whether the maximum degree needs to be at least 0.
216    #[test]
217    fn invalid_max_degree() {
218        for seeded in [false, true] {
219            let result = std::panic::catch_unwind(|| {
220                let _ = PolyOverQ::sample_gauss_optionally_seeded(-1, 0, 1, seeded.then(test_seed))
221                    .unwrap();
222            });
223            assert!(result.is_err());
224        }
225    }
226
227    /// Checks whether the same seed results in the same sample.
228    #[test]
229    fn same_seed_same_sample() {
230        use crate::rational::PolyOverQ;
231        let sample_0 = PolyOverQ::sample_gauss_seeded(2, 0, 1, [42; 32]).unwrap();
232        let sample_1 = PolyOverQ::sample_gauss_seeded(2, 0, 1, [42; 32]).unwrap();
233
234        assert_eq!(sample_0, sample_1);
235    }
236
237    /// Checks whether the seeded variant outputs the known sample for a fixed seed.
238    #[test]
239    fn known_answer() {
240        use crate::rational::PolyOverQ;
241        let sample = PolyOverQ::sample_gauss_seeded(2, 0, 1, [42; 32]).unwrap();
242
243        assert_eq!(
244            "3  2896371026169371/2251799813685248 5860948106209927/4503599627370496 -1555108755990479/1125899906842624",
245            sample.to_string()
246        );
247    }
248}