ruqu_qear/
timeseries.rs

1//! Time series processing for Quantum Echo-Attention Reservoir.
2//!
3//! This module provides utilities for encoding, windowing, and processing
4//! time series data through the quantum reservoir.
5
6use crate::error::{QearError, QearResult};
7use crate::features::FeatureExtractor;
8use crate::fusion::AttentionFusion;
9use crate::reservoir::{QuantumReservoir, ReservoirConfig};
10use ndarray::{Array1, Array2, Axis};
11
12#[cfg(feature = "serde")]
13use serde::{Deserialize, Serialize};
14
15/// Configuration for time series processing.
16#[derive(Debug, Clone)]
17#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
18pub struct TimeSeriesConfig {
19    /// Input dimension of the time series.
20    pub input_dim: usize,
21    /// Window size for sliding window processing.
22    pub window_size: usize,
23    /// Step size for sliding window (stride).
24    pub step_size: usize,
25    /// Horizon for forecasting.
26    pub forecast_horizon: usize,
27    /// Whether to normalize inputs.
28    pub normalize: bool,
29    /// Encoding method for input.
30    pub encoding: EncodingMethod,
31}
32
33impl Default for TimeSeriesConfig {
34    fn default() -> Self {
35        Self {
36            input_dim: 1,
37            window_size: 50,
38            step_size: 1,
39            forecast_horizon: 1,
40            normalize: true,
41            encoding: EncodingMethod::Direct,
42        }
43    }
44}
45
46impl TimeSeriesConfig {
47    /// Create a new time series configuration.
48    pub fn new(input_dim: usize, window_size: usize) -> Self {
49        Self {
50            input_dim,
51            window_size,
52            ..Default::default()
53        }
54    }
55
56    /// Set the forecast horizon.
57    pub fn with_forecast_horizon(mut self, horizon: usize) -> Self {
58        self.forecast_horizon = horizon;
59        self
60    }
61
62    /// Set the step size.
63    pub fn with_step_size(mut self, step_size: usize) -> Self {
64        self.step_size = step_size;
65        self
66    }
67
68    /// Set the encoding method.
69    pub fn with_encoding(mut self, encoding: EncodingMethod) -> Self {
70        self.encoding = encoding;
71        self
72    }
73
74    /// Set normalization.
75    pub fn with_normalize(mut self, normalize: bool) -> Self {
76        self.normalize = normalize;
77        self
78    }
79
80    /// Validate the configuration.
81    pub fn validate(&self) -> QearResult<()> {
82        if self.input_dim == 0 {
83            return Err(QearError::invalid_parameter(
84                "input_dim",
85                "must be greater than 0",
86            ));
87        }
88        if self.window_size == 0 {
89            return Err(QearError::invalid_parameter(
90                "window_size",
91                "must be greater than 0",
92            ));
93        }
94        if self.step_size == 0 {
95            return Err(QearError::invalid_parameter(
96                "step_size",
97                "must be greater than 0",
98            ));
99        }
100        Ok(())
101    }
102}
103
104/// Encoding method for time series input.
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
107pub enum EncodingMethod {
108    /// Direct input (no transformation).
109    Direct,
110    /// Phase encoding (map to angles).
111    Phase,
112    /// Amplitude encoding (map to amplitudes).
113    Amplitude,
114    /// Binary encoding (threshold).
115    Binary,
116    /// Fourier encoding (basis expansion).
117    Fourier,
118}
119
120/// Input encoder for time series data.
121#[derive(Debug, Clone)]
122#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
123pub struct InputEncoder {
124    /// Encoding method.
125    method: EncodingMethod,
126    /// Output dimension (for expanded encodings).
127    output_dim: usize,
128    /// Normalization statistics.
129    norm_mean: Option<Array1<f64>>,
130    norm_std: Option<Array1<f64>>,
131}
132
133impl InputEncoder {
134    /// Create a new input encoder.
135    pub fn new(method: EncodingMethod, output_dim: usize) -> Self {
136        Self {
137            method,
138            output_dim,
139            norm_mean: None,
140            norm_std: None,
141        }
142    }
143
144    /// Fit normalization statistics from data.
145    pub fn fit(&mut self, data: &Array2<f64>) {
146        let mean = data.mean_axis(Axis(0)).unwrap();
147        let std = data.std_axis(Axis(0), 0.0);
148
149        // Avoid division by zero
150        let std = std.mapv(|s| if s < 1e-10 { 1.0 } else { s });
151
152        self.norm_mean = Some(mean);
153        self.norm_std = Some(std);
154    }
155
156    /// Normalize data using fitted statistics.
157    pub fn normalize(&self, data: &Array2<f64>) -> QearResult<Array2<f64>> {
158        match (&self.norm_mean, &self.norm_std) {
159            (Some(mean), Some(std)) => {
160                let mut normalized = data.clone();
161                for mut row in normalized.rows_mut() {
162                    for (j, val) in row.iter_mut().enumerate() {
163                        if j < mean.len() {
164                            *val = (*val - mean[j]) / std[j];
165                        }
166                    }
167                }
168                Ok(normalized)
169            }
170            _ => Ok(data.clone()),
171        }
172    }
173
174    /// Encode a single sample.
175    pub fn encode(&self, input: &Array1<f64>) -> QearResult<Array1<f64>> {
176        match self.method {
177            EncodingMethod::Direct => Ok(input.clone()),
178            EncodingMethod::Phase => self.phase_encode(input),
179            EncodingMethod::Amplitude => self.amplitude_encode(input),
180            EncodingMethod::Binary => self.binary_encode(input),
181            EncodingMethod::Fourier => self.fourier_encode(input),
182        }
183    }
184
185    /// Encode a batch of samples.
186    pub fn encode_batch(&self, inputs: &Array2<f64>) -> QearResult<Array2<f64>> {
187        let n_samples = inputs.nrows();
188        let encoded_dim = self.encoded_dim(inputs.ncols());
189
190        let mut result = Array2::zeros((n_samples, encoded_dim));
191
192        for i in 0..n_samples {
193            let input = inputs.row(i).to_owned();
194            let encoded = self.encode(&input)?;
195            for j in 0..encoded_dim {
196                result[[i, j]] = encoded[j];
197            }
198        }
199
200        Ok(result)
201    }
202
203    /// Get the encoded dimension for a given input dimension.
204    pub fn encoded_dim(&self, input_dim: usize) -> usize {
205        match self.method {
206            EncodingMethod::Direct => input_dim,
207            EncodingMethod::Phase => input_dim * 2, // sin and cos
208            EncodingMethod::Amplitude => input_dim,
209            EncodingMethod::Binary => input_dim,
210            EncodingMethod::Fourier => self.output_dim,
211        }
212    }
213
214    /// Phase encoding: map values to angles.
215    fn phase_encode(&self, input: &Array1<f64>) -> QearResult<Array1<f64>> {
216        let n = input.len();
217        let mut encoded = Array1::zeros(n * 2);
218
219        for i in 0..n {
220            let angle = input[i] * std::f64::consts::PI;
221            encoded[2 * i] = angle.sin();
222            encoded[2 * i + 1] = angle.cos();
223        }
224
225        Ok(encoded)
226    }
227
228    /// Amplitude encoding: normalize to unit norm.
229    fn amplitude_encode(&self, input: &Array1<f64>) -> QearResult<Array1<f64>> {
230        let norm = input.iter().map(|x| x * x).sum::<f64>().sqrt();
231        if norm < 1e-10 {
232            Ok(input.clone())
233        } else {
234            Ok(input / norm)
235        }
236    }
237
238    /// Binary encoding: threshold at 0.
239    fn binary_encode(&self, input: &Array1<f64>) -> QearResult<Array1<f64>> {
240        Ok(input.mapv(|x| if x > 0.0 { 1.0 } else { 0.0 }))
241    }
242
243    /// Fourier encoding: expand to Fourier basis.
244    fn fourier_encode(&self, input: &Array1<f64>) -> QearResult<Array1<f64>> {
245        let n = input.len();
246        let output_dim = self.output_dim;
247        let n_frequencies = output_dim / (2 * n);
248
249        let mut encoded = Array1::zeros(output_dim);
250        let mut idx = 0;
251
252        for i in 0..n {
253            for k in 1..=n_frequencies {
254                let angle = input[i] * k as f64 * std::f64::consts::PI;
255                if idx < output_dim {
256                    encoded[idx] = angle.sin();
257                    idx += 1;
258                }
259                if idx < output_dim {
260                    encoded[idx] = angle.cos();
261                    idx += 1;
262                }
263            }
264        }
265
266        Ok(encoded)
267    }
268
269    /// Get the encoding method.
270    pub fn method(&self) -> EncodingMethod {
271        self.method
272    }
273}
274
275/// Sliding window processor for time series.
276#[derive(Debug, Clone)]
277#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
278pub struct SlidingWindow {
279    /// Window size.
280    window_size: usize,
281    /// Step size (stride).
282    step_size: usize,
283    /// Forecast horizon.
284    forecast_horizon: usize,
285}
286
287impl SlidingWindow {
288    /// Create a new sliding window processor.
289    pub fn new(window_size: usize, step_size: usize, forecast_horizon: usize) -> Self {
290        Self {
291            window_size,
292            step_size,
293            forecast_horizon,
294        }
295    }
296
297    /// Create windows from a time series.
298    ///
299    /// Returns (windows, targets) where:
300    /// - windows: (n_windows, window_size, input_dim)
301    /// - targets: (n_windows, forecast_horizon, input_dim)
302    pub fn create_windows(
303        &self,
304        data: &Array2<f64>,
305    ) -> QearResult<(Vec<Array2<f64>>, Vec<Array2<f64>>)> {
306        let n_samples = data.nrows();
307        let input_dim = data.ncols();
308
309        let total_length = self.window_size + self.forecast_horizon;
310        if n_samples < total_length {
311            return Err(QearError::insufficient_data(total_length, n_samples));
312        }
313
314        let n_windows = (n_samples - total_length) / self.step_size + 1;
315        let mut windows = Vec::with_capacity(n_windows);
316        let mut targets = Vec::with_capacity(n_windows);
317
318        for i in 0..n_windows {
319            let start = i * self.step_size;
320            let window_end = start + self.window_size;
321            let _target_end = window_end + self.forecast_horizon;
322
323            // Extract window
324            let mut window = Array2::zeros((self.window_size, input_dim));
325            for t in 0..self.window_size {
326                for d in 0..input_dim {
327                    window[[t, d]] = data[[start + t, d]];
328                }
329            }
330            windows.push(window);
331
332            // Extract target
333            let mut target = Array2::zeros((self.forecast_horizon, input_dim));
334            for t in 0..self.forecast_horizon {
335                for d in 0..input_dim {
336                    target[[t, d]] = data[[window_end + t, d]];
337                }
338            }
339            targets.push(target);
340        }
341
342        Ok((windows, targets))
343    }
344
345    /// Create windows without targets (for inference).
346    pub fn create_inference_windows(&self, data: &Array2<f64>) -> QearResult<Vec<Array2<f64>>> {
347        let n_samples = data.nrows();
348        let input_dim = data.ncols();
349
350        if n_samples < self.window_size {
351            return Err(QearError::insufficient_data(self.window_size, n_samples));
352        }
353
354        let n_windows = (n_samples - self.window_size) / self.step_size + 1;
355        let mut windows = Vec::with_capacity(n_windows);
356
357        for i in 0..n_windows {
358            let start = i * self.step_size;
359
360            let mut window = Array2::zeros((self.window_size, input_dim));
361            for t in 0..self.window_size {
362                for d in 0..input_dim {
363                    window[[t, d]] = data[[start + t, d]];
364                }
365            }
366            windows.push(window);
367        }
368
369        Ok(windows)
370    }
371
372    /// Get window size.
373    pub fn window_size(&self) -> usize {
374        self.window_size
375    }
376
377    /// Get step size.
378    pub fn step_size(&self) -> usize {
379        self.step_size
380    }
381
382    /// Get forecast horizon.
383    pub fn forecast_horizon(&self) -> usize {
384        self.forecast_horizon
385    }
386}
387
388/// Time series processor combining reservoir, attention, and features.
389#[derive(Debug)]
390pub struct TimeSeriesProcessor {
391    /// Configuration.
392    config: TimeSeriesConfig,
393    /// Quantum reservoir.
394    reservoir: QuantumReservoir,
395    /// Attention fusion (optional).
396    attention: Option<AttentionFusion>,
397    /// Feature extractor.
398    feature_extractor: FeatureExtractor,
399    /// Input encoder.
400    encoder: InputEncoder,
401    /// Sliding window processor.
402    window: SlidingWindow,
403}
404
405impl TimeSeriesProcessor {
406    /// Create a new time series processor.
407    pub fn new(
408        ts_config: TimeSeriesConfig,
409        reservoir_config: ReservoirConfig,
410    ) -> QearResult<Self> {
411        ts_config.validate()?;
412
413        let reservoir = QuantumReservoir::new(reservoir_config)?;
414        let feature_extractor = FeatureExtractor::default_extractor();
415        let encoder = InputEncoder::new(ts_config.encoding, ts_config.input_dim * 8);
416        let window = SlidingWindow::new(
417            ts_config.window_size,
418            ts_config.step_size,
419            ts_config.forecast_horizon,
420        );
421
422        Ok(Self {
423            config: ts_config,
424            reservoir,
425            attention: None,
426            feature_extractor,
427            encoder,
428            window,
429        })
430    }
431
432    /// Enable attention fusion.
433    pub fn with_attention(mut self, attention: AttentionFusion) -> Self {
434        self.attention = Some(attention);
435        self
436    }
437
438    /// Process a single window through the reservoir.
439    pub fn process_window(&mut self, window: &Array2<f64>) -> QearResult<Array2<f64>> {
440        // Encode input
441        let encoded = self.encoder.encode_batch(window)?;
442
443        // Run through reservoir
444        self.reservoir.reset();
445        let states = self.reservoir.run(&encoded)?;
446
447        // Apply attention if enabled
448        let processed = if let Some(ref mut attention) = self.attention {
449            attention.forward(&states, &encoded)?
450        } else {
451            states
452        };
453
454        // Extract features
455        let features = self.feature_extractor.extract(&processed)?;
456
457        Ok(features)
458    }
459
460    /// Process multiple windows in batch.
461    pub fn process_batch(&mut self, windows: &[Array2<f64>]) -> QearResult<Vec<Array2<f64>>> {
462        let mut results = Vec::with_capacity(windows.len());
463
464        for window in windows {
465            let features = self.process_window(window)?;
466            results.push(features);
467        }
468
469        Ok(results)
470    }
471
472    /// Prepare data for training.
473    pub fn prepare_training_data(
474        &mut self,
475        data: &Array2<f64>,
476    ) -> QearResult<(Vec<Array2<f64>>, Vec<Array2<f64>>)> {
477        // Create windows
478        let (windows, targets) = self.window.create_windows(data)?;
479
480        // Process each window
481        let features = self.process_batch(&windows)?;
482
483        Ok((features, targets))
484    }
485
486    /// Get the configuration.
487    pub fn config(&self) -> &TimeSeriesConfig {
488        &self.config
489    }
490
491    /// Get a reference to the reservoir.
492    pub fn reservoir(&self) -> &QuantumReservoir {
493        &self.reservoir
494    }
495
496    /// Get a mutable reference to the reservoir.
497    pub fn reservoir_mut(&mut self) -> &mut QuantumReservoir {
498        &mut self.reservoir
499    }
500
501    /// Get the feature extractor.
502    pub fn feature_extractor(&self) -> &FeatureExtractor {
503        &self.feature_extractor
504    }
505
506    /// Get the window processor.
507    pub fn window(&self) -> &SlidingWindow {
508        &self.window
509    }
510}
511
512/// Prediction head for forecasting.
513#[derive(Debug, Clone)]
514#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
515pub struct PredictionHead {
516    /// Output dimension.
517    output_dim: usize,
518    /// Forecast horizon.
519    horizon: usize,
520    /// Weights (if trained).
521    weights: Option<Array2<f64>>,
522    /// Bias.
523    bias: Option<Array1<f64>>,
524}
525
526impl PredictionHead {
527    /// Create a new prediction head.
528    pub fn new(output_dim: usize, horizon: usize) -> Self {
529        Self {
530            output_dim,
531            horizon,
532            weights: None,
533            bias: None,
534        }
535    }
536
537    /// Initialize weights for given feature dimension.
538    pub fn initialize(&mut self, feature_dim: usize) {
539        let total_output = self.output_dim * self.horizon;
540
541        // Xavier initialization
542        let scale = (2.0 / (feature_dim + total_output) as f64).sqrt();
543        let mut rng = rand::thread_rng();
544        use rand::Rng;
545
546        let mut weights = Array2::zeros((total_output, feature_dim));
547        for i in 0..total_output {
548            for j in 0..feature_dim {
549                weights[[i, j]] = (rng.gen::<f64>() - 0.5) * 2.0 * scale;
550            }
551        }
552
553        self.weights = Some(weights);
554        self.bias = Some(Array1::zeros(total_output));
555    }
556
557    /// Forward pass: features -> predictions.
558    pub fn forward(&self, features: &Array1<f64>) -> QearResult<Array2<f64>> {
559        let weights = self.weights.as_ref().ok_or_else(|| {
560            QearError::not_trained("Prediction head not initialized")
561        })?;
562        let bias = self.bias.as_ref().unwrap();
563
564        if features.len() != weights.ncols() {
565            return Err(QearError::dimension_mismatch(weights.ncols(), features.len()));
566        }
567
568        // Linear transformation
569        let output = weights.dot(features) + bias;
570
571        // Reshape to (horizon, output_dim)
572        let mut predictions = Array2::zeros((self.horizon, self.output_dim));
573        for h in 0..self.horizon {
574            for d in 0..self.output_dim {
575                predictions[[h, d]] = output[h * self.output_dim + d];
576            }
577        }
578
579        Ok(predictions)
580    }
581
582    /// Set weights (for training).
583    pub fn set_weights(&mut self, weights: Array2<f64>, bias: Array1<f64>) {
584        self.weights = Some(weights);
585        self.bias = Some(bias);
586    }
587
588    /// Get weights.
589    pub fn weights(&self) -> Option<&Array2<f64>> {
590        self.weights.as_ref()
591    }
592
593    /// Get bias.
594    pub fn bias(&self) -> Option<&Array1<f64>> {
595        self.bias.as_ref()
596    }
597
598    /// Check if initialized.
599    pub fn is_initialized(&self) -> bool {
600        self.weights.is_some()
601    }
602}
603
604#[cfg(test)]
605mod tests {
606    use super::*;
607
608    #[test]
609    fn test_timeseries_config_default() {
610        let config = TimeSeriesConfig::default();
611        assert_eq!(config.input_dim, 1);
612        assert_eq!(config.window_size, 50);
613    }
614
615    #[test]
616    fn test_timeseries_config_validation() {
617        let config = TimeSeriesConfig::new(0, 50);
618        assert!(config.validate().is_err());
619
620        let config = TimeSeriesConfig::new(1, 50);
621        assert!(config.validate().is_ok());
622    }
623
624    #[test]
625    fn test_input_encoder_direct() {
626        let encoder = InputEncoder::new(EncodingMethod::Direct, 10);
627        let input = Array1::from_vec(vec![1.0, 2.0, 3.0]);
628        let encoded = encoder.encode(&input).unwrap();
629        assert_eq!(encoded.len(), 3);
630    }
631
632    #[test]
633    fn test_input_encoder_phase() {
634        let encoder = InputEncoder::new(EncodingMethod::Phase, 10);
635        let input = Array1::from_vec(vec![0.5, 0.0]);
636        let encoded = encoder.encode(&input).unwrap();
637        assert_eq!(encoded.len(), 4); // 2 * input_dim
638    }
639
640    #[test]
641    fn test_input_encoder_amplitude() {
642        let encoder = InputEncoder::new(EncodingMethod::Amplitude, 10);
643        let input = Array1::from_vec(vec![3.0, 4.0]);
644        let encoded = encoder.encode(&input).unwrap();
645
646        // Should be unit norm
647        let norm: f64 = encoded.iter().map(|x| x * x).sum::<f64>().sqrt();
648        assert!((norm - 1.0).abs() < 1e-10);
649    }
650
651    #[test]
652    fn test_sliding_window_creation() {
653        let window = SlidingWindow::new(10, 2, 3);
654        let data = Array2::from_shape_fn((30, 2), |(i, j)| (i + j) as f64);
655
656        let (windows, targets) = window.create_windows(&data).unwrap();
657
658        // (30 - 10 - 3) / 2 + 1 = 9 windows
659        assert_eq!(windows.len(), 9);
660        assert_eq!(targets.len(), 9);
661        assert_eq!(windows[0].nrows(), 10);
662        assert_eq!(targets[0].nrows(), 3);
663    }
664
665    #[test]
666    fn test_sliding_window_insufficient_data() {
667        let window = SlidingWindow::new(10, 1, 3);
668        let data = Array2::from_shape_fn((10, 2), |(i, j)| (i + j) as f64);
669
670        let result = window.create_windows(&data);
671        assert!(result.is_err());
672    }
673
674    #[test]
675    fn test_time_series_processor_creation() {
676        let ts_config = TimeSeriesConfig::new(3, 20);
677        let reservoir_config = ReservoirConfig::new(4).with_seed(42);
678
679        let processor = TimeSeriesProcessor::new(ts_config, reservoir_config);
680        assert!(processor.is_ok());
681    }
682
683    #[test]
684    fn test_time_series_processor_window() {
685        let ts_config = TimeSeriesConfig::new(3, 20);
686        let reservoir_config = ReservoirConfig::new(4).with_seed(42);
687        let mut processor = TimeSeriesProcessor::new(ts_config, reservoir_config).unwrap();
688
689        let window = Array2::from_shape_fn((20, 3), |(i, j)| ((i + j) as f64 / 23.0).sin());
690        let features = processor.process_window(&window).unwrap();
691
692        assert_eq!(features.nrows(), 20);
693        assert!(features.ncols() > 0);
694    }
695
696    #[test]
697    fn test_prediction_head() {
698        let mut head = PredictionHead::new(2, 3);
699        head.initialize(10);
700
701        let features = Array1::from_vec((0..10).map(|i| i as f64 / 10.0).collect());
702        let predictions = head.forward(&features).unwrap();
703
704        assert_eq!(predictions.nrows(), 3); // horizon
705        assert_eq!(predictions.ncols(), 2); // output_dim
706    }
707
708    #[test]
709    fn test_encoder_normalization() {
710        let mut encoder = InputEncoder::new(EncodingMethod::Direct, 10);
711        let data = Array2::from_shape_fn((100, 3), |(i, j)| (i * 10 + j) as f64);
712
713        encoder.fit(&data);
714        let normalized = encoder.normalize(&data).unwrap();
715
716        // Check that normalized data has approximately zero mean
717        let mean = normalized.mean_axis(Axis(0)).unwrap();
718        for m in mean.iter() {
719            assert!(m.abs() < 1e-10);
720        }
721    }
722
723    #[test]
724    fn test_inference_windows() {
725        let window = SlidingWindow::new(10, 5, 1);
726        let data = Array2::from_shape_fn((50, 2), |(i, j)| (i + j) as f64);
727
728        let windows = window.create_inference_windows(&data).unwrap();
729
730        // (50 - 10) / 5 + 1 = 9 windows
731        assert_eq!(windows.len(), 9);
732    }
733}