1use core::fmt::Debug;
37
38use dyn_stack::{MemStack, StackReq};
39use faer::matrix_free::{BiLinOp, BiPrecond, LinOp, Precond};
40use faer::sparse::{SparseColMatRef, SymbolicSparseColMatRef};
41use faer::{MatMut, MatRef, Par};
42use faer_traits::{ComplexField, Index};
43
44mod apply;
45mod build;
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
49pub enum FsaiPattern {
50 #[default]
52 LowerOfA,
53 LowerOfPower { power: usize },
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
60pub enum FsaiError {
61 NonSquareMatrix { nrows: usize, ncols: usize },
63 InvalidPower,
65 NotPositiveDefinite { row: usize },
67}
68
69impl core::fmt::Display for FsaiError {
70 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
71 match self {
72 Self::NonSquareMatrix { nrows, ncols } => {
73 write!(f, "matrix must be square but is {nrows}x{ncols}")
74 }
75 Self::InvalidPower => f.write_str("FSAI pattern power must be at least 1"),
76 Self::NotPositiveDefinite { row } => {
77 write!(f, "local block at row {row} is not positive definite")
78 }
79 }
80 }
81}
82
83impl core::error::Error for FsaiError {}
84
85#[derive(Debug, Clone)]
91pub struct Fsai<I, T> {
92 pub(crate) dim: usize,
93 pub(crate) g_col_ptr: Vec<I>,
94 pub(crate) g_row_idx: Vec<I>,
95 pub(crate) g_values: Vec<T>,
96}
97
98impl<I, T> Fsai<I, T> {
99 #[inline]
101 pub fn dim(&self) -> usize {
102 self.dim
103 }
104}
105
106impl<I: Index, T: ComplexField> Fsai<I, T> {
107 #[inline]
109 pub(crate) fn g_view(&self) -> SparseColMatRef<'_, I, T> {
110 let symbolic = unsafe {
111 SymbolicSparseColMatRef::<'_, I>::new_unchecked(
112 self.dim,
113 self.dim,
114 &self.g_col_ptr,
115 None,
116 &self.g_row_idx,
117 )
118 };
119 SparseColMatRef::new(symbolic, &self.g_values)
120 }
121}
122
123impl<I, T> LinOp<T> for Fsai<I, T>
124where
125 I: Index,
126 T: ComplexField + Debug + Sync,
127{
128 fn apply_scratch(&self, rhs_ncols: usize, _par: Par) -> StackReq {
129 apply::scratch(self, rhs_ncols)
130 }
131
132 fn nrows(&self) -> usize {
133 self.dim
134 }
135
136 fn ncols(&self) -> usize {
137 self.dim
138 }
139
140 fn apply(&self, out: MatMut<'_, T>, rhs: MatRef<'_, T>, par: Par, stack: &mut MemStack) {
141 apply::apply_out(self, out, rhs, false, par, stack);
142 }
143
144 fn conj_apply(&self, out: MatMut<'_, T>, rhs: MatRef<'_, T>, par: Par, stack: &mut MemStack) {
145 apply::apply_out(self, out, rhs, true, par, stack);
146 }
147}
148
149impl<I, T> Precond<T> for Fsai<I, T>
150where
151 I: Index,
152 T: ComplexField + Debug + Sync,
153{
154 fn apply_in_place_scratch(&self, rhs_ncols: usize, _par: Par) -> StackReq {
155 apply::scratch(self, rhs_ncols)
156 }
157
158 fn apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, stack: &mut MemStack) {
159 apply::apply_inplace(self, rhs, false, par, stack);
160 }
161
162 fn conj_apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, stack: &mut MemStack) {
163 apply::apply_inplace(self, rhs, true, par, stack);
164 }
165}
166
167impl<I, T> BiLinOp<T> for Fsai<I, T>
168where
169 I: Index,
170 T: ComplexField + Debug + Sync,
171{
172 fn transpose_apply_scratch(&self, rhs_ncols: usize, _par: Par) -> StackReq {
173 apply::scratch(self, rhs_ncols)
174 }
175
176 fn transpose_apply(
177 &self,
178 out: MatMut<'_, T>,
179 rhs: MatRef<'_, T>,
180 par: Par,
181 stack: &mut MemStack,
182 ) {
183 apply::apply_out(self, out, rhs, true, par, stack);
185 }
186
187 fn adjoint_apply(
188 &self,
189 out: MatMut<'_, T>,
190 rhs: MatRef<'_, T>,
191 par: Par,
192 stack: &mut MemStack,
193 ) {
194 apply::apply_out(self, out, rhs, false, par, stack);
196 }
197}
198
199impl<I, T> BiPrecond<T> for Fsai<I, T>
200where
201 I: Index,
202 T: ComplexField + Debug + Sync,
203{
204 fn transpose_apply_in_place_scratch(&self, rhs_ncols: usize, _par: Par) -> StackReq {
205 apply::scratch(self, rhs_ncols)
206 }
207
208 fn transpose_apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, stack: &mut MemStack) {
209 apply::apply_inplace(self, rhs, true, par, stack);
210 }
211
212 fn adjoint_apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, stack: &mut MemStack) {
213 apply::apply_inplace(self, rhs, false, par, stack);
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use core::mem::MaybeUninit;
221 use faer::sparse::{SparseColMat, Triplet};
222 use faer::{Mat, MatRef, mat};
223
224 fn with_stack(req: StackReq, f: impl FnOnce(&mut MemStack)) {
225 let nbytes = req.unaligned_bytes_required().max(1);
226 let mut buf = vec![MaybeUninit::<u8>::uninit(); nbytes].into_boxed_slice();
227 f(MemStack::new(&mut buf));
228 }
229
230 fn assert_close(lhs: MatRef<'_, f64>, rhs: MatRef<'_, f64>, tol: f64) {
231 assert_eq!(lhs.nrows(), rhs.nrows());
232 assert_eq!(lhs.ncols(), rhs.ncols());
233 for j in 0..lhs.ncols() {
234 for i in 0..lhs.nrows() {
235 let diff = (*lhs.get(i, j) - *rhs.get(i, j)).abs();
236 assert!(
237 diff <= tol,
238 "mismatch at ({i}, {j}): lhs={}, rhs={}, diff={diff}",
239 *lhs.get(i, j),
240 *rhs.get(i, j),
241 );
242 }
243 }
244 }
245
246 fn to_dense(a: &SparseColMat<usize, f64>) -> Mat<f64> {
247 let n = a.nrows();
248 let mut out = Mat::<f64>::zeros(n, a.ncols());
249 let a_ref = a.as_ref();
250 for j in 0..a.ncols() {
251 let rows = a_ref.symbolic().row_idx_of_col_raw(j);
252 let vals = a_ref.val_of_col(j);
253 for (r, v) in rows.iter().zip(vals.iter()) {
254 *out.as_mut().get_mut(*r, j) = *v;
255 }
256 }
257 out
258 }
259
260 fn tridiagonal(n: usize, diag: f64, off: f64) -> SparseColMat<usize, f64> {
261 let mut triplets = Vec::new();
262 for i in 0..n {
263 triplets.push(Triplet::new(i, i, diag));
264 if i > 0 {
265 triplets.push(Triplet::new(i, i - 1, off));
266 triplets.push(Triplet::new(i - 1, i, off));
267 }
268 }
269 SparseColMat::try_new_from_triplets(n, n, &triplets).unwrap()
270 }
271
272 fn laplacian_2d(grid: usize) -> SparseColMat<usize, f64> {
273 let n = grid * grid;
274 let mut triplets = Vec::new();
275 for gy in 0..grid {
276 for gx in 0..grid {
277 let idx = gy * grid + gx;
278 triplets.push(Triplet::new(idx, idx, 4.0));
279 if gx > 0 {
280 triplets.push(Triplet::new(idx, idx - 1, -1.0));
281 }
282 if gx + 1 < grid {
283 triplets.push(Triplet::new(idx, idx + 1, -1.0));
284 }
285 if gy > 0 {
286 triplets.push(Triplet::new(idx, idx - grid, -1.0));
287 }
288 if gy + 1 < grid {
289 triplets.push(Triplet::new(idx, idx + grid, -1.0));
290 }
291 }
292 }
293 SparseColMat::try_new_from_triplets(n, n, &triplets).unwrap()
294 }
295
296 fn apply_inplace(pc: &Fsai<usize, f64>, rhs: &mut Mat<f64>) {
297 with_stack(pc.apply_in_place_scratch(rhs.ncols(), Par::Seq), |stack| {
298 pc.apply_in_place(rhs.as_mut(), Par::Seq, stack);
299 });
300 }
301
302 fn residual_ratio(a: &SparseColMat<usize, f64>, pc: &Fsai<usize, f64>, b: &Mat<f64>) -> f64 {
303 let a_dense = to_dense(a);
304 let mut x = b.clone();
305 apply_inplace(pc, &mut x);
306 let residual = &a_dense * &x - b;
307 let b_norm: f64 = b.as_ref().col(0).iter().map(|v| v * v).sum::<f64>().sqrt();
308 let r_norm: f64 = residual
309 .as_ref()
310 .col(0)
311 .iter()
312 .map(|v| v * v)
313 .sum::<f64>()
314 .sqrt();
315 r_norm / b_norm
316 }
317
318 #[test]
319 fn diagonal_is_exact_inverse() {
320 let mut triplets = Vec::new();
321 for (i, &v) in [2.0, 4.0, 8.0].iter().enumerate() {
322 triplets.push(Triplet::new(i, i, v));
323 }
324 let a = SparseColMat::<usize, f64>::try_new_from_triplets(3, 3, &triplets).unwrap();
325 let pc = Fsai::try_new(a.as_ref(), FsaiPattern::LowerOfA).unwrap();
326 let mut x = mat![[2.0_f64], [8.0], [16.0]];
327 apply_inplace(&pc, &mut x);
328 let expected = mat![[1.0_f64], [2.0], [2.0]];
329 assert_close(x.as_ref(), expected.as_ref(), 1e-12);
330 }
331
332 #[test]
333 fn reduces_residual_on_laplacian() {
334 let a = laplacian_2d(8);
335 let n = a.nrows();
336 let pc = Fsai::try_new(a.as_ref(), FsaiPattern::LowerOfA).unwrap();
337 let b = Mat::<f64>::from_fn(n, 1, |i, _| (i % 7) as f64 - 3.0);
338 let ratio = residual_ratio(&a, &pc, &b);
339 assert!(ratio < 1.0, "FSAI should reduce the residual: {ratio}");
340 }
341
342 #[test]
343 fn denser_pattern_improves_accuracy() {
344 let a = laplacian_2d(8);
345 let n = a.nrows();
346 let b = Mat::<f64>::from_fn(n, 1, |i, _| (i % 7) as f64 - 3.0);
347 let p1 = Fsai::try_new(a.as_ref(), FsaiPattern::LowerOfA).unwrap();
348 let p2 = Fsai::try_new(a.as_ref(), FsaiPattern::LowerOfPower { power: 2 }).unwrap();
349 let r1 = residual_ratio(&a, &p1, &b);
350 let r2 = residual_ratio(&a, &p2, &b);
351 assert!(r2 < r1, "denser FSAI pattern should help: {r2} !< {r1}");
352 }
353
354 #[test]
355 fn symmetric_two_matvec_matches_dense() {
356 let a = tridiagonal(6, 4.0, -1.0);
357 let pc = Fsai::try_new(a.as_ref(), FsaiPattern::LowerOfA).unwrap();
358 let rhs = mat![[1.0_f64], [-2.0], [3.0], [0.5], [-1.0], [2.0]];
360 let mut fwd = rhs.clone();
361 apply_inplace(&pc, &mut fwd);
362 let mut tr = rhs.clone();
363 with_stack(pc.transpose_apply_in_place_scratch(1, Par::Seq), |stack| {
364 pc.transpose_apply_in_place(tr.as_mut(), Par::Seq, stack);
365 });
366 assert_close(fwd.as_ref(), tr.as_ref(), 1e-12);
367 }
368
369 #[test]
370 fn rejects_non_square() {
371 let mut triplets = Vec::new();
372 for i in 0..3 {
373 triplets.push(Triplet::new(i, i, 1.0));
374 }
375 let a = SparseColMat::<usize, f64>::try_new_from_triplets(3, 4, &triplets).unwrap();
376 assert_eq!(
377 Fsai::try_new(a.as_ref(), FsaiPattern::LowerOfA).unwrap_err(),
378 FsaiError::NonSquareMatrix { nrows: 3, ncols: 4 }
379 );
380 }
381}