1use scirs2_core::ndarray::{Array1, Array2};
10use scirs2_core::random::rngs::StdRng;
11use scirs2_core::random::{rng as make_rng, RngExt, SeedableRng};
12use scirs2_linalg::compat::{svd, ArrayLinalgExt};
13#[cfg(feature = "serde")]
14use serde::{Deserialize, Serialize};
15use sklears_core::{
16 error::{Result, SklearsError},
17 traits::{Fit, Transform, Untrained},
18};
19
20type SideInfoCompletionResult = (
22 (Array2<f64>, Array2<f64>),
23 Option<Array1<f64>>,
24 Option<Array1<f64>>,
25 f64,
26 usize,
27 f64,
28);
29
30#[allow(dead_code)]
32type MatrixCompletionResult = (
33 Array2<f64>,
34 Array2<f64>,
35 Option<(Array2<f64>, Array1<f64>, Array2<f64>)>,
36 usize,
37 f64,
38 f64,
39);
40
41type RPCAResult = (
43 Array2<f64>,
44 Array2<f64>,
45 Option<(Array2<f64>, Array1<f64>, Array2<f64>)>,
46 usize,
47 usize,
48 f64,
49);
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
53#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
54pub enum CompletionAlgorithm {
55 #[default]
57 SVD,
58 ALS,
60 NuclearNorm,
62 SideInfo,
64}
65
66#[derive(Debug, Clone)]
68pub struct MatrixCompletion<State = Untrained> {
69 pub rank: Option<usize>,
71 pub algorithm: CompletionAlgorithm,
73 pub max_iter: usize,
75 pub tol: f64,
77 pub random_state: Option<u64>,
79 pub learning_rate: f64,
81 pub regularization: f64,
83 pub use_bias: bool,
85
86 state: State,
88}
89
90#[derive(Debug, Clone)]
92#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
93pub struct TrainedMatrixCompletion {
94 pub factors: (Array2<f64>, Array2<f64>),
95 pub user_bias: Option<Array1<f64>>,
96 pub item_bias: Option<Array1<f64>>,
97 pub global_bias: f64,
98 pub matrix_shape: (usize, usize),
99 pub rank: usize,
100 pub n_iter: usize,
101 pub reconstruction_error: f64,
102 pub mask: Array2<bool>,
103}
104
105impl Default for MatrixCompletion<Untrained> {
106 fn default() -> Self {
107 Self::new()
108 }
109}
110
111impl MatrixCompletion<Untrained> {
112 pub fn new() -> Self {
114 Self {
115 rank: None,
116 algorithm: CompletionAlgorithm::SVD,
117 max_iter: 100,
118 tol: 1e-6,
119 random_state: None,
120 learning_rate: 0.01,
121 regularization: 0.01,
122 use_bias: true,
123 state: Untrained,
124 }
125 }
126
127 pub fn rank(mut self, rank: usize) -> Self {
129 self.rank = Some(rank);
130 self
131 }
132
133 pub fn algorithm(mut self, algorithm: CompletionAlgorithm) -> Self {
135 self.algorithm = algorithm;
136 self
137 }
138
139 pub fn max_iter(mut self, max_iter: usize) -> Self {
141 self.max_iter = max_iter;
142 self
143 }
144
145 pub fn tol(mut self, tol: f64) -> Self {
147 self.tol = tol;
148 self
149 }
150
151 pub fn random_state(mut self, random_state: u64) -> Self {
153 self.random_state = Some(random_state);
154 self
155 }
156
157 pub fn learning_rate(mut self, learning_rate: f64) -> Self {
159 self.learning_rate = learning_rate;
160 self
161 }
162
163 pub fn regularization(mut self, regularization: f64) -> Self {
165 self.regularization = regularization;
166 self
167 }
168
169 pub fn use_bias(mut self, use_bias: bool) -> Self {
171 self.use_bias = use_bias;
172 self
173 }
174}
175
176impl Fit<Array2<f64>, Array2<bool>> for MatrixCompletion<Untrained> {
177 type Fitted = MatrixCompletion<TrainedMatrixCompletion>;
178
179 fn fit(self, matrix: &Array2<f64>, mask: &Array2<bool>) -> Result<Self::Fitted> {
180 let (n_users, n_items) = matrix.dim();
181
182 if mask.dim() != matrix.dim() {
183 return Err(SklearsError::InvalidInput(
184 "Mask dimensions must match matrix dimensions".to_string(),
185 ));
186 }
187
188 let n_observed = mask.iter().filter(|&&x| x).count();
190 if n_observed == 0 {
191 return Err(SklearsError::InvalidInput(
192 "No observed entries in the matrix".to_string(),
193 ));
194 }
195
196 let rank = self.rank.unwrap_or((n_users.min(n_items) / 2).max(1));
198
199 let mut rng = if let Some(seed) = self.random_state {
201 StdRng::seed_from_u64(seed)
202 } else {
203 StdRng::from_rng(&mut make_rng())
204 };
205
206 let (factors, user_bias, item_bias, global_bias, n_iter, reconstruction_error) =
208 match self.algorithm {
209 CompletionAlgorithm::SVD => self.svd_completion(matrix, mask, rank, &mut rng)?,
210 CompletionAlgorithm::ALS => self.als_completion(matrix, mask, rank, &mut rng)?,
211 CompletionAlgorithm::NuclearNorm => {
212 self.nuclear_norm_completion(matrix, mask, rank, &mut rng)?
213 }
214 CompletionAlgorithm::SideInfo => {
215 self.side_info_completion(matrix, mask, rank, &mut rng)?
216 }
217 };
218
219 Ok(MatrixCompletion {
220 rank: self.rank,
221 algorithm: self.algorithm,
222 max_iter: self.max_iter,
223 tol: self.tol,
224 random_state: self.random_state,
225 learning_rate: self.learning_rate,
226 regularization: self.regularization,
227 use_bias: self.use_bias,
228 state: TrainedMatrixCompletion {
229 factors,
230 user_bias,
231 item_bias,
232 global_bias,
233 matrix_shape: (n_users, n_items),
234 rank,
235 n_iter,
236 reconstruction_error,
237 mask: mask.clone(),
238 },
239 })
240 }
241}
242
243impl MatrixCompletion<Untrained> {
244 fn svd_completion(
246 &self,
247 matrix: &Array2<f64>,
248 mask: &Array2<bool>,
249 rank: usize,
250 _rng: &mut impl RngExt,
251 ) -> Result<SideInfoCompletionResult> {
252 let (n_users, n_items) = matrix.dim();
253
254 let observed_sum: f64 = matrix
256 .iter()
257 .zip(mask.iter())
258 .filter(|(_, &m)| m)
259 .map(|(&val, _)| val)
260 .sum();
261 let n_observed = mask.iter().filter(|&&x| x).count();
262 let global_mean = observed_sum / n_observed as f64;
263
264 let mut completed = matrix.clone();
266 for ((i, j), &is_observed) in scirs2_core::ndarray::indices(matrix.dim())
267 .into_iter()
268 .zip(mask.iter())
269 {
270 if !is_observed {
271 completed[[i, j]] = global_mean;
272 }
273 }
274
275 let mut prev_error = f64::INFINITY;
276 let mut n_iter = 0;
277
278 for iter in 0..self.max_iter {
279 n_iter = iter + 1;
280
281 let (u, s, vt) = self.compute_truncated_svd(&completed, rank)?;
283
284 let mut reconstructed = Array2::zeros((n_users, n_items));
286 for i in 0..n_users {
287 for j in 0..n_items {
288 for k in 0..rank {
289 reconstructed[[i, j]] += u[[i, k]] * s[k] * vt[[k, j]];
290 }
291 }
292 }
293
294 for ((i, j), &is_observed) in scirs2_core::ndarray::indices(matrix.dim())
296 .into_iter()
297 .zip(mask.iter())
298 {
299 if !is_observed {
300 completed[[i, j]] = reconstructed[[i, j]];
301 }
302 }
303
304 let mut error = 0.0;
306 let mut count = 0;
307 for ((i, j), &is_observed) in scirs2_core::ndarray::indices(matrix.dim())
308 .into_iter()
309 .zip(mask.iter())
310 {
311 if is_observed {
312 let diff = matrix[[i, j]] - reconstructed[[i, j]];
313 error += diff * diff;
314 count += 1;
315 }
316 }
317 error = (error / count as f64).sqrt();
318
319 if (prev_error - error).abs() < self.tol {
321 break;
322 }
323 prev_error = error;
324 }
325
326 let (u, s, vt) = self.compute_truncated_svd(&completed, rank)?;
328
329 let mut u_factor = Array2::zeros((n_users, rank));
331 let mut v_factor = Array2::zeros((n_items, rank));
332
333 for i in 0..n_users {
334 for k in 0..rank {
335 u_factor[[i, k]] = u[[i, k]] * s[k].sqrt();
336 }
337 }
338
339 for j in 0..n_items {
340 for k in 0..rank {
341 v_factor[[j, k]] = vt[[k, j]] * s[k].sqrt();
342 }
343 }
344
345 let user_bias = if self.use_bias {
346 Some(Array1::zeros(n_users))
347 } else {
348 None
349 };
350
351 let item_bias = if self.use_bias {
352 Some(Array1::zeros(n_items))
353 } else {
354 None
355 };
356
357 Ok((
358 (u_factor, v_factor),
359 user_bias,
360 item_bias,
361 global_mean,
362 n_iter,
363 prev_error,
364 ))
365 }
366
367 fn als_completion(
369 &self,
370 matrix: &Array2<f64>,
371 mask: &Array2<bool>,
372 rank: usize,
373 rng: &mut impl RngExt,
374 ) -> Result<SideInfoCompletionResult> {
375 let (n_users, n_items) = matrix.dim();
376
377 let mut u = Array2::zeros((n_users, rank));
379 let mut v = Array2::zeros((n_items, rank));
380
381 for i in 0..n_users {
382 for k in 0..rank {
383 u[[i, k]] = rng.random::<f64>() - 0.5;
384 }
385 }
386
387 for j in 0..n_items {
388 for k in 0..rank {
389 v[[j, k]] = rng.random::<f64>() - 0.5;
390 }
391 }
392
393 let mut user_bias = if self.use_bias {
395 Some(Array1::zeros(n_users))
396 } else {
397 None
398 };
399
400 let mut item_bias = if self.use_bias {
401 Some(Array1::zeros(n_items))
402 } else {
403 None
404 };
405
406 let observed_sum: f64 = matrix
408 .iter()
409 .zip(mask.iter())
410 .filter(|(_, &m)| m)
411 .map(|(&val, _)| val)
412 .sum();
413 let n_observed = mask.iter().filter(|&&x| x).count();
414 let global_bias = observed_sum / n_observed as f64;
415
416 let mut prev_error = f64::INFINITY;
417 let mut n_iter = 0;
418
419 for iter in 0..self.max_iter {
420 n_iter = iter + 1;
421
422 for i in 0..n_users {
424 let mut a = Array2::eye(rank) * self.regularization;
425 let mut b = Array1::zeros(rank);
426
427 for j in 0..n_items {
428 if mask[[i, j]] {
429 let rating = matrix[[i, j]] - global_bias;
430 let rating = if let Some(ref ib) = item_bias {
431 rating - ib[j]
432 } else {
433 rating
434 };
435
436 let v_j = v.row(j);
437 for k1 in 0..rank {
438 b[k1] += rating * v_j[k1];
439 for k2 in 0..rank {
440 a[[k1, k2]] += v_j[k1] * v_j[k2];
441 }
442 }
443 }
444 }
445
446 let u_i = self.solve_linear_system_als(&a, &b)?;
448 u.row_mut(i).assign(&u_i);
449 }
450
451 for j in 0..n_items {
453 let mut a = Array2::eye(rank) * self.regularization;
454 let mut b = Array1::zeros(rank);
455
456 for i in 0..n_users {
457 if mask[[i, j]] {
458 let rating = matrix[[i, j]] - global_bias;
459 let rating = if let Some(ref ub) = user_bias {
460 rating - ub[i]
461 } else {
462 rating
463 };
464
465 let u_i = u.row(i);
466 for k1 in 0..rank {
467 b[k1] += rating * u_i[k1];
468 for k2 in 0..rank {
469 a[[k1, k2]] += u_i[k1] * u_i[k2];
470 }
471 }
472 }
473 }
474
475 let v_j = self.solve_linear_system_als(&a, &b)?;
477 v.row_mut(j).assign(&v_j);
478 }
479
480 if self.use_bias {
482 if let Some(ref mut ub) = user_bias {
484 for i in 0..n_users {
485 let mut sum = 0.0;
486 let mut count = 0;
487 for j in 0..n_items {
488 if mask[[i, j]] {
489 let predicted = global_bias + u.row(i).dot(&v.row(j));
490 let predicted = if let Some(ref ib) = item_bias {
491 predicted + ib[j]
492 } else {
493 predicted
494 };
495 sum += matrix[[i, j]] - predicted;
496 count += 1;
497 }
498 }
499 if count > 0 {
500 ub[i] = sum / count as f64;
501 }
502 }
503 }
504
505 if let Some(ref mut ib) = item_bias {
507 for j in 0..n_items {
508 let mut sum = 0.0;
509 let mut count = 0;
510 for i in 0..n_users {
511 if mask[[i, j]] {
512 let predicted = global_bias + u.row(i).dot(&v.row(j));
513 let predicted = if let Some(ref ub) = user_bias {
514 predicted + ub[i]
515 } else {
516 predicted
517 };
518 sum += matrix[[i, j]] - predicted;
519 count += 1;
520 }
521 }
522 if count > 0 {
523 ib[j] = sum / count as f64;
524 }
525 }
526 }
527 }
528
529 let mut error = 0.0;
531 let mut count = 0;
532 for i in 0..n_users {
533 for j in 0..n_items {
534 if mask[[i, j]] {
535 let predicted = global_bias + u.row(i).dot(&v.row(j));
536 let predicted = if self.use_bias {
537 let ub = user_bias.as_deref().map(|a| a[i]).unwrap_or(0.0);
538 let ib = item_bias.as_deref().map(|a| a[j]).unwrap_or(0.0);
539 predicted + ub + ib
540 } else {
541 predicted
542 };
543 let diff = matrix[[i, j]] - predicted;
544 error += diff * diff;
545 count += 1;
546 }
547 }
548 }
549 error = (error / count as f64).sqrt();
550
551 if (prev_error - error).abs() < self.tol {
553 break;
554 }
555 prev_error = error;
556 }
557
558 Ok((
559 (u, v),
560 user_bias,
561 item_bias,
562 global_bias,
563 n_iter,
564 prev_error,
565 ))
566 }
567
568 fn nuclear_norm_completion(
570 &self,
571 matrix: &Array2<f64>,
572 mask: &Array2<bool>,
573 rank: usize,
574 _rng: &mut impl RngExt,
575 ) -> Result<SideInfoCompletionResult> {
576 let (n_users, n_items) = matrix.dim();
578
579 let observed_sum: f64 = matrix
581 .iter()
582 .zip(mask.iter())
583 .filter(|(_, &m)| m)
584 .map(|(&val, _)| val)
585 .sum();
586 let n_observed = mask.iter().filter(|&&x| x).count();
587 let global_mean = observed_sum / n_observed as f64;
588
589 let mut completed = matrix.clone();
590 for ((i, j), &is_observed) in scirs2_core::ndarray::indices(matrix.dim())
591 .into_iter()
592 .zip(mask.iter())
593 {
594 if !is_observed {
595 completed[[i, j]] = global_mean;
596 }
597 }
598
599 let mut prev_error = f64::INFINITY;
600 let mut n_iter = 0;
601 let lambda = self.regularization; for iter in 0..self.max_iter {
604 n_iter = iter + 1;
605
606 let (u, s, vt) = self.compute_full_svd(&completed)?;
608
609 let s_thresh: Vec<f64> = s.iter().map(|&val| (val - lambda).max(0.0)).collect();
611
612 let mut reconstructed = Array2::zeros((n_users, n_items));
614 for i in 0..n_users {
615 for j in 0..n_items {
616 for k in 0..s_thresh.len() {
617 if s_thresh[k] > 0.0 {
618 reconstructed[[i, j]] += u[[i, k]] * s_thresh[k] * vt[[k, j]];
619 }
620 }
621 }
622 }
623
624 for ((i, j), &is_observed) in scirs2_core::ndarray::indices(matrix.dim())
626 .into_iter()
627 .zip(mask.iter())
628 {
629 if !is_observed {
630 completed[[i, j]] = reconstructed[[i, j]];
631 }
632 }
633
634 let mut error = 0.0;
636 let mut count = 0;
637 for ((i, j), &is_observed) in scirs2_core::ndarray::indices(matrix.dim())
638 .into_iter()
639 .zip(mask.iter())
640 {
641 if is_observed {
642 let diff = matrix[[i, j]] - reconstructed[[i, j]];
643 error += diff * diff;
644 count += 1;
645 }
646 }
647 error = (error / count as f64).sqrt();
648
649 if (prev_error - error).abs() < self.tol {
651 break;
652 }
653 prev_error = error;
654 }
655
656 let (u, s, vt) = self.compute_truncated_svd(&completed, rank)?;
658
659 let mut u_factor = Array2::zeros((n_users, rank));
660 let mut v_factor = Array2::zeros((n_items, rank));
661
662 for i in 0..n_users {
663 for k in 0..rank {
664 u_factor[[i, k]] = u[[i, k]] * s[k].sqrt();
665 }
666 }
667
668 for j in 0..n_items {
669 for k in 0..rank {
670 v_factor[[j, k]] = vt[[k, j]] * s[k].sqrt();
671 }
672 }
673
674 let user_bias = if self.use_bias {
675 Some(Array1::zeros(n_users))
676 } else {
677 None
678 };
679 let item_bias = if self.use_bias {
680 Some(Array1::zeros(n_items))
681 } else {
682 None
683 };
684
685 Ok((
686 (u_factor, v_factor),
687 user_bias,
688 item_bias,
689 global_mean,
690 n_iter,
691 prev_error,
692 ))
693 }
694
695 fn side_info_completion(
697 &self,
698 matrix: &Array2<f64>,
699 mask: &Array2<bool>,
700 rank: usize,
701 rng: &mut impl RngExt,
702 ) -> Result<SideInfoCompletionResult> {
703 self.als_completion(matrix, mask, rank, rng)
705 }
706
707 fn compute_truncated_svd(
709 &self,
710 matrix: &Array2<f64>,
711 rank: usize,
712 ) -> Result<(Array2<f64>, Array1<f64>, Array2<f64>)> {
713 let (u, s, vt) = svd(&matrix.view(), true)
715 .map_err(|e| SklearsError::NumericalError(format!("SVD failed: {}", e)))?;
716
717 let actual_rank = rank.min(s.len());
718
719 let u_nd = u
721 .slice(scirs2_core::ndarray::s![.., ..actual_rank])
722 .to_owned();
723 let s_nd = s.slice(scirs2_core::ndarray::s![..actual_rank]).to_owned();
724 let vt_nd = vt
725 .slice(scirs2_core::ndarray::s![..actual_rank, ..])
726 .to_owned();
727
728 Ok((u_nd, s_nd, vt_nd))
729 }
730
731 fn compute_full_svd(
733 &self,
734 matrix: &Array2<f64>,
735 ) -> Result<(Array2<f64>, Array1<f64>, Array2<f64>)> {
736 svd(&matrix.view(), true)
738 .map_err(|e| SklearsError::NumericalError(format!("SVD failed: {}", e)))
739 }
740
741 fn solve_linear_system_als(&self, a: &Array2<f64>, b: &Array1<f64>) -> Result<Array1<f64>> {
743 if let Ok(result) = a.solve(b) {
745 return Ok(result);
746 }
747
748 if let Ok(chol) = a.cholesky() {
750 let result = chol.solve(b).map_err(|e| {
751 SklearsError::NumericalError(format!("Cholesky solve failed: {}", e))
752 })?;
753 return Ok(result);
754 }
755
756 let (u, s, vt) = svd(&a.view(), true)
758 .map_err(|e| SklearsError::NumericalError(format!("SVD failed: {}", e)))?;
759
760 let tolerance = 1e-12;
761
762 let k = s.len();
764 let mut s_inv = Array2::zeros((k, k));
765 for i in 0..k {
766 if s[i] > tolerance {
767 s_inv[[i, i]] = 1.0 / s[i];
768 }
769 }
770
771 let vt_t = vt.t();
773 let u_t = u.t();
774 let temp = s_inv.dot(&u_t);
775 let a_pinv = vt_t.dot(&temp);
776
777 let result = a_pinv.dot(b);
779
780 Ok(result)
781 }
782}
783
784impl MatrixCompletion<TrainedMatrixCompletion> {
785 pub fn user_factors(&self) -> &Array2<f64> {
787 &self.state.factors.0
788 }
789
790 pub fn item_factors(&self) -> &Array2<f64> {
792 &self.state.factors.1
793 }
794
795 pub fn reconstruction_error(&self) -> f64 {
797 self.state.reconstruction_error
798 }
799
800 pub fn n_iter(&self) -> usize {
802 self.state.n_iter
803 }
804
805 pub fn complete(&self, matrix: &Array2<f64>, mask: &Array2<bool>) -> Result<Array2<f64>> {
807 let (n_users, n_items) = matrix.dim();
808
809 if (n_users, n_items) != self.state.matrix_shape {
810 return Err(SklearsError::InvalidInput(
811 "Matrix dimensions don't match training dimensions".to_string(),
812 ));
813 }
814
815 let mut completed = matrix.clone();
816 let u = &self.state.factors.0;
817 let v = &self.state.factors.1;
818
819 for i in 0..n_users {
820 for j in 0..n_items {
821 if !mask[[i, j]] {
822 let mut predicted = self.state.global_bias + u.row(i).dot(&v.row(j));
824
825 if self.use_bias {
826 if let Some(ref user_bias) = self.state.user_bias {
827 predicted += user_bias[i];
828 }
829 if let Some(ref item_bias) = self.state.item_bias {
830 predicted += item_bias[j];
831 }
832 }
833
834 completed[[i, j]] = predicted;
835 }
836 }
837 }
838
839 Ok(completed)
840 }
841
842 pub fn predict(&self, user_id: usize, item_id: usize) -> Result<f64> {
844 let (n_users, n_items) = self.state.matrix_shape;
845
846 if user_id >= n_users || item_id >= n_items {
847 return Err(SklearsError::InvalidInput(
848 "User or item ID out of bounds".to_string(),
849 ));
850 }
851
852 let u = &self.state.factors.0;
853 let v = &self.state.factors.1;
854
855 let mut predicted = self.state.global_bias + u.row(user_id).dot(&v.row(item_id));
856
857 if self.use_bias {
858 if let Some(ref user_bias) = self.state.user_bias {
859 predicted += user_bias[user_id];
860 }
861 if let Some(ref item_bias) = self.state.item_bias {
862 predicted += item_bias[item_id];
863 }
864 }
865
866 Ok(predicted)
867 }
868}
869
870#[derive(Debug, Clone)]
875pub struct LowRankMatrixRecovery<State = Untrained> {
876 pub algorithm: RecoveryAlgorithm,
878 pub rank: Option<usize>,
880 pub max_iter: usize,
882 pub tol: f64,
884 pub lambda: f64,
886 pub mu: f64,
888 pub random_state: Option<u64>,
890
891 state: State,
893}
894
895#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
897#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
898pub enum RecoveryAlgorithm {
899 #[default]
900 PCP,
901 RPCA,
902 IHT,
903 AltMin,
904}
905
906#[derive(Debug, Clone)]
908#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
909pub struct TrainedLowRankMatrixRecovery {
910 pub low_rank_component: Array2<f64>,
912 pub sparse_component: Array2<f64>,
914 pub factors: Option<(Array2<f64>, Array1<f64>, Array2<f64>)>,
916 pub matrix_shape: (usize, usize),
918 pub estimated_rank: usize,
920 pub n_iter: usize,
922 pub objective_value: f64,
924}
925
926impl LowRankMatrixRecovery<Untrained> {
927 pub fn new() -> Self {
929 Self {
930 algorithm: RecoveryAlgorithm::PCP,
931 rank: None,
932 max_iter: 1000,
933 tol: 1e-6,
934 lambda: 0.1,
935 mu: 0.1,
936 random_state: None,
937 state: Untrained,
938 }
939 }
940
941 pub fn algorithm(mut self, algorithm: RecoveryAlgorithm) -> Self {
943 self.algorithm = algorithm;
944 self
945 }
946
947 pub fn rank(mut self, rank: usize) -> Self {
949 self.rank = Some(rank);
950 self
951 }
952
953 pub fn max_iter(mut self, max_iter: usize) -> Self {
955 self.max_iter = max_iter;
956 self
957 }
958
959 pub fn tol(mut self, tol: f64) -> Self {
961 self.tol = tol;
962 self
963 }
964
965 pub fn lambda(mut self, lambda: f64) -> Self {
967 self.lambda = lambda;
968 self
969 }
970
971 pub fn mu(mut self, mu: f64) -> Self {
973 self.mu = mu;
974 self
975 }
976
977 pub fn random_state(mut self, random_state: u64) -> Self {
979 self.random_state = Some(random_state);
980 self
981 }
982}
983
984impl Fit<Array2<f64>, ()> for LowRankMatrixRecovery<Untrained> {
985 type Fitted = LowRankMatrixRecovery<TrainedLowRankMatrixRecovery>;
986
987 fn fit(self, x: &Array2<f64>, _y: &()) -> Result<Self::Fitted> {
988 let (n_rows, n_cols) = x.dim();
989
990 if n_rows == 0 || n_cols == 0 {
991 return Err(SklearsError::InvalidInput(
992 "Input matrix cannot be empty".to_string(),
993 ));
994 }
995
996 let (low_rank, sparse, factors, estimated_rank, n_iter, objective_value) =
997 match self.algorithm {
998 RecoveryAlgorithm::PCP => self.principal_component_pursuit(x)?,
999 RecoveryAlgorithm::RPCA => self.robust_pca(x)?,
1000 RecoveryAlgorithm::IHT => self.iterative_hard_thresholding(x)?,
1001 RecoveryAlgorithm::AltMin => self.alternating_minimization(x)?,
1002 };
1003
1004 Ok(LowRankMatrixRecovery {
1005 algorithm: self.algorithm,
1006 rank: self.rank,
1007 max_iter: self.max_iter,
1008 tol: self.tol,
1009 lambda: self.lambda,
1010 mu: self.mu,
1011 random_state: self.random_state,
1012 state: TrainedLowRankMatrixRecovery {
1013 low_rank_component: low_rank,
1014 sparse_component: sparse,
1015 factors,
1016 matrix_shape: (n_rows, n_cols),
1017 estimated_rank,
1018 n_iter,
1019 objective_value,
1020 },
1021 })
1022 }
1023}
1024
1025impl Transform<Array2<f64>, Array2<f64>> for LowRankMatrixRecovery<TrainedLowRankMatrixRecovery> {
1026 fn transform(&self, x: &Array2<f64>) -> Result<Array2<f64>> {
1027 let (n_rows, n_cols) = x.dim();
1028
1029 if (n_rows, n_cols) != self.state.matrix_shape {
1030 return Err(SklearsError::InvalidInput(
1031 "Input matrix dimensions must match training dimensions".to_string(),
1032 ));
1033 }
1034
1035 self.recover_low_rank_component(x)
1037 }
1038}
1039
1040impl LowRankMatrixRecovery<Untrained> {
1041 fn principal_component_pursuit(&self, x: &Array2<f64>) -> Result<RPCAResult> {
1053 let (m, n) = x.dim();
1054 let mut low_rank = Array2::zeros((m, n));
1055 let mut sparse = Array2::zeros((m, n));
1056 let mut y = Array2::zeros((m, n)); let x_frob = x.iter().map(|&v| v * v).sum::<f64>().sqrt();
1060 let rel_tol = self.tol * x_frob.max(1.0);
1062
1063 let mu_min = if x_frob > f64::EPSILON {
1073 10.0 / x_frob
1074 } else {
1075 self.mu
1076 };
1077 let mut mu = self.mu.max(mu_min);
1078 let rho = 1.5_f64;
1079 let mu_bar = mu * 1e6_f64;
1081
1082 for iter in 0..self.max_iter {
1083 let tau = 1.0 / mu;
1084 let lambda_scaled = self.lambda / mu;
1085
1086 let temp1 = x - &sparse + (&y * (1.0 / mu));
1089 low_rank = self.svd_soft_threshold(&temp1, tau)?;
1090
1091 let temp2 = x - &low_rank + (&y * (1.0 / mu));
1093 sparse = self.element_wise_soft_threshold(&temp2, lambda_scaled);
1094
1095 let residual = x - &low_rank - &sparse;
1097 y = &y + mu * &residual;
1098
1099 let residual_norm = residual.iter().map(|&v| v * v).sum::<f64>().sqrt();
1101 if residual_norm < rel_tol {
1102 let (u, s, vt) = self.compute_svd(&low_rank)?;
1103 let s_max = s.iter().cloned().fold(0.0_f64, f64::max);
1104 let rank_tol = s_max * (m.max(n) as f64) * f64::EPSILON * 1e4;
1105 let estimated_rank = s.iter().filter(|&&v| v > rank_tol).count();
1106 let objective = self.compute_pcp_objective(&low_rank, &sparse);
1107 return Ok((
1108 low_rank,
1109 sparse,
1110 Some((u, s, vt)),
1111 estimated_rank.max(1),
1112 iter + 1,
1113 objective,
1114 ));
1115 }
1116
1117 mu = (rho * mu).min(mu_bar);
1119 }
1120
1121 let (u, s, vt) = self.compute_svd(&low_rank)?;
1122 let s_max = s.iter().cloned().fold(0.0_f64, f64::max);
1123 let rank_tol = s_max * (m.max(n) as f64) * f64::EPSILON * 1e4;
1124 let estimated_rank = s.iter().filter(|&&v| v > rank_tol).count();
1125 let objective = self.compute_pcp_objective(&low_rank, &sparse);
1126 Ok((
1127 low_rank,
1128 sparse,
1129 Some((u, s, vt)),
1130 estimated_rank.max(1),
1131 self.max_iter,
1132 objective,
1133 ))
1134 }
1135
1136 fn robust_pca(&self, x: &Array2<f64>) -> Result<RPCAResult> {
1138 self.principal_component_pursuit(x)
1140 }
1141
1142 fn iterative_hard_thresholding(&self, x: &Array2<f64>) -> Result<RPCAResult> {
1144 let (m, n) = x.dim();
1145 let mut low_rank = x.clone();
1146
1147 let target_rank = self.rank.unwrap_or((m.min(n) / 4).max(1));
1148
1149 for iter in 0..self.max_iter {
1150 let (u, s, vt) = self.compute_svd(&low_rank)?;
1152
1153 let mut s_thresh = Array1::zeros(s.len());
1155 for i in 0..target_rank.min(s.len()) {
1156 s_thresh[i] = s[i];
1157 }
1158
1159 let new_low_rank = self.reconstruct_from_svd(&u, &s_thresh, &vt)?;
1161
1162 let diff = &new_low_rank - &low_rank;
1164 let diff_norm = diff.iter().map(|&x| x * x).sum::<f64>().sqrt();
1165
1166 low_rank = new_low_rank;
1167
1168 if diff_norm < self.tol {
1169 let sparse = x - &low_rank;
1170 let objective = self.compute_iht_objective(&low_rank, &sparse, target_rank);
1171 return Ok((
1172 low_rank,
1173 sparse,
1174 Some((u, s_thresh, vt)),
1175 target_rank,
1176 iter + 1,
1177 objective,
1178 ));
1179 }
1180 }
1181
1182 let sparse = x - &low_rank;
1183 let (u, s, vt) = self.compute_svd(&low_rank)?;
1184 let objective = self.compute_iht_objective(&low_rank, &sparse, target_rank);
1185 Ok((
1186 low_rank,
1187 sparse,
1188 Some((u, s, vt)),
1189 target_rank,
1190 self.max_iter,
1191 objective,
1192 ))
1193 }
1194
1195 fn alternating_minimization(&self, x: &Array2<f64>) -> Result<RPCAResult> {
1197 let (m, n) = x.dim();
1198 let target_rank = self.rank.unwrap_or((m.min(n) / 4).max(1));
1199
1200 let mut rng = if let Some(seed) = self.random_state {
1202 StdRng::seed_from_u64(seed)
1203 } else {
1204 StdRng::from_rng(&mut make_rng())
1205 };
1206
1207 let mut u = Array2::zeros((m, target_rank));
1209 let mut v = Array2::zeros((n, target_rank));
1210
1211 for i in 0..m {
1212 for j in 0..target_rank {
1213 u[[i, j]] = rng.random::<f64>() - 0.5;
1214 }
1215 }
1216
1217 for i in 0..n {
1218 for j in 0..target_rank {
1219 v[[i, j]] = rng.random::<f64>() - 0.5;
1220 }
1221 }
1222
1223 for iter in 0..self.max_iter {
1224 let old_u = u.clone();
1225 let old_v = v.clone();
1226
1227 u = self.update_factor_u(x, &v)?;
1229
1230 v = self.update_factor_v(x, &u)?;
1232
1233 let u_diff = (&u - &old_u).iter().map(|&v| v * v).sum::<f64>().sqrt();
1235 let v_diff = (&v - &old_v).iter().map(|&v| v * v).sum::<f64>().sqrt();
1236
1237 if u_diff < self.tol && v_diff < self.tol {
1238 let low_rank = u.dot(&v.t());
1239 let sparse = x - &low_rank;
1240 let (u_svd, s, vt) = self.truncated_svd_factors(&low_rank, target_rank)?;
1241 let objective = self.compute_altmin_objective(&low_rank, &sparse);
1242 return Ok((
1243 low_rank,
1244 sparse,
1245 Some((u_svd, s, vt)),
1246 target_rank,
1247 iter + 1,
1248 objective,
1249 ));
1250 }
1251 }
1252
1253 let low_rank = u.dot(&v.t());
1254 let sparse = x - &low_rank;
1255 let (u_svd, s, vt) = self.truncated_svd_factors(&low_rank, target_rank)?;
1256 let objective = self.compute_altmin_objective(&low_rank, &sparse);
1257 Ok((
1258 low_rank,
1259 sparse,
1260 Some((u_svd, s, vt)),
1261 target_rank,
1262 self.max_iter,
1263 objective,
1264 ))
1265 }
1266
1267 fn svd_soft_threshold(&self, matrix: &Array2<f64>, threshold: f64) -> Result<Array2<f64>> {
1269 let (u, s, vt) = self.compute_svd(matrix)?;
1270
1271 let s_thresh: Array1<f64> = s
1273 .iter()
1274 .map(|&x| if x > threshold { x - threshold } else { 0.0 })
1275 .collect();
1276
1277 self.reconstruct_from_svd(&u, &s_thresh, &vt)
1278 }
1279
1280 fn element_wise_soft_threshold(&self, matrix: &Array2<f64>, threshold: f64) -> Array2<f64> {
1282 matrix.mapv(|x| {
1283 if x > threshold {
1284 x - threshold
1285 } else if x < -threshold {
1286 x + threshold
1287 } else {
1288 0.0
1289 }
1290 })
1291 }
1292
1293 fn compute_svd(&self, matrix: &Array2<f64>) -> Result<(Array2<f64>, Array1<f64>, Array2<f64>)> {
1295 svd(&matrix.view(), true)
1297 .map_err(|e| SklearsError::NumericalError(format!("SVD failed: {}", e)))
1298 }
1299
1300 fn truncated_svd_factors(
1307 &self,
1308 matrix: &Array2<f64>,
1309 rank: usize,
1310 ) -> Result<(Array2<f64>, Array1<f64>, Array2<f64>)> {
1311 let (u_full, s, vt_full) = self.compute_svd(matrix)?;
1312 let (m, _) = u_full.dim();
1313 let (_, n) = vt_full.dim();
1314 let r = rank.min(m).min(n).min(s.len());
1316 use scirs2_core::ndarray::s as nds;
1318 let u_trunc = u_full.slice(nds![.., ..r]).to_owned();
1319 let vt_trunc = vt_full.slice(nds![..r, ..]).to_owned();
1321 Ok((u_trunc, s, vt_trunc))
1322 }
1323
1324 fn reconstruct_from_svd(
1326 &self,
1327 u: &Array2<f64>,
1328 s: &Array1<f64>,
1329 vt: &Array2<f64>,
1330 ) -> Result<Array2<f64>> {
1331 let (m, k1) = u.dim();
1332 let (k2, n) = vt.dim();
1333
1334 if k1 != k2 || k1 != s.len() {
1335 return Err(SklearsError::InvalidInput(
1336 "Inconsistent dimensions in SVD reconstruction".to_string(),
1337 ));
1338 }
1339
1340 let mut result = Array2::zeros((m, n));
1341
1342 for i in 0..m {
1343 for j in 0..n {
1344 let mut sum = 0.0;
1345 for k in 0..k1 {
1346 sum += u[[i, k]] * s[k] * vt[[k, j]];
1347 }
1348 result[[i, j]] = sum;
1349 }
1350 }
1351
1352 Ok(result)
1353 }
1354
1355 fn update_factor_u(&self, x: &Array2<f64>, v: &Array2<f64>) -> Result<Array2<f64>> {
1357 let mut vtv = v.t().dot(v);
1359 let xv = x.dot(v);
1360
1361 for i in 0..vtv.nrows() {
1363 vtv[[i, i]] += 1e-12;
1364 }
1365
1366 let vtv_inv = vtv.inv().map_err(|e| {
1368 SklearsError::NumericalError(format!("Failed to invert matrix in factor update: {}", e))
1369 })?;
1370
1371 let u = xv.dot(&vtv_inv);
1372
1373 Ok(u)
1374 }
1375
1376 fn update_factor_v(&self, x: &Array2<f64>, u: &Array2<f64>) -> Result<Array2<f64>> {
1378 let mut utu = u.t().dot(u);
1380 let xtu = x.t().dot(u);
1381
1382 for i in 0..utu.nrows() {
1384 utu[[i, i]] += 1e-12;
1385 }
1386
1387 let utu_inv = utu.inv().map_err(|e| {
1389 SklearsError::NumericalError(format!("Failed to invert matrix in factor update: {}", e))
1390 })?;
1391
1392 let v = xtu.dot(&utu_inv);
1393
1394 Ok(v)
1395 }
1396
1397 fn compute_pcp_objective(&self, low_rank: &Array2<f64>, sparse: &Array2<f64>) -> f64 {
1399 let nuclear_norm = if let Ok((_, s, _)) = self.compute_svd(low_rank) {
1400 s.sum()
1401 } else {
1402 0.0
1403 };
1404
1405 let l1_norm = sparse.iter().map(|&x| x.abs()).sum::<f64>();
1406
1407 nuclear_norm + self.lambda * l1_norm
1408 }
1409
1410 fn compute_iht_objective(
1412 &self,
1413 _low_rank: &Array2<f64>,
1414 sparse: &Array2<f64>,
1415 _rank: usize,
1416 ) -> f64 {
1417 sparse.iter().map(|&x| x * x).sum::<f64>().sqrt()
1419 }
1420
1421 fn compute_altmin_objective(&self, _low_rank: &Array2<f64>, sparse: &Array2<f64>) -> f64 {
1423 sparse.iter().map(|&x| x * x).sum::<f64>().sqrt()
1425 }
1426}
1427
1428impl LowRankMatrixRecovery<TrainedLowRankMatrixRecovery> {
1429 pub fn low_rank_component(&self) -> &Array2<f64> {
1431 &self.state.low_rank_component
1432 }
1433
1434 pub fn sparse_component(&self) -> &Array2<f64> {
1436 &self.state.sparse_component
1437 }
1438
1439 pub fn estimated_rank(&self) -> usize {
1441 self.state.estimated_rank
1442 }
1443
1444 pub fn factors(&self) -> &Option<(Array2<f64>, Array1<f64>, Array2<f64>)> {
1446 &self.state.factors
1447 }
1448
1449 pub fn recover_low_rank_component(&self, x: &Array2<f64>) -> Result<Array2<f64>> {
1451 let corruption_estimate = x - &self.state.low_rank_component;
1454 Ok(x - &self.element_wise_soft_threshold(&corruption_estimate, self.lambda))
1455 }
1456
1457 fn element_wise_soft_threshold(&self, matrix: &Array2<f64>, threshold: f64) -> Array2<f64> {
1459 matrix.mapv(|x| {
1460 if x > threshold {
1461 x - threshold
1462 } else if x < -threshold {
1463 x + threshold
1464 } else {
1465 0.0
1466 }
1467 })
1468 }
1469
1470 pub fn reconstruct(&self) -> Array2<f64> {
1472 &self.state.low_rank_component + &self.state.sparse_component
1473 }
1474}
1475
1476impl Default for LowRankMatrixRecovery<Untrained> {
1477 fn default() -> Self {
1478 Self::new()
1479 }
1480}
1481
1482#[allow(non_snake_case)]
1483#[cfg(test)]
1484mod tests {
1485 use super::*;
1486 use scirs2_core::ndarray::array;
1487
1488 #[test]
1489 fn test_matrix_completion_svd() {
1490 let matrix = array![
1492 [5.0, 3.0, 0.0, 1.0],
1493 [4.0, 0.0, 0.0, 1.0],
1494 [1.0, 1.0, 0.0, 5.0],
1495 [1.0, 0.0, 0.0, 4.0],
1496 [0.0, 1.0, 5.0, 4.0],
1497 ];
1498
1499 let mask = array![
1500 [true, true, false, true],
1501 [true, false, false, true],
1502 [true, true, false, true],
1503 [true, false, false, true],
1504 [false, true, true, true],
1505 ];
1506
1507 let mc = MatrixCompletion::new()
1508 .rank(2)
1509 .algorithm(CompletionAlgorithm::SVD)
1510 .max_iter(10)
1511 .random_state(42);
1512
1513 let result = mc.fit(&matrix, &mask);
1514 assert!(result.is_ok());
1515
1516 let trained = result.expect("operation should succeed");
1517 assert_eq!(trained.state.rank, 2);
1518 assert!(trained.state.reconstruction_error.is_finite());
1519
1520 let completed = trained
1522 .complete(&matrix, &mask)
1523 .expect("operation should succeed");
1524 assert_eq!(completed.dim(), matrix.dim());
1525
1526 let prediction = trained.predict(0, 2).expect("prediction should succeed");
1528 assert!(prediction.is_finite());
1529 }
1530
1531 #[test]
1532 fn test_matrix_completion_als() {
1533 let matrix = array![
1534 [5.0, 3.0, 0.0, 1.0],
1535 [4.0, 0.0, 0.0, 1.0],
1536 [1.0, 1.0, 0.0, 5.0],
1537 ];
1538
1539 let mask = array![
1540 [true, true, false, true],
1541 [true, false, false, true],
1542 [true, true, false, true],
1543 ];
1544
1545 let mc = MatrixCompletion::new()
1546 .rank(2)
1547 .algorithm(CompletionAlgorithm::ALS)
1548 .max_iter(5)
1549 .random_state(42);
1550
1551 let result = mc.fit(&matrix, &mask);
1552 assert!(result.is_ok());
1553
1554 let trained = result.expect("operation should succeed");
1555 assert_eq!(trained.state.rank, 2);
1556 assert!(trained.state.n_iter > 0);
1557 }
1558
1559 #[test]
1560 fn test_matrix_completion_nuclear_norm() {
1561 let matrix = array![[5.0, 3.0], [4.0, 2.0], [1.0, 1.0],];
1562
1563 let mask = array![[true, false], [true, true], [false, true],];
1564
1565 let mc = MatrixCompletion::new()
1566 .rank(1)
1567 .algorithm(CompletionAlgorithm::NuclearNorm)
1568 .regularization(0.1)
1569 .max_iter(5)
1570 .random_state(42);
1571
1572 let result = mc.fit(&matrix, &mask);
1573 assert!(result.is_ok());
1574
1575 let trained = result.expect("operation should succeed");
1576 assert!(trained.state.reconstruction_error.is_finite());
1577 }
1578
1579 #[test]
1580 fn test_matrix_completion_parameters() {
1581 let mc = MatrixCompletion::new()
1582 .rank(5)
1583 .algorithm(CompletionAlgorithm::ALS)
1584 .max_iter(200)
1585 .tol(1e-8)
1586 .learning_rate(0.05)
1587 .regularization(0.05)
1588 .use_bias(false);
1589
1590 assert_eq!(mc.rank, Some(5));
1591 assert_eq!(mc.algorithm, CompletionAlgorithm::ALS);
1592 assert_eq!(mc.max_iter, 200);
1593 assert_eq!(mc.tol, 1e-8);
1594 assert_eq!(mc.learning_rate, 0.05);
1595 assert_eq!(mc.regularization, 0.05);
1596 assert!(!mc.use_bias);
1597 }
1598
1599 #[test]
1600 fn test_matrix_completion_invalid_mask() {
1601 let matrix = array![[1.0, 2.0], [3.0, 4.0]];
1602 let mask = array![[true]]; let mc = MatrixCompletion::new();
1605 let result = mc.fit(&matrix, &mask);
1606 assert!(result.is_err());
1607 }
1608
1609 #[test]
1610 fn test_matrix_completion_no_observed() {
1611 let matrix = array![[1.0, 2.0], [3.0, 4.0]];
1612 let mask = array![[false, false], [false, false]]; let mc = MatrixCompletion::new();
1615 let result = mc.fit(&matrix, &mask);
1616 assert!(result.is_err());
1617 }
1618
1619 #[test]
1620 fn test_low_rank_matrix_recovery_pcp() {
1621 let low_rank = array![[1.0, 2.0, 3.0], [2.0, 4.0, 6.0], [3.0, 6.0, 9.0]];
1623 let sparse = array![[0.0, 0.0, 10.0], [0.0, 0.0, 0.0], [0.0, -5.0, 0.0]];
1624 let corrupted = &low_rank + &sparse;
1625
1626 let lrmr = LowRankMatrixRecovery::new()
1627 .algorithm(RecoveryAlgorithm::PCP)
1628 .max_iter(100)
1629 .lambda(0.01) .mu(0.1)
1631 .random_state(42);
1632
1633 let trained_lrmr = lrmr
1634 .fit(&corrupted, &())
1635 .expect("model fitting should succeed");
1636
1637 assert_eq!(trained_lrmr.low_rank_component().dim(), (3, 3));
1638 assert_eq!(trained_lrmr.sparse_component().dim(), (3, 3));
1639 assert!(trained_lrmr.estimated_rank() > 0);
1640 assert!(trained_lrmr.state.objective_value >= 0.0);
1641
1642 let reconstruction = trained_lrmr.reconstruct();
1644 assert_eq!(reconstruction.dim(), (3, 3));
1645
1646 for val in reconstruction.iter() {
1648 assert!(val.is_finite());
1649 }
1650 }
1651
1652 #[test]
1653 fn test_low_rank_matrix_recovery_iht() {
1654 let matrix = array![
1655 [1.0, 2.0],
1656 [2.0, 4.1], ];
1658
1659 let lrmr = LowRankMatrixRecovery::new()
1660 .algorithm(RecoveryAlgorithm::IHT)
1661 .rank(1)
1662 .max_iter(50)
1663 .random_state(42);
1664
1665 let trained_lrmr = lrmr
1666 .fit(&matrix, &())
1667 .expect("model fitting should succeed");
1668
1669 assert_eq!(trained_lrmr.estimated_rank(), 1);
1670 assert!(trained_lrmr.state.n_iter <= 50);
1671
1672 let recovered = trained_lrmr
1674 .transform(&matrix)
1675 .expect("transformation should succeed");
1676 assert_eq!(recovered.dim(), (2, 2));
1677
1678 for val in recovered.iter() {
1680 assert!(val.is_finite());
1681 }
1682 }
1683
1684 #[test]
1685 fn test_low_rank_matrix_recovery_alternating_minimization() {
1686 let matrix = array![
1687 [1.0, 2.0, 3.0],
1688 [2.0, 4.0, 6.0],
1689 [1.1, 2.1, 3.1] ];
1691
1692 let lrmr = LowRankMatrixRecovery::new()
1693 .algorithm(RecoveryAlgorithm::AltMin)
1694 .rank(2)
1695 .max_iter(20)
1696 .lambda(0.01)
1697 .random_state(42);
1698
1699 let trained_lrmr = lrmr
1700 .fit(&matrix, &())
1701 .expect("model fitting should succeed");
1702
1703 assert!(trained_lrmr.estimated_rank() >= 1 && trained_lrmr.estimated_rank() <= 3);
1704 assert!(trained_lrmr.state.n_iter <= 20);
1705
1706 assert!(trained_lrmr.factors().is_some());
1708
1709 let (u, s, vt) = trained_lrmr
1710 .factors()
1711 .as_ref()
1712 .expect("operation should succeed");
1713 assert_eq!(u.ncols(), 2);
1714 assert_eq!(vt.nrows(), 2);
1715 assert_eq!(s.len(), 3); }
1717
1718 #[test]
1719 fn test_low_rank_matrix_recovery_parameters() {
1720 let lrmr = LowRankMatrixRecovery::new()
1721 .algorithm(RecoveryAlgorithm::RPCA)
1722 .rank(5)
1723 .max_iter(500)
1724 .tol(1e-8)
1725 .lambda(0.05)
1726 .mu(0.05)
1727 .random_state(123);
1728
1729 assert_eq!(lrmr.algorithm, RecoveryAlgorithm::RPCA);
1730 assert_eq!(lrmr.rank, Some(5));
1731 assert_eq!(lrmr.max_iter, 500);
1732 assert_eq!(lrmr.tol, 1e-8);
1733 assert_eq!(lrmr.lambda, 0.05);
1734 assert_eq!(lrmr.mu, 0.05);
1735 assert_eq!(lrmr.random_state, Some(123));
1736 }
1737
1738 #[test]
1739 fn test_low_rank_matrix_recovery_error_cases() {
1740 let empty_matrix = Array2::<f64>::zeros((0, 0));
1741 let lrmr = LowRankMatrixRecovery::new();
1742 let result = lrmr.fit(&empty_matrix, &());
1743 assert!(result.is_err());
1744 }
1745
1746 #[test]
1747 fn test_low_rank_matrix_recovery_convergence() {
1748 let matrix = array![[1.0, 2.0], [2.0, 4.0]];
1750
1751 let lrmr = LowRankMatrixRecovery::new()
1752 .algorithm(RecoveryAlgorithm::PCP)
1753 .max_iter(100)
1754 .tol(1e-6)
1755 .lambda(0.01) .mu(0.1)
1757 .random_state(42);
1758
1759 let trained_lrmr = lrmr
1760 .fit(&matrix, &())
1761 .expect("model fitting should succeed");
1762
1763 assert!(trained_lrmr.state.n_iter <= 100);
1765 assert!(trained_lrmr.estimated_rank() <= 2);
1766
1767 let low_rank = trained_lrmr.low_rank_component();
1769 assert_eq!(low_rank.dim(), (2, 2));
1770
1771 let sparse = trained_lrmr.sparse_component();
1773 let sparse_norm = sparse.iter().map(|&x| x.abs()).sum::<f64>();
1774 assert!(sparse_norm < 5.0); }
1776}