#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ForensicBaseline<const D: usize> {
#[cfg_attr(feature = "serde", serde(with = "crate::serde_util::fixed_array_f64"))]
pub observed: [f64; D],
#[cfg_attr(feature = "serde", serde(with = "crate::serde_util::fixed_array_f64"))]
pub expected: [f64; D],
#[cfg_attr(feature = "serde", serde(with = "crate::serde_util::fixed_array_f64"))]
pub stddev: [f64; D],
#[cfg_attr(feature = "serde", serde(with = "crate::serde_util::fixed_array_f64"))]
pub delta: [f64; D],
#[cfg_attr(feature = "serde", serde(with = "crate::serde_util::fixed_array_f64"))]
pub zscore: [f64; D],
pub live_points: usize,
}
impl<const D: usize> ForensicBaseline<D> {
#[must_use]
pub fn argmax_abs_zscore(&self) -> Option<usize> {
if D == 0 || self.live_points == 0 {
return None;
}
let mut best: usize = 0;
let mut best_val = self.zscore[0].abs();
for d in 1..D {
let v = self.zscore[d].abs();
if v > best_val {
best = d;
best_val = v;
}
}
if best_val == 0.0 { None } else { Some(best) }
}
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use super::*;
#[test]
fn argmax_abs_zscore_picks_biggest() {
let b = ForensicBaseline::<4> {
observed: [0.0, 0.0, 0.0, 0.0],
expected: [0.0, 0.0, 0.0, 0.0],
stddev: [1.0, 1.0, 1.0, 1.0],
delta: [0.1, -2.0, 0.5, 1.0],
zscore: [0.1, -2.0, 0.5, 1.0],
live_points: 16,
};
assert_eq!(b.argmax_abs_zscore(), Some(1));
}
#[test]
fn argmax_abs_zscore_empty_when_no_live_points() {
let b = ForensicBaseline::<2> {
observed: [0.0, 0.0],
expected: [0.0, 0.0],
stddev: [0.0, 0.0],
delta: [0.0, 0.0],
zscore: [0.0, 0.0],
live_points: 0,
};
assert!(b.argmax_abs_zscore().is_none());
}
#[test]
fn argmax_abs_zscore_empty_when_all_zero() {
let b = ForensicBaseline::<2> {
observed: [0.0, 0.0],
expected: [0.0, 0.0],
stddev: [1.0, 1.0],
delta: [0.0, 0.0],
zscore: [0.0, 0.0],
live_points: 4,
};
assert!(b.argmax_abs_zscore().is_none());
}
}