use crate::grammar::GrammarState;
use crate::sign::SignTuple;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DsaScore(pub f32);
impl DsaScore {
pub const ZERO: Self = Self(0.0);
#[inline]
pub fn value(&self) -> f32 { self.0 }
#[inline]
pub fn meets_threshold(&self, tau: f32) -> bool { self.0 >= tau }
}
pub struct DsaWindow<const W: usize> {
boundary_flags: [bool; W],
drift_flags: [bool; W],
slew_flags: [bool; W],
ewma_flags: [bool; W],
motif_flags: [bool; W],
head: usize,
count: usize,
ewma_norm: f32,
lambda: f32,
ewma_threshold: f32,
weights: [f32; 5],
delta_s: f32,
}
impl<const W: usize> DsaWindow<W> {
pub const fn new(ewma_threshold: f32) -> Self {
Self {
boundary_flags: [false; W],
drift_flags: [false; W],
slew_flags: [false; W],
ewma_flags: [false; W],
motif_flags: [false; W],
head: 0,
count: 0,
ewma_norm: 0.0,
lambda: 0.20,
ewma_threshold,
weights: [1.0; 5],
delta_s: 0.05,
}
}
pub fn push(
&mut self,
sign: &SignTuple,
grammar: GrammarState,
motif_fired: bool,
) -> DsaScore {
self.ewma_norm = self.lambda * sign.norm + (1.0 - self.lambda) * self.ewma_norm;
let b = grammar.requires_attention();
let d = sign.drift > 0.0;
let s = sign.slew.abs() > self.delta_s;
let e = self.ewma_norm > self.ewma_threshold;
let mu = motif_fired;
let h = self.head;
self.boundary_flags[h] = b;
self.drift_flags[h] = d;
self.slew_flags[h] = s;
self.ewma_flags[h] = e;
self.motif_flags[h] = mu;
self.head = (self.head + 1) % W;
if self.count < W { self.count += 1; }
let n = self.count as f32;
let b_score = self.boundary_flags[..self.count].iter().filter(|&&x| x).count() as f32 / n;
let d_score = self.drift_flags[..self.count].iter().filter(|&&x| x).count() as f32 / n;
let s_score = self.slew_flags[..self.count].iter().filter(|&&x| x).count() as f32 / n;
let e_score = self.ewma_flags[..self.count].iter().filter(|&&x| x).count() as f32 / n;
let mu_score = self.motif_flags[..self.count].iter().filter(|&&x| x).count() as f32 / n;
let score = self.weights[0] * b_score
+ self.weights[1] * d_score
+ self.weights[2] * s_score
+ self.weights[3] * e_score
+ self.weights[4] * mu_score;
DsaScore(score)
}
pub fn reset(&mut self) {
self.boundary_flags = [false; W];
self.drift_flags = [false; W];
self.slew_flags = [false; W];
self.ewma_flags = [false; W];
self.motif_flags = [false; W];
self.head = 0;
self.count = 0;
self.ewma_norm = 0.0;
}
pub fn calibrate_ewma_threshold(&mut self, healthy_norms: &[f32]) {
if healthy_norms.is_empty() {
return;
}
let mut ewma = 0.0_f32;
let mut ewma_vals = [0.0_f32; 256];
let clip = healthy_norms.len().min(256);
for (i, &n) in healthy_norms[..clip].iter().enumerate() {
ewma = self.lambda * n + (1.0 - self.lambda) * ewma;
ewma_vals[i] = ewma;
}
let n = clip as f32;
let mean = ewma_vals[..clip].iter().sum::<f32>() / n;
let var = ewma_vals[..clip].iter()
.map(|&x| (x - mean) * (x - mean))
.sum::<f32>() / n;
self.ewma_threshold = mean + 3.0 * crate::math::sqrt_f32(var);
self.ewma_norm = 0.0; }
}
pub struct CorroborationAccumulator<const K: usize> {
counts: [u8; K],
head: usize,
filled: usize,
persistence_threshold: u8,
}
impl<const K: usize> CorroborationAccumulator<K> {
pub const fn new(persistence_threshold: u8) -> Self {
Self {
counts: [0; K],
head: 0,
filled: 0,
persistence_threshold,
}
}
pub fn push(&mut self, count: u8) -> bool {
self.counts[self.head] = count;
self.head = (self.head + 1) % K;
if self.filled < K { self.filled += 1; }
if self.filled < K {
return false;
}
self.counts.iter().all(|&c| c >= self.persistence_threshold)
}
pub fn reset(&mut self) {
self.counts = [0; K];
self.head = 0;
self.filled = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::grammar::{GrammarState, ReasonCode};
#[test]
fn dsa_zero_for_clean_signal() {
let mut w = DsaWindow::<10>::new(1.0);
let sign = SignTuple::new(0.01, 0.0, 0.0);
for _ in 0..10 {
let score = w.push(&sign, GrammarState::Admissible, false);
assert!(score.value() < 0.5, "clean signal DSA should be low");
}
}
#[test]
fn dsa_rises_for_sustained_boundary() {
let mut w = DsaWindow::<10>::new(0.05);
let sign = SignTuple::new(0.07, 0.005, 0.0);
let grammar = GrammarState::Boundary(ReasonCode::SustainedOutwardDrift);
let mut last = DsaScore::ZERO;
for _ in 0..10 {
last = w.push(&sign, grammar, true);
}
assert!(last.value() > 1.5, "sustained boundary DSA should be elevated: {}", last.value());
}
#[test]
fn corroboration_fires_after_k_consecutive() {
let mut acc = CorroborationAccumulator::<4>::new(1);
assert!(!acc.push(2));
assert!(!acc.push(2));
assert!(!acc.push(2));
assert!(acc.push(2));
}
#[test]
fn corroboration_requires_all_k_above_threshold() {
let mut acc = CorroborationAccumulator::<4>::new(2);
acc.push(3); acc.push(3); acc.push(0); let fires = acc.push(3);
assert!(!fires, "should not fire with one slot below threshold");
}
#[test]
fn dsa_threshold_check() {
let score = DsaScore(2.5);
assert!(score.meets_threshold(2.0));
assert!(!score.meets_threshold(3.0));
}
}