#![cfg(feature = "std")]
use std::vec::Vec;
use crate::detector::DetectorCell;
use crate::fixed::Q16;
use crate::motif::MotifClass;
use crate::sign::SignCell;
pub const TEMPORAL_WINDOW: u32 = 5;
pub const MAX_DETECTORS: u32 = MotifClass::COUNT as u32;
pub const MAX_TEMPORAL: u32 = MAX_DETECTORS * TEMPORAL_WINDOW;
#[repr(C)]
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
pub struct ConsensusCell {
pub window_idx: u32,
pub entity_id: u32,
pub detector_count: u32,
pub axis1_residual_q: Q16,
pub axis2_drift_q: Q16,
pub axis3_slew_q: Q16,
pub axis4_temporal_q: Q16,
pub axis7_consensus_q: Q16,
}
#[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 form(
signs: &[SignCell],
detectors: &[DetectorCell],
n_windows: u32,
n_entities: u32,
) -> Vec<ConsensusCell> {
let total = (n_windows as usize) * (n_entities as usize);
debug_assert_eq!(signs.len(), total, "sign grid shape mismatch");
debug_assert_eq!(detectors.len(), total, "detector grid shape mismatch");
let mut out: Vec<ConsensusCell> = Vec::with_capacity(total);
for entity_id in 0..n_entities {
for window_idx in 0..n_windows {
let idx = flat(entity_id, window_idx, n_windows);
let sign = &signs[idx];
let det = &detectors[idx];
let detector_count = det.detector_mask.count_ones();
let mut sum_counts: u32 = 0;
let mut k: u32 = 0;
while k < TEMPORAL_WINDOW {
if k > window_idx {
break;
}
let w = window_idx - k;
let nb_idx = flat(entity_id, w, n_windows);
sum_counts =
sum_counts.saturating_add(detectors[nb_idx].detector_mask.count_ones());
k += 1;
}
let axis4_temporal_q = if MAX_TEMPORAL == 0 {
Q16::ZERO
} else {
Q16::from_raw(((i64::from(sum_counts) << 16) / i64::from(MAX_TEMPORAL)) as i32)
};
let axis7_consensus_q = Q16::from_raw(
((i64::from(detector_count) << 16) / i64::from(MAX_DETECTORS)) as i32,
);
out.push(ConsensusCell {
window_idx,
entity_id,
detector_count,
axis1_residual_q: sign.norm_q,
axis2_drift_q: sign.drift_q,
axis3_slew_q: sign.slew_q.abs(),
axis4_temporal_q,
axis7_consensus_q,
});
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::detector::{evaluate as detector_evaluate, DetectorThresholds};
use crate::fixture::{synthesize, DEFAULT_SEED, N_ENTITIES, N_WINDOWS, WINDOW_SIZE_NS};
use crate::motif::MotifClass;
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<ConsensusCell> {
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 detectors = detector_evaluate(
&residuals,
&signs,
&DetectorThresholds::CANONICAL,
N_WINDOWS,
N_ENTITIES,
);
form(&signs, &detectors, N_WINDOWS, N_ENTITIES)
}
#[test]
fn consensus_grid_shape_matches_inputs() {
let grid = full_pipeline();
assert_eq!(grid.len(), (N_WINDOWS as usize) * (N_ENTITIES as usize));
}
#[test]
fn consensus_is_deterministic() {
let a = full_pipeline();
let b = full_pipeline();
assert_eq!(a, b);
}
#[test]
fn axis7_consensus_q_is_zero_when_no_detectors_fire() {
let grid = full_pipeline();
let idx = WindowFeature::flat_index(0, 5, N_WINDOWS);
let q = grid[idx].axis7_consensus_q.raw();
assert!((0..=65_536).contains(&q));
}
#[test]
fn axis7_consensus_q_is_in_range_everywhere() {
let grid = full_pipeline();
for cell in &grid {
let q = cell.axis7_consensus_q.raw();
assert!((0..=65_536).contains(&q), "axis7 out of range: {q}");
}
}
#[test]
fn axis4_temporal_q_is_in_range_everywhere() {
let grid = full_pipeline();
for cell in &grid {
let q = cell.axis4_temporal_q.raw();
assert!((0..=65_536).contains(&q), "axis4 out of range: {q}");
}
}
#[test]
fn ramp_cells_have_high_axis7_and_axis4() {
let grid = full_pipeline();
let idx = WindowFeature::flat_index(3, 34, N_WINDOWS);
let cell = grid[idx];
assert!(
cell.axis7_consensus_q.raw() > 0x2000,
"axis7 too low: {}",
cell.axis7_consensus_q.raw()
);
assert!(
cell.axis4_temporal_q.raw() > 0x2000,
"axis4 too low: {}",
cell.axis4_temporal_q.raw()
);
}
#[test]
fn detector_count_field_matches_popcount() {
let signs = vec![SignCell::default(); 4];
let detectors = vec![
DetectorCell {
window_idx: 0,
entity_id: 0,
detector_mask: 0,
},
DetectorCell {
window_idx: 1,
entity_id: 0,
detector_mask: MotifClass::ResidualSpike.bit_mask(),
},
DetectorCell {
window_idx: 0,
entity_id: 1,
detector_mask: MotifClass::ResidualSpike.bit_mask()
| MotifClass::DriftRamp.bit_mask(),
},
DetectorCell {
window_idx: 1,
entity_id: 1,
detector_mask: 0xFFFF,
},
];
let grid = form(&signs, &detectors, 2, 2);
assert_eq!(grid[0].detector_count, 0);
assert_eq!(grid[1].detector_count, 1);
assert_eq!(grid[2].detector_count, 2);
assert_eq!(grid[3].detector_count, MotifClass::COUNT as u32);
}
}