//! 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, /// SARIMA config (if applicable) pub sarima_config: Option, /// Prophet config (if applicable) pub prophet_config: Option, /// 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, /// Optional column name pub name: Option, /// Data frequency (e.g., "daily", "monthly", "hourly") pub frequency: Option, } /// Forecast result with predictions and confidence intervals #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ForecastResult { /// Forecasted values (mean) pub forecast: Vec, /// Lower confidence bound pub lower_bound: Vec, /// Upper confidence bound pub upper_bound: Vec, /// Fitted values for historical data pub fitted: Vec, /// Forecast components (if available) pub components: Option, /// 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>, /// Seasonal component pub seasonal: Option>, /// Residual/noise component pub residual: Option>, } /// 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, /// 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); } }