#[derive(Debug, Clone, Copy)]
pub struct StageResult {
pub label: &'static str,
pub n_obs: u32,
pub n_calm_obs: u32,
pub n_false_alarms: u32,
pub n_detections: u32,
pub first_detection_k: Option<u32>,
pub lambda_at_detection: Option<f32>,
pub lambda_event_peak: f32,
pub ground_truth_onset_k: u32,
}
impl StageResult {
pub const fn new(label: &'static str, ground_truth_onset_k: u32) -> Self {
Self {
label,
n_obs: 0,
n_calm_obs: 0,
n_false_alarms: 0,
n_detections: 0,
first_detection_k: None,
lambda_at_detection: None,
lambda_event_peak: 0.0,
ground_truth_onset_k,
}
}
pub fn false_alarm_rate(&self) -> f32 {
if self.n_calm_obs == 0 { return 0.0; }
self.n_false_alarms as f32 / self.n_calm_obs as f32
}
pub fn lead_time_samples(&self) -> Option<i32> {
self.first_detection_k.map(|k| {
self.ground_truth_onset_k as i32 - k as i32
})
}
pub fn lead_time_ms(&self, sample_rate_hz: f32) -> Option<f32> {
self.lead_time_samples().map(|lt| {
lt as f32 / sample_rate_hz * 1000.0
})
}
pub fn meets_1e5_fa_threshold(&self) -> bool {
self.false_alarm_rate() < 1e-5
}
}
#[derive(Debug, Clone, Copy)]
pub struct SigMfAnnotation {
pub label: &'static str,
pub onset_sample: u32,
pub end_sample: u32,
pub confidence: f32,
}
impl SigMfAnnotation {
pub const fn precise(label: &'static str, onset: u32, end: u32) -> Self {
Self {
label,
onset_sample: onset,
end_sample: end,
confidence: 1.0,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct AuditReport {
pub dataset_label: &'static str,
pub stage_i: StageResult,
pub stage_ii: StageResult,
pub stage_iii: StageResult,
pub sample_rate_hz: f32,
pub observer_contract_holds: bool,
pub unsafe_count: u32,
pub non_claim: &'static str,
}
impl AuditReport {
#[cfg(feature = "std")]
pub fn print(&self) {
extern crate std;
use std::println;
println!();
println!("┌─────────────────────────────────────────────────────");
println!("│ CONTINUOUS RIGOR AUDIT — {}", self.dataset_label);
let obs_status = if self.observer_contract_holds { "HOLDS" } else { "VIOLATED" };
println!("│ Sample rate: {:.0} Hz Observer contract: {} unsafe: {}",
self.sample_rate_hz, obs_status, self.unsafe_count);
println!("├─────────────────────────────────────────────────────");
for (idx, stage) in [&self.stage_i, &self.stage_ii, &self.stage_iii]
.iter().enumerate()
{
let stage_num = idx + 1;
let fa_rate = stage.false_alarm_rate();
let fa_flag = if fa_rate < 1e-5 { "✓ < 10⁻⁵" }
else if fa_rate < 1e-3 { "⚠ < 10⁻³" }
else { "✗ ≥ 10⁻³" };
println!("│");
println!("│ Stage {} {}", stage_num, stage.label);
println!("│ Observations : {} ({} calm)",
stage.n_obs, stage.n_calm_obs);
println!("│ False alarms : {} FA rate: {:.2e} [{}]",
stage.n_false_alarms, fa_rate, fa_flag);
match stage.first_detection_k {
Some(k) => println!("│ Detections : {} First at k={}",
stage.n_detections, k),
None => println!("│ Detections : {} First at k=NONE",
stage.n_detections),
}
match stage.lead_time_samples() {
Some(lt) => println!("│ Lead time : {:+} samples ({:+.1} ms)",
lt, lt as f32 / self.sample_rate_hz * 1000.0),
None => println!("│ Lead time : N/A"),
}
if let Some(lam) = stage.lambda_at_detection {
println!("│ λ at detect : {:+.4}", lam);
}
println!("│ λ_event_peak : {:+.4}", stage.lambda_event_peak);
println!("│ GT onset : k={}", stage.ground_truth_onset_k);
}
println!("├─────────────────────────────────────────────────────");
println!("│ NON-CLAIM: {}", self.non_claim);
println!("└─────────────────────────────────────────────────────");
println!();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stage_result_fa_rate_zero_calm_obs() {
let r = StageResult::new("test", 100);
assert_eq!(r.false_alarm_rate(), 0.0);
}
#[test]
fn stage_result_lead_time_none_when_no_detection() {
let r = StageResult::new("test", 500);
assert_eq!(r.lead_time_samples(), None);
}
#[test]
fn stage_result_lead_time_positive_early_detection() {
let mut r = StageResult::new("test", 500);
r.first_detection_k = Some(480);
assert_eq!(r.lead_time_samples(), Some(20));
}
#[test]
fn stage_result_lead_time_negative_late_detection() {
let mut r = StageResult::new("test", 500);
r.first_detection_k = Some(520);
assert_eq!(r.lead_time_samples(), Some(-20));
}
#[test]
fn stage_result_fa_rate_threshold() {
let mut r = StageResult::new("test", 100);
r.n_calm_obs = 10_000;
r.n_false_alarms = 0;
assert!(r.meets_1e5_fa_threshold());
r.n_false_alarms = 1; assert!(!r.meets_1e5_fa_threshold());
}
#[test]
fn sigmf_annotation_precise() {
let ann = SigMfAnnotation::precise("onset", 1000, 2000);
assert_eq!(ann.onset_sample, 1000);
assert!((ann.confidence - 1.0).abs() < 1e-6);
}
}