1use faer::{Mat, MatRef};
6use num_traits::{Float, ToPrimitive};
7use rayon::prelude::*;
8
9use super::NmfInput;
10use crate::prelude::*;
11use bixverse_core::core::math::pca_svd::SvdResults;
12use bixverse_core::core::math::sparse::sparse_svd_lanczos;
13use bixverse_core::core::math::vector_helpers::sum_sq_f64;
14
15fn cast_and_validate<T, S>(src: &[T]) -> Result<Vec<S>, MethodsErrors>
34where
35 T: Copy + Into<S>,
36 S: Float,
37{
38 let mut out = Vec::with_capacity(src.len());
39 for &v in src {
40 let s: S = v.into();
41 if !s.is_finite() {
42 return Err(MethodsErrors::NmfNonFinite);
43 }
44 if s < S::zero() {
45 return Err(MethodsErrors::NmfNonNegativeViolated);
46 }
47 out.push(s);
48 }
49 Ok(out)
50}
51
52fn compute_sq_frob<S, F>(data: &[S]) -> F
66where
67 S: BixverseNumeric + ToPrimitive,
68 F: BixverseFloat,
69{
70 F::from_f64(sum_sq_f64(data)).unwrap()
71}
72
73fn build_pair<S, T, U>(
90 source: &CompressedSparseData2<T, U>,
91 other: CompressedSparseData2<T, U>,
92 source_cast: Vec<S>,
93 other_cast: Vec<S>,
94 shape: (usize, usize),
95) -> (CompressedSparseData2<S>, CompressedSparseData2<S>)
96where
97 S: Clone,
98 T: Clone,
99 U: Clone,
100{
101 match source.cs_type {
102 CompressedSparseFormat::Csr => {
103 let csr = CompressedSparseData2 {
104 data: source_cast,
105 indices: source.indices.clone(),
106 indptr: source.indptr.clone(),
107 cs_type: CompressedSparseFormat::Csr,
108 data_2: None,
109 shape,
110 };
111 let csc = CompressedSparseData2 {
112 data: other_cast,
113 indices: other.indices,
114 indptr: other.indptr,
115 cs_type: CompressedSparseFormat::Csc,
116 data_2: None,
117 shape,
118 };
119 (csr, csc)
120 }
121 CompressedSparseFormat::Csc => {
122 let csc = CompressedSparseData2 {
123 data: source_cast,
124 indices: source.indices.clone(),
125 indptr: source.indptr.clone(),
126 cs_type: CompressedSparseFormat::Csc,
127 data_2: None,
128 shape,
129 };
130 let csr = CompressedSparseData2 {
131 data: other_cast,
132 indices: other.indices,
133 indptr: other.indptr,
134 cs_type: CompressedSparseFormat::Csr,
135 data_2: None,
136 shape,
137 };
138 (csr, csc)
139 }
140 }
141}
142
143fn spmm_csc_wt_v<S, F>(csc: &CompressedSparseData2<S>, w: MatRef<F>, out: &mut Mat<F>)
154where
155 S: BixverseNumeric + Into<F> + Sync,
156 F: BixverseFloat + Send + Sync,
157{
158 let n = csc.shape.1;
159 let k = w.ncols();
160
161 let mut wt = Mat::<F>::zeros(k, w.nrows());
163 wt.par_col_iter_mut().enumerate().for_each(|(i, mut col)| {
164 for r in 0..k {
165 col[r] = w[(i, r)];
166 }
167 });
168
169 let out_ptr = out.as_ptr_mut() as usize;
170 let out_row_stride = out.row_stride();
171 let out_col_stride = out.col_stride();
172
173 (0..n).into_par_iter().for_each_init(
174 || vec![F::zero(); k],
175 |acc, j| {
176 acc.fill(F::zero());
177
178 let start = csc.indptr[j] as usize;
179 let end = csc.indptr[j + 1] as usize;
180 for idx in start..end {
181 let i = csc.indices[idx] as usize;
182 let val_f: F = csc.data[idx].into();
183 for (a, &w_ir) in acc.iter_mut().zip(wt.col_as_slice(i)) {
184 *a += val_f * w_ir;
185 }
186 }
187
188 let base = out_ptr as *mut F;
189 for r in 0..k {
190 unsafe {
191 let ptr =
192 base.offset(r as isize * out_row_stride + j as isize * out_col_stride);
193 *ptr = acc[r];
194 }
195 }
196 },
197 );
198}
199
200fn spmm_csr_v_ht<S, F>(csr: &CompressedSparseData2<S>, h: MatRef<F>, out: &mut Mat<F>)
211where
212 S: BixverseNumeric + Into<F> + Sync,
213 F: BixverseFloat + Send + Sync,
214{
215 let m = csr.shape.0;
216 let k = h.nrows();
217
218 let out_ptr = out.as_ptr_mut() as usize;
219 let out_row_stride = out.row_stride();
220 let out_col_stride = out.col_stride();
221
222 (0..m).into_par_iter().for_each_init(
223 || vec![F::zero(); k],
224 |acc, i| {
225 acc.fill(F::zero());
226
227 let start = csr.indptr[i] as usize;
228 let end = csr.indptr[i + 1] as usize;
229 for idx in start..end {
230 let j = csr.indices[idx] as usize;
231 let val_f: F = csr.data[idx].into();
232 for r in 0..k {
233 acc[r] += val_f * h[(r, j)];
234 }
235 }
236
237 let base = out_ptr as *mut F;
238 for r in 0..k {
239 unsafe {
240 let ptr =
241 base.offset(i as isize * out_row_stride + r as isize * out_col_stride);
242 *ptr = acc[r];
243 }
244 }
245 },
246 );
247}
248
249#[derive(Debug)]
258pub struct SparseInput<S, F>
259where
260 S: BixverseNumeric + Into<F> + Float,
261 F: BixverseFloat,
262{
263 csr: CompressedSparseData2<S>,
265 csc: CompressedSparseData2<S>,
267 sq_frob: F,
269 shape: (usize, usize),
271}
272
273impl<S, F> SparseInput<S, F>
274where
275 S: BixverseNumeric + BixverseSimd + Into<F> + Float + Clone,
276 F: BixverseFloat + BixverseSimd + std::iter::Sum,
277{
278 pub fn from_primary<T, U>(matrix: &CompressedSparseData2<T, U>) -> Result<Self, MethodsErrors>
292 where
293 T: BixverseNumeric + Into<S>,
294 U: BixverseNumeric,
295 {
296 let shape = matrix.shape;
297 let other = matrix.transform_single_layer(false)?;
298
299 let primary: Vec<S> = cast_and_validate::<T, S>(&matrix.data)?;
300 let secondary: Vec<S> = other.data.iter().map(|&v| v.into()).collect();
301
302 let (csr, csc) = build_pair(matrix, other, primary, secondary, shape);
303 let sq_frob = compute_sq_frob::<S, F>(&csr.data);
304
305 Ok(Self {
306 csr,
307 csc,
308 sq_frob,
309 shape,
310 })
311 }
312
313 pub fn from_secondary<T, U>(matrix: &CompressedSparseData2<T, U>) -> Result<Self, MethodsErrors>
329 where
330 T: BixverseNumeric,
331 U: BixverseNumeric + Into<S>,
332 {
333 let shape = matrix.shape;
334 let other = matrix.transform_single_layer(true)?;
335
336 let m_data2 = matrix
337 .data_2
338 .as_ref()
339 .ok_or(CoreErrors::Data2NotAvailable)?;
340 let o_data2 = other.data_2.as_ref().ok_or(CoreErrors::Data2NotAvailable)?;
341
342 let primary: Vec<S> = cast_and_validate::<U, S>(m_data2)?;
343 let secondary: Vec<S> = o_data2.iter().map(|&v| v.into()).collect();
344
345 let (csr, csc) = build_pair(matrix, other, primary, secondary, shape);
346 let sq_frob = compute_sq_frob::<S, F>(&csr.data);
347
348 Ok(Self {
349 csr,
350 csc,
351 sq_frob,
352 shape,
353 })
354 }
355
356 pub fn csr(&self) -> &CompressedSparseData2<S> {
365 &self.csr
366 }
367
368 pub fn csc(&self) -> &CompressedSparseData2<S> {
376 &self.csc
377 }
378}
379
380impl<S, F> NmfInput<F> for SparseInput<S, F>
382where
383 S: BixverseNumeric + BixverseSimd + Into<F> + Float + Clone + Sync,
384 F: BixverseFloat + BixverseSimd + Send + Sync + std::iter::Sum,
385{
386 fn shape(&self) -> (usize, usize) {
387 self.shape
388 }
389
390 fn sq_frob(&self) -> F {
391 self.sq_frob
392 }
393
394 fn wt_v(&self, w: MatRef<F>, out: &mut Mat<F>) {
395 spmm_csc_wt_v::<S, F>(&self.csc, w, out);
396 }
397
398 fn v_ht(&self, h: MatRef<F>, out: &mut Mat<F>) {
399 spmm_csr_v_ht::<S, F>(&self.csr, h, out);
400 }
401
402 fn top_k_svd(&self, k: usize) -> Result<SvdResults<F>, MethodsErrors> {
403 sparse_svd_lanczos::<S, S, F>(&self.csr, k, 42, false, None, None, None).map_err(Into::into)
404 }
405}
406
407#[cfg(test)]
412mod tests {
413 use super::*;
414 use crate::methods::nmf_hals::dense::DenseInput;
415 use faer::Mat;
416
417 fn fixture_csr() -> (CompressedSparseData2<f64, f64>, Mat<f64>) {
419 let data = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
420 let indices: Vec<u32> = vec![0, 2, 1, 3, 0, 3];
421 let indptr: Vec<u32> = vec![0, 2, 4, 6];
422 let csr =
423 CompressedSparseData2::<f64, f64>::new_csr(&data, &indices, &indptr, None, (3, 4));
424 let dense = Mat::from_fn(3, 4, |i, j| match (i, j) {
425 (0, 0) => 1.0,
426 (0, 2) => 2.0,
427 (1, 1) => 3.0,
428 (1, 3) => 4.0,
429 (2, 0) => 5.0,
430 (2, 3) => 6.0,
431 _ => 0.0,
432 });
433 (csr, dense)
434 }
435
436 #[test]
441 fn sq_frob_survives_a_swamped_accumulator() {
442 let n_ones = 1_000_000usize;
445 let mut data = vec![1e4f32];
446 data.extend(std::iter::repeat_n(1.0f32, n_ones));
447 let nnz = data.len();
448 let indices: Vec<u32> = (0..nnz as u32).collect();
449 let indptr: Vec<u32> = vec![0, nnz as u32];
450 let csr =
451 CompressedSparseData2::<f32, f32>::new_csr(&data, &indices, &indptr, None, (1, nnz));
452
453 let sparse_in: SparseInput<f32, f32> = SparseInput::from_primary(&csr).unwrap();
454 let exact = 1e8 + n_ones as f64;
455 let got = sparse_in.sq_frob() as f64;
456 assert!(
457 (got - exact).abs() <= 1e-5 * exact,
458 "sq_frob is off: got {got}, exact {exact}"
459 );
460
461 let naive = data.iter().fold(0f32, |acc, &v| acc + v * v) as f64;
462 assert!(
463 (naive - exact).abs() > 0.005 * exact,
464 "the f32 reference was supposed to be visibly wrong, so this test proves nothing"
465 );
466 }
467
468 #[test]
470 fn shape_and_sq_frob_match_dense() {
471 let (csr, dense) = fixture_csr();
472 let sparse_in: SparseInput<f64, f64> = SparseInput::from_primary(&csr).unwrap();
473 let dense_in = DenseInput::new(dense.as_ref()).unwrap();
474 assert_eq!(sparse_in.shape(), dense_in.shape());
475 assert!((sparse_in.sq_frob() - dense_in.sq_frob()).abs() < 1e-12);
476 }
477
478 #[test]
480 fn rejects_negative() {
481 let data = vec![1.0, -2.0];
482 let indices: Vec<u32> = vec![0, 1];
483 let indptr: Vec<u32> = vec![0, 2];
484 let csr =
485 CompressedSparseData2::<f64, f64>::new_csr(&data, &indices, &indptr, None, (1, 2));
486 let err = SparseInput::<f64, f64>::from_primary(&csr).unwrap_err();
487 assert!(matches!(err, MethodsErrors::NmfNonNegativeViolated));
488 }
489
490 #[test]
492 fn rejects_non_finite() {
493 let data = vec![1.0, f64::NAN];
494 let indices: Vec<u32> = vec![0, 1];
495 let indptr: Vec<u32> = vec![0, 2];
496 let csr =
497 CompressedSparseData2::<f64, f64>::new_csr(&data, &indices, &indptr, None, (1, 2));
498 let err = SparseInput::<f64, f64>::from_primary(&csr).unwrap_err();
499 assert!(matches!(err, MethodsErrors::NmfNonFinite));
500 }
501
502 #[test]
504 fn wt_v_matches_dense() {
505 let (csr, dense) = fixture_csr();
506 let k = 2;
507 let w: Mat<f64> = Mat::from_fn(3, k, |i, j| (i + j + 1) as f64);
508 let sparse_in: SparseInput<f64, f64> = SparseInput::from_primary(&csr).unwrap();
509 let dense_in = DenseInput::new(dense.as_ref()).unwrap();
510 let mut out_s = Mat::<f64>::zeros(k, 4);
511 let mut out_d = Mat::<f64>::zeros(k, 4);
512 sparse_in.wt_v(w.as_ref(), &mut out_s);
513 dense_in.wt_v(w.as_ref(), &mut out_d);
514 for r in 0..k {
515 for j in 0..4 {
516 assert!((out_s[(r, j)] - out_d[(r, j)]).abs() < 1e-9);
517 }
518 }
519 }
520
521 #[test]
523 fn v_ht_matches_dense() {
524 let (csr, dense) = fixture_csr();
525 let k = 2;
526 let h: Mat<f64> = Mat::from_fn(k, 4, |i, j| (i + j + 1) as f64);
527 let sparse_in: SparseInput<f64, f64> = SparseInput::from_primary(&csr).unwrap();
528 let dense_in = DenseInput::new(dense.as_ref()).unwrap();
529 let mut out_s = Mat::<f64>::zeros(3, k);
530 let mut out_d = Mat::<f64>::zeros(3, k);
531 sparse_in.v_ht(h.as_ref(), &mut out_s);
532 dense_in.v_ht(h.as_ref(), &mut out_d);
533 for i in 0..3 {
534 for r in 0..k {
535 assert!((out_s[(i, r)] - out_d[(i, r)]).abs() < 1e-9);
536 }
537 }
538 }
539
540 #[test]
542 fn csc_input_produces_same_products() {
543 let data = vec![1.0, 5.0, 3.0, 2.0, 4.0, 6.0];
545 let indices: Vec<u32> = vec![0, 2, 1, 0, 1, 2];
546 let indptr: Vec<u32> = vec![0, 2, 3, 4, 6];
547 let csc =
548 CompressedSparseData2::<f64, f64>::new_csc(&data, &indices, &indptr, None, (3, 4));
549 let dense = Mat::from_fn(3, 4, |i, j| match (i, j) {
550 (0, 0) => 1.0,
551 (0, 2) => 2.0,
552 (1, 1) => 3.0,
553 (1, 3) => 4.0,
554 (2, 0) => 5.0,
555 (2, 3) => 6.0,
556 _ => 0.0,
557 });
558
559 let sparse_in: SparseInput<f64, f64> = SparseInput::from_primary(&csc).unwrap();
560 let dense_in = DenseInput::new(dense.as_ref()).unwrap();
561
562 let k = 2;
563 let w: Mat<f64> = Mat::from_fn(3, k, |i, j| (i + j + 1) as f64);
564 let mut out_s = Mat::<f64>::zeros(k, 4);
565 let mut out_d = Mat::<f64>::zeros(k, 4);
566 sparse_in.wt_v(w.as_ref(), &mut out_s);
567 dense_in.wt_v(w.as_ref(), &mut out_d);
568 for r in 0..k {
569 for j in 0..4 {
570 assert!((out_s[(r, j)] - out_d[(r, j)]).abs() < 1e-9);
571 }
572 }
573 }
574
575 #[test]
577 fn from_secondary_uses_data_2() {
578 let data = vec![10.0, 20.0, 30.0, 40.0, 50.0, 60.0];
579 let data_2 = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
580 let indices: Vec<u32> = vec![0, 2, 1, 3, 0, 3];
581 let indptr: Vec<u32> = vec![0, 2, 4, 6];
582 let csr = CompressedSparseData2::<f64, f64>::new_csr(
583 &data,
584 &indices,
585 &indptr,
586 Some(&data_2),
587 (3, 4),
588 );
589
590 let sparse_in: SparseInput<f64, f64> = SparseInput::from_secondary(&csr).unwrap();
591 let expected: f64 = data_2.iter().map(|v| v * v).sum();
593 assert!((sparse_in.sq_frob() - expected).abs() < 1e-12);
594 }
595
596 #[test]
598 fn top_k_svd_shapes() {
599 let (csr, _) = fixture_csr();
600 let sparse_in: SparseInput<f64, f64> = SparseInput::from_primary(&csr).unwrap();
601 let svd = sparse_in.top_k_svd(2).unwrap();
602 assert_eq!(svd.u.shape(), (3, 2));
603 assert_eq!(svd.v.shape(), (4, 2));
604 assert_eq!(svd.s.len(), 2);
605 assert!(svd.s[0] >= svd.s[1]);
606 }
607}