Skip to main content

sklears_decomposition/
ica.rs

1//! Independent Component Analysis (ICA) implementation.
2//!
3//! ICA is a statistical technique for separating a multivariate signal into
4//! additive independent components. It assumes that observed signals are linear
5//! mixtures of independent source signals.
6
7use 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/// ICA algorithm variants
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
21pub enum ICAAlgorithm {
22    /// Parallel FastICA algorithm
23    #[default]
24    Parallel,
25    /// Deflation FastICA algorithm  
26    Deflation,
27    /// Infomax ICA algorithm using maximum likelihood
28    Infomax,
29    /// Natural gradient ICA algorithm
30    NaturalGradient,
31    /// Temporal ICA for time series data
32    Temporal,
33    /// Constrained ICA for semi-supervised learning
34    Constrained,
35}
36
37/// Non-linearity functions for ICA
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
40pub enum ICAFunction {
41    /// Logcosh function: G(u) = tanh(a*u), g(u) = a*(1-tanh²(a*u))
42    #[default]
43    Logcosh,
44    /// Exponential function: G(u) = u*exp(-u²/2), g(u) = (1-u²)*exp(-u²/2)
45    Exp,
46    /// Cubic function: G(u) = u³, g(u) = 3*u²
47    Cube,
48}
49
50/// Independent Component Analysis transformer
51#[derive(Debug, Clone)]
52pub struct ICA<State = Untrained> {
53    /// Number of components to extract
54    pub n_components: Option<usize>,
55    /// ICA algorithm to use
56    pub algorithm: ICAAlgorithm,
57    /// Non-linearity function
58    pub fun: ICAFunction,
59    /// Parameter for logcosh function
60    pub fun_args: f64,
61    /// Maximum number of iterations
62    pub max_iter: usize,
63    /// Tolerance for convergence
64    pub tol: f64,
65    /// Whether to whiten the data
66    pub whiten: bool,
67    /// Random state for reproducibility
68    pub random_state: Option<u64>,
69    /// Learning rate for gradient-based algorithms
70    pub learning_rate: f64,
71    /// Temporal window size for temporal ICA
72    pub temporal_window: Option<usize>,
73    /// Momentum factor for natural gradient
74    pub momentum: f64,
75    /// Prior knowledge matrix for constrained ICA
76    pub constraint_matrix: Option<Array2<f64>>,
77    /// Constraint weight for regularization
78    pub constraint_weight: f64,
79    /// Constraint tolerance for convergence
80    pub constraint_tol: f64,
81
82    /// Trained state
83    state: State,
84}
85
86/// Trained ICA state
87#[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    /// Create a new ICA transformer
101    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    /// Set the number of components
122    pub fn n_components(mut self, n_components: usize) -> Self {
123        self.n_components = Some(n_components);
124        self
125    }
126
127    /// Set the algorithm
128    pub fn algorithm(mut self, algorithm: ICAAlgorithm) -> Self {
129        self.algorithm = algorithm;
130        self
131    }
132
133    /// Set the non-linearity function
134    pub fn fun(mut self, fun: ICAFunction) -> Self {
135        self.fun = fun;
136        self
137    }
138
139    /// Set the function parameter
140    pub fn fun_args(mut self, fun_args: f64) -> Self {
141        self.fun_args = fun_args;
142        self
143    }
144
145    /// Set maximum iterations
146    pub fn max_iter(mut self, max_iter: usize) -> Self {
147        self.max_iter = max_iter;
148        self
149    }
150
151    /// Set tolerance
152    pub fn tol(mut self, tol: f64) -> Self {
153        self.tol = tol;
154        self
155    }
156
157    /// Set whether to whiten data
158    pub fn whiten(mut self, whiten: bool) -> Self {
159        self.whiten = whiten;
160        self
161    }
162
163    /// Set random state
164    pub fn random_state(mut self, random_state: u64) -> Self {
165        self.random_state = Some(random_state);
166        self
167    }
168
169    /// Set learning rate for gradient-based algorithms
170    pub fn learning_rate(mut self, learning_rate: f64) -> Self {
171        self.learning_rate = learning_rate;
172        self
173    }
174
175    /// Set temporal window size for temporal ICA
176    pub fn temporal_window(mut self, window_size: usize) -> Self {
177        self.temporal_window = Some(window_size);
178        self
179    }
180
181    /// Set momentum factor for natural gradient
182    pub fn momentum(mut self, momentum: f64) -> Self {
183        self.momentum = momentum;
184        self
185    }
186
187    /// Set constraint matrix for constrained ICA
188    pub fn constraint_matrix(mut self, constraint_matrix: Array2<f64>) -> Self {
189        self.constraint_matrix = Some(constraint_matrix);
190        self
191    }
192
193    /// Set constraint weight for regularization
194    pub fn constraint_weight(mut self, constraint_weight: f64) -> Self {
195        self.constraint_weight = constraint_weight;
196        self
197    }
198
199    /// Set constraint tolerance for convergence
200    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        // Center the data
227        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        // Whiten the data if requested
233        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        // Initialize random number generator with optional seed for reproducibility
241        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        // Run ICA algorithm
248        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        // Compute mixing matrix (pseudoinverse of components)
264        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        // Center the data
306        let x_centered = x - &self.state.mean;
307
308        // Apply whitening if it was used during fitting
309        let x_processed = if let Some(ref whitening) = self.state.whitening {
310            x_centered.dot(whitening)
311        } else {
312            x_centered
313        };
314
315        // Apply ICA transformation
316        let x_transformed = x_processed.dot(&self.state.components.t());
317
318        Ok(x_transformed)
319    }
320}
321
322impl ICA<Untrained> {
323    /// Whiten the data using eigendecomposition
324    fn whiten_data(&self, x: &Array2<f64>) -> Result<(Array2<f64>, Array2<f64>)> {
325        let (n_samples, n_features) = x.dim();
326
327        // Compute covariance matrix
328        let cov = x.t().dot(x) / (n_samples - 1) as f64;
329
330        // Eigendecomposition using scirs2-linalg
331        let (eigenvalues, eigenvectors) = eigh(&cov.view(), UPLO::Lower).map_err(|e| {
332            SklearsError::NumericalError(format!("Eigendecomposition failed: {:?}", e))
333        })?;
334
335        // Sort eigenvalues in descending order
336        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        // Build whitening matrix
344        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        // Apply whitening
355        let x_whitened = x.dot(&whitening);
356
357        Ok((x_whitened, whitening))
358    }
359
360    /// Parallel FastICA algorithm
361    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        // Initialize weight matrix randomly
370        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        // Orthogonalize initial weights
378        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            // Update weights in parallel for all components
387            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                // FastICA update rule
394                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            // Orthogonalize weights
403            self.orthogonalize(&mut w)?;
404
405            // Check convergence
406            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    /// Deflation FastICA algorithm
423    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            // Initialize weight vector randomly
435            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            // Normalize
441            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                // FastICA update rule
454                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                // Orthogonalize against previous components
460                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                // Normalize
467                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                // Check convergence
476                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            // Store component
487            for i in 0..n_features {
488                components[[comp, i]] = w[i];
489            }
490        }
491
492        Ok((components, max_iter_reached))
493    }
494
495    /// Apply non-linearity function and its derivative
496    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    /// Orthogonalize weight matrix using Gram-Schmidt process
519    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            // Normalize current row
524            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            // Orthogonalize against previous rows
541            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                // Renormalize
552                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    /// Compute mixing matrix as pseudoinverse of components
574    fn compute_mixing_matrix(&self, components: &Array2<f64>) -> Result<Array2<f64>> {
575        let (n_components, n_features) = components.dim();
576
577        // For square matrices, compute inverse directly
578        if n_components == n_features {
579            // Use scirs2-linalg for inversion
580            let comp_inv = components.inv().map_err(|e| {
581                SklearsError::NumericalError(format!("Matrix inversion failed: {:?}", e))
582            })?;
583
584            // Transpose to get mixing matrix
585            Ok(comp_inv.t().to_owned())
586        } else {
587            // For non-square matrices, compute pseudoinverse
588            // mixing = (components^T * components)^(-1) * components^T
589            let comp_t = components.t();
590            let gram = components.dot(&comp_t);
591
592            // Use scirs2-linalg for inversion
593            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    /// Infomax ICA algorithm using maximum likelihood estimation
603    ///
604    /// This algorithm maximizes the information flow through the network by
605    /// maximizing the joint entropy of the outputs using natural gradient.
606    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        // Initialize weight matrix randomly
615        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        // Orthogonalize initial weights
623        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            // Compute outputs: y = W * x
632            let y = w.dot(&x.t());
633
634            // Compute sigmoid activation: φ(y) = tanh(y)
635            let phi_y = y.mapv(|val| val.tanh());
636
637            // Compute derivative: φ'(y) = 1 - tanh²(y)
638            let phi_y_deriv = y.mapv(|val| 1.0 - val.tanh().powi(2));
639
640            // Natural gradient update: ΔW = η[I - φ(y)y^T]W
641            // where η is the learning rate
642            for i in 0..n_components {
643                for j in 0..n_features {
644                    let mut delta = 0.0;
645
646                    // Compute the term: [I - φ(y)y^T]
647                    for k in 0..n_samples {
648                        let _y_k = y[[i, k]];
649                        let _phi_y_k = phi_y[[i, k]];
650
651                        // Diagonal term: I
652                        delta += x[[k, j]] * phi_y_deriv[[i, k]];
653
654                        // Off-diagonal term: -φ(y)y^T
655                        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            // Orthogonalize weights to maintain independence
667            self.orthogonalize(&mut w)?;
668
669            // Check convergence
670            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    /// Natural gradient ICA algorithm
687    ///
688    /// Uses natural gradient descent with momentum for efficient learning.
689    /// This algorithm exploits the natural geometry of the parameter space.
690    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        // Initialize weight matrix randomly
699        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        // Initialize momentum terms
707        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            // Compute outputs: y = W * x^T
716            let y = w.dot(&x.t());
717
718            // Apply non-linearity (tanh) and its derivative
719            let g_y = y.mapv(|val| val.tanh());
720            let g_y_deriv = y.mapv(|val| 1.0 - val.tanh().powi(2));
721
722            // Compute natural gradient: ∇W = E[g'(y)]I - E[g(y)y^T]W
723            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                    // First term: E[g'(y)]δ_ij * x_j
730                    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                    // Second term: -E[g(y_i) * y_k] * w_kj
740                    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            // Apply momentum: v = momentum * v + learning_rate * gradient
754            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            // Check convergence
763            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    /// Temporal ICA algorithm for time series data
780    ///
781    /// Exploits temporal structure in the data by incorporating
782    /// temporal dependencies and autocorrelations.
783    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        // Initialize weight matrix randomly
801        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            // Create temporal windows
815            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                // Extract window data
822                let x_window = x.slice(scirs2_core::ndarray::s![window_start..window_end, ..]);
823
824                // Compute outputs for current window
825                let y_window = w.dot(&x_window.t());
826
827                // Apply non-linearity
828                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                // Compute temporal contrast function
832                // This encourages components to have different temporal structures
833                for i in 0..n_components {
834                    let y_i = y_window.row(i);
835
836                    // Compute autocorrelation at lag 1
837                    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                    // Temporal gradient update
844                    for j in 0..n_features {
845                        let mut temp_grad = 0.0;
846
847                        // Standard ICA gradient
848                        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                        // Temporal regularization term
854                        // Encourage temporal independence by penalizing autocorrelation
855                        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            // Average gradient over all windows
864            temporal_gradient /= n_windows as f64;
865
866            // Update weights
867            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            // Orthogonalize to maintain component independence
874            self.orthogonalize(&mut w)?;
875
876            // Check convergence
877            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    /// Constrained ICA algorithm for semi-supervised learning
894    ///
895    /// Incorporates prior knowledge through constraint matrix to guide
896    /// the component extraction process towards desired solutions.
897    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        // Check if constraint matrix is provided
906        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        // Validate constraint matrix dimensions
916        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        // Initialize weight matrix using constraint as guidance
924        let mut w = Array2::<f64>::zeros((n_components, n_features));
925
926        // Initialize with constrained initialization
927        for i in 0..n_components {
928            for j in 0..n_features {
929                // Start with constraint matrix and add small random perturbation
930                w[[i, j]] = constraint_matrix[[i, j]] + 0.1 * (rng.random::<f64>() - 0.5);
931            }
932        }
933
934        // Normalize initial weight vectors
935        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                // Extract current component weight vector
952                let mut w_i = w.row(i).to_owned();
953
954                // FastICA update with constraint regularization
955                let y = x.dot(&w_i);
956                let (_g_y, g_y_deriv) = self.apply_nonlinearity(&y);
957
958                // Standard FastICA gradient
959                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                // Constraint regularization term
966                // Pull the weight vector towards the constraint
967                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                // Combined gradient update
972                w_i = &w_i + self.learning_rate * (&gradient + &constraint_gradient);
973
974                // Orthogonalization against previous components
975                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                // Normalize
982                let norm = w_i.dot(&w_i).sqrt();
983                if norm > 1e-12 {
984                    w_i /= norm;
985                }
986
987                // Update weight matrix
988                w.row_mut(i).assign(&w_i);
989            }
990
991            // Check convergence based on weight changes
992            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            // Also check constraint satisfaction
1001            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    /// Get the unmixing matrix (components)
1021    pub fn components(&self) -> &Array2<f64> {
1022        &self.state.components
1023    }
1024
1025    /// Get the mixing matrix
1026    pub fn mixing(&self) -> &Array2<f64> {
1027        &self.state.mixing
1028    }
1029
1030    /// Get the number of iterations performed
1031    pub fn n_iter(&self) -> usize {
1032        self.state.n_iter
1033    }
1034
1035    /// Inverse transform (reconstruct original signal from components)
1036    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        // Apply mixing matrix
1047        let x_mixed = x.dot(&self.state.mixing.t());
1048
1049        // Apply inverse whitening if whitening was used
1050        let x_processed = if let Some(ref whitening) = self.state.whitening {
1051            // Inverse whitening: multiply by whitening^T
1052            x_mixed.dot(&whitening.t())
1053        } else {
1054            x_mixed
1055        };
1056
1057        // Add back the mean
1058        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        // Create mixed signals (simple 2D case)
1097        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        // The reconstruction should be close to the original (may not be exact due to numerical precision)
1128        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]]; // Only 1 sample
1195        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); // More components than features
1201        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        // Test with whitening
1210        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        // Test without whitening
1218        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) // Reduced for faster test
1242            .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        // Check that all values are finite
1253        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) // Reduced for faster test
1268            .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        // Check that all values are finite
1279        for &val in x_transformed.iter() {
1280            assert!(val.is_finite());
1281        }
1282    }
1283
1284    #[test]
1285    fn test_ica_temporal_algorithm() {
1286        // Create time series data with temporal structure
1287        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) // Reduced for faster test
1304            .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        // Check that all values are finite
1315        for &val in x_transformed.iter() {
1316            assert!(val.is_finite());
1317        }
1318    }
1319
1320    #[test]
1321    fn test_ica_temporal_insufficient_data() {
1322        // Test with insufficient data for temporal window
1323        let x = array![[1.0, 2.0], [3.0, 4.0]]; // Only 2 samples
1324
1325        let ica = ICA::new()
1326            .n_components(2)
1327            .algorithm(ICAAlgorithm::Temporal)
1328            .temporal_window(5) // Window larger than data
1329            .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        // Test basic constrained ICA functionality
1350        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        // Create a constraint matrix (prior knowledge about components)
1359        let constraint_matrix = array![
1360            [1.0, 0.0, 0.0], // First component should focus on first feature
1361            [0.0, 1.0, 0.0], // Second component should focus on second feature
1362        ];
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        // Check that all values are finite
1382        for &val in x_transformed.iter() {
1383            assert!(val.is_finite());
1384        }
1385
1386        // Check that constraints are reasonably satisfied
1387        let components = trained_ica.components();
1388        assert!(components[[0, 0]].abs() > components[[0, 1]].abs()); // First component prefers first feature
1389        assert!(components[[1, 1]].abs() > components[[1, 0]].abs()); // Second component prefers second feature
1390    }
1391
1392    #[test]
1393    fn test_constrained_ica_missing_constraint() {
1394        // Test that constrained ICA fails without constraint matrix
1395        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        // Test with incorrect constraint matrix shape
1409        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        // Wrong shape constraint matrix (should be 2x3, not 2x2)
1417        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}