764 lines
23 KiB
Rust
764 lines
23 KiB
Rust
//! Shared IPC types for the Time Series Forecast demo
|
|
//!
|
|
//! This crate defines the data structures shared between the Rust backend
|
|
//! and the TypeScript frontend for the time series forecasting demo.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Available time series model types
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ModelType {
|
|
/// AutoRegressive Integrated Moving Average
|
|
Arima,
|
|
/// Seasonal ARIMA
|
|
Sarima,
|
|
/// Facebook Prophet-style model
|
|
Prophet,
|
|
/// Exponential Smoothing (Holt-Winters)
|
|
ExponentialSmoothing,
|
|
/// Neural Prophet (Prophet with neural networks)
|
|
NeuralProphet,
|
|
/// Transformer-based forecasting
|
|
Transformer,
|
|
}
|
|
|
|
impl ModelType {
|
|
/// Get display name
|
|
pub fn display_name(&self) -> &'static str {
|
|
match self {
|
|
Self::Arima => "ARIMA",
|
|
Self::Sarima => "SARIMA",
|
|
Self::Prophet => "Prophet",
|
|
Self::ExponentialSmoothing => "Exponential Smoothing",
|
|
Self::NeuralProphet => "Neural Prophet",
|
|
Self::Transformer => "Transformer",
|
|
}
|
|
}
|
|
|
|
/// Get description
|
|
pub fn description(&self) -> &'static str {
|
|
match self {
|
|
Self::Arima => "Classical autoregressive model for stationary time series",
|
|
Self::Sarima => "ARIMA with seasonal components",
|
|
Self::Prophet => "Decomposable model with trend, seasonality, and holidays",
|
|
Self::ExponentialSmoothing => "Weighted average with exponential decay",
|
|
Self::NeuralProphet => "Prophet enhanced with neural network components",
|
|
Self::Transformer => "Attention-based deep learning model",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// ARIMA model configuration (p, d, q)
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ArimaConfig {
|
|
/// Autoregressive order
|
|
pub p: usize,
|
|
/// Differencing order
|
|
pub d: usize,
|
|
/// Moving average order
|
|
pub q: usize,
|
|
}
|
|
|
|
impl Default for ArimaConfig {
|
|
fn default() -> Self {
|
|
Self { p: 1, d: 1, q: 1 }
|
|
}
|
|
}
|
|
|
|
/// SARIMA model configuration with seasonal parameters
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SarimaConfig {
|
|
/// Non-seasonal ARIMA (p, d, q)
|
|
pub order: ArimaConfig,
|
|
/// Seasonal order (P, D, Q)
|
|
pub seasonal_order: ArimaConfig,
|
|
/// Seasonal period (e.g., 12 for monthly data)
|
|
pub seasonal_period: usize,
|
|
}
|
|
|
|
impl Default for SarimaConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
order: ArimaConfig::default(),
|
|
seasonal_order: ArimaConfig { p: 1, d: 1, q: 1 },
|
|
seasonal_period: 12,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Prophet model configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ProphetConfig {
|
|
/// Growth type: "linear" or "logistic"
|
|
pub growth: String,
|
|
/// Enable yearly seasonality
|
|
pub yearly_seasonality: bool,
|
|
/// Enable weekly seasonality
|
|
pub weekly_seasonality: bool,
|
|
/// Enable daily seasonality
|
|
pub daily_seasonality: bool,
|
|
/// Changepoint prior scale (flexibility of trend)
|
|
pub changepoint_prior_scale: f64,
|
|
}
|
|
|
|
impl Default for ProphetConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
growth: "linear".to_string(),
|
|
yearly_seasonality: true,
|
|
weekly_seasonality: true,
|
|
daily_seasonality: false,
|
|
changepoint_prior_scale: 0.05,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Configuration for initializing a forecasting model
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ForecastConfig {
|
|
/// Model type
|
|
pub model_type: ModelType,
|
|
/// Forecast horizon (number of future periods)
|
|
pub horizon: usize,
|
|
/// Confidence level for prediction intervals (0.0 - 1.0)
|
|
pub confidence_level: f64,
|
|
/// ARIMA config (if applicable)
|
|
pub arima_config: Option<ArimaConfig>,
|
|
/// SARIMA config (if applicable)
|
|
pub sarima_config: Option<SarimaConfig>,
|
|
/// Prophet config (if applicable)
|
|
pub prophet_config: Option<ProphetConfig>,
|
|
/// Whether to use GPU if available
|
|
pub use_gpu: bool,
|
|
}
|
|
|
|
impl Default for ForecastConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
model_type: ModelType::Arima,
|
|
horizon: 30,
|
|
confidence_level: 0.95,
|
|
arima_config: Some(ArimaConfig::default()),
|
|
sarima_config: None,
|
|
prophet_config: None,
|
|
use_gpu: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A single data point in the time series
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct DataPoint {
|
|
/// Timestamp (Unix milliseconds or index)
|
|
pub timestamp: f64,
|
|
/// Value
|
|
pub value: f64,
|
|
}
|
|
|
|
/// Input time series data
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct TimeSeriesData {
|
|
/// Data points
|
|
pub data: Vec<DataPoint>,
|
|
/// Optional column name
|
|
pub name: Option<String>,
|
|
/// Data frequency (e.g., "daily", "monthly", "hourly")
|
|
pub frequency: Option<String>,
|
|
}
|
|
|
|
/// Forecast result with predictions and confidence intervals
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ForecastResult {
|
|
/// Forecasted values (mean)
|
|
pub forecast: Vec<DataPoint>,
|
|
/// Lower confidence bound
|
|
pub lower_bound: Vec<DataPoint>,
|
|
/// Upper confidence bound
|
|
pub upper_bound: Vec<DataPoint>,
|
|
/// Fitted values for historical data
|
|
pub fitted: Vec<DataPoint>,
|
|
/// Forecast components (if available)
|
|
pub components: Option<ForecastComponents>,
|
|
/// Model metrics
|
|
pub metrics: FitMetrics,
|
|
/// Processing time in milliseconds
|
|
pub processing_time_ms: f64,
|
|
/// Model used
|
|
pub model: String,
|
|
}
|
|
|
|
/// Decomposed forecast components
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ForecastComponents {
|
|
/// Trend component
|
|
pub trend: Option<Vec<DataPoint>>,
|
|
/// Seasonal component
|
|
pub seasonal: Option<Vec<DataPoint>>,
|
|
/// Residual/noise component
|
|
pub residual: Option<Vec<DataPoint>>,
|
|
}
|
|
|
|
/// Model fit metrics
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct FitMetrics {
|
|
/// Akaike Information Criterion
|
|
pub aic: f64,
|
|
/// Bayesian Information Criterion
|
|
pub bic: f64,
|
|
/// Mean Absolute Error
|
|
pub mae: f64,
|
|
/// Root Mean Squared Error
|
|
pub rmse: f64,
|
|
/// Mean Absolute Percentage Error
|
|
pub mape: f64,
|
|
/// R-squared
|
|
pub r_squared: f64,
|
|
}
|
|
|
|
/// Status of the forecaster service
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ForecasterStatus {
|
|
/// Whether a model is fitted and ready
|
|
pub initialized: bool,
|
|
/// Current model type (if loaded)
|
|
pub model: Option<String>,
|
|
/// Compute device being used
|
|
pub device: String,
|
|
/// Number of forecasts generated
|
|
pub forecast_count: u64,
|
|
/// Number of data points processed
|
|
pub data_points_processed: u64,
|
|
}
|
|
|
|
/// Sample datasets for demo
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum SampleDataset {
|
|
/// Airline passengers (classic seasonal data)
|
|
AirlinePassengers,
|
|
/// Stock prices (financial data)
|
|
StockPrices,
|
|
/// Daily temperatures
|
|
Temperature,
|
|
/// Web traffic
|
|
WebTraffic,
|
|
/// Sine wave (synthetic)
|
|
SineWave,
|
|
/// Random walk (synthetic)
|
|
RandomWalk,
|
|
}
|
|
|
|
impl SampleDataset {
|
|
/// Get display name
|
|
pub fn display_name(&self) -> &'static str {
|
|
match self {
|
|
Self::AirlinePassengers => "Airline Passengers",
|
|
Self::StockPrices => "Stock Prices",
|
|
Self::Temperature => "Daily Temperature",
|
|
Self::WebTraffic => "Web Traffic",
|
|
Self::SineWave => "Sine Wave (Synthetic)",
|
|
Self::RandomWalk => "Random Walk (Synthetic)",
|
|
}
|
|
}
|
|
|
|
/// Get description
|
|
pub fn description(&self) -> &'static str {
|
|
match self {
|
|
Self::AirlinePassengers => "Monthly airline passenger counts (1949-1960)",
|
|
Self::StockPrices => "Daily stock closing prices with trend and volatility",
|
|
Self::Temperature => "Daily temperature readings with seasonal patterns",
|
|
Self::WebTraffic => "Hourly web traffic with weekly seasonality",
|
|
Self::SineWave => "Clean sine wave for testing",
|
|
Self::RandomWalk => "Random walk process for baseline comparison",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
// ========== Config Tests ==========
|
|
|
|
#[test]
|
|
fn test_config_default() {
|
|
let config = ForecastConfig::default();
|
|
assert_eq!(config.horizon, 30);
|
|
assert_eq!(config.confidence_level, 0.95);
|
|
}
|
|
|
|
#[test]
|
|
fn test_arima_config_default() {
|
|
let config = ArimaConfig::default();
|
|
assert_eq!(config.p, 1);
|
|
assert_eq!(config.d, 1);
|
|
assert_eq!(config.q, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_sarima_config_default() {
|
|
let config = SarimaConfig::default();
|
|
assert_eq!(config.order.p, 1);
|
|
assert_eq!(config.seasonal_period, 12);
|
|
}
|
|
|
|
#[test]
|
|
fn test_prophet_config_default() {
|
|
let config = ProphetConfig::default();
|
|
assert_eq!(config.growth, "linear");
|
|
assert!(config.yearly_seasonality);
|
|
assert!(config.weekly_seasonality);
|
|
assert!(!config.daily_seasonality);
|
|
assert_eq!(config.changepoint_prior_scale, 0.05);
|
|
}
|
|
|
|
#[test]
|
|
fn test_forecast_config_with_arima() {
|
|
let config = ForecastConfig {
|
|
model_type: ModelType::Arima,
|
|
horizon: 10,
|
|
confidence_level: 0.90,
|
|
arima_config: Some(ArimaConfig { p: 2, d: 1, q: 2 }),
|
|
sarima_config: None,
|
|
prophet_config: None,
|
|
use_gpu: false,
|
|
};
|
|
assert_eq!(config.horizon, 10);
|
|
assert_eq!(config.confidence_level, 0.90);
|
|
assert!(config.arima_config.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn test_forecast_config_with_sarima() {
|
|
let config = ForecastConfig {
|
|
model_type: ModelType::Sarima,
|
|
horizon: 24,
|
|
confidence_level: 0.95,
|
|
arima_config: None,
|
|
sarima_config: Some(SarimaConfig {
|
|
order: ArimaConfig { p: 1, d: 1, q: 1 },
|
|
seasonal_order: ArimaConfig { p: 1, d: 1, q: 1 },
|
|
seasonal_period: 12,
|
|
}),
|
|
prophet_config: None,
|
|
use_gpu: true,
|
|
};
|
|
assert_eq!(config.model_type, ModelType::Sarima);
|
|
assert!(config.sarima_config.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn test_forecast_config_with_prophet() {
|
|
let config = ForecastConfig {
|
|
model_type: ModelType::Prophet,
|
|
horizon: 30,
|
|
confidence_level: 0.95,
|
|
arima_config: None,
|
|
sarima_config: None,
|
|
prophet_config: Some(ProphetConfig {
|
|
growth: "logistic".to_string(),
|
|
yearly_seasonality: true,
|
|
weekly_seasonality: false,
|
|
daily_seasonality: true,
|
|
changepoint_prior_scale: 0.1,
|
|
}),
|
|
use_gpu: false,
|
|
};
|
|
assert_eq!(config.model_type, ModelType::Prophet);
|
|
let prophet = config.prophet_config.unwrap();
|
|
assert_eq!(prophet.growth, "logistic");
|
|
}
|
|
|
|
// ========== ModelType Tests ==========
|
|
|
|
#[test]
|
|
fn test_model_type_display() {
|
|
assert_eq!(ModelType::Arima.display_name(), "ARIMA");
|
|
assert_eq!(ModelType::Prophet.display_name(), "Prophet");
|
|
}
|
|
|
|
#[test]
|
|
fn test_model_type_all_display_names() {
|
|
assert_eq!(ModelType::Arima.display_name(), "ARIMA");
|
|
assert_eq!(ModelType::Sarima.display_name(), "SARIMA");
|
|
assert_eq!(ModelType::Prophet.display_name(), "Prophet");
|
|
assert_eq!(
|
|
ModelType::ExponentialSmoothing.display_name(),
|
|
"Exponential Smoothing"
|
|
);
|
|
assert_eq!(ModelType::NeuralProphet.display_name(), "Neural Prophet");
|
|
assert_eq!(ModelType::Transformer.display_name(), "Transformer");
|
|
}
|
|
|
|
#[test]
|
|
fn test_model_type_all_descriptions() {
|
|
assert!(!ModelType::Arima.description().is_empty());
|
|
assert!(!ModelType::Sarima.description().is_empty());
|
|
assert!(!ModelType::Prophet.description().is_empty());
|
|
assert!(!ModelType::ExponentialSmoothing.description().is_empty());
|
|
assert!(!ModelType::NeuralProphet.description().is_empty());
|
|
assert!(!ModelType::Transformer.description().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_model_type_equality() {
|
|
assert_eq!(ModelType::Arima, ModelType::Arima);
|
|
assert_ne!(ModelType::Arima, ModelType::Prophet);
|
|
}
|
|
|
|
#[test]
|
|
fn test_model_type_clone() {
|
|
let model = ModelType::Transformer;
|
|
let cloned = model;
|
|
assert_eq!(model, cloned);
|
|
}
|
|
|
|
// ========== DataPoint Tests ==========
|
|
|
|
#[test]
|
|
fn test_data_point_creation() {
|
|
let point = DataPoint {
|
|
timestamp: 1000.0,
|
|
value: 42.5,
|
|
};
|
|
assert_eq!(point.timestamp, 1000.0);
|
|
assert_eq!(point.value, 42.5);
|
|
}
|
|
|
|
#[test]
|
|
fn test_data_point_clone() {
|
|
let point = DataPoint {
|
|
timestamp: 100.0,
|
|
value: 50.0,
|
|
};
|
|
let cloned = point.clone();
|
|
assert_eq!(point.timestamp, cloned.timestamp);
|
|
assert_eq!(point.value, cloned.value);
|
|
}
|
|
|
|
// ========== TimeSeriesData Tests ==========
|
|
|
|
#[test]
|
|
fn test_time_series_data_creation() {
|
|
let data = TimeSeriesData {
|
|
data: vec![
|
|
DataPoint {
|
|
timestamp: 0.0,
|
|
value: 1.0,
|
|
},
|
|
DataPoint {
|
|
timestamp: 1.0,
|
|
value: 2.0,
|
|
},
|
|
],
|
|
name: Some("Test".to_string()),
|
|
frequency: Some("daily".to_string()),
|
|
};
|
|
assert_eq!(data.data.len(), 2);
|
|
assert_eq!(data.name, Some("Test".to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn test_time_series_data_empty() {
|
|
let data = TimeSeriesData {
|
|
data: vec![],
|
|
name: None,
|
|
frequency: None,
|
|
};
|
|
assert!(data.data.is_empty());
|
|
assert!(data.name.is_none());
|
|
}
|
|
|
|
// ========== ForecastResult Tests ==========
|
|
|
|
#[test]
|
|
fn test_forecast_result_structure() {
|
|
let result = ForecastResult {
|
|
forecast: vec![DataPoint {
|
|
timestamp: 10.0,
|
|
value: 100.0,
|
|
}],
|
|
lower_bound: vec![DataPoint {
|
|
timestamp: 10.0,
|
|
value: 90.0,
|
|
}],
|
|
upper_bound: vec![DataPoint {
|
|
timestamp: 10.0,
|
|
value: 110.0,
|
|
}],
|
|
fitted: vec![DataPoint {
|
|
timestamp: 0.0,
|
|
value: 95.0,
|
|
}],
|
|
components: None,
|
|
metrics: FitMetrics {
|
|
aic: 100.0,
|
|
bic: 105.0,
|
|
mae: 5.0,
|
|
rmse: 6.0,
|
|
mape: 0.05,
|
|
r_squared: 0.95,
|
|
},
|
|
processing_time_ms: 10.5,
|
|
model: "ARIMA".to_string(),
|
|
};
|
|
assert_eq!(result.forecast.len(), 1);
|
|
assert_eq!(result.model, "ARIMA");
|
|
}
|
|
|
|
#[test]
|
|
fn test_forecast_result_with_components() {
|
|
let result = ForecastResult {
|
|
forecast: vec![],
|
|
lower_bound: vec![],
|
|
upper_bound: vec![],
|
|
fitted: vec![],
|
|
components: Some(ForecastComponents {
|
|
trend: Some(vec![DataPoint {
|
|
timestamp: 0.0,
|
|
value: 10.0,
|
|
}]),
|
|
seasonal: Some(vec![DataPoint {
|
|
timestamp: 0.0,
|
|
value: 5.0,
|
|
}]),
|
|
residual: Some(vec![DataPoint {
|
|
timestamp: 0.0,
|
|
value: 1.0,
|
|
}]),
|
|
}),
|
|
metrics: FitMetrics {
|
|
aic: 0.0,
|
|
bic: 0.0,
|
|
mae: 0.0,
|
|
rmse: 0.0,
|
|
mape: 0.0,
|
|
r_squared: 0.0,
|
|
},
|
|
processing_time_ms: 0.0,
|
|
model: "Prophet".to_string(),
|
|
};
|
|
assert!(result.components.is_some());
|
|
let components = result.components.unwrap();
|
|
assert!(components.trend.is_some());
|
|
assert!(components.seasonal.is_some());
|
|
}
|
|
|
|
// ========== FitMetrics Tests ==========
|
|
|
|
#[test]
|
|
fn test_fit_metrics_creation() {
|
|
let metrics = FitMetrics {
|
|
aic: 100.0,
|
|
bic: 110.0,
|
|
mae: 5.0,
|
|
rmse: 6.5,
|
|
mape: 0.05,
|
|
r_squared: 0.92,
|
|
};
|
|
assert_eq!(metrics.aic, 100.0);
|
|
assert_eq!(metrics.r_squared, 0.92);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fit_metrics_clone() {
|
|
let metrics = FitMetrics {
|
|
aic: 100.0,
|
|
bic: 110.0,
|
|
mae: 5.0,
|
|
rmse: 6.5,
|
|
mape: 0.05,
|
|
r_squared: 0.92,
|
|
};
|
|
let cloned = metrics.clone();
|
|
assert_eq!(metrics.aic, cloned.aic);
|
|
assert_eq!(metrics.rmse, cloned.rmse);
|
|
}
|
|
|
|
// ========== ForecasterStatus Tests ==========
|
|
|
|
#[test]
|
|
fn test_forecaster_status_uninitialized() {
|
|
let status = ForecasterStatus {
|
|
initialized: false,
|
|
model: None,
|
|
device: "cpu".to_string(),
|
|
forecast_count: 0,
|
|
data_points_processed: 0,
|
|
};
|
|
assert!(!status.initialized);
|
|
assert!(status.model.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_forecaster_status_initialized() {
|
|
let status = ForecasterStatus {
|
|
initialized: true,
|
|
model: Some("ARIMA(1,1,1)".to_string()),
|
|
device: "cuda:0".to_string(),
|
|
forecast_count: 10,
|
|
data_points_processed: 1000,
|
|
};
|
|
assert!(status.initialized);
|
|
assert_eq!(status.model, Some("ARIMA(1,1,1)".to_string()));
|
|
assert_eq!(status.forecast_count, 10);
|
|
}
|
|
|
|
// ========== SampleDataset Tests ==========
|
|
|
|
#[test]
|
|
fn test_sample_dataset_display_names() {
|
|
assert_eq!(
|
|
SampleDataset::AirlinePassengers.display_name(),
|
|
"Airline Passengers"
|
|
);
|
|
assert_eq!(SampleDataset::StockPrices.display_name(), "Stock Prices");
|
|
assert_eq!(
|
|
SampleDataset::Temperature.display_name(),
|
|
"Daily Temperature"
|
|
);
|
|
assert_eq!(SampleDataset::WebTraffic.display_name(), "Web Traffic");
|
|
assert_eq!(
|
|
SampleDataset::SineWave.display_name(),
|
|
"Sine Wave (Synthetic)"
|
|
);
|
|
assert_eq!(
|
|
SampleDataset::RandomWalk.display_name(),
|
|
"Random Walk (Synthetic)"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_sample_dataset_descriptions() {
|
|
assert!(!SampleDataset::AirlinePassengers.description().is_empty());
|
|
assert!(!SampleDataset::StockPrices.description().is_empty());
|
|
assert!(!SampleDataset::Temperature.description().is_empty());
|
|
assert!(!SampleDataset::WebTraffic.description().is_empty());
|
|
assert!(!SampleDataset::SineWave.description().is_empty());
|
|
assert!(!SampleDataset::RandomWalk.description().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_sample_dataset_equality() {
|
|
assert_eq!(
|
|
SampleDataset::AirlinePassengers,
|
|
SampleDataset::AirlinePassengers
|
|
);
|
|
assert_ne!(SampleDataset::AirlinePassengers, SampleDataset::StockPrices);
|
|
}
|
|
|
|
// ========== Serialization Tests ==========
|
|
|
|
#[test]
|
|
fn test_model_type_serialization() {
|
|
let model = ModelType::Arima;
|
|
let json = serde_json::to_string(&model).unwrap();
|
|
assert_eq!(json, "\"arima\"");
|
|
}
|
|
|
|
#[test]
|
|
fn test_model_type_deserialization() {
|
|
let model: ModelType = serde_json::from_str("\"prophet\"").unwrap();
|
|
assert_eq!(model, ModelType::Prophet);
|
|
}
|
|
|
|
#[test]
|
|
fn test_data_point_serialization() {
|
|
let point = DataPoint {
|
|
timestamp: 100.0,
|
|
value: 50.0,
|
|
};
|
|
let json = serde_json::to_string(&point).unwrap();
|
|
let parsed: DataPoint = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(point.timestamp, parsed.timestamp);
|
|
assert_eq!(point.value, parsed.value);
|
|
}
|
|
|
|
#[test]
|
|
fn test_forecast_config_serialization() {
|
|
let config = ForecastConfig::default();
|
|
let json = serde_json::to_string(&config).unwrap();
|
|
let parsed: ForecastConfig = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(config.horizon, parsed.horizon);
|
|
assert_eq!(config.confidence_level, parsed.confidence_level);
|
|
}
|
|
|
|
#[test]
|
|
fn test_arima_config_serialization() {
|
|
let config = ArimaConfig { p: 2, d: 1, q: 2 };
|
|
let json = serde_json::to_string(&config).unwrap();
|
|
let parsed: ArimaConfig = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(config.p, parsed.p);
|
|
assert_eq!(config.d, parsed.d);
|
|
assert_eq!(config.q, parsed.q);
|
|
}
|
|
|
|
#[test]
|
|
fn test_sarima_config_serialization() {
|
|
let config = SarimaConfig::default();
|
|
let json = serde_json::to_string(&config).unwrap();
|
|
let parsed: SarimaConfig = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(config.seasonal_period, parsed.seasonal_period);
|
|
}
|
|
|
|
#[test]
|
|
fn test_prophet_config_serialization() {
|
|
let config = ProphetConfig::default();
|
|
let json = serde_json::to_string(&config).unwrap();
|
|
let parsed: ProphetConfig = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(config.growth, parsed.growth);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fit_metrics_serialization() {
|
|
let metrics = FitMetrics {
|
|
aic: 100.0,
|
|
bic: 110.0,
|
|
mae: 5.0,
|
|
rmse: 6.5,
|
|
mape: 0.05,
|
|
r_squared: 0.92,
|
|
};
|
|
let json = serde_json::to_string(&metrics).unwrap();
|
|
let parsed: FitMetrics = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(metrics.aic, parsed.aic);
|
|
assert_eq!(metrics.rmse, parsed.rmse);
|
|
}
|
|
|
|
#[test]
|
|
fn test_sample_dataset_serialization() {
|
|
let dataset = SampleDataset::AirlinePassengers;
|
|
let json = serde_json::to_string(&dataset).unwrap();
|
|
assert_eq!(json, "\"airline_passengers\"");
|
|
}
|
|
|
|
#[test]
|
|
fn test_sample_dataset_deserialization() {
|
|
let dataset: SampleDataset = serde_json::from_str("\"stock_prices\"").unwrap();
|
|
assert_eq!(dataset, SampleDataset::StockPrices);
|
|
}
|
|
|
|
#[test]
|
|
fn test_time_series_data_serialization() {
|
|
let data = TimeSeriesData {
|
|
data: vec![
|
|
DataPoint {
|
|
timestamp: 0.0,
|
|
value: 1.0,
|
|
},
|
|
DataPoint {
|
|
timestamp: 1.0,
|
|
value: 2.0,
|
|
},
|
|
],
|
|
name: Some("Test".to_string()),
|
|
frequency: Some("daily".to_string()),
|
|
};
|
|
let json = serde_json::to_string(&data).unwrap();
|
|
let parsed: TimeSeriesData = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(data.data.len(), parsed.data.len());
|
|
assert_eq!(data.name, parsed.name);
|
|
}
|
|
}
|