1extern crate alloc;
11use alloc::vec::Vec;
12
13use crate::math::sqrt;
14use crate::result::{EffectEstimate, EffectPattern, MeasurementQuality};
15use crate::types::{Matrix2, Matrix9, Vector2, Vector9};
16
17#[derive(Clone, Debug)]
29pub struct Posterior {
30 pub delta_post: Vector9,
32
33 pub lambda_post: Matrix9,
35
36 pub beta_proj: Vector2,
40
41 pub beta_proj_cov: Matrix2,
43
44 pub beta_draws: Vec<Vector2>,
47
48 pub leak_probability: f64,
51
52 pub projection_mismatch_q: f64,
56
57 pub n: usize,
59
60 pub lambda_mean: Option<f64>,
64
65 pub lambda_mixing_ok: Option<bool>,
69
70 pub kappa_mean: Option<f64>,
73
74 pub kappa_cv: Option<f64>,
77
78 pub kappa_ess: Option<f64>,
81
82 pub kappa_mixing_ok: Option<bool>,
85}
86
87impl Posterior {
88 #[allow(clippy::too_many_arguments)]
90 pub fn new(
91 delta_post: Vector9,
92 lambda_post: Matrix9,
93 beta_proj: Vector2,
94 beta_proj_cov: Matrix2,
95 beta_draws: Vec<Vector2>,
96 leak_probability: f64,
97 projection_mismatch_q: f64,
98 n: usize,
99 ) -> Self {
100 Self {
101 delta_post,
102 lambda_post,
103 beta_proj,
104 beta_proj_cov,
105 beta_draws,
106 leak_probability,
107 projection_mismatch_q,
108 n,
109 lambda_mean: None, lambda_mixing_ok: None, kappa_mean: None, kappa_cv: None, kappa_ess: None, kappa_mixing_ok: None, }
116 }
117
118 #[allow(clippy::too_many_arguments)]
120 pub fn new_with_gibbs(
121 delta_post: Vector9,
122 lambda_post: Matrix9,
123 beta_proj: Vector2,
124 beta_proj_cov: Matrix2,
125 beta_draws: Vec<Vector2>,
126 leak_probability: f64,
127 projection_mismatch_q: f64,
128 n: usize,
129 lambda_mean: f64,
130 lambda_mixing_ok: bool,
131 kappa_mean: f64,
132 kappa_cv: f64,
133 kappa_ess: f64,
134 kappa_mixing_ok: bool,
135 ) -> Self {
136 Self {
137 delta_post,
138 lambda_post,
139 beta_proj,
140 beta_proj_cov,
141 beta_draws,
142 leak_probability,
143 projection_mismatch_q,
144 n,
145 lambda_mean: Some(lambda_mean),
146 lambda_mixing_ok: Some(lambda_mixing_ok),
147 kappa_mean: Some(kappa_mean),
148 kappa_cv: Some(kappa_cv),
149 kappa_ess: Some(kappa_ess),
150 kappa_mixing_ok: Some(kappa_mixing_ok),
151 }
152 }
153
154 #[inline]
156 pub fn shift_ns(&self) -> f64 {
157 self.beta_proj[0]
158 }
159
160 #[inline]
162 pub fn tail_ns(&self) -> f64 {
163 self.beta_proj[1]
164 }
165
166 #[inline]
168 pub fn shift_se(&self) -> f64 {
169 sqrt(self.beta_proj_cov[(0, 0)])
170 }
171
172 #[inline]
174 pub fn tail_se(&self) -> f64 {
175 sqrt(self.beta_proj_cov[(1, 1)])
176 }
177
178 pub fn max_effect_ns(&self) -> f64 {
180 self.delta_post
181 .iter()
182 .map(|x| x.abs())
183 .fold(0.0_f64, f64::max)
184 }
185
186 pub fn to_effect_estimate(&self) -> EffectEstimate {
191 let pattern = self.classify_pattern_from_draws();
193
194 let shift_std = self.shift_se();
196 let tail_std = self.tail_se();
197 let total_effect =
198 sqrt(self.shift_ns() * self.shift_ns() + self.tail_ns() * self.tail_ns());
199 let total_std = sqrt((shift_std * shift_std + tail_std * tail_std) / 2.0);
200
201 let ci_low = (total_effect - 1.96 * total_std).max(0.0);
202 let ci_high = total_effect + 1.96 * total_std;
203
204 EffectEstimate {
205 shift_ns: self.shift_ns(),
206 tail_ns: self.tail_ns(),
207 credible_interval_ns: (ci_low, ci_high),
208 pattern,
209 interpretation_caveat: None,
210 }
211 }
212
213 fn classify_pattern_from_draws(&self) -> EffectPattern {
225 const DOMINANCE_RATIO: f64 = 5.0;
227 const DOMINANCE_PROB: f64 = 0.80;
229 const MIN_SIGNIFICANT_NS: f64 = 10.0;
232
233 if self.beta_draws.is_empty() {
234 return self.classify_pattern_point_estimate();
236 }
237
238 let n = self.beta_draws.len() as f64;
239
240 let mut shift_significant_count = 0;
242 let mut tail_significant_count = 0;
243 let mut shift_dominates_count = 0;
244 let mut tail_dominates_count = 0;
245
246 for beta in &self.beta_draws {
247 let shift_abs = beta[0].abs();
248 let tail_abs = beta[1].abs();
249
250 if shift_abs > MIN_SIGNIFICANT_NS {
252 shift_significant_count += 1;
253 }
254 if tail_abs > MIN_SIGNIFICANT_NS {
255 tail_significant_count += 1;
256 }
257
258 if tail_abs < 1e-12 || shift_abs >= DOMINANCE_RATIO * tail_abs {
260 shift_dominates_count += 1;
261 }
262 if shift_abs < 1e-12 || tail_abs >= DOMINANCE_RATIO * shift_abs {
263 tail_dominates_count += 1;
264 }
265 }
266
267 let p_shift_significant = shift_significant_count as f64 / n;
269 let p_tail_significant = tail_significant_count as f64 / n;
270 let p_shift_dominates = shift_dominates_count as f64 / n;
271 let p_tail_dominates = tail_dominates_count as f64 / n;
272
273 if p_shift_dominates >= DOMINANCE_PROB {
276 EffectPattern::UniformShift
277 } else if p_tail_dominates >= DOMINANCE_PROB {
278 EffectPattern::TailEffect
279 } else if p_shift_significant >= DOMINANCE_PROB && p_tail_significant >= DOMINANCE_PROB {
280 EffectPattern::Mixed
282 } else {
283 EffectPattern::Indeterminate
284 }
285 }
286
287 fn classify_pattern_point_estimate(&self) -> EffectPattern {
289 const DOMINANCE_RATIO: f64 = 5.0;
290
291 let shift_abs = self.shift_ns().abs();
292 let tail_abs = self.tail_ns().abs();
293 let shift_se = self.shift_se();
294 let tail_se = self.tail_se();
295
296 let shift_significant = shift_abs > 2.0 * shift_se;
298 let tail_significant = tail_abs > 2.0 * tail_se;
299
300 match (shift_significant, tail_significant) {
301 (true, false) => EffectPattern::UniformShift,
302 (false, true) => EffectPattern::TailEffect,
303 (true, true) => {
304 if shift_abs > tail_abs * DOMINANCE_RATIO {
306 EffectPattern::UniformShift
307 } else if tail_abs > shift_abs * DOMINANCE_RATIO {
308 EffectPattern::TailEffect
309 } else {
310 EffectPattern::Mixed
311 }
312 }
313 (false, false) => EffectPattern::Indeterminate,
314 }
315 }
316
317 pub fn measurement_quality(&self) -> MeasurementQuality {
322 let effect_se = self.shift_se();
323 MeasurementQuality::from_mde_ns(effect_se * 2.0)
324 }
325
326 pub fn to_summary(&self) -> crate::ffi_summary::PosteriorSummary {
331 let effect = self.to_effect_estimate();
332
333 crate::ffi_summary::PosteriorSummary {
334 shift_ns: self.shift_ns(),
335 tail_ns: self.tail_ns(),
336 shift_se: self.shift_se(),
337 tail_se: self.tail_se(),
338 ci_low_ns: effect.credible_interval_ns.0,
339 ci_high_ns: effect.credible_interval_ns.1,
340 pattern: effect.pattern,
341 leak_probability: self.leak_probability,
342 projection_mismatch_q: self.projection_mismatch_q,
343 n: self.n,
344 lambda_mean: self.lambda_mean.unwrap_or(1.0),
345 lambda_mixing_ok: self.lambda_mixing_ok.unwrap_or(true),
346 kappa_mean: self.kappa_mean.unwrap_or(1.0),
347 kappa_cv: self.kappa_cv.unwrap_or(0.0),
348 kappa_ess: self.kappa_ess.unwrap_or(0.0),
349 kappa_mixing_ok: self.kappa_mixing_ok.unwrap_or(true),
350 }
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357
358 #[test]
359 fn test_posterior_accessors() {
360 let delta_post =
361 Vector9::from_row_slice(&[10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0, 10.0]);
362 let lambda_post = Matrix9::identity();
363 let beta_proj = Vector2::new(10.0, 0.0);
364 let beta_proj_cov = Matrix2::new(4.0, 0.0, 0.0, 1.0);
365
366 let posterior = Posterior::new(
367 delta_post,
368 lambda_post,
369 beta_proj,
370 beta_proj_cov,
371 Vec::new(), 0.75,
373 0.5,
374 1000,
375 );
376
377 assert_eq!(posterior.shift_ns(), 10.0);
378 assert_eq!(posterior.tail_ns(), 0.0);
379 assert_eq!(posterior.shift_se(), 2.0); assert_eq!(posterior.tail_se(), 1.0); assert_eq!(posterior.leak_probability, 0.75);
382 assert_eq!(posterior.n, 1000);
383 assert!((posterior.max_effect_ns() - 10.0).abs() < 1e-10);
384 }
385
386 #[test]
387 fn test_posterior_clone() {
388 let delta_post = Vector9::from_row_slice(&[5.0; 9]);
389 let lambda_post = Matrix9::identity();
390 let beta_proj = Vector2::new(5.0, 3.0);
391 let beta_proj_cov = Matrix2::identity();
392
393 let posterior = Posterior::new(
394 delta_post,
395 lambda_post,
396 beta_proj,
397 beta_proj_cov,
398 Vec::new(), 0.5,
400 1.0,
401 500,
402 );
403
404 let cloned = posterior.clone();
405 assert_eq!(cloned.shift_ns(), posterior.shift_ns());
406 assert_eq!(cloned.tail_ns(), posterior.tail_ns());
407 assert_eq!(cloned.leak_probability, posterior.leak_probability);
408 }
409}