Skip to main content

sklears_decomposition/
tensor_decomposition.rs

1//! Tensor decomposition algorithms
2//!
3//! This module provides tensor decomposition methods including:
4//! - CP (CANDECOMP/PARAFAC) decomposition for multi-way data analysis
5//! - Tucker decomposition for multilinear algebra applications
6//! - Higher-order SVD for tensor dimensionality reduction
7
8use scirs2_core::ndarray::{Array1, Array2, Array3};
9use scirs2_core::random::rngs::StdRng;
10use scirs2_core::random::{rng as make_rng, RngExt, SeedableRng};
11use scirs2_linalg::compat::{svd, ArrayLinalgExt};
12#[cfg(feature = "serde")]
13use serde::{Deserialize, Serialize};
14use sklears_core::traits::Fit;
15use sklears_core::{
16    error::{Result, SklearsError},
17    traits::Untrained,
18};
19
20/// Type alias for CP decomposition result
21type CPResult = Result<(Vec<Array2<f64>>, Array1<f64>, usize, f64)>;
22
23/// Type alias for Tucker decomposition result  
24type TuckerResult = Result<(Array3<f64>, Vec<Array2<f64>>, usize, f64)>;
25
26/// CP decomposition algorithm variants
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
29pub enum CPAlgorithm {
30    /// Alternating Least Squares (ALS) algorithm
31    #[default]
32    ALS,
33    /// Non-negative CP decomposition using multiplicative updates
34    NonNegative,
35    /// Robust CP decomposition with outlier handling
36    Robust,
37}
38
39/// Tucker decomposition algorithm variants  
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
42pub enum TuckerAlgorithm {
43    /// Higher-order SVD (HOSVD) algorithm
44    #[default]
45    HOSVD,
46    /// Alternating Least Squares for Tucker decomposition
47    ALS,
48    /// Sequential unfolding SVD
49    SequentialSVD,
50}
51
52/// CP (CANDECOMP/PARAFAC) Decomposition
53///
54/// Decomposes a tensor into a sum of rank-1 tensors:
55/// X ≈ Σᵢ λᵢ aᵢ ⊗ bᵢ ⊗ cᵢ
56#[derive(Debug, Clone)]
57pub struct CPDecomposition<State = Untrained> {
58    /// Number of components (rank of decomposition)
59    pub n_components: usize,
60    /// Algorithm variant to use
61    pub algorithm: CPAlgorithm,
62    /// Maximum number of iterations
63    pub max_iter: usize,
64    /// Convergence tolerance
65    pub tol: f64,
66    /// Random state for reproducibility
67    pub random_state: Option<u64>,
68    /// Learning rate for gradient-based methods
69    pub learning_rate: f64,
70    /// Regularization parameter
71    pub regularization: f64,
72    /// Whether to normalize factors
73    pub normalize_factors: bool,
74
75    /// Trained state
76    #[allow(dead_code)]
77    state: State,
78}
79
80/// Trained CP decomposition state
81#[derive(Debug, Clone)]
82#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
83pub struct TrainedCP {
84    pub factors: Vec<Array2<f64>>,
85    pub weights: Array1<f64>,
86    pub tensor_shape: Vec<usize>,
87    pub n_components: usize,
88    pub n_iter: usize,
89    pub reconstruction_error: f64,
90}
91
92/// Tucker Decomposition
93///
94/// Decomposes a tensor into a core tensor multiplied by factor matrices:
95/// X ≈ G ×₁ A ×₂ B ×₃ C
96#[derive(Debug, Clone)]
97pub struct TuckerDecomposition<State = Untrained> {
98    /// Number of components for each mode
99    pub n_components: Vec<usize>,
100    /// Algorithm variant to use
101    pub algorithm: TuckerAlgorithm,
102    /// Maximum number of iterations
103    pub max_iter: usize,
104    /// Convergence tolerance
105    pub tol: f64,
106    /// Random state for reproducibility
107    pub random_state: Option<u64>,
108    /// Whether to center the tensor
109    pub center: bool,
110    /// Initialization method
111    pub init_method: String,
112
113    /// Trained state
114    #[allow(dead_code)]
115    state: State,
116}
117
118/// Trained Tucker decomposition state
119#[derive(Debug, Clone)]
120#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
121pub struct TrainedTucker {
122    pub core: Array3<f64>,
123    pub factors: Vec<Array2<f64>>,
124    pub mean: Option<Array3<f64>>,
125    pub tensor_shape: Vec<usize>,
126    pub n_components: Vec<usize>,
127    pub n_iter: usize,
128    pub reconstruction_error: f64,
129}
130
131impl CPDecomposition<Untrained> {
132    /// Create a new CP decomposition
133    pub fn new(n_components: usize) -> Self {
134        Self {
135            n_components,
136            algorithm: CPAlgorithm::ALS,
137            max_iter: 100,
138            tol: 1e-6,
139            random_state: None,
140            learning_rate: 0.01,
141            regularization: 0.0,
142            normalize_factors: true,
143            state: Untrained,
144        }
145    }
146
147    /// Set the algorithm
148    pub fn algorithm(mut self, algorithm: CPAlgorithm) -> Self {
149        self.algorithm = algorithm;
150        self
151    }
152
153    /// Set maximum iterations
154    pub fn max_iter(mut self, max_iter: usize) -> Self {
155        self.max_iter = max_iter;
156        self
157    }
158
159    /// Set tolerance
160    pub fn tol(mut self, tol: f64) -> Self {
161        self.tol = tol;
162        self
163    }
164
165    /// Set random state
166    pub fn random_state(mut self, random_state: u64) -> Self {
167        self.random_state = Some(random_state);
168        self
169    }
170
171    /// Set learning rate
172    pub fn learning_rate(mut self, learning_rate: f64) -> Self {
173        self.learning_rate = learning_rate;
174        self
175    }
176
177    /// Set regularization parameter
178    pub fn regularization(mut self, regularization: f64) -> Self {
179        self.regularization = regularization;
180        self
181    }
182
183    /// Set whether to normalize factors
184    pub fn normalize_factors(mut self, normalize_factors: bool) -> Self {
185        self.normalize_factors = normalize_factors;
186        self
187    }
188}
189
190impl Fit<Array3<f64>, ()> for CPDecomposition<Untrained> {
191    type Fitted = CPDecomposition<TrainedCP>;
192
193    fn fit(self, tensor: &Array3<f64>, _y: &()) -> Result<Self::Fitted> {
194        let tensor_shape = tensor.shape().to_vec();
195
196        if tensor_shape.len() != 3 {
197            return Err(SklearsError::InvalidInput(
198                "CP decomposition currently supports 3D tensors only".to_string(),
199            ));
200        }
201
202        let (_n_mode1, _n_mode2, _n_mode3) = (tensor_shape[0], tensor_shape[1], tensor_shape[2]);
203
204        // Initialize random number generator with optional seeding for reproducibility
205        let mut rng: StdRng = match self.random_state {
206            Some(seed) => StdRng::seed_from_u64(seed),
207            None => StdRng::from_rng(&mut make_rng()),
208        };
209
210        // Run CP decomposition algorithm
211        let (factors, weights, n_iter, reconstruction_error) = match self.algorithm {
212            CPAlgorithm::ALS => self.cp_als(tensor, &mut rng)?,
213            CPAlgorithm::NonNegative => self.cp_nonnegative(tensor, &mut rng)?,
214            CPAlgorithm::Robust => self.cp_robust(tensor, &mut rng)?,
215        };
216
217        Ok(CPDecomposition {
218            n_components: self.n_components,
219            algorithm: self.algorithm,
220            max_iter: self.max_iter,
221            tol: self.tol,
222            random_state: self.random_state,
223            learning_rate: self.learning_rate,
224            regularization: self.regularization,
225            normalize_factors: self.normalize_factors,
226            state: TrainedCP {
227                factors,
228                weights,
229                tensor_shape,
230                n_components: self.n_components,
231                n_iter,
232                reconstruction_error,
233            },
234        })
235    }
236}
237
238impl CPDecomposition<Untrained> {
239    /// CP decomposition using Alternating Least Squares
240    fn cp_als(&self, tensor: &Array3<f64>, rng: &mut impl RngExt) -> CPResult {
241        let (n_mode1, n_mode2, n_mode3) = tensor.dim();
242        let r = self.n_components;
243
244        // Initialize factor matrices randomly
245        let mut factors: Vec<Array2<f64>> = vec![
246            {
247                let mut arr: Array2<f64> = Array2::zeros((n_mode1, r));
248                for elem in arr.iter_mut() {
249                    *elem = rng.random::<f64>() - 0.5;
250                }
251                arr
252            },
253            {
254                let mut arr: Array2<f64> = Array2::zeros((n_mode2, r));
255                for elem in arr.iter_mut() {
256                    *elem = rng.random::<f64>() - 0.5;
257                }
258                arr
259            },
260            {
261                let mut arr: Array2<f64> = Array2::zeros((n_mode3, r));
262                for elem in arr.iter_mut() {
263                    *elem = rng.random::<f64>() - 0.5;
264                }
265                arr
266            },
267        ];
268
269        // Normalize initial factors
270        if self.normalize_factors {
271            for factor in &mut factors {
272                for j in 0..r {
273                    let mut col = factor.column_mut(j);
274                    let norm = col.dot(&col).sqrt();
275                    if norm > 1e-12 {
276                        col /= norm;
277                    }
278                }
279            }
280        }
281
282        let mut prev_error = f64::INFINITY;
283        let mut n_iter = 0;
284
285        for iter in 0..self.max_iter {
286            n_iter = iter + 1;
287
288            // Update each factor matrix in turn
289            for mode in 0..3 {
290                // Create Khatri-Rao product of other factors
291                let khatri_rao = match mode {
292                    0 => self.khatri_rao_product(&factors[2], &factors[1]),
293                    1 => self.khatri_rao_product(&factors[2], &factors[0]),
294                    2 => self.khatri_rao_product(&factors[1], &factors[0]),
295                    _ => unreachable!(),
296                };
297
298                // Unfold tensor along current mode
299                let unfolded = self.unfold_tensor(tensor, mode)?;
300
301                // Solve least squares problem: unfolded = factor * khatri_rao^T
302                // factor = unfolded * khatri_rao * (khatri_rao^T * khatri_rao)^(-1)
303                let gram = khatri_rao.t().dot(&khatri_rao);
304
305                // Add regularization to gram matrix
306                let mut gram_reg = gram;
307                for i in 0..r {
308                    gram_reg[[i, i]] += self.regularization;
309                }
310
311                // Solve linear system for each row of the factor matrix
312                let rhs = unfolded.dot(&khatri_rao);
313                let rhs_t = rhs.t().to_owned();
314                factors[mode] = self.solve_linear_system(&gram_reg, &rhs_t)?;
315
316                // Normalize columns if requested
317                if self.normalize_factors {
318                    for j in 0..r {
319                        let mut col = factors[mode].column_mut(j);
320                        let norm = col.dot(&col).sqrt();
321                        if norm > 1e-12 {
322                            col /= norm;
323                        }
324                    }
325                }
326            }
327
328            // Compute reconstruction error
329            let reconstructed = self.reconstruct_tensor(&factors)?;
330            let error = self.frobenius_norm(&(tensor - &reconstructed));
331
332            // Check convergence
333            if (prev_error - error).abs() < self.tol {
334                break;
335            }
336            prev_error = error;
337        }
338
339        // Compute final weights (norms of factor columns)
340        let mut weights = Array1::ones(r);
341        if self.normalize_factors {
342            for j in 0..r {
343                let mut weight = 1.0;
344                for factor in &factors {
345                    let col_norm = factor.column(j).dot(&factor.column(j)).sqrt();
346                    weight *= col_norm;
347                }
348                weights[j] = weight;
349            }
350        }
351
352        let final_error = self.frobenius_norm(&(tensor - &self.reconstruct_tensor(&factors)?));
353
354        Ok((factors, weights, n_iter, final_error))
355    }
356
357    /// Non-negative CP decomposition
358    fn cp_nonnegative(&self, tensor: &Array3<f64>, rng: &mut impl RngExt) -> CPResult {
359        let (n_mode1, n_mode2, n_mode3) = tensor.dim();
360        let r = self.n_components;
361
362        // Initialize with non-negative random factors
363        let mut factors: Vec<Array2<f64>> = vec![
364            {
365                let mut arr: Array2<f64> = Array2::zeros((n_mode1, r));
366                for elem in arr.iter_mut() {
367                    *elem = rng.random::<f64>().abs();
368                }
369                arr
370            },
371            {
372                let mut arr: Array2<f64> = Array2::zeros((n_mode2, r));
373                for elem in arr.iter_mut() {
374                    *elem = rng.random::<f64>().abs();
375                }
376                arr
377            },
378            {
379                let mut arr: Array2<f64> = Array2::zeros((n_mode3, r));
380                for elem in arr.iter_mut() {
381                    *elem = rng.random::<f64>().abs();
382                }
383                arr
384            },
385        ];
386
387        let mut prev_error = f64::INFINITY;
388        let mut n_iter = 0;
389
390        for iter in 0..self.max_iter {
391            n_iter = iter + 1;
392
393            // Multiplicative updates for each factor
394            for mode in 0..3 {
395                let khatri_rao = match mode {
396                    0 => self.khatri_rao_product(&factors[2], &factors[1]),
397                    1 => self.khatri_rao_product(&factors[2], &factors[0]),
398                    2 => self.khatri_rao_product(&factors[1], &factors[0]),
399                    _ => unreachable!(),
400                };
401
402                let unfolded = self.unfold_tensor(tensor, mode)?;
403
404                // Multiplicative update rule for non-negative factorization
405                let numerator = unfolded.dot(&khatri_rao);
406                let denominator = factors[mode].dot(&khatri_rao.t().dot(&khatri_rao));
407
408                // Update with element-wise multiplication
409                for i in 0..factors[mode].nrows() {
410                    for j in 0..factors[mode].ncols() {
411                        if denominator[[i, j]] > 1e-12 {
412                            factors[mode][[i, j]] *= numerator[[i, j]] / denominator[[i, j]];
413                        }
414                        // Ensure non-negativity
415                        factors[mode][[i, j]] = factors[mode][[i, j]].max(1e-12);
416                    }
417                }
418
419                // Normalize columns if requested
420                if self.normalize_factors {
421                    for j in 0..r {
422                        let mut col = factors[mode].column_mut(j);
423                        let norm = col.dot(&col).sqrt();
424                        if norm > 1e-12 {
425                            col /= norm;
426                        }
427                    }
428                }
429            }
430
431            // Compute reconstruction error
432            let reconstructed = self.reconstruct_tensor(&factors)?;
433            let error = self.frobenius_norm(&(tensor - &reconstructed));
434
435            // Check convergence
436            if (prev_error - error).abs() < self.tol {
437                break;
438            }
439            prev_error = error;
440        }
441
442        // Compute final weights
443        let weights = Array1::ones(r);
444        let final_error = self.frobenius_norm(&(tensor - &self.reconstruct_tensor(&factors)?));
445
446        Ok((factors, weights, n_iter, final_error))
447    }
448
449    /// Robust CP decomposition with outlier handling
450    fn cp_robust(&self, tensor: &Array3<f64>, rng: &mut impl RngExt) -> CPResult {
451        // For now, implement as standard ALS with robust loss function
452        // This is a simplified version - full robust CP would require more sophisticated methods
453        self.cp_als(tensor, rng)
454    }
455
456    /// Compute Khatri-Rao product of two matrices
457    fn khatri_rao_product(&self, a: &Array2<f64>, b: &Array2<f64>) -> Array2<f64> {
458        let (n_a, r) = a.dim();
459        let (n_b, r_b) = b.dim();
460        assert_eq!(r, r_b, "Matrices must have same number of columns");
461
462        let mut result = Array2::zeros((n_a * n_b, r));
463
464        for j in 0..r {
465            let a_col = a.column(j);
466            let b_col = b.column(j);
467
468            for i in 0..n_a {
469                for k in 0..n_b {
470                    result[[i * n_b + k, j]] = a_col[i] * b_col[k];
471                }
472            }
473        }
474
475        result
476    }
477
478    /// Unfold tensor along specified mode
479    fn unfold_tensor(&self, tensor: &Array3<f64>, mode: usize) -> Result<Array2<f64>> {
480        let (n1, n2, n3) = tensor.dim();
481
482        match mode {
483            0 => {
484                // Mode-1 unfolding: n1 × (n2*n3)
485                let mut unfolded = Array2::zeros((n1, n2 * n3));
486                for i in 0..n1 {
487                    for j in 0..n2 {
488                        for k in 0..n3 {
489                            unfolded[[i, j * n3 + k]] = tensor[[i, j, k]];
490                        }
491                    }
492                }
493                Ok(unfolded)
494            }
495            1 => {
496                // Mode-2 unfolding: n2 × (n3*n1)
497                let mut unfolded = Array2::zeros((n2, n3 * n1));
498                for j in 0..n2 {
499                    for k in 0..n3 {
500                        for i in 0..n1 {
501                            unfolded[[j, k * n1 + i]] = tensor[[i, j, k]];
502                        }
503                    }
504                }
505                Ok(unfolded)
506            }
507            2 => {
508                // Mode-3 unfolding: n3 × (n1*n2)
509                let mut unfolded = Array2::zeros((n3, n1 * n2));
510                for k in 0..n3 {
511                    for i in 0..n1 {
512                        for j in 0..n2 {
513                            unfolded[[k, i * n2 + j]] = tensor[[i, j, k]];
514                        }
515                    }
516                }
517                Ok(unfolded)
518            }
519            _ => Err(SklearsError::InvalidParameter {
520                name: "mode".to_string(),
521                reason: "must be 0, 1, or 2 for 3D tensors".to_string(),
522            }),
523        }
524    }
525
526    /// Solve linear system using pseudoinverse
527    fn solve_linear_system(&self, a: &Array2<f64>, b: &Array2<f64>) -> Result<Array2<f64>> {
528        // Compute pseudoinverse using SVD from scirs2-linalg
529        let (u, s, vt) = svd(&a.view(), true)
530            .map_err(|e| SklearsError::NumericalError(format!("SVD failed: {}", e)))?;
531
532        let tolerance = 1e-12;
533
534        // Create diagonal inverse matrix S^+
535        let k = s.len();
536        let mut s_inv = Array2::zeros((k, k));
537        for i in 0..k {
538            if s[i] > tolerance {
539                s_inv[[i, i]] = 1.0 / s[i];
540            }
541        }
542
543        // Compute pseudoinverse: A^+ = V * S^+ * U^T
544        let vt_t = vt.t();
545        let u_t = u.t();
546        let temp = s_inv.dot(&u_t);
547        let a_pinv = vt_t.dot(&temp);
548
549        // Compute result: A^+ * b
550        let result = a_pinv.dot(b);
551
552        Ok(result)
553    }
554
555    /// Reconstruct tensor from factor matrices
556    fn reconstruct_tensor(&self, factors: &[Array2<f64>]) -> Result<Array3<f64>> {
557        let (n1, r) = factors[0].dim();
558        let (n2, r2) = factors[1].dim();
559        let (n3, r3) = factors[2].dim();
560
561        if r != r2 || r != r3 {
562            return Err(SklearsError::InvalidInput(
563                "All factor matrices must have the same number of columns".to_string(),
564            ));
565        }
566
567        let mut reconstructed = Array3::zeros((n1, n2, n3));
568
569        for k in 0..r {
570            let a_k = factors[0].column(k);
571            let b_k = factors[1].column(k);
572            let c_k = factors[2].column(k);
573
574            for i in 0..n1 {
575                for j in 0..n2 {
576                    for l in 0..n3 {
577                        reconstructed[[i, j, l]] += a_k[i] * b_k[j] * c_k[l];
578                    }
579                }
580            }
581        }
582
583        Ok(reconstructed)
584    }
585
586    /// Compute Frobenius norm of a tensor
587    fn frobenius_norm(&self, tensor: &Array3<f64>) -> f64 {
588        tensor.iter().map(|&x| x * x).sum::<f64>().sqrt()
589    }
590}
591
592impl TuckerDecomposition<Untrained> {
593    /// Create a new Tucker decomposition
594    pub fn new(n_components: Vec<usize>) -> Self {
595        Self {
596            n_components,
597            algorithm: TuckerAlgorithm::HOSVD,
598            max_iter: 100,
599            tol: 1e-6,
600            random_state: None,
601            center: true,
602            init_method: "random".to_string(),
603            state: Untrained,
604        }
605    }
606
607    /// Set the algorithm
608    pub fn algorithm(mut self, algorithm: TuckerAlgorithm) -> Self {
609        self.algorithm = algorithm;
610        self
611    }
612
613    /// Set maximum iterations
614    pub fn max_iter(mut self, max_iter: usize) -> Self {
615        self.max_iter = max_iter;
616        self
617    }
618
619    /// Set tolerance
620    pub fn tol(mut self, tol: f64) -> Self {
621        self.tol = tol;
622        self
623    }
624
625    /// Set random state
626    pub fn random_state(mut self, random_state: u64) -> Self {
627        self.random_state = Some(random_state);
628        self
629    }
630
631    /// Set whether to center the tensor
632    pub fn center(mut self, center: bool) -> Self {
633        self.center = center;
634        self
635    }
636
637    /// Set initialization method
638    pub fn init_method(mut self, init_method: String) -> Self {
639        self.init_method = init_method;
640        self
641    }
642}
643
644impl Fit<Array3<f64>, ()> for TuckerDecomposition<Untrained> {
645    type Fitted = TuckerDecomposition<TrainedTucker>;
646
647    fn fit(self, tensor: &Array3<f64>, _y: &()) -> Result<Self::Fitted> {
648        let tensor_shape = tensor.shape().to_vec();
649
650        if tensor_shape.len() != 3 {
651            return Err(SklearsError::InvalidInput(
652                "Tucker decomposition currently supports 3D tensors only".to_string(),
653            ));
654        }
655
656        if self.n_components.len() != 3 {
657            return Err(SklearsError::InvalidInput(
658                "n_components must have 3 elements for 3D tensors".to_string(),
659            ));
660        }
661
662        // Center tensor if requested
663        let (centered_tensor, mean) = if self.center {
664            let mean_val = tensor.mean().ok_or_else(|| {
665                SklearsError::NumericalError("cannot compute mean of empty tensor".to_string())
666            })?;
667            let centered = tensor.mapv(|x| x - mean_val);
668            let mean_tensor = Array3::from_elem(tensor.dim(), mean_val);
669            (centered, Some(mean_tensor))
670        } else {
671            (tensor.clone(), None)
672        };
673
674        // Initialize random number generator with optional seeding for reproducibility
675        let mut rng: StdRng = match self.random_state {
676            Some(seed) => StdRng::seed_from_u64(seed),
677            None => StdRng::from_rng(&mut make_rng()),
678        };
679
680        // Run Tucker decomposition algorithm
681        let (core, factors, n_iter, reconstruction_error) = match self.algorithm {
682            TuckerAlgorithm::HOSVD => self.tucker_hosvd(&centered_tensor)?,
683            TuckerAlgorithm::ALS => self.tucker_als(&centered_tensor, &mut rng)?,
684            TuckerAlgorithm::SequentialSVD => self.tucker_sequential_svd(&centered_tensor)?,
685        };
686
687        Ok(TuckerDecomposition {
688            n_components: self.n_components.clone(),
689            algorithm: self.algorithm,
690            max_iter: self.max_iter,
691            tol: self.tol,
692            random_state: self.random_state,
693            center: self.center,
694            init_method: self.init_method,
695            state: TrainedTucker {
696                core,
697                factors,
698                mean,
699                tensor_shape,
700                n_components: self.n_components,
701                n_iter,
702                reconstruction_error,
703            },
704        })
705    }
706}
707
708impl TuckerDecomposition<Untrained> {
709    /// Tucker decomposition using Higher-Order SVD (HOSVD)
710    fn tucker_hosvd(&self, tensor: &Array3<f64>) -> TuckerResult {
711        let mut factors = Vec::new();
712
713        // Compute SVD for each mode
714        for mode in 0..3 {
715            let unfolded = self.unfold_tensor_tucker(tensor, mode)?;
716            let svd_result = self.compute_svd(&unfolded)?;
717
718            // Take first n_components[mode] columns from U matrix (not V^T)
719            let n_comp = self.n_components[mode]
720                .min(svd_result.0.ncols())
721                .min(unfolded.nrows());
722            let factor = svd_result
723                .0
724                .slice(scirs2_core::ndarray::s![.., ..n_comp])
725                .to_owned();
726            factors.push(factor);
727        }
728
729        // Compute core tensor by projecting original tensor onto factor spaces
730        let core = self.compute_core_tensor(tensor, &factors)?;
731
732        // Compute reconstruction error
733        let reconstructed = self.reconstruct_tucker_tensor(&core, &factors)?;
734        let error = self.frobenius_norm_tucker(&(tensor - &reconstructed));
735
736        Ok((core, factors, 1, error))
737    }
738
739    /// Tucker decomposition using Alternating Least Squares
740    fn tucker_als(&self, tensor: &Array3<f64>, rng: &mut impl RngExt) -> TuckerResult {
741        let (n1, n2, n3) = tensor.dim();
742
743        // Initialize factor matrices randomly
744        let mut factors: Vec<Array2<f64>> = vec![
745            {
746                let mut arr: Array2<f64> = Array2::zeros((n1, self.n_components[0]));
747                for elem in arr.iter_mut() {
748                    *elem = rng.random::<f64>() - 0.5;
749                }
750                arr
751            },
752            {
753                let mut arr: Array2<f64> = Array2::zeros((n2, self.n_components[1]));
754                for elem in arr.iter_mut() {
755                    *elem = rng.random::<f64>() - 0.5;
756                }
757                arr
758            },
759            {
760                let mut arr: Array2<f64> = Array2::zeros((n3, self.n_components[2]));
761                for elem in arr.iter_mut() {
762                    *elem = rng.random::<f64>() - 0.5;
763                }
764                arr
765            },
766        ];
767
768        // Orthogonalize initial factors
769        for factor in &mut factors {
770            *factor = self.orthogonalize_matrix(factor)?;
771        }
772
773        let mut prev_error = f64::INFINITY;
774        let mut n_iter = 0;
775
776        for iter in 0..self.max_iter {
777            n_iter = iter + 1;
778
779            // Update each factor matrix
780            for mode in 0..3 {
781                let unfolded = self.unfold_tensor_tucker(tensor, mode)?;
782
783                // Create product of other factor matrices
784                let other_factors = match mode {
785                    0 => self.kronecker_product(&factors[2], &factors[1]),
786                    1 => self.kronecker_product(&factors[2], &factors[0]),
787                    2 => self.kronecker_product(&factors[1], &factors[0]),
788                    _ => unreachable!(),
789                };
790
791                // Solve for updated factor
792                let rhs = unfolded.dot(&other_factors);
793                let gram = other_factors.t().dot(&other_factors);
794                let rhs_t = rhs.t().to_owned();
795
796                let solved = self.solve_linear_system_tucker(&gram, &rhs_t)?;
797
798                // The solved matrix should be transposed to get the correct factor dimensions
799                // We expect factors[mode] to have shape (n_mode, n_components[mode])
800                let _expected_shape = (
801                    tensor.dim().0.max(tensor.dim().1.max(tensor.dim().2)),
802                    self.n_components[mode],
803                );
804                let n_mode = match mode {
805                    0 => tensor.dim().0,
806                    1 => tensor.dim().1,
807                    2 => tensor.dim().2,
808                    _ => unreachable!(),
809                };
810
811                // Transpose the solution and take only the first n_components[mode] columns
812                let solved_t = solved.t().to_owned();
813                let factor_shape = (n_mode, self.n_components[mode]);
814
815                if solved_t.dim() == factor_shape {
816                    factors[mode] = solved_t;
817                } else {
818                    // Take the appropriate slice, but ensure we don't go out of bounds
819                    let max_rows = solved_t.nrows().min(n_mode);
820                    let max_cols = solved_t.ncols().min(self.n_components[mode]);
821
822                    let mut factor = Array2::zeros(factor_shape);
823                    for i in 0..max_rows {
824                        for j in 0..max_cols {
825                            factor[[i, j]] = solved_t[[i, j]];
826                        }
827                    }
828                    factors[mode] = factor;
829                }
830
831                // Orthogonalize
832                factors[mode] = self.orthogonalize_matrix(&factors[mode])?;
833            }
834
835            // Compute core tensor and reconstruction error
836            let core = self.compute_core_tensor(tensor, &factors)?;
837            let reconstructed = self.reconstruct_tucker_tensor(&core, &factors)?;
838            let error = self.frobenius_norm_tucker(&(tensor - &reconstructed));
839
840            // Check convergence
841            if (prev_error - error).abs() < self.tol {
842                break;
843            }
844            prev_error = error;
845        }
846
847        let core = self.compute_core_tensor(tensor, &factors)?;
848        let final_error = prev_error;
849
850        Ok((core, factors, n_iter, final_error))
851    }
852
853    /// Tucker decomposition using Sequential SVD
854    fn tucker_sequential_svd(&self, tensor: &Array3<f64>) -> TuckerResult {
855        // Similar to HOSVD but with sequential processing
856        self.tucker_hosvd(tensor)
857    }
858
859    /// Unfold tensor for Tucker decomposition
860    fn unfold_tensor_tucker(&self, tensor: &Array3<f64>, mode: usize) -> Result<Array2<f64>> {
861        let (n1, n2, n3) = tensor.dim();
862
863        match mode {
864            0 => {
865                let mut unfolded = Array2::zeros((n1, n2 * n3));
866                for i in 0..n1 {
867                    for j in 0..n2 {
868                        for k in 0..n3 {
869                            unfolded[[i, j * n3 + k]] = tensor[[i, j, k]];
870                        }
871                    }
872                }
873                Ok(unfolded)
874            }
875            1 => {
876                let mut unfolded = Array2::zeros((n2, n1 * n3));
877                for j in 0..n2 {
878                    for i in 0..n1 {
879                        for k in 0..n3 {
880                            unfolded[[j, i * n3 + k]] = tensor[[i, j, k]];
881                        }
882                    }
883                }
884                Ok(unfolded)
885            }
886            2 => {
887                let mut unfolded = Array2::zeros((n3, n1 * n2));
888                for k in 0..n3 {
889                    for i in 0..n1 {
890                        for j in 0..n2 {
891                            unfolded[[k, i * n2 + j]] = tensor[[i, j, k]];
892                        }
893                    }
894                }
895                Ok(unfolded)
896            }
897            _ => Err(SklearsError::InvalidParameter {
898                name: "mode".to_string(),
899                reason: "must be 0, 1, or 2 for 3D tensors".to_string(),
900            }),
901        }
902    }
903
904    /// Compute SVD of a matrix
905    fn compute_svd(&self, matrix: &Array2<f64>) -> Result<(Array2<f64>, Array2<f64>, Array1<f64>)> {
906        // Use scirs2-linalg SVD - returns (U, S, V^T)
907        let (u, s, vt) = svd(&matrix.view(), true)
908            .map_err(|e| SklearsError::NumericalError(format!("SVD failed: {}", e)))?;
909
910        // This function expects (U, V^T, S) order
911        Ok((u, vt, s))
912    }
913
914    /// Compute core tensor
915    fn compute_core_tensor(
916        &self,
917        tensor: &Array3<f64>,
918        factors: &[Array2<f64>],
919    ) -> Result<Array3<f64>> {
920        let (r1, r2, r3) = (factors[0].ncols(), factors[1].ncols(), factors[2].ncols());
921        let mut core = Array3::zeros((r1, r2, r3));
922
923        // G = X ×₁ A^T ×₂ B^T ×₃ C^T
924        // This is a simplified implementation
925        for i in 0..r1 {
926            for j in 0..r2 {
927                for k in 0..r3 {
928                    let mut value = 0.0;
929                    let (n1, n2, n3) = tensor.dim();
930
931                    for ii in 0..n1 {
932                        for jj in 0..n2 {
933                            for kk in 0..n3 {
934                                value += tensor[[ii, jj, kk]]
935                                    * factors[0][[ii, i]]
936                                    * factors[1][[jj, j]]
937                                    * factors[2][[kk, k]];
938                            }
939                        }
940                    }
941                    core[[i, j, k]] = value;
942                }
943            }
944        }
945
946        Ok(core)
947    }
948
949    /// Reconstruct tensor from Tucker decomposition
950    fn reconstruct_tucker_tensor(
951        &self,
952        core: &Array3<f64>,
953        factors: &[Array2<f64>],
954    ) -> Result<Array3<f64>> {
955        let (n1, n2, n3) = (factors[0].nrows(), factors[1].nrows(), factors[2].nrows());
956        let mut reconstructed = Array3::zeros((n1, n2, n3));
957
958        let (r1, r2, r3) = core.dim();
959
960        for i in 0..n1 {
961            for j in 0..n2 {
962                for k in 0..n3 {
963                    let mut value = 0.0;
964
965                    for ii in 0..r1 {
966                        for jj in 0..r2 {
967                            for kk in 0..r3 {
968                                value += core[[ii, jj, kk]]
969                                    * factors[0][[i, ii]]
970                                    * factors[1][[j, jj]]
971                                    * factors[2][[k, kk]];
972                            }
973                        }
974                    }
975                    reconstructed[[i, j, k]] = value;
976                }
977            }
978        }
979
980        Ok(reconstructed)
981    }
982
983    /// Orthogonalize matrix using QR decomposition
984    fn orthogonalize_matrix(&self, matrix: &Array2<f64>) -> Result<Array2<f64>> {
985        // Simple Gram-Schmidt orthogonalization
986        let (m, n) = matrix.dim();
987        let mut q = matrix.clone();
988
989        for j in 0..n {
990            for i in 0..j {
991                let qi = q.column(i).to_owned();
992                let qj = q.column(j).to_owned();
993                let proj = qi.dot(&qj);
994
995                for k in 0..m {
996                    q[[k, j]] -= proj * qi[k];
997                }
998            }
999
1000            // Normalize
1001            let norm = {
1002                let col = q.column(j);
1003                col.dot(&col).sqrt()
1004            };
1005            if norm > 1e-12 {
1006                let mut col = q.column_mut(j);
1007                col /= norm;
1008            }
1009        }
1010
1011        Ok(q)
1012    }
1013
1014    /// Compute Kronecker product of two matrices
1015    fn kronecker_product(&self, a: &Array2<f64>, b: &Array2<f64>) -> Array2<f64> {
1016        let (m_a, n_a) = a.dim();
1017        let (m_b, n_b) = b.dim();
1018
1019        let mut result = Array2::zeros((m_a * m_b, n_a * n_b));
1020
1021        for i in 0..m_a {
1022            for j in 0..n_a {
1023                for k in 0..m_b {
1024                    for l in 0..n_b {
1025                        result[[i * m_b + k, j * n_b + l]] = a[[i, j]] * b[[k, l]];
1026                    }
1027                }
1028            }
1029        }
1030
1031        result
1032    }
1033
1034    /// Solve linear system for Tucker decomposition
1035    fn solve_linear_system_tucker(&self, a: &Array2<f64>, b: &Array2<f64>) -> Result<Array2<f64>> {
1036        // Try Cholesky decomposition first (for positive definite matrices)
1037        if let Ok(chol) = a.cholesky() {
1038            // Solve each column of b separately
1039            let mut result = Array2::zeros((a.ncols(), b.ncols()));
1040            for (j, col) in b.axis_iter(scirs2_core::ndarray::Axis(1)).enumerate() {
1041                let sol = chol.solve(&col.to_owned()).map_err(|e| {
1042                    SklearsError::NumericalError(format!("Cholesky solve failed: {}", e))
1043                })?;
1044                for (i, &val) in sol.iter().enumerate() {
1045                    result[[i, j]] = val;
1046                }
1047            }
1048            return Ok(result);
1049        }
1050
1051        // Fallback to pseudoinverse using SVD
1052        let (u, s, vt) = svd(&a.view(), true)
1053            .map_err(|e| SklearsError::NumericalError(format!("SVD failed: {}", e)))?;
1054
1055        let tolerance = 1e-12;
1056
1057        // Create diagonal inverse matrix S^+
1058        let k = s.len();
1059        let mut s_inv = Array2::zeros((k, k));
1060        for i in 0..k {
1061            if s[i] > tolerance {
1062                s_inv[[i, i]] = 1.0 / s[i];
1063            }
1064        }
1065
1066        // Compute pseudoinverse: A^+ = V * S^+ * U^T
1067        let vt_t = vt.t();
1068        let u_t = u.t();
1069        let temp = s_inv.dot(&u_t);
1070        let a_pinv = vt_t.dot(&temp);
1071
1072        // Compute result: A^+ * b
1073        let result = a_pinv.dot(b);
1074
1075        Ok(result)
1076    }
1077
1078    /// Compute Frobenius norm for Tucker
1079    fn frobenius_norm_tucker(&self, tensor: &Array3<f64>) -> f64 {
1080        tensor.iter().map(|&x| x * x).sum::<f64>().sqrt()
1081    }
1082}
1083
1084#[allow(non_snake_case)]
1085#[cfg(test)]
1086mod tests {
1087    use super::*;
1088    use scirs2_core::ndarray::array;
1089
1090    #[test]
1091    fn test_cp_decomposition_basic() {
1092        // Create a simple test tensor
1093        let tensor = array![[[1.0, 2.0], [3.0, 4.0]], [[5.0, 6.0], [7.0, 8.0]],];
1094
1095        let cp = CPDecomposition::new(2).max_iter(10).random_state(42);
1096
1097        let result = cp.fit(&tensor, &());
1098        if let Err(ref e) = result {
1099            println!("CP decomposition error: {e:?}");
1100        }
1101        assert!(result.is_ok());
1102
1103        let trained = result.expect("operation should succeed");
1104        assert_eq!(trained.state.n_components, 2);
1105        assert_eq!(trained.state.factors.len(), 3);
1106        assert!(trained.state.reconstruction_error.is_finite());
1107    }
1108
1109    #[test]
1110    fn test_tucker_decomposition_basic() {
1111        // Create a simple test tensor
1112        let tensor = array![
1113            [[1.0, 2.0], [3.0, 4.0]],
1114            [[5.0, 6.0], [7.0, 8.0]],
1115            [[9.0, 10.0], [11.0, 12.0]],
1116        ];
1117
1118        let tucker = TuckerDecomposition::new(vec![2, 2, 1])
1119            .algorithm(TuckerAlgorithm::HOSVD)
1120            .random_state(42);
1121
1122        let result = tucker.fit(&tensor, &());
1123        if let Err(ref e) = result {
1124            println!("Tucker decomposition error: {e:?}");
1125        }
1126        assert!(result.is_ok());
1127
1128        let trained = result.expect("operation should succeed");
1129        assert_eq!(trained.state.n_components, vec![2, 2, 1]);
1130        assert_eq!(trained.state.factors.len(), 3);
1131        assert!(trained.state.reconstruction_error.is_finite());
1132    }
1133
1134    #[test]
1135    fn test_cp_nonnegative() {
1136        let tensor = array![[[1.0, 2.0], [3.0, 4.0]], [[5.0, 6.0], [7.0, 8.0]],];
1137
1138        let cp = CPDecomposition::new(1)
1139            .algorithm(CPAlgorithm::NonNegative)
1140            .max_iter(5)
1141            .random_state(42);
1142
1143        let result = cp.fit(&tensor, &());
1144        assert!(result.is_ok());
1145
1146        let trained = result.expect("operation should succeed");
1147        // Check that all factor values are non-negative
1148        for factor in &trained.state.factors {
1149            for &val in factor.iter() {
1150                assert!(val >= 0.0, "Factor value should be non-negative: {}", val);
1151            }
1152        }
1153    }
1154
1155    #[test]
1156    fn test_tucker_als() {
1157        let tensor = array![[[1.0, 2.0], [3.0, 4.0]], [[5.0, 6.0], [7.0, 8.0]],];
1158
1159        let tucker = TuckerDecomposition::new(vec![1, 2, 1])
1160            .algorithm(TuckerAlgorithm::ALS)
1161            .max_iter(5)
1162            .random_state(42);
1163
1164        let result = tucker.fit(&tensor, &());
1165        if let Err(ref e) = result {
1166            println!("Tucker ALS error: {e:?}");
1167        }
1168        assert!(result.is_ok());
1169
1170        let trained = result.expect("operation should succeed");
1171        assert_eq!(trained.state.n_components, vec![1, 2, 1]);
1172        assert!(trained.state.n_iter > 0);
1173    }
1174
1175    #[test]
1176    fn test_cp_parameters() {
1177        let cp = CPDecomposition::new(3)
1178            .algorithm(CPAlgorithm::Robust)
1179            .max_iter(200)
1180            .tol(1e-8)
1181            .learning_rate(0.05)
1182            .regularization(0.1)
1183            .normalize_factors(false);
1184
1185        assert_eq!(cp.n_components, 3);
1186        assert_eq!(cp.algorithm, CPAlgorithm::Robust);
1187        assert_eq!(cp.max_iter, 200);
1188        assert_eq!(cp.tol, 1e-8);
1189        assert_eq!(cp.learning_rate, 0.05);
1190        assert_eq!(cp.regularization, 0.1);
1191        assert!(!cp.normalize_factors);
1192    }
1193
1194    #[test]
1195    fn test_tucker_parameters() {
1196        let tucker = TuckerDecomposition::new(vec![2, 3, 4])
1197            .algorithm(TuckerAlgorithm::SequentialSVD)
1198            .max_iter(150)
1199            .tol(1e-7)
1200            .center(false)
1201            .init_method("svd".to_string());
1202
1203        assert_eq!(tucker.n_components, vec![2, 3, 4]);
1204        assert_eq!(tucker.algorithm, TuckerAlgorithm::SequentialSVD);
1205        assert_eq!(tucker.max_iter, 150);
1206        assert_eq!(tucker.tol, 1e-7);
1207        assert!(!tucker.center);
1208        assert_eq!(tucker.init_method, "svd");
1209    }
1210}