use crate::TimeSeries;
use torsh_tensor::{creation::zeros, Tensor};
pub struct SSA {
window_length: usize,
num_components: usize,
}
impl SSA {
pub fn new(window_length: usize, num_components: usize) -> Self {
Self {
window_length,
num_components,
}
}
pub fn fit(&self, series: &TimeSeries) -> Vec<Tensor> {
let data = series.values.to_vec().expect("conversion should succeed");
let n = data.len();
if self.window_length >= n || self.window_length < 2 {
return vec![series.values.clone()];
}
let k = n - self.window_length + 1;
let mut trajectory_matrix = vec![vec![0.0f32; k]; self.window_length];
for i in 0..self.window_length {
for j in 0..k {
trajectory_matrix[i][j] = data[i + j];
}
}
let components = self.compute_ssa_components(&trajectory_matrix, k);
self.reconstruct_components(&components, n, &series.values.device())
}
fn compute_ssa_components(&self, trajectory_matrix: &[Vec<f32>], k: usize) -> Vec<Vec<f32>> {
let m = self.window_length;
let mut covariance = vec![vec![0.0f32; m]; m];
for i in 0..m {
for j in 0..m {
let mut sum = 0.0f32;
for l in 0..k {
sum += trajectory_matrix[i][l] * trajectory_matrix[j][l];
}
covariance[i][j] = sum / k as f32;
}
}
let mut components = Vec::new();
let num_components = self.num_components.min(m);
for _ in 0..num_components {
let eigenvector = self.power_iteration(&covariance);
components.push(eigenvector);
for i in 0..m {
covariance[i][i] *= 0.9;
}
}
components
}
fn power_iteration(&self, matrix: &[Vec<f32>]) -> Vec<f32> {
let n = matrix.len();
let mut vector = vec![1.0f32; n];
let iterations = 100;
for _ in 0..iterations {
let mut new_vector = vec![0.0f32; n];
for i in 0..n {
for j in 0..n {
new_vector[i] += matrix[i][j] * vector[j];
}
}
let norm = new_vector.iter().map(|&x| x * x).sum::<f32>().sqrt();
if norm > 1e-10 {
for i in 0..n {
new_vector[i] /= norm;
}
}
vector = new_vector;
}
vector
}
fn reconstruct_components(
&self,
eigenvectors: &[Vec<f32>],
n: usize,
_device: &torsh_core::device::DeviceType,
) -> Vec<Tensor> {
let mut reconstructed_components = Vec::new();
for eigenvector in eigenvectors {
let mut reconstructed = vec![0.0f32; n];
for i in 0..n {
let mut sum = 0.0f32;
let mut count = 0;
for j in 0..self.window_length {
if i >= j && i - j < n - self.window_length + 1 {
sum += eigenvector[j];
count += 1;
}
}
if count > 0 {
reconstructed[i] = sum / count as f32;
}
}
let tensor =
Tensor::from_vec(reconstructed, &[n]).expect("tensor creation should succeed");
reconstructed_components.push(tensor);
}
reconstructed_components
}
pub fn forecast(&self, series: &TimeSeries, steps: usize) -> Tensor {
let components = self.fit(series);
if components.is_empty() {
return zeros(&[steps, series.num_features()]).expect("tensor creation should succeed");
}
let main_component = &components[0];
let data = main_component.to_vec().expect("conversion should succeed");
let n = data.len();
if n < 2 {
return zeros(&[steps, series.num_features()]).expect("tensor creation should succeed");
}
let window = 5.min(n);
let mut forecast = Vec::with_capacity(steps);
let trend = if window > 1 {
(data[n - 1] - data[n - window]) / (window - 1) as f32
} else {
0.0f32
};
let last_value = data[n - 1];
for i in 0..steps {
forecast.push(last_value + trend * (i + 1) as f32);
}
Tensor::from_vec(forecast, &[steps]).expect("tensor creation should succeed")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::TimeSeries;
fn create_test_series() -> TimeSeries {
let mut data = Vec::new();
for i in 0..50 {
let trend = i as f32 * 0.1;
let seasonal = (i as f32 * 2.0 * std::f32::consts::PI / 12.0).sin() * 2.0;
let noise = 0.1;
data.push(trend + seasonal + noise);
}
let tensor = Tensor::from_vec(data, &[50]).expect("Tensor should succeed");
TimeSeries::new(tensor)
}
#[test]
fn test_ssa_decomposition() {
let series = create_test_series();
let ssa = SSA::new(20, 10); let components = ssa.fit(&series);
assert!(!components.is_empty());
}
#[test]
fn test_ssa_forecasting() {
let series = create_test_series();
let ssa = SSA::new(20, 10); let forecast = ssa.forecast(&series, 5);
assert_eq!(forecast.shape().dims()[0], 5);
}
}