#![cfg(feature = "std")]
use std::vec::Vec;
use crate::fixed::Q16;
use crate::motif::MotifClass;
use crate::residual::ResidualCell;
use crate::sign::SignCell;
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub struct DetectorThresholds {
pub spike_q16_raw: i32,
pub sustain_q16_raw: i32,
pub slew_shock_q16_raw: i32,
pub plateau_min_q16_raw: i32,
pub plateau_slew_max_q16_raw: i32,
pub plateau_windows: u32,
pub oscillation_window: u32,
pub oscillation_alternations: u32,
pub deadband_low_q16_raw: i32,
pub deadband_high_q16_raw: i32,
pub error_burst_q16_raw: i32,
pub coupling_lat_q16_raw: i32,
pub coupling_err_q16_raw: i32,
pub variance_window: u32,
pub variance_threshold_q16_raw: i32,
pub ramp_window: u32,
pub recovery_min_norm_q16_raw: i32,
pub clean_band_q16_raw: i32,
pub confuser_min_q16_raw: i32,
pub fanout_drift_q16_raw: i32,
pub entity_anomaly_factor_q16_raw: i32,
pub history_window: u32,
}
impl DetectorThresholds {
pub const CANONICAL: Self = Self {
spike_q16_raw: 10 * 65_536,
sustain_q16_raw: 5 * 65_536,
slew_shock_q16_raw: 20 * 65_536,
plateau_min_q16_raw: 5 * 65_536,
plateau_slew_max_q16_raw: 65_536, plateau_windows: 3,
oscillation_window: 6,
oscillation_alternations: 3,
deadband_low_q16_raw: 2 * 65_536,
deadband_high_q16_raw: 4 * 65_536,
error_burst_q16_raw: 0x4000, coupling_lat_q16_raw: 5 * 65_536,
coupling_err_q16_raw: 0x1000, variance_window: 5,
variance_threshold_q16_raw: 30 * 65_536,
ramp_window: 4,
recovery_min_norm_q16_raw: 5 * 65_536,
clean_band_q16_raw: 65_536, confuser_min_q16_raw: 10 * 65_536,
fanout_drift_q16_raw: 3 * 65_536,
entity_anomaly_factor_q16_raw: 4 * 65_536,
history_window: 8,
};
}
#[repr(C)]
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
pub struct DetectorCell {
pub window_idx: u32,
pub entity_id: u32,
pub detector_mask: u32,
}
pub type DetectorMask2048 = [u64; 32];
#[repr(C)]
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
pub struct DetectorCellWide {
pub window_idx: u32,
pub entity_id: u32,
pub detector_mask: DetectorMask2048,
}
impl DetectorCellWide {
#[must_use]
pub const fn fired_by_id(&self, detector_id: u32) -> bool {
if detector_id >= 2048 {
return false;
}
let word = (detector_id / 64) as usize;
let bit = detector_id % 64;
(self.detector_mask[word] & (1u64 << bit)) != 0
}
#[must_use]
pub fn popcount(&self) -> u32 {
let mut total = 0u32;
let mut i = 0;
while i < self.detector_mask.len() {
total += self.detector_mask[i].count_ones();
i += 1;
}
total
}
pub fn set_bit(&mut self, detector_id: u32) {
if detector_id >= 2048 {
return;
}
let word = (detector_id / 64) as usize;
let bit = detector_id % 64;
self.detector_mask[word] |= 1u64 << bit;
}
}
pub const D64_VARIANT_COUNT: u32 = 4;
pub const D64_TOTAL_DETECTORS: u32 = 16 * D64_VARIANT_COUNT;
pub const D64_VARIANT_SCALES_Q16: [i32; 4] = [
1 << 16, 1 << 15, (1 << 16) + (1 << 15), (1 << 16) - (1 << 14), ];
pub const D128_VARIANT_COUNT: u32 = 8;
pub const D128_TOTAL_DETECTORS: u32 = 16 * D128_VARIANT_COUNT;
pub const D128_VARIANT_SCALES_Q16: [i32; 8] = [
1 << 16, 1 << 15, (1 << 16) + (1 << 15), (1 << 16) - (1 << 14), 1 << 14, (1 << 16) + (1 << 14), (1 << 17), (1 << 17) + (1 << 16), ];
pub const D205_VARIANT_COUNT: u32 = 13;
pub const D205_ACTIVE_BITS: u32 = 205;
pub const D205_TOTAL_SLOTS: u32 = 16 * D205_VARIANT_COUNT;
pub const D205_VARIANT_SCALES_Q16: [i32; 13] = [
1 << 16, 1 << 15, (1 << 16) + (1 << 15), (1 << 16) - (1 << 14), 1 << 14, (1 << 16) + (1 << 14), 1 << 17, (1 << 17) + (1 << 16), (1 << 14) + (1 << 13), (1 << 15) + (1 << 13), (1 << 16) - (1 << 13), (1 << 16) + (1 << 13), (1 << 17) - (1 << 14), ];
#[must_use]
pub fn scale_q16_threshold(value_raw: i32, scale_q16: i32) -> i32 {
let result = (i64::from(value_raw) * i64::from(scale_q16)) >> 16;
if result > i64::from(i32::MAX) {
i32::MAX
} else if result < i64::from(i32::MIN) {
i32::MIN
} else {
result as i32
}
}
#[must_use]
pub fn scale_window(window: u32, scale_q16: i32) -> u32 {
let scaled = (i64::from(window) * i64::from(scale_q16) + (1 << 15)) >> 16;
if scaled < 1 {
1
} else if scaled > i64::from(u32::MAX) {
u32::MAX
} else {
scaled as u32
}
}
#[must_use]
pub fn scale_thresholds(t: &DetectorThresholds, scale_q16: i32) -> DetectorThresholds {
DetectorThresholds {
spike_q16_raw: scale_q16_threshold(t.spike_q16_raw, scale_q16),
sustain_q16_raw: scale_q16_threshold(t.sustain_q16_raw, scale_q16),
slew_shock_q16_raw: scale_q16_threshold(t.slew_shock_q16_raw, scale_q16),
plateau_min_q16_raw: scale_q16_threshold(t.plateau_min_q16_raw, scale_q16),
plateau_slew_max_q16_raw: scale_q16_threshold(t.plateau_slew_max_q16_raw, scale_q16),
plateau_windows: scale_window(t.plateau_windows, scale_q16),
oscillation_window: scale_window(t.oscillation_window, scale_q16),
oscillation_alternations: t.oscillation_alternations,
deadband_low_q16_raw: scale_q16_threshold(t.deadband_low_q16_raw, scale_q16),
deadband_high_q16_raw: scale_q16_threshold(t.deadband_high_q16_raw, scale_q16),
error_burst_q16_raw: scale_q16_threshold(t.error_burst_q16_raw, scale_q16),
coupling_lat_q16_raw: scale_q16_threshold(t.coupling_lat_q16_raw, scale_q16),
coupling_err_q16_raw: scale_q16_threshold(t.coupling_err_q16_raw, scale_q16),
variance_window: scale_window(t.variance_window, scale_q16),
variance_threshold_q16_raw: scale_q16_threshold(t.variance_threshold_q16_raw, scale_q16),
ramp_window: scale_window(t.ramp_window, scale_q16),
recovery_min_norm_q16_raw: scale_q16_threshold(t.recovery_min_norm_q16_raw, scale_q16),
clean_band_q16_raw: scale_q16_threshold(t.clean_band_q16_raw, scale_q16),
confuser_min_q16_raw: scale_q16_threshold(t.confuser_min_q16_raw, scale_q16),
fanout_drift_q16_raw: scale_q16_threshold(t.fanout_drift_q16_raw, scale_q16),
entity_anomaly_factor_q16_raw: scale_q16_threshold(
t.entity_anomaly_factor_q16_raw,
scale_q16,
),
history_window: t.history_window,
}
}
impl DetectorCell {
#[must_use]
pub const fn fired(&self, class: MotifClass) -> bool {
(self.detector_mask & class.bit_mask()) != 0
}
#[must_use]
pub const fn count(&self) -> u32 {
self.detector_mask.count_ones()
}
}
#[inline]
const fn flat(entity_id: u32, window_idx: u32, n_windows: u32) -> usize {
(entity_id * n_windows + window_idx) as usize
}
#[must_use]
pub fn evaluate(
residuals: &[ResidualCell],
signs: &[SignCell],
thresholds: &DetectorThresholds,
n_windows: u32,
n_entities: u32,
) -> Vec<DetectorCell> {
let total = (n_windows as usize) * (n_entities as usize);
debug_assert_eq!(residuals.len(), total, "residual grid shape mismatch");
debug_assert_eq!(signs.len(), total, "sign grid shape mismatch");
let mut out: Vec<DetectorCell> = Vec::with_capacity(total);
for entity_id in 0..n_entities {
for window_idx in 0..n_windows {
let mask = eval_motifs_for_cell(
residuals, signs, thresholds, entity_id, window_idx, n_windows,
);
out.push(DetectorCell {
window_idx,
entity_id,
detector_mask: mask,
});
}
}
out
}
#[must_use]
pub fn eval_motifs_for_cell(
residuals: &[ResidualCell],
signs: &[SignCell],
thresholds: &DetectorThresholds,
entity_id: u32,
window_idx: u32,
n_windows: u32,
) -> u32 {
let idx = flat(entity_id, window_idx, n_windows);
let r = residuals[idx];
let s = signs[idx];
let mut mask = 0u32;
if s.norm_q.raw() > thresholds.spike_q16_raw {
mask |= MotifClass::ResidualSpike.bit_mask();
}
if s.drift_q.raw() > thresholds.sustain_q16_raw {
mask |= MotifClass::SustainedResidualElevation.bit_mask();
}
if drift_ramp_fires(signs, entity_id, window_idx, n_windows, thresholds) {
mask |= MotifClass::DriftRamp.bit_mask();
}
if s.slew_q.abs().raw() > thresholds.slew_shock_q16_raw {
mask |= MotifClass::SlewShock.bit_mask();
}
if plateau_fires(signs, entity_id, window_idx, n_windows, thresholds) {
mask |= MotifClass::Plateau.bit_mask();
}
if oscillation_fires(signs, entity_id, window_idx, n_windows, thresholds) {
mask |= MotifClass::Oscillation.bit_mask();
}
if deadband_exit_fires(signs, entity_id, window_idx, n_windows, thresholds) {
mask |= MotifClass::DeadbandExit.bit_mask();
}
if r.residual_error_q.raw() > thresholds.error_burst_q16_raw {
mask |= MotifClass::ErrorRateBurst.bit_mask();
}
if r.residual_latency_q.raw() > thresholds.coupling_lat_q16_raw
&& r.residual_error_q.raw() > thresholds.coupling_err_q16_raw
{
mask |= MotifClass::LatencyErrorCoupling.bit_mask();
}
if entity_local_anomaly_fires(&s, thresholds) {
mask |= MotifClass::EntityLocalAnomaly.bit_mask();
}
if (mask & MotifClass::ResidualSpike.bit_mask()) != 0 && r.residual_error_q.raw() > 0 {
mask |= MotifClass::RouteLocalAnomaly.bit_mask();
}
if fanout_precursor_fires(&s, &r, thresholds) {
mask |= MotifClass::FanoutPrecursor.bit_mask();
}
if variance_expansion_fires(signs, entity_id, window_idx, n_windows, thresholds) {
mask |= MotifClass::VarianceExpansion.bit_mask();
}
if recovery_edge_fires(signs, entity_id, window_idx, n_windows, thresholds) {
mask |= MotifClass::RecoveryEdge.bit_mask();
}
if confuser_like_transient_fires(signs, entity_id, window_idx, n_windows, thresholds) {
mask |= MotifClass::ConfuserLikeTransient.bit_mask();
}
let any_non_clean = mask & !MotifClass::CleanWindowStability.bit_mask();
if any_non_clean == 0
&& s.norm_q.abs().raw() <= thresholds.clean_band_q16_raw
&& s.drift_q.abs().raw() <= thresholds.clean_band_q16_raw
&& s.slew_q.abs().raw() <= thresholds.clean_band_q16_raw
{
mask |= MotifClass::CleanWindowStability.bit_mask();
}
mask
}
#[must_use]
pub fn evaluate_wide(
profile: crate::motif::DetectorProfile,
residuals: &[ResidualCell],
signs: &[SignCell],
thresholds: &DetectorThresholds,
n_windows: u32,
n_entities: u32,
) -> Vec<DetectorCellWide> {
use crate::motif::DetectorProfile;
let variants_per_motif: u32 = match profile {
DetectorProfile::D16 => 1,
DetectorProfile::D64 => D64_VARIANT_COUNT,
DetectorProfile::D128 => D128_VARIANT_COUNT,
DetectorProfile::D205 => D205_VARIANT_COUNT,
DetectorProfile::D512 | DetectorProfile::D1024 | DetectorProfile::D2000 => {
panic!(
"DetectorProfile::{} not yet implemented in R.9.d.2; expected D16, D64, D128, or D205",
profile.name()
);
}
};
let total = (n_windows as usize) * (n_entities as usize);
debug_assert_eq!(residuals.len(), total, "residual grid shape mismatch");
debug_assert_eq!(signs.len(), total, "sign grid shape mismatch");
let scales_slice: &[i32] = match profile {
DetectorProfile::D16 | DetectorProfile::D64 => &D64_VARIANT_SCALES_Q16,
DetectorProfile::D128 => &D128_VARIANT_SCALES_Q16,
DetectorProfile::D205 => &D205_VARIANT_SCALES_Q16,
_ => unreachable!("guarded above by the panic on D512+ profiles"),
};
let active_bit_limit: u32 = match profile {
DetectorProfile::D205 => D205_ACTIVE_BITS,
_ => u32::MAX,
};
let scaled: Vec<DetectorThresholds> = (0..variants_per_motif)
.map(|v| scale_thresholds(thresholds, scales_slice[v as usize]))
.collect();
let mut out: Vec<DetectorCellWide> = Vec::with_capacity(total);
for entity_id in 0..n_entities {
for window_idx in 0..n_windows {
let mut wide = DetectorCellWide {
window_idx,
entity_id,
detector_mask: [0u64; 32],
};
for variant in 0..variants_per_motif {
let scaled_thresh = &scaled[variant as usize];
let d16_mask = eval_motifs_for_cell(
residuals,
signs,
scaled_thresh,
entity_id,
window_idx,
n_windows,
);
for motif_id in 0..16u32 {
if (d16_mask & (1u32 << motif_id)) != 0 {
let det_id = motif_id * variants_per_motif + variant;
if det_id < active_bit_limit {
wide.set_bit(det_id);
}
}
}
}
out.push(wide);
}
}
out
}
fn drift_ramp_fires(
signs: &[SignCell],
entity_id: u32,
window_idx: u32,
n_windows: u32,
t: &DetectorThresholds,
) -> bool {
if window_idx + 1 < t.ramp_window {
return false;
}
let mut prev = i32::MIN;
for k in 0..t.ramp_window {
let w = window_idx + 1 - t.ramp_window + k;
let idx = flat(entity_id, w, n_windows);
let d = signs[idx].drift_q.raw();
if d <= prev {
return false;
}
prev = d;
}
true
}
fn plateau_fires(
signs: &[SignCell],
entity_id: u32,
window_idx: u32,
n_windows: u32,
t: &DetectorThresholds,
) -> bool {
if window_idx + 1 < t.plateau_windows {
return false;
}
for k in 0..t.plateau_windows {
let w = window_idx + 1 - t.plateau_windows + k;
let idx = flat(entity_id, w, n_windows);
let c = &signs[idx];
if c.norm_q.raw() < t.plateau_min_q16_raw {
return false;
}
if c.slew_q.abs().raw() > t.plateau_slew_max_q16_raw {
return false;
}
}
true
}
fn oscillation_fires(
signs: &[SignCell],
entity_id: u32,
window_idx: u32,
n_windows: u32,
t: &DetectorThresholds,
) -> bool {
if window_idx + 1 < t.oscillation_window {
return false;
}
let mut alternations = 0u32;
let mut last_sign: i32 = 0;
for k in 0..t.oscillation_window {
let w = window_idx + 1 - t.oscillation_window + k;
let idx = flat(entity_id, w, n_windows);
let raw = signs[idx].slew_q.raw();
let sign = match raw.cmp(&0) {
core::cmp::Ordering::Greater => 1,
core::cmp::Ordering::Less => -1,
core::cmp::Ordering::Equal => 0,
};
if sign != 0 && last_sign != 0 && sign != last_sign {
alternations += 1;
}
if sign != 0 {
last_sign = sign;
}
}
alternations >= t.oscillation_alternations
}
fn deadband_exit_fires(
signs: &[SignCell],
entity_id: u32,
window_idx: u32,
n_windows: u32,
t: &DetectorThresholds,
) -> bool {
if window_idx == 0 {
return false;
}
let prev = &signs[flat(entity_id, window_idx - 1, n_windows)];
let cur = &signs[flat(entity_id, window_idx, n_windows)];
prev.norm_q.raw() < t.deadband_low_q16_raw && cur.norm_q.raw() > t.deadband_high_q16_raw
}
fn entity_local_anomaly_fires(s: &SignCell, t: &DetectorThresholds) -> bool {
let factor = i64::from(t.entity_anomaly_factor_q16_raw);
let drift = i64::from(s.drift_q.raw());
let lhs = i64::from(s.norm_q.raw()) << 16; let rhs = factor.saturating_mul(drift);
lhs > rhs && s.drift_q.raw() > 0
}
fn fanout_precursor_fires(s: &SignCell, r: &ResidualCell, t: &DetectorThresholds) -> bool {
s.drift_q.raw() > t.fanout_drift_q16_raw && r.residual_error_q.raw() > 0
}
fn variance_expansion_fires(
signs: &[SignCell],
entity_id: u32,
window_idx: u32,
n_windows: u32,
t: &DetectorThresholds,
) -> bool {
if window_idx + 1 < t.variance_window {
return false;
}
let mut hi = i32::MIN;
let mut lo = i32::MAX;
for k in 0..t.variance_window {
let w = window_idx + 1 - t.variance_window + k;
let idx = flat(entity_id, w, n_windows);
let raw = signs[idx].norm_q.raw();
if raw > hi {
hi = raw;
}
if raw < lo {
lo = raw;
}
}
Q16::from_raw(hi).sat_sub(Q16::from_raw(lo)).raw() > t.variance_threshold_q16_raw
}
fn recovery_edge_fires(
signs: &[SignCell],
entity_id: u32,
window_idx: u32,
n_windows: u32,
t: &DetectorThresholds,
) -> bool {
if window_idx == 0 {
return false;
}
let prev = &signs[flat(entity_id, window_idx - 1, n_windows)];
let cur = &signs[flat(entity_id, window_idx, n_windows)];
cur.drift_q.raw() < prev.drift_q.raw() && cur.norm_q.raw() > t.recovery_min_norm_q16_raw
}
fn confuser_like_transient_fires(
signs: &[SignCell],
entity_id: u32,
window_idx: u32,
n_windows: u32,
t: &DetectorThresholds,
) -> bool {
if window_idx == 0 {
return false;
}
let prev = &signs[flat(entity_id, window_idx - 1, n_windows)];
let cur = &signs[flat(entity_id, window_idx, n_windows)];
cur.norm_q.raw() > t.confuser_min_q16_raw && prev.norm_q.abs().raw() <= t.clean_band_q16_raw
}
#[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};
use crate::sign::compute as sign_compute;
use crate::window::{compute_features, WindowFeature};
const ALPHA: Q16 = Q16::from_raw(0x2000);
fn full_pipeline() -> Vec<DetectorCell> {
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 = sign_compute(&residuals, ALPHA, N_WINDOWS, N_ENTITIES);
evaluate(
&residuals,
&signs,
&DetectorThresholds::CANONICAL,
N_WINDOWS,
N_ENTITIES,
)
}
#[test]
fn detector_grid_has_expected_shape() {
let grid = full_pipeline();
assert_eq!(grid.len(), (N_WINDOWS as usize) * (N_ENTITIES as usize));
}
#[test]
fn detector_evaluation_is_deterministic() {
let a = full_pipeline();
let b = full_pipeline();
assert_eq!(a, b);
}
#[test]
fn ramp_episode_fires_spike_and_sustain_and_ramp() {
let grid = full_pipeline();
let idx = WindowFeature::flat_index(3, 34, N_WINDOWS);
let cell = grid[idx];
assert!(cell.fired(MotifClass::ResidualSpike));
assert!(cell.fired(MotifClass::SustainedResidualElevation));
assert!(cell.fired(MotifClass::DriftRamp));
}
#[test]
fn burst_episode_fires_error_rate_burst_and_coupling() {
let grid = full_pipeline();
let idx = WindowFeature::flat_index(7, 62, N_WINDOWS);
let cell = grid[idx];
assert!(cell.fired(MotifClass::ErrorRateBurst));
}
#[test]
fn shock_episode_fires_slew_shock_and_recovery_edge_in_subsequent_windows() {
let grid = full_pipeline();
let shock_idx = WindowFeature::flat_index(11, 90, N_WINDOWS);
assert!(grid[shock_idx].fired(MotifClass::SlewShock));
let any_recovery = (91..96).any(|w| {
let idx = WindowFeature::flat_index(11, w, N_WINDOWS);
grid[idx].fired(MotifClass::RecoveryEdge)
});
assert!(
any_recovery,
"no recovery edge fired in the post-shock window range"
);
}
#[test]
fn clean_windows_fire_only_clean_stability_bit() {
let grid = full_pipeline();
let idx = WindowFeature::flat_index(0, 5, N_WINDOWS);
let cell = grid[idx];
if cell.fired(MotifClass::CleanWindowStability) {
let non_clean = cell.detector_mask & !MotifClass::CleanWindowStability.bit_mask();
assert_eq!(non_clean, 0);
}
}
#[test]
fn confuser_detector_does_not_fire_on_sustained_ramp() {
let grid = full_pipeline();
let idx = WindowFeature::flat_index(3, 34, N_WINDOWS);
assert!(!grid[idx].fired(MotifClass::ConfuserLikeTransient));
}
use crate::motif::DetectorProfile;
fn evaluate_both_d16(
events: &[crate::event::TraceEvent],
) -> (Vec<DetectorCell>, Vec<DetectorCellWide>) {
let features = compute_features(events, N_WINDOWS, N_ENTITIES, WINDOW_SIZE_NS);
let residuals = residual_compute(&features, &Baseline::CANONICAL);
let signs = sign_compute(&residuals, ALPHA, N_WINDOWS, N_ENTITIES);
let legacy = evaluate(
&residuals,
&signs,
&DetectorThresholds::CANONICAL,
N_WINDOWS,
N_ENTITIES,
);
let wide = evaluate_wide(
DetectorProfile::D16,
&residuals,
&signs,
&DetectorThresholds::CANONICAL,
N_WINDOWS,
N_ENTITIES,
);
(legacy, wide)
}
#[test]
fn d16_legacy_and_wide_masks_match_bit_for_bit() {
let events = synthesize(DEFAULT_SEED);
let (legacy, wide) = evaluate_both_d16(&events);
assert_eq!(legacy.len(), wide.len());
for (i, (a, b)) in legacy.iter().zip(wide.iter()).enumerate() {
assert_eq!(a.window_idx, b.window_idx, "cell {i} window_idx mismatch");
assert_eq!(a.entity_id, b.entity_id, "cell {i} entity_id mismatch");
assert_eq!(
u64::from(a.detector_mask),
b.detector_mask[0],
"cell {i} mask divergence: legacy={:08x} wide[0]={:016x}",
a.detector_mask,
b.detector_mask[0]
);
for (w, &word) in b.detector_mask.iter().enumerate().skip(1) {
assert_eq!(word, 0, "cell {i} word {w} non-zero in D16 wide mask");
}
}
}
#[test]
fn d64_v0_bits_match_d16_bits() {
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 = sign_compute(&residuals, ALPHA, N_WINDOWS, N_ENTITIES);
let d16 = evaluate(
&residuals,
&signs,
&DetectorThresholds::CANONICAL,
N_WINDOWS,
N_ENTITIES,
);
let d64 = evaluate_wide(
DetectorProfile::D64,
&residuals,
&signs,
&DetectorThresholds::CANONICAL,
N_WINDOWS,
N_ENTITIES,
);
assert_eq!(d16.len(), d64.len());
for (i, (cell16, cell64)) in d16.iter().zip(d64.iter()).enumerate() {
for motif_id in 0..16u32 {
let d16_bit = (cell16.detector_mask & (1u32 << motif_id)) != 0;
let d64_det_id = motif_id * D64_VARIANT_COUNT;
let d64_bit = cell64.fired_by_id(d64_det_id);
assert_eq!(
d16_bit, d64_bit,
"cell {i} motif_id {motif_id}: D16.bit_{motif_id} != D64.bit_{d64_det_id}"
);
}
}
}
#[test]
fn d64_evaluation_is_deterministic_across_runs() {
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 = sign_compute(&residuals, ALPHA, N_WINDOWS, N_ENTITIES);
let a = evaluate_wide(
DetectorProfile::D64,
&residuals,
&signs,
&DetectorThresholds::CANONICAL,
N_WINDOWS,
N_ENTITIES,
);
let b = evaluate_wide(
DetectorProfile::D64,
&residuals,
&signs,
&DetectorThresholds::CANONICAL,
N_WINDOWS,
N_ENTITIES,
);
assert_eq!(a.len(), b.len());
for (i, (ca, cb)) in a.iter().zip(b.iter()).enumerate() {
assert_eq!(ca, cb, "D64 cell {i} differs between runs");
}
}
#[test]
fn d64_total_firings_strictly_exceed_d16() {
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 = sign_compute(&residuals, ALPHA, N_WINDOWS, N_ENTITIES);
let d16 = evaluate(
&residuals,
&signs,
&DetectorThresholds::CANONICAL,
N_WINDOWS,
N_ENTITIES,
);
let d64 = evaluate_wide(
DetectorProfile::D64,
&residuals,
&signs,
&DetectorThresholds::CANONICAL,
N_WINDOWS,
N_ENTITIES,
);
let d16_total: u64 = d16
.iter()
.map(|c| u64::from(c.detector_mask.count_ones()))
.sum();
let d64_total: u64 = d64.iter().map(|c| u64::from(c.popcount())).sum();
assert!(
d64_total >= d16_total,
"D64 total firings ({d64_total}) must be >= D16 total firings ({d16_total}) \
because V0 ≡ canonical and V1..V3 add or repeat firings"
);
}
#[test]
fn scale_threshold_identity_at_unit_scale() {
let canon = DetectorThresholds::CANONICAL;
let scaled = scale_thresholds(&canon, 1 << 16);
assert_eq!(scaled, canon, "scale_thresholds(_, 1.0) must be identity");
}
#[test]
fn scale_window_clamps_below_one() {
assert_eq!(scale_window(8, 0), 1, "scale × 0 must clamp to 1");
assert_eq!(scale_window(8, 1), 1, "extreme small scale still clamps");
assert_eq!(scale_window(8, 1 << 16), 8, "scale × 1.0 is identity");
assert_eq!(scale_window(8, 1 << 17), 16, "scale × 2.0 doubles");
}
#[test]
fn d64_variant_scales_v0_is_unity() {
assert_eq!(
D64_VARIANT_SCALES_Q16[0],
1 << 16,
"D64_VARIANT_SCALES_Q16[V0] must equal 1.0 in Q16.16"
);
}
}