use heapless::Vec as HVec;
use super::{MicroHNSW, HNSWConfig, MicroVector, DistanceMetric, euclidean_distance_i8};
pub const MAX_PATTERNS: usize = 128;
pub const PATTERN_DIM: usize = 32;
pub const MAX_CLUSTERS: usize = 8;
#[derive(Debug, Clone)]
pub struct AnomalyConfig {
pub threshold: i32,
pub min_samples: usize,
pub adaptive: bool,
pub smoothing: u8,
pub num_clusters: usize,
}
impl Default for AnomalyConfig {
fn default() -> Self {
Self {
threshold: 500, min_samples: 10, adaptive: true, smoothing: 80, num_clusters: 4, }
}
}
#[derive(Debug, Clone)]
pub struct AnomalyResult {
pub is_anomaly: bool,
pub distance: i32,
pub score: u8,
pub nearest_cluster: Option<u8>,
pub confidence: u8,
pub anomaly_type: AnomalyType,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AnomalyType {
Normal,
Point,
Contextual,
Collective,
Drift,
Spike,
Unknown,
}
#[derive(Debug, Clone)]
struct Cluster {
centroid: HVec<i32, PATTERN_DIM>,
count: u32,
sum: HVec<i64, PATTERN_DIM>,
variance: i32,
}
impl Default for Cluster {
fn default() -> Self {
Self {
centroid: HVec::new(),
count: 0,
sum: HVec::new(),
variance: 0,
}
}
}
pub struct AnomalyDetector {
config: AnomalyConfig,
index: MicroHNSW<PATTERN_DIM, MAX_PATTERNS>,
patterns: HVec<HVec<i8, PATTERN_DIM>, MAX_PATTERNS>,
clusters: HVec<Cluster, MAX_CLUSTERS>,
avg_distance: i32,
variance: i32,
sample_count: u32,
anomaly_streak: u16,
recent_window: HVec<i32, 16>,
}
impl AnomalyDetector {
pub fn new(config: AnomalyConfig) -> Self {
let hnsw_config = HNSWConfig {
m: 4,
m_max0: 8,
ef_construction: 16,
ef_search: 8,
metric: DistanceMetric::Euclidean,
binary_mode: false,
};
let mut clusters = HVec::new();
for _ in 0..config.num_clusters {
let _ = clusters.push(Cluster::default());
}
Self {
config,
index: MicroHNSW::new(hnsw_config),
patterns: HVec::new(),
clusters,
avg_distance: 0,
variance: 0,
sample_count: 0,
anomaly_streak: 0,
recent_window: HVec::new(),
}
}
pub fn pattern_count(&self) -> usize {
self.patterns.len()
}
pub fn is_trained(&self) -> bool {
self.sample_count >= self.config.min_samples as u32
}
pub fn memory_bytes(&self) -> usize {
self.index.memory_bytes() +
self.patterns.len() * PATTERN_DIM +
self.clusters.len() * core::mem::size_of::<Cluster>()
}
pub fn learn(&mut self, embedding: &[i8]) -> Result<(), &'static str> {
if self.patterns.len() >= MAX_PATTERNS {
self.patterns.swap_remove(0);
}
let mut pattern = HVec::new();
for &v in embedding.iter().take(PATTERN_DIM) {
pattern.push(v).map_err(|_| "Pattern overflow")?;
}
let vec = MicroVector {
data: pattern.clone(),
id: self.patterns.len() as u32,
};
self.index.insert(&vec)?;
self.update_clusters(&pattern);
self.patterns.push(pattern).map_err(|_| "Pattern storage full")?;
self.sample_count += 1;
Ok(())
}
pub fn detect(&mut self, embedding: &[i8]) -> AnomalyResult {
if !self.is_trained() {
let _ = self.learn(embedding);
return AnomalyResult {
is_anomaly: false,
distance: 0,
score: 0,
nearest_cluster: None,
confidence: 0,
anomaly_type: AnomalyType::Normal,
};
}
let results = self.index.search(embedding, 3);
let distance = if results.is_empty() {
i32::MAX
} else {
results[0].distance
};
let (nearest_cluster, cluster_distance) = self.find_nearest_cluster(embedding);
self.update_statistics(distance);
let threshold = if self.config.adaptive {
self.avg_distance + 2 * self.variance.max(100)
} else {
self.config.threshold
};
let is_anomaly = distance > threshold;
let anomaly_type = self.classify_anomaly(distance, is_anomaly);
if is_anomaly {
self.anomaly_streak = self.anomaly_streak.saturating_add(1);
} else {
self.anomaly_streak = 0;
if distance < threshold / 2 {
let _ = self.learn(embedding);
}
}
let score = if threshold > 0 {
((distance * 100) / threshold).min(100) as u8
} else {
0
};
let confidence = self.sample_count.min(100) as u8;
AnomalyResult {
is_anomaly,
distance,
score,
nearest_cluster: Some(nearest_cluster),
confidence,
anomaly_type,
}
}
fn update_statistics(&mut self, distance: i32) {
self.sample_count += 1;
let n = self.sample_count as i64;
let delta = distance - self.avg_distance;
self.avg_distance += (delta / n as i32);
let delta2 = distance - self.avg_distance;
self.variance = ((self.variance as i64 * (n - 1) + (delta as i64 * delta2 as i64)) / n) as i32;
if self.recent_window.len() >= 16 {
self.recent_window.remove(0);
}
let _ = self.recent_window.push(distance);
}
fn update_clusters(&mut self, pattern: &[i8]) {
let (cluster_idx, _) = self.find_nearest_cluster(pattern);
if let Some(cluster) = self.clusters.get_mut(cluster_idx as usize) {
if cluster.count == 0 {
for &v in pattern.iter().take(PATTERN_DIM) {
let _ = cluster.centroid.push(v as i32);
let _ = cluster.sum.push(v as i64);
}
} else {
for (i, &v) in pattern.iter().take(PATTERN_DIM).enumerate() {
if i < cluster.sum.len() {
cluster.sum[i] += v as i64;
}
if i < cluster.centroid.len() {
cluster.centroid[i] = (cluster.sum[i] / (cluster.count as i64 + 1)) as i32;
}
}
}
cluster.count += 1;
}
}
fn find_nearest_cluster(&self, pattern: &[i8]) -> (u8, i32) {
let mut best_idx = 0u8;
let mut best_dist = i32::MAX;
for (i, cluster) in self.clusters.iter().enumerate() {
if cluster.count == 0 {
continue;
}
let mut dist = 0i32;
for (j, &v) in pattern.iter().take(PATTERN_DIM).enumerate() {
if j < cluster.centroid.len() {
let diff = v as i32 - cluster.centroid[j];
dist += diff * diff;
}
}
if dist < best_dist {
best_dist = dist;
best_idx = i as u8;
}
}
(best_idx, best_dist)
}
fn classify_anomaly(&self, distance: i32, is_anomaly: bool) -> AnomalyType {
if !is_anomaly {
return AnomalyType::Normal;
}
if distance > self.avg_distance * 3 {
return AnomalyType::Spike;
}
let anomalies_in_window = self.recent_window.iter()
.filter(|&&d| d > self.config.threshold)
.count();
if anomalies_in_window >= 3 {
return AnomalyType::Collective;
}
if self.recent_window.len() >= 8 {
let first_half_avg: i32 = self.recent_window[..4].iter().sum::<i32>() / 4;
let second_half_avg: i32 = self.recent_window[4..8].iter().sum::<i32>() / 4;
if second_half_avg > first_half_avg + self.variance {
return AnomalyType::Drift;
}
}
if self.anomaly_streak > 2 {
return AnomalyType::Collective;
}
AnomalyType::Point
}
pub fn current_threshold(&self) -> i32 {
if self.config.adaptive {
self.avg_distance + 2 * self.variance.max(100)
} else {
self.config.threshold
}
}
pub fn reset(&mut self) {
self.patterns.clear();
self.sample_count = 0;
self.avg_distance = 0;
self.variance = 0;
self.anomaly_streak = 0;
self.recent_window.clear();
for cluster in self.clusters.iter_mut() {
cluster.count = 0;
cluster.centroid.clear();
cluster.sum.clear();
}
}
}
impl Default for AnomalyDetector {
fn default() -> Self {
Self::new(AnomalyConfig::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_anomaly_detector() {
let mut detector = AnomalyDetector::default();
for i in 0..20 {
let pattern: HVec<i8, PATTERN_DIM> = (0..PATTERN_DIM).map(|j| ((i + j) % 20) as i8).collect();
detector.learn(&pattern).unwrap();
}
assert!(detector.is_trained());
assert!(detector.pattern_count() >= 10);
}
#[test]
fn test_detect_anomaly() {
let mut detector = AnomalyDetector::default();
for _ in 0..20 {
let pattern = [10i8; PATTERN_DIM];
detector.learn(&pattern).unwrap();
}
let normal = [11i8; PATTERN_DIM];
let result = detector.detect(&normal);
assert!(!result.is_anomaly || result.score < 50);
let anomaly = [100i8; PATTERN_DIM];
let result = detector.detect(&anomaly);
assert!(result.is_anomaly || result.score > 50);
}
}