use u_numflow::special::{inverse_normal_cdf, standard_normal_cdf};
pub fn sigma_to_ppm(sigma: f64) -> f64 {
1_000_000.0 * (1.0 - standard_normal_cdf(sigma - 1.5))
}
pub fn ppm_to_sigma(ppm: f64) -> Option<f64> {
if ppm.is_nan() || ppm <= 0.0 || ppm >= 1_000_000.0 {
return None;
}
let p = 1.0 - ppm / 1_000_000.0;
let z = inverse_normal_cdf(p);
if z.is_finite() {
Some(z + 1.5)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn six_sigma_ppm() {
let ppm = sigma_to_ppm(6.0);
assert!(
(ppm - 3.4).abs() < 1.0,
"6-sigma should be ~3.4 PPM, got {ppm}"
);
}
#[test]
fn five_sigma_ppm() {
let ppm = sigma_to_ppm(5.0);
assert!(
(ppm - 233.0).abs() < 20.0,
"5-sigma should be ~233 PPM, got {ppm}"
);
}
#[test]
fn four_sigma_ppm() {
let ppm = sigma_to_ppm(4.0);
assert!(
(ppm - 6_210.0).abs() < 200.0,
"4-sigma should be ~6,210 PPM, got {ppm}"
);
}
#[test]
fn three_sigma_ppm() {
let ppm = sigma_to_ppm(3.0);
assert!(
(ppm - 66_807.0).abs() < 500.0,
"3-sigma should be ~66,807 PPM, got {ppm}"
);
}
#[test]
fn two_sigma_ppm() {
let ppm = sigma_to_ppm(2.0);
assert!(
(ppm - 308_538.0).abs() < 3_000.0,
"2-sigma should be ~308,538 PPM, got {ppm}"
);
}
#[test]
fn one_sigma_ppm() {
let ppm = sigma_to_ppm(1.0);
assert!(
(ppm - 691_462.0).abs() < 5_000.0,
"1-sigma should be ~691,462 PPM, got {ppm}"
);
}
#[test]
fn sigma_to_ppm_is_monotonically_decreasing() {
let sigmas = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let ppms: Vec<f64> = sigmas.iter().map(|&s| sigma_to_ppm(s)).collect();
for window in ppms.windows(2) {
assert!(
window[0] > window[1],
"PPM should decrease with higher sigma: {} > {}",
window[0],
window[1]
);
}
}
#[test]
fn sigma_to_ppm_non_negative() {
for sigma in [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0] {
let ppm = sigma_to_ppm(sigma);
assert!(ppm >= 0.0, "PPM must be >= 0 for sigma={sigma}, got {ppm}");
}
}
#[test]
fn sigma_zero_ppm() {
let ppm = sigma_to_ppm(0.0);
assert!(
ppm > 900_000.0 && ppm < 950_000.0,
"0-sigma PPM should be ~933,193, got {ppm}"
);
}
#[test]
fn roundtrip_sigma_ppm_sigma() {
for &sigma in &[2.0, 3.0, 4.0, 5.0, 6.0] {
let ppm = sigma_to_ppm(sigma);
let recovered = ppm_to_sigma(ppm).expect("roundtrip should succeed");
assert!(
(recovered - sigma).abs() < 0.1,
"roundtrip failed: sigma={sigma}, ppm={ppm}, recovered={recovered}"
);
}
}
#[test]
fn ppm_to_sigma_rejects_zero() {
assert!(ppm_to_sigma(0.0).is_none());
}
#[test]
fn ppm_to_sigma_rejects_million() {
assert!(ppm_to_sigma(1_000_000.0).is_none());
}
#[test]
fn ppm_to_sigma_rejects_negative() {
assert!(ppm_to_sigma(-1.0).is_none());
}
#[test]
fn ppm_to_sigma_rejects_nan() {
assert!(ppm_to_sigma(f64::NAN).is_none());
}
#[test]
fn ppm_to_sigma_rejects_above_million() {
assert!(ppm_to_sigma(1_500_000.0).is_none());
}
#[test]
fn ppm_to_sigma_known_values() {
let cases: &[(f64, f64)] = &[
(3.4, 6.0),
(233.0, 5.0),
(6_210.0, 4.0),
(66_807.0, 3.0),
(308_538.0, 2.0),
];
for &(ppm, expected_sigma) in cases {
let sigma = ppm_to_sigma(ppm).expect("valid PPM should return Some");
assert!(
(sigma - expected_sigma).abs() < 0.15,
"PPM={ppm}: expected sigma~{expected_sigma}, got {sigma}"
);
}
}
}