Skip to main content

torsh_series/
lib.rs

1//! Time series analysis components for ToRSh
2//!
3//! This module provides PyTorch-compatible time series models and operations,
4//! built on top of SciRS2's time series decomposition and state-space methods.
5
6pub mod anomaly;
7pub mod changepoint;
8pub mod decomposition;
9pub mod forecast;
10pub mod frequency;
11pub mod state_space;
12pub mod utils;
13
14use torsh_tensor::Tensor;
15// scirs2_series will be used when specific APIs are available
16
17/// Time series data container
18#[derive(Debug, Clone)]
19pub struct TimeSeries {
20    /// Data values (time x features)
21    pub values: Tensor,
22    /// Timestamps (optional)
23    pub timestamps: Option<Vec<f64>>,
24    /// Sampling frequency
25    pub frequency: Option<f64>,
26    /// Feature names
27    pub features: Option<Vec<String>>,
28}
29
30impl TimeSeries {
31    /// Create a new time series
32    pub fn new(values: Tensor) -> Self {
33        Self {
34            values,
35            timestamps: None,
36            frequency: None,
37            features: None,
38        }
39    }
40
41    /// Set timestamps
42    pub fn with_timestamps(mut self, timestamps: Vec<f64>) -> Self {
43        self.timestamps = Some(timestamps);
44        self
45    }
46
47    /// Set sampling frequency
48    pub fn with_frequency(mut self, frequency: f64) -> Self {
49        self.frequency = Some(frequency);
50        self
51    }
52
53    /// Set feature names
54    pub fn with_features(mut self, features: Vec<String>) -> Self {
55        self.features = Some(features);
56        self
57    }
58
59    /// Get number of time points
60    pub fn len(&self) -> usize {
61        self.values.shape().dims()[0]
62    }
63
64    /// Check if series is empty
65    pub fn is_empty(&self) -> bool {
66        self.len() == 0
67    }
68
69    /// Get number of features
70    pub fn num_features(&self) -> usize {
71        let shape = self.values.shape();
72        let dims = shape.dims();
73        if dims.len() > 1 {
74            dims[1]
75        } else {
76            1
77        }
78    }
79
80    /// Slice time series
81    pub fn slice(
82        &self,
83        start: usize,
84        end: usize,
85    ) -> Result<TimeSeries, torsh_core::error::TorshError> {
86        // Validate bounds
87        if start > end || end > self.len() {
88            return Err(torsh_core::error::TorshError::InvalidArgument(format!(
89                "Invalid slice bounds: start={}, end={}, len={}",
90                start,
91                end,
92                self.len()
93            )));
94        }
95
96        // Create a sliced tensor by extracting elements using flat indexing
97        let slice_len = end - start;
98        let num_features = self.num_features();
99
100        // Extract data for the time slice
101        let mut sliced_data = Vec::with_capacity(slice_len * num_features);
102        for t in start..end {
103            for f in 0..num_features {
104                let flat_idx = t * num_features + f;
105                let value = self.values.get_item_flat(flat_idx).map_err(|e| {
106                    torsh_core::error::TorshError::InvalidArgument(format!(
107                        "Failed to slice tensor: {}",
108                        e
109                    ))
110                })?;
111                sliced_data.push(value);
112            }
113        }
114
115        let shape = if num_features == 1 {
116            vec![slice_len]
117        } else {
118            vec![slice_len, num_features]
119        };
120        let values = Tensor::from_vec(sliced_data, &shape).map_err(|e| {
121            torsh_core::error::TorshError::InvalidArgument(format!(
122                "Failed to create sliced tensor: {}",
123                e
124            ))
125        })?;
126
127        let timestamps = self.timestamps.as_ref().map(|ts| ts[start..end].to_vec());
128
129        Ok(TimeSeries {
130            values,
131            timestamps,
132            frequency: self.frequency,
133            features: self.features.clone(),
134        })
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    use torsh_tensor::creation::*;
143
144    #[test]
145    fn test_timeseries_creation() {
146        let data = vec![1.0f32, 2.0, 3.0, 4.0, 5.0];
147        let tensor = Tensor::from_vec(data, &[5]).unwrap();
148        let ts = TimeSeries::new(tensor);
149
150        assert_eq!(ts.len(), 5);
151        assert_eq!(ts.num_features(), 1);
152        assert!(!ts.is_empty());
153    }
154
155    #[test]
156    fn test_timeseries_with_metadata() {
157        let data = vec![1.0f32, 2.0, 3.0, 4.0, 5.0];
158        let tensor = Tensor::from_vec(data, &[5]).unwrap();
159        let timestamps = vec![0.0, 1.0, 2.0, 3.0, 4.0];
160        let features = vec!["value".to_string()];
161
162        let ts = TimeSeries::new(tensor)
163            .with_timestamps(timestamps.clone())
164            .with_frequency(1.0)
165            .with_features(features.clone());
166
167        assert_eq!(ts.timestamps, Some(timestamps));
168        assert_eq!(ts.frequency, Some(1.0));
169        assert_eq!(ts.features, Some(features));
170    }
171
172    #[test]
173    fn test_timeseries_multivariate() {
174        let data = vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0];
175        let tensor = Tensor::from_vec(data, &[3, 2]).unwrap();
176        let ts = TimeSeries::new(tensor);
177
178        assert_eq!(ts.len(), 3);
179        assert_eq!(ts.num_features(), 2);
180    }
181
182    #[test]
183    fn test_timeseries_slice() {
184        let data = vec![1.0f32, 2.0, 3.0, 4.0, 5.0];
185        let tensor = Tensor::from_vec(data, &[5]).unwrap();
186        let ts = TimeSeries::new(tensor);
187
188        let sliced = ts.slice(1, 4).unwrap();
189        assert_eq!(sliced.len(), 3);
190    }
191
192    #[test]
193    fn test_empty_timeseries() {
194        let tensor = zeros(&[0]).unwrap();
195        let ts = TimeSeries::new(tensor);
196
197        assert_eq!(ts.len(), 0);
198        assert!(ts.is_empty());
199    }
200}