Skip to main content

sklears_decomposition/
matrix_completion.rs

1//! Matrix completion algorithms
2//!
3//! This module provides matrix completion methods for filling missing values:
4//! - Low-rank matrix completion using SVD
5//! - Iterative matrix completion with ALS
6//! - Nuclear norm minimization
7//! - Matrix completion with side information
8
9use 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
20/// Type alias for complex completion result with side information
21type SideInfoCompletionResult = (
22    (Array2<f64>, Array2<f64>),
23    Option<Array1<f64>>,
24    Option<Array1<f64>>,
25    f64,
26    usize,
27    f64,
28);
29
30/// Type alias for matrix completion result
31#[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
41/// Type alias for RPCA decomposition result
42type RPCAResult = (
43    Array2<f64>,
44    Array2<f64>,
45    Option<(Array2<f64>, Array1<f64>, Array2<f64>)>,
46    usize,
47    usize,
48    f64,
49);
50
51/// Matrix completion algorithm variants
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
53#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
54pub enum CompletionAlgorithm {
55    /// SVD-based low-rank matrix completion
56    #[default]
57    SVD,
58    /// Alternating Least Squares
59    ALS,
60    /// Nuclear norm minimization (approximate)
61    NuclearNorm,
62    /// Matrix completion with side information
63    SideInfo,
64}
65
66/// Matrix completion transformer
67#[derive(Debug, Clone)]
68pub struct MatrixCompletion<State = Untrained> {
69    /// Rank of the matrix completion
70    pub rank: Option<usize>,
71    /// Algorithm to use
72    pub algorithm: CompletionAlgorithm,
73    /// Maximum number of iterations
74    pub max_iter: usize,
75    /// Convergence tolerance
76    pub tol: f64,
77    /// Random state for reproducibility
78    pub random_state: Option<u64>,
79    /// Learning rate for gradient-based methods
80    pub learning_rate: f64,
81    /// Regularization parameter
82    pub regularization: f64,
83    /// Whether to use bias terms
84    pub use_bias: bool,
85
86    /// Trained state
87    state: State,
88}
89
90/// Trained matrix completion state
91#[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    /// Create a new matrix completion transformer
113    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    /// Set the rank for matrix completion
128    pub fn rank(mut self, rank: usize) -> Self {
129        self.rank = Some(rank);
130        self
131    }
132
133    /// Set the algorithm
134    pub fn algorithm(mut self, algorithm: CompletionAlgorithm) -> Self {
135        self.algorithm = algorithm;
136        self
137    }
138
139    /// Set maximum iterations
140    pub fn max_iter(mut self, max_iter: usize) -> Self {
141        self.max_iter = max_iter;
142        self
143    }
144
145    /// Set tolerance
146    pub fn tol(mut self, tol: f64) -> Self {
147        self.tol = tol;
148        self
149    }
150
151    /// Set random state
152    pub fn random_state(mut self, random_state: u64) -> Self {
153        self.random_state = Some(random_state);
154        self
155    }
156
157    /// Set learning rate
158    pub fn learning_rate(mut self, learning_rate: f64) -> Self {
159        self.learning_rate = learning_rate;
160        self
161    }
162
163    /// Set regularization parameter
164    pub fn regularization(mut self, regularization: f64) -> Self {
165        self.regularization = regularization;
166        self
167    }
168
169    /// Set whether to use bias terms
170    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        // Count observed entries
189        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        // Determine rank
197        let rank = self.rank.unwrap_or((n_users.min(n_items) / 2).max(1));
198
199        // Initialize random number generator with optional seed for reproducibility
200        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        // Run matrix completion algorithm
207        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    /// SVD-based matrix completion
245    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        // Initialize with mean of observed values
255        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        // Create initial completed matrix with global mean for missing values
265        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            // Perform SVD on current completed matrix
282            let (u, s, vt) = self.compute_truncated_svd(&completed, rank)?;
283
284            // Reconstruct with truncated SVD
285            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            // Update only missing entries
295            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            // Compute error on observed entries
305            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            // Check convergence
320            if (prev_error - error).abs() < self.tol {
321                break;
322            }
323            prev_error = error;
324        }
325
326        // Final SVD to get factors
327        let (u, s, vt) = self.compute_truncated_svd(&completed, rank)?;
328
329        // Create factor matrices: U * sqrt(S) and V * sqrt(S)
330        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    /// Alternating Least Squares matrix completion
368    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        // Initialize factors randomly
378        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        // Initialize biases
394        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        // Compute global bias
407        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            // Update user factors
423            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                // Solve linear system A * u_i = b
447                let u_i = self.solve_linear_system_als(&a, &b)?;
448                u.row_mut(i).assign(&u_i);
449            }
450
451            // Update item factors
452            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                // Solve linear system A * v_j = b
476                let v_j = self.solve_linear_system_als(&a, &b)?;
477                v.row_mut(j).assign(&v_j);
478            }
479
480            // Update biases if enabled
481            if self.use_bias {
482                // Update user biases
483                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                // Update item biases
506                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            // Compute error
530            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            // Check convergence
552            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    /// Nuclear norm minimization (simplified version)
569    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        // For simplicity, implement this as a variation of SVD completion with soft thresholding
577        let (n_users, n_items) = matrix.dim();
578
579        // Initialize with mean of observed values
580        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; // Nuclear norm regularization parameter
602
603        for iter in 0..self.max_iter {
604            n_iter = iter + 1;
605
606            // Compute SVD
607            let (u, s, vt) = self.compute_full_svd(&completed)?;
608
609            // Soft thresholding on singular values (nuclear norm regularization)
610            let s_thresh: Vec<f64> = s.iter().map(|&val| (val - lambda).max(0.0)).collect();
611
612            // Reconstruct with thresholded singular values
613            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            // Update only missing entries
625            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            // Compute error on observed entries
635            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            // Check convergence
650            if (prev_error - error).abs() < self.tol {
651                break;
652            }
653            prev_error = error;
654        }
655
656        // Final decomposition to get factors
657        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    /// Matrix completion with side information (simplified)
696    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        // For now, implement this as regular ALS (can be extended with side information later)
704        self.als_completion(matrix, mask, rank, rng)
705    }
706
707    /// Compute truncated SVD
708    fn compute_truncated_svd(
709        &self,
710        matrix: &Array2<f64>,
711        rank: usize,
712    ) -> Result<(Array2<f64>, Array1<f64>, Array2<f64>)> {
713        // Use scirs2-linalg SVD
714        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        // Truncate to desired rank
720        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    /// Compute full SVD (for nuclear norm method)
732    fn compute_full_svd(
733        &self,
734        matrix: &Array2<f64>,
735    ) -> Result<(Array2<f64>, Array1<f64>, Array2<f64>)> {
736        // Use scirs2-linalg SVD directly
737        svd(&matrix.view(), true)
738            .map_err(|e| SklearsError::NumericalError(format!("SVD failed: {}", e)))
739    }
740
741    /// Solve linear system for ALS
742    fn solve_linear_system_als(&self, a: &Array2<f64>, b: &Array1<f64>) -> Result<Array1<f64>> {
743        // Try direct solve first (uses LU decomposition internally)
744        if let Ok(result) = a.solve(b) {
745            return Ok(result);
746        }
747
748        // Fallback to Cholesky if matrix is positive definite
749        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        // Final fallback: pseudoinverse using SVD
757        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        // Create diagonal inverse matrix S^+
763        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        // Compute pseudoinverse: A^+ = V * S^+ * U^T
772        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        // Compute result: A^+ * b
778        let result = a_pinv.dot(b);
779
780        Ok(result)
781    }
782}
783
784impl MatrixCompletion<TrainedMatrixCompletion> {
785    /// Get the user factors
786    pub fn user_factors(&self) -> &Array2<f64> {
787        &self.state.factors.0
788    }
789
790    /// Get the item factors
791    pub fn item_factors(&self) -> &Array2<f64> {
792        &self.state.factors.1
793    }
794
795    /// Get the reconstruction error
796    pub fn reconstruction_error(&self) -> f64 {
797        self.state.reconstruction_error
798    }
799
800    /// Get the number of iterations
801    pub fn n_iter(&self) -> usize {
802        self.state.n_iter
803    }
804
805    /// Complete the matrix (fill in missing values)
806    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                    // Fill missing value
823                    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    /// Predict rating for a specific user-item pair
843    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/// Low-rank Matrix Recovery algorithms for robust decomposition
871///
872/// These methods are designed to recover low-rank matrices from corrupted observations,
873/// handling outliers and sparse corruption better than standard matrix completion.
874#[derive(Debug, Clone)]
875pub struct LowRankMatrixRecovery<State = Untrained> {
876    /// Algorithm to use for recovery
877    pub algorithm: RecoveryAlgorithm,
878    /// Rank of the low-rank component (if known)
879    pub rank: Option<usize>,
880    /// Maximum number of iterations
881    pub max_iter: usize,
882    /// Convergence tolerance
883    pub tol: f64,
884    /// Regularization parameter for sparse component
885    pub lambda: f64,
886    /// Regularization parameter for nuclear norm
887    pub mu: f64,
888    /// Random state for reproducibility
889    pub random_state: Option<u64>,
890
891    /// Trained state
892    state: State,
893}
894
895/// Low-rank matrix recovery algorithms
896#[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/// Trained low-rank matrix recovery state
907#[derive(Debug, Clone)]
908#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
909pub struct TrainedLowRankMatrixRecovery {
910    /// Low-rank component
911    pub low_rank_component: Array2<f64>,
912    /// Sparse component (corruption/outliers)
913    pub sparse_component: Array2<f64>,
914    /// SVD factors of low-rank component
915    pub factors: Option<(Array2<f64>, Array1<f64>, Array2<f64>)>,
916    /// Matrix dimensions
917    pub matrix_shape: (usize, usize),
918    /// Estimated rank
919    pub estimated_rank: usize,
920    /// Number of iterations performed
921    pub n_iter: usize,
922    /// Final objective value
923    pub objective_value: f64,
924}
925
926impl LowRankMatrixRecovery<Untrained> {
927    /// Create a new low-rank matrix recovery transformer
928    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    /// Set the recovery algorithm
942    pub fn algorithm(mut self, algorithm: RecoveryAlgorithm) -> Self {
943        self.algorithm = algorithm;
944        self
945    }
946
947    /// Set the rank (if known)
948    pub fn rank(mut self, rank: usize) -> Self {
949        self.rank = Some(rank);
950        self
951    }
952
953    /// Set maximum iterations
954    pub fn max_iter(mut self, max_iter: usize) -> Self {
955        self.max_iter = max_iter;
956        self
957    }
958
959    /// Set tolerance
960    pub fn tol(mut self, tol: f64) -> Self {
961        self.tol = tol;
962        self
963    }
964
965    /// Set sparsity regularization parameter
966    pub fn lambda(mut self, lambda: f64) -> Self {
967        self.lambda = lambda;
968        self
969    }
970
971    /// Set nuclear norm regularization parameter
972    pub fn mu(mut self, mu: f64) -> Self {
973        self.mu = mu;
974        self
975    }
976
977    /// Set random state
978    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        // Return the low-rank component recovered from the input
1036        self.recover_low_rank_component(x)
1037    }
1038}
1039
1040impl LowRankMatrixRecovery<Untrained> {
1041    /// Principal Component Pursuit using inexact ALM (ADMM with adaptive mu)
1042    ///
1043    /// Numerically stable implementation following Lin, Chen & Ma (2010):
1044    /// - mu is initialised as `max(user_mu, 1.25 / ||M||_F)` so that the initial
1045    ///   SVD threshold `tau = 1/mu` stays below the spectral norm and avoids the
1046    ///   trivial decomposition (L=0, S=M).
1047    /// - Adaptive mu schedule: mu_{k+1} = rho * mu_k (capped at mu_bar)
1048    /// - SVD threshold tau = 1 / mu_k
1049    /// - Sparse threshold: user-supplied lambda / mu_k
1050    /// - Relative convergence: ||M - L - S||_F / max(1, ||M||_F) < tol
1051    /// - Estimated rank uses threshold relative to largest singular value
1052    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)); // Lagrange multipliers
1057
1058        // Frobenius norm of input (quick upper bound on spectral norm)
1059        let x_frob = x.iter().map(|&v| v * v).sum::<f64>().sqrt();
1060        // Scale tolerance relative to ||M||_F to avoid premature/delayed convergence
1061        let rel_tol = self.tol * x_frob.max(1.0);
1062
1063        // Initialise mu: ensure tau = 1/mu is well below the spectral norm so
1064        // the SVD soft-threshold puts meaningful content in L rather than zeroing
1065        // all singular values and placing all signal in S (trivial solution).
1066        //
1067        // We use ||M||_F as a quick upper-bound on ||M||_2 and scale so that
1068        // the initial threshold tau = 1/mu ≈ ||M||_F / 10, which is safely
1069        // below the spectral norm for typical matrices while keeping tau large
1070        // enough for the nuclear-norm penalty to have effect.  The user's mu
1071        // is honoured if it already produces a smaller tau.
1072        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        // mu_bar: cap growth to avoid numeric overflow in y / mu
1080        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            // Update low-rank component using SVD soft thresholding
1087            // Guard: y / mu is well-defined because mu > 0
1088            let temp1 = x - &sparse + (&y * (1.0 / mu));
1089            low_rank = self.svd_soft_threshold(&temp1, tau)?;
1090
1091            // Update sparse component using element-wise soft thresholding
1092            let temp2 = x - &low_rank + (&y * (1.0 / mu));
1093            sparse = self.element_wise_soft_threshold(&temp2, lambda_scaled);
1094
1095            // Update Lagrange multipliers
1096            let residual = x - &low_rank - &sparse;
1097            y = &y + mu * &residual;
1098
1099            // Relative convergence criterion: ||residual||_F / max(1, ||M||_F)
1100            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            // Grow mu (inexact ALM schedule)
1118            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    /// Robust PCA (simplified version of PCP)
1137    fn robust_pca(&self, x: &Array2<f64>) -> Result<RPCAResult> {
1138        // Use PCP as the underlying algorithm for RPCA
1139        self.principal_component_pursuit(x)
1140    }
1141
1142    /// Iterative Hard Thresholding
1143    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            // SVD and hard thresholding to enforce rank constraint
1151            let (u, s, vt) = self.compute_svd(&low_rank)?;
1152
1153            // Keep only top `target_rank` singular values
1154            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            // Reconstruct low-rank matrix
1160            let new_low_rank = self.reconstruct_from_svd(&u, &s_thresh, &vt)?;
1161
1162            // Check convergence
1163            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    /// Alternating Minimization
1196    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        // Initialize random number generator with optional seed for reproducibility
1201        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        // Initialize factors randomly
1208        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            // Update U by solving least squares
1228            u = self.update_factor_u(x, &v)?;
1229
1230            // Update V by solving least squares
1231            v = self.update_factor_v(x, &u)?;
1232
1233            // Check convergence
1234            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    /// SVD soft thresholding
1268    fn svd_soft_threshold(&self, matrix: &Array2<f64>, threshold: f64) -> Result<Array2<f64>> {
1269        let (u, s, vt) = self.compute_svd(matrix)?;
1270
1271        // Apply soft thresholding to singular values
1272        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    /// Element-wise soft thresholding
1281    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    /// Compute SVD decomposition (full matrices)
1294    fn compute_svd(&self, matrix: &Array2<f64>) -> Result<(Array2<f64>, Array1<f64>, Array2<f64>)> {
1295        // Use scirs2-linalg SVD directly (full_matrices = true)
1296        svd(&matrix.view(), true)
1297            .map_err(|e| SklearsError::NumericalError(format!("SVD failed: {}", e)))
1298    }
1299
1300    /// Compute SVD and return truncated factors (U[:, :rank], s[:min(m,n)], Vt[:rank, :]).
1301    ///
1302    /// The returned tuple satisfies:
1303    /// - `u.ncols() == rank`
1304    /// - `vt.nrows() == rank`
1305    /// - `s.len() == min(m, n)` (full singular value vector retained for inspection)
1306    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        // Clamp rank to valid range
1315        let r = rank.min(m).min(n).min(s.len());
1316        // Slice U to first r columns
1317        use scirs2_core::ndarray::s as nds;
1318        let u_trunc = u_full.slice(nds![.., ..r]).to_owned();
1319        // Slice Vt to first r rows
1320        let vt_trunc = vt_full.slice(nds![..r, ..]).to_owned();
1321        Ok((u_trunc, s, vt_trunc))
1322    }
1323
1324    /// Reconstruct matrix from SVD components
1325    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    /// Update factor U in alternating minimization
1356    fn update_factor_u(&self, x: &Array2<f64>, v: &Array2<f64>) -> Result<Array2<f64>> {
1357        // Solve for U in ||X - UV^T||_F^2 by solving U = XV(V^TV)^{-1}
1358        let mut vtv = v.t().dot(v);
1359        let xv = x.dot(v);
1360
1361        // Add regularization for numerical stability
1362        for i in 0..vtv.nrows() {
1363            vtv[[i, i]] += 1e-12;
1364        }
1365
1366        // Use scirs2-linalg for matrix inversion
1367        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    /// Update factor V in alternating minimization
1377    fn update_factor_v(&self, x: &Array2<f64>, u: &Array2<f64>) -> Result<Array2<f64>> {
1378        // Solve for V in ||X - UV^T||_F^2 by solving V = X^TU(U^TU)^{-1}
1379        let mut utu = u.t().dot(u);
1380        let xtu = x.t().dot(u);
1381
1382        // Add regularization for numerical stability
1383        for i in 0..utu.nrows() {
1384            utu[[i, i]] += 1e-12;
1385        }
1386
1387        // Use scirs2-linalg for matrix inversion
1388        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    /// Compute PCP objective function
1398    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    /// Compute IHT objective function
1411    fn compute_iht_objective(
1412        &self,
1413        _low_rank: &Array2<f64>,
1414        sparse: &Array2<f64>,
1415        _rank: usize,
1416    ) -> f64 {
1417        // Simple Frobenius norm of the sparse component
1418        sparse.iter().map(|&x| x * x).sum::<f64>().sqrt()
1419    }
1420
1421    /// Compute alternating minimization objective function
1422    fn compute_altmin_objective(&self, _low_rank: &Array2<f64>, sparse: &Array2<f64>) -> f64 {
1423        // Frobenius norm of the sparse component
1424        sparse.iter().map(|&x| x * x).sum::<f64>().sqrt()
1425    }
1426}
1427
1428impl LowRankMatrixRecovery<TrainedLowRankMatrixRecovery> {
1429    /// Get the recovered low-rank component
1430    pub fn low_rank_component(&self) -> &Array2<f64> {
1431        &self.state.low_rank_component
1432    }
1433
1434    /// Get the sparse component (outliers/corruption)
1435    pub fn sparse_component(&self) -> &Array2<f64> {
1436        &self.state.sparse_component
1437    }
1438
1439    /// Get the estimated rank
1440    pub fn estimated_rank(&self) -> usize {
1441        self.state.estimated_rank
1442    }
1443
1444    /// Get SVD factors if available
1445    pub fn factors(&self) -> &Option<(Array2<f64>, Array1<f64>, Array2<f64>)> {
1446        &self.state.factors
1447    }
1448
1449    /// Recover low-rank component from new corrupted data
1450    pub fn recover_low_rank_component(&self, x: &Array2<f64>) -> Result<Array2<f64>> {
1451        // For simplicity, assume the same corruption pattern and apply the learned decomposition
1452        // In practice, this would involve solving the recovery problem for new data
1453        let corruption_estimate = x - &self.state.low_rank_component;
1454        Ok(x - &self.element_wise_soft_threshold(&corruption_estimate, self.lambda))
1455    }
1456
1457    /// Element-wise soft thresholding (helper method)
1458    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    /// Reconstruct the original matrix (low-rank + sparse)
1471    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        // Create a test matrix with some missing values
1491        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        // Test completion
1521        let completed = trained
1522            .complete(&matrix, &mask)
1523            .expect("operation should succeed");
1524        assert_eq!(completed.dim(), matrix.dim());
1525
1526        // Test prediction
1527        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]]; // Wrong dimensions
1603
1604        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]]; // No observed values
1613
1614        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        // Create a low-rank matrix with sparse corruption
1622        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) // Smaller lambda to allow low-rank recovery
1630            .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        // Check that reconstruction is close to original
1643        let reconstruction = trained_lrmr.reconstruct();
1644        assert_eq!(reconstruction.dim(), (3, 3));
1645
1646        // All values should be finite
1647        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], // Slightly corrupted low-rank matrix
1657        ];
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        // Test transform
1673        let recovered = trained_lrmr
1674            .transform(&matrix)
1675            .expect("transformation should succeed");
1676        assert_eq!(recovered.dim(), (2, 2));
1677
1678        // All values should be finite
1679        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] // Approximately rank-1 with noise
1690        ];
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        // Check that factors are available
1707        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); // min(m, n)
1716    }
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        // Create a simple rank-1 matrix
1749        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) // Smaller lambda for cleaner low-rank decomposition
1756            .mu(0.1)
1757            .random_state(42);
1758
1759        let trained_lrmr = lrmr
1760            .fit(&matrix, &())
1761            .expect("model fitting should succeed");
1762
1763        // Should converge quickly for a simple case
1764        assert!(trained_lrmr.state.n_iter <= 100);
1765        assert!(trained_lrmr.estimated_rank() <= 2);
1766
1767        // Low-rank component should be close to original for this clean case
1768        let low_rank = trained_lrmr.low_rank_component();
1769        assert_eq!(low_rank.dim(), (2, 2));
1770
1771        // Sparse component should be small for this clean case
1772        let sparse = trained_lrmr.sparse_component();
1773        let sparse_norm = sparse.iter().map(|&x| x.abs()).sum::<f64>();
1774        assert!(sparse_norm < 5.0); // Should be relatively small for clean data
1775    }
1776}