use crate::utils::gemm::gemm;
use faer::{Accum, Mat, MatRef};
use super::NmfInput;
use crate::core::math::pca_svd::{SvdResults, randomised_svd};
use crate::prelude::*;
use crate::utils::faer_parallelism;
#[derive(Debug)]
pub struct DenseInput<'a, F: BixverseFloat> {
v: MatRef<'a, F>,
sq_frob: F,
}
impl<'a, F: BixverseFloat> DenseInput<'a, F> {
pub fn new(v: MatRef<'a, F>) -> Result<Self, BixverseErrors> {
let (m, n) = (v.nrows(), v.ncols());
let mut sq_frob = 0f64;
for j in 0..n {
for i in 0..m {
let x = v[(i, j)];
if !x.is_finite() {
return Err(BixverseErrors::NmfNonFinite);
}
if x < F::zero() {
return Err(BixverseErrors::NmfNonNegativeViolated);
}
let d = x.to_f64().unwrap();
sq_frob += d * d;
}
}
Ok(Self {
v,
sq_frob: F::from_f64(sq_frob).unwrap(),
})
}
}
impl<F: BixverseFloat> NmfInput<F> for DenseInput<'_, F> {
fn shape(&self) -> (usize, usize) {
(self.v.nrows(), self.v.ncols())
}
fn sq_frob(&self) -> F {
self.sq_frob
}
fn wt_v(&self, w: MatRef<F>, out: &mut Mat<F>) {
gemm(
out.as_mut(),
Accum::Replace,
w.transpose(),
self.v,
F::one(),
faer_parallelism(),
);
}
fn v_ht(&self, h: MatRef<F>, out: &mut Mat<F>) {
gemm(
out.as_mut(),
Accum::Replace,
self.v,
h.transpose(),
F::one(),
faer_parallelism(),
);
}
fn top_k_svd(&self, k: usize) -> Result<SvdResults<F>, BixverseErrors> {
let (n, m) = (self.v.nrows(), self.v.ncols());
let min_dim = m.min(n);
if min_dim < k {
return Err(BixverseErrors::NmfRankTooLarge {
requested: k,
available: min_dim,
});
}
let use_randomised = k * 4 < min_dim && min_dim > 64;
if use_randomised {
let rsvd = randomised_svd(self.v, k, 42, None, None)?;
let u = Mat::<F>::from_fn(rsvd.u.nrows(), k, |i, j| rsvd.u[(i, j)]);
let v = Mat::<F>::from_fn(rsvd.v.nrows(), k, |i, j| rsvd.v[(i, j)]);
let s: Vec<F> = rsvd.s.into_iter().take(k).collect();
Ok(SvdResults { u, v, s })
} else {
let svd = self
.v
.thin_svd()
.map_err(|e| BixverseErrors::FaerSvdError(format!("{e:?}")))?;
let u_full = svd.U();
let v_full = svd.V();
let s_full = svd.S().column_vector();
let u = Mat::<F>::from_fn(u_full.nrows(), k, |i, j| u_full[(i, j)]);
let v = Mat::<F>::from_fn(v_full.nrows(), k, |i, j| v_full[(i, j)]);
let s: Vec<F> = (0..k).map(|i| s_full[i]).collect();
Ok(SvdResults { u, v, s })
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use faer::Mat;
#[test]
fn sq_frob_survives_a_swamped_accumulator() {
let (m, n) = (1_000usize, 1_000usize);
let v = Mat::<f32>::from_fn(m, n, |i, j| if i == 0 && j == 0 { 1e4 } else { 1.0 });
let input = DenseInput::new(v.as_ref()).unwrap();
let exact = 1e8 + (m * n - 1) as f64;
let got = input.sq_frob() as f64;
assert!(
(got - exact).abs() <= 1e-5 * exact,
"sq_frob is off: got {got}, exact {exact}"
);
let mut naive = 0f32;
for j in 0..n {
for i in 0..m {
let x = v[(i, j)];
naive += x * x;
}
}
assert!(
(naive as f64 - exact).abs() > 0.005 * exact,
"the f32 reference was supposed to be visibly wrong, so this test proves nothing"
);
}
#[test]
fn shape_and_sq_frob() {
let v: Mat<f64> = Mat::from_fn(3, 2, |i, j| (i + j) as f64);
let input = DenseInput::new(v.as_ref()).unwrap();
assert_eq!(input.shape(), (3, 2));
assert!((input.sq_frob() - 19.0).abs() < 1e-12);
}
#[test]
fn rejects_negative() {
let v: Mat<f64> = Mat::from_fn(2, 2, |i, _| if i == 0 { 1.0 } else { -1.0 });
let err = DenseInput::new(v.as_ref()).unwrap_err();
assert!(matches!(err, BixverseErrors::NmfNonNegativeViolated));
}
#[test]
fn rejects_non_finite() {
let v: Mat<f64> = Mat::from_fn(2, 2, |i, _| if i == 0 { 1.0 } else { f64::NAN });
let err = DenseInput::new(v.as_ref()).unwrap_err();
assert!(matches!(err, BixverseErrors::NmfNonFinite));
let v: Mat<f64> = Mat::from_fn(2, 2, |i, _| if i == 0 { 1.0 } else { f64::INFINITY });
let err = DenseInput::new(v.as_ref()).unwrap_err();
assert!(matches!(err, BixverseErrors::NmfNonFinite));
}
#[test]
fn wt_v_matches_reference() {
let (m, n, k) = (4, 3, 2);
let v_mat: Mat<f64> = Mat::from_fn(m, n, |i, j| (i * n + j + 1) as f64);
let w: Mat<f64> = Mat::from_fn(m, k, |i, j| (i + j + 1) as f64);
let input = DenseInput::new(v_mat.as_ref()).unwrap();
let mut out = Mat::<f64>::zeros(k, n);
input.wt_v(w.as_ref(), &mut out);
for r in 0..k {
for j in 0..n {
let expected: f64 = (0..m).map(|i| w[(i, r)] * v_mat[(i, j)]).sum();
assert!((out[(r, j)] - expected).abs() < 1e-9);
}
}
}
#[test]
fn v_ht_matches_reference() {
let (m, n, k) = (4, 3, 2);
let v_mat: Mat<f64> = Mat::from_fn(m, n, |i, j| (i * n + j + 1) as f64);
let h: Mat<f64> = Mat::from_fn(k, n, |i, j| (i + j + 1) as f64);
let input = DenseInput::new(v_mat.as_ref()).unwrap();
let mut out = Mat::<f64>::zeros(m, k);
input.v_ht(h.as_ref(), &mut out);
for i in 0..m {
for r in 0..k {
let expected: f64 = (0..n).map(|j| v_mat[(i, j)] * h[(r, j)]).sum();
assert!((out[(i, r)] - expected).abs() < 1e-9);
}
}
}
#[test]
fn top_k_svd_shapes_and_ordering() {
let v_mat: Mat<f64> = Mat::from_fn(5, 4, |i, j| (i * 4 + j + 1) as f64);
let input = DenseInput::new(v_mat.as_ref()).unwrap();
let svd = input.top_k_svd(2).unwrap();
assert_eq!(svd.u.shape(), (5, 2));
assert_eq!(svd.v.shape(), (4, 2));
assert_eq!(svd.s.len(), 2);
assert!(svd.s[0] >= svd.s[1]);
assert!(svd.s[1] >= 0.0);
}
#[test]
fn top_k_svd_rejects_too_large_k() {
let v_mat: Mat<f64> = Mat::from_fn(3, 2, |i, j| (i + j + 1) as f64);
let input = DenseInput::new(v_mat.as_ref()).unwrap();
let err = input.top_k_svd(3).unwrap_err();
assert!(matches!(err, BixverseErrors::NmfRankTooLarge { .. }));
}
}