use libm::fabsf;
const MAX_SC: usize = 32;
const PHASE_JUMP_THRESHOLD: f32 = 2.5;
const MIN_AMPLITUDE_VARIANCE: f32 = 0.001;
const MAX_ENERGY_RATIO: f32 = 50.0;
const BASELINE_FRAMES: u32 = 100;
const ANOMALY_COOLDOWN: u16 = 20;
pub struct AnomalyDetector {
prev_phases: [f32; MAX_SC],
baseline_amp: [f32; MAX_SC],
baseline_energy: f32,
baseline_count: u32,
baseline_sum: [f32; MAX_SC],
baseline_energy_sum: f32,
calibrated: bool,
phase_initialized: bool,
cooldown: u16,
anomaly_count: u32,
}
impl AnomalyDetector {
pub const fn new() -> Self {
Self {
prev_phases: [0.0; MAX_SC],
baseline_amp: [0.0; MAX_SC],
baseline_energy: 0.0,
baseline_count: 0,
baseline_sum: [0.0; MAX_SC],
baseline_energy_sum: 0.0,
calibrated: false,
phase_initialized: false,
cooldown: 0,
anomaly_count: 0,
}
}
pub fn process_frame(&mut self, phases: &[f32], amplitudes: &[f32]) -> bool {
let n_sc = phases.len().min(amplitudes.len()).min(MAX_SC);
if self.cooldown > 0 {
self.cooldown -= 1;
}
if !self.calibrated {
let mut energy = 0.0f32;
for i in 0..n_sc {
self.baseline_sum[i] += amplitudes[i];
energy += amplitudes[i] * amplitudes[i];
}
self.baseline_energy_sum += energy;
self.baseline_count += 1;
if !self.phase_initialized {
for i in 0..n_sc {
self.prev_phases[i] = phases[i];
}
self.phase_initialized = true;
}
if self.baseline_count >= BASELINE_FRAMES {
let n = self.baseline_count as f32;
for i in 0..n_sc {
self.baseline_amp[i] = self.baseline_sum[i] / n;
}
self.baseline_energy = self.baseline_energy_sum / n;
self.calibrated = true;
}
return false;
}
let mut anomaly = false;
if self.phase_initialized {
let mut jump_count = 0u32;
for i in 0..n_sc {
let delta = fabsf(phases[i] - self.prev_phases[i]);
if delta > PHASE_JUMP_THRESHOLD {
jump_count += 1;
}
}
if n_sc > 0 && jump_count > (n_sc as u32) / 2 {
anomaly = true;
}
}
if n_sc >= 4 {
let mut amp_mean = 0.0f32;
for i in 0..n_sc {
amp_mean += amplitudes[i];
}
amp_mean /= n_sc as f32;
let mut amp_var = 0.0f32;
for i in 0..n_sc {
let d = amplitudes[i] - amp_mean;
amp_var += d * d;
}
amp_var /= n_sc as f32;
if amp_var < MIN_AMPLITUDE_VARIANCE && amp_mean > 0.01 {
anomaly = true;
}
}
{
let mut current_energy = 0.0f32;
for i in 0..n_sc {
current_energy += amplitudes[i] * amplitudes[i];
}
if self.baseline_energy > 0.0 {
let ratio = current_energy / self.baseline_energy;
if ratio > MAX_ENERGY_RATIO {
anomaly = true;
}
}
}
for i in 0..n_sc {
self.prev_phases[i] = phases[i];
}
self.phase_initialized = true;
if anomaly && self.cooldown == 0 {
self.anomaly_count += 1;
self.cooldown = ANOMALY_COOLDOWN;
true
} else {
false
}
}
pub fn total_anomalies(&self) -> u32 {
self.anomaly_count
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_anomaly_detector_init() {
let det = AnomalyDetector::new();
assert!(!det.calibrated);
assert!(!det.phase_initialized);
assert_eq!(det.total_anomalies(), 0);
}
#[test]
fn test_calibration_phase() {
let mut det = AnomalyDetector::new();
let phases = [0.0f32; 16];
let amps = [1.0f32; 16];
for _ in 0..BASELINE_FRAMES {
assert!(!det.process_frame(&phases, &s));
}
assert!(det.calibrated);
}
#[test]
fn test_normal_signal_no_anomaly() {
let mut det = AnomalyDetector::new();
let phases = [0.0f32; 16];
let mut amps = [0.0f32; 16];
for i in 0..16 {
amps[i] = 1.0 + (i as f32) * 0.1;
}
for _ in 0..BASELINE_FRAMES {
det.process_frame(&phases, &s);
}
for _ in 0..50 {
assert!(!det.process_frame(&phases, &s));
}
assert_eq!(det.total_anomalies(), 0);
}
#[test]
fn test_phase_jump_detection() {
let mut det = AnomalyDetector::new();
let phases = [0.0f32; 16];
let amps = [1.0f32; 16];
for _ in 0..BASELINE_FRAMES {
det.process_frame(&phases, &s);
}
let jumped_phases = [5.0f32; 16];
let detected = det.process_frame(&jumped_phases, &s);
assert!(detected, "phase jump should trigger anomaly detection");
assert_eq!(det.total_anomalies(), 1);
}
#[test]
fn test_amplitude_flatline_detection() {
let mut det = AnomalyDetector::new();
let mut amps = [0.0f32; 16];
for i in 0..16 {
amps[i] = 0.5 + (i as f32) * 0.1;
}
let phases = [0.0f32; 16];
for _ in 0..BASELINE_FRAMES {
det.process_frame(&phases, &s);
}
let flat_amps = [1.0f32; 16];
let detected = det.process_frame(&phases, &flat_amps);
assert!(detected, "flatline amplitude should trigger anomaly detection");
}
#[test]
fn test_energy_spike_detection() {
let mut det = AnomalyDetector::new();
let phases = [0.0f32; 16];
let amps = [1.0f32; 16];
for _ in 0..BASELINE_FRAMES {
det.process_frame(&phases, &s);
}
let spike_amps = [100.0f32; 16];
let detected = det.process_frame(&phases, &spike_amps);
assert!(detected, "energy spike should trigger anomaly detection");
}
#[test]
fn test_cooldown_prevents_flood() {
let mut det = AnomalyDetector::new();
let phases = [0.0f32; 16];
let amps = [1.0f32; 16];
for _ in 0..BASELINE_FRAMES {
det.process_frame(&phases, &s);
}
let spike_amps = [100.0f32; 16];
assert!(det.process_frame(&phases, &spike_amps));
for _ in 0..10 {
assert!(!det.process_frame(&phases, &spike_amps));
}
assert_eq!(det.total_anomalies(), 1, "cooldown should prevent counting duplicates");
}
}