use super::config::*;
use super::optimizer::{Adaptation, AdaptationType, StreamingDataPoint};
use crate::utils::scalar_or;
use scirs2_core::numeric::Float;
use std::collections::{HashMap, VecDeque};
use std::time::{Duration, Instant};
pub struct AnomalyDetector<A: Float + Send + Sync> {
config: AnomalyConfig,
statistical_detectors: HashMap<String, Box<dyn StatisticalAnomalyDetector<A>>>,
ml_detectors: HashMap<String, Box<dyn MLAnomalyDetector<A>>>,
ensemble_detector: EnsembleAnomalyDetector<A>,
threshold_manager: AdaptiveThresholdManager<A>,
anomaly_history: VecDeque<AnomalyEvent<A>>,
false_positive_tracker: FalsePositiveTracker<A>,
response_system: AnomalyResponseSystem<A>,
recent_points: VecDeque<StreamingDataPoint<A>>,
recent_capacity: usize,
context_performance_metrics: Vec<A>,
context_resource_usage: Vec<A>,
context_drift_indicators: Vec<A>,
recent_scores: VecDeque<A>,
points_since_recalibration: usize,
}
#[derive(Debug, Clone)]
pub struct AnomalyEvent<A: Float + Send + Sync> {
pub id: u64,
pub timestamp: Instant,
pub anomaly_type: AnomalyType,
pub severity: AnomalySeverity,
pub confidence: A,
pub data_point: StreamingDataPoint<A>,
pub detector_name: String,
pub anomaly_score: A,
pub context: AnomalyContext<A>,
pub response_actions: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum AnomalyType {
StatisticalOutlier,
PatternChange,
TemporalAnomaly,
SpatialAnomaly,
ContextualAnomaly,
CollectiveAnomaly,
PointAnomaly,
DataQualityAnomaly,
PerformanceAnomaly,
Custom(String),
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum AnomalySeverity {
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone)]
pub struct AnomalyContext<A: Float + Send + Sync> {
pub recent_statistics: DataStatistics<A>,
pub performance_metrics: Vec<A>,
pub resource_usage: Vec<A>,
pub drift_indicators: Vec<A>,
pub time_since_last_anomaly: Duration,
}
#[derive(Debug, Clone)]
pub struct DataStatistics<A: Float + Send + Sync> {
pub means: Vec<A>,
pub std_devs: Vec<A>,
pub min_values: Vec<A>,
pub max_values: Vec<A>,
pub medians: Vec<A>,
pub skewness: Vec<A>,
pub kurtosis: Vec<A>,
}
pub trait StatisticalAnomalyDetector<A: Float + Send + Sync>: Send + Sync {
fn detect_anomaly(
&mut self,
data_point: &StreamingDataPoint<A>,
) -> Result<AnomalyDetectionResult<A>, String>;
fn update(&mut self, data_point: &StreamingDataPoint<A>) -> Result<(), String>;
fn reset(&mut self);
fn name(&self) -> String;
fn get_threshold(&self) -> A;
fn set_threshold(&mut self, threshold: A);
}
pub trait MLAnomalyDetector<A: Float + Send + Sync>: Send + Sync {
fn detect_anomaly(
&mut self,
data_point: &StreamingDataPoint<A>,
) -> Result<AnomalyDetectionResult<A>, String>;
fn train(&mut self, training_data: &[StreamingDataPoint<A>]) -> Result<(), String>;
fn update_incremental(&mut self, data_point: &StreamingDataPoint<A>) -> Result<(), String>;
fn record_outcome(&mut self, predicted_anomaly: bool, was_true_anomaly: bool);
fn get_performance_metrics(&self) -> Result<MLModelMetrics<A>, String>;
fn name(&self) -> String;
}
pub use super::anomaly_scoring::DetectionCounters;
#[derive(Debug, Clone)]
pub struct AnomalyDetectionResult<A: Float + Send + Sync> {
pub is_anomaly: bool,
pub anomaly_score: A,
pub confidence: A,
pub anomaly_type: Option<AnomalyType>,
pub severity: AnomalySeverity,
pub metadata: HashMap<String, A>,
}
#[derive(Debug, Clone)]
pub struct MLModelMetrics<A: Float + Send + Sync> {
pub accuracy: A,
pub precision: A,
pub recall: A,
pub f1_score: A,
pub auc_roc: Option<A>,
pub false_positive_rate: A,
pub training_time: Duration,
pub inference_time: Duration,
}
pub use super::anomaly_ensemble::{
EnsembleAnomalyDetector, EnsembleConfig, EnsembleVotingStrategy,
};
#[derive(Debug, Clone)]
pub struct DetectorPerformance<A: Float + Send + Sync> {
pub recent_accuracy: A,
pub historical_accuracy: A,
pub false_positive_rate: A,
pub false_negative_rate: A,
pub detection_latency: Duration,
pub reliability_score: A,
}
pub struct AdaptiveThresholdManager<A: Float + Send + Sync> {
thresholds: HashMap<String, A>,
threshold_bounds: HashMap<String, (A, A)>,
}
#[derive(Debug, Clone)]
pub enum ThresholdAdaptationStrategy {
Fixed,
PerformanceBased,
QuantileBased { quantile: f64 },
ROCOptimized,
PROptimized,
FPRControlled { target_fpr: f64 },
DistributionAdaptive,
}
#[derive(Debug, Clone)]
pub struct ThresholdPerformanceFeedback<A: Float + Send + Sync> {
pub detector_name: String,
pub threshold: A,
pub true_positives: usize,
pub false_positives: usize,
pub true_negatives: usize,
pub false_negatives: usize,
pub timestamp: Instant,
}
#[derive(Debug, Clone)]
pub struct ThresholdAdaptationParams<A: Float + Send + Sync> {
pub learning_rate: A,
pub momentum: A,
pub min_change: A,
pub max_change: A,
pub adaptation_frequency: usize,
}
pub struct FalsePositiveTracker<A: Float + Send + Sync> {
false_positives: VecDeque<FalsePositiveEvent<A>>,
fp_rate_calculator: FPRateCalculator<A>,
}
#[derive(Debug, Clone)]
pub struct FalsePositiveEvent<A: Float + Send + Sync> {
pub timestamp: Instant,
pub data_point: StreamingDataPoint<A>,
pub detector_name: String,
pub anomaly_score: A,
pub context: AnomalyContext<A>,
}
pub struct FPRateCalculator<A: Float + Send + Sync> {
recent_results: VecDeque<DetectionResult>,
window_size: usize,
current_fp_rate: A,
}
#[derive(Debug, Clone)]
pub struct DetectionResult {
pub timestamp: Instant,
pub anomaly_detected: bool,
pub ground_truth: Option<bool>,
pub detector_name: String,
}
#[derive(Debug, Clone)]
pub struct FalsePositivePatterns<A: Float + Send + Sync> {
pub temporal_patterns: Vec<TemporalPattern>,
pub feature_patterns: HashMap<String, A>,
pub context_patterns: Vec<ContextPattern<A>>,
pub detector_patterns: HashMap<String, Vec<A>>,
}
#[derive(Debug, Clone)]
pub struct TemporalPattern {
pub pattern_type: TemporalPatternType,
pub strength: f64,
pub period: Option<Duration>,
pub confidence: f64,
}
#[derive(Debug, Clone)]
pub enum TemporalPatternType {
Periodic,
TimeSpecific,
Burst,
Trend,
}
#[derive(Debug, Clone)]
pub struct ContextPattern<A: Float + Send + Sync> {
pub context_features: Vec<A>,
pub frequency: usize,
pub reliability: A,
}
#[derive(Debug, Clone)]
pub enum FPMitigationStrategy {
ThresholdAdjustment,
FeatureAdjustment,
EnsembleReweighting,
ContextFiltering,
TemporalFiltering,
ModelRetraining,
}
pub struct AnomalyResponseSystem<A: Float + Send + Sync> {
response_strategies: HashMap<AnomalyType, Vec<ResponseAction>>,
response_executor: ResponseExecutor<A>,
next_response_id: u64,
log_entries: VecDeque<String>,
alert_entries: VecDeque<String>,
quarantined_points: VecDeque<StreamingDataPoint<A>>,
pending_threshold_adjustment: Option<f64>,
monitoring_level: u32,
}
const RECENT_POINT_WINDOW: usize = 512;
const RESPONSE_HISTORY_CAPACITY: usize = 1000;
const QUARANTINE_CAPACITY: usize = 256;
#[derive(Debug, Clone)]
pub enum ResponseAction {
Log,
Alert,
Quarantine,
ModelAdjustment,
IncreaseMonitoring,
TriggerRecovery,
Custom(String),
}
pub struct ResponseExecutor<A: Float + Send + Sync> {
pending_responses: VecDeque<PendingResponse<A>>,
execution_history: VecDeque<ResponseExecution<A>>,
resource_limits: ResponseResourceLimits,
}
#[derive(Debug, Clone)]
pub struct PendingResponse<A: Float + Send + Sync> {
pub id: u64,
pub anomaly_event: AnomalyEvent<A>,
pub action: ResponseAction,
pub priority: ResponsePriority,
pub scheduled_time: Instant,
pub timeout: Duration,
}
#[derive(Debug, Clone)]
pub struct ResponseExecution<A: Float + Send + Sync> {
pub id: u64,
pub response: PendingResponse<A>,
pub start_time: Instant,
pub duration: Duration,
pub success: bool,
pub error_message: Option<String>,
pub resources_consumed: HashMap<String, A>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum ResponsePriority {
Low = 0,
Normal = 1,
High = 2,
Critical = 3,
}
#[derive(Debug, Clone)]
pub struct ResponseResourceLimits {
pub max_concurrent_responses: usize,
pub max_cpu_usage: f64,
pub max_memory_usage: usize,
pub max_execution_time: Duration,
}
#[derive(Debug, Clone)]
pub struct EffectivenessMetrics<A: Float + Send + Sync> {
pub success_rate: A,
pub avg_response_time: Duration,
pub resolution_rate: A,
pub false_alarm_reduction: A,
pub cost_benefit_ratio: A,
}
#[derive(Debug, Clone)]
pub struct ResponseOutcome<A: Float + Send + Sync> {
pub execution: ResponseExecution<A>,
pub outcome: OutcomeMeasurement<A>,
pub follow_up_required: bool,
pub lessons_learned: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct OutcomeMeasurement<A: Float + Send + Sync> {
pub issue_resolved: bool,
pub time_to_resolution: Duration,
pub performance_impact: A,
pub side_effects: Vec<String>,
pub effectiveness_score: A,
}
#[derive(Debug, Clone)]
pub struct TrendAnalysis<A: Float + Send + Sync> {
pub trend_direction: TrendDirection,
pub trend_magnitude: A,
pub trend_confidence: A,
pub trend_stability: A,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TrendDirection {
Improving,
Declining,
Stable,
Oscillating,
}
#[derive(Debug, Clone)]
pub struct EscalationRule<A: Float + Send + Sync> {
pub name: String,
pub conditions: Vec<EscalationCondition<A>>,
pub actions: Vec<EscalationAction>,
pub priority: EscalationPriority,
}
#[derive(Debug, Clone)]
pub struct EscalationCondition<A: Float + Send + Sync> {
pub condition_type: EscalationConditionType,
pub threshold: A,
pub time_window: Duration,
}
#[derive(Debug, Clone)]
pub enum EscalationConditionType {
MultipleAnomalies,
HighSeverity,
ResponseFailure,
PerformanceDegradation,
ResourceExhaustion,
}
#[derive(Debug, Clone)]
pub enum EscalationAction {
NotifyAdmin,
EmergencyProtocol,
SystemShutdown,
ActivateBackup,
IncreaseResources,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum EscalationPriority {
Normal = 0,
Urgent = 1,
Emergency = 2,
}
impl<A: Float + Default + Clone + std::iter::Sum + Send + Sync + 'static> AnomalyDetector<A> {
pub fn new(config: &StreamingConfig) -> Result<Self, String> {
let anomaly_config = config.anomaly_config.clone();
let mut statistical_detectors: HashMap<String, Box<dyn StatisticalAnomalyDetector<A>>> =
HashMap::new();
let mut ml_detectors: HashMap<String, Box<dyn MLAnomalyDetector<A>>> = HashMap::new();
statistical_detectors.insert(
"zscore".to_string(),
Box::new(super::anomaly_statistical::ZScoreDetector::new(
anomaly_config.threshold,
)?),
);
statistical_detectors.insert(
"iqr".to_string(),
Box::new(super::anomaly_statistical::IQRDetector::new(
anomaly_config.threshold,
)?),
);
match anomaly_config.detection_method {
AnomalyDetectionMethod::IsolationForest => {
ml_detectors.insert(
"isolation_forest".to_string(),
Box::new(super::anomaly_ml::IsolationForestDetector::new()?),
);
}
AnomalyDetectionMethod::OneClassSVM => {
ml_detectors.insert(
"one_class_svm".to_string(),
Box::new(super::anomaly_ml::OneClassSvmDetector::new()?),
);
}
AnomalyDetectionMethod::LocalOutlierFactor => {
ml_detectors.insert(
"lof".to_string(),
Box::new(super::anomaly_ml::LofDetector::new()?),
);
}
_ => {
}
}
let ensemble_detector = EnsembleAnomalyDetector::new(EnsembleVotingStrategy::Weighted)?;
let threshold_manager = AdaptiveThresholdManager::new()?;
let false_positive_tracker = FalsePositiveTracker::new();
let response_system = AnomalyResponseSystem::new(&anomaly_config.response_strategy)?;
let recent_capacity = RECENT_POINT_WINDOW;
Ok(Self {
config: anomaly_config,
statistical_detectors,
ml_detectors,
ensemble_detector,
threshold_manager,
anomaly_history: VecDeque::with_capacity(10000),
false_positive_tracker,
response_system,
recent_points: VecDeque::with_capacity(recent_capacity),
recent_capacity,
context_performance_metrics: Vec::new(),
context_resource_usage: Vec::new(),
context_drift_indicators: Vec::new(),
recent_scores: VecDeque::with_capacity(recent_capacity),
points_since_recalibration: 0,
})
}
fn recalibrate_thresholds(&mut self) -> Result<(), String> {
if !self.config.enable_adaptive_threshold {
return Ok(());
}
let window = self.config.window_size.max(1);
if self.points_since_recalibration < window || self.recent_scores.len() < window {
return Ok(());
}
self.points_since_recalibration = 0;
let contamination = self.config.contamination_rate;
if !(contamination > 0.0 && contamination < 1.0) {
return Err(format!(
"AnomalyConfig::contamination_rate must be in (0, 1), got {contamination}"
));
}
let mut scores: Vec<A> = self.recent_scores.iter().copied().collect();
scores.sort_by(crate::utils::total_order);
let rank = ((1.0 - contamination) * scores.len() as f64).floor() as usize;
let index = rank.min(scores.len().saturating_sub(1));
let Some(&target) = scores.get(index) else {
return Ok(());
};
for (name, detector) in &mut self.statistical_detectors {
let bounded = match self.threshold_manager.threshold_bounds.get(name) {
Some(&(low, high)) => target.max(low).min(high),
None => target,
};
detector.set_threshold(bounded);
self.threshold_manager
.thresholds
.insert(name.clone(), bounded);
}
Ok(())
}
#[cfg(test)]
pub(crate) fn calibrated_threshold_names_for_test(&self) -> Vec<A> {
self.threshold_manager
.thresholds
.values()
.copied()
.collect()
}
pub fn calibrated_threshold(&self, detector_name: &str) -> Option<A> {
self.threshold_manager
.thresholds
.get(detector_name)
.copied()
}
pub fn update_context_signals(
&mut self,
performance_metrics: Vec<A>,
resource_usage: Vec<A>,
drift_indicators: Vec<A>,
) {
self.context_performance_metrics = performance_metrics;
self.context_resource_usage = resource_usage;
self.context_drift_indicators = drift_indicators;
}
pub fn record_detection_outcome(&mut self, predicted_anomaly: bool, was_true_anomaly: bool) {
for detector in self.ml_detectors.values_mut() {
detector.record_outcome(predicted_anomaly, was_true_anomaly);
}
self.ensemble_detector.record_outcome(was_true_anomaly);
self.false_positive_tracker
.record_outcome(predicted_anomaly, was_true_anomaly);
if predicted_anomaly && !was_true_anomaly {
if let Some(event) = self.anomaly_history.back().cloned() {
self.false_positive_tracker.record_false_positive(&event);
}
}
}
pub fn confirmed_false_positive_count(&self) -> usize {
self.false_positive_tracker.confirmed_false_positive_count()
}
pub fn set_ensemble_voting_strategy(&mut self, strategy: EnsembleVotingStrategy) {
self.ensemble_detector.set_voting_strategy(strategy);
}
pub fn set_detector_weight(&mut self, detector_name: &str, weight: A) {
self.ensemble_detector
.set_detector_weight(detector_name, weight);
}
pub fn detector_balanced_accuracy(&self, detector_name: &str) -> Option<f64> {
self.ensemble_detector
.detector_balanced_accuracy(detector_name)
}
pub fn response_execution_count(&self) -> usize {
self.response_system.execution_count()
}
pub fn response_log_entry_count(&self) -> usize {
self.response_system.log_entry_count()
}
pub fn response_alert_entry_count(&self) -> usize {
self.response_system.alert_entry_count()
}
pub fn quarantined_point_count(&self) -> usize {
self.response_system.quarantined_count()
}
pub fn monitoring_level(&self) -> u32 {
self.response_system.monitoring_level()
}
pub fn ml_performance_metrics(&self) -> HashMap<String, Result<MLModelMetrics<A>, String>> {
self.ml_detectors
.iter()
.map(|(name, detector)| (name.clone(), detector.get_performance_metrics()))
.collect()
}
pub fn recent_window_len(&self) -> usize {
self.recent_points.len()
}
pub fn build_context_for_test(
&self,
data_point: &StreamingDataPoint<A>,
) -> Result<AnomalyContext<A>, String> {
self.create_anomaly_context(data_point)
}
fn remember_point(&mut self, data_point: &StreamingDataPoint<A>) {
if self.recent_points.len() >= self.recent_capacity {
self.recent_points.pop_front();
}
self.recent_points.push_back(data_point.clone());
}
pub fn detect_anomaly(&mut self, data_point: &StreamingDataPoint<A>) -> Result<bool, String> {
let mut detection_results = HashMap::new();
for (name, detector) in &mut self.statistical_detectors {
let result = detector.detect_anomaly(data_point)?;
detection_results.insert(name.clone(), result);
}
for (name, detector) in &mut self.ml_detectors {
let result = detector.detect_anomaly(data_point)?;
detection_results.insert(name.clone(), result);
}
let ensemble_result = self.ensemble_detector.combine_results(detection_results)?;
self.remember_point(data_point);
if self.recent_scores.len() >= self.recent_capacity {
self.recent_scores.pop_front();
}
self.recent_scores.push_back(ensemble_result.anomaly_score);
self.points_since_recalibration = self.points_since_recalibration.saturating_add(1);
self.recalibrate_thresholds()?;
if ensemble_result.is_anomaly {
let mut anomaly_event = AnomalyEvent {
id: self.generate_event_id(),
timestamp: Instant::now(),
anomaly_type: ensemble_result
.anomaly_type
.as_ref()
.cloned()
.unwrap_or(AnomalyType::StatisticalOutlier),
severity: ensemble_result.severity.clone(),
confidence: ensemble_result.confidence,
data_point: data_point.clone(),
detector_name: "ensemble".to_string(),
anomaly_score: ensemble_result.anomaly_score,
context: self.create_anomaly_context(data_point)?,
response_actions: Vec::new(),
};
anomaly_event.response_actions =
self.response_system.trigger_response(&anomaly_event)?;
let pending_adjustment = self
.response_system
.take_pending_threshold_adjustment()
.filter(|_| self.config.enable_adaptive_threshold);
if let Some(adjustment) = pending_adjustment {
let magnitude = A::from(adjustment).ok_or_else(|| {
format!("threshold adjustment {adjustment} is not representable")
})?;
for detector in self.statistical_detectors.values_mut() {
let updated = detector.get_threshold() + magnitude;
detector.set_threshold(updated);
}
self.ensemble_detector.adjust_sensitivity(magnitude)?;
}
self.record_anomaly(anomaly_event)?;
return Ok(true);
}
for detector in self.statistical_detectors.values_mut() {
detector.update(data_point)?;
}
for detector in self.ml_detectors.values_mut() {
detector.update_incremental(data_point)?;
}
Ok(false)
}
fn generate_event_id(&self) -> u64 {
self.anomaly_history.len() as u64 + 1
}
fn create_anomaly_context(
&self,
data_point: &StreamingDataPoint<A>,
) -> Result<AnomalyContext<A>, String> {
let recent_statistics = self.calculate_recent_statistics(data_point)?;
let time_since_last_anomaly = match self.anomaly_history.back() {
Some(last_anomaly) => last_anomaly.timestamp.elapsed(),
None => self
.recent_points
.front()
.map(|point| point.timestamp.elapsed())
.unwrap_or(Duration::ZERO),
};
Ok(AnomalyContext {
recent_statistics,
performance_metrics: self.context_performance_metrics.clone(),
resource_usage: self.context_resource_usage.clone(),
drift_indicators: self.context_drift_indicators.clone(),
time_since_last_anomaly,
})
}
fn calculate_recent_statistics(
&self,
data_point: &StreamingDataPoint<A>,
) -> Result<DataStatistics<A>, String> {
let feature_count = self
.recent_points
.iter()
.map(|point| point.features.len())
.chain(std::iter::once(data_point.features.len()))
.max()
.unwrap_or(0);
let mut means = Vec::with_capacity(feature_count);
let mut std_devs = Vec::with_capacity(feature_count);
let mut min_values = Vec::with_capacity(feature_count);
let mut max_values = Vec::with_capacity(feature_count);
let mut medians = Vec::with_capacity(feature_count);
let mut skewness = Vec::with_capacity(feature_count);
let mut kurtosis = Vec::with_capacity(feature_count);
for index in 0..feature_count {
let mut column: Vec<A> = self
.recent_points
.iter()
.filter_map(|point| point.features.get(index).copied())
.collect();
if let Some(value) = data_point.features.get(index) {
column.push(*value);
}
if column.is_empty() {
means.push(A::zero());
std_devs.push(A::zero());
min_values.push(A::zero());
max_values.push(A::zero());
medians.push(A::zero());
skewness.push(A::zero());
kurtosis.push(A::zero());
continue;
}
let count = A::from(column.len())
.ok_or_else(|| format!("sample count {} is not representable", column.len()))?;
let mean = column.iter().fold(A::zero(), |acc, &v| acc + v) / count;
let variance = column
.iter()
.fold(A::zero(), |acc, &v| acc + (v - mean) * (v - mean))
/ count;
let std_dev = variance.sqrt();
let minimum = column
.iter()
.copied()
.reduce(|a, b| {
if super::statistics::total_order(&b, &a) == std::cmp::Ordering::Less {
b
} else {
a
}
})
.unwrap_or_else(A::zero);
let maximum = column
.iter()
.copied()
.reduce(|a, b| {
if super::statistics::total_order(&b, &a) == std::cmp::Ordering::Greater {
b
} else {
a
}
})
.unwrap_or_else(A::zero);
let median = super::statistics::median_in_place(&mut column).unwrap_or(mean);
let (skew, kurt) = if std_dev > A::zero() {
let mut third = A::zero();
let mut fourth = A::zero();
for &value in column.iter() {
let z = (value - mean) / std_dev;
let z2 = z * z;
third = third + z2 * z;
fourth = fourth + z2 * z2;
}
let three = A::from(3.0).ok_or_else(|| "3.0 is not representable".to_string())?;
(third / count, fourth / count - three)
} else {
(A::zero(), A::zero())
};
means.push(mean);
std_devs.push(std_dev);
min_values.push(minimum);
max_values.push(maximum);
medians.push(median);
skewness.push(skew);
kurtosis.push(kurt);
}
Ok(DataStatistics {
means,
std_devs,
min_values,
max_values,
medians,
skewness,
kurtosis,
})
}
fn record_anomaly(&mut self, anomaly_event: AnomalyEvent<A>) -> Result<(), String> {
if self.anomaly_history.len() >= 10000 {
self.anomaly_history.pop_front();
}
self.anomaly_history.push_back(anomaly_event);
Ok(())
}
pub fn apply_adaptation(&mut self, adaptation: &Adaptation<A>) -> Result<(), String> {
if adaptation.adaptation_type == AdaptationType::AnomalyDetection {
let threshold_adjustment = adaptation.magnitude;
for detector in self.statistical_detectors.values_mut() {
let current_threshold = detector.get_threshold();
let new_threshold = current_threshold + threshold_adjustment;
detector.set_threshold(new_threshold);
}
self.ensemble_detector
.adjust_sensitivity(threshold_adjustment)?;
}
Ok(())
}
pub fn get_recent_anomalies(&self, count: usize) -> Vec<&AnomalyEvent<A>> {
self.anomaly_history.iter().rev().take(count).collect()
}
pub fn get_diagnostics(&self) -> AnomalyDiagnostics {
AnomalyDiagnostics {
total_anomalies: self.anomaly_history.len(),
recent_anomaly_rate: self.calculate_recent_anomaly_rate(),
false_positive_rate: self.false_positive_tracker.get_current_fp_rate(),
detector_count: self.statistical_detectors.len() + self.ml_detectors.len(),
response_success_rate: self.response_system.get_success_rate(),
response_executions: self.response_system.execution_count(),
recent_window_len: self.recent_points.len(),
}
}
fn calculate_recent_anomaly_rate(&self) -> f64 {
let recent_window = Duration::from_secs(3600); let now = Instant::now();
let recent_count = self
.anomaly_history
.iter()
.filter(|event| now.duration_since(event.timestamp) <= recent_window)
.count();
recent_count as f64 / recent_window.as_secs_f64() }
}
impl<A: Float + Default + Clone + Send + Sync + Send + Sync> AdaptiveThresholdManager<A> {
fn new() -> Result<Self, String> {
Ok(Self {
thresholds: HashMap::new(),
threshold_bounds: HashMap::new(),
})
}
}
impl<A: Float + Default + Clone + Send + Sync + Send + Sync> FalsePositiveTracker<A> {
fn new() -> Self {
Self {
false_positives: VecDeque::with_capacity(1000),
fp_rate_calculator: FPRateCalculator {
recent_results: VecDeque::with_capacity(1000),
window_size: 1000,
current_fp_rate: scalar_or(0.05, A::zero()),
},
}
}
fn get_current_fp_rate(&self) -> Option<f64> {
if self.fp_rate_calculator.recent_results.is_empty() {
return None;
}
self.fp_rate_calculator.current_fp_rate.to_f64()
}
fn record_outcome(&mut self, predicted_anomaly: bool, was_true_anomaly: bool) {
let calculator = &mut self.fp_rate_calculator;
if calculator.recent_results.len() >= calculator.window_size {
calculator.recent_results.pop_front();
}
calculator.recent_results.push_back(DetectionResult {
timestamp: Instant::now(),
anomaly_detected: predicted_anomaly,
ground_truth: Some(was_true_anomaly),
detector_name: "ensemble".to_string(),
});
let negatives = calculator
.recent_results
.iter()
.filter(|result| result.ground_truth == Some(false))
.count();
if negatives > 0 {
let false_positives = calculator
.recent_results
.iter()
.filter(|result| result.anomaly_detected && result.ground_truth == Some(false))
.count();
if let Some(rate) = A::from(false_positives as f64 / negatives as f64) {
calculator.current_fp_rate = rate;
}
}
}
fn record_false_positive(&mut self, event: &AnomalyEvent<A>) {
if self.false_positives.len() >= RESPONSE_HISTORY_CAPACITY {
self.false_positives.pop_front();
}
self.false_positives.push_back(FalsePositiveEvent {
timestamp: event.timestamp,
data_point: event.data_point.clone(),
detector_name: event.detector_name.clone(),
anomaly_score: event.anomaly_score,
context: event.context.clone(),
});
}
fn confirmed_false_positive_count(&self) -> usize {
self.false_positives.len()
}
}
impl<A: Float + Default + Clone + Send + Sync + Send + Sync> AnomalyResponseSystem<A> {
fn new(response_strategy: &AnomalyResponseStrategy) -> Result<Self, String> {
let mut response_strategies = HashMap::new();
match response_strategy {
AnomalyResponseStrategy::Ignore => {
response_strategies
.insert(AnomalyType::StatisticalOutlier, vec![ResponseAction::Log]);
}
AnomalyResponseStrategy::Filter => {
response_strategies.insert(
AnomalyType::StatisticalOutlier,
vec![ResponseAction::Quarantine],
);
}
AnomalyResponseStrategy::Adaptive => {
response_strategies.insert(
AnomalyType::StatisticalOutlier,
vec![ResponseAction::Log, ResponseAction::ModelAdjustment],
);
}
_ => {
response_strategies
.insert(AnomalyType::StatisticalOutlier, vec![ResponseAction::Alert]);
}
}
Ok(Self {
response_strategies,
response_executor: ResponseExecutor {
pending_responses: VecDeque::new(),
execution_history: VecDeque::with_capacity(1000),
resource_limits: ResponseResourceLimits {
max_concurrent_responses: 10,
max_cpu_usage: 0.2,
max_memory_usage: 100 * 1024 * 1024, max_execution_time: Duration::from_secs(60),
},
},
next_response_id: 0,
log_entries: VecDeque::with_capacity(RESPONSE_HISTORY_CAPACITY),
alert_entries: VecDeque::with_capacity(RESPONSE_HISTORY_CAPACITY),
quarantined_points: VecDeque::with_capacity(QUARANTINE_CAPACITY),
pending_threshold_adjustment: None,
monitoring_level: 0,
})
}
fn trigger_response(&mut self, event: &AnomalyEvent<A>) -> Result<Vec<String>, String> {
let actions = self
.response_strategies
.get(&event.anomaly_type)
.or_else(|| {
self.response_strategies
.get(&AnomalyType::StatisticalOutlier)
})
.cloned()
.unwrap_or_default();
if actions.is_empty() {
return Ok(Vec::new());
}
let priority = match event.severity {
AnomalySeverity::Critical => ResponsePriority::Critical,
AnomalySeverity::High => ResponsePriority::High,
AnomalySeverity::Medium => ResponsePriority::Normal,
AnomalySeverity::Low => ResponsePriority::Low,
};
let timeout = self.response_executor.resource_limits.max_execution_time;
for action in actions {
if self.response_executor.pending_responses.len()
>= self
.response_executor
.resource_limits
.max_concurrent_responses
{
break;
}
self.next_response_id += 1;
self.response_executor
.pending_responses
.push_back(PendingResponse {
id: self.next_response_id,
anomaly_event: event.clone(),
action,
priority: priority.clone(),
scheduled_time: Instant::now(),
timeout,
});
}
self.execute_pending_responses()
}
fn execute_pending_responses(&mut self) -> Result<Vec<String>, String> {
self.response_executor
.pending_responses
.make_contiguous()
.sort_by(|a, b| b.priority.cmp(&a.priority));
let mut executed = Vec::new();
while let Some(pending) = self.response_executor.pending_responses.pop_front() {
let started = Instant::now();
let outcome = self.perform_action(&pending);
let duration = started.elapsed();
let (success, error_message) = match &outcome {
Ok(()) => (true, None),
Err(reason) => (false, Some(reason.clone())),
};
if success {
executed.push(format!("{:?}", pending.action));
}
let mut resources_consumed = HashMap::new();
if let Some(millis) = A::from(duration.as_secs_f64() * 1000.0) {
resources_consumed.insert("execution_time_ms".to_string(), millis);
}
let execution = ResponseExecution {
id: pending.id,
response: pending,
start_time: started,
duration,
success,
error_message,
resources_consumed,
};
if self.response_executor.execution_history.len() >= RESPONSE_HISTORY_CAPACITY {
self.response_executor.execution_history.pop_front();
}
self.response_executor
.execution_history
.push_back(execution);
}
Ok(executed)
}
fn perform_action(&mut self, pending: &PendingResponse<A>) -> Result<(), String> {
match &pending.action {
ResponseAction::Log => {
self.push_bounded(
ResponseChannel::Log,
format!(
"anomaly {} type={:?} severity={:?} score={:?}",
pending.anomaly_event.id,
pending.anomaly_event.anomaly_type,
pending.anomaly_event.severity,
pending.anomaly_event.anomaly_score.to_f64()
),
);
Ok(())
}
ResponseAction::Alert => {
self.push_bounded(
ResponseChannel::Alert,
format!(
"ALERT: anomaly {} severity={:?}",
pending.anomaly_event.id, pending.anomaly_event.severity
),
);
Ok(())
}
ResponseAction::Quarantine => {
if self.quarantined_points.len() >= QUARANTINE_CAPACITY {
self.quarantined_points.pop_front();
}
self.quarantined_points
.push_back(pending.anomaly_event.data_point.clone());
Ok(())
}
ResponseAction::ModelAdjustment => {
let step = match pending.anomaly_event.severity {
AnomalySeverity::Critical => 0.20,
AnomalySeverity::High => 0.10,
AnomalySeverity::Medium => 0.05,
AnomalySeverity::Low => 0.01,
};
self.pending_threshold_adjustment =
Some(self.pending_threshold_adjustment.unwrap_or(0.0) + step);
Ok(())
}
ResponseAction::IncreaseMonitoring => {
self.monitoring_level = self.monitoring_level.saturating_add(1);
Ok(())
}
ResponseAction::TriggerRecovery => Err(
"no recovery procedure is registered with this response system; \
a recovery handler must be installed before this action can run"
.to_string(),
),
ResponseAction::Custom(name) => Err(format!(
"no handler is registered for custom response action '{name}'"
)),
}
}
fn push_bounded(&mut self, channel: ResponseChannel, message: String) {
let sink = match channel {
ResponseChannel::Log => &mut self.log_entries,
ResponseChannel::Alert => &mut self.alert_entries,
};
if sink.len() >= RESPONSE_HISTORY_CAPACITY {
sink.pop_front();
}
sink.push_back(message);
}
fn take_pending_threshold_adjustment(&mut self) -> Option<f64> {
self.pending_threshold_adjustment.take()
}
fn get_success_rate(&self) -> Option<f64> {
let history = &self.response_executor.execution_history;
if history.is_empty() {
return None;
}
let successes = history.iter().filter(|execution| execution.success).count();
Some(successes as f64 / history.len() as f64)
}
fn log_entry_count(&self) -> usize {
self.log_entries.len()
}
fn alert_entry_count(&self) -> usize {
self.alert_entries.len()
}
fn quarantined_count(&self) -> usize {
self.quarantined_points.len()
}
fn monitoring_level(&self) -> u32 {
self.monitoring_level
}
fn execution_count(&self) -> usize {
self.response_executor.execution_history.len()
}
}
#[derive(Debug, Clone, Copy)]
enum ResponseChannel {
Log,
Alert,
}
#[derive(Debug, Clone)]
pub struct AnomalyDiagnostics {
pub total_anomalies: usize,
pub recent_anomaly_rate: f64,
pub false_positive_rate: Option<f64>,
pub detector_count: usize,
pub response_success_rate: Option<f64>,
pub response_executions: usize,
pub recent_window_len: usize,
}
#[cfg(test)]
#[path = "anomaly_detection_regression_tests.rs"]
mod regression_tests;