#[cfg(feature = "burn")]
pub mod burn_sdp;
use std::f64::consts::{FRAC_1_SQRT_2, PI};
#[derive(Debug, Clone)]
pub struct Moments {
pub mean: Vec<f64>,
pub cov: Vec<Vec<f64>>,
}
#[derive(Debug, Clone)]
pub enum Layer {
Linear {
weight: Vec<Vec<f64>>,
bias: Vec<f64>,
},
ReLU,
}
fn std_normal_cdf(x: f64) -> f64 {
0.5 * (1.0 + erf(x * FRAC_1_SQRT_2))
}
fn std_normal_pdf(x: f64) -> f64 {
(-0.5 * x * x).exp() / (2.0 * PI).sqrt()
}
fn erf(x: f64) -> f64 {
let sign = x.signum();
let x = x.abs();
let t = 1.0 / (1.0 + 0.3275911 * x);
let poly = t
* (0.254829592
+ t * (-0.284496736 + t * (1.421413741 + t * (-1.453152027 + t * 1.061405429))));
sign * (1.0 - poly * (-x * x).exp())
}
fn mat_vec(a: &[Vec<f64>], v: &[f64]) -> Vec<f64> {
a.iter()
.map(|row| row.iter().zip(v).map(|(a, b)| a * b).sum())
.collect()
}
fn mat_mul(a: &[Vec<f64>], b: &[Vec<f64>]) -> Vec<Vec<f64>> {
let n = b[0].len();
let k = b.len();
a.iter()
.map(|row_a| {
(0..n)
.map(|j| (0..k).map(|l| row_a[l] * b[l][j]).sum())
.collect()
})
.collect()
}
fn transpose(a: &[Vec<f64>]) -> Vec<Vec<f64>> {
let m = a.len();
let n = a[0].len();
(0..n).map(|j| (0..m).map(|i| a[i][j]).collect()).collect()
}
pub fn propagate_linear(moments: &Moments, weight: &[Vec<f64>], bias: &[f64]) -> Moments {
let new_mean: Vec<f64> = mat_vec(weight, &moments.mean)
.iter()
.zip(bias)
.map(|(m, b)| m + b)
.collect();
let wc = mat_mul(weight, &moments.cov);
let wt = transpose(weight);
let new_cov = mat_mul(&wc, &wt);
Moments {
mean: new_mean,
cov: new_cov,
}
}
pub fn propagate_relu(moments: &Moments) -> Moments {
let n = moments.mean.len();
let mut new_mean = vec![0.0; n];
let mut new_cov = vec![vec![0.0; n]; n];
for i in 0..n {
let mu = moments.mean[i];
let var = moments.cov[i][i];
if var < 1e-15 {
let relu_mu = mu.max(0.0);
new_mean[i] = relu_mu;
continue;
}
let sigma = var.sqrt();
let alpha = mu / sigma;
let phi = std_normal_pdf(alpha);
let big_phi = std_normal_cdf(alpha);
let mu_out = mu * big_phi + sigma * phi;
let var_out = (mu * mu + var) * big_phi + mu * sigma * phi - mu_out * mu_out;
new_mean[i] = mu_out;
new_cov[i][i] = var_out.max(0.0); }
Moments {
mean: new_mean,
cov: new_cov,
}
}
pub fn propagate_sequential(layers: &[Layer], input_mean: &[f64], input_std: &[f64]) -> Moments {
let n = input_mean.len();
let mut moments = Moments {
mean: input_mean.to_vec(),
cov: (0..n)
.map(|i| {
let mut row = vec![0.0; n];
row[i] = input_std[i] * input_std[i];
row
})
.collect(),
};
for layer in layers {
moments = match layer {
Layer::Linear { weight, bias } => propagate_linear(&moments, weight, bias),
Layer::ReLU => propagate_relu(&moments),
};
}
moments
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq(a: f64, b: f64, tol: f64) {
assert!(
(a - b).abs() < tol,
"{a} != {b} (diff = {}, tol = {tol})",
(a - b).abs()
);
}
#[test]
fn linear_propagation_matches_analytical() {
let moments = Moments {
mean: vec![1.0, 2.0],
cov: vec![vec![0.5, 0.1], vec![0.1, 0.3]],
};
let w = vec![vec![2.0, 0.0], vec![0.0, 3.0]];
let b = vec![1.0, -1.0];
let out = propagate_linear(&moments, &w, &b);
approx_eq(out.mean[0], 3.0, 1e-12);
approx_eq(out.mean[1], 5.0, 1e-12);
approx_eq(out.cov[0][0], 2.0, 1e-12);
approx_eq(out.cov[0][1], 0.6, 1e-12);
approx_eq(out.cov[1][0], 0.6, 1e-12);
approx_eq(out.cov[1][1], 2.7, 1e-12);
}
#[test]
fn relu_reduces_variance() {
let moments = Moments {
mean: vec![1.0, -1.0, 0.0],
cov: vec![
vec![1.0, 0.0, 0.0],
vec![0.0, 1.0, 0.0],
vec![0.0, 0.0, 1.0],
],
};
let out = propagate_relu(&moments);
for i in 0..3 {
assert!(
out.cov[i][i] <= moments.cov[i][i] + 1e-12,
"dim {i}: post-ReLU var {} > pre-ReLU var {}",
out.cov[i][i],
moments.cov[i][i]
);
}
assert!(out.mean[0] > 0.5);
assert!(out.mean[1] > 0.0);
assert!(out.mean[1] < 0.5);
approx_eq(out.mean[2], 1.0 / (2.0 * PI).sqrt(), 1e-4);
}
#[test]
fn sequential_three_layer_network() {
let layers = vec![
Layer::Linear {
weight: vec![vec![1.0, 0.5], vec![-0.5, 1.0], vec![0.3, -0.3]],
bias: vec![0.1, 0.0, -0.1],
},
Layer::ReLU,
Layer::Linear {
weight: vec![vec![1.0, -1.0, 0.5], vec![0.5, 1.0, -0.5]],
bias: vec![0.0, 0.0],
},
Layer::ReLU,
Layer::Linear {
weight: vec![vec![1.0, 1.0]],
bias: vec![0.0],
},
];
let input_mean = vec![1.0, 0.5];
let input_std = vec![0.3, 0.2];
let out = propagate_sequential(&layers, &input_mean, &input_std);
assert_eq!(out.mean.len(), 1);
assert_eq!(out.cov.len(), 1);
assert_eq!(out.cov[0].len(), 1);
assert!(out.mean[0] >= 0.0, "output mean = {}", out.mean[0]);
assert!(out.cov[0][0] >= 0.0, "output var = {}", out.cov[0][0]);
assert!(out.mean[0].is_finite());
assert!(out.cov[0][0].is_finite());
assert!(
out.mean[0] > 0.01,
"output mean suspiciously small: {}",
out.mean[0]
);
}
}