Skip to main content

do_memory_mcp/patterns/statistical/
tests.rs

1//! # Statistical Analysis Tests
2//!
3//! Test suite for the statistical analysis engine.
4
5use anyhow::Result;
6use std::collections::HashMap;
7
8// Import types from parent module (statistical which re-exports from analysis)
9use super::{
10    AnalysisMetadata, BOCPDConfig, BOCPDResult, ChangepointResult, CorrelationResult, SimpleBOCPD,
11    StatisticalEngine, StatisticalResults, TrendDirection, TrendResult,
12};
13
14// Import helper functions directly from analysis module
15use 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    // Clear mean shift around the midpoint
23    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    // Should have at least one changepoint in the neighborhood of the shift
34    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    // BOCPD may emit low-confidence candidates; ensure we do not see many high-confidence.
52    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    // Allow small floating point differences
90    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    // Empty data should fail
123    assert!(engine.validate_data(&data).is_err());
124
125    // Data with NaN should fail
126    data.insert("bad".to_string(), vec![1.0, f64::NAN, 3.0]);
127    assert!(engine.validate_data(&data).is_err());
128}
129
130// BOCPD Implementation Tests
131#[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    // Create data with a clear changepoint at index 25
153    let mut data = Vec::new();
154    for i in 0..20 {
155        data.push(10.0 + (i as f64 * 0.1)); // Gradually increasing
156    }
157    for i in 20..40 {
158        data.push(20.0 + (i as f64 * 0.2)); // Clear shift to higher values
159    }
160
161    let results = bocpd.detect_changepoints(&data).unwrap();
162
163    // Should detect at least one changepoint
164    assert!(!results.is_empty());
165
166    // At least one result should have reasonable confidence
167    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    // Test with simple data
178    let test_data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 10.0, 11.0, 12.0]; // Clear break at index 5
179
180    for &value in &test_data {
181        bocpd.update_state(value).unwrap();
182    }
183
184    // Check that posterior is properly normalized (sum should be close to 1)
185    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    // Add data that exceeds buffer size
205    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    // Verify buffer size is maintained
211    assert_eq!(bocpd.state.data_buffer.len(), 5);
212
213    // Verify oldest values are removed
214    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    // Add data with low variance first
228    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    // Add data with high variance - hazard rate should adapt based on variance
235    for i in 0..15 {
236        let value = 10.0 + (i as f64 * 10.0); // Much higher variance
237        bocpd.update_state(value).unwrap();
238    }
239
240    // State should be updated (processed points should increase)
241    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    // Create data with multiple types of patterns
255    let data = vec![
256        // Short-term pattern: gentle oscillation
257        1.0, 1.1, 1.0, 1.1, 1.0, 1.1, 1.0, 1.1, 1.0, 1.1,
258        // Medium-term shift: mean change
259        5.0, 5.1, 5.0, 5.1, 5.0, 5.1, 5.0, 5.1, 5.0, 5.1,
260        // Long-term trend: clear trend change
261        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    // Should detect some patterns
267    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    // Test empty data
276    let empty_results = bocpd.detect_changepoints(&[]);
277    assert!(empty_results.is_ok());
278    assert!(empty_results.unwrap().is_empty());
279
280    // Test constant series (no changepoints expected)
281    let constant_data = vec![5.0; 30];
282    let constant_results = bocpd.detect_changepoints(&constant_data).unwrap();
283    // Should not detect many changepoints in constant data
284    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    // Test rapid changes (multiple changepoints)
294    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    // Should detect some changepoints in rapidly changing data
300    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    // Test with extreme values
309    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    // Test log-space arithmetic functions
315    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}