use crate::error::{MlError, ModelError, Result};
use scirs2_core::ndarray::{Array3, ArrayView3, s};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ForecastConfig {
pub input_features: usize,
pub hidden_dim: usize,
pub num_layers: usize,
pub forecast_horizon: usize,
pub dropout: f32,
pub bidirectional: bool,
pub sequence_length: usize,
}
impl Default for ForecastConfig {
fn default() -> Self {
Self {
input_features: 1,
hidden_dim: 64,
num_layers: 2,
forecast_horizon: 1,
dropout: 0.2,
bidirectional: false,
sequence_length: 12,
}
}
}
impl ForecastConfig {
pub fn new(
input_features: usize,
hidden_dim: usize,
num_layers: usize,
forecast_horizon: usize,
) -> Self {
Self {
input_features,
hidden_dim,
num_layers,
forecast_horizon,
..Default::default()
}
}
pub fn with_dropout(mut self, dropout: f32) -> Self {
self.dropout = dropout;
self
}
pub fn with_bidirectional(mut self, bidirectional: bool) -> Self {
self.bidirectional = bidirectional;
self
}
pub fn with_sequence_length(mut self, length: usize) -> Self {
self.sequence_length = length;
self
}
pub fn validate(&self) -> Result<()> {
if self.input_features == 0 {
return Err(MlError::InvalidConfig(
"input_features must be greater than 0".to_string(),
));
}
if self.hidden_dim == 0 {
return Err(MlError::InvalidConfig(
"hidden_dim must be greater than 0".to_string(),
));
}
if self.num_layers == 0 {
return Err(MlError::InvalidConfig(
"num_layers must be greater than 0".to_string(),
));
}
if self.forecast_horizon == 0 {
return Err(MlError::InvalidConfig(
"forecast_horizon must be greater than 0".to_string(),
));
}
if self.sequence_length == 0 {
return Err(MlError::InvalidConfig(
"sequence_length must be greater than 0".to_string(),
));
}
if !(0.0..=1.0).contains(&self.dropout) {
return Err(MlError::InvalidConfig(
"dropout must be between 0.0 and 1.0".to_string(),
));
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct ForecastResult {
pub predictions: Array3<f32>,
pub confidence_intervals: Option<(Array3<f32>, Array3<f32>)>,
}
impl ForecastResult {
pub fn new(predictions: Array3<f32>) -> Self {
Self {
predictions,
confidence_intervals: None,
}
}
pub fn with_confidence_intervals(mut self, lower: Array3<f32>, upper: Array3<f32>) -> Self {
self.confidence_intervals = Some((lower, upper));
self
}
pub fn shape(&self) -> (usize, usize, usize) {
let shape = self.predictions.shape();
(shape[0], shape[1], shape[2])
}
}
#[cfg(feature = "temporal")]
pub struct TemporalForecaster {
config: ForecastConfig,
encoder: TemporalLSTM,
}
#[cfg(feature = "temporal")]
impl TemporalForecaster {
pub fn new(config: ForecastConfig) -> Result<Self> {
config.validate()?;
let lstm_config = LSTMConfig::new(
config.input_features,
config.hidden_dim,
config.num_layers,
config.dropout,
)
.with_bidirectional(config.bidirectional);
let encoder =
TemporalLSTM::new(lstm_config).map_err(|e| ModelError::InitializationFailed {
reason: format!("Failed to create LSTM encoder: {}", e),
})?;
Ok(Self { config, encoder })
}
pub fn predict(&self, input: &Array3<f32>) -> Result<ForecastResult> {
let input_shape = input.shape();
if input_shape[2] != self.config.input_features {
return Err(MlError::InvalidConfig(format!(
"Input features mismatch: expected {}, got {}",
self.config.input_features, input_shape[2]
)));
}
let batch_size = input_shape[0];
let encoded = self
.encoder
.forward(&input.clone().into_dyn())
.map_err(|e| ModelError::InitializationFailed {
reason: format!("LSTM encoding failed: {}", e),
})?;
let encoded_shape = encoded.shape();
let seq_len = encoded_shape[1];
let hidden_dim = encoded_shape[2];
let last_hidden = encoded.slice(s![.., seq_len - 1, ..]).to_owned();
let mut predictions = Array3::<f32>::zeros((
batch_size,
self.config.forecast_horizon,
self.config.input_features,
));
for t in 0..self.config.forecast_horizon {
for b in 0..batch_size {
for f in 0..self.config.input_features {
let hidden_idx = f.min(hidden_dim - 1);
predictions[[b, t, f]] = last_hidden[[b, hidden_idx]];
}
}
}
Ok(ForecastResult::new(predictions))
}
pub fn predict_autoregressive(&self, input: &Array3<f32>) -> Result<ForecastResult> {
let input_shape = input.shape();
if input_shape[2] != self.config.input_features {
return Err(MlError::InvalidConfig(format!(
"Input features mismatch: expected {}, got {}",
self.config.input_features, input_shape[2]
)));
}
let batch_size = input_shape[0];
let seq_len = input_shape[1];
let mut current_seq = input.clone();
let mut all_predictions = Array3::<f32>::zeros((
batch_size,
self.config.forecast_horizon,
self.config.input_features,
));
for t in 0..self.config.forecast_horizon {
let encoded = self
.encoder
.forward(¤t_seq.clone().into_dyn())
.map_err(|e| ModelError::InitializationFailed {
reason: format!("LSTM encoding failed at step {}: {}", t, e),
})?;
let encoded_shape = encoded.shape();
let current_seq_len = encoded_shape[1];
let hidden_dim = encoded_shape[2];
let last_hidden = encoded.slice(s![.., current_seq_len - 1, ..]).to_owned();
for b in 0..batch_size {
for f in 0..self.config.input_features {
let hidden_idx = f.min(hidden_dim - 1);
all_predictions[[b, t, f]] = last_hidden[[b, hidden_idx]];
}
}
if t < self.config.forecast_horizon - 1 {
let mut new_seq =
Array3::<f32>::zeros((batch_size, seq_len, self.config.input_features));
for b in 0..batch_size {
for s in 1..seq_len {
for f in 0..self.config.input_features {
new_seq[[b, s - 1, f]] = current_seq[[b, s, f]];
}
}
for f in 0..self.config.input_features {
new_seq[[b, seq_len - 1, f]] = all_predictions[[b, t, f]];
}
}
current_seq = new_seq;
}
}
Ok(ForecastResult::new(all_predictions))
}
pub fn config(&self) -> &ForecastConfig {
&self.config
}
}
#[cfg(not(feature = "temporal"))]
pub struct TemporalForecaster {
config: ForecastConfig,
}
#[cfg(not(feature = "temporal"))]
impl TemporalForecaster {
pub fn new(config: ForecastConfig) -> Result<Self> {
config.validate()?;
Err(MlError::FeatureNotAvailable {
feature: "Temporal forecasting".to_string(),
flag: "temporal".to_string(),
})
}
pub fn config(&self) -> &ForecastConfig {
&self.config
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_forecast_config_default() {
let config = ForecastConfig::default();
assert_eq!(config.input_features, 1);
assert_eq!(config.hidden_dim, 64);
assert_eq!(config.num_layers, 2);
assert_eq!(config.forecast_horizon, 1);
assert_eq!(config.sequence_length, 12);
}
#[test]
fn test_forecast_config_new() {
let config = ForecastConfig::new(10, 128, 3, 6);
assert_eq!(config.input_features, 10);
assert_eq!(config.hidden_dim, 128);
assert_eq!(config.num_layers, 3);
assert_eq!(config.forecast_horizon, 6);
}
#[test]
fn test_forecast_config_validation() {
let valid_config = ForecastConfig::new(10, 128, 2, 6);
assert!(valid_config.validate().is_ok());
let invalid_config = ForecastConfig::new(0, 128, 2, 6);
assert!(invalid_config.validate().is_err());
let invalid_horizon = ForecastConfig::new(10, 128, 2, 0);
assert!(invalid_horizon.validate().is_err());
}
#[test]
fn test_forecast_result() {
let predictions = Array3::<f32>::zeros((2, 6, 1));
let result = ForecastResult::new(predictions);
assert_eq!(result.shape(), (2, 6, 1));
assert!(result.confidence_intervals.is_none());
}
#[cfg(feature = "temporal")]
#[test]
fn test_temporal_forecaster_creation() {
let config = ForecastConfig::new(1, 64, 2, 6);
let forecaster = TemporalForecaster::new(config);
assert!(forecaster.is_ok());
}
#[cfg(feature = "temporal")]
#[test]
fn test_temporal_forecaster_predict() {
let config = ForecastConfig::new(1, 64, 2, 6);
let forecaster = TemporalForecaster::new(config).expect("Failed to create forecaster");
let input = Array3::<f32>::zeros((2, 12, 1));
let result = forecaster.predict(&input);
assert!(result.is_ok());
let result = result.expect("Prediction failed");
assert_eq!(result.shape(), (2, 6, 1));
}
}