#![cfg(feature = "std")]
use std::vec::Vec;
use crate::fixed::Q16;
use crate::residual::ResidualCell;
#[repr(C)]
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
pub struct SignCell {
pub window_idx: u32,
pub entity_id: u32,
pub norm_q: Q16,
pub drift_q: Q16,
pub slew_q: Q16,
}
#[must_use]
pub fn compute(
residuals: &[ResidualCell],
alpha: Q16,
n_windows: u32,
n_entities: u32,
) -> Vec<SignCell> {
debug_assert_eq!(
residuals.len(),
(n_windows as usize) * (n_entities as usize),
"residual grid shape mismatch"
);
let mut out: Vec<SignCell> = Vec::with_capacity(residuals.len());
for entity_id in 0..n_entities {
let mut drift_state = Q16::ZERO;
let mut prev_norm = Q16::ZERO;
for window_idx in 0..n_windows {
let idx = (entity_id * n_windows + window_idx) as usize;
let cell = &residuals[idx];
let norm = cell
.residual_latency_q
.abs()
.sat_add(cell.residual_error_q.abs());
let slew = if window_idx == 0 {
Q16::ZERO
} else {
norm.sat_sub(prev_norm)
};
drift_state = drift_state.lerp(norm, alpha);
out.push(SignCell {
window_idx,
entity_id,
norm_q: norm,
drift_q: drift_state,
slew_q: slew,
});
prev_norm = norm;
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fixture::{synthesize, DEFAULT_SEED, N_ENTITIES, N_WINDOWS, WINDOW_SIZE_NS};
use crate::residual::{compute as residual_compute, Baseline, ResidualCell};
use crate::window::{compute_features, WindowFeature};
const ALPHA: Q16 = Q16::from_raw(0x2000);
fn make_grid(
n_windows: u32,
n_entities: u32,
cells: &[(u32, u32, i32, i32)],
) -> Vec<ResidualCell> {
let mut grid: Vec<ResidualCell> = (0..n_entities)
.flat_map(|e| {
(0..n_windows).map(move |w| ResidualCell {
window_idx: w,
entity_id: e,
residual_latency_q: Q16::ZERO,
residual_error_q: Q16::ZERO,
})
})
.collect();
for &(e, w, lat_raw, err_raw) in cells {
let idx = (e * n_windows + w) as usize;
grid[idx].residual_latency_q = Q16::from_raw(lat_raw);
grid[idx].residual_error_q = Q16::from_raw(err_raw);
}
grid
}
#[test]
fn first_window_has_zero_slew() {
let grid = make_grid(2, 1, &[(0, 0, 5 * 65_536, 0)]);
let signs = compute(&grid, ALPHA, 2, 1);
assert_eq!(signs[0].slew_q, Q16::ZERO);
}
#[test]
fn norm_is_l1_sum_of_residuals() {
let grid = make_grid(1, 1, &[(0, 0, 3 * 65_536, -2 * 65_536)]);
let signs = compute(&grid, ALPHA, 1, 1);
assert_eq!(signs[0].norm_q.raw(), 5 * 65_536);
}
#[test]
fn ewma_drift_converges_toward_constant_input() {
let grid = make_grid(
3,
1,
&[
(0, 0, 100 * 65_536, 0),
(0, 1, 100 * 65_536, 0),
(0, 2, 100 * 65_536, 0),
],
);
let signs = compute(&grid, ALPHA, 3, 1);
assert!(signs[0].drift_q.raw() < signs[1].drift_q.raw());
assert!(signs[1].drift_q.raw() < signs[2].drift_q.raw());
assert_eq!(signs[0].drift_q.raw(), 819_200);
}
#[test]
fn slew_picks_up_step_changes() {
let grid = make_grid(2, 1, &[(0, 0, 65_536, 0), (0, 1, 10 * 65_536, 0)]);
let signs = compute(&grid, ALPHA, 2, 1);
assert_eq!(signs[1].slew_q.raw(), 9 * 65_536);
}
#[test]
fn entity_streams_do_not_bleed_into_each_other() {
let grid = make_grid(2, 2, &[(0, 0, 50 * 65_536, 0), (0, 1, 50 * 65_536, 0)]);
let signs = compute(&grid, ALPHA, 2, 2);
let e0_w1 = &signs[1];
let e1_w1 = &signs[3];
assert!(e0_w1.drift_q.raw() > 0);
assert_eq!(e1_w1.drift_q.raw(), 0);
}
#[test]
fn sign_pipeline_is_deterministic_over_synthesized_fixture() {
let events = synthesize(DEFAULT_SEED);
let features = compute_features(&events, N_WINDOWS, N_ENTITIES, WINDOW_SIZE_NS);
let residuals = residual_compute(&features, &Baseline::CANONICAL);
let a = compute(&residuals, ALPHA, N_WINDOWS, N_ENTITIES);
let b = compute(&residuals, ALPHA, N_WINDOWS, N_ENTITIES);
assert_eq!(a, b);
}
#[test]
fn shock_window_carries_the_highest_positive_slew_for_entity_eleven() {
let events = synthesize(DEFAULT_SEED);
let features = compute_features(&events, N_WINDOWS, N_ENTITIES, WINDOW_SIZE_NS);
let residuals = residual_compute(&features, &Baseline::CANONICAL);
let signs = compute(&residuals, ALPHA, N_WINDOWS, N_ENTITIES);
let mut best = (0u32, i32::MIN);
for window_idx in 0..N_WINDOWS {
let idx = WindowFeature::flat_index(11, window_idx, N_WINDOWS);
let slew = signs[idx].slew_q.raw();
if slew > best.1 {
best = (window_idx, slew);
}
}
assert_eq!(best.0, 90);
}
}