Files
rustytorch/demos/timeseries-shared/src/lib.rs
T
2026-03-04 00:08:42 +00:00

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);
}
}