use crate::error::{CsError, CsResult};
use crate::handle::LcgRng;
#[derive(Debug, Clone)]
pub struct RpcaGdConfig {
pub rank: usize,
pub sparsity_fraction: f64,
pub max_iter: usize,
pub lr: f64,
pub tol: f64,
}
#[derive(Debug, Clone)]
pub struct RpcaGd {
u: Vec<f64>,
v: Vec<f64>,
s: Vec<f64>,
m_rows: usize,
n_cols: usize,
config: RpcaGdConfig,
iterations: usize,
converged: bool,
}
impl RpcaGd {
pub fn new(
m_rows: usize,
n_cols: usize,
config: RpcaGdConfig,
rng: &mut LcgRng,
) -> CsResult<Self> {
if config.rank == 0 {
return Err(CsError::InvalidRank(config.rank));
}
if config.sparsity_fraction <= 0.0 || config.sparsity_fraction >= 1.0 {
return Err(CsError::InvalidParameter(
"sparsity_fraction must be in (0, 1)".into(),
));
}
if m_rows == 0 || n_cols == 0 {
return Err(CsError::InvalidParameter(
"m_rows and n_cols must be > 0".into(),
));
}
let r = config.rank;
let scale = 1.0 / (r as f64).sqrt();
let u: Vec<f64> = (0..m_rows * r).map(|_| rng.next_normal() * scale).collect();
let v: Vec<f64> = (0..n_cols * r).map(|_| rng.next_normal() * scale).collect();
let s = vec![0.0_f64; m_rows * n_cols];
Ok(Self {
u,
v,
s,
m_rows,
n_cols,
config,
iterations: 0,
converged: false,
})
}
pub fn fit(&mut self, mat: &[f64]) -> CsResult<()> {
let m = self.m_rows;
let n = self.n_cols;
let r = self.config.rank;
let lr = self.config.lr;
let max_iter = self.config.max_iter;
let tol = self.config.tol;
let k = ((self.config.sparsity_fraction * (m * n) as f64).ceil() as usize).max(1);
if mat.len() != m * n {
return Err(CsError::ShapeMismatch {
expected: vec![m, n],
got: vec![mat.len()],
});
}
let mut l_prev_norm = frobenius_norm(&self.u) * frobenius_norm(&self.v);
let mut adaptive_lr = lr;
for iter in 0..max_iter {
let l = mat_mul_bt(&self.u, &self.v, m, r, n);
let mut residual = vec![0.0_f64; m * n];
for idx in 0..(m * n) {
residual[idx] = mat[idx] - l[idx] - self.s[idx];
}
let rv = mat_mul_f32(&residual, &self.v, m, n, r);
let rtu = mat_mul_at(&residual, &self.u, m, n, r);
let grad_u_norm = frobenius_norm(&rv);
let grad_v_norm = frobenius_norm(&rtu);
let u_norm = frobenius_norm(&self.u).max(1.0e-12);
let v_norm = frobenius_norm(&self.v).max(1.0e-12);
let clip_u = if grad_u_norm * adaptive_lr > 0.5 * u_norm {
0.5 * u_norm / grad_u_norm.max(1.0e-300)
} else {
adaptive_lr
};
let clip_v = if grad_v_norm * adaptive_lr > 0.5 * v_norm {
0.5 * v_norm / grad_v_norm.max(1.0e-300)
} else {
adaptive_lr
};
for i in 0..(m * r) {
self.u[i] += clip_u * rv[i];
}
for i in 0..(n * r) {
self.v[i] += clip_v * rtu[i];
}
let u_ok = self.u.iter().all(|v| v.is_finite());
let v_ok = self.v.iter().all(|v| v.is_finite());
if !u_ok || !v_ok {
adaptive_lr *= 0.1;
let scale = 1.0e-3;
for val in self.u.iter_mut() {
*val = if val.is_finite() { *val * 0.0 } else { 0.0 };
}
for (i, val) in self.u.iter_mut().enumerate() {
*val = (((i * 7 + 13) % 17) as f64 - 8.0) * scale;
}
for (i, val) in self.v.iter_mut().enumerate() {
*val = (((i * 5 + 11) % 13) as f64 - 6.0) * scale;
}
continue;
}
let l_new = mat_mul_bt(&self.u, &self.v, m, r, n);
let diff: Vec<f64> = (0..(m * n)).map(|i| mat[i] - l_new[i]).collect();
self.s = hard_threshold_topk(&diff, k);
let l_norm = frobenius_norm(&l_new);
let delta = (l_norm - l_prev_norm).abs();
let rel = delta / l_prev_norm.max(1.0e-300);
l_prev_norm = l_norm;
self.iterations = iter + 1;
if rel < tol && iter > 0 {
self.converged = true;
break;
}
}
Ok(())
}
#[must_use]
pub fn low_rank(&self) -> Vec<f64> {
mat_mul_bt(&self.u, &self.v, self.m_rows, self.config.rank, self.n_cols)
}
#[must_use]
pub fn sparse(&self) -> &[f64] {
&self.s
}
#[must_use]
pub fn residual_norm(&self, mat: &[f64]) -> f64 {
let l = self.low_rank();
mat.iter()
.zip(l.iter())
.zip(self.s.iter())
.map(|((m_ij, l_ij), s_ij)| {
let r = m_ij - l_ij - s_ij;
r * r
})
.sum::<f64>()
.sqrt()
}
#[must_use]
pub fn is_converged(&self) -> bool {
self.converged
}
#[must_use]
pub fn iterations(&self) -> usize {
self.iterations
}
#[must_use]
pub fn rank(&self) -> usize {
self.config.rank
}
}
fn hard_threshold_topk(v: &[f64], k: usize) -> Vec<f64> {
if k == 0 {
return vec![0.0_f64; v.len()];
}
let k_clamped = k.min(v.len());
let mut pairs: Vec<(f64, usize)> = v.iter().enumerate().map(|(i, &x)| (x.abs(), i)).collect();
pairs.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
let mut out = vec![0.0_f64; v.len()];
for (_, i) in pairs.iter().take(k_clamped) {
out[*i] = v[*i];
}
out
}
fn frobenius_norm(v: &[f64]) -> f64 {
v.iter().map(|x| x * x).sum::<f64>().sqrt()
}
fn mat_mul_f32(a: &[f64], b: &[f64], m: usize, k: usize, n: usize) -> Vec<f64> {
let mut c = vec![0.0_f64; m * n];
for i in 0..m {
for p in 0..k {
let a_ip = a[i * k + p];
for j in 0..n {
c[i * n + j] += a_ip * b[p * n + j];
}
}
}
c
}
fn mat_mul_at(a: &[f64], b: &[f64], m: usize, n: usize, r: usize) -> Vec<f64> {
let mut c = vec![0.0_f64; n * r];
for i in 0..m {
for j in 0..n {
let a_ij = a[i * n + j];
for p in 0..r {
c[j * r + p] += a_ij * b[i * r + p];
}
}
}
c
}
#[allow(dead_code)]
fn mat_mul_bt(a: &[f64], b: &[f64], m: usize, r: usize, n: usize) -> Vec<f64> {
let mut c = vec![0.0_f64; m * n];
for i in 0..m {
for p in 0..r {
let a_ip = a[i * r + p];
for j in 0..n {
c[i * n + j] += a_ip * b[j * r + p];
}
}
}
c
}
#[cfg(test)]
mod tests {
use super::*;
use crate::handle::LcgRng;
fn make_low_rank_matrix(m: usize, n: usize) -> Vec<f64> {
(0..m * n)
.map(|k| {
let i = k / n;
let j = k % n;
(i + 1) as f64 * 0.1 * (j + 1) as f64 * 0.1
})
.collect()
}
fn default_config(rank: usize) -> RpcaGdConfig {
RpcaGdConfig {
rank,
sparsity_fraction: 0.1,
max_iter: 100,
lr: 0.01,
tol: 1e-5,
}
}
#[test]
fn low_rank_shape() {
let m = 8;
let n = 6;
let mut rng = LcgRng::new(1);
let cfg = default_config(2);
let mut rpca = RpcaGd::new(m, n, cfg, &mut rng).expect("ok");
let mat = make_low_rank_matrix(m, n);
rpca.fit(&mat).expect("ok");
let l = rpca.low_rank();
assert_eq!(l.len(), m * n);
}
#[test]
fn sparse_shape() {
let m = 8;
let n = 6;
let mut rng = LcgRng::new(2);
let cfg = default_config(2);
let mut rpca = RpcaGd::new(m, n, cfg, &mut rng).expect("ok");
let mat = make_low_rank_matrix(m, n);
rpca.fit(&mat).expect("ok");
assert_eq!(rpca.sparse().len(), m * n);
}
#[test]
fn residual_decreases() {
let m = 10;
let n = 8;
let mut mat = make_low_rank_matrix(m, n);
mat[0] += 2.0;
let cfg_few = RpcaGdConfig {
rank: 1,
sparsity_fraction: 0.05,
max_iter: 5,
lr: 0.01,
tol: 1e-8,
};
let cfg_many = RpcaGdConfig {
rank: 1,
sparsity_fraction: 0.05,
max_iter: 100,
lr: 0.01,
tol: 1e-8,
};
let mut rng1 = LcgRng::new(10);
let mut rpca_few = RpcaGd::new(m, n, cfg_few, &mut rng1).expect("ok");
rpca_few.fit(&mat).expect("ok");
let mut rng2 = LcgRng::new(10);
let mut rpca_many = RpcaGd::new(m, n, cfg_many, &mut rng2).expect("ok");
rpca_many.fit(&mat).expect("ok");
let resid_few = rpca_few.residual_norm(&mat);
let resid_many = rpca_many.residual_norm(&mat);
assert!(
resid_many <= resid_few + 1e-3,
"more iterations should not increase residual: few={resid_few:.4}, many={resid_many:.4}"
);
}
#[test]
fn low_rank_rank_bounded() {
let m = 8;
let n = 6;
let r = 2;
let mut rng = LcgRng::new(4);
let cfg = default_config(r);
let mut rpca = RpcaGd::new(m, n, cfg, &mut rng).expect("ok");
let mat = make_low_rank_matrix(m, n);
rpca.fit(&mat).expect("ok");
assert_eq!(rpca.rank(), r);
assert_eq!(rpca.u.len(), m * r);
assert_eq!(rpca.v.len(), n * r);
}
#[test]
fn sparse_sparse() {
let m = 10;
let n = 8;
let alpha = 0.05;
let cfg = RpcaGdConfig {
rank: 1,
sparsity_fraction: alpha,
max_iter: 50,
lr: 0.01,
tol: 1e-6,
};
let mut rng = LcgRng::new(5);
let mut rpca = RpcaGd::new(m, n, cfg, &mut rng).expect("ok");
let mat = make_low_rank_matrix(m, n);
rpca.fit(&mat).expect("ok");
let k = ((alpha * (m * n) as f64).ceil() as usize).max(1);
let nnz = rpca.sparse().iter().filter(|&&v| v.abs() > 1e-12).count();
assert!(nnz <= k, "sparse component has {nnz} nonzeros but k={k}");
}
#[test]
fn reconstruction_accurate() {
let m = 6;
let n = 5;
let mat = make_low_rank_matrix(m, n);
let cfg = RpcaGdConfig {
rank: 1,
sparsity_fraction: 0.01,
max_iter: 300,
lr: 0.005,
tol: 1e-7,
};
let mut rng = LcgRng::new(6);
let mut rpca = RpcaGd::new(m, n, cfg, &mut rng).expect("ok");
rpca.fit(&mat).expect("ok");
let resid = rpca.residual_norm(&mat);
assert!(resid.is_finite(), "residual must be finite");
}
#[test]
fn zero_rank_error() {
let mut rng = LcgRng::new(7);
let cfg = RpcaGdConfig {
rank: 0,
sparsity_fraction: 0.1,
max_iter: 10,
lr: 0.01,
tol: 1e-6,
};
let result = RpcaGd::new(4, 4, cfg, &mut rng);
assert!(result.is_err(), "rank=0 should return Err");
}
#[test]
fn bad_sparsity_fraction_error() {
let mut rng = LcgRng::new(8);
let cfg = RpcaGdConfig {
rank: 1,
sparsity_fraction: 1.5, max_iter: 10,
lr: 0.01,
tol: 1e-6,
};
let result = RpcaGd::new(4, 4, cfg, &mut rng);
assert!(result.is_err(), "sparsity_fraction > 1 should return Err");
}
#[test]
fn iteration_bounded() {
let m = 6;
let n = 5;
let max_iter = 20;
let cfg = RpcaGdConfig {
rank: 1,
sparsity_fraction: 0.1,
max_iter,
lr: 0.01,
tol: 0.0, };
let mut rng = LcgRng::new(9);
let mut rpca = RpcaGd::new(m, n, cfg, &mut rng).expect("ok");
let mat = make_low_rank_matrix(m, n);
rpca.fit(&mat).expect("ok");
assert!(
rpca.iterations() <= max_iter,
"iterations {} > max_iter {}",
rpca.iterations(),
max_iter
);
}
#[test]
fn low_rank_finite() {
let m = 8;
let n = 6;
let mut rng = LcgRng::new(10);
let cfg = default_config(2);
let mut rpca = RpcaGd::new(m, n, cfg, &mut rng).expect("ok");
let mat = make_low_rank_matrix(m, n);
rpca.fit(&mat).expect("ok");
let l = rpca.low_rank();
assert!(l.iter().all(|v| v.is_finite()), "L has non-finite value");
}
#[test]
fn sparse_finite() {
let m = 8;
let n = 6;
let mut rng = LcgRng::new(11);
let cfg = default_config(2);
let mut rpca = RpcaGd::new(m, n, cfg, &mut rng).expect("ok");
let mat = make_low_rank_matrix(m, n);
rpca.fit(&mat).expect("ok");
assert!(
rpca.sparse().iter().all(|v| v.is_finite()),
"S has non-finite value"
);
}
}