1pub mod kernels;
12pub mod masked;
13
14use crate::types::SvdFloat;
15use ndarray::{Array1, ArrayView1, ArrayView2, ArrayViewMut2};
16use rayon::prelude::*;
17use sprs::{CsMatI, CsMatViewI, SpIndex};
18
19pub use kernels::DEFAULT_SCRATCH_BUDGET;
20pub use masked::MaskedCsMat;
21
22pub type SvdMat<T, I = u32, Iptr = u64> = CsMatI<T, I, Iptr>;
24pub type SvdMatView<'a, T, I = u32, Iptr = u64> = CsMatViewI<'a, T, I, Iptr>;
26
27pub trait SparseMat<T: SvdFloat>: Sync {
35 fn rows(&self) -> usize;
36 fn cols(&self) -> usize;
37 fn nnz(&self) -> usize;
38
39 fn mul_vec(&self, x: &[T], y: &mut [T], trans: bool);
44
45 fn squared_frobenius(&self) -> f64;
48
49 fn centered_squared_frobenius(&self, means: ArrayView1<T>) -> f64 {
58 let shift: f64 = means.iter().map(|&m| m.to_f64() * m.to_f64()).sum();
59 (self.squared_frobenius() - self.rows() as f64 * shift).max(0.0)
60 }
61}
62
63pub fn total_squared_norm<T: SvdFloat, M: SparseMat<T> + ?Sized>(
66 a: &M,
67 means: Option<ArrayView1<T>>,
68) -> f64 {
69 match means {
70 Some(m) => a.centered_squared_frobenius(m),
71 None => a.squared_frobenius(),
72 }
73}
74
75#[inline]
78fn centered_from_parts<T: SvdFloat>(
79 stored: f64,
80 col_nnz: &[usize],
81 means: ArrayView1<T>,
82 rows: usize,
83) -> f64 {
84 let missing: f64 = col_nnz
85 .iter()
86 .zip(means.iter())
87 .map(|(&n, &m)| {
88 let m = m.to_f64();
89 (rows - n) as f64 * m * m
90 })
91 .sum();
92 (stored + missing).max(0.0)
93}
94
95pub trait SparseMatDense<T: SvdFloat>: SparseMat<T> {
102 fn mul_dense(&self, rhs: ArrayView2<T>, out: ArrayViewMut2<T>, trans: bool);
106
107 fn col_means(&self) -> Array1<T> {
112 let m = self.rows();
113 let ones = vec![T::one(); m];
114 let mut sums = vec![T::zero(); self.cols()];
115 self.mul_vec(&ones, &mut sums, true);
116 let scale = if m == 0 {
117 T::zero()
118 } else {
119 T::one() / T::from_f64_val(m as f64)
120 };
121 Array1::from_vec(sums) * scale
122 }
123
124 fn mul_dense_centered(
133 &self,
134 rhs: ArrayView2<T>,
135 mut out: ArrayViewMut2<T>,
136 trans: bool,
137 means: ArrayView1<T>,
138 ) {
139 assert_eq!(
140 means.len(),
141 self.cols(),
142 "mul_dense_centered: means must have length cols()"
143 );
144 self.mul_dense(rhs, out.view_mut(), trans);
145 apply_centering(rhs, out, trans, means);
146 }
147}
148
149pub fn apply_centering<T: SvdFloat>(
152 rhs: ArrayView2<T>,
153 mut out: ArrayViewMut2<T>,
154 trans: bool,
155 means: ArrayView1<T>,
156) {
157 let k = rhs.ncols();
158 if k == 0 {
159 return;
160 }
161 if !trans {
162 debug_assert_eq!(rhs.nrows(), means.len());
164 let mut corr = vec![T::zero(); k];
165 for (j, &mj) in means.iter().enumerate() {
166 if mj.is_zero() {
167 continue;
168 }
169 for (c, cv) in corr.iter_mut().enumerate() {
170 *cv += mj * rhs[[j, c]];
171 }
172 }
173 for mut orow in out.rows_mut() {
174 for (o, &c) in orow.iter_mut().zip(corr.iter()) {
175 *o -= c;
176 }
177 }
178 } else {
179 let mut colsum = vec![T::zero(); k];
181 for row in rhs.rows() {
182 for (c, cv) in colsum.iter_mut().enumerate() {
183 *cv += row[c];
184 }
185 }
186 debug_assert_eq!(out.nrows(), means.len());
187 for (j, mut orow) in out.rows_mut().into_iter().enumerate() {
188 let mj = means[j];
189 if mj.is_zero() {
190 continue;
191 }
192 for (o, &cs) in orow.iter_mut().zip(colsum.iter()) {
193 *o -= mj * cs;
194 }
195 }
196 }
197}
198
199#[inline]
204fn csr_view<T, I: SpIndex, Iptr: SpIndex>(
205 m: &CsMatI<T, I, Iptr>,
206) -> (CsMatViewI<'_, T, I, Iptr>, bool) {
207 if m.is_csr() {
208 (m.view(), false)
209 } else {
210 (m.transpose_view(), true)
211 }
212}
213
214impl<T, I, Iptr> SparseMat<T> for CsMatI<T, I, Iptr>
215where
216 T: SvdFloat,
217 I: SpIndex,
218 Iptr: SpIndex,
219{
220 fn rows(&self) -> usize {
221 CsMatI::rows(self)
222 }
223 fn cols(&self) -> usize {
224 CsMatI::cols(self)
225 }
226 fn nnz(&self) -> usize {
227 CsMatI::nnz(self)
228 }
229
230 fn mul_vec(&self, x: &[T], y: &mut [T], trans: bool) {
231 let (view, flipped) = csr_view(self);
232 if trans ^ flipped {
233 kernels::scatter_mul_vec(view, x, y);
234 } else {
235 kernels::gather_mul_vec(view, x, y);
236 }
237 }
238
239 fn squared_frobenius(&self) -> f64 {
240 self.data()
242 .par_iter()
243 .map(|&v| {
244 let x = v.to_f64();
245 x * x
246 })
247 .sum()
248 }
249
250 fn centered_squared_frobenius(&self, means: ArrayView1<T>) -> f64 {
251 assert_eq!(
252 means.len(),
253 SparseMat::cols(self),
254 "centered_squared_frobenius: means must have length cols()"
255 );
256 let rows = SparseMat::rows(self);
257 let cols = SparseMat::cols(self);
258
259 if self.is_csc() {
260 let total: f64 = (0..cols)
262 .into_par_iter()
263 .map(|j| {
264 let m = means[j].to_f64();
265 let (sum, n) = self.outer_view(j).map_or((0.0, 0), |col| {
266 (
267 col.iter()
268 .map(|(_, &v)| {
269 let e = v.to_f64() - m;
270 e * e
271 })
272 .sum::<f64>(),
273 col.nnz(),
274 )
275 });
276 sum + (rows - n) as f64 * m * m
277 })
278 .sum();
279 return total.max(0.0);
280 }
281
282 let mut col_nnz = vec![0usize; cols];
284 for j in self.indices() {
285 col_nnz[j.index()] += 1;
286 }
287 let stored: f64 = (0..rows)
288 .into_par_iter()
289 .map(|i| {
290 self.outer_view(i).map_or(0.0, |row| {
291 row.iter()
292 .map(|(j, &v)| {
293 let e = v.to_f64() - means[j].to_f64();
294 e * e
295 })
296 .sum()
297 })
298 })
299 .sum();
300 centered_from_parts(stored, &col_nnz, means, rows)
301 }
302}
303
304impl<T, I, Iptr> SparseMatDense<T> for CsMatI<T, I, Iptr>
305where
306 T: SvdFloat,
307 I: SpIndex,
308 Iptr: SpIndex,
309{
310 fn mul_dense(&self, rhs: ArrayView2<T>, out: ArrayViewMut2<T>, trans: bool) {
311 let (view, flipped) = csr_view(self);
312 if trans ^ flipped {
313 kernels::scatter_mul(view, rhs, out, DEFAULT_SCRATCH_BUDGET);
314 } else {
315 kernels::gather_mul(view, rhs, out);
316 }
317 }
318}
319
320impl<T: SvdFloat, M: SparseMat<T> + ?Sized> SparseMat<T> for &M {
322 fn rows(&self) -> usize {
323 (**self).rows()
324 }
325 fn cols(&self) -> usize {
326 (**self).cols()
327 }
328 fn nnz(&self) -> usize {
329 (**self).nnz()
330 }
331 fn mul_vec(&self, x: &[T], y: &mut [T], trans: bool) {
332 (**self).mul_vec(x, y, trans)
333 }
334 fn squared_frobenius(&self) -> f64 {
335 (**self).squared_frobenius()
336 }
337 fn centered_squared_frobenius(&self, means: ArrayView1<T>) -> f64 {
338 (**self).centered_squared_frobenius(means)
339 }
340}
341
342impl<T: SvdFloat, M: SparseMatDense<T> + ?Sized> SparseMatDense<T> for &M {
343 fn mul_dense(&self, rhs: ArrayView2<T>, out: ArrayViewMut2<T>, trans: bool) {
344 (**self).mul_dense(rhs, out, trans)
345 }
346 fn col_means(&self) -> Array1<T> {
347 (**self).col_means()
348 }
349 fn mul_dense_centered(
350 &self,
351 rhs: ArrayView2<T>,
352 out: ArrayViewMut2<T>,
353 trans: bool,
354 means: ArrayView1<T>,
355 ) {
356 (**self).mul_dense_centered(rhs, out, trans, means)
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363 use ndarray::{arr2, Array2};
364 use sprs::TriMatI;
365
366 fn tiny_csr() -> SvdMat<f64> {
367 let mut t = TriMatI::<f64, u32>::new((4, 3));
368 t.add_triplet(0, 0, 1.0);
369 t.add_triplet(0, 2, 2.0);
370 t.add_triplet(1, 1, 3.0);
371 t.add_triplet(2, 0, 4.0);
372 t.add_triplet(2, 2, 5.0);
373 t.add_triplet(3, 1, 6.0);
374 t.to_csr::<u64>()
375 }
376
377 use crate::testing::dense_of;
378
379 #[test]
382 fn csr_and_csc_agree() {
383 let csr = tiny_csr();
384 let csc = csr.to_other_storage();
385 assert!(csr.is_csr() && csc.is_csc());
386 let d = dense_of(&csr);
387
388 let rhs = arr2(&[[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]);
389 let mut a = Array2::zeros((4, 2));
390 let mut b = Array2::zeros((4, 2));
391 SparseMatDense::mul_dense(&csr, rhs.view(), a.view_mut(), false);
392 SparseMatDense::mul_dense(&csc, rhs.view(), b.view_mut(), false);
393 assert_eq!(a, d.dot(&rhs));
394 assert_eq!(b, d.dot(&rhs));
395
396 let rhs_t = arr2(&[[1.0], [2.0], [3.0], [4.0]]);
397 let mut at = Array2::zeros((3, 1));
398 let mut bt = Array2::zeros((3, 1));
399 SparseMatDense::mul_dense(&csr, rhs_t.view(), at.view_mut(), true);
400 SparseMatDense::mul_dense(&csc, rhs_t.view(), bt.view_mut(), true);
401 assert_eq!(at, d.t().dot(&rhs_t));
402 assert_eq!(bt, d.t().dot(&rhs_t));
403 }
404
405 #[test]
406 fn col_means_match_dense() {
407 let a = tiny_csr();
408 let d = dense_of(&a);
409 let got = SparseMatDense::col_means(&a);
410 let want = d.mean_axis(ndarray::Axis(0)).unwrap();
411 for (g, w) in got.iter().zip(want.iter()) {
412 approx::assert_relative_eq!(g, w, max_relative = 1e-14);
413 }
414 let got_csc = SparseMatDense::col_means(&a.to_other_storage());
416 for (g, w) in got_csc.iter().zip(want.iter()) {
417 approx::assert_relative_eq!(g, w, max_relative = 1e-14);
418 }
419 }
420
421 #[test]
423 fn centering_matches_explicit_dense_centering() {
424 let a = tiny_csr();
425 let d = dense_of(&a);
426 let means = SparseMatDense::col_means(&a);
427 let centered = &d - &means.view().insert_axis(ndarray::Axis(0));
428
429 let rhs = arr2(&[[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]);
430 let mut out = Array2::zeros((4, 2));
431 SparseMatDense::mul_dense_centered(&a, rhs.view(), out.view_mut(), false, means.view());
432 let want = centered.dot(&rhs);
433 for (g, w) in out.iter().zip(want.iter()) {
434 approx::assert_relative_eq!(g, w, max_relative = 1e-12);
435 }
436
437 let rhs_t = arr2(&[[1.0, 0.5], [2.0, 1.5], [3.0, 2.5], [4.0, 3.5]]);
438 let mut out_t = Array2::zeros((3, 2));
439 SparseMatDense::mul_dense_centered(&a, rhs_t.view(), out_t.view_mut(), true, means.view());
440 let want_t = centered.t().dot(&rhs_t);
441 for (g, w) in out_t.iter().zip(want_t.iter()) {
442 approx::assert_relative_eq!(g, w, max_relative = 1e-12);
443 }
444 }
445
446 #[test]
447 fn squared_frobenius_matches_dense_and_is_storage_agnostic() {
448 let a = tiny_csr();
449 let d = dense_of(&a);
450 let want: f64 = d.iter().map(|&v| v * v).sum();
451 approx::assert_relative_eq!(SparseMat::squared_frobenius(&a), want, max_relative = 1e-14);
452 approx::assert_relative_eq!(
454 SparseMat::squared_frobenius(&a.to_other_storage()),
455 want,
456 max_relative = 1e-14
457 );
458 }
459
460 #[test]
462 fn centered_squared_frobenius_matches_explicit_centering() {
463 let a = tiny_csr();
464 let d = dense_of(&a);
465 let means = SparseMatDense::col_means(&a);
466 let centered = &d - &means.view().insert_axis(ndarray::Axis(0));
467 let want: f64 = centered.iter().map(|&v| v * v).sum();
468
469 approx::assert_relative_eq!(
470 SparseMat::centered_squared_frobenius(&a, means.view()),
471 want,
472 max_relative = 1e-12
473 );
474 approx::assert_relative_eq!(
476 SparseMat::centered_squared_frobenius(&a.to_other_storage(), means.view()),
477 want,
478 max_relative = 1e-12
479 );
480 }
481
482 #[test]
484 fn centering_never_increases_the_norm() {
485 let a = tiny_csr();
486 let means = SparseMatDense::col_means(&a);
487 let raw = SparseMat::squared_frobenius(&a);
488 let centered = SparseMat::centered_squared_frobenius(&a, means.view());
489 assert!(centered >= 0.0);
490 assert!(centered <= raw);
491 }
492
493 #[test]
496 fn centered_norm_survives_a_large_column_offset() {
497 for offset in [0.0f32, 10.0, 100.0, 1000.0] {
498 let (rows, cols) = (200usize, 20usize);
499 let mut t = sprs::TriMatI::<f32, u32>::new((rows, cols));
500 let mut dense = Array2::<f64>::zeros((rows, cols));
501 for i in 0..rows {
502 for j in 0..cols {
503 let v = offset + (((i * 7 + j * 3) % 11) as f32 - 5.0) * 0.1;
504 t.add_triplet(i, j, v);
505 dense[[i, j]] = v as f64;
506 }
507 }
508 let a: SvdMat<f32> = t.to_csr::<u64>();
509 let means = SparseMatDense::col_means(&a);
510
511 let mut want = 0.0f64;
514 for i in 0..rows {
515 for j in 0..cols {
516 let e = dense[[i, j]] - means[j] as f64;
517 want += e * e;
518 }
519 }
520
521 let got = SparseMat::centered_squared_frobenius(&a, means.view());
522 let rel = (got - want).abs() / want;
523 assert!(
524 rel < 1e-6,
525 "offset {offset}: centered norm {got:.6e} vs {want:.6e} (rel {rel:.3e})"
526 );
527 }
528 }
529
530 #[test]
531 fn mul_vec_matches_dense_both_directions() {
532 let a = tiny_csr();
533 let d = dense_of(&a);
534 let mut y = vec![0.0; 4];
535 SparseMat::mul_vec(&a, &[1.0, 2.0, 3.0], &mut y, false);
536 assert_eq!(y, d.dot(&ndarray::arr1(&[1.0, 2.0, 3.0])).to_vec());
537
538 let mut yt = vec![0.0; 3];
539 SparseMat::mul_vec(&a, &[1.0, 2.0, 3.0, 4.0], &mut yt, true);
540 assert_eq!(
541 yt,
542 d.t().dot(&ndarray::arr1(&[1.0, 2.0, 3.0, 4.0])).to_vec()
543 );
544 }
545}