use crate::hash::{format_digest, sha256};
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
#[repr(u8)]
pub enum MotifClass {
ResidualSpike = 0,
SustainedResidualElevation = 1,
DriftRamp = 2,
SlewShock = 3,
Plateau = 4,
Oscillation = 5,
DeadbandExit = 6,
ErrorRateBurst = 7,
LatencyErrorCoupling = 8,
EntityLocalAnomaly = 9,
RouteLocalAnomaly = 10,
FanoutPrecursor = 11,
VarianceExpansion = 12,
RecoveryEdge = 13,
CleanWindowStability = 14,
ConfuserLikeTransient = 15,
}
impl MotifClass {
pub const COUNT: usize = 16;
#[must_use]
pub const fn bit_index(self) -> u32 {
self as u32
}
#[must_use]
pub const fn bit_mask(self) -> u32 {
1u32 << self.bit_index()
}
#[must_use]
pub const fn from_bit_index(bit: u32) -> Option<Self> {
match bit {
0 => Some(Self::ResidualSpike),
1 => Some(Self::SustainedResidualElevation),
2 => Some(Self::DriftRamp),
3 => Some(Self::SlewShock),
4 => Some(Self::Plateau),
5 => Some(Self::Oscillation),
6 => Some(Self::DeadbandExit),
7 => Some(Self::ErrorRateBurst),
8 => Some(Self::LatencyErrorCoupling),
9 => Some(Self::EntityLocalAnomaly),
10 => Some(Self::RouteLocalAnomaly),
11 => Some(Self::FanoutPrecursor),
12 => Some(Self::VarianceExpansion),
13 => Some(Self::RecoveryEdge),
14 => Some(Self::CleanWindowStability),
15 => Some(Self::ConfuserLikeTransient),
_ => None,
}
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::ResidualSpike => "residual_spike",
Self::SustainedResidualElevation => "sustained_residual_elevation",
Self::DriftRamp => "drift_ramp",
Self::SlewShock => "slew_shock",
Self::Plateau => "plateau",
Self::Oscillation => "oscillation",
Self::DeadbandExit => "deadband_exit",
Self::ErrorRateBurst => "error_rate_burst",
Self::LatencyErrorCoupling => "latency_error_coupling",
Self::EntityLocalAnomaly => "entity_local_anomaly",
Self::RouteLocalAnomaly => "route_local_anomaly",
Self::FanoutPrecursor => "fanout_precursor",
Self::VarianceExpansion => "variance_expansion",
Self::RecoveryEdge => "recovery_edge",
Self::CleanWindowStability => "clean_window_stability",
Self::ConfuserLikeTransient => "confuser_like_transient",
}
}
}
pub const MOTIF_CATALOG: [MotifClass; MotifClass::COUNT] = [
MotifClass::ResidualSpike,
MotifClass::SustainedResidualElevation,
MotifClass::DriftRamp,
MotifClass::SlewShock,
MotifClass::Plateau,
MotifClass::Oscillation,
MotifClass::DeadbandExit,
MotifClass::ErrorRateBurst,
MotifClass::LatencyErrorCoupling,
MotifClass::EntityLocalAnomaly,
MotifClass::RouteLocalAnomaly,
MotifClass::FanoutPrecursor,
MotifClass::VarianceExpansion,
MotifClass::RecoveryEdge,
MotifClass::CleanWindowStability,
MotifClass::ConfuserLikeTransient,
];
#[must_use]
pub fn registry_canonical_bytes() -> [u8; 16 * 64] {
let mut buf = [0u8; 16 * 64];
let mut pos = 0usize;
let mut i = 0usize;
while i < MOTIF_CATALOG.len() {
if i > 0 {
buf[pos] = b',';
pos += 1;
}
let name = MOTIF_CATALOG[i].name().as_bytes();
let mut j = 0;
while j < name.len() {
buf[pos] = name[j];
pos += 1;
j += 1;
}
i += 1;
}
let _ = pos; buf
}
#[must_use]
pub fn registry_canonical_len() -> usize {
let mut total = 0usize;
let mut i = 0usize;
while i < MOTIF_CATALOG.len() {
if i > 0 {
total += 1; }
total += MOTIF_CATALOG[i].name().len();
i += 1;
}
total
}
#[must_use]
pub fn registry_hash() -> [u8; 32] {
let bytes = registry_canonical_bytes();
let len = registry_canonical_len();
sha256(&bytes[..len])
}
#[must_use]
pub fn registry_hash_string() -> [u8; 71] {
let digest = registry_hash();
format_digest(&digest)
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
#[repr(u32)]
pub enum DetectorProfile {
D16 = 16,
D64 = 64,
D128 = 128,
D205 = 205,
D512 = 512,
D1024 = 1024,
D2000 = 2000,
}
impl DetectorProfile {
#[must_use]
pub const fn active_detector_count(self) -> u32 {
self as u32
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::D16 => "D16",
Self::D64 => "D64",
Self::D128 => "D128",
Self::D205 => "D205",
Self::D512 => "D512",
Self::D1024 => "D1024",
Self::D2000 => "D2000",
}
}
#[must_use]
pub const fn mask_word_count(self) -> u32 {
32
}
#[must_use]
pub fn registry_hash(self) -> [u8; 32] {
const DOMAIN: &[u8] = b"DSFB-GPU-DEBUG:detector-profile:v1\0";
if matches!(self, Self::D16) {
return registry_hash();
}
let canonical = registry_hash();
let name = self.name().as_bytes();
let count = self.active_detector_count().to_le_bytes();
let mut buf = [0u8; 128];
let mut pos = 0usize;
let mut i = 0;
while i < DOMAIN.len() {
buf[pos] = DOMAIN[i];
pos += 1;
i += 1;
}
let mut j = 0;
while j < 32 {
buf[pos] = canonical[j];
pos += 1;
j += 1;
}
let mut k = 0;
while k < name.len() {
buf[pos] = name[k];
pos += 1;
k += 1;
}
buf[pos] = 0;
pos += 1;
let mut m = 0;
while m < 4 {
buf[pos] = count[m];
pos += 1;
m += 1;
}
sha256(&buf[..pos])
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::vec::Vec;
#[test]
fn motif_class_round_trips_through_bit_index() {
for class in MOTIF_CATALOG {
let bit = class.bit_index();
let recovered = MotifClass::from_bit_index(bit).unwrap();
assert_eq!(class, recovered);
}
}
#[test]
fn bit_masks_are_disjoint_powers_of_two() {
let mut union = 0u32;
for class in MOTIF_CATALOG {
let mask = class.bit_mask();
assert_eq!(mask.count_ones(), 1, "non-power-of-two mask for {class:?}");
assert_eq!(union & mask, 0, "duplicate bit for {class:?}");
union |= mask;
}
assert_eq!(union, 0xFFFF, "expected 16 bits set, got {union:#x}");
}
#[test]
fn catalog_order_matches_bit_indices() {
for (i, &class) in MOTIF_CATALOG.iter().enumerate() {
assert_eq!(class.bit_index() as usize, i);
}
}
#[test]
fn names_are_unique_and_lowercase_snake_case() {
let mut seen: Vec<&'static str> = Vec::new();
for class in MOTIF_CATALOG {
let name = class.name();
assert!(!seen.contains(&name), "duplicate motif name {name}");
assert!(
name.bytes().all(|b| b.is_ascii_lowercase() || b == b'_'),
"motif name {name} contains non-lowercase-snake-case byte"
);
seen.push(name);
}
assert_eq!(seen.len(), MotifClass::COUNT);
}
#[test]
fn registry_canonical_length_matches_computed_bytes() {
let bytes = registry_canonical_bytes();
let len = registry_canonical_len();
let s = core::str::from_utf8(&bytes[..len]).expect("ASCII");
assert!(s.starts_with("residual_spike,"));
assert!(s.ends_with(",confuser_like_transient"));
}
#[test]
fn registry_hash_is_stable_across_calls() {
let a = registry_hash();
let b = registry_hash();
assert_eq!(a, b);
}
#[test]
fn registry_hash_is_what_we_expect() {
let bytes = registry_canonical_bytes();
let len = registry_canonical_len();
let expected = sha256(&bytes[..len]);
assert_eq!(registry_hash(), expected);
assert_ne!(registry_hash(), [0u8; 32]);
assert_ne!(registry_hash(), sha256(b""));
}
#[test]
fn d16_profile_hash_equals_canonical_registry_hash() {
assert_eq!(DetectorProfile::D16.registry_hash(), registry_hash());
}
#[test]
fn wider_profile_hashes_differ_from_d16() {
let d16 = DetectorProfile::D16.registry_hash();
for p in [
DetectorProfile::D64,
DetectorProfile::D128,
DetectorProfile::D205,
DetectorProfile::D512,
DetectorProfile::D1024,
DetectorProfile::D2000,
] {
assert_ne!(
p.registry_hash(),
d16,
"{} hash collides with D16",
p.name()
);
}
}
#[test]
fn profile_hashes_are_pairwise_distinct() {
let profiles = [
DetectorProfile::D16,
DetectorProfile::D64,
DetectorProfile::D128,
DetectorProfile::D205,
DetectorProfile::D512,
DetectorProfile::D1024,
DetectorProfile::D2000,
];
for (i, &a) in profiles.iter().enumerate() {
for &b in profiles.iter().skip(i + 1) {
assert_ne!(
a.registry_hash(),
b.registry_hash(),
"{} and {} share a registry hash",
a.name(),
b.name()
);
}
}
}
#[test]
fn profile_hashes_are_deterministic() {
for p in [
DetectorProfile::D16,
DetectorProfile::D64,
DetectorProfile::D128,
DetectorProfile::D205,
DetectorProfile::D512,
DetectorProfile::D1024,
DetectorProfile::D2000,
] {
assert_eq!(p.registry_hash(), p.registry_hash());
}
}
#[test]
fn profile_active_detector_count_matches_repr_u32() {
assert_eq!(DetectorProfile::D16.active_detector_count(), 16);
assert_eq!(DetectorProfile::D64.active_detector_count(), 64);
assert_eq!(DetectorProfile::D128.active_detector_count(), 128);
assert_eq!(DetectorProfile::D205.active_detector_count(), 205);
assert_eq!(DetectorProfile::D512.active_detector_count(), 512);
assert_eq!(DetectorProfile::D1024.active_detector_count(), 1024);
assert_eq!(DetectorProfile::D2000.active_detector_count(), 2000);
}
#[test]
fn profile_mask_word_count_fits_all_profiles() {
for p in [
DetectorProfile::D16,
DetectorProfile::D64,
DetectorProfile::D128,
DetectorProfile::D205,
DetectorProfile::D512,
DetectorProfile::D1024,
DetectorProfile::D2000,
] {
let bits = u64::from(p.mask_word_count()) * 64;
assert!(
bits >= u64::from(p.active_detector_count()),
"{}: mask width {} < active count {}",
p.name(),
bits,
p.active_detector_count()
);
}
}
}