Skip to main content

apex_solver/linalg/sparse/
pattern.rs

1//! Structural fingerprints for sparse-matrix caches.
2//!
3//! Several solvers cache work keyed on "the sparsity pattern hasn't changed":
4//! the normal-equations symbolic product, the chunk-wise Schur layout, the
5//! `H_ee` block-diagonality verdict and the implicit-Schur visibility index.
6//! All of them used to key on the `(nrows, ncols, nnz)` triple, which aliases
7//! patterns that permute entries at equal nonzero count — reusing a stale
8//! cache for such a pattern silently produces a wrong step.
9//!
10//! [`PatternFingerprint`] keeps the triple as a fast reject but adds a
11//! deterministic FNV-1a hash over the full column-row structure, so equality
12//! means "same pattern" and not just "same shape". The hash costs one O(nnz)
13//! integer pass per check — negligible next to the floating-point assembly it
14//! guards. (FNV, not `std`'s `DefaultHasher`: the latter is randomly seeded
15//! per instance and cannot be compared across caches.)
16
17use faer::sparse::SparseColMat;
18
19/// Fold one value into an FNV-1a hash.
20///
21/// Values are pre-mixed with a golden-ratio multiply because raw structural
22/// indices (0, 1, 2, …) carry entropy only in their low bits; without the mix
23/// consecutive columns would cancel each other out of the hash.
24#[inline]
25fn fold(mut hash: u64, value: usize) -> u64 {
26    const PRIME: u64 = 0x100000001b3;
27    const MIX: u64 = 0x9E3779B97F4A7C15;
28    hash ^= (value as u64).wrapping_mul(MIX);
29    hash.wrapping_mul(PRIME)
30}
31
32/// Structural identity of a sparse matrix: dimensions, nonzero count, and a
33/// hash of the full sparsity pattern.
34///
35/// Compare with `==`: the three cheap fields reject first, so the common
36/// "same pattern" case costs three integer compares and the "different shape"
37/// case never touches the hash.
38///
39/// `Default` is the all-zero placeholder (never equal to a real fingerprint);
40/// caches holding it must overwrite it at build time, as before.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
42pub struct PatternFingerprint {
43    nrows: usize,
44    ncols: usize,
45    nnz: usize,
46    hash: u64,
47}
48
49impl PatternFingerprint {
50    /// Fingerprint the sparsity pattern of `matrix` (values ignored).
51    pub fn of(matrix: &SparseColMat<usize, f64>) -> Self {
52        let symbolic = matrix.symbolic();
53        // One xor-multiply per structural integer — the same pass `compute_nnz`
54        // style checks already pay, against O(nnz) floating-point assembly
55        // plus factorization downstream.
56        let mut hash: u64 = 0xcbf29ce484222325;
57        hash = fold(hash, matrix.nrows());
58        hash = fold(hash, matrix.ncols());
59        let mut nnz = 0usize;
60        for col in 0..matrix.ncols() {
61            let rows = symbolic.row_idx_of_col_raw(col);
62            hash = fold(hash, rows.len());
63            for &row in rows {
64                hash = fold(hash, row);
65                nnz += 1;
66            }
67        }
68        Self {
69            nrows: matrix.nrows(),
70            ncols: matrix.ncols(),
71            nnz,
72            hash,
73        }
74    }
75
76    /// Rows of the fingerprinted matrix.
77    pub fn nrows(&self) -> usize {
78        self.nrows
79    }
80
81    /// Columns of the fingerprinted matrix.
82    pub fn ncols(&self) -> usize {
83        self.ncols
84    }
85
86    /// Nonzero count of the fingerprinted matrix.
87    pub fn nnz(&self) -> usize {
88        self.nnz
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use faer::sparse::Triplet;
96
97    type TestResult = Result<(), Box<dyn std::error::Error>>;
98
99    #[test]
100    fn equal_patterns_agree() -> TestResult {
101        let t = vec![
102            Triplet::new(0usize, 0usize, 1.0f64),
103            Triplet::new(1, 0, 2.0),
104            Triplet::new(1, 1, 3.0),
105        ];
106        let a = SparseColMat::try_new_from_triplets(2, 2, &t)?;
107        let b = SparseColMat::try_new_from_triplets(2, 2, &t)?;
108        assert_eq!(PatternFingerprint::of(&a), PatternFingerprint::of(&b));
109        // Values are ignored: same pattern, different values still match.
110        let t2 = vec![
111            Triplet::new(0usize, 0usize, 9.0f64),
112            Triplet::new(1, 0, 8.0),
113            Triplet::new(1, 1, 7.0),
114        ];
115        let c = SparseColMat::try_new_from_triplets(2, 2, &t2)?;
116        assert_eq!(PatternFingerprint::of(&a), PatternFingerprint::of(&c));
117        Ok(())
118    }
119
120    #[test]
121    fn permuted_pattern_at_equal_nnz_disagrees() -> TestResult {
122        let t = vec![
123            Triplet::new(0usize, 0usize, 1.0f64),
124            Triplet::new(1, 0, 2.0),
125            Triplet::new(1, 1, 3.0),
126        ];
127        let a = SparseColMat::try_new_from_triplets(2, 2, &t)?;
128        // Same shape (2x2) and same nnz (3), but column 1 holds row 0
129        // instead of row 1: a genuinely different pattern.
130        let t2 = vec![
131            Triplet::new(0usize, 0usize, 1.0f64),
132            Triplet::new(1, 0, 2.0),
133            Triplet::new(0, 1, 3.0),
134        ];
135        let b = SparseColMat::try_new_from_triplets(2, 2, &t2)?;
136        assert_eq!((a.nrows(), a.ncols(), 3), (b.nrows(), b.ncols(), 3));
137        assert_ne!(
138            PatternFingerprint::of(&a),
139            PatternFingerprint::of(&b),
140            "equal-nnz permutation must invalidate the fingerprint"
141        );
142        Ok(())
143    }
144}