extern crate std;
use std::collections::BTreeMap;
use std::format;
use std::string::String;
use std::vec::Vec;
use crate::incumbent_baselines::DetectorOutput;
const SELECTIVITY_EPS: f64 = 0.01;
#[derive(Debug, Clone)]
pub struct DetectorSelectivity {
pub detector_name: &'static str,
pub fixture_name: &'static str,
pub healthy_firing_rate: f64,
pub fault_firing_rate: f64,
pub selectivity_index: f64,
}
pub fn compute_detector_selectivity_per_fixture(
per_detector: &[DetectorOutput],
fixture_name: &'static str,
) -> Vec<DetectorSelectivity> {
per_detector.iter().map(|d| {
let healthy_firing_rate = if d.clean_windows > 0 {
d.clean_window_false_alerts as f64 / d.clean_windows as f64
} else {
0.0
};
let fault_firing_rate = if d.total_faults > 0 {
d.captured_faults as f64 / d.total_faults as f64
} else {
0.0
};
let selectivity_index =
fault_firing_rate / (healthy_firing_rate + SELECTIVITY_EPS);
DetectorSelectivity {
detector_name: d.detector_name,
fixture_name,
healthy_firing_rate,
fault_firing_rate,
selectivity_index,
}
}).collect()
}
#[derive(Debug, Clone)]
pub struct CrossFixtureDetectorEntry {
pub detector_name: &'static str,
pub fixtures_observed: usize,
pub mean_selectivity: f64,
pub stddev_selectivity: f64,
pub mean_healthy_rate: f64,
pub mean_fault_rate: f64,
pub top_3_fixtures: Vec<(&'static str, f64)>,
pub bottom_3_fixtures: Vec<(&'static str, f64)>,
}
#[derive(Debug, Clone)]
pub struct CrossFixtureDetectorReport {
pub entries: Vec<CrossFixtureDetectorEntry>,
}
pub fn aggregate_detector_audit(
per_fixture: &[Vec<DetectorSelectivity>],
) -> CrossFixtureDetectorReport {
let mut by_name: BTreeMap<&'static str, Vec<&DetectorSelectivity>> = BTreeMap::new();
for fix in per_fixture {
for entry in fix {
by_name.entry(entry.detector_name).or_default().push(entry);
}
}
let mut entries: Vec<CrossFixtureDetectorEntry> = by_name.into_iter()
.map(|(name, recs)| {
let n = recs.len();
let mean_sel = recs.iter().map(|r| r.selectivity_index)
.sum::<f64>() / n as f64;
let var_sel = recs.iter()
.map(|r| (r.selectivity_index - mean_sel).powi(2))
.sum::<f64>() / n as f64;
let stddev_sel = var_sel.sqrt();
let mean_healthy = recs.iter().map(|r| r.healthy_firing_rate)
.sum::<f64>() / n as f64;
let mean_fault = recs.iter().map(|r| r.fault_firing_rate)
.sum::<f64>() / n as f64;
let mut sorted = recs.clone();
sorted.sort_by(|a, b| b.selectivity_index
.partial_cmp(&a.selectivity_index)
.unwrap_or(std::cmp::Ordering::Equal));
let top_3: Vec<(&'static str, f64)> = sorted.iter().take(3)
.map(|r| (r.fixture_name, r.selectivity_index)).collect();
let bottom_3: Vec<(&'static str, f64)> = sorted.iter().rev().take(3)
.map(|r| (r.fixture_name, r.selectivity_index)).collect();
CrossFixtureDetectorEntry {
detector_name: name,
fixtures_observed: n,
mean_selectivity: mean_sel,
stddev_selectivity: stddev_sel,
mean_healthy_rate: mean_healthy,
mean_fault_rate: mean_fault,
top_3_fixtures: top_3,
bottom_3_fixtures: bottom_3,
}
}).collect();
entries.sort_by(|a, b| b.mean_selectivity
.partial_cmp(&a.mean_selectivity)
.unwrap_or(std::cmp::Ordering::Equal));
CrossFixtureDetectorReport { entries }
}
pub fn render_detector_selectivity_md(report: &CrossFixtureDetectorReport) -> String {
let mut out = String::new();
out.push_str("# Per-detector selectivity audit\n\n");
out.push_str("Selectivity index = fault_firing_rate / (healthy_firing_rate + 0.01).\n");
out.push_str("Higher = more discriminating on the cross-fixture surface.\n\n");
out.push_str("Source: Phase ζ.1 audit harness (`src/audit/detector_firing.rs`).\n");
out.push_str("Data: verbatim from `DetectorOutput` per fixture run.\n\n");
out.push_str("| Rank | Detector | Mean selectivity | Stddev | Mean healthy | Mean fault | N fix | Top fixture |\n");
out.push_str("|-----:|----------|-----------------:|-------:|-------------:|-----------:|------:|-------------|\n");
for (i, e) in report.entries.iter().enumerate() {
let top_fix = e.top_3_fixtures.first()
.map(|(n, _)| *n).unwrap_or("--");
out.push_str(&format!(
"| {} | `{}` | {:.4} | {:.4} | {:.4} | {:.4} | {} | {} |\n",
i + 1,
e.detector_name,
e.mean_selectivity,
e.stddev_selectivity,
e.mean_healthy_rate,
e.mean_fault_rate,
e.fixtures_observed,
top_fix,
));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use std::vec;
use crate::incumbent_baselines::DetectorOutput;
fn mock_detector(
name: &'static str,
clean_window_false_alerts: u64,
clean_windows: u64,
captured_faults: u64,
total_faults: u64,
) -> DetectorOutput {
DetectorOutput {
detector_name: name,
raw_alert_count: clean_window_false_alerts + captured_faults,
alerts_per_signal: [0; 32],
alert_windows: clean_window_false_alerts + captured_faults,
episode_count: clean_window_false_alerts + captured_faults,
captured_faults,
total_faults,
clean_window_false_alerts,
clean_windows,
}
}
#[test]
fn perfect_detector_high_selectivity() {
let det = mock_detector("perfect", 0, 100, 10, 10);
let v = compute_detector_selectivity_per_fixture(&[det], "test_fixture");
assert_eq!(v.len(), 1);
assert!((v[0].selectivity_index - 100.0).abs() < 1e-9);
assert!((v[0].fault_firing_rate - 1.0).abs() < 1e-9);
assert!(v[0].healthy_firing_rate.abs() < 1e-9);
}
#[test]
fn noisy_detector_low_selectivity() {
let det = mock_detector("noisy", 100, 100, 10, 10);
let v = compute_detector_selectivity_per_fixture(&[det], "test_fixture");
assert!(v[0].selectivity_index < 1.0);
assert!(v[0].selectivity_index > 0.9);
}
#[test]
fn aggregation_orders_by_selectivity() {
let det_perfect = mock_detector("perfect", 0, 100, 10, 10);
let det_noisy = mock_detector("noisy", 100, 100, 10, 10);
let f1 = compute_detector_selectivity_per_fixture(
&[det_perfect.clone(), det_noisy.clone()], "fixture1");
let f2 = compute_detector_selectivity_per_fixture(
&[det_perfect.clone(), det_noisy.clone()], "fixture2");
let report = aggregate_detector_audit(&[f1, f2]);
assert_eq!(report.entries.len(), 2);
assert_eq!(report.entries[0].detector_name, "perfect");
assert_eq!(report.entries[1].detector_name, "noisy");
assert!(report.entries[0].mean_selectivity
> report.entries[1].mean_selectivity);
}
#[test]
fn aggregation_handles_missing_detector_per_fixture() {
let f1 = vec![
DetectorSelectivity { detector_name: "a", fixture_name: "f1",
healthy_firing_rate: 0.0, fault_firing_rate: 1.0,
selectivity_index: 100.0 },
DetectorSelectivity { detector_name: "b", fixture_name: "f1",
healthy_firing_rate: 0.5, fault_firing_rate: 0.5,
selectivity_index: 0.98 },
];
let f2 = vec![
DetectorSelectivity { detector_name: "a", fixture_name: "f2",
healthy_firing_rate: 0.1, fault_firing_rate: 1.0,
selectivity_index: 9.09 },
];
let report = aggregate_detector_audit(&[f1, f2]);
let a = report.entries.iter().find(|e| e.detector_name == "a").unwrap();
let b = report.entries.iter().find(|e| e.detector_name == "b").unwrap();
assert_eq!(a.fixtures_observed, 2);
assert_eq!(b.fixtures_observed, 1);
}
#[test]
fn deterministic_replay_same_inputs_same_outputs() {
let det = mock_detector("d", 5, 100, 8, 10);
let v1 = compute_detector_selectivity_per_fixture(&[det.clone()], "fix");
let v2 = compute_detector_selectivity_per_fixture(&[det.clone()], "fix");
assert_eq!(v1.len(), v2.len());
assert!((v1[0].selectivity_index - v2[0].selectivity_index).abs() < 1e-15);
}
}