extern crate std;
use std::vec::Vec;
use crate::engine::{DsfbRfEngine, ObservationResult};
use crate::platform::PlatformContext;
use crate::policy::PolicyDecision;
use crate::math::{mean_f32, std_dev_f32};
pub const HEALTHY_WINDOW_SIZE: usize = 100;
pub const SIGN_WINDOW_W: usize = 5;
pub const DSA_WINDOW_W: usize = 10;
pub const GRAMMAR_K: usize = 4;
pub const DSA_TAU: f32 = 2.0;
pub const CORROBORATION_M: u8 = 1;
pub const EWMA_LAMBDA: f32 = 0.20;
pub const CUSUM_KAPPA_SIGMA: f32 = 0.5;
pub const CUSUM_H_SIGMA: f32 = 5.0;
pub const WPRED: usize = 5;
pub const SNR_FLOOR_DB: f32 = -10.0;
#[derive(Debug, Clone)]
pub struct RfObservation {
pub k: usize,
pub residual_norm: f32,
pub snr_db: f32,
pub is_healthy: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct RegimeTransitionEvent {
pub k: usize,
pub label: &'static str,
}
#[derive(Debug)]
pub struct ScalarComparators {
pub threshold_mean: f32,
pub threshold_3sigma: f32,
pub ewma: f32,
pub ewma_threshold: f32,
pub cusum_pos: f32,
pub cusum_kappa: f32,
pub cusum_h: f32,
pub energy_threshold: f32,
}
impl ScalarComparators {
pub fn calibrate(healthy_norms: &[f32]) -> Self {
let m = mean_f32(healthy_norms);
let s = std_dev_f32(healthy_norms);
let mut ewma_vals: Vec<f32> = Vec::with_capacity(healthy_norms.len());
let mut ewma = 0.0_f32;
for &n in healthy_norms {
ewma = EWMA_LAMBDA * n + (1.0 - EWMA_LAMBDA) * ewma;
ewma_vals.push(ewma);
}
let ewma_mean = mean_f32(&ewma_vals);
let ewma_std = std_dev_f32(&ewma_vals);
Self {
threshold_mean: m,
threshold_3sigma: m + 3.0 * s,
ewma: 0.0,
ewma_threshold: ewma_mean + 3.0 * ewma_std,
cusum_pos: 0.0,
cusum_kappa: CUSUM_KAPPA_SIGMA * s,
cusum_h: CUSUM_H_SIGMA * s,
energy_threshold: m + 3.0 * s,
}
}
pub fn update(&mut self, norm: f32) -> (bool, bool, bool, bool) {
let thr = norm > self.threshold_3sigma;
self.ewma = EWMA_LAMBDA * norm + (1.0 - EWMA_LAMBDA) * self.ewma;
let ewma_alarm = self.ewma > self.ewma_threshold;
self.cusum_pos = (self.cusum_pos + norm - self.cusum_kappa).max(0.0);
let cusum_alarm = self.cusum_pos > self.cusum_h;
let energy_alarm = norm > self.energy_threshold;
(thr, ewma_alarm, cusum_alarm, energy_alarm)
}
pub fn reset_cusum(&mut self) {
self.cusum_pos = 0.0;
}
}
#[derive(Debug, Clone)]
pub struct Episode {
pub open_k: usize,
pub close_k: Option<usize>,
pub is_precursor: bool,
}
#[derive(Debug)]
pub struct EvaluationResult {
pub dataset: &'static str,
pub raw_boundary_count: usize,
pub dsfb_episode_count: usize,
pub episode_precision: f32,
pub recall_numerator: usize,
pub recall_denominator: usize,
pub compression_factor: f32,
pub precision_gain: f32,
pub raw_precision_proxy: f32,
pub false_episode_rate_clean: f32,
pub trace: Vec<ObservationResult>,
pub episodes: Vec<Episode>,
}
impl EvaluationResult {
pub fn recall(&self) -> f32 {
if self.recall_denominator == 0 { return 0.0; }
self.recall_numerator as f32 / self.recall_denominator as f32
}
pub fn print_summary(&self) {
std::println!("══════════════════════════════════════════════════════");
std::println!(" DSFB-RF Stage III Evaluation — {}", self.dataset);
std::println!("══════════════════════════════════════════════════════");
std::println!(" Raw boundary events: {:>8}", self.raw_boundary_count);
std::println!(" DSFB episodes: {:>8}", self.dsfb_episode_count);
std::println!(" Compression: {:>7.1}×", self.compression_factor);
std::println!(" Episode precision: {:>7.1}% (raw proxy: {:.2}%)",
self.episode_precision * 100.0, self.raw_precision_proxy * 100.0);
std::println!(" Precision gain: {:>7.1}×", self.precision_gain);
std::println!(" Recall: {}/{} ({:.1}%)",
self.recall_numerator, self.recall_denominator,
self.recall() * 100.0);
std::println!(" False ep. rate (clean): {:>7.1}%", self.false_episode_rate_clean * 100.0);
std::println!("══════════════════════════════════════════════════════");
}
pub fn check_paper_lock(&self, expected: &PaperLockExpected) -> Result<(), std::string::String> {
let eps = 0.005; if (self.episode_precision - expected.precision).abs() > eps {
return Err(std::format!(
"[{}] episode_precision={:.4} expected={:.4} (±{:.4})",
self.dataset, self.episode_precision, expected.precision, eps));
}
if self.recall_numerator < expected.recall_min {
return Err(std::format!(
"[{}] recall={}/{} below minimum {}",
self.dataset, self.recall_numerator,
self.recall_denominator, expected.recall_min));
}
if self.dsfb_episode_count != expected.episode_count {
return Err(std::format!(
"[{}] episode_count={} expected={}",
self.dataset, self.dsfb_episode_count, expected.episode_count));
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct PaperLockExpected {
pub episode_count: usize,
pub precision: f32,
pub recall_min: usize,
}
pub fn run_stage_iii(
dataset: &'static str,
observations: &[RfObservation],
events: &[RegimeTransitionEvent],
) -> EvaluationResult {
let healthy = collect_healthy_window(observations);
assert!(!healthy.is_empty(), "healthy window must not be empty");
let mut engine = DsfbRfEngine::<DSA_WINDOW_W, GRAMMAR_K, 32>::from_calibration(
&healthy, DSA_TAU,
)
.unwrap_or_else(|| DsfbRfEngine::<DSA_WINDOW_W, GRAMMAR_K, 32>::new(1.0, DSA_TAU));
engine = engine.with_snr_floor(SNR_FLOOR_DB);
let mut comparators = ScalarComparators::calibrate(&healthy);
let run = run_evaluation_pass(&mut engine, &mut comparators, observations, events);
let episodes = finalise_episodes(run.episodes, run.episode_open_k, run.episode_open, observations.len(), events);
let metrics = compute_evaluation_metrics(&episodes, run.raw_boundary_count, run.false_episodes_clean, run.clean_window_obs, events, observations.len());
EvaluationResult {
dataset,
raw_boundary_count: run.raw_boundary_count,
dsfb_episode_count: episodes.len(),
episode_precision: metrics.episode_precision,
recall_numerator: metrics.covered,
recall_denominator: events.len(),
compression_factor: metrics.compression,
precision_gain: metrics.precision_gain,
raw_precision_proxy: metrics.raw_precision_proxy,
false_episode_rate_clean: metrics.false_ep_rate,
trace: run.trace,
episodes,
}
}
fn collect_healthy_window(observations: &[RfObservation]) -> Vec<f32> {
observations.iter()
.filter(|o| o.is_healthy)
.take(HEALTHY_WINDOW_SIZE)
.map(|o| o.residual_norm)
.collect()
}
struct EvaluationRun {
trace: Vec<ObservationResult>,
episodes: Vec<Episode>,
raw_boundary_count: usize,
false_episodes_clean: usize,
clean_window_obs: usize,
episode_open: bool,
episode_open_k: usize,
}
fn run_evaluation_pass(
engine: &mut DsfbRfEngine<DSA_WINDOW_W, GRAMMAR_K, 32>,
comparators: &mut ScalarComparators,
observations: &[RfObservation],
events: &[RegimeTransitionEvent],
) -> EvaluationRun {
let mut trace: Vec<ObservationResult> = Vec::with_capacity(observations.len());
let mut episodes: Vec<Episode> = Vec::new();
let mut raw_boundary_count = 0usize;
let mut false_episodes_clean = 0usize;
let mut clean_window_obs = 0usize;
let mut episode_open = false;
let mut episode_open_k = 0usize;
for obs in observations.iter().filter(|o| !o.is_healthy) {
let ctx = PlatformContext::with_snr(obs.snr_db);
let result = engine.observe(obs.residual_norm, ctx);
let k = obs.k;
let (thr, _, _, _) = comparators.update(obs.residual_norm);
if thr { raw_boundary_count += 1; }
let is_active = matches!(result.policy,
PolicyDecision::Review | PolicyDecision::Escalate);
if is_active && !episode_open {
episode_open = true;
episode_open_k = k;
} else if !is_active && episode_open {
episode_open = false;
episodes.push(Episode {
open_k: episode_open_k,
close_k: Some(k),
is_precursor: false,
});
}
if is_clean_window(k, events, WPRED) {
clean_window_obs += 1;
if is_active { false_episodes_clean += 1; }
}
trace.push(result);
}
EvaluationRun {
trace, episodes, raw_boundary_count, false_episodes_clean,
clean_window_obs, episode_open, episode_open_k,
}
}
fn finalise_episodes(
mut episodes: Vec<Episode>,
episode_open_k: usize,
episode_open: bool,
n_obs: usize,
events: &[RegimeTransitionEvent],
) -> Vec<Episode> {
if episode_open {
episodes.push(Episode {
open_k: episode_open_k,
close_k: None,
is_precursor: false,
});
}
for ep in &mut episodes {
let close = ep.close_k.unwrap_or(n_obs);
ep.is_precursor = events.iter().any(|ev| {
close <= ev.k && ev.k <= close + WPRED
});
}
episodes
}
struct EvaluationMetrics {
episode_precision: f32,
covered: usize,
compression: f32,
precision_gain: f32,
raw_precision_proxy: f32,
false_ep_rate: f32,
}
fn compute_evaluation_metrics(
episodes: &[Episode],
raw_boundary_count: usize,
false_episodes_clean: usize,
clean_window_obs: usize,
events: &[RegimeTransitionEvent],
n_obs: usize,
) -> EvaluationMetrics {
let covered: usize = events.iter().filter(|ev| {
episodes.iter().any(|ep| {
let close = ep.close_k.unwrap_or(n_obs);
close <= ev.k && ev.k <= close + WPRED
})
}).count();
let n_eps = episodes.len();
let n_precursor = episodes.iter().filter(|e| e.is_precursor).count();
let episode_precision = if n_eps > 0 { n_precursor as f32 / n_eps as f32 } else { 0.0 };
let raw_precision_proxy = if raw_boundary_count > 0 {
events.len() as f32 / raw_boundary_count as f32
} else { 0.0 };
let compression = if n_eps > 0 {
raw_boundary_count as f32 / n_eps as f32
} else { raw_boundary_count as f32 };
let precision_gain = if raw_precision_proxy > 0.0 {
episode_precision / raw_precision_proxy
} else { 0.0 };
let false_ep_rate = if clean_window_obs > 0 {
false_episodes_clean as f32 / clean_window_obs as f32
} else { 0.0 };
EvaluationMetrics {
episode_precision, covered, compression, precision_gain, raw_precision_proxy, false_ep_rate,
}
}
fn is_clean_window(k: usize, events: &[RegimeTransitionEvent], wpred: usize) -> bool {
!events.iter().any(|ev| {
let lo = ev.k.saturating_sub(wpred);
let hi = ev.k + wpred;
k >= lo && k <= hi
})
}
pub fn synthetic_radioml_stream(
n_obs: usize,
drift_events_at: &[usize],
base_snr_db: f32,
) -> (Vec<RfObservation>, Vec<RegimeTransitionEvent>) {
let mut obs = Vec::with_capacity(n_obs);
let mut events = Vec::new();
for k in 0..HEALTHY_WINDOW_SIZE.min(n_obs) {
obs.push(RfObservation {
k,
residual_norm: 0.02 + (k as f32 * 0.0001),
snr_db: base_snr_db,
is_healthy: true,
});
}
let mut norm = 0.025_f32;
let drift_set: std::collections::HashSet<usize> =
drift_events_at.iter().copied().take(drift_events_at.len()).collect();
for k in HEALTHY_WINDOW_SIZE..n_obs {
let near_event = drift_events_at.iter().any(|&ek| {
k >= ek.saturating_sub(20) && k <= ek + 5
});
if near_event {
norm = (norm + 0.006).min(0.35);
} else {
norm = (norm * 0.97).max(0.018);
}
let snr = if near_event { base_snr_db - 5.0 } else { base_snr_db };
let is_transition = drift_set.contains(&k);
obs.push(RfObservation {
k,
residual_norm: norm,
snr_db: snr,
is_healthy: false,
});
if is_transition {
events.push(RegimeTransitionEvent { k, label: "SNR_regime_transition" });
}
}
(obs, events)
}
#[cfg(test)]
mod tests {
use super::*;
use std::println;
use std::vec;
use std::vec::Vec;
#[test]
fn synthetic_pipeline_completes_without_panic() {
let drift_at = vec![150, 250, 350, 450, 550, 650, 750, 850, 950, 1050];
let (obs, events) = synthetic_radioml_stream(1200, &drift_at, 15.0);
let result = run_stage_iii("synthetic_test", &obs, &events);
assert_eq!(result.recall_denominator, drift_at.len());
assert!(result.episode_precision >= 0.0 && result.episode_precision <= 1.0);
assert!(result.recall() >= 0.0 && result.recall() <= 1.0);
assert!(result.compression_factor >= 0.0);
println!("Episodes: {}, Raw: {}, Precision: {:.2}%, Recall: {}/{}",
result.dsfb_episode_count, result.raw_boundary_count,
result.episode_precision * 100.0,
result.recall_numerator, result.recall_denominator);
}
#[test]
fn healthy_calibration_window_used() {
let drift_at = vec![200, 400];
let (obs, events) = synthetic_radioml_stream(500, &drift_at, 10.0);
let healthy_count = obs.iter().filter(|o| o.is_healthy).count();
assert_eq!(healthy_count, HEALTHY_WINDOW_SIZE);
let result = run_stage_iii("calibration_test", &obs, &events);
assert!(result.dsfb_episode_count < result.raw_boundary_count,
"DSFB must compress vs raw threshold");
}
#[test]
fn sub_threshold_snr_events_missed_gracefully() {
let n = 300;
let mut obs = Vec::new();
for k in 0..HEALTHY_WINDOW_SIZE {
obs.push(RfObservation { k, residual_norm: 0.02, snr_db: 15.0, is_healthy: true });
}
for k in HEALTHY_WINDOW_SIZE..n {
obs.push(RfObservation { k, residual_norm: 0.30, snr_db: -20.0, is_healthy: false });
}
let events = vec![
RegimeTransitionEvent { k: 150, label: "sub_threshold_event" },
];
let result = run_stage_iii("sub_threshold_test", &obs, &events);
assert_eq!(result.recall_denominator, 1);
}
#[test]
fn clean_window_detection() {
let events = vec![
RegimeTransitionEvent { k: 100, label: "ev1" },
RegimeTransitionEvent { k: 200, label: "ev2" },
];
assert!(is_clean_window(50, &events, WPRED));
assert!(!is_clean_window(98, &events, WPRED));
assert!(!is_clean_window(205, &events, WPRED));
}
#[test]
fn scalar_comparators_calibrate_correctly() {
let healthy: Vec<f32> = (0..100).map(|i| 0.03 + i as f32 * 0.0002).collect();
let comp = ScalarComparators::calibrate(&healthy);
assert!(comp.threshold_3sigma > comp.threshold_mean,
"3sigma threshold must exceed mean");
assert!(comp.cusum_h > comp.cusum_kappa,
"CUSUM alarm threshold must exceed allowance");
assert!(comp.ewma_threshold > 0.0);
}
}