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 += ¢er;
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}