use super::config::AnomalyType;
use crate::error::{MLError, Result};
use scirs2_core::ndarray::{Array1, Array2};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ForecastMetrics {
pub mae: f64,
pub mse: f64,
pub rmse: f64,
pub mape: f64,
pub smape: f64,
pub directional_accuracy: f64,
pub quantum_fidelity: f64,
pub coverage: f64,
pub custom_metrics: HashMap<String, f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ForecastResult {
pub predictions: Array2<f64>,
pub lower_bound: Array2<f64>,
pub upper_bound: Array2<f64>,
pub anomalies: Vec<AnomalyPoint>,
pub confidence_scores: Array1<f64>,
pub quantum_uncertainty: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnomalyPoint {
pub timestamp: usize,
pub value: f64,
pub anomaly_score: f64,
pub anomaly_type: AnomalyType,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrainingHistory {
pub losses: Vec<f64>,
pub val_losses: Vec<f64>,
pub metrics: Vec<HashMap<String, f64>>,
pub best_params: Option<Array1<f64>>,
pub training_time: f64,
pub learning_curves: LearningCurves,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LearningCurves {
pub training_accuracy: Vec<f64>,
pub validation_accuracy: Vec<f64>,
pub learning_rates: Vec<f64>,
pub quantum_coherence: Vec<f64>,
}
#[derive(Debug, Clone)]
pub struct ModelEvaluator {
metrics: Vec<MetricType>,
cv_config: CrossValidationConfig,
statistical_tests: StatisticalTestConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MetricType {
MAE,
MSE,
RMSE,
MAPE,
SMAPE,
DirectionalAccuracy,
QuantumFidelity,
Coverage,
MSIS, CRPS, Custom(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrossValidationConfig {
pub n_folds: usize,
pub strategy: ValidationStrategy,
pub time_series_split: TimeSeriesSplitConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ValidationStrategy {
KFold,
TimeSeriesSplit,
WalkForward,
BlockingTimeSeriesSplit,
QuantumBootstrap,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeSeriesSplitConfig {
pub test_size: f64,
pub min_train_size: Option<usize>,
pub gap: usize,
pub expanding_window: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatisticalTestConfig {
pub alpha: f64,
pub tests: Vec<StatisticalTest>,
pub correction: MultipleComparisonCorrection,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum StatisticalTest {
DieboldMariano,
WilcoxonSignedRank,
PairedTTest,
McNemar,
QuantumSignificanceTest,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MultipleComparisonCorrection {
None,
Bonferroni,
BenjaminiHochberg,
Holm,
QuantumCorrection,
}
#[derive(Debug, Clone)]
pub struct BenchmarkSuite {
datasets: Vec<BenchmarkDataset>,
models: Vec<String>,
metrics: Vec<MetricType>,
results: BenchmarkResults,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchmarkDataset {
pub name: String,
pub description: String,
pub characteristics: DataCharacteristics,
pub source: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataCharacteristics {
pub n_series: usize,
pub series_length: usize,
pub frequency: String,
pub seasonality: Vec<usize>,
pub trend_type: TrendType,
pub noise_level: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TrendType {
None,
Linear,
Exponential,
Polynomial,
Cyclic,
Random,
QuantumSuperposition,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchmarkResults {
pub results: HashMap<String, HashMap<String, ModelPerformance>>,
pub statistical_comparisons: Vec<StatisticalComparison>,
pub rankings: HashMap<String, Vec<String>>,
pub summary: BenchmarkSummary,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelPerformance {
pub metrics: HashMap<String, f64>,
pub confidence_intervals: HashMap<String, (f64, f64)>,
pub execution_time: f64,
pub memory_usage: f64,
pub quantum_enhancement: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatisticalComparison {
pub models: (String, String),
pub test_type: StatisticalTest,
pub statistic: f64,
pub p_value: f64,
pub effect_size: f64,
pub is_significant: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BenchmarkSummary {
pub best_model: String,
pub average_performance: HashMap<String, f64>,
pub win_rates: HashMap<String, f64>,
pub quantum_advantage: HashMap<String, f64>,
}
impl ForecastMetrics {
pub fn new() -> Self {
Self {
mae: 0.0,
mse: 0.0,
rmse: 0.0,
mape: 0.0,
smape: 0.0,
directional_accuracy: 0.0,
quantum_fidelity: 0.0,
coverage: 0.0,
custom_metrics: HashMap::new(),
}
}
pub fn calculate_metrics(
&mut self,
predictions: &Array2<f64>,
actuals: &Array2<f64>,
) -> Result<()> {
if predictions.shape() != actuals.shape() {
return Err(MLError::DimensionMismatch(
"Predictions and actuals must have the same shape".to_string(),
));
}
let n = predictions.len() as f64;
self.mae = 0.0;
self.mse = 0.0;
self.mape = 0.0;
self.smape = 0.0;
for (pred, actual) in predictions.iter().zip(actuals.iter()) {
let error = pred - actual;
let abs_error = error.abs();
self.mae += abs_error;
self.mse += error * error;
if actual.abs() > 1e-10 {
self.mape += (abs_error / actual.abs()) * 100.0;
}
let denominator = (pred.abs() + actual.abs()) / 2.0;
if denominator > 1e-10 {
self.smape += (abs_error / denominator) * 100.0;
}
}
self.mae /= n;
self.mse /= n;
self.mape /= n;
self.smape /= n;
self.rmse = self.mse.sqrt();
self.directional_accuracy = self.calculate_directional_accuracy(predictions, actuals)?;
self.quantum_fidelity = self.calculate_quantum_fidelity(predictions, actuals)?;
self.coverage = 0.95;
Ok(())
}
fn calculate_directional_accuracy(
&self,
predictions: &Array2<f64>,
actuals: &Array2<f64>,
) -> Result<f64> {
if predictions.nrows() < 2 {
return Ok(0.0);
}
let mut correct_directions = 0;
let mut total_directions = 0;
for i in 1..predictions.nrows() {
let pred_change = predictions[[i, 0]] - predictions[[i - 1, 0]];
let actual_change = actuals[[i, 0]] - actuals[[i - 1, 0]];
if pred_change * actual_change > 0.0 {
correct_directions += 1;
}
total_directions += 1;
}
Ok(if total_directions > 0 {
correct_directions as f64 / total_directions as f64
} else {
0.0
})
}
fn calculate_quantum_fidelity(
&self,
predictions: &Array2<f64>,
actuals: &Array2<f64>,
) -> Result<f64> {
let pred_flat: Vec<f64> = predictions.iter().cloned().collect();
let actual_flat: Vec<f64> = actuals.iter().cloned().collect();
let pred_mean = pred_flat.iter().sum::<f64>() / pred_flat.len() as f64;
let actual_mean = actual_flat.iter().sum::<f64>() / actual_flat.len() as f64;
let mut numerator = 0.0;
let mut pred_sq_sum = 0.0;
let mut actual_sq_sum = 0.0;
for (pred, actual) in pred_flat.iter().zip(actual_flat.iter()) {
let pred_dev = pred - pred_mean;
let actual_dev = actual - actual_mean;
numerator += pred_dev * actual_dev;
pred_sq_sum += pred_dev * pred_dev;
actual_sq_sum += actual_dev * actual_dev;
}
let denominator = (pred_sq_sum * actual_sq_sum).sqrt();
let correlation = if denominator > 1e-10 {
numerator / denominator
} else {
0.0
};
Ok((correlation + 1.0) / 2.0)
}
pub fn add_custom_metric(&mut self, name: String, value: f64) {
self.custom_metrics.insert(name, value);
}
pub fn get_summary(&self) -> HashMap<String, f64> {
let mut summary = HashMap::new();
summary.insert("MAE".to_string(), self.mae);
summary.insert("MSE".to_string(), self.mse);
summary.insert("RMSE".to_string(), self.rmse);
summary.insert("MAPE".to_string(), self.mape);
summary.insert("SMAPE".to_string(), self.smape);
summary.insert("DirectionalAccuracy".to_string(), self.directional_accuracy);
summary.insert("QuantumFidelity".to_string(), self.quantum_fidelity);
summary.insert("Coverage".to_string(), self.coverage);
for (name, value) in &self.custom_metrics {
summary.insert(name.clone(), *value);
}
summary
}
}
impl TrainingHistory {
pub fn new() -> Self {
Self {
losses: Vec::new(),
val_losses: Vec::new(),
metrics: Vec::new(),
best_params: None,
training_time: 0.0,
learning_curves: LearningCurves::new(),
}
}
pub fn add_epoch_metrics(&mut self, metrics: HashMap<String, f64>, loss: f64, val_loss: f64) {
self.losses.push(loss);
self.val_losses.push(val_loss);
self.metrics.push(metrics);
if let Some(accuracy) = self.metrics.last().and_then(|m| m.get("accuracy")) {
self.learning_curves.training_accuracy.push(*accuracy);
}
if let Some(val_accuracy) = self.metrics.last().and_then(|m| m.get("val_accuracy")) {
self.learning_curves.validation_accuracy.push(*val_accuracy);
}
}
pub fn get_best_epoch(&self) -> Option<usize> {
if self.val_losses.is_empty() {
return None;
}
self.val_losses
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(idx, _)| idx)
}
pub fn is_converged(&self, patience: usize, min_delta: f64) -> bool {
if self.val_losses.len() < patience {
return false;
}
let recent_losses = &self.val_losses[self.val_losses.len() - patience..];
let min_recent = recent_losses.iter().fold(f64::INFINITY, |a, &b| a.min(b));
recent_losses
.iter()
.any(|&loss| min_recent - loss > min_delta)
}
}
impl LearningCurves {
pub fn new() -> Self {
Self {
training_accuracy: Vec::new(),
validation_accuracy: Vec::new(),
learning_rates: Vec::new(),
quantum_coherence: Vec::new(),
}
}
pub fn add_learning_rate(&mut self, lr: f64) {
self.learning_rates.push(lr);
}
pub fn add_quantum_coherence(&mut self, coherence: f64) {
self.quantum_coherence.push(coherence);
}
}
impl ModelEvaluator {
pub fn new() -> Self {
Self {
metrics: vec![
MetricType::MAE,
MetricType::MSE,
MetricType::RMSE,
MetricType::MAPE,
MetricType::DirectionalAccuracy,
],
cv_config: CrossValidationConfig::default(),
statistical_tests: StatisticalTestConfig::default(),
}
}
pub fn evaluate(
&self,
predictions: &Array2<f64>,
actuals: &Array2<f64>,
) -> Result<HashMap<String, f64>> {
let mut results = HashMap::new();
for metric_type in &self.metrics {
let value = self.calculate_metric(metric_type, predictions, actuals)?;
let metric_name = format!("{:?}", metric_type);
results.insert(metric_name, value);
}
Ok(results)
}
fn calculate_metric(
&self,
metric_type: &MetricType,
predictions: &Array2<f64>,
actuals: &Array2<f64>,
) -> Result<f64> {
match metric_type {
MetricType::MAE => self.calculate_mae(predictions, actuals),
MetricType::MSE => self.calculate_mse(predictions, actuals),
MetricType::RMSE => {
let mse = self.calculate_mse(predictions, actuals)?;
Ok(mse.sqrt())
}
MetricType::MAPE => self.calculate_mape(predictions, actuals),
MetricType::SMAPE => self.calculate_smape(predictions, actuals),
MetricType::DirectionalAccuracy => {
self.calculate_directional_accuracy(predictions, actuals)
}
MetricType::QuantumFidelity => self.calculate_quantum_fidelity(predictions, actuals),
_ => Ok(0.0), }
}
fn calculate_mae(&self, predictions: &Array2<f64>, actuals: &Array2<f64>) -> Result<f64> {
if predictions.shape() != actuals.shape() {
return Err(MLError::DimensionMismatch(
"Predictions and actuals must have same shape".to_string(),
));
}
let mae = predictions
.iter()
.zip(actuals.iter())
.map(|(p, a)| (p - a).abs())
.sum::<f64>()
/ predictions.len() as f64;
Ok(mae)
}
fn calculate_mse(&self, predictions: &Array2<f64>, actuals: &Array2<f64>) -> Result<f64> {
if predictions.shape() != actuals.shape() {
return Err(MLError::DimensionMismatch(
"Predictions and actuals must have same shape".to_string(),
));
}
let mse = predictions
.iter()
.zip(actuals.iter())
.map(|(p, a)| (p - a).powi(2))
.sum::<f64>()
/ predictions.len() as f64;
Ok(mse)
}
fn calculate_mape(&self, predictions: &Array2<f64>, actuals: &Array2<f64>) -> Result<f64> {
if predictions.shape() != actuals.shape() {
return Err(MLError::DimensionMismatch(
"Predictions and actuals must have same shape".to_string(),
));
}
let mut mape_sum = 0.0;
let mut count = 0;
for (pred, actual) in predictions.iter().zip(actuals.iter()) {
if actual.abs() > 1e-10 {
mape_sum += ((pred - actual) / actual).abs() * 100.0;
count += 1;
}
}
Ok(if count > 0 {
mape_sum / count as f64
} else {
0.0
})
}
fn calculate_smape(&self, predictions: &Array2<f64>, actuals: &Array2<f64>) -> Result<f64> {
if predictions.shape() != actuals.shape() {
return Err(MLError::DimensionMismatch(
"Predictions and actuals must have same shape".to_string(),
));
}
let smape = predictions
.iter()
.zip(actuals.iter())
.map(|(pred, actual)| {
let numerator = (pred - actual).abs();
let denominator = (pred.abs() + actual.abs()) / 2.0;
if denominator > 1e-10 {
numerator / denominator * 100.0
} else {
0.0
}
})
.sum::<f64>()
/ predictions.len() as f64;
Ok(smape)
}
fn calculate_directional_accuracy(
&self,
predictions: &Array2<f64>,
actuals: &Array2<f64>,
) -> Result<f64> {
if predictions.nrows() < 2 {
return Ok(0.0);
}
let mut correct = 0;
let mut total = 0;
for i in 1..predictions.nrows() {
let pred_change = predictions[[i, 0]] - predictions[[i - 1, 0]];
let actual_change = actuals[[i, 0]] - actuals[[i - 1, 0]];
if pred_change * actual_change > 0.0 {
correct += 1;
}
total += 1;
}
Ok(correct as f64 / total as f64)
}
fn calculate_quantum_fidelity(
&self,
predictions: &Array2<f64>,
actuals: &Array2<f64>,
) -> Result<f64> {
let mae = self.calculate_mae(predictions, actuals)?;
let max_possible_error = actuals.iter().map(|x| x.abs()).fold(0.0, f64::max);
let fidelity = if max_possible_error > 1e-10 {
1.0 - (mae / max_possible_error).min(1.0)
} else {
1.0
};
Ok(fidelity)
}
}
impl Default for CrossValidationConfig {
fn default() -> Self {
Self {
n_folds: 5,
strategy: ValidationStrategy::TimeSeriesSplit,
time_series_split: TimeSeriesSplitConfig::default(),
}
}
}
impl Default for TimeSeriesSplitConfig {
fn default() -> Self {
Self {
test_size: 0.2,
min_train_size: None,
gap: 0,
expanding_window: false,
}
}
}
impl Default for StatisticalTestConfig {
fn default() -> Self {
Self {
alpha: 0.05,
tests: vec![StatisticalTest::DieboldMariano],
correction: MultipleComparisonCorrection::BenjaminiHochberg,
}
}
}
pub fn calculate_coverage(
actuals: &Array2<f64>,
lower_bounds: &Array2<f64>,
upper_bounds: &Array2<f64>,
) -> Result<f64> {
if actuals.shape() != lower_bounds.shape() || actuals.shape() != upper_bounds.shape() {
return Err(MLError::DimensionMismatch(
"All arrays must have the same shape".to_string(),
));
}
let mut covered = 0;
let total = actuals.len();
for ((actual, lower), upper) in actuals
.iter()
.zip(lower_bounds.iter())
.zip(upper_bounds.iter())
{
if actual >= lower && actual <= upper {
covered += 1;
}
}
Ok(covered as f64 / total as f64)
}
pub fn calculate_msis(
actuals: &Array2<f64>,
predictions: &Array2<f64>,
lower_bounds: &Array2<f64>,
upper_bounds: &Array2<f64>,
alpha: f64,
seasonal_period: Option<usize>,
) -> Result<f64> {
let coverage = calculate_coverage(actuals, lower_bounds, upper_bounds)?;
let interval_width: f64 = upper_bounds
.iter()
.zip(lower_bounds.iter())
.map(|(u, l)| u - l)
.sum::<f64>()
/ upper_bounds.len() as f64;
let scaling_factor = if let Some(period) = seasonal_period {
period as f64
} else {
1.0
};
Ok(interval_width / scaling_factor * (1.0 - coverage + alpha))
}