Skip to main content

ringkernel_accnet/kernels/
temporal.rs

1//! Temporal analysis kernels for time-series anomaly detection.
2//!
3//! These kernels analyze transaction patterns over time to detect:
4//! - Seasonal variations
5//! - Behavioral anomalies
6//! - Trend changes
7
8use crate::models::{
9    AccountingNetwork, BehavioralBaseline, HybridTimestamp, SeasonalPattern, TemporalAlert,
10    TemporalAlertType, TimeGranularity,
11};
12use std::collections::HashMap;
13
14/// Configuration for temporal analysis kernels.
15#[derive(Debug, Clone)]
16pub struct TemporalConfig {
17    /// Block size for GPU dispatch.
18    pub block_size: u32,
19    /// Z-score threshold for anomalies.
20    pub z_score_threshold: f64,
21    /// Minimum data points for baseline.
22    pub min_baseline_points: usize,
23    /// Time granularity for analysis.
24    pub granularity: TimeGranularity,
25}
26
27impl Default for TemporalConfig {
28    fn default() -> Self {
29        Self {
30            block_size: 256,
31            z_score_threshold: 3.0,
32            min_baseline_points: 30,
33            granularity: TimeGranularity::Daily,
34        }
35    }
36}
37
38/// Result of temporal analysis.
39#[derive(Debug, Clone, Default)]
40pub struct TemporalResult {
41    /// Account baselines.
42    pub baselines: HashMap<u16, BehavioralBaseline>,
43    /// Detected seasonal patterns.
44    pub seasonal_patterns: Vec<SeasonalPattern>,
45    /// Generated alerts.
46    pub alerts: Vec<TemporalAlert>,
47    /// Analysis statistics.
48    pub stats: TemporalStats,
49}
50
51/// Statistics from temporal analysis.
52#[derive(Debug, Clone, Default)]
53pub struct TemporalStats {
54    /// Time periods analyzed.
55    pub periods_analyzed: usize,
56    /// Baselines computed.
57    pub baselines_computed: usize,
58    /// Anomalies detected.
59    pub anomalies_detected: usize,
60}
61
62/// Temporal analysis kernel dispatcher.
63pub struct TemporalKernel {
64    config: TemporalConfig,
65}
66
67impl TemporalKernel {
68    /// Create a new temporal kernel.
69    pub fn new(config: TemporalConfig) -> Self {
70        Self { config }
71    }
72
73    /// Analyze temporal patterns (CPU fallback).
74    pub fn analyze(&self, network: &AccountingNetwork) -> TemporalResult {
75        let mut result = TemporalResult::default();
76
77        // Build time series per account
78        let account_series = self.build_time_series(network);
79
80        // Compute baselines
81        for (account_idx, series) in &account_series {
82            if series.len() >= self.config.min_baseline_points {
83                let baseline = self.compute_baseline(*account_idx, series);
84                result.baselines.insert(*account_idx, baseline);
85                result.stats.baselines_computed += 1;
86            }
87        }
88
89        // Detect anomalies using baselines
90        for (account_idx, series) in &account_series {
91            if let Some(baseline) = result.baselines.get(account_idx) {
92                for (timestamp, value) in series {
93                    let (is_anomaly, score) = baseline.is_anomaly(*value);
94                    if is_anomaly {
95                        result.alerts.push(TemporalAlert {
96                            id: uuid::Uuid::new_v4(),
97                            account_id: *account_idx,
98                            alert_type: TemporalAlertType::Anomaly,
99                            severity: score,
100                            trigger_value: *value,
101                            expected_value: baseline.mean,
102                            deviation: (*value - baseline.mean).abs() / baseline.std_dev.max(0.001),
103                            timestamp: *timestamp,
104                            message: format!(
105                                "Anomalous activity: {:.2} (expected {:.2} ± {:.2})",
106                                value,
107                                baseline.mean,
108                                baseline.std_dev * 2.0
109                            ),
110                        });
111                        result.stats.anomalies_detected += 1;
112                    }
113                }
114            }
115        }
116
117        result.stats.periods_analyzed = account_series.values().map(|s| s.len()).sum();
118
119        result
120    }
121
122    /// Build time series from flows.
123    fn build_time_series(
124        &self,
125        network: &AccountingNetwork,
126    ) -> HashMap<u16, Vec<(HybridTimestamp, f64)>> {
127        let mut series: HashMap<u16, Vec<(HybridTimestamp, f64)>> = HashMap::new();
128
129        for flow in &network.flows {
130            let amount = flow.amount.to_f64();
131
132            // Add to source account (outflow)
133            series
134                .entry(flow.source_account_index)
135                .or_default()
136                .push((flow.timestamp, -amount));
137
138            // Add to target account (inflow)
139            series
140                .entry(flow.target_account_index)
141                .or_default()
142                .push((flow.timestamp, amount));
143        }
144
145        // Sort by timestamp
146        for series_vec in series.values_mut() {
147            series_vec.sort_by_key(|(ts, _)| ts.physical);
148        }
149
150        series
151    }
152
153    /// Compute baseline for a time series.
154    fn compute_baseline(
155        &self,
156        account_id: u16,
157        series: &[(HybridTimestamp, f64)],
158    ) -> BehavioralBaseline {
159        let values: Vec<f64> = series.iter().map(|(_, v)| *v).collect();
160        let n = values.len() as f64;
161
162        let mean = values.iter().sum::<f64>() / n;
163        let variance = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n;
164        let std_dev = variance.sqrt();
165
166        let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
167        let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
168
169        // Compute percentiles
170        let mut sorted = values.clone();
171        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
172
173        let p5_idx = ((n * 0.05) as usize).min(sorted.len().saturating_sub(1));
174        let p25_idx = ((n * 0.25) as usize).min(sorted.len().saturating_sub(1));
175        let p50_idx = ((n * 0.50) as usize).min(sorted.len().saturating_sub(1));
176        let p75_idx = ((n * 0.75) as usize).min(sorted.len().saturating_sub(1));
177        let p95_idx = ((n * 0.95) as usize).min(sorted.len().saturating_sub(1));
178
179        let median = sorted.get(p50_idx).copied().unwrap_or(mean);
180        let q1 = sorted.get(p25_idx).copied().unwrap_or(min);
181        let q3 = sorted.get(p75_idx).copied().unwrap_or(max);
182        let iqr = q3 - q1;
183
184        // Compute MAD (Median Absolute Deviation)
185        let mut abs_deviations: Vec<f64> = values.iter().map(|v| (v - median).abs()).collect();
186        abs_deviations.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
187        let mad_idx = ((abs_deviations.len() as f64 * 0.5) as usize)
188            .min(abs_deviations.len().saturating_sub(1));
189        let mad = abs_deviations.get(mad_idx).copied().unwrap_or(0.0);
190
191        let mut baseline = BehavioralBaseline::new(account_id);
192        baseline.mean = mean;
193        baseline.std_dev = std_dev;
194        baseline.min_value = min;
195        baseline.max_value = max;
196        baseline.median = median;
197        baseline.mad = mad;
198        baseline.q1 = q1;
199        baseline.q3 = q3;
200        baseline.iqr = iqr;
201        baseline.p5 = sorted.get(p5_idx).copied().unwrap_or(min);
202        baseline.p95 = sorted.get(p95_idx).copied().unwrap_or(max);
203        baseline.period_count = series.len() as u16;
204
205        baseline
206    }
207}
208
209impl Default for TemporalKernel {
210    fn default() -> Self {
211        Self::new(TemporalConfig::default())
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn test_temporal_kernel_creation() {
221        let kernel = TemporalKernel::default();
222        assert_eq!(kernel.config.z_score_threshold, 3.0);
223    }
224
225    #[test]
226    fn test_baseline_computation() {
227        let kernel = TemporalKernel::default();
228        let series: Vec<(HybridTimestamp, f64)> = (0..100)
229            .map(|i| (HybridTimestamp::new(i * 1000, 0), (i % 10) as f64))
230            .collect();
231
232        let baseline = kernel.compute_baseline(0, &series);
233        assert!(baseline.mean >= 0.0);
234        assert!(baseline.std_dev >= 0.0);
235        assert_eq!(baseline.period_count, 100);
236    }
237}