extern crate std;
use std::format;
use std::string::String;
use std::vec::Vec;
use super::detector_firing::DetectorSelectivity;
const AXIS_EPS: f64 = 0.01;
#[derive(Debug, Clone)]
pub struct AxisDiscriminationEntry {
pub tier_letter: &'static str,
pub tier_bit: u32,
pub detectors_in_axis: usize,
pub mean_healthy_rate: f64,
pub mean_fault_rate: f64,
pub discrimination_ratio: f64,
pub fixtures_observed: usize,
}
#[derive(Debug, Clone)]
pub struct AxisDiscriminationReport {
pub entries: Vec<AxisDiscriminationEntry>,
}
fn tier_of(detector_name: &str) -> Option<&'static str> {
match detector_name {
"scalar_3sigma" | "scalar" | "cusum" | "ewma" => Some("A"),
"robust_z" | "page_hinkley" | "tukey_iqr" => Some("B"),
"spectral_residual" | "matrix_profile" | "bocpd" |
"isolation_forest" | "lof" => Some("C"),
"mann_kendall" | "rolling_z" | "ar1_residual" |
"mahalanobis" | "ks_rolling" => Some("D"),
"poisson_burst" | "saturation_chain" | "chi_squared_prop" => Some("E"),
"max_interval_burst" | "log_isi_burst" |
"rank_surprise_burst" | "misi_burst" => Some("F"),
"glr" | "adwin" | "mewma" | "retry_storm" | "correlation_break" => Some("EXTRA"),
"causal_lag" => Some("M"),
"dsfb_structural" => Some("STRUCTURAL"),
_ => None,
}
}
fn tier_of_family(detector_name: &str) -> Option<&'static str> {
if detector_name.starts_with("g_") { return Some("G"); }
if detector_name.starts_with("h_") { return Some("H"); }
if detector_name.starts_with("i_") { return Some("I"); }
if detector_name.starts_with("j_") { return Some("J"); }
if detector_name.starts_with("k_") { return Some("K"); }
if detector_name.starts_with("l_") { return Some("L"); }
if detector_name.starts_with("m_") { return Some("M"); }
if detector_name.starts_with("n_") { return Some("N"); }
if detector_name.starts_with("o_") { return Some("O"); }
if detector_name.starts_with("p_") { return Some("P"); }
if detector_name.starts_with("q_") { return Some("Q"); }
if detector_name.starts_with("r_") { return Some("R"); }
if detector_name.starts_with("s_") { return Some("S"); }
if detector_name.starts_with("t_") { return Some("T"); }
if detector_name.starts_with("u_") { return Some("U"); }
if detector_name.starts_with("v_") { return Some("V"); }
if detector_name.starts_with("x_") { return Some("X"); }
if detector_name.starts_with("y_") { return Some("Y"); }
if detector_name.starts_with("z_") { return Some("Z"); }
if detector_name.starts_with("aa_") { return Some("AA"); }
None
}
fn resolve_tier(detector_name: &str) -> &'static str {
if let Some(t) = tier_of(detector_name) { return t; }
if let Some(t) = tier_of_family(detector_name) { return t; }
"?"
}
fn tier_bit(letter: &str) -> u32 {
match letter {
"A" => 1 << 0,
"B" => 1 << 1,
"C" => 1 << 2,
"D" => 1 << 3,
"E" => 1 << 4,
"F" => 1 << 5,
"EXTRA" => 1 << 6,
"G" => 1 << 7,
"H" => 1 << 8,
"I" => 1 << 9,
"J" => 1 << 10,
"K" => 1 << 11,
"L" => 1 << 12,
"M" => 1 << 13,
"N" => 1 << 14,
"O" => 1 << 15,
"P" => 1 << 16,
"Q" => 1 << 17,
"R" => 1 << 18,
"S" => 1 << 19,
"T" => 1 << 20,
"U" => 1 << 21,
"V" => 1 << 22,
"X" => 1 << 24,
"Y" => 1 << 25,
"Z" => 1 << 26,
"AA" => 1 << 27,
_ => 0,
}
}
pub fn compute_axis_discrimination(
per_fixture: &[Vec<DetectorSelectivity>],
) -> AxisDiscriminationReport {
use std::collections::BTreeMap;
let mut by_tier: BTreeMap<&'static str, Vec<&DetectorSelectivity>>
= BTreeMap::new();
let mut fixtures_per_tier: BTreeMap<&'static str,
std::collections::BTreeSet<&'static str>>
= BTreeMap::new();
for fix in per_fixture {
for entry in fix {
let tier = resolve_tier(entry.detector_name);
if tier == "?" || tier == "STRUCTURAL" { continue; }
by_tier.entry(tier).or_default().push(entry);
fixtures_per_tier.entry(tier).or_default().insert(entry.fixture_name);
}
}
let mut entries: Vec<AxisDiscriminationEntry> = by_tier.into_iter()
.map(|(letter, recs)| {
let n = recs.len() as f64;
let mean_healthy = recs.iter().map(|r| r.healthy_firing_rate)
.sum::<f64>() / n;
let mean_fault = recs.iter().map(|r| r.fault_firing_rate)
.sum::<f64>() / n;
let discrimination_ratio = mean_fault / (mean_healthy + AXIS_EPS);
let fixtures_observed = fixtures_per_tier.get(letter)
.map(|s| s.len()).unwrap_or(0);
AxisDiscriminationEntry {
tier_letter: letter,
tier_bit: tier_bit(letter),
detectors_in_axis: recs.len(),
mean_healthy_rate: mean_healthy,
mean_fault_rate: mean_fault,
discrimination_ratio,
fixtures_observed,
}
}).collect();
entries.sort_by(|a, b| b.discrimination_ratio
.partial_cmp(&a.discrimination_ratio)
.unwrap_or(std::cmp::Ordering::Equal));
AxisDiscriminationReport { entries }
}
pub fn render_axis_discrimination_md(report: &AxisDiscriminationReport) -> String {
let mut out = String::new();
out.push_str("# Per-axis (tier) discrimination audit\n\n");
out.push_str("Discrimination ratio = mean_fault_rate / (mean_healthy_rate + 0.01)\n");
out.push_str("aggregated across all detectors in each axis, across all 12 fixtures.\n\n");
out.push_str("Higher = axis fires more selectively on fault windows.\n");
out.push_str("A ratio near 1.0 indicates the axis fires equally on healthy/fault.\n");
out.push_str("A ratio near 0.0 indicates the axis is dormant on the current surface.\n\n");
out.push_str("Source: Phase ζ.6 audit harness (`src/audit/axis_discrimination.rs`).\n\n");
out.push_str("| Tier | Bit | Detectors | Fixtures | Mean healthy | Mean fault | Discrimination |\n");
out.push_str("|:----:|----:|----------:|---------:|-------------:|-----------:|---------------:|\n");
for e in &report.entries {
out.push_str(&format!(
"| {} | 0x{:08x} | {} | {} | {:.4} | {:.4} | {:.4} |\n",
e.tier_letter,
e.tier_bit,
e.detectors_in_axis,
e.fixtures_observed,
e.mean_healthy_rate,
e.mean_fault_rate,
e.discrimination_ratio,
));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use std::vec;
fn sel(name: &'static str, fixture: &'static str,
healthy: f64, fault: f64) -> DetectorSelectivity {
DetectorSelectivity {
detector_name: name,
fixture_name: fixture,
healthy_firing_rate: healthy,
fault_firing_rate: fault,
selectivity_index: fault / (healthy + 0.01),
}
}
#[test]
fn tier_a_resolves_for_parametric_trio() {
assert_eq!(resolve_tier("scalar_3sigma"), "A");
assert_eq!(resolve_tier("cusum"), "A");
assert_eq!(resolve_tier("ewma"), "A");
}
#[test]
fn family_prefix_resolves() {
assert_eq!(resolve_tier("g_ddm"), "G");
assert_eq!(resolve_tier("h_jensen_shannon"), "H");
assert_eq!(resolve_tier("aa_arch_residual"), "AA");
}
#[test]
fn axis_discrimination_aggregates_across_detectors() {
let f1 = vec![
sel("scalar_3sigma", "f1", 0.0, 1.0),
sel("cusum", "f1", 0.0, 1.0),
];
let f2 = vec![
sel("scalar_3sigma", "f2", 0.0, 1.0),
sel("cusum", "f2", 0.0, 1.0),
];
let report = compute_axis_discrimination(&[f1, f2]);
let a = report.entries.iter().find(|e| e.tier_letter == "A").unwrap();
assert_eq!(a.detectors_in_axis, 4); assert_eq!(a.fixtures_observed, 2);
assert!((a.mean_fault_rate - 1.0).abs() < 1e-9);
assert!(a.discrimination_ratio > 50.0);
}
}