#![cfg(feature = "std")]
use std::vec::Vec;
use crate::fixed::Q16;
use crate::window::WindowFeature;
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub struct Baseline {
pub latency_us: u32,
pub error_rate_q16_raw: i32,
}
impl Baseline {
pub const CANONICAL: Self = Self {
latency_us: 1_000,
error_rate_q16_raw: 0,
};
}
#[repr(C)]
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
pub struct ResidualCell {
pub window_idx: u32,
pub entity_id: u32,
pub residual_latency_q: Q16,
pub residual_error_q: Q16,
}
#[must_use]
pub fn q16_ms_from_us(us: i64) -> Q16 {
let numer = us.saturating_mul(1_i64 << 16);
let q = numer / 1_000;
let clamped = if q > i64::from(i32::MAX) {
i32::MAX
} else if q < i64::from(i32::MIN) {
i32::MIN
} else {
q as i32
};
Q16::from_raw(clamped)
}
#[must_use]
pub fn q16_error_rate(error_count: u32, event_count: u32) -> Q16 {
if event_count == 0 {
return Q16::ZERO;
}
let numer = i64::from(error_count).saturating_mul(1_i64 << 16);
let q = numer / i64::from(event_count);
Q16::from_raw(q as i32)
}
#[must_use]
pub fn compute(features: &[WindowFeature], baseline: &Baseline) -> Vec<ResidualCell> {
let mut out: Vec<ResidualCell> = Vec::with_capacity(features.len());
for cell in features {
let mean_us = cell.mean_latency_us();
let delta_us = i64::from(mean_us) - i64::from(baseline.latency_us);
let residual_latency_q = q16_ms_from_us(delta_us);
let observed_error_q = q16_error_rate(cell.error_count, cell.event_count);
let baseline_error_q = Q16::from_raw(baseline.error_rate_q16_raw);
let residual_error_q = observed_error_q.sat_sub(baseline_error_q);
out.push(ResidualCell {
window_idx: cell.window_idx,
entity_id: cell.entity_id,
residual_latency_q,
residual_error_q,
});
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fixture::{synthesize, DEFAULT_SEED, N_ENTITIES, N_WINDOWS, WINDOW_SIZE_NS};
use crate::window::compute_features;
#[test]
fn baseline_cell_yields_zero_residual() {
let feature = WindowFeature {
window_idx: 0,
entity_id: 0,
event_count: 10,
error_count: 0,
sum_latency_us: 10_000, };
let cells = compute(&[feature], &Baseline::CANONICAL);
assert_eq!(cells.len(), 1);
assert_eq!(cells[0].residual_latency_q, Q16::ZERO);
assert_eq!(cells[0].residual_error_q, Q16::ZERO);
}
#[test]
fn ten_ms_mean_latency_produces_residual_of_nine() {
let feature = WindowFeature {
window_idx: 0,
entity_id: 0,
event_count: 10,
error_count: 0,
sum_latency_us: 100_000,
};
let cells = compute(&[feature], &Baseline::CANONICAL);
assert_eq!(cells[0].residual_latency_q.raw(), 9 * 65_536);
}
#[test]
fn full_error_window_produces_residual_one() {
let feature = WindowFeature {
window_idx: 0,
entity_id: 0,
event_count: 10,
error_count: 10,
sum_latency_us: 10_000,
};
let cells = compute(&[feature], &Baseline::CANONICAL);
assert_eq!(cells[0].residual_error_q, Q16::ONE);
}
#[test]
fn half_error_window_produces_residual_half() {
let feature = WindowFeature {
window_idx: 0,
entity_id: 0,
event_count: 10,
error_count: 5,
sum_latency_us: 10_000,
};
let cells = compute(&[feature], &Baseline::CANONICAL);
assert_eq!(cells[0].residual_error_q.raw(), 0x8000);
}
#[test]
fn empty_cell_produces_negative_baseline_latency_residual() {
let feature = WindowFeature {
window_idx: 0,
entity_id: 0,
event_count: 0,
error_count: 0,
sum_latency_us: 0,
};
let cells = compute(&[feature], &Baseline::CANONICAL);
assert_eq!(cells[0].residual_latency_q.raw(), -65_536);
assert_eq!(cells[0].residual_error_q, Q16::ZERO);
}
#[test]
fn residuals_are_deterministic_over_synthesized_fixture() {
let events = synthesize(DEFAULT_SEED);
let features = compute_features(&events, N_WINDOWS, N_ENTITIES, WINDOW_SIZE_NS);
let a = compute(&features, &Baseline::CANONICAL);
let b = compute(&features, &Baseline::CANONICAL);
assert_eq!(a, b);
}
#[test]
fn ramp_residual_is_strongly_positive_on_entity_three() {
let events = synthesize(DEFAULT_SEED);
let features = compute_features(&events, N_WINDOWS, N_ENTITIES, WINDOW_SIZE_NS);
let cells = compute(&features, &Baseline::CANONICAL);
let idx = WindowFeature::flat_index(3, 35, N_WINDOWS);
assert!(
cells[idx].residual_latency_q.raw() > 30 * 65_536,
"ramp cell residual latency raw={}",
cells[idx].residual_latency_q.raw()
);
}
#[test]
fn burst_residual_lights_up_error_axis_on_entity_seven() {
let events = synthesize(DEFAULT_SEED);
let features = compute_features(&events, N_WINDOWS, N_ENTITIES, WINDOW_SIZE_NS);
let cells = compute(&features, &Baseline::CANONICAL);
let idx = WindowFeature::flat_index(7, 62, N_WINDOWS);
assert!(
cells[idx].residual_error_q.raw() > 0x4000,
"burst cell residual error raw={}",
cells[idx].residual_error_q.raw()
);
}
#[test]
fn shock_residual_spikes_then_recovers_on_entity_eleven() {
let events = synthesize(DEFAULT_SEED);
let features = compute_features(&events, N_WINDOWS, N_ENTITIES, WINDOW_SIZE_NS);
let cells = compute(&features, &Baseline::CANONICAL);
let shock_idx = WindowFeature::flat_index(11, 90, N_WINDOWS);
let recovery_end_idx = WindowFeature::flat_index(11, 95, N_WINDOWS);
assert!(
cells[shock_idx].residual_latency_q.raw()
> cells[recovery_end_idx].residual_latency_q.raw() * 4,
"shock raw={}, recovery raw={}",
cells[shock_idx].residual_latency_q.raw(),
cells[recovery_end_idx].residual_latency_q.raw()
);
}
}