1use std::sync::Mutex;
2
3use faer::prelude::*;
4use faer::sparse::linalg::solvers::{Lu, SymbolicLu};
5use faer::sparse::{SymbolicSparseColMat, SymbolicSparseColMatRef};
6use nalgebra::DVector;
7
8use crate::model::SparseMatrix;
9
10use super::AnalysisError;
11
12pub struct SparseSolver {
20 symbolic_cache: Mutex<Option<CachedSymbolic>>,
23 #[cfg(test)]
24 symbolic_analyses: std::sync::atomic::AtomicUsize,
25}
26
27struct CachedSymbolic {
28 pattern: SymbolicSparseColMat<usize>,
29 factorization: SymbolicLu<usize>,
30}
31
32impl CachedSymbolic {
33 fn matches(&self, pattern: SymbolicSparseColMatRef<'_, usize>) -> bool {
34 self.pattern.nrows() == pattern.nrows()
35 && self.pattern.ncols() == pattern.ncols()
36 && self.pattern.col_ptr() == pattern.col_ptr()
37 && self.pattern.col_nnz() == pattern.col_nnz()
38 && self.pattern.row_idx() == pattern.row_idx()
39 }
40}
41
42impl SparseSolver {
43 pub fn new() -> Self {
44 Self {
45 symbolic_cache: Mutex::new(None),
46 #[cfg(test)]
47 symbolic_analyses: std::sync::atomic::AtomicUsize::new(0),
48 }
49 }
50
51 pub fn factor(&self, k: &SparseMatrix) -> Result<SparseFactorization, AnalysisError> {
61 let symbolic = {
62 let mut cache = self
63 .symbolic_cache
64 .lock()
65 .expect("symbolic LU cache poisoned");
66 let pattern = k.symbolic();
67 if !cache.as_ref().is_some_and(|cached| cached.matches(pattern)) {
68 let factorization =
69 SymbolicLu::try_new(pattern).map_err(|_| AnalysisError::SingularSystem)?;
70 let pattern = pattern
71 .to_owned()
72 .map_err(|_| AnalysisError::SingularSystem)?;
73 *cache = Some(CachedSymbolic {
74 pattern,
75 factorization,
76 });
77 #[cfg(test)]
78 self.symbolic_analyses
79 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
80 }
81 cache
82 .as_ref()
83 .expect("symbolic cache populated above")
84 .factorization
85 .clone()
86 };
87 Ok(SparseFactorization(
88 Lu::try_new_with_symbolic(symbolic, k.as_ref())
89 .map_err(|_| AnalysisError::SingularSystem)?,
90 ))
91 }
92
93 pub fn solve(&self, k: &SparseMatrix, r: &DVector<f64>) -> Result<DVector<f64>, AnalysisError> {
98 Ok(self.factor(k)?.solve(r))
99 }
100}
101
102impl Default for SparseSolver {
103 fn default() -> Self {
104 Self::new()
105 }
106}
107
108pub struct SparseFactorization(Lu<usize, f64>);
114
115impl SparseFactorization {
116 pub fn solve(&self, r: &DVector<f64>) -> DVector<f64> {
117 let n = r.len();
118 let rhs = faer::col::Col::from_fn(n, |i| r[i]);
119 let x = self.0.solve(&rhs);
120 DVector::from_fn(n, |i, _| x[i])
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use faer::sparse::Triplet;
128 use std::sync::atomic::Ordering;
129
130 fn matrix(n: usize, entries: &[(usize, usize, f64)]) -> SparseMatrix {
131 let triplets: Vec<_> = entries
132 .iter()
133 .map(|&(i, j, v)| Triplet::new(i, j, v))
134 .collect();
135 SparseMatrix::try_new_from_triplets(n, n, &triplets).unwrap()
136 }
137
138 fn assert_solution(factor: &SparseFactorization, rhs: &[f64], expected: &[f64]) {
139 let solution = factor.solve(&DVector::from_column_slice(rhs));
140 assert!((solution - DVector::from_column_slice(expected)).norm() < 1e-12);
141 }
142
143 #[test]
144 fn changing_values_and_pivots_reuses_symbolic_but_not_numeric_factors() {
145 let solver = SparseSolver::new();
146 let first = matrix(2, &[(0, 0, 4.0), (0, 1, 2.0), (1, 0, 3.0), (1, 1, 5.0)]);
147 let factor = solver.factor(&first).unwrap();
148 assert_solution(&factor, &[8.0, 13.0], &[1.0, 2.0]);
149
150 let second = matrix(2, &[(0, 0, 0.0), (0, 1, 2.0), (1, 0, 3.0), (1, 1, 4.0)]);
152 let updated = solver.factor(&second).unwrap();
153 assert_solution(&updated, &[4.0, 11.0], &[1.0, 2.0]);
154 assert_eq!(solver.symbolic_analyses.load(Ordering::Relaxed), 1);
155 assert_solution(&factor, &[8.0, 13.0], &[1.0, 2.0]);
157 }
158
159 #[test]
160 fn changed_pattern_with_same_size_and_nnz_and_changed_dimensions_rebuild_cache() {
161 let solver = SparseSolver::new();
162 let upper = matrix(2, &[(0, 0, 4.0), (0, 1, 1.0), (1, 1, 3.0)]);
163 assert_solution(&solver.factor(&upper).unwrap(), &[6.0, 6.0], &[1.0, 2.0]);
164 let lower = matrix(2, &[(0, 0, 4.0), (1, 0, 1.0), (1, 1, 3.0)]);
165 assert_solution(&solver.factor(&lower).unwrap(), &[4.0, 7.0], &[1.0, 2.0]);
166 assert_eq!(solver.symbolic_analyses.load(Ordering::Relaxed), 2);
167
168 let larger = matrix(3, &[(0, 0, 2.0), (1, 1, 3.0), (2, 2, 4.0)]);
169 assert_solution(
170 &solver.factor(&larger).unwrap(),
171 &[2.0, 6.0, 12.0],
172 &[1.0, 2.0, 3.0],
173 );
174 assert_eq!(solver.symbolic_analyses.load(Ordering::Relaxed), 3);
175 assert_solution(&solver.factor(&lower).unwrap(), &[4.0, 7.0], &[1.0, 2.0]);
176 assert_eq!(solver.symbolic_analyses.load(Ordering::Relaxed), 4);
177 }
178
179 #[test]
180 fn singular_pattern_does_not_poison_cache_or_invalidate_owned_factors() {
181 let solver = SparseSolver::new();
182 let nonsingular = matrix(2, &[(0, 0, 2.0), (1, 1, 3.0)]);
183 let factor = solver.factor(&nonsingular).unwrap();
184 let singular = matrix(2, &[(1, 1, 3.0)]);
185 assert!(matches!(
186 solver.factor(&singular),
187 Err(AnalysisError::SingularSystem)
188 ));
189 assert_solution(&factor, &[2.0, 6.0], &[1.0, 2.0]);
190 assert_solution(
191 &solver.factor(&nonsingular).unwrap(),
192 &[2.0, 6.0],
193 &[1.0, 2.0],
194 );
195 }
196}