use log::warn;
use statrs::distribution::{ContinuousCDF, Normal};
use crate::geometry::{ConvexPolytope2D, HalfSpace2D, Point2D};
#[derive(Debug)]
pub struct OutsideConstraintsError {
dimensions: [String; 2],
value: (f64, f64),
polytope: ConvexPolytope2D,
}
impl OutsideConstraintsError {
pub fn dimensions(&self) -> &[String; 2] {
&self.dimensions
}
pub fn value(&self) -> (f64, f64) {
self.value
}
pub fn polytope(&self) -> &ConvexPolytope2D {
&self.polytope
}
}
impl std::fmt::Display for OutsideConstraintsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Value {:?} is outside the constraints of polytope {:?}",
self.value, self.polytope
)
}
}
pub type StandardDeviationResult = Result<f64, OutsideConstraintsError>;
pub type SecurityLevelResult = Result<f64, OutsideConstraintsError>;
fn evaluate_polynomial(coeffs: &[f64], x: f64) -> f64 {
let mut result = 0.0;
for (i, c) in coeffs.iter().enumerate() {
result += c * x.powi(i as i32);
}
result
}
pub fn evaluate_polynomial_2d<const M: usize, const N: usize>(
coeffs: &[[f64; N]; M],
x: f64,
y: f64,
) -> f64 {
let mut result = 0.0;
#[allow(clippy::needless_range_loop)]
for i in 0..M {
for j in 0..N {
result += coeffs[i][j] * x.powi(i as i32) * y.powi(j as i32);
}
}
result
}
fn probability_away_from_mean_gaussian_low(x: f64, std: f64) -> f64 {
let normal = Normal::new(0.0, 1.0).unwrap();
let single_tail_area = 1.0 - normal.cdf(x / std);
let both_tail_area = 2.0 * single_tail_area;
both_tail_area.log10()
}
fn probability_away_from_mean_gaussian_high(x: f64, std: f64) -> f64 {
let ratio = x / std;
if ratio > 30.0 {
warn!("Ratio too high for approximation, not validated for this region");
}
let coeffs = [
-0.31904236601958913,
-0.13390834324063405,
-0.20902566462352498,
-0.0003178660849038345,
6.75504783552659e-06,
-5.91907446763691e-08,
];
evaluate_polynomial(&coeffs, ratio)
}
pub fn probability_away_from_mean_gaussian(x: f64, std: f64) -> f64 {
if x / std < 7.0 {
probability_away_from_mean_gaussian_low(x, std)
} else {
probability_away_from_mean_gaussian_high(x, std)
}
}
pub fn lwe_security_level_to_std(dimension: usize, security_level: f64) -> StandardDeviationResult {
let security_polytope = ConvexPolytope2D {
half_spaces: vec![
HalfSpace2D::new((-1.0, 0.0), -368.0),
HalfSpace2D::new((1.0, 0.0), 2048.0),
HalfSpace2D::new((0.0, -1.0), -78.0),
HalfSpace2D::new((0.0, 1.0), 130.0),
HalfSpace2D::new((0.05678074392712544, -1.0), 3.5151045883938177),
],
};
if !security_polytope.inside(Point2D::new(dimension as f64, security_level)) {
return Err(OutsideConstraintsError {
dimensions: ["dimension".to_string(), "security_level".to_string()],
value: (dimension as f64, security_level),
polytope: security_polytope,
});
}
let coeffs = [
[
2.89630547e+00,
-1.26321873e-01,
2.13993467e-03,
-1.49515549e-05,
3.84468453e-08,
],
[
-5.60568533e-02,
1.33311189e-03,
-1.56200244e-05,
8.93067686e-08,
-2.00996854e-10,
],
[
7.39088707e-07,
-9.61269520e-08,
2.15766569e-09,
-1.82462028e-11,
5.45243818e-14,
],
[
1.49456164e-09,
-4.28264022e-11,
4.30538855e-13,
-1.50621118e-15,
0.00000000e+00,
],
[
9.49334890e-14,
-2.17539853e-15,
1.22195316e-17,
0.00000000e+00,
0.00000000e+00,
],
];
let log_std = evaluate_polynomial_2d(&coeffs, dimension as f64, security_level);
Ok(10.0f64.powf(log_std))
}
pub fn lwe_std_to_security_level(dimension: usize, std: f64) -> SecurityLevelResult {
let log_std = std.log10();
let std_polytope = ConvexPolytope2D {
half_spaces: vec![
HalfSpace2D::new((-1.0, 0.0), -386.0),
HalfSpace2D::new((1.0, 0.0), 2048.0),
HalfSpace2D::new((-0.012501482876757172, -1.0), -0.5040411014606384),
HalfSpace2D::new((0.0077927720025765665, 1.0), 0.7390928205510939),
HalfSpace2D::new((0.0, -1.0), 17.67),
],
};
if !std_polytope.inside(Point2D::new(dimension as f64, log_std)) {
return Err(OutsideConstraintsError {
dimensions: ["dimension".to_string(), "log_std".to_string()],
value: (dimension as f64, log_std),
polytope: std_polytope,
});
}
let coeffs = [
[
6.90381015e+01,
5.02853460e+01,
1.94568148e+01,
4.20275108e+00,
5.70115313e-01,
3.84445029e-02,
1.01123781e-03,
],
[
5.74446364e-01,
2.16090358e-01,
4.33027422e-02,
5.96469779e-03,
3.47705471e-05,
-3.75600129e-05,
-1.73396859e-06,
],
[
1.38947894e-04,
-1.97798175e-06,
6.18022031e-06,
-8.44553282e-06,
-9.87061302e-07,
-1.98799589e-08,
7.73239565e-10,
],
[
-1.76700147e-07,
4.46397961e-08,
-8.48859329e-08,
-6.50906497e-09,
2.29684491e-10,
2.23006735e-11,
0.00000000e+00,
],
[
2.73798876e-10,
-4.27647020e-10,
-1.56129840e-12,
5.18444880e-12,
2.50320308e-13,
0.00000000e+00,
0.00000000e+00,
],
[
-9.58735744e-13,
1.71390444e-13,
3.36603110e-14,
1.30767385e-15,
0.00000000e+00,
0.00000000e+00,
0.00000000e+00,
],
[
5.98968287e-16,
7.74296283e-17,
2.66615159e-18,
0.00000000e+00,
0.00000000e+00,
0.00000000e+00,
0.00000000e+00,
],
];
Ok(evaluate_polynomial_2d(&coeffs, dimension as f64, log_std))
}
#[cfg(test)]
mod tests {
use super::{lwe_security_level_to_std, lwe_std_to_security_level};
#[test]
fn lwe_security_to_std_and_back() {
let tolerance = 0.05;
for dimension in 368..=2048 {
for security_level in 80..=128 {
let std = if let Ok(value) =
lwe_security_level_to_std(dimension, security_level as f64)
{
value
} else {
continue;
};
let recovered_security_level =
if let Ok(value) = lwe_std_to_security_level(dimension, std) {
value
} else {
continue;
};
let diff = (recovered_security_level - security_level as f64).abs();
assert!(
diff < tolerance,
"Security level tolerance violated. Dimension: {dimension}, std: {std}, security_level: {security_level}, recovered_level: {recovered_security_level}"
);
}
}
}
}