1use scirs2_core::ndarray::{Array1, Array2, Axis, Zip};
7use scirs2_linalg::compat::{Eigh, SVD};
8use sklears_core::{
9 error::{Result, SklearsError},
10 types::Float,
11};
12use std::collections::VecDeque;
13
14#[derive(Debug, Clone)]
16pub struct StreamingConfig {
17 pub window_size: usize,
19 pub forgetting_factor: Float,
21 pub update_frequency: usize,
23 pub min_samples: usize,
25 pub detect_drift: bool,
27 pub drift_threshold: Float,
29}
30
31impl Default for StreamingConfig {
32 fn default() -> Self {
33 Self {
34 window_size: 1000,
35 forgetting_factor: 0.95,
36 update_frequency: 10,
37 min_samples: 50,
38 detect_drift: true,
39 drift_threshold: 0.1,
40 }
41 }
42}
43
44#[derive(Debug, Clone)]
46pub struct StreamingPCA {
47 config: StreamingConfig,
48 n_components: usize,
49 components: Option<Array2<Float>>,
51 explained_variance: Option<Array1<Float>>,
52 mean: Option<Array1<Float>>,
53 n_samples_seen: usize,
54 data_buffer: VecDeque<Array1<Float>>,
56 covariance_matrix: Option<Array2<Float>>,
57 sum_weights: Float,
58 previous_components: Option<Array2<Float>>,
60 drift_detected: bool,
61}
62
63impl StreamingPCA {
64 pub fn new(n_components: usize) -> Self {
66 Self {
67 config: StreamingConfig::default(),
68 n_components,
69 components: None,
70 explained_variance: None,
71 mean: None,
72 n_samples_seen: 0,
73 data_buffer: VecDeque::new(),
74 covariance_matrix: None,
75 sum_weights: 0.0,
76 previous_components: None,
77 drift_detected: false,
78 }
79 }
80
81 pub fn with_config(mut self, config: StreamingConfig) -> Self {
83 self.config = config;
84 self
85 }
86
87 pub fn partial_fit(&mut self, sample: &Array1<Float>) -> Result<()> {
89 let n_features = sample.len();
90
91 if self.mean.is_none() {
93 self.mean = Some(Array1::zeros(n_features));
94 self.covariance_matrix = Some(Array2::zeros((n_features, n_features)));
95 }
96
97 self.data_buffer.push_back(sample.clone());
99 if self.data_buffer.len() > self.config.window_size {
100 self.data_buffer.pop_front();
101 }
102
103 self.n_samples_seen += 1;
104
105 self.update_statistics(sample)?;
107
108 if self.n_samples_seen >= self.config.min_samples
110 && self
111 .n_samples_seen
112 .is_multiple_of(self.config.update_frequency)
113 {
114 self.update_decomposition()?;
115 }
116
117 Ok(())
118 }
119
120 fn update_statistics(&mut self, sample: &Array1<Float>) -> Result<()> {
122 let weight = 1.0 / (self.n_samples_seen as Float);
123 let alpha = self.config.forgetting_factor;
124
125 if let Some(ref mut mean) = self.mean {
126 let delta = sample - &*mean;
127 mean.zip_mut_with(&delta, |m, d| *m += d * weight);
128
129 if let Some(ref mut cov) = self.covariance_matrix {
130 let delta_mean = sample - &*mean;
131 for i in 0..sample.len() {
132 for j in 0..sample.len() {
133 cov[[i, j]] =
134 alpha * cov[[i, j]] + (1.0 - alpha) * delta_mean[i] * delta_mean[j];
135 }
136 }
137 }
138 }
139
140 Ok(())
141 }
142
143 fn update_decomposition(&mut self) -> Result<()> {
145 if let Some(ref cov) = self.covariance_matrix {
146 let symmetric_cov = (cov + &cov.t()) / 2.0;
148
149 let (eigenvalues, eigenvectors) = symmetric_cov
151 .eigh(scirs2_linalg::compat::UPLO::Upper)
152 .map_err(|e| {
153 SklearsError::InvalidOperation(format!("Eigendecomposition failed: {:?}", e))
154 })?;
155
156 let mut indices: Vec<usize> = (0..eigenvalues.len()).collect();
158 indices.sort_by(|&i, &j| {
159 eigenvalues[j]
160 .partial_cmp(&eigenvalues[i])
161 .expect("operation should succeed")
162 });
163
164 let n_components = self.n_components.min(eigenvalues.len());
166 let mut components = Array2::<Float>::zeros((n_components, cov.nrows()));
167 let mut explained_variance = Array1::<Float>::zeros(n_components);
168
169 for (i, &idx) in indices.iter().take(n_components).enumerate() {
170 components.row_mut(i).assign(&eigenvectors.column(idx));
171 explained_variance[i] = eigenvalues[idx].max(0.0);
172 }
173
174 if self.config.detect_drift {
176 self.detect_concept_drift(&components)?;
177 }
178
179 self.components = Some(components);
180 self.explained_variance = Some(explained_variance);
181 }
182
183 Ok(())
184 }
185
186 fn detect_concept_drift(&mut self, new_components: &Array2<Float>) -> Result<()> {
188 if let Some(ref prev_components) = self.previous_components {
189 let similarity = self.compute_subspace_similarity(prev_components, new_components)?;
190
191 if similarity < (1.0 - self.config.drift_threshold) {
192 self.drift_detected = true;
193 println!("Concept drift detected! Similarity: {similarity:.4}");
194 } else {
195 self.drift_detected = false;
196 }
197 }
198
199 self.previous_components = Some(new_components.clone());
200 Ok(())
201 }
202
203 fn compute_subspace_similarity(&self, a: &Array2<Float>, b: &Array2<Float>) -> Result<Float> {
205 let ab = a.dot(&b.t());
207 let (_u, s, _vt) = ab
208 .svd(true)
209 .map_err(|e| SklearsError::InvalidOperation(format!("SVD failed: {:?}", e)))?;
210
211 let similarity = s.iter().map(|&x| x.powi(2)).sum::<Float>() / s.len() as Float;
213 Ok(similarity)
214 }
215
216 pub fn transform(&self, x: &Array2<Float>) -> Result<Array2<Float>> {
218 if let (Some(ref components), Some(ref mean)) = (&self.components, &self.mean) {
219 let centered = x - mean;
220 Ok(centered.dot(&components.t()))
221 } else {
222 Err(SklearsError::NotFitted {
223 operation: "transform".to_string(),
224 })
225 }
226 }
227
228 pub fn components(&self) -> Option<&Array2<Float>> {
230 self.components.as_ref()
231 }
232
233 pub fn explained_variance(&self) -> Option<&Array1<Float>> {
235 self.explained_variance.as_ref()
236 }
237
238 pub fn is_drift_detected(&self) -> bool {
240 self.drift_detected
241 }
242
243 pub fn n_samples_seen(&self) -> usize {
245 self.n_samples_seen
246 }
247
248 pub fn force_update(&mut self) -> Result<()> {
250 if self.n_samples_seen >= self.config.min_samples {
251 self.update_decomposition()
252 } else {
253 Err(SklearsError::InvalidParameter {
254 name: "min_samples".to_string(),
255 reason: "Not enough samples for decomposition update".to_string(),
256 })
257 }
258 }
259
260 pub fn reset(&mut self) {
262 self.components = None;
263 self.explained_variance = None;
264 self.mean = None;
265 self.n_samples_seen = 0;
266 self.data_buffer.clear();
267 self.covariance_matrix = None;
268 self.sum_weights = 0.0;
269 self.previous_components = None;
270 self.drift_detected = false;
271 }
272
273 pub fn transform_sample(&self, sample: &Array1<Float>) -> Result<Array1<Float>> {
275 if let (Some(ref components), Some(ref mean)) = (&self.components, &self.mean) {
276 let centered = sample - mean;
277 Ok(components.dot(¢ered))
278 } else {
279 Err(SklearsError::NotFitted {
280 operation: "transform_sample".to_string(),
281 })
282 }
283 }
284
285 pub fn reconstruction_error(&self, sample: &Array1<Float>) -> Result<Float> {
287 if let (Some(ref components), Some(ref mean)) = (&self.components, &self.mean) {
288 let centered = sample - mean;
289 let transformed = components.dot(¢ered);
290 let reconstructed = components.t().dot(&transformed);
291 let error = (¢ered - &reconstructed)
292 .mapv(|x| x.powi(2))
293 .sum()
294 .sqrt();
295 Ok(error)
296 } else {
297 Err(SklearsError::NotFitted {
298 operation: "reconstruction_error".to_string(),
299 })
300 }
301 }
302}
303
304#[derive(Debug, Clone)]
306pub struct StreamingICA {
307 config: StreamingConfig,
308 n_components: usize,
309 components: Option<Array2<Float>>,
311 mixing_matrix: Option<Array2<Float>>,
312 mean: Option<Array1<Float>>,
313 n_samples_seen: usize,
314 data_buffer: VecDeque<Array1<Float>>,
316 learning_rate: Float,
317 previous_components: Option<Array2<Float>>,
319 drift_detected: bool,
320}
321
322impl StreamingICA {
323 pub fn new(n_components: usize) -> Self {
325 Self {
326 config: StreamingConfig::default(),
327 n_components,
328 components: None,
329 mixing_matrix: None,
330 mean: None,
331 n_samples_seen: 0,
332 data_buffer: VecDeque::new(),
333 learning_rate: 0.01,
334 previous_components: None,
335 drift_detected: false,
336 }
337 }
338
339 pub fn with_config(mut self, config: StreamingConfig) -> Self {
341 self.config = config;
342 self
343 }
344
345 pub fn partial_fit(&mut self, sample: &Array1<Float>) -> Result<()> {
347 let n_features = sample.len();
348
349 if self.mean.is_none() {
351 self.mean = Some(Array1::zeros(n_features));
352 self.components = Some(Array2::eye(self.n_components));
353 }
354
355 self.data_buffer.push_back(sample.clone());
357 if self.data_buffer.len() > self.config.window_size {
358 self.data_buffer.pop_front();
359 }
360
361 self.n_samples_seen += 1;
362
363 self.update_mean(sample)?;
365
366 if self.n_samples_seen >= self.config.min_samples
368 && self
369 .n_samples_seen
370 .is_multiple_of(self.config.update_frequency)
371 {
372 self.update_decomposition()?;
373 }
374
375 Ok(())
376 }
377
378 fn update_mean(&mut self, sample: &Array1<Float>) -> Result<()> {
380 let weight = 1.0 / (self.n_samples_seen as Float);
381
382 if let Some(ref mut mean) = self.mean {
383 let delta = sample - &*mean;
384 mean.zip_mut_with(&delta, |m, d| *m += d * weight);
385 }
386
387 Ok(())
388 }
389
390 fn update_decomposition(&mut self) -> Result<()> {
392 if let (Some(ref mean), Some(_)) = (&self.mean, &self.components) {
393 let recent_samples: Vec<_> = self
395 .data_buffer
396 .iter()
397 .rev()
398 .take(self.config.update_frequency)
399 .cloned()
400 .collect();
401
402 for sample in recent_samples {
403 let centered = &sample - mean;
404 if let Some(ref mut components) = self.components {
406 let y = components.dot(¢ered);
407 let g: Array1<Float> = y.mapv(|val| val.tanh());
408 let g_prime: Array1<Float> = y.mapv(|val| 1.0 - val.tanh().powi(2));
409
410 let outer_product = g
411 .insert_axis(Axis(1))
412 .dot(¢ered.clone().insert_axis(Axis(0)));
413 let diagonal = Array2::from_diag(&g_prime);
414
415 let gradient = outer_product - diagonal.dot(&*components);
416 *components += &(gradient * self.learning_rate);
417 }
418 }
419
420 if let Some(ref mut components) = self.components {
422 Self::orthogonalize_components_static(components)?;
423 }
424
425 if self.config.detect_drift {
427 if let Some(ref components) = self.components {
428 let components_clone = components.clone();
429 self.detect_concept_drift(&components_clone)?;
430 }
431 }
432 }
433
434 Ok(())
435 }
436
437 #[allow(dead_code)]
439 fn online_ica_update(
440 &mut self,
441 components: &mut Array2<Float>,
442 x: &Array1<Float>,
443 ) -> Result<()> {
444 let y = components.dot(x);
446
447 let g: Array1<Float> = y.mapv(|val| val.tanh());
449 let g_prime: Array1<Float> = y.mapv(|val| 1.0 - val.tanh().powi(2));
450
451 let outer_product = g.insert_axis(Axis(1)).dot(&x.clone().insert_axis(Axis(0)));
453 let _identity = Array2::<Float>::eye(self.n_components);
454 let diagonal = Array2::from_diag(&g_prime);
455
456 let gradient = outer_product - diagonal.dot(components);
457
458 *components += &(gradient * self.learning_rate);
460
461 self.orthogonalize_components(components)?;
463
464 Ok(())
465 }
466
467 #[allow(dead_code)]
469 fn orthogonalize_components(&self, components: &mut Array2<Float>) -> Result<()> {
470 Self::orthogonalize_components_static(components)
471 }
472
473 fn orthogonalize_components_static(components: &mut Array2<Float>) -> Result<()> {
475 for i in 0..components.nrows() {
476 let norm = components.row(i).dot(&components.row(i)).sqrt();
478 if norm > 1e-10 {
479 components.row_mut(i).mapv_inplace(|x| x / norm);
480 }
481
482 for j in 0..i {
484 let projection = components.row(i).dot(&components.row(j));
485 let row_j = components.row(j).to_owned();
486 Zip::from(components.row_mut(i))
487 .and(&row_j)
488 .for_each(|a, &b| *a -= projection * b);
489 }
490 }
491
492 Ok(())
493 }
494
495 fn detect_concept_drift(&mut self, new_components: &Array2<Float>) -> Result<()> {
497 if let Some(ref prev_components) = self.previous_components {
498 let similarity = self.compute_subspace_similarity(prev_components, new_components)?;
499
500 if similarity < (1.0 - self.config.drift_threshold) {
501 self.drift_detected = true;
502 println!("Concept drift detected in ICA! Similarity: {similarity:.4}");
503 } else {
504 self.drift_detected = false;
505 }
506 }
507
508 self.previous_components = Some(new_components.clone());
509 Ok(())
510 }
511
512 fn compute_subspace_similarity(&self, a: &Array2<Float>, b: &Array2<Float>) -> Result<Float> {
514 let ab = a.dot(&b.t());
515 let (_u, s, _vt) = ab
516 .svd(true)
517 .map_err(|e| SklearsError::InvalidOperation(format!("SVD failed: {:?}", e)))?;
518
519 let similarity = s.iter().map(|&x| x.powi(2)).sum::<Float>() / s.len() as Float;
520 Ok(similarity)
521 }
522
523 pub fn transform(&self, x: &Array2<Float>) -> Result<Array2<Float>> {
525 if let (Some(ref components), Some(ref mean)) = (&self.components, &self.mean) {
526 let centered = x - mean;
527 Ok(components.dot(¢ered.t()).t().to_owned())
528 } else {
529 Err(SklearsError::NotFitted {
530 operation: "transform".to_string(),
531 })
532 }
533 }
534
535 pub fn components(&self) -> Option<&Array2<Float>> {
537 self.components.as_ref()
538 }
539
540 pub fn is_drift_detected(&self) -> bool {
542 self.drift_detected
543 }
544
545 pub fn n_samples_seen(&self) -> usize {
547 self.n_samples_seen
548 }
549
550 pub fn transform_sample(&self, sample: &Array1<Float>) -> Result<Array1<Float>> {
552 if let (Some(ref components), Some(ref mean)) = (&self.components, &self.mean) {
553 let centered = sample - mean;
554 Ok(components.dot(¢ered))
555 } else {
556 Err(SklearsError::NotFitted {
557 operation: "transform_sample".to_string(),
558 })
559 }
560 }
561
562 pub fn mixing_matrix(&self) -> Option<&Array2<Float>> {
564 self.mixing_matrix.as_ref()
565 }
566
567 pub fn separate_sources(&self, mixed_sample: &Array1<Float>) -> Result<Array1<Float>> {
569 if let (Some(ref components), Some(ref mean)) = (&self.components, &self.mean) {
570 let centered = mixed_sample - mean;
571 Ok(components.dot(¢ered))
572 } else {
573 Err(SklearsError::NotFitted {
574 operation: "separate_sources".to_string(),
575 })
576 }
577 }
578
579 pub fn set_learning_rate(&mut self, learning_rate: Float) {
581 self.learning_rate = learning_rate;
582 }
583
584 pub fn learning_rate(&self) -> Float {
586 self.learning_rate
587 }
588
589 pub fn force_update(&mut self) -> Result<()> {
591 if self.n_samples_seen >= self.config.min_samples {
592 self.update_decomposition()
593 } else {
594 Err(SklearsError::InvalidParameter {
595 name: "min_samples".to_string(),
596 reason: "Not enough samples for decomposition update".to_string(),
597 })
598 }
599 }
600
601 pub fn reset(&mut self) {
603 self.components = None;
604 self.mixing_matrix = None;
605 self.mean = None;
606 self.n_samples_seen = 0;
607 self.data_buffer.clear();
608 self.previous_components = None;
609 self.drift_detected = false;
610 }
611}
612
613#[derive(Debug, Clone)]
615pub struct AdaptiveDecomposition {
616 #[allow(dead_code)]
617 config: StreamingConfig,
618 active_algorithm: String,
620 streaming_pca: Option<StreamingPCA>,
622 streaming_ica: Option<StreamingICA>,
623 reconstruction_errors: VecDeque<Float>,
625 adaptation_threshold: Float,
626}
627
628impl AdaptiveDecomposition {
629 pub fn new(n_components: usize) -> Self {
631 Self {
632 config: StreamingConfig::default(),
633 active_algorithm: "PCA".to_string(),
634 streaming_pca: Some(StreamingPCA::new(n_components)),
635 streaming_ica: Some(StreamingICA::new(n_components)),
636 reconstruction_errors: VecDeque::new(),
637 adaptation_threshold: 0.05,
638 }
639 }
640
641 pub fn partial_fit(&mut self, sample: &Array1<Float>) -> Result<()> {
643 if let Some(ref mut pca) = self.streaming_pca {
645 pca.partial_fit(sample)?;
646 }
647 if let Some(ref mut ica) = self.streaming_ica {
648 ica.partial_fit(sample)?;
649 }
650
651 self.evaluate_and_adapt(sample)?;
653
654 Ok(())
655 }
656
657 fn evaluate_and_adapt(&mut self, sample: &Array1<Float>) -> Result<()> {
659 let pca_error = self.compute_reconstruction_error_pca(sample)?;
661 let ica_error = self.compute_reconstruction_error_ica(sample)?;
662
663 self.reconstruction_errors
665 .push_back(pca_error.min(ica_error));
666 if self.reconstruction_errors.len() > 100 {
667 self.reconstruction_errors.pop_front();
668 }
669
670 if pca_error < ica_error - self.adaptation_threshold {
672 if self.active_algorithm != "PCA" {
673 println!("Switching to PCA (error: {pca_error:.4})");
674 self.active_algorithm = "PCA".to_string();
675 }
676 } else if ica_error < pca_error - self.adaptation_threshold
677 && self.active_algorithm != "ICA"
678 {
679 println!("Switching to ICA (error: {ica_error:.4})");
680 self.active_algorithm = "ICA".to_string();
681 }
682
683 Ok(())
684 }
685
686 fn compute_reconstruction_error_pca(&self, sample: &Array1<Float>) -> Result<Float> {
688 if let Some(ref pca) = self.streaming_pca {
689 if let (Some(components), Some(mean)) = (pca.components(), pca.mean.as_ref()) {
690 let centered = sample - mean;
691 let transformed = components.dot(¢ered);
692 let reconstructed = components.t().dot(&transformed) + mean;
693 let error = (sample - &reconstructed).mapv(|x| x.powi(2)).sum().sqrt();
694 return Ok(error);
695 }
696 }
697 Ok(Float::INFINITY)
698 }
699
700 fn compute_reconstruction_error_ica(&self, sample: &Array1<Float>) -> Result<Float> {
702 if let Some(ref ica) = self.streaming_ica {
703 if let (Some(components), Some(mean)) = (ica.components(), ica.mean.as_ref()) {
704 let centered = sample - mean;
705 let transformed = components.dot(¢ered);
706 let reconstructed = components.t().dot(&transformed) + mean;
707 let error = (sample - &reconstructed).mapv(|x| x.powi(2)).sum().sqrt();
708 return Ok(error);
709 }
710 }
711 Ok(Float::INFINITY)
712 }
713
714 pub fn active_algorithm(&self) -> &str {
716 &self.active_algorithm
717 }
718
719 pub fn average_reconstruction_error(&self) -> Float {
721 if self.reconstruction_errors.is_empty() {
722 0.0
723 } else {
724 self.reconstruction_errors.iter().sum::<Float>()
725 / self.reconstruction_errors.len() as Float
726 }
727 }
728}
729
730#[allow(non_snake_case)]
731#[cfg(test)]
732mod tests {
733 use super::*;
734
735 #[test]
736 fn test_streaming_pca_basic() {
737 let mut pca = StreamingPCA::new(2);
738
739 for i in 0..100 {
741 let sample = Array1::from_vec(vec![i as Float, (i * 2) as Float, (i * 3) as Float]);
742 pca.partial_fit(&sample).expect("sampling should succeed");
743 }
744
745 assert!(pca.n_samples_seen() == 100);
746 assert!(pca.components().is_some());
747 }
748
749 #[test]
750 fn test_streaming_ica_basic() {
751 let mut ica = StreamingICA::new(2);
752
753 for i in 0..100 {
755 let sample = Array1::from_vec(vec![i as Float, (i * 2) as Float]);
756 ica.partial_fit(&sample).expect("sampling should succeed");
757 }
758
759 assert!(ica.n_samples_seen() == 100);
760 assert!(ica.components().is_some());
761 }
762
763 #[test]
764 fn test_adaptive_decomposition() {
765 let mut adaptive = AdaptiveDecomposition::new(2);
766
767 for i in 0..100 {
769 let sample = Array1::from_vec(vec![i as Float, (i * 2) as Float]);
770 adaptive
771 .partial_fit(&sample)
772 .expect("sampling should succeed");
773 }
774
775 assert!(adaptive.active_algorithm() == "PCA" || adaptive.active_algorithm() == "ICA");
776 }
777}