use crate::utils::gemm as dense;
use faer::linalg::solvers::DenseSolveCore;
use faer::{Accum, Mat, MatRef, Side};
use rand::prelude::*;
use rand_distr::Normal;
use rayon::prelude::*;
use crate::prelude::*;
use crate::utils::faer_parallelism;
const SCALE_EPS: f64 = 1e-12;
const PPCA_MIN_ITER: usize = 5;
const PPCA_REPORT_EVERY: usize = 25;
const BPCA_GALPHA0: f64 = 1e-10;
const BPCA_BALPHA0: f64 = 1.0;
const BPCA_GMU0: f64 = 0.001;
const BPCA_GTAU0: f64 = 1e-10;
const BPCA_BTAU0: f64 = 1.0;
const BPCA_TAU_MIN: f64 = 1e-10;
const BPCA_TAU_MAX: f64 = 1e10;
const BPCA_TAU_START: f64 = 1000.0;
const BPCA_CHECK_EVERY: usize = 10;
#[derive(Clone, Copy, Debug)]
pub struct PpcaParams {
pub n_pcs: usize,
pub max_iter: usize,
pub tol: f64,
pub seed: u64,
pub centre: bool,
pub scale: bool,
}
impl PpcaParams {
pub fn new(
n_pcs: usize,
max_iter: usize,
tol: f64,
seed: u64,
centre: bool,
scale: bool,
) -> Self {
Self {
n_pcs,
max_iter,
tol,
seed,
centre,
scale,
}
}
}
impl Default for PpcaParams {
fn default() -> Self {
Self {
n_pcs: 2,
max_iter: 1000,
tol: 1e-5,
seed: 42,
centre: true,
scale: false,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct BpcaParams {
pub n_pcs: usize,
pub max_iter: usize,
pub tol: f64,
pub centre: bool,
pub scale: bool,
}
impl BpcaParams {
pub fn new(n_pcs: usize, max_iter: usize, tol: f64, centre: bool, scale: bool) -> Self {
Self {
n_pcs,
max_iter,
tol,
centre,
scale,
}
}
}
impl Default for BpcaParams {
fn default() -> Self {
Self {
n_pcs: 2,
max_iter: 100,
tol: 1e-4,
centre: true,
scale: false,
}
}
}
#[derive(Clone, Debug)]
pub struct MissingPcaResults<T> {
pub scores: Mat<T>,
pub loadings: Mat<T>,
pub r2_cum: Vec<T>,
pub centre: Vec<T>,
pub scale: Vec<T>,
pub completed: Mat<T>,
pub noise_var: T,
pub n_iter: usize,
pub converged: bool,
}
struct PreppedData {
y: Vec<f64>,
missing: Vec<bool>,
n_miss_row: Vec<usize>,
n: usize,
d: usize,
n_missing: usize,
centre: Vec<f64>,
scale: Vec<f64>,
}
struct Fit {
scores: Mat<f64>,
loadings: Mat<f64>,
r2_cum: Vec<f64>,
noise_var: f64,
n_iter: usize,
converged: bool,
}
fn prep_data<T>(
y: MatRef<T>,
n_pcs: usize,
centre: bool,
scale: bool,
) -> Result<PreppedData, CoreErrors>
where
T: BixverseFloat,
{
let (n, d) = y.shape();
if n_pcs == 0 {
return Err(CoreErrors::MustBePositive("n_pcs".to_string()));
}
if n < 2 {
return Err(CoreErrors::InvalidArgument(
"PCA with missing values needs at least two rows".to_string(),
));
}
let max = n.min(d);
if n_pcs > max {
return Err(CoreErrors::PcaTooManyComponents { n_pcs, max });
}
let col_stats: Vec<(f64, f64, usize, bool)> = y
.par_col_iter()
.map(|col| {
let mut sum = 0.0;
let mut n_obs = 0usize;
let mut finite = true;
for v in col.iter() {
let v = v.to_f64().unwrap();
if v.is_nan() {
continue;
}
finite &= v.is_finite();
sum += v;
n_obs += 1;
}
let mean = sum / n_obs as f64;
let ss: f64 = col
.iter()
.map(|v| v.to_f64().unwrap())
.filter(|v| !v.is_nan())
.map(|v| (v - mean) * (v - mean))
.sum();
let sd = (ss / (n_obs as f64 - 1.0)).sqrt();
(mean, sd, n_obs, finite)
})
.collect();
if col_stats.iter().any(|s| !s.3) {
return Err(CoreErrors::InvalidArgument(
"PCA input contains infinite values".to_string(),
));
}
if let Some(j) = col_stats.iter().position(|s| s.2 == 0) {
return Err(CoreErrors::PcaAllMissing {
axis: "column",
index: j,
});
}
let centre: Vec<f64> = col_stats
.iter()
.map(|s| if centre { s.0 } else { 0.0 })
.collect();
let scale: Vec<f64> = col_stats
.iter()
.map(|s| {
if scale && s.2 > 1 && s.1 >= SCALE_EPS {
s.1
} else {
1.0
}
})
.collect();
let mut y_rm = vec![0.0; n * d];
let mut missing = vec![false; n * d];
let n_miss_row: Vec<usize> = y_rm
.par_chunks_mut(d)
.zip(missing.par_chunks_mut(d))
.enumerate()
.map(|(i, (row, m_row))| {
let mut nm = 0;
for j in 0..d {
let v = y[(i, j)].to_f64().unwrap();
if v.is_nan() {
m_row[j] = true;
nm += 1;
} else {
row[j] = (v - centre[j]) / scale[j];
}
}
nm
})
.collect();
if let Some(i) = n_miss_row.iter().position(|&nm| nm == d) {
return Err(CoreErrors::PcaAllMissing {
axis: "row",
index: i,
});
}
let n_missing = n_miss_row.iter().sum();
Ok(PreppedData {
y: y_rm,
missing,
n_miss_row,
n,
d,
n_missing,
centre,
scale,
})
}
fn gemm(a: MatRef<f64>, b: MatRef<f64>) -> Mat<f64> {
let mut out = Mat::zeros(a.nrows(), b.ncols());
dense::gemm(out.as_mut(), Accum::Replace, a, b, 1.0, faer_parallelism());
out
}
fn frob_dot(a: MatRef<f64>, b: MatRef<f64>) -> f64 {
a.col_iter()
.zip(b.col_iter())
.map(|(ca, cb)| ca.iter().zip(cb.iter()).map(|(x, y)| x * y).sum::<f64>())
.sum()
}
fn spd_inverse(a: MatRef<f64>) -> Result<(Mat<f64>, f64), CoreErrors> {
let llt = a.llt(Side::Lower)?;
let logdet = 2.0
* llt
.L()
.diagonal()
.column_vector()
.iter()
.map(|v| v.ln())
.sum::<f64>();
Ok((llt.inverse(), logdet))
}
fn r2_cum(
y: &[f64],
mask: Option<&[bool]>,
d: usize,
scores: MatRef<f64>,
loadings: MatRef<f64>,
) -> Vec<f64> {
let k = scores.ncols();
let w_rows = loadings.transpose().to_owned();
let rows: Vec<(f64, Vec<f64>)> = y
.par_chunks(d)
.enumerate()
.map(|(i, row)| {
let (mut tss, mut rss) = (0.0, vec![0.0; k]);
for (j, &v) in row.iter().enumerate() {
if mask.is_some_and(|m| m[i * d + j]) {
continue;
}
tss += v * v;
let w = w_rows.col(j);
let mut rec = 0.0;
for l in 0..k {
rec += scores[(i, l)] * w[l];
let r = v - rec;
rss[l] += r * r;
}
}
(tss, rss)
})
.collect();
let (mut tss, mut rss) = (0.0, vec![0.0; k]);
for (t, r) in rows {
tss += t;
rss.iter_mut().zip(r).for_each(|(a, b)| *a += b);
}
rss.iter().map(|r| 1.0 - r / tss).collect()
}
fn finish<T>(y: MatRef<T>, prep: &PreppedData, fit: Fit) -> MissingPcaResults<T>
where
T: BixverseFloat,
{
let (n, d) = (prep.n, prep.d);
let k = fit.scores.ncols();
let w_rows = fit.loadings.transpose().to_owned();
let to_t = |v: f64| T::from_f64(v).unwrap();
let mut completed_rm = vec![0.0; n * d];
completed_rm
.par_chunks_mut(d)
.enumerate()
.for_each(|(i, row)| {
for j in 0..d {
row[j] = if prep.missing[i * d + j] {
let w = w_rows.col(j);
let rec: f64 = (0..k).map(|l| fit.scores[(i, l)] * w[l]).sum();
rec * prep.scale[j] + prep.centre[j]
} else {
y[(i, j)].to_f64().unwrap()
};
}
});
MissingPcaResults {
scores: Mat::from_fn(n, k, |i, j| to_t(fit.scores[(i, j)])),
loadings: Mat::from_fn(d, k, |i, j| to_t(fit.loadings[(i, j)])),
r2_cum: fit.r2_cum.into_iter().map(to_t).collect(),
centre: prep.centre.iter().map(|&v| to_t(v)).collect(),
scale: prep.scale.iter().map(|&v| to_t(v)).collect(),
completed: Mat::from_fn(n, d, |i, j| to_t(completed_rm[i * d + j])),
noise_var: to_t(fit.noise_var),
n_iter: fit.n_iter,
converged: fit.converged,
}
}
fn fill_missing(y: &mut [f64], missing: &[bool], d: usize, x: MatRef<f64>, c: MatRef<f64>) -> f64 {
let k = x.ncols();
let c_rows = c.transpose().to_owned();
y.par_chunks_mut(d)
.zip(missing.par_chunks(d))
.enumerate()
.map(|(i, (row, m_row))| {
let mut sq = 0.0;
for j in 0..d {
if m_row[j] {
let cj = c_rows.col(j);
let v: f64 = (0..k).map(|l| x[(i, l)] * cj[l]).sum();
row[j] = v;
sq += v * v;
}
}
sq
})
.collect::<Vec<f64>>()
.iter()
.sum()
}
fn ppca_fit(
prep: &mut PreppedData,
mut c: Mat<f64>,
params: &PpcaParams,
verbosity: Verbosity,
) -> Result<Fit, CoreErrors> {
let (n, d, k) = (prep.n, prep.d, params.n_pcs);
let (nf, df, n_miss) = (n as f64, d as f64, prep.n_missing as f64);
let eye = Mat::<f64>::identity(k, k);
let y_obs_sq: f64 = prep.y.iter().map(|v| v * v).sum();
let mut ctc = c.transpose() * &c;
let yc = gemm(MatRef::from_row_major_slice(&prep.y, n, d), c.as_ref());
let mut x = &yc * spd_inverse(ctc.as_ref())?.0;
let xtx = x.transpose() * &x;
let resid_all =
y_obs_sq - 2.0 * frob_dot(yc.as_ref(), x.as_ref()) + frob_dot(xtx.as_ref(), ctc.as_ref());
let miss_sq = if prep.n_missing > 0 {
fill_missing(&mut prep.y, &prep.missing, d, x.as_ref(), c.as_ref())
} else {
0.0
};
let mut ss = (resid_all - miss_sq) / (nf * df - n_miss);
let mut y_sq = y_obs_sq + miss_sq;
let mut old = f64::INFINITY;
let mut n_iter = 0;
let mut converged = false;
loop {
let (sx, logdet_a) = spd_inverse((&eye + &ctc * (1.0 / ss)).as_ref())?;
let logdet_sx = -logdet_a;
let ss_old = ss;
if prep.n_missing > 0 {
y_sq = y_obs_sq + fill_missing(&mut prep.y, &prep.missing, d, x.as_ref(), c.as_ref());
}
let y_mat = MatRef::from_row_major_slice(&prep.y, n, d);
x = gemm(y_mat, c.as_ref()) * (&sx * (1.0 / ss));
let xtx = gemm(x.transpose(), x.as_ref());
let ytx = gemm(y_mat.transpose(), x.as_ref());
c = &ytx * spd_inverse((&xtx + &sx * nf).as_ref())?.0;
ctc = c.transpose() * &c;
let resid =
y_sq - 2.0 * frob_dot(ytx.as_ref(), c.as_ref()) + frob_dot(xtx.as_ref(), ctc.as_ref());
let ctc_sx = &ctc * &sx;
let ctc_sx_sum: f64 = ctc_sx.col_iter().map(|col| col.iter().sum::<f64>()).sum();
ss = (resid + nf * ctc_sx_sum + n_miss * ss_old) / (nf * df);
let tr_sx: f64 = sx.diagonal().column_vector().iter().sum();
let tr_xtx: f64 = xtx.diagonal().column_vector().iter().sum();
let objective = nf * (df * ss.ln() + tr_sx - logdet_sx) + tr_xtx - n_miss * ss_old.ln();
let rel_ch = (1.0 - objective / old).abs();
old = objective;
n_iter += 1;
if verbosity.detailed_verbosity()
|| (verbosity.normal_verbosity() && n_iter % PPCA_REPORT_EVERY == 0)
{
println!(
"PPCA: iteration {n_iter}, objective {objective:.6e}, relative change {rel_ch:.3e}"
);
}
if rel_ch < params.tol && n_iter + 1 > PPCA_MIN_ITER {
converged = true;
break;
}
if n_iter + 1 > params.max_iter {
break;
}
}
if verbosity.normal_verbosity() {
let status = if converged {
"converged"
} else {
"hit max_iter"
};
println!("PPCA: {status} after {n_iter} iterations, noise variance {ss:.4e}");
}
let y_mat = MatRef::from_row_major_slice(&prep.y, n, d);
let c_orth = c
.thin_svd()
.map_err(|e| CoreErrors::FaerSvdError(format!("{e:?}")))?
.U()
.to_owned();
let yc = gemm(y_mat, c_orth.as_ref());
let means: Vec<f64> = yc
.col_iter()
.map(|col| col.iter().sum::<f64>() / nf)
.collect();
let yc_c = Mat::from_fn(n, k, |i, j| yc[(i, j)] - means[j]);
let cov = (yc_c.transpose() * &yc_c) * (1.0 / (nf - 1.0));
let eig = cov
.self_adjoint_eigen(Side::Lower)
.map_err(|_| CoreErrors::FaerEigenError)?;
let u = eig.U();
let vecs = Mat::from_fn(k, k, |i, j| u[(i, k - 1 - j)]);
let loadings = &c_orth * &vecs;
let scores = gemm(y_mat, loadings.as_ref());
let r2 = r2_cum(&prep.y, None, d, scores.as_ref(), loadings.as_ref());
Ok(Fit {
scores,
loadings,
r2_cum: r2,
noise_var: ss,
n_iter,
converged,
})
}
pub fn ppca<T>(
y: MatRef<T>,
params: Option<PpcaParams>,
verbosity: Verbosity,
) -> Result<MissingPcaResults<T>, CoreErrors>
where
T: BixverseFloat,
{
let params = params.unwrap_or_default();
let mut rng = StdRng::seed_from_u64(params.seed);
let normal = Normal::new(0.0, 1.0).expect("unit normal is valid");
let c0 = Mat::<T>::from_fn(y.ncols(), params.n_pcs, |_, _| {
T::from_f64(normal.sample(&mut rng)).unwrap()
});
ppca_from_init(y, c0.as_ref(), Some(params), verbosity)
}
pub fn ppca_from_init<T>(
y: MatRef<T>,
c0: MatRef<T>,
params: Option<PpcaParams>,
verbosity: Verbosity,
) -> Result<MissingPcaResults<T>, CoreErrors>
where
T: BixverseFloat,
{
let params = params.unwrap_or_default();
let mut prep = prep_data(y, params.n_pcs, params.centre, params.scale)?;
if c0.shape() != (prep.d, params.n_pcs) {
return Err(CoreErrors::InvalidArgument(format!(
"initial loadings are {} x {}, expected {} x {}",
c0.nrows(),
c0.ncols(),
prep.d,
params.n_pcs
)));
}
let c = Mat::from_fn(prep.d, params.n_pcs, |i, j| c0[(i, j)].to_f64().unwrap());
let fit = ppca_fit(&mut prep, c, ¶ms, verbosity)?;
Ok(finish(y, &prep, fit))
}
struct BpcaState {
pa: Mat<f64>,
tau: f64,
alpha: Vec<f64>,
sig_w: Mat<f64>,
}
fn bpca_init(prep: &PreppedData, k: usize) -> Result<BpcaState, CoreErrors> {
let (n, d) = (prep.n, prep.d);
let nf = n as f64;
let y = MatRef::from_row_major_slice(&prep.y, n, d);
let zero_means: Vec<f64> = y
.par_col_iter()
.map(|col| col.iter().sum::<f64>() / nf)
.collect();
let yc = Mat::from_fn(n, d, |i, j| y[(i, j)] - zero_means[j]);
let tr_cov = frob_dot(yc.as_ref(), yc.as_ref()) / (nf - 1.0);
let svd = yc
.thin_svd()
.map_err(|e| CoreErrors::FaerSvdError(format!("{e:?}")))?;
let s = svd.S().column_vector();
let v = svd.V();
let eig: Vec<f64> = (0..k).map(|l| s[l] * s[l] / (nf - 1.0)).collect();
let pa = Mat::from_fn(d, k, |j, l| v[(j, l)] * eig[l].sqrt());
let tau = (1.0 / (tr_cov - eig.iter().sum::<f64>())).clamp(BPCA_TAU_MIN, BPCA_TAU_MAX);
let alpha = pa
.col_iter()
.map(|col| {
let sq: f64 = col.iter().map(|v| v * v).sum();
(2.0 * BPCA_GALPHA0 + d as f64) / (tau * sq + 2.0 * BPCA_GALPHA0 / BPCA_BALPHA0)
})
.collect();
Ok(BpcaState {
pa,
tau,
alpha,
sig_w: Mat::identity(k, k),
})
}
fn bpca_step(
state: &mut BpcaState,
dy: &mut [f64],
prep: &PreppedData,
mean_sq: f64,
) -> Result<Mat<f64>, CoreErrors> {
let (n, d) = (prep.n, prep.d);
let k = state.pa.ncols();
let (nf, df) = (n as f64, d as f64);
let tau = state.tau;
let eye = Mat::<f64>::identity(k, k);
let ptp = state.pa.transpose() * &state.pa;
let rx = &eye + &ptp * tau + &state.sig_w;
let (rx_inv, _) = spd_inverse(rx.as_ref())?;
let base = &eye + &state.sig_w;
let pa_rows = state.pa.transpose().to_owned();
let rows: Vec<(Vec<f64>, Option<Mat<f64>>, f64)> = dy
.par_chunks_mut(d)
.zip(prep.missing.par_chunks(d))
.zip(prep.n_miss_row.par_iter())
.map(|((row, m_row), &nm)| {
let mut ex = vec![0.0; k];
for j in 0..d {
if !m_row[j] {
let w = pa_rows.col(j);
for l in 0..k {
ex[l] += w[l] * row[j];
}
}
}
if nm == 0 {
let x: Vec<f64> = (0..k)
.map(|a| tau * (0..k).map(|b| rx_inv[(a, b)] * ex[b]).sum::<f64>())
.collect();
let dd: f64 = row.iter().map(|v| v * v).sum();
return Ok((x, None, dd));
}
let use_missing = nm < d - nm;
let mut g = if use_missing {
rx.clone()
} else {
base.clone()
};
let sign = if use_missing { -tau } else { tau };
for j in 0..d {
if m_row[j] == use_missing {
let w = pa_rows.col(j);
for a in 0..k {
for b in 0..k {
g[(a, b)] += sign * w[a] * w[b];
}
}
}
}
let (g_inv, _) = spd_inverse(g.as_ref())?;
let x: Vec<f64> = (0..k)
.map(|a| tau * (0..k).map(|b| g_inv[(a, b)] * ex[b]).sum::<f64>())
.collect();
let mut quad = 0.0;
for j in 0..d {
if m_row[j] {
let w = pa_rows.col(j);
row[j] = (0..k).map(|l| w[l] * x[l]).sum();
for a in 0..k {
for b in 0..k {
quad += w[a] * g_inv[(a, b)] * w[b];
}
}
}
}
let dd: f64 = row.iter().map(|v| v * v).sum();
Ok((x, Some(g_inv), dd + nm as f64 / tau + quad))
})
.collect::<Result<_, CoreErrors>>()?;
let scores = Mat::from_fn(n, k, |i, l| rows[i].0[l]);
let tr_s = rows.iter().map(|r| r.2).sum::<f64>() / nf;
let mut t = gemm(
MatRef::from_row_major_slice(dy, n, d).transpose(),
scores.as_ref(),
);
let incomplete: Vec<usize> = (0..n).filter(|&i| prep.n_miss_row[i] > 0).collect();
if !incomplete.is_empty() {
let mut corr = vec![0.0; d * k];
corr.par_chunks_mut(k).enumerate().for_each(|(j, out)| {
let w = pa_rows.col(j);
for &i in &incomplete {
if prep.missing[i * d + j] {
let g_inv = rows[i].1.as_ref().expect("incomplete rows carry Rxinv");
for b in 0..k {
out[b] += (0..k).map(|a| w[a] * g_inv[(a, b)]).sum::<f64>();
}
}
}
});
for j in 0..d {
for b in 0..k {
t[(j, b)] += corr[j * k + b];
}
}
}
t *= faer::Scale(1.0 / nf);
let alpha_diag = Mat::from_fn(k, k, |a, b| if a == b { state.alpha[a] / nf } else { 0.0 });
let dw = &rx_inv + (t.transpose() * &state.pa * &rx_inv) * tau + alpha_diag;
let dw_inv = dw.partial_piv_lu().inverse();
state.pa = &t * &dw_inv;
let tr_tpa = frob_dot(t.as_ref(), state.pa.as_ref());
state.tau = (df + 2.0 * BPCA_GTAU0 / nf)
/ (tr_s - tr_tpa + (mean_sq * BPCA_GMU0 + 2.0 * BPCA_GTAU0 / BPCA_BTAU0) / nf);
state.sig_w = dw_inv * (df / nf);
state.alpha = state
.pa
.col_iter()
.enumerate()
.map(|(l, col)| {
let sq: f64 = col.iter().map(|v| v * v).sum();
(2.0 * BPCA_GALPHA0 + df)
/ (state.tau * sq + state.sig_w[(l, l)] + 2.0 * BPCA_GALPHA0 / BPCA_BALPHA0)
})
.collect();
Ok(scores)
}
fn bpca_fit(
prep: &PreppedData,
params: &BpcaParams,
verbosity: Verbosity,
) -> Result<Fit, CoreErrors> {
let (n, d, k) = (prep.n, prep.d, params.n_pcs);
let mut state = bpca_init(prep, k)?;
let obs_mean: Vec<f64> = (0..d)
.into_par_iter()
.map(|j| {
let (s, c) = (0..n)
.filter(|&i| !prep.missing[i * d + j])
.fold((0.0, 0usize), |(s, c), i| (s + prep.y[i * d + j], c + 1));
s / c as f64
})
.collect();
let mean_sq: f64 = obs_mean.iter().map(|v| v * v).sum();
let mut dy = prep.y.clone();
dy.par_chunks_mut(d).for_each(|row| {
row.iter_mut().zip(&obs_mean).for_each(|(v, m)| *v -= m);
});
let mut tau_old = BPCA_TAU_START;
let mut scores = Mat::zeros(n, k);
let mut n_iter = 0;
let mut converged = false;
for step in 1..=params.max_iter {
scores = bpca_step(&mut state, &mut dy, prep, mean_sq)?;
n_iter = step;
let check = step % BPCA_CHECK_EVERY == 0;
if verbosity.detailed_verbosity() && !check {
println!("BPCA: step {step}, tau {:.6e}", state.tau);
}
if check {
let dtau = (state.tau.log10() - tau_old.log10()).abs();
if verbosity.normal_verbosity() {
println!(
"BPCA: step {step}, tau {:.6e}, change in log10(tau) {dtau:.3e}",
state.tau
);
}
if dtau < params.tol {
converged = true;
break;
}
tau_old = state.tau;
}
}
if verbosity.normal_verbosity() {
let status = if converged {
"converged"
} else {
"hit max_iter"
};
println!(
"BPCA: {status} after {n_iter} steps, noise variance {:.4e}",
1.0 / state.tau
);
}
let r2 = r2_cum(
&prep.y,
Some(&prep.missing),
d,
scores.as_ref(),
state.pa.as_ref(),
);
Ok(Fit {
scores,
loadings: state.pa,
r2_cum: r2,
noise_var: 1.0 / state.tau,
n_iter,
converged,
})
}
pub fn bpca<T>(
y: MatRef<T>,
params: Option<BpcaParams>,
verbosity: Verbosity,
) -> Result<MissingPcaResults<T>, CoreErrors>
where
T: BixverseFloat,
{
let params = params.unwrap_or_default();
let prep = prep_data(y, params.n_pcs, params.centre, params.scale)?;
let fit = bpca_fit(&prep, ¶ms, verbosity)?;
Ok(finish(y, &prep, fit))
}
#[cfg(test)]
mod tests {
use super::*;
fn low_rank(n: usize, d: usize, k: usize, noise: f64, miss: f64) -> (Mat<f64>, Mat<f64>) {
let mut rng = StdRng::seed_from_u64(1);
let normal = Normal::new(0.0, 1.0).unwrap();
let z = Mat::<f64>::from_fn(n, k, |_, _| normal.sample(&mut rng));
let w = Mat::<f64>::from_fn(d, k, |_, _| normal.sample(&mut rng));
let signal = &z * w.transpose();
let full = Mat::from_fn(n, d, |i, j| {
signal[(i, j)] + noise * normal.sample(&mut rng)
});
let masked = Mat::from_fn(n, d, |i, j| {
if rng.random::<f64>() < miss {
f64::NAN
} else {
full[(i, j)]
}
});
(masked, full)
}
fn subspace_gap(a: MatRef<f64>, b: MatRef<f64>) -> f64 {
let qa = a.qr().compute_thin_Q();
let qb = b.qr().compute_thin_Q();
let diff = &qa * qa.transpose() - &qb * qb.transpose();
frob_dot(diff.as_ref(), diff.as_ref()).sqrt()
}
fn svd_basis(y: MatRef<f64>, k: usize) -> Mat<f64> {
let (n, d) = y.shape();
let means: Vec<f64> = (0..d)
.map(|j| (0..n).map(|i| y[(i, j)]).sum::<f64>() / n as f64)
.collect();
let yc = Mat::from_fn(n, d, |i, j| y[(i, j)] - means[j]);
let svd = yc.thin_svd().unwrap();
svd.V().subcols(0, k).to_owned()
}
#[test]
fn test_complete_data_matches_svd_subspace() {
let (_, full) = low_rank(80, 15, 3, 0.1, 0.0);
let basis = svd_basis(full.as_ref(), 3);
let params = PpcaParams {
n_pcs: 3,
..PpcaParams::default()
};
let pp = ppca(full.as_ref(), Some(params), Verbosity::Quiet).unwrap();
assert!(pp.converged);
assert!(subspace_gap(pp.loadings.as_ref(), basis.as_ref()) < 1e-4);
let params = BpcaParams {
n_pcs: 3,
..BpcaParams::default()
};
let bp = bpca(full.as_ref(), Some(params), Verbosity::Quiet).unwrap();
assert!(subspace_gap(bp.loadings.as_ref(), basis.as_ref()) < 1e-4);
}
#[test]
fn test_ppca_seed_reproducible_and_observed_untouched() {
let (masked, _) = low_rank(50, 10, 2, 0.2, 0.2);
let a = ppca(masked.as_ref(), None, Verbosity::Quiet).unwrap();
let b = ppca(masked.as_ref(), None, Verbosity::Quiet).unwrap();
assert_eq!(a.n_iter, b.n_iter);
for i in 0..50 {
for j in 0..10 {
assert_eq!(a.completed[(i, j)], b.completed[(i, j)]);
if !masked[(i, j)].is_nan() {
assert_eq!(a.completed[(i, j)], masked[(i, j)]);
}
}
}
}
#[test]
fn test_imputation_recovers_low_rank_signal() {
let (masked, full) = low_rank(120, 30, 3, 0.1, 0.3);
let rmse = |c: &Mat<f64>| {
let (mut s, mut m) = (0.0, 0usize);
for i in 0..120 {
for j in 0..30 {
if masked[(i, j)].is_nan() {
s += (c[(i, j)] - full[(i, j)]).powi(2);
m += 1;
}
}
}
(s / m as f64).sqrt()
};
for scale in [false, true] {
let pp = ppca(
masked.as_ref(),
Some(PpcaParams {
n_pcs: 3,
scale,
..PpcaParams::default()
}),
Verbosity::Quiet,
)
.unwrap();
let bp = bpca(
masked.as_ref(),
Some(BpcaParams {
n_pcs: 3,
scale,
..BpcaParams::default()
}),
Verbosity::Quiet,
)
.unwrap();
assert!(
rmse(&pp.completed) < 0.3,
"ppca scale={scale}: {}",
rmse(&pp.completed)
);
assert!(
rmse(&bp.completed) < 0.3,
"bpca scale={scale}: {}",
rmse(&bp.completed)
);
assert!(pp.r2_cum.windows(2).all(|w| w[0] <= w[1]));
}
}
#[test]
fn test_f32_input_matches_f64() {
let (masked, _) = low_rank(40, 8, 2, 0.2, 0.2);
let masked32 = Mat::<f32>::from_fn(40, 8, |i, j| masked[(i, j)] as f32);
let a = bpca(masked.as_ref(), None, Verbosity::Quiet).unwrap();
let b = bpca(masked32.as_ref(), None, Verbosity::Quiet).unwrap();
for i in 0..40 {
for j in 0..8 {
assert!((a.completed[(i, j)] - b.completed[(i, j)] as f64).abs() < 1e-3);
}
}
}
#[test]
fn test_invalid_inputs_error() {
let (mut masked, _) = low_rank(20, 6, 2, 0.1, 0.0);
let too_many = PpcaParams {
n_pcs: 7,
..PpcaParams::default()
};
assert!(matches!(
ppca(masked.as_ref(), Some(too_many), Verbosity::Quiet),
Err(CoreErrors::PcaTooManyComponents { n_pcs: 7, max: 6 })
));
for i in 0..20 {
masked[(i, 4)] = f64::NAN;
}
assert!(matches!(
bpca(masked.as_ref(), None, Verbosity::Quiet),
Err(CoreErrors::PcaAllMissing {
axis: "column",
index: 4
})
));
let (mut masked, _) = low_rank(20, 6, 2, 0.1, 0.0);
for j in 0..6 {
masked[(3, j)] = f64::NAN;
}
assert!(matches!(
ppca(masked.as_ref(), None, Verbosity::Quiet),
Err(CoreErrors::PcaAllMissing {
axis: "row",
index: 3
})
));
let c0 = Mat::<f64>::zeros(5, 2);
let (full, _) = low_rank(20, 6, 2, 0.1, 0.0);
assert!(matches!(
ppca_from_init(full.as_ref(), c0.as_ref(), None, Verbosity::Quiet),
Err(CoreErrors::InvalidArgument(_))
));
}
}