1use scirs2_core::ndarray::{s, Array1, Array2, Axis};
8use scirs2_core::random::rngs::StdRng;
9use scirs2_core::random::{thread_rng, RngExt, SeedableRng};
10use scirs2_linalg::compat::{eigh, ArrayLinalgExt, UPLO};
11#[cfg(feature = "serde")]
12use serde::{Deserialize, Serialize};
13use sklears_core::{
14 error::{Result, SklearsError},
15 traits::{Fit, Transform, Untrained},
16};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
21pub enum ICAAlgorithm {
22 #[default]
24 Parallel,
25 Deflation,
27 Infomax,
29 NaturalGradient,
31 Temporal,
33 Constrained,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
40pub enum ICAFunction {
41 #[default]
43 Logcosh,
44 Exp,
46 Cube,
48}
49
50#[derive(Debug, Clone)]
52pub struct ICA<State = Untrained> {
53 pub n_components: Option<usize>,
55 pub algorithm: ICAAlgorithm,
57 pub fun: ICAFunction,
59 pub fun_args: f64,
61 pub max_iter: usize,
63 pub tol: f64,
65 pub whiten: bool,
67 pub random_state: Option<u64>,
69 pub learning_rate: f64,
71 pub temporal_window: Option<usize>,
73 pub momentum: f64,
75 pub constraint_matrix: Option<Array2<f64>>,
77 pub constraint_weight: f64,
79 pub constraint_tol: f64,
81
82 state: State,
84}
85
86#[derive(Debug, Clone)]
88#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
89pub struct TrainedICA {
90 pub components: Array2<f64>,
91 pub mixing: Array2<f64>,
92 pub mean: Array1<f64>,
93 pub whitening: Option<Array2<f64>>,
94 pub n_features_in: usize,
95 pub n_components: usize,
96 pub n_iter: usize,
97}
98
99impl ICA<Untrained> {
100 pub fn new() -> Self {
102 Self {
103 n_components: None,
104 algorithm: ICAAlgorithm::Parallel,
105 fun: ICAFunction::Logcosh,
106 fun_args: 1.0,
107 max_iter: 200,
108 tol: 1e-4,
109 whiten: true,
110 random_state: None,
111 learning_rate: 0.01,
112 temporal_window: None,
113 momentum: 0.9,
114 constraint_matrix: None,
115 constraint_weight: 1.0,
116 constraint_tol: 1e-6,
117 state: Untrained,
118 }
119 }
120
121 pub fn n_components(mut self, n_components: usize) -> Self {
123 self.n_components = Some(n_components);
124 self
125 }
126
127 pub fn algorithm(mut self, algorithm: ICAAlgorithm) -> Self {
129 self.algorithm = algorithm;
130 self
131 }
132
133 pub fn fun(mut self, fun: ICAFunction) -> Self {
135 self.fun = fun;
136 self
137 }
138
139 pub fn fun_args(mut self, fun_args: f64) -> Self {
141 self.fun_args = fun_args;
142 self
143 }
144
145 pub fn max_iter(mut self, max_iter: usize) -> Self {
147 self.max_iter = max_iter;
148 self
149 }
150
151 pub fn tol(mut self, tol: f64) -> Self {
153 self.tol = tol;
154 self
155 }
156
157 pub fn whiten(mut self, whiten: bool) -> Self {
159 self.whiten = whiten;
160 self
161 }
162
163 pub fn random_state(mut self, random_state: u64) -> Self {
165 self.random_state = Some(random_state);
166 self
167 }
168
169 pub fn learning_rate(mut self, learning_rate: f64) -> Self {
171 self.learning_rate = learning_rate;
172 self
173 }
174
175 pub fn temporal_window(mut self, window_size: usize) -> Self {
177 self.temporal_window = Some(window_size);
178 self
179 }
180
181 pub fn momentum(mut self, momentum: f64) -> Self {
183 self.momentum = momentum;
184 self
185 }
186
187 pub fn constraint_matrix(mut self, constraint_matrix: Array2<f64>) -> Self {
189 self.constraint_matrix = Some(constraint_matrix);
190 self
191 }
192
193 pub fn constraint_weight(mut self, constraint_weight: f64) -> Self {
195 self.constraint_weight = constraint_weight;
196 self
197 }
198
199 pub fn constraint_tol(mut self, constraint_tol: f64) -> Self {
201 self.constraint_tol = constraint_tol;
202 self
203 }
204}
205
206impl Fit<Array2<f64>, ()> for ICA<Untrained> {
207 type Fitted = ICA<TrainedICA>;
208
209 fn fit(self, x: &Array2<f64>, _y: &()) -> Result<Self::Fitted> {
210 let (n_samples, n_features) = x.dim();
211
212 if n_samples < 2 {
213 return Err(SklearsError::InvalidInput(
214 "ICA requires at least 2 samples".to_string(),
215 ));
216 }
217
218 let n_components = self.n_components.unwrap_or(n_features);
219
220 if n_components > n_features {
221 return Err(SklearsError::InvalidInput(
222 "n_components cannot be larger than n_features".to_string(),
223 ));
224 }
225
226 let mean = x
228 .mean_axis(Axis(0))
229 .expect("array should have elements for mean computation");
230 let x_centered = x - &mean;
231
232 let (x_whitened, whitening_matrix) = if self.whiten {
234 let (whitened, whitening) = self.whiten_data(&x_centered)?;
235 (whitened, Some(whitening))
236 } else {
237 (x_centered, None)
238 };
239
240 let mut rng = if let Some(seed) = self.random_state {
242 StdRng::seed_from_u64(seed)
243 } else {
244 StdRng::from_rng(&mut thread_rng())
245 };
246
247 let (components, n_iter) = match self.algorithm {
249 ICAAlgorithm::Parallel => self.parallel_fastica(&x_whitened, n_components, &mut rng)?,
250 ICAAlgorithm::Deflation => {
251 self.deflation_fastica(&x_whitened, n_components, &mut rng)?
252 }
253 ICAAlgorithm::Infomax => self.infomax_ica(&x_whitened, n_components, &mut rng)?,
254 ICAAlgorithm::NaturalGradient => {
255 self.natural_gradient_ica(&x_whitened, n_components, &mut rng)?
256 }
257 ICAAlgorithm::Temporal => self.temporal_ica(&x_whitened, n_components, &mut rng)?,
258 ICAAlgorithm::Constrained => {
259 self.constrained_ica(&x_whitened, n_components, &mut rng)?
260 }
261 };
262
263 let mixing = self.compute_mixing_matrix(&components)?;
265
266 Ok(ICA {
267 n_components: self.n_components,
268 algorithm: self.algorithm,
269 fun: self.fun,
270 fun_args: self.fun_args,
271 max_iter: self.max_iter,
272 tol: self.tol,
273 whiten: self.whiten,
274 random_state: self.random_state,
275 learning_rate: self.learning_rate,
276 temporal_window: self.temporal_window,
277 momentum: self.momentum,
278 constraint_matrix: self.constraint_matrix,
279 constraint_weight: self.constraint_weight,
280 constraint_tol: self.constraint_tol,
281 state: TrainedICA {
282 components,
283 mixing,
284 mean,
285 whitening: whitening_matrix,
286 n_features_in: n_features,
287 n_components,
288 n_iter,
289 },
290 })
291 }
292}
293
294impl Transform<Array2<f64>, Array2<f64>> for ICA<TrainedICA> {
295 fn transform(&self, x: &Array2<f64>) -> Result<Array2<f64>> {
296 let (_n_samples, n_features) = x.dim();
297
298 if n_features != self.state.n_features_in {
299 return Err(SklearsError::FeatureMismatch {
300 expected: self.state.n_features_in,
301 actual: n_features,
302 });
303 }
304
305 let x_centered = x - &self.state.mean;
307
308 let x_processed = if let Some(ref whitening) = self.state.whitening {
310 x_centered.dot(whitening)
311 } else {
312 x_centered
313 };
314
315 let x_transformed = x_processed.dot(&self.state.components.t());
317
318 Ok(x_transformed)
319 }
320}
321
322impl ICA<Untrained> {
323 fn whiten_data(&self, x: &Array2<f64>) -> Result<(Array2<f64>, Array2<f64>)> {
325 let (n_samples, n_features) = x.dim();
326
327 let cov = x.t().dot(x) / (n_samples - 1) as f64;
329
330 let (eigenvalues, eigenvectors) = eigh(&cov.view(), UPLO::Lower).map_err(|e| {
332 SklearsError::NumericalError(format!("Eigendecomposition failed: {:?}", e))
333 })?;
334
335 let mut sorted_indices: Vec<usize> = (0..n_features).collect();
337 sorted_indices.sort_by(|&i, &j| {
338 eigenvalues[j]
339 .partial_cmp(&eigenvalues[i])
340 .expect("operation should succeed")
341 });
342
343 let mut whitening = Array2::zeros((n_features, n_features));
345 for (i, &idx) in sorted_indices.iter().enumerate() {
346 if eigenvalues[idx] > 1e-12 {
347 let scale = 1.0 / eigenvalues[idx].sqrt();
348 for j in 0..n_features {
349 whitening[[j, i]] = eigenvectors[[j, idx]] * scale;
350 }
351 }
352 }
353
354 let x_whitened = x.dot(&whitening);
356
357 Ok((x_whitened, whitening))
358 }
359
360 fn parallel_fastica(
362 &self,
363 x: &Array2<f64>,
364 n_components: usize,
365 rng: &mut impl RngExt,
366 ) -> Result<(Array2<f64>, usize)> {
367 let (n_samples, n_features) = x.dim();
368
369 let mut w = Array2::<f64>::zeros((n_components, n_features));
371 for i in 0..n_components {
372 for j in 0..n_features {
373 w[[i, j]] = rng.random::<f64>() - 0.5;
374 }
375 }
376
377 self.orthogonalize(&mut w)?;
379
380 let mut n_iter = 0;
381 for iter in 0..self.max_iter {
382 n_iter = iter + 1;
383
384 let w_old = w.clone();
385
386 for i in 0..n_components {
388 let w_i = w.slice(s![i, ..]).to_owned();
389 let wx = x.dot(&w_i);
390
391 let (g, g_prime) = self.apply_nonlinearity(&wx);
392
393 let update1 = x.t().dot(&g) / n_samples as f64;
395 let update2 = &w_i * (g_prime.sum() / n_samples as f64);
396
397 for j in 0..n_features {
398 w[[i, j]] = update1[j] - update2[j];
399 }
400 }
401
402 self.orthogonalize(&mut w)?;
404
405 let mut max_diff: f64 = 0.0;
407 for i in 0..n_components {
408 for j in 0..n_features {
409 let diff = (w[[i, j]] - w_old[[i, j]]).abs();
410 max_diff = max_diff.max(diff);
411 }
412 }
413
414 if max_diff < self.tol {
415 break;
416 }
417 }
418
419 Ok((w, n_iter))
420 }
421
422 fn deflation_fastica(
424 &self,
425 x: &Array2<f64>,
426 n_components: usize,
427 rng: &mut impl RngExt,
428 ) -> Result<(Array2<f64>, usize)> {
429 let (n_samples, n_features) = x.dim();
430 let mut components = Array2::zeros((n_components, n_features));
431 let mut max_iter_reached = 0;
432
433 for comp in 0..n_components {
434 let mut w = Array1::<f64>::zeros(n_features);
436 for i in 0..n_features {
437 w[i] = rng.random::<f64>() - 0.5;
438 }
439
440 let norm = w.mapv(|x: f64| x * x).sum().sqrt();
442 w /= norm;
443
444 let mut n_iter = 0;
445 for iter in 0..self.max_iter {
446 n_iter = iter + 1;
447
448 let w_old = w.clone();
449
450 let wx = x.dot(&w);
451 let (g, g_prime) = self.apply_nonlinearity(&wx);
452
453 let update1 = x.t().dot(&g) / n_samples as f64;
455 let update2 = &w * (g_prime.sum() / n_samples as f64);
456
457 w = update1 - update2;
458
459 for prev_comp in 0..comp {
461 let prev_w = components.slice(s![prev_comp, ..]);
462 let projection = w.dot(&prev_w);
463 w = w - &prev_w * projection;
464 }
465
466 let norm = w.mapv(|x| x * x).sum().sqrt();
468 if norm < 1e-12 {
469 return Err(SklearsError::NumericalError(
470 "Component became zero during deflation".to_string(),
471 ));
472 }
473 w /= norm;
474
475 let diff = (&w - &w_old)
477 .mapv(|x| x.abs())
478 .fold(0.0f64, |acc, &x| acc.max(x));
479 if diff < self.tol {
480 break;
481 }
482 }
483
484 max_iter_reached = max_iter_reached.max(n_iter);
485
486 for i in 0..n_features {
488 components[[comp, i]] = w[i];
489 }
490 }
491
492 Ok((components, max_iter_reached))
493 }
494
495 fn apply_nonlinearity(&self, x: &Array1<f64>) -> (Array1<f64>, Array1<f64>) {
497 match self.fun {
498 ICAFunction::Logcosh => {
499 let alpha = self.fun_args;
500 let tanh_x = x.mapv(|val| (alpha * val).tanh());
501 let g = tanh_x.clone();
502 let g_prime = tanh_x.mapv(|val| alpha * (1.0 - val * val));
503 (g, g_prime)
504 }
505 ICAFunction::Exp => {
506 let g = x.mapv(|val| val * (-val * val / 2.0).exp());
507 let g_prime = x.mapv(|val| (1.0 - val * val) * (-val * val / 2.0).exp());
508 (g, g_prime)
509 }
510 ICAFunction::Cube => {
511 let g = x.mapv(|val| val * val * val);
512 let g_prime = x.mapv(|val| 3.0 * val * val);
513 (g, g_prime)
514 }
515 }
516 }
517
518 fn orthogonalize(&self, w: &mut Array2<f64>) -> Result<()> {
520 let (n_components, n_features) = w.dim();
521
522 for i in 0..n_components {
523 let mut norm = 0.0;
525 for j in 0..n_features {
526 norm += w[[i, j]] * w[[i, j]];
527 }
528 norm = norm.sqrt();
529
530 if norm < 1e-12 {
531 return Err(SklearsError::NumericalError(
532 "Zero vector encountered during orthogonalization".to_string(),
533 ));
534 }
535
536 for j in 0..n_features {
537 w[[i, j]] /= norm;
538 }
539
540 for k in 0..i {
542 let mut dot_product = 0.0;
543 for j in 0..n_features {
544 dot_product += w[[i, j]] * w[[k, j]];
545 }
546
547 for j in 0..n_features {
548 w[[i, j]] -= dot_product * w[[k, j]];
549 }
550
551 let mut norm = 0.0;
553 for j in 0..n_features {
554 norm += w[[i, j]] * w[[i, j]];
555 }
556 norm = norm.sqrt();
557
558 if norm < 1e-12 {
559 return Err(SklearsError::NumericalError(
560 "Linear dependence detected during orthogonalization".to_string(),
561 ));
562 }
563
564 for j in 0..n_features {
565 w[[i, j]] /= norm;
566 }
567 }
568 }
569
570 Ok(())
571 }
572
573 fn compute_mixing_matrix(&self, components: &Array2<f64>) -> Result<Array2<f64>> {
575 let (n_components, n_features) = components.dim();
576
577 if n_components == n_features {
579 let comp_inv = components.inv().map_err(|e| {
581 SklearsError::NumericalError(format!("Matrix inversion failed: {:?}", e))
582 })?;
583
584 Ok(comp_inv.t().to_owned())
586 } else {
587 let comp_t = components.t();
590 let gram = components.dot(&comp_t);
591
592 let gram_inv = gram.inv().map_err(|e| {
594 SklearsError::NumericalError(format!("Gram matrix inversion failed: {:?}", e))
595 })?;
596
597 let mixing = comp_t.dot(&gram_inv);
598 Ok(mixing)
599 }
600 }
601
602 fn infomax_ica(
607 &self,
608 x: &Array2<f64>,
609 n_components: usize,
610 rng: &mut impl RngExt,
611 ) -> Result<(Array2<f64>, usize)> {
612 let (n_samples, n_features) = x.dim();
613
614 let mut w = Array2::<f64>::zeros((n_components, n_features));
616 for i in 0..n_components {
617 for j in 0..n_features {
618 w[[i, j]] = rng.random::<f64>() - 0.5;
619 }
620 }
621
622 self.orthogonalize(&mut w)?;
624
625 let mut n_iter = 0;
626 for iter in 0..self.max_iter {
627 n_iter = iter + 1;
628
629 let w_old = w.clone();
630
631 let y = w.dot(&x.t());
633
634 let phi_y = y.mapv(|val| val.tanh());
636
637 let phi_y_deriv = y.mapv(|val| 1.0 - val.tanh().powi(2));
639
640 for i in 0..n_components {
643 for j in 0..n_features {
644 let mut delta = 0.0;
645
646 for k in 0..n_samples {
648 let _y_k = y[[i, k]];
649 let _phi_y_k = phi_y[[i, k]];
650
651 delta += x[[k, j]] * phi_y_deriv[[i, k]];
653
654 for l in 0..n_components {
656 if l != i {
657 delta -= phi_y[[l, k]] * y[[i, k]] * w[[l, j]];
658 }
659 }
660 }
661
662 w[[i, j]] += self.learning_rate * delta / n_samples as f64;
663 }
664 }
665
666 self.orthogonalize(&mut w)?;
668
669 let mut max_diff: f64 = 0.0;
671 for i in 0..n_components {
672 for j in 0..n_features {
673 let diff = (w[[i, j]] - w_old[[i, j]]).abs();
674 max_diff = max_diff.max(diff);
675 }
676 }
677
678 if max_diff < self.tol {
679 break;
680 }
681 }
682
683 Ok((w, n_iter))
684 }
685
686 fn natural_gradient_ica(
691 &self,
692 x: &Array2<f64>,
693 n_components: usize,
694 rng: &mut impl RngExt,
695 ) -> Result<(Array2<f64>, usize)> {
696 let (n_samples, n_features) = x.dim();
697
698 let mut w = Array2::<f64>::zeros((n_components, n_features));
700 for i in 0..n_components {
701 for j in 0..n_features {
702 w[[i, j]] = rng.random::<f64>() - 0.5;
703 }
704 }
705
706 let mut velocity = Array2::<f64>::zeros((n_components, n_features));
708
709 let mut n_iter = 0;
710 for iter in 0..self.max_iter {
711 n_iter = iter + 1;
712
713 let w_old = w.clone();
714
715 let y = w.dot(&x.t());
717
718 let g_y = y.mapv(|val| val.tanh());
720 let g_y_deriv = y.mapv(|val| 1.0 - val.tanh().powi(2));
721
722 let mut gradient = Array2::zeros((n_components, n_features));
724
725 for i in 0..n_components {
726 for j in 0..n_features {
727 let mut grad_sum = 0.0;
728
729 let mean_g_deriv = g_y_deriv
731 .row(i)
732 .mean()
733 .expect("array should have elements for mean computation");
734 grad_sum += mean_g_deriv
735 * x.column(j)
736 .mean()
737 .expect("array should have elements for mean computation");
738
739 for k in 0..n_components {
741 let mut corr_sum = 0.0;
742 for s in 0..n_samples {
743 corr_sum += g_y[[i, s]] * y[[k, s]];
744 }
745 corr_sum /= n_samples as f64;
746 grad_sum -= corr_sum * w[[k, j]];
747 }
748
749 gradient[[i, j]] = grad_sum;
750 }
751 }
752
753 for i in 0..n_components {
755 for j in 0..n_features {
756 velocity[[i, j]] =
757 self.momentum * velocity[[i, j]] + self.learning_rate * gradient[[i, j]];
758 w[[i, j]] += velocity[[i, j]];
759 }
760 }
761
762 let mut max_diff: f64 = 0.0;
764 for i in 0..n_components {
765 for j in 0..n_features {
766 let diff = (w[[i, j]] - w_old[[i, j]]).abs();
767 max_diff = max_diff.max(diff);
768 }
769 }
770
771 if max_diff < self.tol {
772 break;
773 }
774 }
775
776 Ok((w, n_iter))
777 }
778
779 fn temporal_ica(
784 &self,
785 x: &Array2<f64>,
786 n_components: usize,
787 rng: &mut impl RngExt,
788 ) -> Result<(Array2<f64>, usize)> {
789 let (n_samples, n_features) = x.dim();
790 let requested_window = self.temporal_window.unwrap_or(3);
791
792 if n_samples <= requested_window {
793 return Err(SklearsError::InvalidInput(
794 "Not enough samples for temporal ICA window".to_string(),
795 ));
796 }
797
798 let window_size = requested_window;
799
800 let mut w = Array2::<f64>::zeros((n_components, n_features));
802 for i in 0..n_components {
803 for j in 0..n_features {
804 w[[i, j]] = rng.random::<f64>() - 0.5;
805 }
806 }
807
808 let mut n_iter = 0;
809 for iter in 0..self.max_iter {
810 n_iter = iter + 1;
811
812 let w_old = w.clone();
813
814 let n_windows = n_samples - window_size;
816 let mut temporal_gradient = Array2::zeros((n_components, n_features));
817
818 for window_start in 0..n_windows {
819 let window_end = window_start + window_size;
820
821 let x_window = x.slice(scirs2_core::ndarray::s![window_start..window_end, ..]);
823
824 let y_window = w.dot(&x_window.t());
826
827 let _g_y = y_window.mapv(|val| val.tanh());
829 let g_y_deriv = y_window.mapv(|val| 1.0 - val.tanh().powi(2));
830
831 for i in 0..n_components {
834 let y_i = y_window.row(i);
835
836 let mut autocorr = 0.0;
838 for t in 1..window_size {
839 autocorr += y_i[t] * y_i[t - 1];
840 }
841 autocorr /= (window_size - 1) as f64;
842
843 for j in 0..n_features {
845 let mut temp_grad = 0.0;
846
847 for t in 0..window_size {
849 temp_grad += g_y_deriv[[i, t]] * x_window[[t, j]];
850 }
851 temp_grad /= window_size as f64;
852
853 let temporal_penalty = 0.1 * autocorr * autocorr;
856 temp_grad -= temporal_penalty;
857
858 temporal_gradient[[i, j]] += temp_grad;
859 }
860 }
861 }
862
863 temporal_gradient /= n_windows as f64;
865
866 for i in 0..n_components {
868 for j in 0..n_features {
869 w[[i, j]] += self.learning_rate * temporal_gradient[[i, j]];
870 }
871 }
872
873 self.orthogonalize(&mut w)?;
875
876 let mut max_diff: f64 = 0.0;
878 for i in 0..n_components {
879 for j in 0..n_features {
880 let diff = (w[[i, j]] - w_old[[i, j]]).abs();
881 max_diff = max_diff.max(diff);
882 }
883 }
884
885 if max_diff < self.tol {
886 break;
887 }
888 }
889
890 Ok((w, n_iter))
891 }
892
893 fn constrained_ica(
898 &self,
899 x: &Array2<f64>,
900 n_components: usize,
901 rng: &mut impl RngExt,
902 ) -> Result<(Array2<f64>, usize)> {
903 let (n_samples, n_features) = x.dim();
904
905 let constraint_matrix = match &self.constraint_matrix {
907 Some(matrix) => matrix,
908 None => {
909 return Err(SklearsError::InvalidInput(
910 "Constraint matrix must be provided for constrained ICA".to_string(),
911 ));
912 }
913 };
914
915 let (c_rows, c_cols) = constraint_matrix.dim();
917 if c_rows != n_components || c_cols != n_features {
918 return Err(SklearsError::InvalidInput(format!(
919 "Constraint matrix shape ({c_rows}, {c_cols}) must match (n_components, n_features) = ({n_components}, {n_features})"
920 )));
921 }
922
923 let mut w = Array2::<f64>::zeros((n_components, n_features));
925
926 for i in 0..n_components {
928 for j in 0..n_features {
929 w[[i, j]] = constraint_matrix[[i, j]] + 0.1 * (rng.random::<f64>() - 0.5);
931 }
932 }
933
934 for i in 0..n_components {
936 let mut row = w.row_mut(i);
937 let norm = row.dot(&row).sqrt();
938 if norm > 1e-12 {
939 row /= norm;
940 }
941 }
942
943 let mut n_iter = 0;
944 let mut prev_w = w.clone();
945
946 for iter in 0..self.max_iter {
947 n_iter = iter + 1;
948 prev_w.assign(&w);
949
950 for i in 0..n_components {
951 let mut w_i = w.row(i).to_owned();
953
954 let y = x.dot(&w_i);
956 let (_g_y, g_y_deriv) = self.apply_nonlinearity(&y);
957
958 let mut gradient = Array1::<f64>::zeros(n_features);
960 for (j, x_sample) in x.rows().into_iter().enumerate() {
961 gradient += &(g_y_deriv[j] * x_sample.to_owned());
962 }
963 gradient /= n_samples as f64;
964
965 let constraint_vec = constraint_matrix.row(i);
968 let constraint_diff = &constraint_vec.to_owned() - &w_i;
969 let constraint_gradient = self.constraint_weight * &constraint_diff;
970
971 w_i = &w_i + self.learning_rate * (&gradient + &constraint_gradient);
973
974 for j in 0..i {
976 let w_j = w.row(j);
977 let projection = w_i.dot(&w_j);
978 w_i = &w_i - projection * &w_j.to_owned();
979 }
980
981 let norm = w_i.dot(&w_i).sqrt();
983 if norm > 1e-12 {
984 w_i /= norm;
985 }
986
987 w.row_mut(i).assign(&w_i);
989 }
990
991 let mut max_change: f64 = 0.0;
993 for i in 0..n_components {
994 for j in 0..n_features {
995 let change = (w[[i, j]] - prev_w[[i, j]]).abs();
996 max_change = max_change.max(change);
997 }
998 }
999
1000 let mut constraint_error = 0.0;
1002 for i in 0..n_components {
1003 let constraint_vec = constraint_matrix.row(i);
1004 let w_vec = w.row(i);
1005 let diff = &constraint_vec.to_owned() - &w_vec.to_owned();
1006 constraint_error += diff.dot(&diff);
1007 }
1008 constraint_error = constraint_error.sqrt();
1009
1010 if max_change < self.tol && constraint_error < self.constraint_tol {
1011 break;
1012 }
1013 }
1014
1015 Ok((w, n_iter))
1016 }
1017}
1018
1019impl ICA<TrainedICA> {
1020 pub fn components(&self) -> &Array2<f64> {
1022 &self.state.components
1023 }
1024
1025 pub fn mixing(&self) -> &Array2<f64> {
1027 &self.state.mixing
1028 }
1029
1030 pub fn n_iter(&self) -> usize {
1032 self.state.n_iter
1033 }
1034
1035 pub fn inverse_transform(&self, x: &Array2<f64>) -> Result<Array2<f64>> {
1037 let (_n_samples, n_components) = x.dim();
1038
1039 if n_components != self.state.n_components {
1040 return Err(SklearsError::FeatureMismatch {
1041 expected: self.state.n_components,
1042 actual: n_components,
1043 });
1044 }
1045
1046 let x_mixed = x.dot(&self.state.mixing.t());
1048
1049 let x_processed = if let Some(ref whitening) = self.state.whitening {
1051 x_mixed.dot(&whitening.t())
1053 } else {
1054 x_mixed
1055 };
1056
1057 let x_reconstructed = x_processed + &self.state.mean;
1059
1060 Ok(x_reconstructed)
1061 }
1062}
1063
1064impl Default for ICA<Untrained> {
1065 fn default() -> Self {
1066 Self::new()
1067 }
1068}
1069
1070#[allow(non_snake_case)]
1071#[cfg(test)]
1072mod tests {
1073 use super::*;
1074 use approx::assert_abs_diff_eq;
1075 use scirs2_core::ndarray::array;
1076
1077 #[test]
1078 fn test_ica_creation() {
1079 let ica = ICA::new()
1080 .n_components(2)
1081 .algorithm(ICAAlgorithm::Parallel)
1082 .fun(ICAFunction::Logcosh)
1083 .max_iter(100)
1084 .tol(1e-6)
1085 .random_state(42);
1086
1087 assert_eq!(ica.n_components, Some(2));
1088 assert_eq!(ica.algorithm, ICAAlgorithm::Parallel);
1089 assert_eq!(ica.fun, ICAFunction::Logcosh);
1090 assert_eq!(ica.max_iter, 100);
1091 assert_abs_diff_eq!(ica.tol, 1e-6, epsilon = 1e-10);
1092 }
1093
1094 #[test]
1095 fn test_ica_fit_transform() {
1096 let x = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0], [4.0, 5.0], [5.0, 6.0],];
1098
1099 let ica = ICA::new().n_components(2).random_state(42);
1100
1101 let trained_ica = ica.fit(&x, &()).expect("model fitting should succeed");
1102 let x_transformed = trained_ica
1103 .transform(&x)
1104 .expect("transformation should succeed");
1105
1106 assert_eq!(x_transformed.dim(), (5, 2));
1107 assert_eq!(trained_ica.state.n_features_in, 2);
1108 assert_eq!(trained_ica.state.n_components, 2);
1109 }
1110
1111 #[test]
1112 fn test_ica_inverse_transform() {
1113 let x = array![[1.0, 1.0], [2.0, 2.0], [3.0, 3.0], [4.0, 4.0],];
1114
1115 let ica = ICA::new().n_components(2).random_state(123);
1116
1117 let trained_ica = ica.fit(&x, &()).expect("model fitting should succeed");
1118 let x_transformed = trained_ica
1119 .transform(&x)
1120 .expect("transformation should succeed");
1121 let x_reconstructed = trained_ica
1122 .inverse_transform(&x_transformed)
1123 .expect("operation should succeed");
1124
1125 assert_eq!(x_reconstructed.dim(), x.dim());
1126
1127 for i in 0..x.nrows() {
1129 for j in 0..x.ncols() {
1130 assert!((x_reconstructed[[i, j]] - x[[i, j]]).abs() < 5.0);
1131 }
1132 }
1133 }
1134
1135 #[test]
1136 fn test_ica_different_algorithms() {
1137 let x = array![
1138 [1.0, 2.0, 3.0],
1139 [2.0, 3.0, 4.0],
1140 [3.0, 4.0, 5.0],
1141 [4.0, 5.0, 6.0],
1142 [5.0, 6.0, 7.0],
1143 ];
1144
1145 let algorithms = vec![ICAAlgorithm::Parallel, ICAAlgorithm::Deflation];
1146
1147 for algorithm in algorithms {
1148 let ica = ICA::new()
1149 .n_components(3)
1150 .algorithm(algorithm)
1151 .random_state(42);
1152
1153 let trained_ica = ica.fit(&x, &()).expect("model fitting should succeed");
1154 let x_transformed = trained_ica
1155 .transform(&x)
1156 .expect("transformation should succeed");
1157
1158 assert_eq!(x_transformed.dim(), (5, 3));
1159 assert_eq!(trained_ica.state.n_components, 3);
1160 }
1161 }
1162
1163 #[test]
1164 fn test_ica_different_functions() {
1165 let x = array![
1166 [1.0, 2.0],
1167 [3.0, 1.0],
1168 [2.0, 4.0],
1169 [5.0, 2.0],
1170 [4.0, 6.0],
1171 [6.0, 3.0],
1172 [5.0, 8.0],
1173 [8.0, 4.0],
1174 [7.0, 10.0],
1175 [10.0, 5.0]
1176 ];
1177
1178 let functions = vec![ICAFunction::Logcosh, ICAFunction::Exp, ICAFunction::Cube];
1179
1180 for fun in functions {
1181 let ica = ICA::new().n_components(2).fun(fun).random_state(42);
1182
1183 let trained_ica = ica.fit(&x, &()).expect("model fitting should succeed");
1184 let x_transformed = trained_ica
1185 .transform(&x)
1186 .expect("transformation should succeed");
1187
1188 assert_eq!(x_transformed.dim(), (10, 2));
1189 }
1190 }
1191
1192 #[test]
1193 fn test_ica_error_cases() {
1194 let x_small = array![[1.0, 2.0]]; let ica = ICA::new();
1196 let result = ica.fit(&x_small, &());
1197 assert!(result.is_err());
1198
1199 let x = array![[1.0, 2.0], [3.0, 4.0]];
1200 let ica = ICA::new().n_components(3); let result = ica.fit(&x, &());
1202 assert!(result.is_err());
1203 }
1204
1205 #[test]
1206 fn test_ica_whiten_option() {
1207 let x = array![[1.0, 2.0], [2.0, 3.0], [3.0, 4.0], [4.0, 5.0],];
1208
1209 let ica_whiten = ICA::new().n_components(2).whiten(true).random_state(42);
1211
1212 let trained_whiten = ica_whiten
1213 .fit(&x, &())
1214 .expect("model fitting should succeed");
1215 assert!(trained_whiten.state.whitening.is_some());
1216
1217 let ica_no_whiten = ICA::new().n_components(2).whiten(false).random_state(42);
1219
1220 let trained_no_whiten = ica_no_whiten
1221 .fit(&x, &())
1222 .expect("model fitting should succeed");
1223 assert!(trained_no_whiten.state.whitening.is_none());
1224 }
1225
1226 #[test]
1227 fn test_ica_infomax_algorithm() {
1228 let x = array![
1229 [1.0, 2.0],
1230 [3.0, 1.0],
1231 [2.0, 4.0],
1232 [5.0, 2.0],
1233 [4.0, 6.0],
1234 [6.0, 3.0],
1235 ];
1236
1237 let ica = ICA::new()
1238 .n_components(2)
1239 .algorithm(ICAAlgorithm::Infomax)
1240 .learning_rate(0.01)
1241 .max_iter(50) .random_state(42);
1243
1244 let trained_ica = ica.fit(&x, &()).expect("model fitting should succeed");
1245 let x_transformed = trained_ica
1246 .transform(&x)
1247 .expect("transformation should succeed");
1248
1249 assert_eq!(x_transformed.dim(), (6, 2));
1250 assert_eq!(trained_ica.state.n_components, 2);
1251
1252 for &val in x_transformed.iter() {
1254 assert!(val.is_finite());
1255 }
1256 }
1257
1258 #[test]
1259 fn test_ica_natural_gradient_algorithm() {
1260 let x = array![[1.0, 2.0], [3.0, 1.0], [2.0, 4.0], [5.0, 2.0], [4.0, 6.0],];
1261
1262 let ica = ICA::new()
1263 .n_components(2)
1264 .algorithm(ICAAlgorithm::NaturalGradient)
1265 .learning_rate(0.005)
1266 .momentum(0.9)
1267 .max_iter(30) .random_state(123);
1269
1270 let trained_ica = ica.fit(&x, &()).expect("model fitting should succeed");
1271 let x_transformed = trained_ica
1272 .transform(&x)
1273 .expect("transformation should succeed");
1274
1275 assert_eq!(x_transformed.dim(), (5, 2));
1276 assert_eq!(trained_ica.state.n_components, 2);
1277
1278 for &val in x_transformed.iter() {
1280 assert!(val.is_finite());
1281 }
1282 }
1283
1284 #[test]
1285 fn test_ica_temporal_algorithm() {
1286 let x = array![
1288 [1.0, 2.0],
1289 [1.1, 2.1],
1290 [1.2, 2.2],
1291 [2.0, 1.0],
1292 [2.1, 1.1],
1293 [2.2, 1.2],
1294 [3.0, 3.0],
1295 [3.1, 3.1],
1296 ];
1297
1298 let ica = ICA::new()
1299 .n_components(2)
1300 .algorithm(ICAAlgorithm::Temporal)
1301 .temporal_window(3)
1302 .learning_rate(0.01)
1303 .max_iter(20) .random_state(456);
1305
1306 let trained_ica = ica.fit(&x, &()).expect("model fitting should succeed");
1307 let x_transformed = trained_ica
1308 .transform(&x)
1309 .expect("transformation should succeed");
1310
1311 assert_eq!(x_transformed.dim(), (8, 2));
1312 assert_eq!(trained_ica.state.n_components, 2);
1313
1314 for &val in x_transformed.iter() {
1316 assert!(val.is_finite());
1317 }
1318 }
1319
1320 #[test]
1321 fn test_ica_temporal_insufficient_data() {
1322 let x = array![[1.0, 2.0], [3.0, 4.0]]; let ica = ICA::new()
1326 .n_components(2)
1327 .algorithm(ICAAlgorithm::Temporal)
1328 .temporal_window(5) .random_state(42);
1330
1331 let result = ica.fit(&x, &());
1332 assert!(result.is_err());
1333 }
1334
1335 #[test]
1336 fn test_ica_new_parameters() {
1337 let ica = ICA::new()
1338 .learning_rate(0.05)
1339 .momentum(0.8)
1340 .temporal_window(5);
1341
1342 assert_eq!(ica.learning_rate, 0.05);
1343 assert_eq!(ica.momentum, 0.8);
1344 assert_eq!(ica.temporal_window, Some(5));
1345 }
1346
1347 #[test]
1348 fn test_constrained_ica_basic() {
1349 let x = array![
1351 [1.0, 0.5, 0.3],
1352 [0.8, 1.2, 0.7],
1353 [1.1, 0.9, 0.4],
1354 [0.9, 1.1, 0.6],
1355 [1.2, 0.7, 0.5],
1356 ];
1357
1358 let constraint_matrix = array![
1360 [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], ];
1363
1364 let ica = ICA::new()
1365 .n_components(2)
1366 .algorithm(ICAAlgorithm::Constrained)
1367 .constraint_matrix(constraint_matrix)
1368 .constraint_weight(0.5)
1369 .constraint_tol(1e-4)
1370 .max_iter(50)
1371 .random_state(42);
1372
1373 let trained_ica = ica.fit(&x, &()).expect("model fitting should succeed");
1374 let x_transformed = trained_ica
1375 .transform(&x)
1376 .expect("transformation should succeed");
1377
1378 assert_eq!(x_transformed.dim(), (5, 2));
1379 assert_eq!(trained_ica.state.n_components, 2);
1380
1381 for &val in x_transformed.iter() {
1383 assert!(val.is_finite());
1384 }
1385
1386 let components = trained_ica.components();
1388 assert!(components[[0, 0]].abs() > components[[0, 1]].abs()); assert!(components[[1, 1]].abs() > components[[1, 0]].abs()); }
1391
1392 #[test]
1393 fn test_constrained_ica_missing_constraint() {
1394 let x = array![[1.0, 0.5], [0.8, 1.2], [1.1, 0.9], [0.9, 1.1],];
1396
1397 let ica = ICA::new()
1398 .n_components(2)
1399 .algorithm(ICAAlgorithm::Constrained)
1400 .random_state(42);
1401
1402 let result = ica.fit(&x, &());
1403 assert!(result.is_err());
1404 }
1405
1406 #[test]
1407 fn test_constrained_ica_invalid_constraint_shape() {
1408 let x = array![
1410 [1.0, 0.5, 0.3],
1411 [0.8, 1.2, 0.7],
1412 [1.1, 0.9, 0.4],
1413 [0.9, 1.1, 0.6],
1414 ];
1415
1416 let constraint_matrix = array![[1.0, 0.0], [0.0, 1.0],];
1418
1419 let ica = ICA::new()
1420 .n_components(2)
1421 .algorithm(ICAAlgorithm::Constrained)
1422 .constraint_matrix(constraint_matrix)
1423 .random_state(42);
1424
1425 let result = ica.fit(&x, &());
1426 assert!(result.is_err());
1427 }
1428
1429 #[test]
1430 fn test_constrained_ica_parameters() {
1431 let constraint_matrix = array![[1.0, 0.0], [0.0, 1.0]];
1432
1433 let ica = ICA::new()
1434 .constraint_matrix(constraint_matrix.clone())
1435 .constraint_weight(2.0)
1436 .constraint_tol(1e-5);
1437
1438 assert_eq!(ica.constraint_weight, 2.0);
1439 assert_eq!(ica.constraint_tol, 1e-5);
1440 assert!(ica.constraint_matrix.is_some());
1441
1442 let stored_matrix = ica
1443 .constraint_matrix
1444 .as_ref()
1445 .expect("operation should succeed");
1446 assert_eq!(stored_matrix.dim(), (2, 2));
1447 }
1448}