do_memory_mcp/patterns/statistical/
tests.rs1use anyhow::Result;
6use std::collections::HashMap;
7
8use super::{
10 AnalysisMetadata, BOCPDConfig, BOCPDResult, ChangepointResult, CorrelationResult, SimpleBOCPD,
11 StatisticalEngine, StatisticalResults, TrendDirection, TrendResult,
12};
13
14use super::analysis::log_sum_exp;
16
17#[test]
18fn test_bocpd_detects_mean_shift() -> Result<()> {
19 let mut engine = StatisticalEngine::new()?;
20 let mut data = HashMap::new();
21
22 let mut series = vec![1.0; 30];
24 series.extend(vec![10.0; 30]);
25 data.insert("x".to_string(), series);
26
27 let results = engine.analyze_time_series(&data)?;
28 assert!(
29 !results.changepoints.is_empty(),
30 "Expected at least one changepoint"
31 );
32
33 let has_near_mid = results
35 .changepoints
36 .iter()
37 .any(|cp| (cp.index as i64 - 30).abs() <= 5 && cp.confidence >= 0.0);
38 assert!(has_near_mid, "Expected a changepoint near index 30");
39
40 Ok(())
41}
42
43#[test]
44fn test_bocpd_constant_series_no_high_confidence_changepoints() -> Result<()> {
45 let mut engine = StatisticalEngine::new()?;
46 let mut data = HashMap::new();
47 data.insert("x".to_string(), vec![5.0; 60]);
48
49 let results = engine.analyze_time_series(&data)?;
50
51 let high_confidence = results
53 .changepoints
54 .iter()
55 .filter(|cp| cp.confidence > 0.9)
56 .count();
57 assert!(
58 high_confidence <= 1,
59 "Constant series should not have many high-confidence changepoints"
60 );
61
62 Ok(())
63}
64
65#[test]
66fn test_statistical_engine_creation() {
67 let engine = StatisticalEngine::new();
68 assert!(engine.is_ok());
69}
70
71#[test]
72fn test_correlation_calculation() -> Result<()> {
73 let mut engine = StatisticalEngine::new()?;
74 let mut data = HashMap::new();
75 data.insert("x".to_string(), vec![1.0, 2.0, 3.0, 4.0, 5.0]);
76 data.insert("y".to_string(), vec![2.0, 4.0, 6.0, 8.0, 10.0]);
77
78 let results = engine.analyze_time_series(&data)?;
79 assert!(!results.correlations.is_empty());
80
81 let corr = results
82 .correlations
83 .iter()
84 .find(|corr| {
85 corr.variables == ("x".to_string(), "y".to_string())
86 || corr.variables == ("y".to_string(), "x".to_string())
87 })
88 .expect("Expected correlation for (x, y)");
89 assert!(
91 (corr.coefficient - 1.0).abs() < 0.01,
92 "Correlation coefficient should be close to 1.0, got {}",
93 corr.coefficient
94 );
95 assert!(corr.significant);
96
97 Ok(())
98}
99
100#[test]
101fn test_trend_analysis() -> Result<()> {
102 let mut engine = StatisticalEngine::new()?;
103 let mut data = HashMap::new();
104 data.insert("trend".to_string(), vec![1.0, 2.0, 3.0, 4.0, 5.0]);
105
106 let results = engine.analyze_time_series(&data)?;
107 assert!(!results.trends.is_empty());
108
109 let trend = &results.trends[0];
110 assert_eq!(trend.variable, "trend");
111 assert!(matches!(trend.direction, TrendDirection::Increasing));
112 assert!(trend.significant);
113
114 Ok(())
115}
116
117#[test]
118fn test_data_validation() {
119 let engine = StatisticalEngine::new().unwrap();
120 let mut data = HashMap::new();
121
122 assert!(engine.validate_data(&data).is_err());
124
125 data.insert("bad".to_string(), vec![1.0, f64::NAN, 3.0]);
127 assert!(engine.validate_data(&data).is_err());
128}
129
130#[test]
132fn test_simple_bocpd_creation() {
133 let config = BOCPDConfig::default();
134 let bocpd = SimpleBOCPD::new(config);
135
136 assert_eq!(bocpd.state.processed_points, 0);
137 assert_eq!(bocpd.state.data_buffer.len(), 0);
138}
139
140#[test]
141fn test_joint_anomaly_changepoint_detection() {
142 let config = BOCPDConfig {
143 hazard_rate: 100.0,
144 expected_run_length: 50,
145 max_run_length_hypotheses: 200,
146 alert_threshold: 0.8,
147 buffer_size: 50,
148 };
149
150 let mut bocpd = SimpleBOCPD::new(config);
151
152 let mut data = Vec::new();
154 for i in 0..20 {
155 data.push(10.0 + (i as f64 * 0.1)); }
157 for i in 20..40 {
158 data.push(20.0 + (i as f64 * 0.2)); }
160
161 let results = bocpd.detect_changepoints(&data).unwrap();
162
163 assert!(!results.is_empty());
165
166 let reasonable_confidence_results: Vec<_> =
168 results.iter().filter(|r| r.confidence > 0.3).collect();
169 assert!(!reasonable_confidence_results.is_empty());
170}
171
172#[test]
173fn test_posterior_distribution_computation() {
174 let config = BOCPDConfig::default();
175 let mut bocpd = SimpleBOCPD::new(config);
176
177 let test_data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 10.0, 11.0, 12.0]; for &value in &test_data {
181 bocpd.update_state(value).unwrap();
182 }
183
184 let normalized = bocpd.normalize_distribution();
186 let sum: f64 = normalized.iter().sum();
187 assert!(
188 (sum - 1.0).abs() < 1e-10,
189 "Posterior should be normalized, got sum: {}",
190 sum
191 );
192}
193
194#[test]
195fn test_streaming_updates_and_circular_buffers() {
196 let config = BOCPDConfig {
197 max_run_length_hypotheses: 100,
198 buffer_size: 5,
199 ..Default::default()
200 };
201
202 let mut bocpd = SimpleBOCPD::new(config);
203
204 for i in 0..10 {
206 bocpd.update_state(i as f64).unwrap();
207 assert_eq!(bocpd.state.data_buffer.len(), (i + 1).min(5));
208 }
209
210 assert_eq!(bocpd.state.data_buffer.len(), 5);
212
213 let buffer_values: Vec<f64> = bocpd.state.data_buffer.iter().cloned().collect();
215 assert_eq!(buffer_values, vec![5.0, 6.0, 7.0, 8.0, 9.0]);
216}
217
218#[test]
219fn test_hazard_rate_adaptation() {
220 let config = BOCPDConfig {
221 hazard_rate: 200.0,
222 ..Default::default()
223 };
224
225 let mut bocpd = SimpleBOCPD::new(config);
226
227 for i in 0..15 {
229 bocpd.update_state(10.0 + (i as f64 * 0.01)).unwrap();
230 }
231
232 let _initial_hazard = bocpd.state.hazard_rate;
233
234 for i in 0..15 {
236 let value = 10.0 + (i as f64 * 10.0); bocpd.update_state(value).unwrap();
238 }
239
240 assert!(bocpd.state.processed_points > 15);
242}
243
244#[test]
245fn test_multi_resolution_detection() {
246 let config = BOCPDConfig {
247 buffer_size: 100,
248 expected_run_length: 50,
249 ..Default::default()
250 };
251
252 let mut bocpd = SimpleBOCPD::new(config);
253
254 let data = vec![
256 1.0, 1.1, 1.0, 1.1, 1.0, 1.1, 1.0, 1.1, 1.0, 1.1,
258 5.0, 5.1, 5.0, 5.1, 5.0, 5.1, 5.0, 5.1, 5.0, 5.1,
260 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0,
262 ];
263
264 let results = bocpd.detect_changepoints(&data).unwrap();
265
266 assert!(!results.is_empty());
268}
269
270#[test]
271fn test_edge_cases() {
272 let config = BOCPDConfig::default();
273 let mut bocpd = SimpleBOCPD::new(config);
274
275 let empty_results = bocpd.detect_changepoints(&[]);
277 assert!(empty_results.is_ok());
278 assert!(empty_results.unwrap().is_empty());
279
280 let constant_data = vec![5.0; 30];
282 let constant_results = bocpd.detect_changepoints(&constant_data).unwrap();
283 let high_confidence_count = constant_results
285 .iter()
286 .filter(|r| r.confidence > 0.8)
287 .count();
288 assert!(
289 high_confidence_count <= 2,
290 "Constant series should not have many high-confidence changepoints"
291 );
292
293 let rapid_changes = vec![
295 1.0, 1.0, 1.0, 10.0, 10.0, 10.0, 2.0, 2.0, 2.0, 15.0, 15.0, 15.0, 3.0, 3.0, 3.0,
296 ];
297 let rapid_results = bocpd.detect_changepoints(&rapid_changes).unwrap();
298
299 assert!(!rapid_results.is_empty());
301}
302
303#[test]
304fn test_numerical_stability() {
305 let config = BOCPDConfig::default();
306 let mut bocpd = SimpleBOCPD::new(config);
307
308 let extreme_data = vec![1e10, 1e10, -1e10, 1e-10, 1e-10, f64::MAX, f64::MIN_POSITIVE];
310
311 let results = bocpd.detect_changepoints(&extreme_data);
312 assert!(results.is_ok(), "Should handle extreme values gracefully");
313
314 let test_values = vec![-1000.0, -500.0, -100.0, 0.0, 100.0, 500.0, 1000.0];
316 let log_sum = log_sum_exp(&test_values);
317 assert!(log_sum.is_finite(), "Log-sum-exp should be finite");
318
319 let log_add = log_sum_exp(&[-1000.0, -500.0]);
320 assert!(log_add.is_finite(), "Log-add-exp should be finite");
321}