#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum NoiseType {
WhitePM,
FlickerPM,
WhiteFM,
FlickerFM,
RandomWalkFM,
}
impl NoiseType {
#[must_use]
pub fn alpha(self) -> i32 {
match self {
NoiseType::WhitePM => 2,
NoiseType::FlickerPM => 1,
NoiseType::WhiteFM => 0,
NoiseType::FlickerFM => -1,
NoiseType::RandomWalkFM => -2,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum VarType {
Allan,
Modified,
Hadamard,
Total,
}
#[must_use]
pub fn edf(noise: NoiseType, n: usize, m: usize, var: VarType) -> f64 {
let _ = var;
if m == 0 || n < 4 || 2 * m >= n {
return f64::NAN;
}
let nn = n as f64;
let mm = m as f64;
match noise.alpha() {
2 => {
(nn + 1.0) * (nn - 2.0 * mm) / (2.0 * (nn - mm))
}
1 => {
let a = (nn - 1.0) / (2.0 * mm);
let b = (2.0 * mm + 1.0) * (nn - 1.0) / 4.0;
(a.ln() * b.ln()).sqrt().exp()
}
0 => {
((3.0 * (nn - 1.0) / (2.0 * mm)) - (2.0 * (nn - 2.0) / nn))
* ((4.0 * mm * mm) / ((4.0 * mm * mm) + 5.0))
}
-1 => {
if m == 1 {
2.0 * (nn - 2.0) / (2.3 * nn - 4.9)
} else {
5.0 * nn * nn / (4.0 * mm * (nn + 3.0 * mm))
}
}
-2 => {
let a = (nn - 2.0) / (mm * (nn - 3.0) * (nn - 3.0));
let b = (nn - 1.0) * (nn - 1.0);
let c = 3.0 * mm * (nn - 1.0);
let d = 4.0 * mm * mm;
a * (b - c + d)
}
_ => f64::NAN,
}
}
#[must_use]
pub fn chi2_inv(p: f64, dof: f64) -> f64 {
if p <= 0.0 {
return 0.0;
}
if p >= 1.0 {
return f64::INFINITY;
}
let z = norm_inv(p);
let t = 2.0 / (9.0 * dof);
let base = 1.0 - t + z * t.sqrt();
let mut x = dof * base * base * base;
if x <= 0.0 || !x.is_finite() {
x = 0.5 * dof.max(1e-3);
}
let a = 0.5 * dof;
for _ in 0..64 {
let cdf = reg_lower_gamma(a, 0.5 * x);
let pdf = chi2_pdf(x, dof);
if pdf <= 0.0 || !pdf.is_finite() {
break;
}
let dx = (cdf - p) / pdf;
let mut xn = x - dx;
if xn <= 0.0 {
xn = 0.5 * x;
}
let converged = (xn - x).abs() <= 1e-12 * x.max(1.0);
x = xn;
if converged {
break;
}
}
x
}
fn chi2_pdf(x: f64, dof: f64) -> f64 {
if x <= 0.0 {
return 0.0;
}
let a = 0.5 * dof;
let ln_pdf = (a - 1.0) * x.ln() - 0.5 * x - a * core::f64::consts::LN_2 - ln_gamma(a);
ln_pdf.exp()
}
fn ln_gamma(x: f64) -> f64 {
const COF: [f64; 6] = [
76.180_091_729_471_46,
-86.505_320_329_416_77,
24.014_098_240_830_91,
-1.231_739_572_450_155,
0.120_865_097_386_617_9e-2,
-0.539_523_938_495_3e-5,
];
let mut y = x;
let tmp = (x + 5.5) - (x + 0.5) * (x + 5.5).ln();
let mut ser = 1.000_000_000_190_015;
for &c in &COF {
y += 1.0;
ser += c / y;
}
-tmp + (2.506_628_274_631_000_5 * ser / x).ln()
}
fn reg_lower_gamma(a: f64, x: f64) -> f64 {
if x <= 0.0 {
return 0.0;
}
if x < a + 1.0 {
let mut ap = a;
let mut del = 1.0 / a;
let mut sum = del;
for _ in 0..1000 {
ap += 1.0;
del *= x / ap;
sum += del;
if del.abs() < sum.abs() * 1e-15 {
break;
}
}
sum * (-x + a * x.ln() - ln_gamma(a)).exp()
} else {
const TINY: f64 = 1e-300;
let mut b = x + 1.0 - a;
let mut c = 1.0 / TINY;
let mut d = 1.0 / b;
let mut h = d;
for i in 1..1000 {
let an = -(i as f64) * (i as f64 - a);
b += 2.0;
d = an * d + b;
if d.abs() < TINY {
d = TINY;
}
c = b + an / c;
if c.abs() < TINY {
c = TINY;
}
d = 1.0 / d;
let del = d * c;
h *= del;
if (del - 1.0).abs() < 1e-15 {
break;
}
}
let q = (-x + a * x.ln() - ln_gamma(a)).exp() * h;
1.0 - q
}
}
fn norm_inv(p: f64) -> f64 {
const A: [f64; 6] = [
-3.969683028665376e+01,
2.209460984245205e+02,
-2.759285104469687e+02,
1.38357751867269e+02,
-3.066479806614716e+01,
2.506628277459239e+00,
];
const B: [f64; 5] = [
-5.447609879822406e+01,
1.615858368580409e+02,
-1.556989798598866e+02,
6.680131188771972e+01,
-1.328068155288572e+01,
];
const C: [f64; 6] = [
-7.784894002430293e-03,
-3.223964580411365e-01,
-2.400758277161838e+00,
-2.549732539343734e+00,
4.374664141464968e+00,
2.938163982698783e+00,
];
const D: [f64; 4] = [
7.784695709041462e-03,
3.224671290700398e-01,
2.445134137142996e+00,
3.754408661907416e+00,
];
const P_LOW: f64 = 0.02425;
let p_high = 1.0 - P_LOW;
if p <= 0.0 {
return f64::NEG_INFINITY;
}
if p >= 1.0 {
return f64::INFINITY;
}
if p < P_LOW {
let q = (-2.0 * p.ln()).sqrt();
(((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
/ ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
} else if p <= p_high {
let q = p - 0.5;
let r = q * q;
(((((A[0] * r + A[1]) * r + A[2]) * r + A[3]) * r + A[4]) * r + A[5]) * q
/ (((((B[0] * r + B[1]) * r + B[2]) * r + B[3]) * r + B[4]) * r + 1.0)
} else {
let q = (-2.0 * (1.0 - p).ln()).sqrt();
-(((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
/ ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
}
}
#[must_use]
pub fn confidence_interval(variance: f64, edf: f64, p_lower: f64, p_upper: f64) -> (f64, f64) {
(
variance * edf / chi2_inv(p_upper, edf),
variance * edf / chi2_inv(p_lower, edf),
)
}
#[cfg(test)]
mod tests {
use super::*;
fn rel(a: f64, b: f64) -> f64 {
((a - b) / b).abs()
}
#[test]
fn chi2_inv_upper_tail() {
assert!(rel(chi2_inv(0.975, 10.0), 20.483) < 2e-2);
}
#[test]
fn chi2_inv_lower_tail() {
assert!(rel(chi2_inv(0.025, 10.0), 3.247) < 2e-2);
}
#[test]
fn chi2_inv_low_dof_k1() {
assert!(rel(chi2_inv(0.95, 1.0), 3.841459) < 2e-2);
}
#[test]
fn chi2_inv_low_dof_k2() {
assert!(rel(chi2_inv(0.95, 2.0), 5.991465) < 2e-2);
}
#[test]
fn chi2_inv_fractional_low_dof() {
assert!(rel(chi2_inv(0.95, 2.5), 6.928076) < 2e-2);
}
#[test]
fn edf_white_pm_kat() {
assert!(rel(edf(NoiseType::WhitePM, 1000, 10, VarType::Allan), 495.4444) < 2e-2);
}
#[test]
fn edf_white_fm_kat() {
assert!(rel(edf(NoiseType::WhiteFM, 1000, 1, VarType::Allan), 665.113) < 2e-2);
}
#[test]
fn edf_rw_fm_kat() {
assert!(
rel(
edf(NoiseType::RandomWalkFM, 1000, 10, VarType::Allan),
97.23
) < 2e-2
);
}
#[test]
fn confidence_interval_brackets_estimate() {
let nu = edf(NoiseType::WhiteFM, 1000, 1, VarType::Allan);
let s2 = 1.0e-24;
let (lo, hi) = confidence_interval(s2, nu, 0.025, 0.975);
assert!(lo < s2 && s2 < hi, "CI must bracket the point estimate");
assert!(lo > 0.0);
}
}