1use bixverse_core::utils::gemm::gram;
5use faer::linalg::matmul::triangular::BlockStructure;
6use faer::{Accum, Mat, MatRef};
7use num_traits::Float;
8use rand::SeedableRng;
9use rand::rngs::StdRng;
10use rand_distr::{Distribution, Normal};
11use rayon::prelude::*;
12
13use crate::prelude::*;
14use bixverse_core::core::math::pca_svd::SvdResults;
15use bixverse_core::utils::faer_parallelism;
16
17pub mod consensus;
18pub mod dense;
19pub mod nmf_preprocessing;
20pub mod refit;
21pub mod sparse;
22
23const HALS_COL_CHUNK: usize = 64;
25
26const HALS_ROW_TILE: usize = 256;
28
29#[derive(Clone, Debug, Copy, Default)]
34pub enum NmfInit {
36 #[default]
39 Nndsvd,
40 Random {
42 seed: u64,
44 },
45}
46
47pub fn parse_nmf_init(s: &str, seed: usize) -> Option<NmfInit> {
58 match s.to_lowercase().as_str() {
59 "nndsvd" | "svd" => Some(NmfInit::Nndsvd),
60 "random" => Some(NmfInit::Random { seed: seed as u64 }),
61 _ => None,
62 }
63}
64
65pub fn random_init<F: BixverseFloat>(
79 m: usize,
80 n: usize,
81 k: usize,
82 sq_frob: F,
83 seed: u64,
84) -> (Mat<F>, Mat<F>) {
85 let mut rng = StdRng::seed_from_u64(seed);
86 let normal = Normal::new(0.0, 1.0).unwrap();
87
88 let mn_k = F::from_usize(m * n * k).unwrap();
89 let denom = if mn_k > F::zero() { mn_k } else { F::one() };
90 let scale = (sq_frob / denom).sqrt();
91
92 let w = Mat::<F>::from_fn(m, k, |_, _| {
93 let v: f64 = normal.sample(&mut rng).abs();
94 F::from_f64(v).unwrap() * scale
95 });
96 let h = Mat::<F>::from_fn(k, n, |_, _| {
97 let v: f64 = normal.sample(&mut rng).abs();
98 F::from_f64(v).unwrap() * scale
99 });
100
101 (w, h)
102}
103
104pub fn nndsvd_from_svd<F: BixverseFloat>(
118 svd: &SvdResults<F>,
119 k: usize,
120 m: usize,
121 n: usize,
122) -> Result<(Mat<F>, Mat<F>), MethodsErrors> {
123 let avail = svd.s.len();
124 if avail < k {
125 return Err(MethodsErrors::NmfRankTooLarge {
126 requested: k,
127 available: avail,
128 });
129 }
130
131 let mut w = Mat::<F>::zeros(m, k);
132 let mut h = Mat::<F>::zeros(k, n);
133
134 let s0_sqrt = svd.s[0].sqrt();
135 for i in 0..m {
136 w[(i, 0)] = svd.u[(i, 0)].abs() * s0_sqrt;
137 }
138 for j in 0..n {
139 h[(0, j)] = svd.v[(j, 0)].abs() * s0_sqrt;
140 }
141
142 for r in 1..k {
143 let mut up = vec![F::zero(); m];
144 let mut un = vec![F::zero(); m];
145 let mut vp = vec![F::zero(); n];
146 let mut vn = vec![F::zero(); n];
147 let mut up_sq = 0f64;
151 let mut un_sq = 0f64;
152 let mut vp_sq = 0f64;
153 let mut vn_sq = 0f64;
154
155 for i in 0..m {
156 let x = svd.u[(i, r)];
157 let d = x.to_f64().unwrap();
158 if x > F::zero() {
159 up[i] = x;
160 up_sq += d * d;
161 } else if x < F::zero() {
162 un[i] = -x;
163 un_sq += d * d;
164 }
165 }
166 for j in 0..n {
167 let y = svd.v[(j, r)];
168 let d = y.to_f64().unwrap();
169 if y > F::zero() {
170 vp[j] = y;
171 vp_sq += d * d;
172 } else if y < F::zero() {
173 vn[j] = -y;
174 vn_sq += d * d;
175 }
176 }
177
178 let up_norm = F::from_f64(up_sq.sqrt()).unwrap();
179 let un_norm = F::from_f64(un_sq.sqrt()).unwrap();
180 let vp_norm = F::from_f64(vp_sq.sqrt()).unwrap();
181 let vn_norm = F::from_f64(vn_sq.sqrt()).unwrap();
182 let m_pos = up_norm * vp_norm;
183 let m_neg = un_norm * vn_norm;
184
185 let (chosen_u, chosen_v, u_norm, v_norm, m_chosen) = if m_pos >= m_neg {
186 (&up, &vp, up_norm, vp_norm, m_pos)
187 } else {
188 (&un, &vn, un_norm, vn_norm, m_neg)
189 };
190
191 if u_norm <= F::zero() || v_norm <= F::zero() {
192 continue;
193 }
194 let factor = (svd.s[r] * m_chosen).sqrt();
195 let scale_u = factor / u_norm;
196 let scale_v = factor / v_norm;
197 for i in 0..m {
198 w[(i, r)] = chosen_u[i] * scale_u;
199 }
200 for j in 0..n {
201 h[(r, j)] = chosen_v[j] * scale_v;
202 }
203 }
204
205 Ok((w, h))
206}
207
208#[derive(Clone, Debug)]
214pub struct HalsOpts<F: BixverseFloat> {
215 pub max_iter: usize,
217 pub tol: F,
219 pub eps: F,
221 pub check_every: usize,
223 pub init: NmfInit,
225}
226
227impl<F: BixverseFloat> HalsOpts<F> {
228 pub fn new(max_iter: usize, tol: F, eps: F, check_every: usize, init: NmfInit) -> Self {
243 Self {
244 max_iter,
245 tol,
246 eps,
247 check_every,
248 init,
249 }
250 }
251}
252
253impl<T> Default for HalsOpts<T>
254where
255 T: BixverseFloat,
256{
257 fn default() -> Self {
258 Self {
259 max_iter: 250,
260 tol: T::from_f64(1e-4).unwrap(),
261 eps: T::from_f64(1e-10).unwrap(),
262 check_every: 10,
263 init: NmfInit::Nndsvd,
264 }
265 }
266}
267
268pub struct NmfResult<F: BixverseFloat> {
274 pub w: Mat<F>,
276 pub h: Mat<F>,
278 pub final_loss: F,
280 pub n_iter: usize,
282 pub converged: bool,
284}
285
286pub trait NmfInput<F: BixverseFloat> {
293 fn shape(&self) -> (usize, usize);
299
300 fn sq_frob(&self) -> F;
308
309 fn wt_v(&self, w: MatRef<F>, out: &mut Mat<F>);
318
319 fn v_ht(&self, h: MatRef<F>, out: &mut Mat<F>);
329
330 fn top_k_svd(&self, k: usize) -> Result<SvdResults<F>, MethodsErrors>;
343}
344
345pub fn gram_wt_w<F: BixverseFloat>(w: MatRef<F>, out: &mut Mat<F>) {
358 let k = w.ncols();
359 gram(
360 out.as_mut(),
361 BlockStructure::TriangularLower,
362 Accum::Replace,
363 w.transpose(),
364 w,
365 F::one(),
366 faer_parallelism(),
367 );
368 for j in 0..k {
369 for i in 0..j {
370 out[(i, j)] = out[(j, i)];
371 }
372 }
373}
374
375pub fn gram_h_ht<F: BixverseFloat>(h: MatRef<F>, out: &mut Mat<F>) {
384 let k = h.nrows();
385 gram(
386 out.as_mut(),
387 BlockStructure::TriangularLower,
388 Accum::Replace,
389 h,
390 h.transpose(),
391 F::one(),
392 faer_parallelism(),
393 );
394 for j in 0..k {
395 for i in 0..j {
396 out[(i, j)] = out[(j, i)];
397 }
398 }
399}
400
401pub fn hals_sweep_rows<F>(h: &mut Mat<F>, b: MatRef<F>, a: MatRef<F>, eps: F)
414where
415 F: BixverseFloat + Send + Sync,
416{
417 let k = h.nrows();
418
419 let b_rows: Vec<F> = (0..k)
422 .flat_map(|r| (0..k).map(move |s| (r, s)))
423 .map(|(r, s)| b[(r, s)])
424 .collect();
425
426 h.par_col_iter_mut()
427 .with_min_len(HALS_COL_CHUNK)
428 .enumerate()
429 .for_each(|(j, col)| {
430 let col = col.try_as_col_major_mut().unwrap().as_slice_mut();
431 let a_col = a.col(j);
432 for r in 0..k {
433 let brr = b_rows[r * k + r];
434 if brr <= F::zero() {
435 continue;
436 }
437 let inv_brr = F::one() / brr;
438 let mut bh_rj = F::zero();
439 for (&b_rs, &h_sj) in b_rows[r * k..(r + 1) * k].iter().zip(col.iter()) {
440 bh_rj += b_rs * h_sj;
441 }
442 let new_val = col[r] + (a_col[r] - bh_rj) * inv_brr;
443 col[r] = if new_val > eps { new_val } else { eps };
444 }
445 });
446}
447
448pub fn hals_sweep_cols<F>(w: &mut Mat<F>, d: MatRef<F>, c_mat: MatRef<F>, eps: F)
461where
462 F: BixverseFloat + Send + Sync,
463{
464 let m = w.nrows();
465 let k = w.ncols();
466
467 let w_ptr = w.as_ptr_mut() as usize;
468 let w_row_stride = w.row_stride();
469 let w_col_stride = w.col_stride();
470
471 let d_cols: Vec<F> = (0..k)
472 .flat_map(|c| (0..k).map(move |s| (s, c)))
473 .map(|(s, c)| d[(s, c)])
474 .collect();
475
476 (0..m.div_ceil(HALS_ROW_TILE))
479 .into_par_iter()
480 .for_each_init(
481 || vec![F::zero(); HALS_ROW_TILE * k],
482 |scratch, tile| {
483 let i0 = tile * HALS_ROW_TILE;
484 let rows = HALS_ROW_TILE.min(m - i0);
485 let base = w_ptr as *mut F;
486 let offset =
487 |i: usize, s: usize| i as isize * w_row_stride + s as isize * w_col_stride;
488
489 for s in 0..k {
490 for ii in 0..rows {
491 scratch[ii * k + s] = unsafe { *base.offset(offset(i0 + ii, s)) };
493 }
494 }
495
496 for c in 0..k {
497 let dcc = d_cols[c * k + c];
498 if dcc <= F::zero() {
499 continue;
500 }
501 let inv_dcc = F::one() / dcc;
502 let d_col_c = &d_cols[c * k..(c + 1) * k];
503 for ii in 0..rows {
504 let row = &mut scratch[ii * k..(ii + 1) * k];
505 let mut wd_ic = F::zero();
506 for (&w_is, &d_sc) in row.iter().zip(d_col_c) {
507 wd_ic += w_is * d_sc;
508 }
509 let new_val = row[c] + (c_mat[(i0 + ii, c)] - wd_ic) * inv_dcc;
510 row[c] = if new_val > eps { new_val } else { eps };
511 }
512 }
513
514 for s in 0..k {
515 for ii in 0..rows {
516 unsafe {
518 *base.offset(offset(i0 + ii, s)) = scratch[ii * k + s];
519 }
520 }
521 }
522 },
523 );
524}
525
526pub fn normalise_w_cols<F: BixverseFloat>(w: &mut Mat<F>, h: &mut Mat<F>) {
536 let m = w.nrows();
537 let k = w.ncols();
538 let n = h.ncols();
539 for c in 0..k {
540 let mut sq_norm = 0f64;
546 for i in 0..m {
547 let x = w[(i, c)].to_f64().unwrap();
548 sq_norm += x * x;
549 }
550 let norm = F::from_f64(sq_norm.sqrt()).unwrap();
551 if norm <= F::zero() {
552 continue;
553 }
554 let inv_norm = F::one() / norm;
555 for i in 0..m {
556 w[(i, c)] *= inv_norm;
557 }
558 for j in 0..n {
559 h[(c, j)] *= norm;
560 }
561 }
562}
563
564pub fn compute_objective<F: BixverseFloat + Send + Sync>(
583 sq_frob_v: F,
584 h: MatRef<F>,
585 a: MatRef<F>,
586 b: MatRef<F>,
587 d: MatRef<F>,
588) -> F {
589 let k = h.nrows();
590 let n = h.ncols();
591
592 let inner_ha: f64 = (0..n)
593 .into_par_iter()
594 .with_min_len(64)
595 .map(|j| {
596 let mut acc = 0f64;
597 for r in 0..k {
598 acc += h[(r, j)].to_f64().unwrap() * a[(r, j)].to_f64().unwrap();
599 }
600 acc
601 })
602 .reduce(|| 0f64, |x, y| x + y);
603
604 let mut inner_bd = 0f64;
605 for j in 0..k {
606 for i in 0..k {
607 inner_bd += b[(i, j)].to_f64().unwrap() * d[(i, j)].to_f64().unwrap();
608 }
609 }
610
611 let result = sq_frob_v.to_f64().unwrap() - 2.0 * inner_ha + inner_bd;
612 F::from_f64(result.max(0.0)).unwrap()
613}
614
615pub fn nmf_hals<F, In>(
639 v: &In,
640 k: usize,
641 opts: &HalsOpts<F>,
642 verbose: usize,
643) -> Result<NmfResult<F>, MethodsErrors>
644where
645 F: BixverseFloat + Send + Sync,
646 In: NmfInput<F> + Sync,
647{
648 let (m, n) = v.shape();
649 let sq_frob = v.sq_frob();
650
651 let verbosity = parse_verbosity_level(verbose);
652
653 let (mut w, mut h) = match &opts.init {
654 NmfInit::Nndsvd => {
655 let svd = v.top_k_svd(k)?;
656 nndsvd_from_svd(&svd, k, m, n)?
657 }
658 NmfInit::Random { seed } => random_init(m, n, k, sq_frob, *seed),
659 };
660
661 let mut wtv = Mat::<F>::zeros(k, n);
662 let mut vht = Mat::<F>::zeros(m, k);
663 let mut wtw = Mat::<F>::zeros(k, k);
664 let mut hht = Mat::<F>::zeros(k, k);
665
666 let mut final_loss = F::infinity();
667 let mut last_loss = F::infinity();
668 let mut converged = false;
669 let mut n_iter = 0usize;
670
671 let mut grams_current = false;
673
674 for iter in 0..opts.max_iter {
675 n_iter = iter + 1;
676
677 if !grams_current {
678 gram_wt_w(w.as_ref(), &mut wtw);
679 v.wt_v(w.as_ref(), &mut wtv);
680 }
681 grams_current = false;
682 hals_sweep_rows(&mut h, wtw.as_ref(), wtv.as_ref(), opts.eps);
683
684 gram_h_ht(h.as_ref(), &mut hht);
685 v.v_ht(h.as_ref(), &mut vht);
686 hals_sweep_cols(&mut w, hht.as_ref(), vht.as_ref(), opts.eps);
687
688 normalise_w_cols(&mut w, &mut h);
689
690 if n_iter.is_multiple_of(opts.check_every) {
691 gram_wt_w(w.as_ref(), &mut wtw);
692 v.wt_v(w.as_ref(), &mut wtv);
693 gram_h_ht(h.as_ref(), &mut hht);
694 grams_current = true;
695
696 let loss = compute_objective(
697 sq_frob,
698 h.as_ref(),
699 wtv.as_ref(),
700 wtw.as_ref(),
701 hht.as_ref(),
702 );
703 final_loss = loss;
704
705 if n_iter > opts.check_every {
706 if verbosity.normal_verbosity() {
707 println!(
708 " NMF: Iteration {} out of {} - current loss: {:.2?}",
709 iter + 1,
710 opts.max_iter,
711 loss
712 );
713 }
714
715 let denom = if sq_frob > F::one() {
716 sq_frob
717 } else {
718 F::one()
719 };
720 let rel = (last_loss - loss).abs() / denom;
721 if rel < opts.tol {
722 converged = true;
723 if verbosity.normal_verbosity() {
724 println!(" NMF converged successfully after {} iters", iter + 1)
725 };
726
727 break;
728 }
729 }
730 last_loss = loss;
731 }
732 }
733
734 if !n_iter.is_multiple_of(opts.check_every) {
735 gram_wt_w(w.as_ref(), &mut wtw);
736 v.wt_v(w.as_ref(), &mut wtv);
737 gram_h_ht(h.as_ref(), &mut hht);
738 final_loss = compute_objective(
739 sq_frob,
740 h.as_ref(),
741 wtv.as_ref(),
742 wtw.as_ref(),
743 hht.as_ref(),
744 );
745 }
746
747 Ok(NmfResult {
748 w,
749 h,
750 final_loss,
751 n_iter,
752 converged,
753 })
754}
755
756pub struct StabilisedNmfResult<F: BixverseFloat> {
762 pub w_all: Mat<F>,
765 pub h_per_run: Vec<Mat<F>>,
767 pub losses: Vec<F>,
769 pub converged: Vec<bool>,
771 pub best_idx: usize,
773}
774
775pub fn stabilised_nmf<F, In>(
799 v: &In,
800 k: usize,
801 n_runs: usize,
802 base_seed: u64,
803 opts: &HalsOpts<F>,
804 verbose: usize,
805) -> Result<StabilisedNmfResult<F>, MethodsErrors>
806where
807 F: BixverseFloat + Send + Sync,
808 In: NmfInput<F> + Sync,
809{
810 assert!(n_runs >= 1, "n_runs must be >= 1");
811
812 let verbosity = parse_verbosity_level(verbose);
813 let cores = rayon::current_num_threads();
814 let n_outer = n_runs.min((cores / 2).max(1));
815
816 let pool = rayon::ThreadPoolBuilder::new()
817 .num_threads(n_outer)
818 .build()
819 .expect("failed to build thread pool for stabilised NMF");
820
821 let runs: Vec<NmfResult<F>> = pool.install(|| {
822 (0..n_runs)
823 .into_par_iter()
824 .map(|i| {
825 let opts_i = HalsOpts {
826 max_iter: opts.max_iter,
827 tol: opts.tol,
828 eps: opts.eps,
829 check_every: opts.check_every,
830 init: NmfInit::Random {
831 seed: base_seed + i as u64,
832 },
833 };
834 let inner_verbose = verbose.saturating_sub(1);
835
836 let res = nmf_hals(v, k, &opts_i, inner_verbose);
837
838 if verbosity.normal_verbosity() {
839 println!(" Finished stabilised NMF run: {}", i + 1);
840 }
841
842 res
843 })
844 .collect::<Result<Vec<_>, _>>()
845 })?;
846
847 let (m, _) = v.shape();
848
849 let w_all = Mat::<F>::from_fn(m, k * n_runs, |row, col| {
850 let run_idx = col / k;
851 let comp_idx = col % k;
852 runs[run_idx].w[(row, comp_idx)]
853 });
854
855 let losses: Vec<F> = runs.iter().map(|r| r.final_loss).collect();
856 let converged: Vec<bool> = runs.iter().map(|r| r.converged).collect();
857 let h_per_run: Vec<Mat<F>> = runs.into_iter().map(|r| r.h).collect();
858
859 let best_idx = losses
860 .iter()
861 .enumerate()
862 .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
863 .map(|(i, _)| i)
864 .unwrap_or(0);
865
866 Ok(StabilisedNmfResult {
867 w_all,
868 h_per_run,
869 losses,
870 converged,
871 best_idx,
872 })
873}
874
875#[cfg(test)]
880mod tests {
881 use super::*;
882 use crate::methods::nmf_hals::dense::DenseInput;
883 use crate::methods::nmf_hals::sparse::SparseInput;
884 use faer::Mat;
885
886 #[test]
888 fn gram_wt_w_full_symmetric() {
889 let (m, k) = (5, 3);
890 let w: Mat<f64> = Mat::from_fn(m, k, |i, j| (i + j + 1) as f64);
891 let mut out = Mat::<f64>::zeros(k, k);
892 gram_wt_w(w.as_ref(), &mut out);
893 for r in 0..k {
894 for c in 0..k {
895 let expected: f64 = (0..m).map(|i| w[(i, r)] * w[(i, c)]).sum();
896 assert!((out[(r, c)] - expected).abs() < 1e-9);
897 }
898 }
899 }
900
901 #[test]
903 fn gram_h_ht_full_symmetric() {
904 let (k, n) = (3, 5);
905 let h: Mat<f64> = Mat::from_fn(k, n, |i, j| (i + j + 1) as f64);
906 let mut out = Mat::<f64>::zeros(k, k);
907 gram_h_ht(h.as_ref(), &mut out);
908 for r in 0..k {
909 for c in 0..k {
910 let expected: f64 = (0..n).map(|j| h[(r, j)] * h[(c, j)]).sum();
911 assert!((out[(r, c)] - expected).abs() < 1e-9);
912 }
913 }
914 }
915
916 #[test]
918 fn objective_matches_direct() {
919 let (m, n, k) = (4, 3, 2);
920 let v_mat: Mat<f64> = Mat::from_fn(m, n, |i, j| (i + j + 1) as f64);
921 let w: Mat<f64> = Mat::from_fn(m, k, |i, j| (i + j + 1) as f64 * 0.1);
922 let h: Mat<f64> = Mat::from_fn(k, n, |i, j| (i + j + 1) as f64 * 0.2);
923
924 let mut sq_frob = 0.0;
925 for i in 0..m {
926 for j in 0..n {
927 sq_frob += v_mat[(i, j)].powi(2);
928 }
929 }
930
931 let mut direct = 0.0;
932 for i in 0..m {
933 for j in 0..n {
934 let mut wh = 0.0;
935 for r in 0..k {
936 wh += w[(i, r)] * h[(r, j)];
937 }
938 direct += (v_mat[(i, j)] - wh).powi(2);
939 }
940 }
941
942 let input = DenseInput::new(v_mat.as_ref()).unwrap();
943 let mut wtv = Mat::<f64>::zeros(k, n);
944 let mut wtw = Mat::<f64>::zeros(k, k);
945 let mut hht = Mat::<f64>::zeros(k, k);
946 input.wt_v(w.as_ref(), &mut wtv);
947 gram_wt_w(w.as_ref(), &mut wtw);
948 gram_h_ht(h.as_ref(), &mut hht);
949
950 let via = compute_objective(
951 sq_frob,
952 h.as_ref(),
953 wtv.as_ref(),
954 wtw.as_ref(),
955 hht.as_ref(),
956 );
957 assert!((direct - via).abs() < 1e-8);
958 }
959
960 #[test]
962 fn normalise_preserves_product_and_unit_norms() {
963 let (m, n, k) = (4, 3, 2);
964 let mut w: Mat<f64> = Mat::from_fn(m, k, |i, j| (i + j + 1) as f64);
965 let mut h: Mat<f64> = Mat::from_fn(k, n, |i, j| (i + j + 1) as f64);
966
967 let mut before = Vec::with_capacity(m * n);
968 for i in 0..m {
969 for j in 0..n {
970 let mut prod = 0.0;
971 for r in 0..k {
972 prod += w[(i, r)] * h[(r, j)];
973 }
974 before.push(prod);
975 }
976 }
977
978 normalise_w_cols(&mut w, &mut h);
979
980 for c in 0..k {
981 let mut sq = 0.0;
982 for i in 0..m {
983 sq += w[(i, c)].powi(2);
984 }
985 assert!((sq.sqrt() - 1.0).abs() < 1e-9);
986 }
987
988 let mut after = Vec::with_capacity(m * n);
989 for i in 0..m {
990 for j in 0..n {
991 let mut prod = 0.0;
992 for r in 0..k {
993 prod += w[(i, r)] * h[(r, j)];
994 }
995 after.push(prod);
996 }
997 }
998 for (a, b) in after.iter().zip(before.iter()) {
999 assert!((a - b).abs() < 1e-9);
1000 }
1001 }
1002
1003 #[test]
1005 fn hals_recovers_rank_k_dense() {
1006 let (m, n, k) = (30, 20, 3);
1007 let w_true: Mat<f64> = Mat::from_fn(m, k, |i, j| ((i * 7 + j * 3) % 5 + 1) as f64);
1008 let h_true: Mat<f64> = Mat::from_fn(k, n, |i, j| ((i * 5 + j * 2) % 4 + 1) as f64);
1009 let v_mat: Mat<f64> = Mat::from_fn(m, n, |i, j| {
1010 let mut s = 0.0;
1011 for r in 0..k {
1012 s += w_true[(i, r)] * h_true[(r, j)];
1013 }
1014 s
1015 });
1016
1017 let mut sq_frob_v = 0.0;
1018 for i in 0..m {
1019 for j in 0..n {
1020 sq_frob_v += v_mat[(i, j)].powi(2);
1021 }
1022 }
1023
1024 let input = DenseInput::new(v_mat.as_ref()).unwrap();
1025 let opts = HalsOpts::<f64>::new(400, 1e-9, 1e-12, 5, NmfInit::Nndsvd);
1026 let res = nmf_hals(&input, k, &opts, 0).unwrap();
1027
1028 let rel = res.final_loss / sq_frob_v;
1029 assert!(rel < 1e-3, "rel loss {rel}");
1030
1031 for i in 0..m {
1032 for r in 0..k {
1033 assert!(res.w[(i, r)] >= 0.0);
1034 }
1035 }
1036 for r in 0..k {
1037 for j in 0..n {
1038 assert!(res.h[(r, j)] >= 0.0);
1039 }
1040 }
1041 }
1042
1043 #[test]
1045 fn hals_sparse_recovers_rank_k() {
1046 let (m, n, k) = (20, 15, 2);
1047 let w_true: Mat<f64> = Mat::from_fn(m, k, |i, j| {
1048 if (i + j) % 3 == 0 {
1049 (i + 1) as f64
1050 } else {
1051 0.0
1052 }
1053 });
1054 let h_true: Mat<f64> = Mat::from_fn(k, n, |i, j| ((i * 3 + j) % 4 + 1) as f64);
1055 let v_dense: Mat<f64> = Mat::from_fn(m, n, |i, j| {
1056 let mut s = 0.0;
1057 for r in 0..k {
1058 s += w_true[(i, r)] * h_true[(r, j)];
1059 }
1060 s
1061 });
1062
1063 let mut sq_frob_v = 0.0;
1064 for i in 0..m {
1065 for j in 0..n {
1066 sq_frob_v += v_dense[(i, j)].powi(2);
1067 }
1068 }
1069
1070 let csr = CompressedSparseData2::<f64, f64>::from_dense_matrix(
1071 v_dense.as_ref(),
1072 CompressedSparseFormat::Csr,
1073 );
1074 let sparse_in: SparseInput<f64, f64> = SparseInput::from_primary(&csr).unwrap();
1075
1076 let opts = HalsOpts::<f64>::new(400, 1e-9, 1e-12, 10, NmfInit::Nndsvd);
1077 let res = nmf_hals(&sparse_in, k, &opts, 0).unwrap();
1078
1079 let rel = res.final_loss / sq_frob_v;
1080 assert!(rel < 1e-2, "rel loss {rel}");
1081 }
1082
1083 #[test]
1085 fn hals_converged_flag_set_when_tol_met() {
1086 let (m, n, k) = (15, 10, 2);
1087 let w_true: Mat<f64> = Mat::from_fn(m, k, |i, j| ((i + 2 * j) % 4 + 1) as f64);
1088 let h_true: Mat<f64> = Mat::from_fn(k, n, |i, j| ((2 * i + j) % 3 + 1) as f64);
1089 let v_mat: Mat<f64> = Mat::from_fn(m, n, |i, j| {
1090 (0..k).map(|r| w_true[(i, r)] * h_true[(r, j)]).sum()
1091 });
1092 let input = DenseInput::new(v_mat.as_ref()).unwrap();
1093 let opts = HalsOpts::<f64>::new(1000, 1e-6, 1e-12, 5, NmfInit::Nndsvd);
1094 let res = nmf_hals(&input, k, &opts, 0).unwrap();
1095 assert!(res.converged);
1096 assert!(res.n_iter < 1000);
1097 }
1098
1099 #[test]
1101 fn stabilised_nmf_best_idx_is_min_loss() {
1102 let (m, n, k) = (10, 8, 2);
1103 let n_runs = 5;
1104 let v_mat: Mat<f64> = Mat::from_fn(m, n, |i, j| (i * 2 + j + 1) as f64);
1105 let input = DenseInput::new(v_mat.as_ref()).unwrap();
1106 let opts = HalsOpts::<f64>::new(100, 1e-8, 1e-12, 10, NmfInit::Nndsvd);
1107
1108 let res = stabilised_nmf(&input, k, n_runs, 0, &opts, 0).unwrap();
1109
1110 let best_loss = res.losses[res.best_idx];
1111 for &loss in &res.losses {
1112 assert!(loss >= best_loss - 1e-12);
1113 }
1114 }
1115
1116 #[test]
1118 fn stabilised_nmf_deterministic_with_same_seed() {
1119 let (m, n, k) = (10, 8, 2);
1120 let v_mat: Mat<f64> = Mat::from_fn(m, n, |i, j| (i + j + 1) as f64);
1121 let input = DenseInput::new(v_mat.as_ref()).unwrap();
1122 let opts = HalsOpts::<f64>::new(50, 1e-6, 1e-12, 10, NmfInit::Nndsvd);
1123
1124 let res_a = stabilised_nmf(&input, k, 3, 123, &opts, 0).unwrap();
1125 let res_b = stabilised_nmf(&input, k, 3, 123, &opts, 0).unwrap();
1126
1127 assert_eq!(res_a.losses.len(), res_b.losses.len());
1128 for run in 0..res_a.losses.len() {
1129 assert!((res_a.losses[run] - res_b.losses[run]).abs() < 1e-9);
1130 }
1131 for col in 0..res_a.w_all.ncols() {
1132 for row in 0..res_a.w_all.nrows() {
1133 assert!((res_a.w_all[(row, col)] - res_b.w_all[(row, col)]).abs() < 1e-9);
1134 }
1135 }
1136 }
1137
1138 #[test]
1140 fn stabilised_nmf_recovers_rank_k() {
1141 let (m, n, k) = (25, 18, 2);
1142 let w_true: Mat<f64> = Mat::from_fn(m, k, |i, j| ((i * 3 + j) % 4 + 1) as f64);
1143 let h_true: Mat<f64> = Mat::from_fn(k, n, |i, j| ((i * 2 + j) % 3 + 1) as f64);
1144 let v_mat: Mat<f64> = Mat::from_fn(m, n, |i, j| {
1145 let mut s = 0.0;
1146 for r in 0..k {
1147 s += w_true[(i, r)] * h_true[(r, j)];
1148 }
1149 s
1150 });
1151
1152 let mut sq_frob_v = 0.0;
1153 for i in 0..m {
1154 for j in 0..n {
1155 sq_frob_v += v_mat[(i, j)].powi(2);
1156 }
1157 }
1158
1159 let input = DenseInput::new(v_mat.as_ref()).unwrap();
1160 let opts = HalsOpts::<f64>::new(300, 1e-8, 1e-12, 10, NmfInit::Nndsvd);
1161
1162 let res = stabilised_nmf(&input, k, 5, 0, &opts, 0).unwrap();
1163
1164 let rel = res.losses[res.best_idx] / sq_frob_v;
1165 assert!(rel < 1e-2, "best rel loss {rel}");
1166 }
1167
1168 #[test]
1170 fn stabilised_nmf_ignores_init_field() {
1171 let (m, n, k) = (10, 8, 2);
1174 let v_mat: Mat<f64> = Mat::from_fn(m, n, |i, j| (i + j + 1) as f64);
1175 let input = DenseInput::new(v_mat.as_ref()).unwrap();
1176 let opts = HalsOpts::<f64>::new(20, 1e-3, 1e-12, 10, NmfInit::Nndsvd);
1177
1178 let res = stabilised_nmf(&input, k, 3, 0, &opts, 0).unwrap();
1179
1180 let mut max_diff = 0.0_f64;
1182 for col in 0..k {
1183 for row in 0..m {
1184 let a = res.w_all[(row, col)];
1185 let b = res.w_all[(row, col + k)];
1186 let d = (a - b).abs();
1187 if d > max_diff {
1188 max_diff = d;
1189 }
1190 }
1191 }
1192 assert!(max_diff > 1e-6, "init field was not overridden to Random");
1193 }
1194}