use anyhow::Result;
use std::collections::HashMap;
use super::{
AnalysisMetadata, BOCPDConfig, BOCPDResult, ChangepointResult, CorrelationResult, SimpleBOCPD,
StatisticalEngine, StatisticalResults, TrendDirection, TrendResult,
};
use super::analysis::log_sum_exp;
#[test]
fn test_bocpd_detects_mean_shift() -> Result<()> {
let mut engine = StatisticalEngine::new()?;
let mut data = HashMap::new();
let mut series = vec![1.0; 30];
series.extend(vec![10.0; 30]);
data.insert("x".to_string(), series);
let results = engine.analyze_time_series(&data)?;
assert!(
!results.changepoints.is_empty(),
"Expected at least one changepoint"
);
let has_near_mid = results
.changepoints
.iter()
.any(|cp| (cp.index as i64 - 30).abs() <= 5 && cp.confidence >= 0.0);
assert!(has_near_mid, "Expected a changepoint near index 30");
Ok(())
}
#[test]
fn test_bocpd_constant_series_no_high_confidence_changepoints() -> Result<()> {
let mut engine = StatisticalEngine::new()?;
let mut data = HashMap::new();
data.insert("x".to_string(), vec![5.0; 60]);
let results = engine.analyze_time_series(&data)?;
let high_confidence = results
.changepoints
.iter()
.filter(|cp| cp.confidence > 0.9)
.count();
assert!(
high_confidence <= 1,
"Constant series should not have many high-confidence changepoints"
);
Ok(())
}
#[test]
fn test_statistical_engine_creation() {
let engine = StatisticalEngine::new();
assert!(engine.is_ok());
}
#[test]
fn test_correlation_calculation() -> Result<()> {
let mut engine = StatisticalEngine::new()?;
let mut data = HashMap::new();
data.insert("x".to_string(), vec![1.0, 2.0, 3.0, 4.0, 5.0]);
data.insert("y".to_string(), vec![2.0, 4.0, 6.0, 8.0, 10.0]);
let results = engine.analyze_time_series(&data)?;
assert!(!results.correlations.is_empty());
let corr = results
.correlations
.iter()
.find(|corr| {
corr.variables == ("x".to_string(), "y".to_string())
|| corr.variables == ("y".to_string(), "x".to_string())
})
.expect("Expected correlation for (x, y)");
assert!(
(corr.coefficient - 1.0).abs() < 0.01,
"Correlation coefficient should be close to 1.0, got {}",
corr.coefficient
);
assert!(corr.significant);
Ok(())
}
#[test]
fn test_trend_analysis() -> Result<()> {
let mut engine = StatisticalEngine::new()?;
let mut data = HashMap::new();
data.insert("trend".to_string(), vec![1.0, 2.0, 3.0, 4.0, 5.0]);
let results = engine.analyze_time_series(&data)?;
assert!(!results.trends.is_empty());
let trend = &results.trends[0];
assert_eq!(trend.variable, "trend");
assert!(matches!(trend.direction, TrendDirection::Increasing));
assert!(trend.significant);
Ok(())
}
#[test]
fn test_data_validation() {
let engine = StatisticalEngine::new().unwrap();
let mut data = HashMap::new();
assert!(engine.validate_data(&data).is_err());
data.insert("bad".to_string(), vec![1.0, f64::NAN, 3.0]);
assert!(engine.validate_data(&data).is_err());
}
#[test]
fn test_simple_bocpd_creation() {
let config = BOCPDConfig::default();
let bocpd = SimpleBOCPD::new(config);
assert_eq!(bocpd.state.processed_points, 0);
assert_eq!(bocpd.state.data_buffer.len(), 0);
}
#[test]
fn test_joint_anomaly_changepoint_detection() {
let config = BOCPDConfig {
hazard_rate: 100.0,
expected_run_length: 50,
max_run_length_hypotheses: 200,
alert_threshold: 0.8,
buffer_size: 50,
};
let mut bocpd = SimpleBOCPD::new(config);
let mut data = Vec::new();
for i in 0..20 {
data.push(10.0 + (i as f64 * 0.1)); }
for i in 20..40 {
data.push(20.0 + (i as f64 * 0.2)); }
let results = bocpd.detect_changepoints(&data).unwrap();
assert!(!results.is_empty());
let reasonable_confidence_results: Vec<_> =
results.iter().filter(|r| r.confidence > 0.3).collect();
assert!(!reasonable_confidence_results.is_empty());
}
#[test]
fn test_posterior_distribution_computation() {
let config = BOCPDConfig::default();
let mut bocpd = SimpleBOCPD::new(config);
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 {
bocpd.update_state(value).unwrap();
}
let normalized = bocpd.normalize_distribution();
let sum: f64 = normalized.iter().sum();
assert!(
(sum - 1.0).abs() < 1e-10,
"Posterior should be normalized, got sum: {}",
sum
);
}
#[test]
fn test_streaming_updates_and_circular_buffers() {
let config = BOCPDConfig {
max_run_length_hypotheses: 100,
buffer_size: 5,
..Default::default()
};
let mut bocpd = SimpleBOCPD::new(config);
for i in 0..10 {
bocpd.update_state(i as f64).unwrap();
assert_eq!(bocpd.state.data_buffer.len(), (i + 1).min(5));
}
assert_eq!(bocpd.state.data_buffer.len(), 5);
let buffer_values: Vec<f64> = bocpd.state.data_buffer.iter().cloned().collect();
assert_eq!(buffer_values, vec![5.0, 6.0, 7.0, 8.0, 9.0]);
}
#[test]
fn test_hazard_rate_adaptation() {
let config = BOCPDConfig {
hazard_rate: 200.0,
..Default::default()
};
let mut bocpd = SimpleBOCPD::new(config);
for i in 0..15 {
bocpd.update_state(10.0 + (i as f64 * 0.01)).unwrap();
}
let _initial_hazard = bocpd.state.hazard_rate;
for i in 0..15 {
let value = 10.0 + (i as f64 * 10.0); bocpd.update_state(value).unwrap();
}
assert!(bocpd.state.processed_points > 15);
}
#[test]
fn test_multi_resolution_detection() {
let config = BOCPDConfig {
buffer_size: 100,
expected_run_length: 50,
..Default::default()
};
let mut bocpd = SimpleBOCPD::new(config);
let data = vec![
1.0, 1.1, 1.0, 1.1, 1.0, 1.1, 1.0, 1.1, 1.0, 1.1,
5.0, 5.1, 5.0, 5.1, 5.0, 5.1, 5.0, 5.1, 5.0, 5.1,
10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0,
];
let results = bocpd.detect_changepoints(&data).unwrap();
assert!(!results.is_empty());
}
#[test]
fn test_edge_cases() {
let config = BOCPDConfig::default();
let mut bocpd = SimpleBOCPD::new(config);
let empty_results = bocpd.detect_changepoints(&[]);
assert!(empty_results.is_ok());
assert!(empty_results.unwrap().is_empty());
let constant_data = vec![5.0; 30];
let constant_results = bocpd.detect_changepoints(&constant_data).unwrap();
let high_confidence_count = constant_results
.iter()
.filter(|r| r.confidence > 0.8)
.count();
assert!(
high_confidence_count <= 2,
"Constant series should not have many high-confidence changepoints"
);
let rapid_changes = vec![
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,
];
let rapid_results = bocpd.detect_changepoints(&rapid_changes).unwrap();
assert!(!rapid_results.is_empty());
}
#[test]
fn test_numerical_stability() {
let config = BOCPDConfig::default();
let mut bocpd = SimpleBOCPD::new(config);
let extreme_data = vec![1e10, 1e10, -1e10, 1e-10, 1e-10, f64::MAX, f64::MIN_POSITIVE];
let results = bocpd.detect_changepoints(&extreme_data);
assert!(results.is_ok(), "Should handle extreme values gracefully");
let test_values = vec![-1000.0, -500.0, -100.0, 0.0, 100.0, 500.0, 1000.0];
let log_sum = log_sum_exp(&test_values);
assert!(log_sum.is_finite(), "Log-sum-exp should be finite");
let log_add = log_sum_exp(&[-1000.0, -500.0]);
assert!(log_add.is_finite(), "Log-add-exp should be finite");
}