1use super::{apply_centering, SparseMat, SparseMatDense};
18use crate::types::SvdFloat;
19use ndarray::{Array1, ArrayView1, ArrayView2, ArrayViewMut2, Axis};
20use rayon::prelude::*;
21use sprs::{CsMatI, SpIndex};
22
23const EXCLUDED: usize = usize::MAX;
28
29pub struct MaskedCsMat<'a, T, I = u32, Iptr = u64>
46where
47 I: SpIndex,
48 Iptr: SpIndex,
49{
50 matrix: &'a CsMatI<T, I, Iptr>,
51 rows: Option<Vec<usize>>,
53 cols: Option<Vec<usize>>,
55 col_to_masked: Vec<usize>,
57 nnz: usize,
58}
59
60impl<'a, T, I, Iptr> MaskedCsMat<'a, T, I, Iptr>
61where
62 T: SvdFloat,
63 I: SpIndex,
64 Iptr: SpIndex,
65{
66 pub fn submatrix(
74 matrix: &'a CsMatI<T, I, Iptr>,
75 rows: Option<&[usize]>,
76 cols: Option<&[usize]>,
77 ) -> Self {
78 assert!(matrix.is_csr(), "MaskedCsMat requires a CSR matrix");
79
80 let rows = rows.map(|r| Self::normalise(r, matrix.rows(), "row"));
81 let cols = cols.map(|c| Self::normalise(c, matrix.cols(), "column"));
82
83 let col_to_masked = match &cols {
84 Some(c) => {
85 let mut table = vec![EXCLUDED; matrix.cols()];
86 for (masked, &orig) in c.iter().enumerate() {
87 table[orig] = masked;
88 }
89 table
90 }
91 None => Vec::new(),
92 };
93
94 let mut view = Self {
95 matrix,
96 rows,
97 cols,
98 col_to_masked,
99 nnz: 0,
100 };
101 view.nnz = view.count_nnz();
102 view
103 }
104
105 pub fn with_columns(matrix: &'a CsMatI<T, I, Iptr>, columns: &[usize]) -> Self {
107 Self::submatrix(matrix, None, Some(columns))
108 }
109
110 pub fn with_rows(matrix: &'a CsMatI<T, I, Iptr>, rows: &[usize]) -> Self {
112 Self::submatrix(matrix, Some(rows), None)
113 }
114
115 pub fn from_masks(
120 matrix: &'a CsMatI<T, I, Iptr>,
121 row_mask: Option<&[bool]>,
122 col_mask: Option<&[bool]>,
123 ) -> Self {
124 let to_indices = |mask: &[bool], n: usize, what: &str| {
125 assert_eq!(
126 mask.len(),
127 n,
128 "{what} mask has length {} but the matrix has {n} {what}s",
129 mask.len()
130 );
131 mask.iter()
132 .enumerate()
133 .filter_map(|(i, &keep)| keep.then_some(i))
134 .collect::<Vec<_>>()
135 };
136 let r = row_mask.map(|m| to_indices(m, matrix.rows(), "row"));
137 let c = col_mask.map(|m| to_indices(m, matrix.cols(), "column"));
138 Self::submatrix(matrix, r.as_deref(), c.as_deref())
139 }
140
141 fn normalise(indices: &[usize], limit: usize, what: &str) -> Vec<usize> {
143 let mut v = indices.to_vec();
144 v.sort_unstable();
145 v.dedup();
146 if let Some(&last) = v.last() {
147 assert!(
148 last < limit,
149 "{what} index {last} is out of bounds ({limit})"
150 );
151 }
152 v
153 }
154
155 fn count_nnz(&self) -> usize {
156 let masked = !self.col_to_masked.is_empty();
157 (0..self.rows_len())
158 .into_par_iter()
159 .map(|i| {
160 let orig = self.row_of(i);
161 self.matrix.outer_view(orig).map_or(0, |row| {
162 if masked {
163 row.indices()
164 .iter()
165 .filter(|j| self.col_to_masked[j.index()] != EXCLUDED)
166 .count()
167 } else {
168 row.nnz()
169 }
170 })
171 })
172 .sum()
173 }
174
175 #[inline]
176 fn rows_len(&self) -> usize {
177 self.rows.as_ref().map_or(self.matrix.rows(), |r| r.len())
178 }
179
180 #[inline]
182 fn row_of(&self, i: usize) -> usize {
183 match &self.rows {
184 Some(r) => r[i],
185 None => i,
186 }
187 }
188
189 #[inline]
191 fn masked_col(&self, j: usize) -> usize {
192 if self.col_to_masked.is_empty() {
193 j
194 } else {
195 self.col_to_masked[j]
196 }
197 }
198
199 pub fn selected_rows(&self) -> Option<&[usize]> {
201 self.rows.as_deref()
202 }
203
204 pub fn selected_columns(&self) -> Option<&[usize]> {
206 self.cols.as_deref()
207 }
208
209 pub fn is_identity(&self) -> bool {
212 self.rows.is_none() && self.cols.is_none()
213 }
214
215 pub fn uses_all_columns(&self) -> bool {
217 self.cols.is_none()
218 }
219
220 pub fn inner(&self) -> &'a CsMatI<T, I, Iptr> {
222 self.matrix
223 }
224
225 pub fn to_sparse(&self) -> CsMatI<T, I, Iptr> {
237 let (m, n) = (self.rows(), self.cols());
238 let mut indptr: Vec<Iptr> = Vec::with_capacity(m + 1);
239 let mut indices: Vec<I> = Vec::with_capacity(self.nnz);
240 let mut data: Vec<T> = Vec::with_capacity(self.nnz);
241
242 indptr.push(Iptr::from_usize(0));
243 for i in 0..m {
244 if let Some(row) = self.matrix.outer_view(self.row_of(i)) {
245 for (j, &v) in row.indices().iter().zip(row.data().iter()) {
246 let c = self.masked_col(j.index());
247 if c != EXCLUDED {
248 indices.push(I::from_usize(c));
252 data.push(v);
253 }
254 }
255 }
256 indptr.push(Iptr::from_usize(indices.len()));
257 }
258
259 CsMatI::new((m, n), indptr, indices, data)
260 }
261}
262
263impl<T, I, Iptr> SparseMat<T> for MaskedCsMat<'_, T, I, Iptr>
264where
265 T: SvdFloat,
266 I: SpIndex,
267 Iptr: SpIndex,
268{
269 fn rows(&self) -> usize {
270 self.rows_len()
271 }
272 fn cols(&self) -> usize {
273 self.cols.as_ref().map_or(self.matrix.cols(), |c| c.len())
274 }
275 fn nnz(&self) -> usize {
276 self.nnz
277 }
278
279 fn squared_frobenius(&self) -> f64 {
282 if self.is_identity() {
283 return SparseMat::squared_frobenius(self.matrix);
284 }
285 let masked = !self.col_to_masked.is_empty();
286 (0..self.rows_len())
287 .into_par_iter()
288 .map(|i| {
289 let orig = self.row_of(i);
290 self.matrix.outer_view(orig).map_or(0.0, |row| {
291 row.iter()
292 .filter(|(j, _)| !masked || self.col_to_masked[j.index()] != EXCLUDED)
293 .map(|(_, &v)| {
294 let x = v.to_f64();
295 x * x
296 })
297 .sum()
298 })
299 })
300 .sum()
301 }
302
303 fn centered_squared_frobenius(&self, means: ArrayView1<T>) -> f64 {
305 assert_eq!(
306 means.len(),
307 SparseMat::cols(self),
308 "centered_squared_frobenius: means must have length cols()"
309 );
310 if self.is_identity() {
311 return SparseMat::centered_squared_frobenius(self.matrix, means);
312 }
313 let rows = self.rows_len();
314 let ncols = SparseMat::cols(self);
315 let masked = !self.col_to_masked.is_empty();
316
317 let (stored, col_nnz) = (0..rows)
319 .into_par_iter()
320 .fold(
321 || (0.0f64, vec![0usize; ncols]),
322 |(mut acc, mut cnt), i| {
323 if let Some(row) = self.matrix.outer_view(self.row_of(i)) {
324 for (j, &v) in row.iter() {
325 let c = if masked {
326 self.col_to_masked[j.index()]
327 } else {
328 j.index()
329 };
330 if c == EXCLUDED {
331 continue;
332 }
333 let e = v.to_f64() - means[c].to_f64();
334 acc += e * e;
335 cnt[c] += 1;
336 }
337 }
338 (acc, cnt)
339 },
340 )
341 .reduce(
342 || (0.0f64, vec![0usize; ncols]),
343 |(a1, mut c1), (a2, c2)| {
344 for (x, y) in c1.iter_mut().zip(c2) {
345 *x += y;
346 }
347 (a1 + a2, c1)
348 },
349 );
350
351 super::centered_from_parts(stored, &col_nnz, means, rows)
352 }
353
354 fn mul_vec(&self, x: &[T], y: &mut [T], trans: bool) {
355 if self.is_identity() {
359 return SparseMat::mul_vec(self.matrix, x, y, trans);
360 }
361
362 let (m, n) = (self.rows(), self.cols());
363 if trans {
364 assert_eq!(x.len(), m, "mul_vec: x must have length rows()");
365 assert_eq!(y.len(), n, "mul_vec: y must have length cols()");
366 y.fill(T::zero());
367
368 let p = rayon::current_num_threads().max(1);
369 if p == 1 || self.nnz <= (8 << 10) {
370 for i in 0..m {
371 let xi = x[i];
372 if xi.is_zero() {
373 continue;
374 }
375 let Some(row) = self.matrix.outer_view(self.row_of(i)) else {
376 continue;
377 };
378 for (j, &v) in row.indices().iter().zip(row.data().iter()) {
379 let c = self.masked_col(j.index());
380 if c != EXCLUDED {
381 y[c] += v * xi;
382 }
383 }
384 }
385 return;
386 }
387 let chunk = m.div_ceil(p).max(1);
389 let partials: Vec<Vec<T>> = (0..p)
390 .into_par_iter()
391 .map(|t| {
392 let lo = (t * chunk).min(m);
393 let hi = ((t + 1) * chunk).min(m);
394 let mut acc = vec![T::zero(); n];
395 for i in lo..hi {
396 let xi = x[i];
397 if xi.is_zero() {
398 continue;
399 }
400 let Some(row) = self.matrix.outer_view(self.row_of(i)) else {
401 continue;
402 };
403 for (j, &v) in row.indices().iter().zip(row.data().iter()) {
404 let c = self.masked_col(j.index());
405 if c != EXCLUDED {
406 acc[c] += v * xi;
407 }
408 }
409 }
410 acc
411 })
412 .collect();
413 for acc in &partials {
414 for (yi, &a) in y.iter_mut().zip(acc.iter()) {
415 *yi += a;
416 }
417 }
418 } else {
419 assert_eq!(x.len(), n, "mul_vec: x must have length cols()");
420 assert_eq!(y.len(), m, "mul_vec: y must have length rows()");
421 let dot = |i: usize| -> T {
424 let Some(row) = self.matrix.outer_view(self.row_of(i)) else {
425 return T::zero();
426 };
427 let mut sum = T::zero();
428 for (j, &v) in row.indices().iter().zip(row.data().iter()) {
429 let c = self.masked_col(j.index());
430 if c != EXCLUDED {
431 sum += v * x[c];
432 }
433 }
434 sum
435 };
436 let chunk = m.div_ceil(rayon::current_num_threads().max(1) * 4).max(1);
437 y.par_chunks_mut(chunk).enumerate().for_each(|(ci, blk)| {
438 let base = ci * chunk;
439 for (local, yi) in blk.iter_mut().enumerate() {
440 *yi = dot(base + local);
441 }
442 });
443 }
444 }
445}
446
447impl<T, I, Iptr> SparseMatDense<T> for MaskedCsMat<'_, T, I, Iptr>
448where
449 T: SvdFloat,
450 I: SpIndex,
451 Iptr: SpIndex,
452{
453 fn mul_dense(&self, rhs: ArrayView2<T>, mut out: ArrayViewMut2<T>, trans: bool) {
454 if self.is_identity() {
455 return SparseMatDense::mul_dense(self.matrix, rhs, out, trans);
456 }
457
458 let (m, n, k) = (self.rows(), self.cols(), rhs.ncols());
459 if trans {
460 assert_eq!(rhs.nrows(), m, "mul_dense: rhs.rows != rows()");
461 assert_eq!(out.nrows(), n, "mul_dense: out.rows != cols()");
462 } else {
463 assert_eq!(rhs.nrows(), n, "mul_dense: rhs.rows != cols()");
464 assert_eq!(out.nrows(), m, "mul_dense: out.rows != rows()");
465 }
466 assert_eq!(out.ncols(), k, "mul_dense: out.cols != rhs.cols");
467
468 if !trans {
469 let chunk = m.div_ceil(rayon::current_num_threads().max(1) * 4).max(1);
471 out.axis_chunks_iter_mut(Axis(0), chunk)
472 .into_par_iter()
473 .enumerate()
474 .for_each(|(ci, mut block)| {
475 let base = ci * chunk;
476 for (local, mut orow) in block.rows_mut().into_iter().enumerate() {
477 orow.fill(T::zero());
478 let Some(row) = self.matrix.outer_view(self.row_of(base + local)) else {
479 continue;
480 };
481 for (j, &v) in row.indices().iter().zip(row.data().iter()) {
482 let c = self.masked_col(j.index());
483 if c == EXCLUDED {
484 continue;
485 }
486 let rrow = rhs.row(c);
487 for (o, &r) in orow.iter_mut().zip(rrow.iter()) {
488 *o += v * r;
489 }
490 }
491 }
492 });
493 } else {
494 out.fill(T::zero());
496 let p = rayon::current_num_threads().max(1);
497 let chunk = m.div_ceil(p).max(1);
498 let partials: Vec<ndarray::Array2<T>> = (0..p)
499 .into_par_iter()
500 .map(|t| {
501 let lo = (t * chunk).min(m);
502 let hi = ((t + 1) * chunk).min(m);
503 let mut acc = ndarray::Array2::<T>::zeros((n, k));
504 for i in lo..hi {
505 let Some(row) = self.matrix.outer_view(self.row_of(i)) else {
506 continue;
507 };
508 let rrow = rhs.row(i);
509 for (j, &v) in row.indices().iter().zip(row.data().iter()) {
510 let c = self.masked_col(j.index());
511 if c == EXCLUDED {
512 continue;
513 }
514 let mut arow = acc.row_mut(c);
515 for (a, &r) in arow.iter_mut().zip(rrow.iter()) {
516 *a += v * r;
517 }
518 }
519 }
520 acc
521 })
522 .collect();
523 for acc in &partials {
524 for (mut orow, arow) in out.rows_mut().into_iter().zip(acc.rows()) {
525 for (o, &a) in orow.iter_mut().zip(arow.iter()) {
526 *o += a;
527 }
528 }
529 }
530 }
531 }
532
533 fn col_means(&self) -> Array1<T> {
536 let m = self.rows();
537 let ones = vec![T::one(); m];
538 let mut sums = vec![T::zero(); self.cols()];
539 self.mul_vec(&ones, &mut sums, true);
540 let scale = if m == 0 {
541 T::zero()
542 } else {
543 T::one() / T::from_f64_val(m as f64)
544 };
545 Array1::from_vec(sums) * scale
546 }
547
548 fn mul_dense_centered(
549 &self,
550 rhs: ArrayView2<T>,
551 mut out: ArrayViewMut2<T>,
552 trans: bool,
553 means: ArrayView1<T>,
554 ) {
555 assert_eq!(
556 means.len(),
557 self.cols(),
558 "mul_dense_centered: means must have length cols() (the masked width)"
559 );
560 self.mul_dense(rhs, out.view_mut(), trans);
561 apply_centering(rhs, out, trans, means);
562 }
563}
564
565#[cfg(test)]
566mod tests {
567 use super::*;
568 use crate::testing::{dense_of, gen_sparse};
569 use ndarray::{Array2, Axis};
570 use sprs::TriMatI;
571
572 fn sample() -> CsMatI<f64, u32, u64> {
573 let mut t = TriMatI::<f64, u32>::new((3, 5));
578 for &(i, j, v) in &[
579 (0, 0, 1.0),
580 (0, 2, 2.0),
581 (0, 4, 3.0),
582 (1, 1, 4.0),
583 (1, 3, 5.0),
584 (2, 0, 6.0),
585 (2, 2, 7.0),
586 (2, 4, 8.0),
587 ] {
588 t.add_triplet(i, j, v);
589 }
590 t.to_csr::<u64>()
591 }
592
593 fn physical(m: &CsMatI<f64, u32, u64>, rows: &[usize], cols: &[usize]) -> Array2<f64> {
595 let full = dense_of(m);
596 let mut d = Array2::zeros((rows.len(), cols.len()));
597 for (ri, &r) in rows.iter().enumerate() {
598 for (ci, &c) in cols.iter().enumerate() {
599 d[[ri, ci]] = full[[r, c]];
600 }
601 }
602 d
603 }
604
605 fn check_against_physical(view: &MaskedCsMat<f64>, want: &Array2<f64>) {
606 assert_eq!((view.rows(), view.cols()), want.dim(), "shape");
607
608 let x: Vec<f64> = (0..view.cols()).map(|i| (i % 5) as f64 - 2.0).collect();
609 let mut y = vec![0.0; view.rows()];
610 view.mul_vec(&x, &mut y, false);
611 let expect = want.dot(&ndarray::Array1::from_vec(x.clone()));
612 for (g, w) in y.iter().zip(expect.iter()) {
613 approx::assert_relative_eq!(g, w, max_relative = 1e-12, epsilon = 1e-12);
614 }
615
616 let xt: Vec<f64> = (0..view.rows()).map(|i| (i % 3) as f64 - 1.0).collect();
617 let mut yt = vec![0.0; view.cols()];
618 view.mul_vec(&xt, &mut yt, true);
619 let expect_t = want.t().dot(&ndarray::Array1::from_vec(xt.clone()));
620 for (g, w) in yt.iter().zip(expect_t.iter()) {
621 approx::assert_relative_eq!(g, w, max_relative = 1e-12, epsilon = 1e-12);
622 }
623
624 let rhs = Array2::from_shape_fn((view.cols(), 2), |(i, j)| (i + 2 * j) as f64 - 1.0);
626 let mut out = Array2::zeros((view.rows(), 2));
627 view.mul_dense(rhs.view(), out.view_mut(), false);
628 let want_out = want.dot(&rhs);
629 for (g, w) in out.iter().zip(want_out.iter()) {
630 approx::assert_relative_eq!(g, w, max_relative = 1e-12, epsilon = 1e-12);
631 }
632
633 let rhs_t = Array2::from_shape_fn((view.rows(), 2), |(i, j)| (2 * i + j) as f64 - 2.0);
634 let mut out_t = Array2::zeros((view.cols(), 2));
635 view.mul_dense(rhs_t.view(), out_t.view_mut(), true);
636 let want_t = want.t().dot(&rhs_t);
637 for (g, w) in out_t.iter().zip(want_t.iter()) {
638 approx::assert_relative_eq!(g, w, max_relative = 1e-12, epsilon = 1e-12);
639 }
640 }
641
642 #[test]
643 fn column_selection_matches_physical_subset() {
644 let a = sample();
645 let cols = [0usize, 2, 4];
646 let all_rows: Vec<usize> = (0..3).collect();
647 check_against_physical(
648 &MaskedCsMat::with_columns(&a, &cols),
649 &physical(&a, &all_rows, &cols),
650 );
651 }
652
653 #[test]
654 fn row_selection_matches_physical_subset() {
655 let a = sample();
656 let rows = [0usize, 2];
657 let all_cols: Vec<usize> = (0..5).collect();
658 let view = MaskedCsMat::with_rows(&a, &rows);
659 assert_eq!(view.nnz(), 6, "only the two selected rows' non-zeros count");
660 check_against_physical(&view, &physical(&a, &rows, &all_cols));
661 }
662
663 #[test]
664 fn combined_selection_matches_physical_subset() {
665 let a = sample();
666 let rows = [0usize, 2];
667 let cols = [1usize, 2, 4];
668 let view = MaskedCsMat::submatrix(&a, Some(&rows), Some(&cols));
669 assert_eq!((view.rows(), view.cols()), (2, 3));
670 check_against_physical(&view, &physical(&a, &rows, &cols));
671 }
672
673 #[test]
674 fn selections_are_sorted_and_deduplicated() {
675 let a = sample();
676 let view = MaskedCsMat::submatrix(&a, Some(&[2, 0, 2]), Some(&[4, 0, 4, 0]));
677 assert_eq!(view.selected_rows().unwrap(), &[0, 2]);
678 assert_eq!(view.selected_columns().unwrap(), &[0, 4]);
679 check_against_physical(&view, &physical(&a, &[0, 2], &[0, 4]));
680 }
681
682 #[test]
683 fn boolean_masks_agree_with_index_lists() {
684 let a = sample();
685 let by_mask = MaskedCsMat::from_masks(
686 &a,
687 Some(&[true, false, true]),
688 Some(&[false, true, false, true, false]),
689 );
690 let by_index = MaskedCsMat::submatrix(&a, Some(&[0, 2]), Some(&[1, 3]));
691 assert_eq!(by_mask.selected_rows(), by_index.selected_rows());
692 assert_eq!(by_mask.selected_columns(), by_index.selected_columns());
693 assert_eq!(by_mask.nnz(), by_index.nnz());
694 }
695
696 #[test]
699 fn small_masked_matrix_does_not_delegate() {
700 let a = sample();
701 let cols = [0usize, 2, 4];
702 let view = MaskedCsMat::with_columns(&a, &cols);
703 let want = physical(&a, &[0, 1, 2], &cols);
704 let x = [1.0, 2.0, 3.0];
705 let mut y = vec![0.0; 3];
706 view.mul_vec(&x, &mut y, false);
707 assert_eq!(y, want.dot(&ndarray::arr1(&x)).to_vec());
708 }
709
710 #[test]
711 fn identity_view_matches_unmasked() {
712 let a = sample();
713 let view = MaskedCsMat::submatrix(&a, None, None);
714 assert!(view.is_identity());
715 assert_eq!(view.nnz(), a.nnz());
716 let x = [1.0, 2.0, 3.0, 4.0, 5.0];
717 let mut ym = vec![0.0; 3];
718 let mut yu = vec![0.0; 3];
719 view.mul_vec(&x, &mut ym, false);
720 SparseMat::mul_vec(&a, &x, &mut yu, false);
721 assert_eq!(ym, yu);
722 }
723
724 #[test]
727 fn col_means_respect_the_row_selection() {
728 let a = sample();
729 let rows = [0usize, 2];
730 let cols = [0usize, 2, 4];
731 let view = MaskedCsMat::submatrix(&a, Some(&rows), Some(&cols));
732 let want = physical(&a, &rows, &cols);
733
734 let got = view.col_means();
735 let expect = want.mean_axis(Axis(0)).unwrap();
736 for (g, w) in got.iter().zip(expect.iter()) {
737 approx::assert_relative_eq!(g, w, max_relative = 1e-12);
738 }
739
740 let centered = &want - &expect.view().insert_axis(Axis(0));
742 let rhs = ndarray::arr2(&[[1.0], [2.0], [3.0]]);
743 let mut out = Array2::zeros((2, 1));
744 view.mul_dense_centered(rhs.view(), out.view_mut(), false, got.view());
745 let want_out = centered.dot(&rhs);
746 for (g, w) in out.iter().zip(want_out.iter()) {
747 approx::assert_relative_eq!(g, w, max_relative = 1e-12, epsilon = 1e-12);
748 }
749 }
750
751 #[test]
754 fn squared_frobenius_counts_only_selected_entries() {
755 let a = gen_sparse(120, 40, 0.1, 91);
756 let rows: Vec<usize> = (0..120).filter(|r| r % 4 != 0).collect();
757 let cols: Vec<usize> = (0..40).filter(|c| c % 3 == 0).collect();
758 let view = MaskedCsMat::submatrix(&a, Some(&rows), Some(&cols));
759
760 let want: f64 = physical(&a, &rows, &cols).iter().map(|&v| v * v).sum();
761 approx::assert_relative_eq!(view.squared_frobenius(), want, max_relative = 1e-12);
762
763 assert!(view.squared_frobenius() < SparseMat::squared_frobenius(&a));
765 let identity = MaskedCsMat::submatrix(&a, None, None);
766 approx::assert_relative_eq!(
767 identity.squared_frobenius(),
768 SparseMat::squared_frobenius(&a),
769 max_relative = 1e-14
770 );
771 approx::assert_relative_eq!(
773 view.to_sparse().squared_frobenius(),
774 want,
775 max_relative = 1e-12
776 );
777 }
778
779 #[test]
781 fn centered_squared_frobenius_matches_the_selected_submatrix() {
782 let a = gen_sparse(120, 40, 0.2, 77);
783 let rows: Vec<usize> = (0..120).filter(|r| r % 4 != 0).collect();
784 let cols: Vec<usize> = (0..40).filter(|c| c % 3 == 0).collect();
785 let view = MaskedCsMat::submatrix(&a, Some(&rows), Some(&cols));
786
787 let sub = physical(&a, &rows, &cols);
788 let means = view.col_means();
789 let centered = &sub - &means.view().insert_axis(Axis(0));
790 let want: f64 = centered.iter().map(|&v| v * v).sum();
791
792 approx::assert_relative_eq!(
793 view.centered_squared_frobenius(means.view()),
794 want,
795 max_relative = 1e-10
796 );
797 approx::assert_relative_eq!(
799 SparseMat::centered_squared_frobenius(&view.to_sparse(), means.view()),
800 want,
801 max_relative = 1e-10
802 );
803 let identity = MaskedCsMat::submatrix(&a, None, None);
805 let full_means = identity.col_means();
806 approx::assert_relative_eq!(
807 identity.centered_squared_frobenius(full_means.view()),
808 SparseMat::centered_squared_frobenius(&a, full_means.view()),
809 max_relative = 1e-12
810 );
811 }
812
813 #[test]
815 fn to_sparse_matches_the_view() {
816 let a = gen_sparse(120, 40, 0.1, 23);
817 let rows: Vec<usize> = (0..120).filter(|r| r % 4 != 0).collect();
818 let cols: Vec<usize> = (0..40).filter(|c| c % 3 == 0).collect();
819 let view = MaskedCsMat::submatrix(&a, Some(&rows), Some(&cols));
820 let extracted = view.to_sparse();
821
822 assert_eq!(extracted.rows(), view.rows());
823 assert_eq!(extracted.cols(), view.cols());
824 assert_eq!(extracted.nnz(), view.nnz());
825 assert!(extracted.is_csr());
826 for i in 0..extracted.rows() {
828 if let Some(row) = extracted.outer_view(i) {
829 let idx = row.indices();
830 assert!(
831 idx.windows(2).all(|w| w[0] < w[1]),
832 "row {i} indices not sorted"
833 );
834 }
835 }
836
837 assert_eq!(dense_of(&extracted), physical(&a, &rows, &cols));
838
839 let x: Vec<f64> = (0..view.cols()).map(|i| (i % 7) as f64 - 3.0).collect();
841 let (mut yv, mut ye) = (vec![0.0; view.rows()], vec![0.0; view.rows()]);
842 view.mul_vec(&x, &mut yv, false);
843 SparseMat::mul_vec(&extracted, &x, &mut ye, false);
844 assert_eq!(yv, ye);
845 }
846
847 #[test]
849 fn pca_agrees_between_view_and_extraction() {
850 let a = gen_sparse(300, 50, 0.12, 29);
851 let cols: Vec<usize> = (0..50).filter(|c| c % 2 == 0).collect();
852 let view = MaskedCsMat::with_columns(&a, &cols);
853 let extracted = view.to_sparse();
854
855 let by_view = crate::irlba::svd_centered(&view, 6, Some(42)).unwrap();
856 let by_copy = crate::irlba::svd_centered(&extracted, 6, Some(42)).unwrap();
857 for (x, y) in by_view.s.iter().zip(by_copy.s.iter()) {
858 approx::assert_relative_eq!(x, y, max_relative = 1e-9);
859 }
860 }
861
862 #[test]
863 fn empty_selections() {
864 let a = sample();
865 let no_cols = MaskedCsMat::submatrix(&a, None, Some(&[]));
866 assert_eq!(no_cols.cols(), 0);
867 assert_eq!(no_cols.nnz(), 0);
868
869 let no_rows = MaskedCsMat::submatrix(&a, Some(&[]), None);
870 assert_eq!(no_rows.rows(), 0);
871 assert_eq!(no_rows.nnz(), 0);
872 }
873
874 #[test]
875 #[should_panic(expected = "out of bounds")]
876 fn rejects_out_of_range_rows() {
877 let a = sample();
878 let _ = MaskedCsMat::with_rows(&a, &[0, 99]);
879 }
880
881 #[test]
883 fn larger_random_submatrix() {
884 let a = gen_sparse(200, 60, 0.08, 17);
885 let rows: Vec<usize> = (0..200).filter(|r| r % 3 == 0).collect();
886 let cols: Vec<usize> = (0..60).filter(|c| c % 2 == 1).collect();
887 let view = MaskedCsMat::submatrix(&a, Some(&rows), Some(&cols));
888 check_against_physical(&view, &physical(&a, &rows, &cols));
889 }
890}