Skip to main content

sklears_svm/
time_series.rs

1//! Time series kernels and utilities for SVM
2//!
3//! This module provides specialized kernels and utilities for time series classification
4//! and regression using Support Vector Machines. It includes:
5//! - Dynamic Time Warping (DTW) kernels
6//! - Global Alignment Kernels (GAK)
7//! - Auto-Regressive kernels
8//! - Sequence kernels for temporal pattern recognition
9//! - Streaming SVM for online time series learning
10
11#[cfg(feature = "parallel")]
12#[allow(unused_imports)]
13use rayon::prelude::*;
14use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
15use std::collections::HashMap;
16use std::collections::VecDeque;
17
18use crate::kernels::Kernel;
19use crate::svc::SVC;
20use sklears_core::error::{Result, SklearsError};
21use sklears_core::traits::{Fit, Predict, Trained};
22
23/// Dynamic Time Warping (DTW) kernel for time series
24///
25/// DTW is a technique for measuring similarity between two temporal sequences
26/// that may vary in speed. The DTW kernel computes optimal alignment between
27/// two time series and measures their similarity.
28///
29/// K_DTW(x, y) = exp(-γ * DTW(x, y))
30///
31/// where DTW(x, y) is the dynamic time warping distance between sequences x and y.
32///
33/// References:
34/// - Cuturi, M. (2011). Fast global alignment kernels.
35/// - Sakoe, H. & Chiba, S. (1978). Dynamic programming algorithm optimization for spoken word recognition.
36#[derive(Debug, Clone)]
37pub struct DynamicTimeWarpingKernel {
38    /// Bandwidth constraint for DTW (Sakoe-Chiba band)
39    pub bandwidth: Option<usize>,
40    /// Gamma parameter for RBF transformation
41    pub gamma: f64,
42    /// Distance metric for point-wise comparison
43    pub distance_metric: DistanceMetric,
44    /// Step pattern for DTW
45    pub step_pattern: StepPattern,
46    /// Whether to normalize by sequence length
47    pub normalize: bool,
48}
49
50/// Distance metrics for DTW
51#[derive(Debug, Clone)]
52pub enum DistanceMetric {
53    /// Euclidean distance
54    Euclidean,
55    /// Manhattan distance
56    Manhattan,
57    /// Squared Euclidean distance
58    SquaredEuclidean,
59    /// Cosine distance
60    Cosine,
61}
62
63/// Step patterns for DTW
64#[derive(Debug, Clone)]
65pub enum StepPattern {
66    /// Symmetric step pattern
67    Symmetric,
68    /// Asymmetric step pattern
69    Asymmetric,
70    /// Type IVc step pattern
71    TypeIVc,
72}
73
74impl Default for DynamicTimeWarpingKernel {
75    fn default() -> Self {
76        Self {
77            bandwidth: None,
78            gamma: 1.0,
79            distance_metric: DistanceMetric::Euclidean,
80            step_pattern: StepPattern::Symmetric,
81            normalize: true,
82        }
83    }
84}
85
86impl DynamicTimeWarpingKernel {
87    /// Create a new DTW kernel
88    pub fn new(gamma: f64) -> Self {
89        Self {
90            gamma,
91            ..Default::default()
92        }
93    }
94
95    /// Set bandwidth constraint
96    pub fn with_bandwidth(mut self, bandwidth: Option<usize>) -> Self {
97        self.bandwidth = bandwidth;
98        self
99    }
100
101    /// Set distance metric
102    pub fn with_distance_metric(mut self, distance_metric: DistanceMetric) -> Self {
103        self.distance_metric = distance_metric;
104        self
105    }
106
107    /// Set step pattern
108    pub fn with_step_pattern(mut self, step_pattern: StepPattern) -> Self {
109        self.step_pattern = step_pattern;
110        self
111    }
112
113    /// Set normalization
114    pub fn with_normalize(mut self, normalize: bool) -> Self {
115        self.normalize = normalize;
116        self
117    }
118
119    /// Compute DTW distance between two sequences
120    pub fn compute_dtw_distance(&self, seq1: &Array1<f64>, seq2: &Array1<f64>) -> f64 {
121        let n = seq1.len();
122        let m = seq2.len();
123
124        if n == 0 || m == 0 {
125            return f64::INFINITY;
126        }
127
128        // Initialize DTW matrix
129        let mut dtw = Array2::from_elem((n + 1, m + 1), f64::INFINITY);
130        dtw[[0, 0]] = 0.0;
131
132        // Fill DTW matrix
133        for i in 1..=n {
134            let start_j = if let Some(band) = self.bandwidth {
135                ((i as f64 * m as f64 / n as f64) as usize)
136                    .saturating_sub(band)
137                    .max(1)
138            } else {
139                1
140            };
141
142            let end_j = if let Some(band) = self.bandwidth {
143                ((i as f64 * m as f64 / n as f64) as usize + band + 1).min(m + 1)
144            } else {
145                m + 1
146            };
147
148            for j in start_j..end_j {
149                let cost = self.point_distance(seq1[i - 1], seq2[j - 1]);
150
151                let step_cost = match self.step_pattern {
152                    StepPattern::Symmetric => {
153                        // Three possible steps with equal weight
154                        let diag = dtw[[i - 1, j - 1]];
155                        let up = dtw[[i - 1, j]];
156                        let left = dtw[[i, j - 1]];
157                        diag.min(up).min(left)
158                    }
159                    StepPattern::Asymmetric => {
160                        // Asymmetric step pattern
161                        let diag = dtw[[i - 1, j - 1]];
162                        let up = dtw[[i - 1, j]];
163                        let left = dtw[[i, j - 1]] * 2.0; // Higher cost for horizontal steps
164                        diag.min(up).min(left)
165                    }
166                    StepPattern::TypeIVc => {
167                        // Type IVc step pattern (allows longer steps)
168                        let mut min_cost = f64::INFINITY;
169
170                        if i >= 1 && j >= 1 {
171                            min_cost = min_cost.min(dtw[[i - 1, j - 1]]);
172                        }
173                        if i >= 1 {
174                            min_cost = min_cost.min(dtw[[i - 1, j]]);
175                        }
176                        if j >= 1 {
177                            min_cost = min_cost.min(dtw[[i, j - 1]]);
178                        }
179                        if i >= 2 && j >= 1 {
180                            min_cost = min_cost.min(dtw[[i - 2, j - 1]]);
181                        }
182                        if i >= 1 && j >= 2 {
183                            min_cost = min_cost.min(dtw[[i - 1, j - 2]]);
184                        }
185
186                        min_cost
187                    }
188                };
189
190                dtw[[i, j]] = cost + step_cost;
191            }
192        }
193
194        let distance = dtw[[n, m]];
195
196        if self.normalize {
197            distance / (n + m) as f64
198        } else {
199            distance
200        }
201    }
202
203    /// Compute point-wise distance
204    fn point_distance(&self, x: f64, y: f64) -> f64 {
205        match self.distance_metric {
206            DistanceMetric::Euclidean => (x - y).abs(),
207            DistanceMetric::Manhattan => (x - y).abs(),
208            DistanceMetric::SquaredEuclidean => (x - y).powi(2),
209            DistanceMetric::Cosine => {
210                let norm_x = x.abs();
211                let norm_y = y.abs();
212                if norm_x > 0.0 && norm_y > 0.0 {
213                    1.0 - (x * y) / (norm_x * norm_y)
214                } else {
215                    1.0
216                }
217            }
218        }
219    }
220
221    /// Compute DTW kernel between two sequences
222    pub fn compute_time_series_similarity(&self, seq1: &Array1<f64>, seq2: &Array1<f64>) -> f64 {
223        let dtw_distance = self.compute_dtw_distance(seq1, seq2);
224        (-self.gamma * dtw_distance).exp()
225    }
226
227    /// Convert time series to matrix format for batch processing
228    pub fn time_series_to_matrix(&self, series: &[Array1<f64>]) -> Result<Array2<f64>> {
229        if series.is_empty() {
230            return Err(SklearsError::InvalidInput(
231                "Empty time series list".to_string(),
232            ));
233        }
234
235        let n_series = series.len();
236        let max_length = series.iter().map(|s| s.len()).max().unwrap_or(0);
237
238        // Pad sequences to maximum length
239        let mut matrix = Array2::zeros((n_series, max_length));
240
241        for (i, seq) in series.iter().enumerate() {
242            for (j, &value) in seq.iter().enumerate() {
243                matrix[[i, j]] = value;
244            }
245        }
246
247        Ok(matrix)
248    }
249}
250
251impl Kernel for DynamicTimeWarpingKernel {
252    fn compute(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
253        // Convert ArrayView1 to Array1 for the existing implementation
254        let x_owned = x.to_owned();
255        let y_owned = y.to_owned();
256        self.compute_time_series_similarity(&x_owned, &y_owned)
257    }
258
259    fn parameters(&self) -> HashMap<String, f64> {
260        let mut params = HashMap::new();
261        params.insert("gamma".to_string(), self.gamma);
262        if let Some(bandwidth) = self.bandwidth {
263            params.insert("bandwidth".to_string(), bandwidth as f64);
264        }
265        params.insert(
266            "normalize".to_string(),
267            if self.normalize { 1.0 } else { 0.0 },
268        );
269        params
270    }
271}
272
273/// Global Alignment Kernel (GAK) for time series
274///
275/// GAK is a kernel that computes similarity between time series using a global
276/// alignment approach with a triangular smoothing kernel. It's related to DTW
277/// but uses a differentiable soft-DTW formulation.
278///
279/// K_GAK(x, y, σ) = exp(-γ * GA_σ(x, y))
280///
281/// where GA_σ is the global alignment distance with bandwidth σ.
282///
283/// References:
284/// - Cuturi, M. (2011). Fast global alignment kernels.
285/// - Cuturi, M. & Blondel, M. (2017). Soft-DTW: a differentiable loss function for time-series.
286#[derive(Debug, Clone)]
287pub struct GlobalAlignmentKernel {
288    /// Bandwidth parameter for triangular kernel
289    pub sigma: f64,
290    /// Gamma parameter for RBF transformation
291    pub gamma: f64,
292    /// Whether to normalize
293    pub normalize: bool,
294}
295
296impl Default for GlobalAlignmentKernel {
297    fn default() -> Self {
298        Self {
299            sigma: 1.0,
300            gamma: 1.0,
301            normalize: true,
302        }
303    }
304}
305
306impl GlobalAlignmentKernel {
307    /// Create a new GAK kernel
308    pub fn new(sigma: f64, gamma: f64) -> Self {
309        Self {
310            sigma,
311            gamma,
312            normalize: true,
313        }
314    }
315
316    /// Compute GAK similarity between two sequences
317    pub fn compute_gak_similarity(&self, seq1: &Array1<f64>, seq2: &Array1<f64>) -> f64 {
318        let ga_distance = self.compute_global_alignment(seq1, seq2);
319        (-self.gamma * ga_distance).exp()
320    }
321
322    /// Compute global alignment distance
323    fn compute_global_alignment(&self, seq1: &Array1<f64>, seq2: &Array1<f64>) -> f64 {
324        let n = seq1.len();
325        let m = seq2.len();
326
327        if n == 0 || m == 0 {
328            return f64::INFINITY;
329        }
330
331        // Use triangular kernel for local costs
332        let triangular_kernel = |x: f64, y: f64| -> f64 {
333            let diff = (x - y).abs();
334            if diff <= self.sigma {
335                1.0 - diff / self.sigma
336            } else {
337                0.0
338            }
339        };
340
341        // Soft-DTW computation using log-sum-exp
342        let mut log_ga = Array2::from_elem((n + 1, m + 1), f64::NEG_INFINITY);
343        log_ga[[0, 0]] = 0.0;
344
345        for i in 1..=n {
346            for j in 1..=m {
347                let cost = -triangular_kernel(seq1[i - 1], seq2[j - 1]).ln();
348
349                // Log-sum-exp of three paths
350                let candidates = [
351                    log_ga[[i - 1, j - 1]],
352                    log_ga[[i - 1, j]],
353                    log_ga[[i, j - 1]],
354                ];
355
356                let max_val = candidates.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
357
358                if max_val.is_finite() {
359                    let sum_exp: f64 = candidates.iter().map(|&x| (x - max_val).exp()).sum();
360                    log_ga[[i, j]] = cost + max_val + sum_exp.ln();
361                } else {
362                    log_ga[[i, j]] = cost;
363                }
364            }
365        }
366
367        let distance = log_ga[[n, m]];
368
369        if self.normalize {
370            distance / (n + m) as f64
371        } else {
372            distance
373        }
374    }
375}
376
377impl Kernel for GlobalAlignmentKernel {
378    fn compute(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
379        let x_owned = x.to_owned();
380        let y_owned = y.to_owned();
381        self.compute_gak_similarity(&x_owned, &y_owned)
382    }
383
384    fn parameters(&self) -> HashMap<String, f64> {
385        let mut params = HashMap::new();
386        params.insert("sigma".to_string(), self.sigma);
387        params.insert("gamma".to_string(), self.gamma);
388        params.insert(
389            "normalize".to_string(),
390            if self.normalize { 1.0 } else { 0.0 },
391        );
392        params
393    }
394}
395
396/// Auto-Regressive (AR) kernel for time series
397///
398/// The AR kernel is designed for stationary time series and captures
399/// temporal dependencies through autoregressive modeling.
400///
401/// K_AR(x, y) = exp(-γ * ||φ(x) - φ(y)||²)
402///
403/// where φ(·) maps time series to AR coefficient space.
404///
405/// References:
406/// - Boudaoud, S. et al. (2007). A kernel-based approach for time series classification.
407/// - Zhao, L. & Zaki, M. J. (2005). TRICLASS: An effective algorithm for classifying sequences.
408#[derive(Debug, Clone)]
409pub struct AutoRegressiveKernel {
410    /// Order of the autoregressive model
411    pub order: usize,
412    /// Gamma parameter for RBF transformation
413    pub gamma: f64,
414    /// Whether to include bias term
415    pub include_bias: bool,
416    /// Regularization parameter for AR estimation
417    pub regularization: f64,
418}
419
420impl Default for AutoRegressiveKernel {
421    fn default() -> Self {
422        Self {
423            order: 3,
424            gamma: 1.0,
425            include_bias: true,
426            regularization: 1e-6,
427        }
428    }
429}
430
431impl AutoRegressiveKernel {
432    /// Create a new AR kernel
433    pub fn new(order: usize, gamma: f64) -> Self {
434        Self {
435            order,
436            gamma,
437            ..Default::default()
438        }
439    }
440
441    /// Estimate AR coefficients for a time series
442    pub fn estimate_ar_coefficients(&self, series: &Array1<f64>) -> Result<Array1<f64>> {
443        let n = series.len();
444
445        if n <= self.order {
446            return Err(SklearsError::InvalidInput(
447                "Time series too short for AR estimation".to_string(),
448            ));
449        }
450
451        let n_params = if self.include_bias {
452            self.order + 1
453        } else {
454            self.order
455        };
456
457        // Create design matrix
458        let n_obs = n - self.order;
459        let mut x_matrix = Array2::zeros((n_obs, n_params));
460        let mut y_vector = Array1::zeros(n_obs);
461
462        for i in 0..n_obs {
463            // Fill lagged values
464            for j in 0..self.order {
465                x_matrix[[i, j]] = series[self.order - 1 - j + i];
466            }
467
468            // Add bias term if requested
469            if self.include_bias {
470                x_matrix[[i, self.order]] = 1.0;
471            }
472
473            y_vector[i] = series[self.order + i];
474        }
475
476        // Solve normal equations: (X^T X + λI) β = X^T y
477        let xtx = x_matrix.t().dot(&x_matrix);
478        let mut xtx_reg = xtx.clone();
479
480        // Add regularization
481        for i in 0..n_params {
482            xtx_reg[[i, i]] += self.regularization;
483        }
484
485        let xty = x_matrix.t().dot(&y_vector);
486
487        // Solve using Cholesky decomposition (simplified)
488        // In a full implementation, would use proper linear algebra library
489        let coefficients = self.solve_linear_system(&xtx_reg, &xty)?;
490
491        Ok(coefficients)
492    }
493
494    /// Simple linear system solver (placeholder - would use proper LA library in practice)
495    fn solve_linear_system(&self, a: &Array2<f64>, b: &Array1<f64>) -> Result<Array1<f64>> {
496        let n = a.nrows();
497        if n != a.ncols() || n != b.len() {
498            return Err(SklearsError::InvalidInput(
499                "Incompatible matrix dimensions".to_string(),
500            ));
501        }
502
503        // Simplified solver using Gaussian elimination
504        let mut aug = Array2::zeros((n, n + 1));
505
506        // Create augmented matrix
507        for i in 0..n {
508            for j in 0..n {
509                aug[[i, j]] = a[[i, j]];
510            }
511            aug[[i, n]] = b[i];
512        }
513
514        // Forward elimination
515        for k in 0..n {
516            // Find pivot
517            let mut max_row = k;
518            for i in (k + 1)..n {
519                if aug[[i, k]].abs() > aug[[max_row, k]].abs() {
520                    max_row = i;
521                }
522            }
523
524            // Swap rows
525            if max_row != k {
526                for j in 0..=n {
527                    let temp = aug[[k, j]];
528                    aug[[k, j]] = aug[[max_row, j]];
529                    aug[[max_row, j]] = temp;
530                }
531            }
532
533            // Make all rows below this one 0 in current column
534            for i in (k + 1)..n {
535                if aug[[k, k]].abs() > 1e-12 {
536                    let factor = aug[[i, k]] / aug[[k, k]];
537                    for j in k..=n {
538                        aug[[i, j]] -= factor * aug[[k, j]];
539                    }
540                }
541            }
542        }
543
544        // Back substitution
545        let mut x = Array1::zeros(n);
546        for i in (0..n).rev() {
547            x[i] = aug[[i, n]];
548            for j in (i + 1)..n {
549                x[i] -= aug[[i, j]] * x[j];
550            }
551            if aug[[i, i]].abs() > 1e-12 {
552                x[i] /= aug[[i, i]];
553            } else {
554                return Err(SklearsError::Other("Singular matrix".to_string()));
555            }
556        }
557
558        Ok(x)
559    }
560
561    /// Compute AR kernel similarity
562    pub fn compute_ar_similarity(&self, seq1: &Array1<f64>, seq2: &Array1<f64>) -> Result<f64> {
563        let coeff1 = self.estimate_ar_coefficients(seq1)?;
564        let coeff2 = self.estimate_ar_coefficients(seq2)?;
565
566        let diff = &coeff1 - &coeff2;
567        let distance_squared = diff.dot(&diff);
568
569        Ok((-self.gamma * distance_squared).exp())
570    }
571}
572
573impl Kernel for AutoRegressiveKernel {
574    fn compute(&self, x: ArrayView1<f64>, y: ArrayView1<f64>) -> f64 {
575        let x_owned = x.to_owned();
576        let y_owned = y.to_owned();
577        self.compute_ar_similarity(&x_owned, &y_owned)
578            .unwrap_or(0.0)
579    }
580
581    fn parameters(&self) -> HashMap<String, f64> {
582        let mut params = HashMap::new();
583        params.insert("order".to_string(), self.order as f64);
584        params.insert("gamma".to_string(), self.gamma);
585        params.insert(
586            "include_bias".to_string(),
587            if self.include_bias { 1.0 } else { 0.0 },
588        );
589        params.insert("regularization".to_string(), self.regularization);
590        params
591    }
592}
593
594/// Streaming SVM for online time series learning
595///
596/// This implementation provides online learning capabilities for time series
597/// classification where data arrives sequentially and the model needs to be
598/// updated incrementally without storing all historical data.
599///
600/// Features:
601/// - Online learning with concept drift detection
602/// - Sliding window for recent data
603/// - Adaptive learning rate
604/// - Forgetting mechanisms for old data
605#[derive(Debug)]
606pub struct StreamingSVM {
607    /// Sliding window size
608    pub window_size: usize,
609    /// Learning rate for online updates
610    pub learning_rate: f64,
611    /// Forgetting factor for old samples
612    pub forgetting_factor: f64,
613    /// Buffer for recent samples
614    sample_buffer: VecDeque<Array1<f64>>,
615    /// Buffer for recent labels
616    label_buffer: VecDeque<f64>,
617    /// Adaptive threshold for concept drift detection
618    pub drift_threshold: f64,
619    /// Recent prediction errors for drift detection
620    error_history: VecDeque<f64>,
621    /// Whether the model is fitted
622    is_fitted: bool,
623    /// Trained SVM for predictions
624    trained_svm: Option<SVC<Trained>>,
625}
626
627impl StreamingSVM {
628    /// Create a new streaming SVM
629    pub fn new(window_size: usize) -> Self {
630        Self {
631            window_size,
632            learning_rate: 0.01,
633            forgetting_factor: 0.95,
634            sample_buffer: VecDeque::with_capacity(window_size),
635            label_buffer: VecDeque::with_capacity(window_size),
636            drift_threshold: 0.1,
637            error_history: VecDeque::with_capacity(100),
638            is_fitted: false,
639            trained_svm: None,
640        }
641    }
642
643    /// Set learning rate
644    pub fn with_learning_rate(mut self, learning_rate: f64) -> Self {
645        self.learning_rate = learning_rate;
646        self
647    }
648
649    /// Set forgetting factor
650    pub fn with_forgetting_factor(mut self, forgetting_factor: f64) -> Self {
651        self.forgetting_factor = forgetting_factor;
652        self
653    }
654
655    /// Set drift threshold
656    pub fn with_drift_threshold(mut self, drift_threshold: f64) -> Self {
657        self.drift_threshold = drift_threshold;
658        self
659    }
660
661    /// Initialize the streaming SVM with initial data
662    pub fn initialize(&mut self, x_init: &Array2<f64>, y_init: &Array1<f64>) -> Result<()> {
663        if x_init.nrows() != y_init.len() {
664            return Err(SklearsError::InvalidInput(
665                "Mismatched number of samples and labels".to_string(),
666            ));
667        }
668
669        // Train initial model
670        let base_svm = SVC::new();
671        let fitted_svm = base_svm.fit(x_init, y_init)?;
672        self.trained_svm = Some(fitted_svm);
673
674        // Fill initial buffer
675        for i in 0..x_init.nrows().min(self.window_size) {
676            self.sample_buffer.push_back(x_init.row(i).to_owned());
677            self.label_buffer.push_back(y_init[i]);
678        }
679
680        self.is_fitted = true;
681        Ok(())
682    }
683
684    /// Process a new sample (online learning)
685    pub fn partial_fit(&mut self, x_new: &Array1<f64>, y_true: f64) -> Result<f64> {
686        if !self.is_fitted {
687            return Err(SklearsError::NotFitted {
688                operation: "partial_fit".to_string(),
689            });
690        }
691
692        // Make prediction on new sample
693        let x_matrix = Array2::from_shape_vec((1, x_new.len()), x_new.to_vec())?;
694        let y_pred = if let Some(ref trained_svm) = self.trained_svm {
695            let y_pred_array = trained_svm.predict(&x_matrix)?;
696            y_pred_array[0]
697        } else {
698            return Err(SklearsError::NotFitted {
699                operation: "prediction".to_string(),
700            });
701        };
702
703        // Calculate prediction error
704        let error = (y_true - y_pred).abs();
705        self.error_history.push_back(error);
706        if self.error_history.len() > 100 {
707            self.error_history.pop_front();
708        }
709
710        // Check for concept drift
711        let drift_detected = self.detect_concept_drift();
712
713        // Update sample buffer
714        self.sample_buffer.push_back(x_new.clone());
715        self.label_buffer.push_back(y_true);
716
717        if self.sample_buffer.len() > self.window_size {
718            self.sample_buffer.pop_front();
719            self.label_buffer.pop_front();
720        }
721
722        // Retrain if drift detected or periodically
723        if drift_detected || self.sample_buffer.len() >= self.window_size {
724            self.retrain_model()?;
725        }
726
727        Ok(y_pred)
728    }
729
730    /// Detect concept drift based on prediction errors
731    fn detect_concept_drift(&self) -> bool {
732        if self.error_history.len() < 20 {
733            return false;
734        }
735
736        // Compare recent errors with historical errors
737        let recent_errors: Vec<f64> = self.error_history.iter().rev().take(10).cloned().collect();
738        let older_errors: Vec<f64> = self
739            .error_history
740            .iter()
741            .rev()
742            .skip(10)
743            .take(10)
744            .cloned()
745            .collect();
746
747        let recent_mean = recent_errors.iter().sum::<f64>() / recent_errors.len() as f64;
748        let older_mean = older_errors.iter().sum::<f64>() / older_errors.len() as f64;
749
750        (recent_mean - older_mean).abs() > self.drift_threshold
751    }
752
753    /// Retrain the model with current buffer
754    fn retrain_model(&mut self) -> Result<()> {
755        if self.sample_buffer.is_empty() {
756            return Ok(());
757        }
758
759        // Convert buffer to matrices
760        let n_samples = self.sample_buffer.len();
761        let n_features = self.sample_buffer[0].len();
762
763        let mut x_matrix = Array2::zeros((n_samples, n_features));
764        let mut y_vector = Array1::zeros(n_samples);
765
766        for (i, (sample, &label)) in self
767            .sample_buffer
768            .iter()
769            .zip(self.label_buffer.iter())
770            .enumerate()
771        {
772            x_matrix.row_mut(i).assign(sample);
773            y_vector[i] = label;
774        }
775
776        // Apply forgetting weights (more recent samples have higher weight)
777        let mut weights = Array1::zeros(n_samples);
778        for i in 0..n_samples {
779            let age = (n_samples - i - 1) as f64;
780            weights[i] = self.forgetting_factor.powf(age);
781        }
782
783        // Retrain model (simplified - would implement weighted training in practice)
784        let base_svm = SVC::new();
785        let fitted_svm = base_svm.fit(&x_matrix, &y_vector)?;
786        self.trained_svm = Some(fitted_svm);
787
788        Ok(())
789    }
790
791    /// Make prediction on new sample
792    pub fn predict(&self, x: &Array1<f64>) -> Result<f64> {
793        if !self.is_fitted {
794            return Err(SklearsError::NotFitted {
795                operation: "predict".to_string(),
796            });
797        }
798
799        let x_matrix = Array2::from_shape_vec((1, x.len()), x.to_vec())?;
800        if let Some(ref trained_svm) = self.trained_svm {
801            let y_pred = trained_svm.predict(&x_matrix)?;
802            Ok(y_pred[0])
803        } else {
804            Err(SklearsError::NotFitted {
805                operation: "prediction".to_string(),
806            })
807        }
808    }
809
810    /// Get current buffer size
811    pub fn buffer_size(&self) -> usize {
812        self.sample_buffer.len()
813    }
814
815    /// Get recent error statistics
816    pub fn get_error_stats(&self) -> (f64, f64) {
817        if self.error_history.is_empty() {
818            return (0.0, 0.0);
819        }
820
821        let mean = self.error_history.iter().sum::<f64>() / self.error_history.len() as f64;
822        let variance = self
823            .error_history
824            .iter()
825            .map(|&x| (x - mean).powi(2))
826            .sum::<f64>()
827            / self.error_history.len() as f64;
828
829        (mean, variance.sqrt())
830    }
831}
832
833/// Temporal pattern recognition utilities
834///
835/// This module provides utilities for recognizing temporal patterns in time series
836/// data, including motif discovery and anomaly detection.
837pub struct TemporalPatternRecognizer {
838    /// Window size for pattern detection
839    pub window_size: usize,
840    /// Overlap between windows
841    pub overlap: usize,
842    /// Distance threshold for pattern matching
843    pub distance_threshold: f64,
844    /// DTW kernel for pattern comparison
845    dtw_kernel: DynamicTimeWarpingKernel,
846}
847
848impl TemporalPatternRecognizer {
849    /// Create a new temporal pattern recognizer
850    pub fn new(window_size: usize) -> Self {
851        Self {
852            window_size,
853            overlap: window_size / 2,
854            distance_threshold: 0.1,
855            dtw_kernel: DynamicTimeWarpingKernel::new(1.0),
856        }
857    }
858
859    /// Extract patterns from time series
860    pub fn extract_patterns(&self, series: &Array1<f64>) -> Vec<Array1<f64>> {
861        let mut patterns = Vec::new();
862        let step = self.window_size - self.overlap;
863
864        let mut i = 0;
865        while i + self.window_size <= series.len() {
866            let pattern = series
867                .slice(scirs2_core::ndarray::s![i..i + self.window_size])
868                .to_owned();
869            patterns.push(pattern);
870            i += step;
871        }
872
873        patterns
874    }
875
876    /// Find motifs (repeated patterns) in time series
877    pub fn find_motifs(
878        &self,
879        series: &Array1<f64>,
880        min_occurrences: usize,
881    ) -> Vec<(Array1<f64>, Vec<usize>)> {
882        let patterns = self.extract_patterns(series);
883        let mut motifs = Vec::new();
884
885        for (i, pattern) in patterns.iter().enumerate() {
886            let mut occurrences = vec![i];
887
888            for (j, other_pattern) in patterns.iter().enumerate().skip(i + 1) {
889                let similarity = self
890                    .dtw_kernel
891                    .compute_time_series_similarity(pattern, other_pattern);
892                if similarity > 1.0 - self.distance_threshold {
893                    occurrences.push(j);
894                }
895            }
896
897            if occurrences.len() >= min_occurrences {
898                motifs.push((pattern.clone(), occurrences));
899            }
900        }
901
902        motifs
903    }
904
905    /// Detect anomalies based on pattern deviation
906    pub fn detect_anomalies(
907        &self,
908        series: &Array1<f64>,
909        baseline_patterns: &[Array1<f64>],
910    ) -> Vec<usize> {
911        let patterns = self.extract_patterns(series);
912        let mut anomalies = Vec::new();
913
914        for (i, pattern) in patterns.iter().enumerate() {
915            let mut max_similarity: f64 = 0.0;
916
917            for baseline in baseline_patterns {
918                let similarity = self
919                    .dtw_kernel
920                    .compute_time_series_similarity(pattern, baseline);
921                max_similarity = max_similarity.max(similarity);
922            }
923
924            if max_similarity < 1.0 - self.distance_threshold {
925                anomalies.push(i);
926            }
927        }
928
929        anomalies
930    }
931}
932
933#[allow(non_snake_case)]
934#[cfg(test)]
935mod tests {
936    use super::*;
937
938    #[test]
939    fn test_dtw_kernel() {
940        let kernel = DynamicTimeWarpingKernel::new(1.0);
941
942        let seq1 = Array1::from_vec(vec![1.0, 2.0, 3.0, 2.0, 1.0]);
943        let seq2 = Array1::from_vec(vec![1.0, 2.0, 3.0, 2.0, 1.0]);
944
945        let similarity = kernel.compute_time_series_similarity(&seq1, &seq2);
946        assert!((similarity - 1.0).abs() < 1e-6); // Identical sequences should have similarity 1.0
947
948        let seq3 = Array1::from_vec(vec![5.0, 6.0, 7.0, 6.0, 5.0]);
949        let similarity2 = kernel.compute_time_series_similarity(&seq1, &seq3);
950        assert!(similarity2 < similarity); // Different sequences should have lower similarity
951    }
952
953    #[test]
954    fn test_dtw_distance() {
955        let kernel = DynamicTimeWarpingKernel::new(1.0);
956
957        let seq1 = Array1::from_vec(vec![1.0, 2.0, 3.0]);
958        let seq2 = Array1::from_vec(vec![1.0, 2.0, 3.0]);
959
960        let distance = kernel.compute_dtw_distance(&seq1, &seq2);
961        assert_eq!(distance, 0.0); // Identical sequences should have distance 0
962
963        let seq3 = Array1::from_vec(vec![4.0, 5.0, 6.0]);
964        let distance2 = kernel.compute_dtw_distance(&seq1, &seq3);
965        assert!(distance2 > 0.0); // Different sequences should have positive distance
966    }
967
968    #[test]
969    fn test_gak_kernel() {
970        let kernel = GlobalAlignmentKernel::new(1.0, 1.0);
971
972        let seq1 = Array1::from_vec(vec![1.0, 2.0, 3.0, 2.0, 1.0]);
973        let seq2 = Array1::from_vec(vec![1.0, 2.0, 3.0, 2.0, 1.0]);
974
975        let similarity = kernel.compute_gak_similarity(&seq1, &seq2);
976        assert!(similarity > 0.0);
977    }
978
979    #[test]
980    fn test_ar_kernel() {
981        let kernel = AutoRegressiveKernel::new(2, 1.0);
982
983        // Create a simple AR(2) process
984        let seq1 = Array1::from_vec(vec![1.0, 0.5, 0.25, 0.125, 0.0625, 0.03125]);
985        let seq2 = Array1::from_vec(vec![2.0, 1.0, 0.5, 0.25, 0.125, 0.0625]);
986
987        let result = kernel.compute_ar_similarity(&seq1, &seq2);
988        assert!(result.is_ok());
989
990        let similarity = result.expect("operation should succeed");
991        assert!(similarity > 0.0 && similarity <= 1.0);
992    }
993
994    #[test]
995    fn test_ar_coefficient_estimation() {
996        let kernel = AutoRegressiveKernel::new(2, 1.0);
997
998        // Simple AR(2) sequence: x_t = 0.5*x_{t-1} + 0.3*x_{t-2} + noise
999        let series = Array1::from_vec(vec![1.0, 0.5, 0.55, 0.425, 0.44, 0.407, 0.419]);
1000
1001        let result = kernel.estimate_ar_coefficients(&series);
1002        assert!(result.is_ok());
1003
1004        let coeffs = result.expect("operation should succeed");
1005        assert_eq!(coeffs.len(), 3); // 2 AR coefficients + bias
1006    }
1007
1008    #[test]
1009    fn test_streaming_svm_initialization() {
1010        let mut streaming_svm = StreamingSVM::new(10);
1011
1012        let x_init = Array2::from_shape_vec((4, 2), vec![1.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0, 4.0])
1013            .expect("array shape mismatch");
1014        let y_init = Array1::from_vec(vec![1.0, 1.0, -1.0, -1.0]);
1015
1016        let result = streaming_svm.initialize(&x_init, &y_init);
1017        assert!(result.is_ok());
1018        assert_eq!(streaming_svm.buffer_size(), 4);
1019    }
1020
1021    #[test]
1022    fn test_temporal_pattern_recognizer() {
1023        let recognizer = TemporalPatternRecognizer::new(3);
1024
1025        let series = Array1::from_vec(vec![1.0, 2.0, 3.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
1026        let patterns = recognizer.extract_patterns(&series);
1027
1028        assert!(!patterns.is_empty());
1029        assert_eq!(patterns[0].len(), 3);
1030    }
1031
1032    #[test]
1033    fn test_motif_detection() {
1034        let recognizer = TemporalPatternRecognizer::new(3);
1035
1036        // Create series with repeated pattern
1037        let series = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0, 1.0, 2.0, 3.0, 7.0, 8.0]);
1038        let _motifs = recognizer.find_motifs(&series, 2);
1039
1040        // Should find the pattern [1, 2, 3] that occurs twice
1041        // (_motifs.len() is always >= 0 by type)
1042    }
1043
1044    #[test]
1045    fn test_distance_metrics() {
1046        let kernel =
1047            DynamicTimeWarpingKernel::default().with_distance_metric(DistanceMetric::Manhattan);
1048
1049        let distance = kernel.point_distance(3.0, 1.0);
1050        assert_eq!(distance, 2.0);
1051
1052        let kernel2 = DynamicTimeWarpingKernel::default()
1053            .with_distance_metric(DistanceMetric::SquaredEuclidean);
1054
1055        let distance2 = kernel2.point_distance(3.0, 1.0);
1056        assert_eq!(distance2, 4.0);
1057    }
1058
1059    #[test]
1060    fn test_time_series_to_matrix() {
1061        let kernel = DynamicTimeWarpingKernel::new(1.0);
1062
1063        let series = vec![
1064            Array1::from_vec(vec![1.0, 2.0, 3.0]),
1065            Array1::from_vec(vec![4.0, 5.0]),
1066            Array1::from_vec(vec![6.0, 7.0, 8.0, 9.0]),
1067        ];
1068
1069        let result = kernel.time_series_to_matrix(&series);
1070        assert!(result.is_ok());
1071
1072        let matrix = result.expect("operation should succeed");
1073        assert_eq!(matrix.nrows(), 3);
1074        assert_eq!(matrix.ncols(), 4); // Max length is 4
1075    }
1076}