1use crate::utils::gemm as dense;
12use faer::linalg::solvers::DenseSolveCore;
13use faer::{Accum, Mat, MatRef, Side};
14use rand::prelude::*;
15use rand_distr::Normal;
16use rayon::prelude::*;
17
18use crate::prelude::*;
19use crate::utils::faer_parallelism;
20
21const SCALE_EPS: f64 = 1e-12;
28
29const PPCA_MIN_ITER: usize = 5;
32
33const PPCA_REPORT_EVERY: usize = 25;
36
37const BPCA_GALPHA0: f64 = 1e-10;
39
40const BPCA_BALPHA0: f64 = 1.0;
42
43const BPCA_GMU0: f64 = 0.001;
45
46const BPCA_GTAU0: f64 = 1e-10;
48
49const BPCA_BTAU0: f64 = 1.0;
51
52const BPCA_TAU_MIN: f64 = 1e-10;
54
55const BPCA_TAU_MAX: f64 = 1e10;
57
58const BPCA_TAU_START: f64 = 1000.0;
60
61const BPCA_CHECK_EVERY: usize = 10;
64
65#[derive(Clone, Copy, Debug)]
71pub struct PpcaParams {
72 pub n_pcs: usize,
74 pub max_iter: usize,
76 pub tol: f64,
78 pub seed: u64,
80 pub centre: bool,
82 pub scale: bool,
84}
85
86impl PpcaParams {
87 pub fn new(
102 n_pcs: usize,
103 max_iter: usize,
104 tol: f64,
105 seed: u64,
106 centre: bool,
107 scale: bool,
108 ) -> Self {
109 Self {
110 n_pcs,
111 max_iter,
112 tol,
113 seed,
114 centre,
115 scale,
116 }
117 }
118}
119
120impl Default for PpcaParams {
121 fn default() -> Self {
123 Self {
124 n_pcs: 2,
125 max_iter: 1000,
126 tol: 1e-5,
127 seed: 42,
128 centre: true,
129 scale: false,
130 }
131 }
132}
133
134#[derive(Clone, Copy, Debug)]
136pub struct BpcaParams {
137 pub n_pcs: usize,
139 pub max_iter: usize,
141 pub tol: f64,
143 pub centre: bool,
145 pub scale: bool,
147}
148
149impl BpcaParams {
150 pub fn new(n_pcs: usize, max_iter: usize, tol: f64, centre: bool, scale: bool) -> Self {
165 Self {
166 n_pcs,
167 max_iter,
168 tol,
169 centre,
170 scale,
171 }
172 }
173}
174
175impl Default for BpcaParams {
176 fn default() -> Self {
178 Self {
179 n_pcs: 2,
180 max_iter: 100,
181 tol: 1e-4,
182 centre: true,
183 scale: false,
184 }
185 }
186}
187
188#[derive(Clone, Debug)]
194pub struct MissingPcaResults<T> {
195 pub scores: Mat<T>,
197 pub loadings: Mat<T>,
199 pub r2_cum: Vec<T>,
202 pub centre: Vec<T>,
204 pub scale: Vec<T>,
206 pub completed: Mat<T>,
209 pub noise_var: T,
212 pub n_iter: usize,
214 pub converged: bool,
216}
217
218struct PreppedData {
220 y: Vec<f64>,
222 missing: Vec<bool>,
224 n_miss_row: Vec<usize>,
226 n: usize,
228 d: usize,
230 n_missing: usize,
232 centre: Vec<f64>,
234 scale: Vec<f64>,
236}
237
238struct Fit {
240 scores: Mat<f64>,
242 loadings: Mat<f64>,
244 r2_cum: Vec<f64>,
246 noise_var: f64,
248 n_iter: usize,
250 converged: bool,
252}
253
254fn prep_data<T>(
276 y: MatRef<T>,
277 n_pcs: usize,
278 centre: bool,
279 scale: bool,
280) -> Result<PreppedData, CoreErrors>
281where
282 T: BixverseFloat,
283{
284 let (n, d) = y.shape();
285 if n_pcs == 0 {
286 return Err(CoreErrors::MustBePositive("n_pcs".to_string()));
287 }
288 if n < 2 {
289 return Err(CoreErrors::InvalidArgument(
290 "PCA with missing values needs at least two rows".to_string(),
291 ));
292 }
293 let max = n.min(d);
294 if n_pcs > max {
295 return Err(CoreErrors::PcaTooManyComponents { n_pcs, max });
296 }
297
298 let col_stats: Vec<(f64, f64, usize, bool)> = y
299 .par_col_iter()
300 .map(|col| {
301 let mut sum = 0.0;
302 let mut n_obs = 0usize;
303 let mut finite = true;
304 for v in col.iter() {
305 let v = v.to_f64().unwrap();
306 if v.is_nan() {
307 continue;
308 }
309 finite &= v.is_finite();
310 sum += v;
311 n_obs += 1;
312 }
313 let mean = sum / n_obs as f64;
314 let ss: f64 = col
315 .iter()
316 .map(|v| v.to_f64().unwrap())
317 .filter(|v| !v.is_nan())
318 .map(|v| (v - mean) * (v - mean))
319 .sum();
320 let sd = (ss / (n_obs as f64 - 1.0)).sqrt();
321 (mean, sd, n_obs, finite)
322 })
323 .collect();
324
325 if col_stats.iter().any(|s| !s.3) {
326 return Err(CoreErrors::InvalidArgument(
327 "PCA input contains infinite values".to_string(),
328 ));
329 }
330 if let Some(j) = col_stats.iter().position(|s| s.2 == 0) {
331 return Err(CoreErrors::PcaAllMissing {
332 axis: "column",
333 index: j,
334 });
335 }
336
337 let centre: Vec<f64> = col_stats
338 .iter()
339 .map(|s| if centre { s.0 } else { 0.0 })
340 .collect();
341 let scale: Vec<f64> = col_stats
342 .iter()
343 .map(|s| {
344 if scale && s.2 > 1 && s.1 >= SCALE_EPS {
345 s.1
346 } else {
347 1.0
348 }
349 })
350 .collect();
351
352 let mut y_rm = vec![0.0; n * d];
353 let mut missing = vec![false; n * d];
354 let n_miss_row: Vec<usize> = y_rm
355 .par_chunks_mut(d)
356 .zip(missing.par_chunks_mut(d))
357 .enumerate()
358 .map(|(i, (row, m_row))| {
359 let mut nm = 0;
360 for j in 0..d {
361 let v = y[(i, j)].to_f64().unwrap();
362 if v.is_nan() {
363 m_row[j] = true;
364 nm += 1;
365 } else {
366 row[j] = (v - centre[j]) / scale[j];
367 }
368 }
369 nm
370 })
371 .collect();
372
373 if let Some(i) = n_miss_row.iter().position(|&nm| nm == d) {
374 return Err(CoreErrors::PcaAllMissing {
375 axis: "row",
376 index: i,
377 });
378 }
379 let n_missing = n_miss_row.iter().sum();
380
381 Ok(PreppedData {
382 y: y_rm,
383 missing,
384 n_miss_row,
385 n,
386 d,
387 n_missing,
388 centre,
389 scale,
390 })
391}
392
393fn gemm(a: MatRef<f64>, b: MatRef<f64>) -> Mat<f64> {
404 let mut out = Mat::zeros(a.nrows(), b.ncols());
405 dense::gemm(out.as_mut(), Accum::Replace, a, b, 1.0, faer_parallelism());
406 out
407}
408
409fn frob_dot(a: MatRef<f64>, b: MatRef<f64>) -> f64 {
420 a.col_iter()
421 .zip(b.col_iter())
422 .map(|(ca, cb)| ca.iter().zip(cb.iter()).map(|(x, y)| x * y).sum::<f64>())
423 .sum()
424}
425
426fn spd_inverse(a: MatRef<f64>) -> Result<(Mat<f64>, f64), CoreErrors> {
437 let llt = a.llt(Side::Lower)?;
438 let logdet = 2.0
439 * llt
440 .L()
441 .diagonal()
442 .column_vector()
443 .iter()
444 .map(|v| v.ln())
445 .sum::<f64>();
446 Ok((llt.inverse(), logdet))
447}
448
449fn r2_cum(
464 y: &[f64],
465 mask: Option<&[bool]>,
466 d: usize,
467 scores: MatRef<f64>,
468 loadings: MatRef<f64>,
469) -> Vec<f64> {
470 let k = scores.ncols();
471 let w_rows = loadings.transpose().to_owned();
472
473 let rows: Vec<(f64, Vec<f64>)> = y
476 .par_chunks(d)
477 .enumerate()
478 .map(|(i, row)| {
479 let (mut tss, mut rss) = (0.0, vec![0.0; k]);
480 for (j, &v) in row.iter().enumerate() {
481 if mask.is_some_and(|m| m[i * d + j]) {
482 continue;
483 }
484 tss += v * v;
485 let w = w_rows.col(j);
486 let mut rec = 0.0;
487 for l in 0..k {
488 rec += scores[(i, l)] * w[l];
489 let r = v - rec;
490 rss[l] += r * r;
491 }
492 }
493 (tss, rss)
494 })
495 .collect();
496 let (mut tss, mut rss) = (0.0, vec![0.0; k]);
497 for (t, r) in rows {
498 tss += t;
499 rss.iter_mut().zip(r).for_each(|(a, b)| *a += b);
500 }
501
502 rss.iter().map(|r| 1.0 - r / tss).collect()
503}
504
505fn finish<T>(y: MatRef<T>, prep: &PreppedData, fit: Fit) -> MissingPcaResults<T>
517where
518 T: BixverseFloat,
519{
520 let (n, d) = (prep.n, prep.d);
521 let k = fit.scores.ncols();
522 let w_rows = fit.loadings.transpose().to_owned();
523 let to_t = |v: f64| T::from_f64(v).unwrap();
524
525 let mut completed_rm = vec![0.0; n * d];
526 completed_rm
527 .par_chunks_mut(d)
528 .enumerate()
529 .for_each(|(i, row)| {
530 for j in 0..d {
531 row[j] = if prep.missing[i * d + j] {
532 let w = w_rows.col(j);
533 let rec: f64 = (0..k).map(|l| fit.scores[(i, l)] * w[l]).sum();
534 rec * prep.scale[j] + prep.centre[j]
535 } else {
536 y[(i, j)].to_f64().unwrap()
537 };
538 }
539 });
540
541 MissingPcaResults {
542 scores: Mat::from_fn(n, k, |i, j| to_t(fit.scores[(i, j)])),
543 loadings: Mat::from_fn(d, k, |i, j| to_t(fit.loadings[(i, j)])),
544 r2_cum: fit.r2_cum.into_iter().map(to_t).collect(),
545 centre: prep.centre.iter().map(|&v| to_t(v)).collect(),
546 scale: prep.scale.iter().map(|&v| to_t(v)).collect(),
547 completed: Mat::from_fn(n, d, |i, j| to_t(completed_rm[i * d + j])),
548 noise_var: to_t(fit.noise_var),
549 n_iter: fit.n_iter,
550 converged: fit.converged,
551 }
552}
553
554fn fill_missing(y: &mut [f64], missing: &[bool], d: usize, x: MatRef<f64>, c: MatRef<f64>) -> f64 {
572 let k = x.ncols();
573 let c_rows = c.transpose().to_owned();
574 y.par_chunks_mut(d)
575 .zip(missing.par_chunks(d))
576 .enumerate()
577 .map(|(i, (row, m_row))| {
578 let mut sq = 0.0;
579 for j in 0..d {
580 if m_row[j] {
581 let cj = c_rows.col(j);
582 let v: f64 = (0..k).map(|l| x[(i, l)] * cj[l]).sum();
583 row[j] = v;
584 sq += v * v;
585 }
586 }
587 sq
588 })
589 .collect::<Vec<f64>>()
590 .iter()
591 .sum()
592}
593
594fn ppca_fit(
617 prep: &mut PreppedData,
618 mut c: Mat<f64>,
619 params: &PpcaParams,
620 verbosity: Verbosity,
621) -> Result<Fit, CoreErrors> {
622 let (n, d, k) = (prep.n, prep.d, params.n_pcs);
623 let (nf, df, n_miss) = (n as f64, d as f64, prep.n_missing as f64);
624 let eye = Mat::<f64>::identity(k, k);
625
626 let y_obs_sq: f64 = prep.y.iter().map(|v| v * v).sum();
627
628 let mut ctc = c.transpose() * &c;
634 let yc = gemm(MatRef::from_row_major_slice(&prep.y, n, d), c.as_ref());
635 let mut x = &yc * spd_inverse(ctc.as_ref())?.0;
636 let xtx = x.transpose() * &x;
637 let resid_all =
638 y_obs_sq - 2.0 * frob_dot(yc.as_ref(), x.as_ref()) + frob_dot(xtx.as_ref(), ctc.as_ref());
639 let miss_sq = if prep.n_missing > 0 {
640 fill_missing(&mut prep.y, &prep.missing, d, x.as_ref(), c.as_ref())
641 } else {
642 0.0
643 };
644 let mut ss = (resid_all - miss_sq) / (nf * df - n_miss);
645 let mut y_sq = y_obs_sq + miss_sq;
646
647 let mut old = f64::INFINITY;
648 let mut n_iter = 0;
649 let mut converged = false;
650
651 loop {
652 let (sx, logdet_a) = spd_inverse((&eye + &ctc * (1.0 / ss)).as_ref())?;
653 let logdet_sx = -logdet_a;
654 let ss_old = ss;
655
656 if prep.n_missing > 0 {
657 y_sq = y_obs_sq + fill_missing(&mut prep.y, &prep.missing, d, x.as_ref(), c.as_ref());
658 }
659 let y_mat = MatRef::from_row_major_slice(&prep.y, n, d);
660
661 x = gemm(y_mat, c.as_ref()) * (&sx * (1.0 / ss));
663
664 let xtx = gemm(x.transpose(), x.as_ref());
666 let ytx = gemm(y_mat.transpose(), x.as_ref());
667 c = &ytx * spd_inverse((&xtx + &sx * nf).as_ref())?.0;
668 ctc = c.transpose() * &c;
669
670 let resid =
671 y_sq - 2.0 * frob_dot(ytx.as_ref(), c.as_ref()) + frob_dot(xtx.as_ref(), ctc.as_ref());
672 let ctc_sx = &ctc * &sx;
676 let ctc_sx_sum: f64 = ctc_sx.col_iter().map(|col| col.iter().sum::<f64>()).sum();
677 ss = (resid + nf * ctc_sx_sum + n_miss * ss_old) / (nf * df);
678
679 let tr_sx: f64 = sx.diagonal().column_vector().iter().sum();
680 let tr_xtx: f64 = xtx.diagonal().column_vector().iter().sum();
681 let objective = nf * (df * ss.ln() + tr_sx - logdet_sx) + tr_xtx - n_miss * ss_old.ln();
682 let rel_ch = (1.0 - objective / old).abs();
683 old = objective;
684
685 n_iter += 1;
686 if verbosity.detailed_verbosity()
687 || (verbosity.normal_verbosity() && n_iter % PPCA_REPORT_EVERY == 0)
688 {
689 println!(
690 "PPCA: iteration {n_iter}, objective {objective:.6e}, relative change {rel_ch:.3e}"
691 );
692 }
693 if rel_ch < params.tol && n_iter + 1 > PPCA_MIN_ITER {
694 converged = true;
695 break;
696 }
697 if n_iter + 1 > params.max_iter {
698 break;
699 }
700 }
701
702 if verbosity.normal_verbosity() {
703 let status = if converged {
704 "converged"
705 } else {
706 "hit max_iter"
707 };
708 println!("PPCA: {status} after {n_iter} iterations, noise variance {ss:.4e}");
709 }
710
711 let y_mat = MatRef::from_row_major_slice(&prep.y, n, d);
713 let c_orth = c
714 .thin_svd()
715 .map_err(|e| CoreErrors::FaerSvdError(format!("{e:?}")))?
716 .U()
717 .to_owned();
718 let yc = gemm(y_mat, c_orth.as_ref());
719 let means: Vec<f64> = yc
720 .col_iter()
721 .map(|col| col.iter().sum::<f64>() / nf)
722 .collect();
723 let yc_c = Mat::from_fn(n, k, |i, j| yc[(i, j)] - means[j]);
724 let cov = (yc_c.transpose() * &yc_c) * (1.0 / (nf - 1.0));
725 let eig = cov
726 .self_adjoint_eigen(Side::Lower)
727 .map_err(|_| CoreErrors::FaerEigenError)?;
728 let u = eig.U();
730 let vecs = Mat::from_fn(k, k, |i, j| u[(i, k - 1 - j)]);
731 let loadings = &c_orth * &vecs;
732 let scores = gemm(y_mat, loadings.as_ref());
733
734 let r2 = r2_cum(&prep.y, None, d, scores.as_ref(), loadings.as_ref());
735
736 Ok(Fit {
737 scores,
738 loadings,
739 r2_cum: r2,
740 noise_var: ss,
741 n_iter,
742 converged,
743 })
744}
745
746pub fn ppca<T>(
769 y: MatRef<T>,
770 params: Option<PpcaParams>,
771 verbosity: Verbosity,
772) -> Result<MissingPcaResults<T>, CoreErrors>
773where
774 T: BixverseFloat,
775{
776 let params = params.unwrap_or_default();
777 let mut rng = StdRng::seed_from_u64(params.seed);
778 let normal = Normal::new(0.0, 1.0).expect("unit normal is valid");
779 let c0 = Mat::<T>::from_fn(y.ncols(), params.n_pcs, |_, _| {
780 T::from_f64(normal.sample(&mut rng)).unwrap()
781 });
782 ppca_from_init(y, c0.as_ref(), Some(params), verbosity)
783}
784
785pub fn ppca_from_init<T>(
803 y: MatRef<T>,
804 c0: MatRef<T>,
805 params: Option<PpcaParams>,
806 verbosity: Verbosity,
807) -> Result<MissingPcaResults<T>, CoreErrors>
808where
809 T: BixverseFloat,
810{
811 let params = params.unwrap_or_default();
812 let mut prep = prep_data(y, params.n_pcs, params.centre, params.scale)?;
813 if c0.shape() != (prep.d, params.n_pcs) {
814 return Err(CoreErrors::InvalidArgument(format!(
815 "initial loadings are {} x {}, expected {} x {}",
816 c0.nrows(),
817 c0.ncols(),
818 prep.d,
819 params.n_pcs
820 )));
821 }
822 let c = Mat::from_fn(prep.d, params.n_pcs, |i, j| c0[(i, j)].to_f64().unwrap());
823 let fit = ppca_fit(&mut prep, c, ¶ms, verbosity)?;
824 Ok(finish(y, &prep, fit))
825}
826
827struct BpcaState {
833 pa: Mat<f64>,
835 tau: f64,
837 alpha: Vec<f64>,
839 sig_w: Mat<f64>,
841}
842
843fn bpca_init(prep: &PreppedData, k: usize) -> Result<BpcaState, CoreErrors> {
860 let (n, d) = (prep.n, prep.d);
861 let nf = n as f64;
862 let y = MatRef::from_row_major_slice(&prep.y, n, d);
863
864 let zero_means: Vec<f64> = y
865 .par_col_iter()
866 .map(|col| col.iter().sum::<f64>() / nf)
867 .collect();
868 let yc = Mat::from_fn(n, d, |i, j| y[(i, j)] - zero_means[j]);
869 let tr_cov = frob_dot(yc.as_ref(), yc.as_ref()) / (nf - 1.0);
870
871 let svd = yc
872 .thin_svd()
873 .map_err(|e| CoreErrors::FaerSvdError(format!("{e:?}")))?;
874 let s = svd.S().column_vector();
875 let v = svd.V();
876 let eig: Vec<f64> = (0..k).map(|l| s[l] * s[l] / (nf - 1.0)).collect();
877
878 let pa = Mat::from_fn(d, k, |j, l| v[(j, l)] * eig[l].sqrt());
879 let tau = (1.0 / (tr_cov - eig.iter().sum::<f64>())).clamp(BPCA_TAU_MIN, BPCA_TAU_MAX);
880 let alpha = pa
881 .col_iter()
882 .map(|col| {
883 let sq: f64 = col.iter().map(|v| v * v).sum();
884 (2.0 * BPCA_GALPHA0 + d as f64) / (tau * sq + 2.0 * BPCA_GALPHA0 / BPCA_BALPHA0)
885 })
886 .collect();
887
888 Ok(BpcaState {
889 pa,
890 tau,
891 alpha,
892 sig_w: Mat::identity(k, k),
893 })
894}
895
896fn bpca_step(
921 state: &mut BpcaState,
922 dy: &mut [f64],
923 prep: &PreppedData,
924 mean_sq: f64,
925) -> Result<Mat<f64>, CoreErrors> {
926 let (n, d) = (prep.n, prep.d);
927 let k = state.pa.ncols();
928 let (nf, df) = (n as f64, d as f64);
929 let tau = state.tau;
930 let eye = Mat::<f64>::identity(k, k);
931
932 let ptp = state.pa.transpose() * &state.pa;
933 let rx = &eye + &ptp * tau + &state.sig_w;
934 let (rx_inv, _) = spd_inverse(rx.as_ref())?;
935 let base = &eye + &state.sig_w;
936 let pa_rows = state.pa.transpose().to_owned();
938
939 let rows: Vec<(Vec<f64>, Option<Mat<f64>>, f64)> = dy
940 .par_chunks_mut(d)
941 .zip(prep.missing.par_chunks(d))
942 .zip(prep.n_miss_row.par_iter())
943 .map(|((row, m_row), &nm)| {
944 let mut ex = vec![0.0; k];
945 for j in 0..d {
946 if !m_row[j] {
947 let w = pa_rows.col(j);
948 for l in 0..k {
949 ex[l] += w[l] * row[j];
950 }
951 }
952 }
953
954 if nm == 0 {
955 let x: Vec<f64> = (0..k)
956 .map(|a| tau * (0..k).map(|b| rx_inv[(a, b)] * ex[b]).sum::<f64>())
957 .collect();
958 let dd: f64 = row.iter().map(|v| v * v).sum();
959 return Ok((x, None, dd));
960 }
961
962 let use_missing = nm < d - nm;
963 let mut g = if use_missing {
964 rx.clone()
965 } else {
966 base.clone()
967 };
968 let sign = if use_missing { -tau } else { tau };
969 for j in 0..d {
970 if m_row[j] == use_missing {
971 let w = pa_rows.col(j);
972 for a in 0..k {
973 for b in 0..k {
974 g[(a, b)] += sign * w[a] * w[b];
975 }
976 }
977 }
978 }
979 let (g_inv, _) = spd_inverse(g.as_ref())?;
980 let x: Vec<f64> = (0..k)
981 .map(|a| tau * (0..k).map(|b| g_inv[(a, b)] * ex[b]).sum::<f64>())
982 .collect();
983
984 let mut quad = 0.0;
985 for j in 0..d {
986 if m_row[j] {
987 let w = pa_rows.col(j);
988 row[j] = (0..k).map(|l| w[l] * x[l]).sum();
989 for a in 0..k {
990 for b in 0..k {
991 quad += w[a] * g_inv[(a, b)] * w[b];
992 }
993 }
994 }
995 }
996 let dd: f64 = row.iter().map(|v| v * v).sum();
997 Ok((x, Some(g_inv), dd + nm as f64 / tau + quad))
998 })
999 .collect::<Result<_, CoreErrors>>()?;
1000
1001 let scores = Mat::from_fn(n, k, |i, l| rows[i].0[l]);
1002 let tr_s = rows.iter().map(|r| r.2).sum::<f64>() / nf;
1003
1004 let mut t = gemm(
1005 MatRef::from_row_major_slice(dy, n, d).transpose(),
1006 scores.as_ref(),
1007 );
1008
1009 let incomplete: Vec<usize> = (0..n).filter(|&i| prep.n_miss_row[i] > 0).collect();
1011 if !incomplete.is_empty() {
1012 let mut corr = vec![0.0; d * k];
1013 corr.par_chunks_mut(k).enumerate().for_each(|(j, out)| {
1014 let w = pa_rows.col(j);
1015 for &i in &incomplete {
1016 if prep.missing[i * d + j] {
1017 let g_inv = rows[i].1.as_ref().expect("incomplete rows carry Rxinv");
1018 for b in 0..k {
1019 out[b] += (0..k).map(|a| w[a] * g_inv[(a, b)]).sum::<f64>();
1020 }
1021 }
1022 }
1023 });
1024 for j in 0..d {
1025 for b in 0..k {
1026 t[(j, b)] += corr[j * k + b];
1027 }
1028 }
1029 }
1030 t *= faer::Scale(1.0 / nf);
1031
1032 let alpha_diag = Mat::from_fn(k, k, |a, b| if a == b { state.alpha[a] / nf } else { 0.0 });
1034 let dw = &rx_inv + (t.transpose() * &state.pa * &rx_inv) * tau + alpha_diag;
1035 let dw_inv = dw.partial_piv_lu().inverse();
1036 state.pa = &t * &dw_inv;
1037
1038 let tr_tpa = frob_dot(t.as_ref(), state.pa.as_ref());
1039 state.tau = (df + 2.0 * BPCA_GTAU0 / nf)
1040 / (tr_s - tr_tpa + (mean_sq * BPCA_GMU0 + 2.0 * BPCA_GTAU0 / BPCA_BTAU0) / nf);
1041 state.sig_w = dw_inv * (df / nf);
1042 state.alpha = state
1043 .pa
1044 .col_iter()
1045 .enumerate()
1046 .map(|(l, col)| {
1047 let sq: f64 = col.iter().map(|v| v * v).sum();
1048 (2.0 * BPCA_GALPHA0 + df)
1049 / (state.tau * sq + state.sig_w[(l, l)] + 2.0 * BPCA_GALPHA0 / BPCA_BALPHA0)
1050 })
1051 .collect();
1052
1053 Ok(scores)
1054}
1055
1056fn bpca_fit(
1069 prep: &PreppedData,
1070 params: &BpcaParams,
1071 verbosity: Verbosity,
1072) -> Result<Fit, CoreErrors> {
1073 let (n, d, k) = (prep.n, prep.d, params.n_pcs);
1074 let mut state = bpca_init(prep, k)?;
1075
1076 let obs_mean: Vec<f64> = (0..d)
1077 .into_par_iter()
1078 .map(|j| {
1079 let (s, c) = (0..n)
1080 .filter(|&i| !prep.missing[i * d + j])
1081 .fold((0.0, 0usize), |(s, c), i| (s + prep.y[i * d + j], c + 1));
1082 s / c as f64
1083 })
1084 .collect();
1085 let mean_sq: f64 = obs_mean.iter().map(|v| v * v).sum();
1086
1087 let mut dy = prep.y.clone();
1088 dy.par_chunks_mut(d).for_each(|row| {
1089 row.iter_mut().zip(&obs_mean).for_each(|(v, m)| *v -= m);
1090 });
1091
1092 let mut tau_old = BPCA_TAU_START;
1093 let mut scores = Mat::zeros(n, k);
1094 let mut n_iter = 0;
1095 let mut converged = false;
1096
1097 for step in 1..=params.max_iter {
1098 scores = bpca_step(&mut state, &mut dy, prep, mean_sq)?;
1099 n_iter = step;
1100 let check = step % BPCA_CHECK_EVERY == 0;
1101 if verbosity.detailed_verbosity() && !check {
1102 println!("BPCA: step {step}, tau {:.6e}", state.tau);
1103 }
1104 if check {
1105 let dtau = (state.tau.log10() - tau_old.log10()).abs();
1106 if verbosity.normal_verbosity() {
1107 println!(
1108 "BPCA: step {step}, tau {:.6e}, change in log10(tau) {dtau:.3e}",
1109 state.tau
1110 );
1111 }
1112 if dtau < params.tol {
1113 converged = true;
1114 break;
1115 }
1116 tau_old = state.tau;
1117 }
1118 }
1119
1120 if verbosity.normal_verbosity() {
1121 let status = if converged {
1122 "converged"
1123 } else {
1124 "hit max_iter"
1125 };
1126 println!(
1127 "BPCA: {status} after {n_iter} steps, noise variance {:.4e}",
1128 1.0 / state.tau
1129 );
1130 }
1131
1132 let r2 = r2_cum(
1133 &prep.y,
1134 Some(&prep.missing),
1135 d,
1136 scores.as_ref(),
1137 state.pa.as_ref(),
1138 );
1139
1140 Ok(Fit {
1141 scores,
1142 loadings: state.pa,
1143 r2_cum: r2,
1144 noise_var: 1.0 / state.tau,
1145 n_iter,
1146 converged,
1147 })
1148}
1149
1150pub fn bpca<T>(
1171 y: MatRef<T>,
1172 params: Option<BpcaParams>,
1173 verbosity: Verbosity,
1174) -> Result<MissingPcaResults<T>, CoreErrors>
1175where
1176 T: BixverseFloat,
1177{
1178 let params = params.unwrap_or_default();
1179 let prep = prep_data(y, params.n_pcs, params.centre, params.scale)?;
1180 let fit = bpca_fit(&prep, ¶ms, verbosity)?;
1181 Ok(finish(y, &prep, fit))
1182}
1183
1184#[cfg(test)]
1189mod tests {
1190 use super::*;
1191
1192 fn low_rank(n: usize, d: usize, k: usize, noise: f64, miss: f64) -> (Mat<f64>, Mat<f64>) {
1197 let mut rng = StdRng::seed_from_u64(1);
1198 let normal = Normal::new(0.0, 1.0).unwrap();
1199 let z = Mat::<f64>::from_fn(n, k, |_, _| normal.sample(&mut rng));
1200 let w = Mat::<f64>::from_fn(d, k, |_, _| normal.sample(&mut rng));
1201 let signal = &z * w.transpose();
1202 let full = Mat::from_fn(n, d, |i, j| {
1203 signal[(i, j)] + noise * normal.sample(&mut rng)
1204 });
1205 let masked = Mat::from_fn(n, d, |i, j| {
1206 if rng.random::<f64>() < miss {
1207 f64::NAN
1208 } else {
1209 full[(i, j)]
1210 }
1211 });
1212 (masked, full)
1213 }
1214
1215 fn subspace_gap(a: MatRef<f64>, b: MatRef<f64>) -> f64 {
1218 let qa = a.qr().compute_thin_Q();
1219 let qb = b.qr().compute_thin_Q();
1220 let diff = &qa * qa.transpose() - &qb * qb.transpose();
1221 frob_dot(diff.as_ref(), diff.as_ref()).sqrt()
1222 }
1223
1224 fn svd_basis(y: MatRef<f64>, k: usize) -> Mat<f64> {
1226 let (n, d) = y.shape();
1227 let means: Vec<f64> = (0..d)
1228 .map(|j| (0..n).map(|i| y[(i, j)]).sum::<f64>() / n as f64)
1229 .collect();
1230 let yc = Mat::from_fn(n, d, |i, j| y[(i, j)] - means[j]);
1231 let svd = yc.thin_svd().unwrap();
1232 svd.V().subcols(0, k).to_owned()
1233 }
1234
1235 #[test]
1237 fn test_complete_data_matches_svd_subspace() {
1238 let (_, full) = low_rank(80, 15, 3, 0.1, 0.0);
1239 let basis = svd_basis(full.as_ref(), 3);
1240
1241 let params = PpcaParams {
1242 n_pcs: 3,
1243 ..PpcaParams::default()
1244 };
1245 let pp = ppca(full.as_ref(), Some(params), Verbosity::Quiet).unwrap();
1246 assert!(pp.converged);
1247 assert!(subspace_gap(pp.loadings.as_ref(), basis.as_ref()) < 1e-4);
1248
1249 let params = BpcaParams {
1250 n_pcs: 3,
1251 ..BpcaParams::default()
1252 };
1253 let bp = bpca(full.as_ref(), Some(params), Verbosity::Quiet).unwrap();
1254 assert!(subspace_gap(bp.loadings.as_ref(), basis.as_ref()) < 1e-4);
1255 }
1256
1257 #[test]
1259 fn test_ppca_seed_reproducible_and_observed_untouched() {
1260 let (masked, _) = low_rank(50, 10, 2, 0.2, 0.2);
1261 let a = ppca(masked.as_ref(), None, Verbosity::Quiet).unwrap();
1262 let b = ppca(masked.as_ref(), None, Verbosity::Quiet).unwrap();
1263 assert_eq!(a.n_iter, b.n_iter);
1264 for i in 0..50 {
1265 for j in 0..10 {
1266 assert_eq!(a.completed[(i, j)], b.completed[(i, j)]);
1267 if !masked[(i, j)].is_nan() {
1268 assert_eq!(a.completed[(i, j)], masked[(i, j)]);
1269 }
1270 }
1271 }
1272 }
1273
1274 #[test]
1277 fn test_imputation_recovers_low_rank_signal() {
1278 let (masked, full) = low_rank(120, 30, 3, 0.1, 0.3);
1279 let rmse = |c: &Mat<f64>| {
1280 let (mut s, mut m) = (0.0, 0usize);
1281 for i in 0..120 {
1282 for j in 0..30 {
1283 if masked[(i, j)].is_nan() {
1284 s += (c[(i, j)] - full[(i, j)]).powi(2);
1285 m += 1;
1286 }
1287 }
1288 }
1289 (s / m as f64).sqrt()
1290 };
1291
1292 for scale in [false, true] {
1293 let pp = ppca(
1294 masked.as_ref(),
1295 Some(PpcaParams {
1296 n_pcs: 3,
1297 scale,
1298 ..PpcaParams::default()
1299 }),
1300 Verbosity::Quiet,
1301 )
1302 .unwrap();
1303 let bp = bpca(
1304 masked.as_ref(),
1305 Some(BpcaParams {
1306 n_pcs: 3,
1307 scale,
1308 ..BpcaParams::default()
1309 }),
1310 Verbosity::Quiet,
1311 )
1312 .unwrap();
1313 assert!(
1315 rmse(&pp.completed) < 0.3,
1316 "ppca scale={scale}: {}",
1317 rmse(&pp.completed)
1318 );
1319 assert!(
1320 rmse(&bp.completed) < 0.3,
1321 "bpca scale={scale}: {}",
1322 rmse(&bp.completed)
1323 );
1324 assert!(pp.r2_cum.windows(2).all(|w| w[0] <= w[1]));
1325 }
1326 }
1327
1328 #[test]
1330 fn test_f32_input_matches_f64() {
1331 let (masked, _) = low_rank(40, 8, 2, 0.2, 0.2);
1332 let masked32 = Mat::<f32>::from_fn(40, 8, |i, j| masked[(i, j)] as f32);
1333 let a = bpca(masked.as_ref(), None, Verbosity::Quiet).unwrap();
1334 let b = bpca(masked32.as_ref(), None, Verbosity::Quiet).unwrap();
1335 for i in 0..40 {
1336 for j in 0..8 {
1337 assert!((a.completed[(i, j)] - b.completed[(i, j)] as f64).abs() < 1e-3);
1338 }
1339 }
1340 }
1341
1342 #[test]
1344 fn test_invalid_inputs_error() {
1345 let (mut masked, _) = low_rank(20, 6, 2, 0.1, 0.0);
1346 let too_many = PpcaParams {
1347 n_pcs: 7,
1348 ..PpcaParams::default()
1349 };
1350 assert!(matches!(
1351 ppca(masked.as_ref(), Some(too_many), Verbosity::Quiet),
1352 Err(CoreErrors::PcaTooManyComponents { n_pcs: 7, max: 6 })
1353 ));
1354
1355 for i in 0..20 {
1356 masked[(i, 4)] = f64::NAN;
1357 }
1358 assert!(matches!(
1359 bpca(masked.as_ref(), None, Verbosity::Quiet),
1360 Err(CoreErrors::PcaAllMissing {
1361 axis: "column",
1362 index: 4
1363 })
1364 ));
1365
1366 let (mut masked, _) = low_rank(20, 6, 2, 0.1, 0.0);
1367 for j in 0..6 {
1368 masked[(3, j)] = f64::NAN;
1369 }
1370 assert!(matches!(
1371 ppca(masked.as_ref(), None, Verbosity::Quiet),
1372 Err(CoreErrors::PcaAllMissing {
1373 axis: "row",
1374 index: 3
1375 })
1376 ));
1377
1378 let c0 = Mat::<f64>::zeros(5, 2);
1379 let (full, _) = low_rank(20, 6, 2, 0.1, 0.0);
1380 assert!(matches!(
1381 ppca_from_init(full.as_ref(), c0.as_ref(), None, Verbosity::Quiet),
1382 Err(CoreErrors::InvalidArgument(_))
1383 ));
1384 }
1385}