1use log::warn;
2use statrs::distribution::{ContinuousCDF, Normal};
3
4use crate::geometry::{ConvexPolytope2D, HalfSpace2D, Point2D};
5
6#[derive(Debug)]
8pub struct OutsideConstraintsError {
9 dimensions: [String; 2],
11
12 value: (f64, f64),
14
15 polytope: ConvexPolytope2D,
17}
18
19impl OutsideConstraintsError {
20 pub fn dimensions(&self) -> &[String; 2] {
22 &self.dimensions
23 }
24
25 pub fn value(&self) -> (f64, f64) {
27 self.value
28 }
29
30 pub fn polytope(&self) -> &ConvexPolytope2D {
32 &self.polytope
33 }
34}
35
36impl std::fmt::Display for OutsideConstraintsError {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 write!(
39 f,
40 "Value {:?} is outside the constraints of polytope {:?}",
41 self.value, self.polytope
42 )
43 }
44}
45
46pub type StandardDeviationResult = Result<f64, OutsideConstraintsError>;
48
49pub type SecurityLevelResult = Result<f64, OutsideConstraintsError>;
51
52fn evaluate_polynomial(coeffs: &[f64], x: f64) -> f64 {
54 let mut result = 0.0;
55
56 for (i, c) in coeffs.iter().enumerate() {
57 result += c * x.powi(i as i32);
58 }
59
60 result
61}
62
63pub fn evaluate_polynomial_2d<const M: usize, const N: usize>(
66 coeffs: &[[f64; N]; M],
67 x: f64,
68 y: f64,
69) -> f64 {
70 let mut result = 0.0;
71
72 #[allow(clippy::needless_range_loop)]
74 for i in 0..M {
75 for j in 0..N {
76 result += coeffs[i][j] * x.powi(i as i32) * y.powi(j as i32);
77 }
78 }
79
80 result
81}
82
83fn probability_away_from_mean_gaussian_low(x: f64, std: f64) -> f64 {
87 let normal = Normal::new(0.0, 1.0).unwrap();
88
89 let single_tail_area = 1.0 - normal.cdf(x / std);
90 let both_tail_area = 2.0 * single_tail_area;
91
92 both_tail_area.log10()
93}
94
95fn probability_away_from_mean_gaussian_high(x: f64, std: f64) -> f64 {
99 let ratio = x / std;
100
101 if ratio > 30.0 {
102 warn!("Ratio too high for approximation, not validated for this region");
103 }
104
105 let coeffs = [
108 -0.31904236601958913,
109 -0.13390834324063405,
110 -0.20902566462352498,
111 -0.0003178660849038345,
112 6.75504783552659e-06,
113 -5.91907446763691e-08,
114 ];
115
116 evaluate_polynomial(&coeffs, ratio)
117}
118
119pub fn probability_away_from_mean_gaussian(x: f64, std: f64) -> f64 {
144 if x / std < 7.0 {
145 probability_away_from_mean_gaussian_low(x, std)
146 } else {
147 probability_away_from_mean_gaussian_high(x, std)
148 }
149}
150
151pub fn lwe_security_level_to_std(dimension: usize, security_level: f64) -> StandardDeviationResult {
166 let security_polytope = ConvexPolytope2D {
167 half_spaces: vec![
168 HalfSpace2D::new((-1.0, 0.0), -368.0),
169 HalfSpace2D::new((1.0, 0.0), 2048.0),
170 HalfSpace2D::new((0.0, -1.0), -78.0),
171 HalfSpace2D::new((0.0, 1.0), 130.0),
172 HalfSpace2D::new((0.05678074392712544, -1.0), 3.5151045883938177),
175 ],
176 };
177
178 if !security_polytope.inside(Point2D::new(dimension as f64, security_level)) {
179 return Err(OutsideConstraintsError {
180 dimensions: ["dimension".to_string(), "security_level".to_string()],
181 value: (dimension as f64, security_level),
182 polytope: security_polytope,
183 });
184 }
185
186 let coeffs = [
187 [
188 2.89630547e+00,
189 -1.26321873e-01,
190 2.13993467e-03,
191 -1.49515549e-05,
192 3.84468453e-08,
193 ],
194 [
195 -5.60568533e-02,
196 1.33311189e-03,
197 -1.56200244e-05,
198 8.93067686e-08,
199 -2.00996854e-10,
200 ],
201 [
202 7.39088707e-07,
203 -9.61269520e-08,
204 2.15766569e-09,
205 -1.82462028e-11,
206 5.45243818e-14,
207 ],
208 [
209 1.49456164e-09,
210 -4.28264022e-11,
211 4.30538855e-13,
212 -1.50621118e-15,
213 0.00000000e+00,
214 ],
215 [
216 9.49334890e-14,
217 -2.17539853e-15,
218 1.22195316e-17,
219 0.00000000e+00,
220 0.00000000e+00,
221 ],
222 ];
223
224 let log_std = evaluate_polynomial_2d(&coeffs, dimension as f64, security_level);
225
226 Ok(10.0f64.powf(log_std))
227}
228
229pub fn lwe_std_to_security_level(dimension: usize, std: f64) -> SecurityLevelResult {
245 let log_std = std.log10();
246
247 let std_polytope = ConvexPolytope2D {
248 half_spaces: vec![
249 HalfSpace2D::new((-1.0, 0.0), -386.0),
250 HalfSpace2D::new((1.0, 0.0), 2048.0),
251 HalfSpace2D::new((-0.012501482876757172, -1.0), -0.5040411014606384),
253 HalfSpace2D::new((0.0077927720025765665, 1.0), 0.7390928205510939),
254 HalfSpace2D::new((0.0, -1.0), 17.67),
256 ],
257 };
258
259 if !std_polytope.inside(Point2D::new(dimension as f64, log_std)) {
260 return Err(OutsideConstraintsError {
261 dimensions: ["dimension".to_string(), "log_std".to_string()],
262 value: (dimension as f64, log_std),
263 polytope: std_polytope,
264 });
265 }
266
267 let coeffs = [
268 [
269 6.90381015e+01,
270 5.02853460e+01,
271 1.94568148e+01,
272 4.20275108e+00,
273 5.70115313e-01,
274 3.84445029e-02,
275 1.01123781e-03,
276 ],
277 [
278 5.74446364e-01,
279 2.16090358e-01,
280 4.33027422e-02,
281 5.96469779e-03,
282 3.47705471e-05,
283 -3.75600129e-05,
284 -1.73396859e-06,
285 ],
286 [
287 1.38947894e-04,
288 -1.97798175e-06,
289 6.18022031e-06,
290 -8.44553282e-06,
291 -9.87061302e-07,
292 -1.98799589e-08,
293 7.73239565e-10,
294 ],
295 [
296 -1.76700147e-07,
297 4.46397961e-08,
298 -8.48859329e-08,
299 -6.50906497e-09,
300 2.29684491e-10,
301 2.23006735e-11,
302 0.00000000e+00,
303 ],
304 [
305 2.73798876e-10,
306 -4.27647020e-10,
307 -1.56129840e-12,
308 5.18444880e-12,
309 2.50320308e-13,
310 0.00000000e+00,
311 0.00000000e+00,
312 ],
313 [
314 -9.58735744e-13,
315 1.71390444e-13,
316 3.36603110e-14,
317 1.30767385e-15,
318 0.00000000e+00,
319 0.00000000e+00,
320 0.00000000e+00,
321 ],
322 [
323 5.98968287e-16,
324 7.74296283e-17,
325 2.66615159e-18,
326 0.00000000e+00,
327 0.00000000e+00,
328 0.00000000e+00,
329 0.00000000e+00,
330 ],
331 ];
332
333 Ok(evaluate_polynomial_2d(&coeffs, dimension as f64, log_std))
334}
335
336#[cfg(test)]
337mod tests {
338 use super::{lwe_security_level_to_std, lwe_std_to_security_level};
339
340 #[test]
341 fn lwe_security_to_std_and_back() {
342 let tolerance = 0.05;
343
344 for dimension in 368..=2048 {
345 for security_level in 80..=128 {
346 let std = if let Ok(value) =
347 lwe_security_level_to_std(dimension, security_level as f64)
348 {
349 value
350 } else {
351 continue;
352 };
353
354 let recovered_security_level =
355 if let Ok(value) = lwe_std_to_security_level(dimension, std) {
356 value
357 } else {
358 continue;
359 };
360
361 let diff = (recovered_security_level - security_level as f64).abs();
362 assert!(
363 diff < tolerance,
364 "Security level tolerance violated. Dimension: {}, std: {}, security_level: {}, recovered_level: {}",
365 dimension,
366 std,
367 security_level,
368 recovered_security_level
369 );
370 }
371 }
372 }
373}