use crate::context::Context;
use crate::protocol::{Priority, RawData};
#[cfg(not(feature = "std"))]
use alloc::collections::BTreeMap as HashMap;
#[cfg(feature = "std")]
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub struct Classification {
pub priority: Priority,
pub reason: ClassificationReason,
pub delta: f64,
pub confidence: f32,
}
impl Classification {
pub fn new(
priority: Priority,
reason: ClassificationReason,
delta: f64,
confidence: f32,
) -> Self {
Self {
priority,
reason,
delta,
confidence,
}
}
pub fn no_prediction() -> Self {
Self {
priority: Priority::P3Normal,
reason: ClassificationReason::NoPrediction,
delta: 0.0,
confidence: 0.0,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ClassificationReason {
ThresholdExceeded { threshold: f64, actual: f64 },
AnomalyDetected { anomaly_type: AnomalyType },
ScheduledTransmission,
BelowMinimumDelta,
NormalValue,
NoPrediction,
UserRequested,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnomalyType {
ExtremeDeviation,
SignificantDeviation,
Spike,
Drift,
OutOfRange,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CriticalThresholds {
pub min: f64,
pub max: f64,
}
impl CriticalThresholds {
pub fn new(min: f64, max: f64) -> Self {
Self { min, max }
}
}
#[derive(Debug, Clone)]
pub struct ClassifierConfig {
pub anomaly_threshold: f64,
pub critical_anomaly_threshold: f64,
pub minimum_delta_threshold: f64,
pub critical_thresholds: HashMap<u32, CriticalThresholds>,
pub scheduled_interval: u64,
}
impl Default for ClassifierConfig {
fn default() -> Self {
Self {
anomaly_threshold: 0.15,
critical_anomaly_threshold: 0.30,
minimum_delta_threshold: 0.01,
critical_thresholds: HashMap::new(),
scheduled_interval: 0,
}
}
}
#[derive(Debug, Clone)]
pub struct Classifier {
config: ClassifierConfig,
_last_scheduled: HashMap<u32, u64>,
}
impl Classifier {
pub fn new() -> Self {
Self {
config: ClassifierConfig::default(),
_last_scheduled: HashMap::new(),
}
}
pub fn with_config(config: ClassifierConfig) -> Self {
Self {
config,
_last_scheduled: HashMap::new(),
}
}
pub fn set_critical_thresholds(&mut self, source_id: u32, min: f64, max: f64) {
self.config
.critical_thresholds
.insert(source_id, CriticalThresholds::new(min, max));
}
pub fn classify(&self, data: &RawData, context: &Context) -> Classification {
let prediction = match context.predict(data.source_id) {
Some(p) => p,
None => return Classification::no_prediction(),
};
let delta_info = self.calculate_delta(data.value, prediction.value);
if let Some(classification) =
self.check_critical_thresholds(data.value, data.source_id, &delta_info)
{
return classification;
}
if let Some(classification) = self.check_anomaly(&delta_info, prediction.confidence) {
return classification;
}
self.classify_normal(data.timestamp, &delta_info, prediction.confidence)
}
fn calculate_delta(&self, value: f64, predicted: f64) -> DeltaInfo {
let absolute = (value - predicted).abs();
let relative = if predicted.abs() > f64::EPSILON {
absolute / predicted.abs()
} else {
absolute
};
DeltaInfo { absolute, relative }
}
fn check_critical_thresholds(
&self,
value: f64,
source_id: u32,
delta: &DeltaInfo,
) -> Option<Classification> {
let thresholds = self.config.critical_thresholds.get(&source_id)?;
let violated = if value < thresholds.min {
Some(thresholds.min)
} else if value > thresholds.max {
Some(thresholds.max)
} else {
None
}?;
Some(Classification::new(
Priority::P1Critical,
ClassificationReason::ThresholdExceeded {
threshold: violated,
actual: value,
},
delta.relative,
1.0,
))
}
fn check_anomaly(&self, delta: &DeltaInfo, confidence: f32) -> Option<Classification> {
if delta.relative <= self.config.anomaly_threshold {
return None;
}
let (priority, anomaly_type) = if delta.relative > self.config.critical_anomaly_threshold {
(Priority::P1Critical, AnomalyType::ExtremeDeviation)
} else {
(Priority::P2Important, AnomalyType::SignificantDeviation)
};
Some(Classification::new(
priority,
ClassificationReason::AnomalyDetected { anomaly_type },
delta.relative,
confidence,
))
}
fn classify_normal(
&self,
_timestamp: u64,
delta: &DeltaInfo,
confidence: f32,
) -> Classification {
if delta.relative < self.config.minimum_delta_threshold {
return Classification::new(
Priority::P5Disposable,
ClassificationReason::BelowMinimumDelta,
delta.relative,
confidence,
);
}
if self.config.scheduled_interval > 0 {
return Classification::new(
Priority::P3Normal,
ClassificationReason::ScheduledTransmission,
delta.relative,
confidence,
);
}
Classification::new(
Priority::P4Deferred,
ClassificationReason::NormalValue,
delta.relative,
confidence,
)
}
pub fn config(&self) -> &ClassifierConfig {
&self.config
}
pub fn set_config(&mut self, config: ClassifierConfig) {
self.config = config;
}
}
impl Default for Classifier {
fn default() -> Self {
Self::new()
}
}
struct DeltaInfo {
#[allow(dead_code)]
absolute: f64,
relative: f64,
}
#[cfg(test)]
mod tests {
use super::*;
fn make_context_with_prediction(value: f64) -> Context {
let mut ctx = Context::new();
for i in 0..10 {
let data = RawData::new(value + (i as f64 * 0.001), i as u64);
ctx.observe(&data);
}
ctx
}
#[test]
fn test_classify_no_prediction() {
let classifier = Classifier::new();
let context = Context::new(); let data = RawData::new(42.0, 0);
let result = classifier.classify(&data, &context);
assert_eq!(result.priority, Priority::P3Normal);
assert!(matches!(result.reason, ClassificationReason::NoPrediction));
}
#[test]
fn test_classify_normal_value() {
let classifier = Classifier::new();
let context = make_context_with_prediction(20.0);
let data = RawData::new(20.1, 100);
let result = classifier.classify(&data, &context);
assert!(matches!(
result.priority,
Priority::P4Deferred | Priority::P5Disposable
));
}
#[test]
fn test_classify_critical_threshold() {
let mut classifier = Classifier::new();
classifier.set_critical_thresholds(0, 10.0, 30.0);
let context = make_context_with_prediction(20.0);
let data = RawData::new(5.0, 100);
let result = classifier.classify(&data, &context);
assert_eq!(result.priority, Priority::P1Critical);
assert!(matches!(
result.reason,
ClassificationReason::ThresholdExceeded { .. }
));
}
#[test]
fn test_classify_anomaly() {
let classifier = Classifier::new();
let context = make_context_with_prediction(20.0);
let data = RawData::new(30.0, 100);
let result = classifier.classify(&data, &context);
assert!(matches!(
result.priority,
Priority::P1Critical | Priority::P2Important
));
assert!(matches!(
result.reason,
ClassificationReason::AnomalyDetected { .. }
));
}
#[test]
fn test_classify_disposable() {
let classifier = Classifier::new();
let context = make_context_with_prediction(20.0);
let data = RawData::new(20.001, 100);
let result = classifier.classify(&data, &context);
assert_eq!(result.priority, Priority::P5Disposable);
assert!(matches!(
result.reason,
ClassificationReason::BelowMinimumDelta
));
}
#[test]
fn test_priority_should_transmit() {
assert!(Priority::P1Critical.should_transmit());
assert!(Priority::P2Important.should_transmit());
assert!(Priority::P3Normal.should_transmit());
assert!(!Priority::P4Deferred.should_transmit());
assert!(!Priority::P5Disposable.should_transmit());
}
}