tacet_core/analysis/
mde.rs1extern crate alloc;
17
18use crate::math;
19use crate::result::MinDetectableEffect;
20use crate::types::Matrix9;
21
22#[derive(Debug, Clone)]
24pub struct MdeEstimate {
25 pub mde_ns: f64,
27 pub n_simulations: usize,
29}
30
31impl From<MdeEstimate> for MinDetectableEffect {
32 fn from(mde: MdeEstimate) -> Self {
33 MinDetectableEffect { mde_ns: mde.mde_ns }
34 }
35}
36
37pub fn analytical_mde(covariance: &Matrix9, alpha: f64) -> f64 {
53 let max_var = (0..9).map(|i| covariance[(i, i)]).fold(0.0_f64, f64::max);
55
56 let z = probit(1.0 - alpha / 2.0);
58 z * math::sqrt(max_var)
59}
60
61fn probit(p: f64) -> f64 {
66 if p <= 0.0 {
67 return f64::NEG_INFINITY;
68 }
69 if p >= 1.0 {
70 return f64::INFINITY;
71 }
72
73 let (sign, q) = if p < 0.5 { (-1.0, 1.0 - p) } else { (1.0, p) };
75
76 const C0: f64 = 2.515517;
78 const C1: f64 = 0.802853;
79 const C2: f64 = 0.010328;
80 const D1: f64 = 1.432788;
81 const D2: f64 = 0.189269;
82 const D3: f64 = 0.001308;
83
84 let t = math::sqrt(-2.0 * math::ln(1.0 - q));
85 let z = t - (C0 + C1 * t + C2 * t * t) / (1.0 + D1 * t + D2 * t * t + D3 * t * t * t);
86
87 sign * z
88}
89
90pub fn estimate_mde(covariance: &Matrix9, alpha: f64) -> MdeEstimate {
101 let mde_ns = analytical_mde(covariance, alpha);
102
103 MdeEstimate {
104 mde_ns,
105 n_simulations: 0, }
107}
108
109#[allow(dead_code)]
113fn safe_cholesky(matrix: &Matrix9) -> nalgebra::Cholesky<f64, nalgebra::Const<9>> {
114 if let Some(chol) = nalgebra::Cholesky::new(*matrix) {
116 return chol;
117 }
118
119 let trace = matrix.trace();
121 let base_jitter = 1e-10;
122 let adaptive_jitter = (trace / 9.0) * 1e-8;
123 let jitter = base_jitter + adaptive_jitter;
124
125 let mut regularized = *matrix;
126 for i in 0..9 {
127 regularized[(i, i)] += jitter;
128 }
129
130 nalgebra::Cholesky::new(regularized).expect("Cholesky failed even after regularization")
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn test_mde_positive() {
139 let cov = Matrix9::identity();
141 let mde = estimate_mde(&cov, 0.05);
142
143 assert!(mde.mde_ns > 0.0, "MDE should be positive");
144 }
145
146 #[test]
147 fn test_probit_accuracy() {
148 assert!((probit(0.5) - 0.0).abs() < 1e-3, "probit(0.5) should be 0");
150 assert!(
151 (probit(0.975) - 1.96).abs() < 1e-2,
152 "probit(0.975) should be ~1.96"
153 );
154 assert!(
155 (probit(0.995) - 2.576).abs() < 1e-2,
156 "probit(0.995) should be ~2.576"
157 );
158 assert!(
159 (probit(0.025) + 1.96).abs() < 1e-2,
160 "probit(0.025) should be ~-1.96"
161 );
162 }
163
164 #[test]
165 fn test_analytical_mde_sanity_check() {
166 let cov = Matrix9::identity();
172 let mde = analytical_mde(&cov, 0.05);
173
174 let expected = 1.96;
175 assert!(
176 (mde - expected).abs() < 0.05,
177 "MDE should be ~{:.3}, got {:.3}",
178 expected,
179 mde
180 );
181 }
182
183 #[test]
184 fn test_analytical_mde_alpha_scaling() {
185 let cov = Matrix9::identity();
187 let mde_05 = analytical_mde(&cov, 0.05); let mde_01 = analytical_mde(&cov, 0.01); assert!(
191 mde_01 > mde_05,
192 "MDE at α=0.01 ({:.3}) should be larger than α=0.05 ({:.3})",
193 mde_01,
194 mde_05
195 );
196 }
197
198 #[test]
199 fn test_analytical_mde_diagonal_covariance() {
200 let mut cov = Matrix9::zeros();
202 for i in 0..9 {
203 cov[(i, i)] = (i + 1) as f64;
204 }
205
206 let mde = analytical_mde(&cov, 0.05);
207
208 let expected = 1.96 * math::sqrt(9.0);
210 assert!(
211 (mde - expected).abs() < 0.1,
212 "MDE should be ~{:.3}, got {:.3}",
213 expected,
214 mde
215 );
216 }
217}