1use 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
20type CPResult = Result<(Vec<Array2<f64>>, Array1<f64>, usize, f64)>;
22
23type TuckerResult = Result<(Array3<f64>, Vec<Array2<f64>>, usize, f64)>;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
29pub enum CPAlgorithm {
30 #[default]
32 ALS,
33 NonNegative,
35 Robust,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
42pub enum TuckerAlgorithm {
43 #[default]
45 HOSVD,
46 ALS,
48 SequentialSVD,
50}
51
52#[derive(Debug, Clone)]
57pub struct CPDecomposition<State = Untrained> {
58 pub n_components: usize,
60 pub algorithm: CPAlgorithm,
62 pub max_iter: usize,
64 pub tol: f64,
66 pub random_state: Option<u64>,
68 pub learning_rate: f64,
70 pub regularization: f64,
72 pub normalize_factors: bool,
74
75 #[allow(dead_code)]
77 state: State,
78}
79
80#[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#[derive(Debug, Clone)]
97pub struct TuckerDecomposition<State = Untrained> {
98 pub n_components: Vec<usize>,
100 pub algorithm: TuckerAlgorithm,
102 pub max_iter: usize,
104 pub tol: f64,
106 pub random_state: Option<u64>,
108 pub center: bool,
110 pub init_method: String,
112
113 #[allow(dead_code)]
115 state: State,
116}
117
118#[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 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 pub fn algorithm(mut self, algorithm: CPAlgorithm) -> Self {
149 self.algorithm = algorithm;
150 self
151 }
152
153 pub fn max_iter(mut self, max_iter: usize) -> Self {
155 self.max_iter = max_iter;
156 self
157 }
158
159 pub fn tol(mut self, tol: f64) -> Self {
161 self.tol = tol;
162 self
163 }
164
165 pub fn random_state(mut self, random_state: u64) -> Self {
167 self.random_state = Some(random_state);
168 self
169 }
170
171 pub fn learning_rate(mut self, learning_rate: f64) -> Self {
173 self.learning_rate = learning_rate;
174 self
175 }
176
177 pub fn regularization(mut self, regularization: f64) -> Self {
179 self.regularization = regularization;
180 self
181 }
182
183 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 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 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 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 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 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 for mode in 0..3 {
290 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 let unfolded = self.unfold_tensor(tensor, mode)?;
300
301 let gram = khatri_rao.t().dot(&khatri_rao);
304
305 let mut gram_reg = gram;
307 for i in 0..r {
308 gram_reg[[i, i]] += self.regularization;
309 }
310
311 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 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 let reconstructed = self.reconstruct_tensor(&factors)?;
330 let error = self.frobenius_norm(&(tensor - &reconstructed));
331
332 if (prev_error - error).abs() < self.tol {
334 break;
335 }
336 prev_error = error;
337 }
338
339 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 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 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 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 let numerator = unfolded.dot(&khatri_rao);
406 let denominator = factors[mode].dot(&khatri_rao.t().dot(&khatri_rao));
407
408 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 factors[mode][[i, j]] = factors[mode][[i, j]].max(1e-12);
416 }
417 }
418
419 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 let reconstructed = self.reconstruct_tensor(&factors)?;
433 let error = self.frobenius_norm(&(tensor - &reconstructed));
434
435 if (prev_error - error).abs() < self.tol {
437 break;
438 }
439 prev_error = error;
440 }
441
442 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 fn cp_robust(&self, tensor: &Array3<f64>, rng: &mut impl RngExt) -> CPResult {
451 self.cp_als(tensor, rng)
454 }
455
456 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 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 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 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 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 fn solve_linear_system(&self, a: &Array2<f64>, b: &Array2<f64>) -> Result<Array2<f64>> {
528 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 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 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 let result = a_pinv.dot(b);
551
552 Ok(result)
553 }
554
555 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 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 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 pub fn algorithm(mut self, algorithm: TuckerAlgorithm) -> Self {
609 self.algorithm = algorithm;
610 self
611 }
612
613 pub fn max_iter(mut self, max_iter: usize) -> Self {
615 self.max_iter = max_iter;
616 self
617 }
618
619 pub fn tol(mut self, tol: f64) -> Self {
621 self.tol = tol;
622 self
623 }
624
625 pub fn random_state(mut self, random_state: u64) -> Self {
627 self.random_state = Some(random_state);
628 self
629 }
630
631 pub fn center(mut self, center: bool) -> Self {
633 self.center = center;
634 self
635 }
636
637 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 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 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 let (core, factors, n_iter, reconstruction_error) = match self.algorithm {
682 TuckerAlgorithm::HOSVD => self.tucker_hosvd(¢ered_tensor)?,
683 TuckerAlgorithm::ALS => self.tucker_als(¢ered_tensor, &mut rng)?,
684 TuckerAlgorithm::SequentialSVD => self.tucker_sequential_svd(¢ered_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 fn tucker_hosvd(&self, tensor: &Array3<f64>) -> TuckerResult {
711 let mut factors = Vec::new();
712
713 for mode in 0..3 {
715 let unfolded = self.unfold_tensor_tucker(tensor, mode)?;
716 let svd_result = self.compute_svd(&unfolded)?;
717
718 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 let core = self.compute_core_tensor(tensor, &factors)?;
731
732 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 fn tucker_als(&self, tensor: &Array3<f64>, rng: &mut impl RngExt) -> TuckerResult {
741 let (n1, n2, n3) = tensor.dim();
742
743 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 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 for mode in 0..3 {
781 let unfolded = self.unfold_tensor_tucker(tensor, mode)?;
782
783 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 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 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 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 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 factors[mode] = self.orthogonalize_matrix(&factors[mode])?;
833 }
834
835 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 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 fn tucker_sequential_svd(&self, tensor: &Array3<f64>) -> TuckerResult {
855 self.tucker_hosvd(tensor)
857 }
858
859 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 fn compute_svd(&self, matrix: &Array2<f64>) -> Result<(Array2<f64>, Array2<f64>, Array1<f64>)> {
906 let (u, s, vt) = svd(&matrix.view(), true)
908 .map_err(|e| SklearsError::NumericalError(format!("SVD failed: {}", e)))?;
909
910 Ok((u, vt, s))
912 }
913
914 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 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 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 fn orthogonalize_matrix(&self, matrix: &Array2<f64>) -> Result<Array2<f64>> {
985 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 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 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 fn solve_linear_system_tucker(&self, a: &Array2<f64>, b: &Array2<f64>) -> Result<Array2<f64>> {
1036 if let Ok(chol) = a.cholesky() {
1038 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 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 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 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 let result = a_pinv.dot(b);
1074
1075 Ok(result)
1076 }
1077
1078 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 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 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 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}