1use core::fmt::Debug;
40
41use dyn_stack::{MemStack, StackReq};
42use faer::matrix_free::{BiLinOp, BiPrecond, LinOp, Precond};
43use faer::{MatMut, MatRef, Par};
44use faer_traits::{ComplexField, Index};
45
46mod apply;
47mod build;
48
49#[derive(Debug, Clone, Copy, PartialEq)]
51pub struct SsorParams {
52 pub omega: f64,
55}
56
57impl Default for SsorParams {
58 fn default() -> Self {
59 Self { omega: 1.0 }
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub enum SsorError {
66 NonSquareMatrix { nrows: usize, ncols: usize },
68 MissingDiagonal { col: usize },
70 UnsortedRowIndices { col: usize },
72 ZeroDiagonal { col: usize },
74 InvalidOmega,
76 PatternMismatch,
79}
80
81impl core::fmt::Display for SsorError {
82 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
83 match self {
84 Self::NonSquareMatrix { nrows, ncols } => {
85 write!(f, "matrix must be square but is {nrows}x{ncols}")
86 }
87 Self::MissingDiagonal { col } => write!(f, "column {col} is missing its diagonal entry"),
88 Self::UnsortedRowIndices { col } => write!(f, "column {col} has unsorted row indices"),
89 Self::ZeroDiagonal { col } => write!(f, "diagonal entry {col} is zero"),
90 Self::InvalidOmega => f.write_str("omega must lie in the open interval (0, 2)"),
91 Self::PatternMismatch => f.write_str("refactorisation pattern does not match"),
92 }
93 }
94}
95
96impl core::error::Error for SsorError {}
97
98#[derive(Debug, Clone)]
105pub struct Ssor<I, T> {
106 pub(crate) dim: usize,
107 pub(crate) omega: f64,
108 pub(crate) scaled_diag: Vec<T>,
110 pub(crate) l_col_ptr: Vec<I>,
112 pub(crate) l_row_idx: Vec<I>,
113 pub(crate) l_values: Vec<T>,
114 pub(crate) u_col_ptr: Vec<I>,
116 pub(crate) u_row_idx: Vec<I>,
117 pub(crate) u_values: Vec<T>,
118 pub(crate) diag_pos: Vec<usize>,
119}
120
121impl<I, T> Ssor<I, T> {
122 #[inline]
124 pub fn dim(&self) -> usize {
125 self.dim
126 }
127
128 #[inline]
130 pub fn omega(&self) -> f64 {
131 self.omega
132 }
133}
134
135impl<I, T> LinOp<T> for Ssor<I, T>
136where
137 I: Index,
138 T: ComplexField + Debug + Sync,
139{
140 fn apply_scratch(&self, _rhs_ncols: usize, _par: Par) -> StackReq {
141 StackReq::EMPTY
142 }
143
144 fn nrows(&self) -> usize {
145 self.dim
146 }
147
148 fn ncols(&self) -> usize {
149 self.dim
150 }
151
152 fn apply(&self, mut out: MatMut<'_, T>, rhs: MatRef<'_, T>, par: Par, _stack: &mut MemStack) {
153 out.copy_from(rhs);
154 apply::solve_in_place(self, false, false, out, par);
155 }
156
157 fn conj_apply(
158 &self,
159 mut out: MatMut<'_, T>,
160 rhs: MatRef<'_, T>,
161 par: Par,
162 _stack: &mut MemStack,
163 ) {
164 out.copy_from(rhs);
165 apply::solve_in_place(self, false, true, out, par);
166 }
167}
168
169impl<I, T> Precond<T> for Ssor<I, T>
170where
171 I: Index,
172 T: ComplexField + Debug + Sync,
173{
174 fn apply_in_place_scratch(&self, _rhs_ncols: usize, _par: Par) -> StackReq {
175 StackReq::EMPTY
176 }
177
178 fn apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, _stack: &mut MemStack) {
179 apply::solve_in_place(self, false, false, rhs, par);
180 }
181
182 fn conj_apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, _stack: &mut MemStack) {
183 apply::solve_in_place(self, false, true, rhs, par);
184 }
185}
186
187impl<I, T> BiLinOp<T> for Ssor<I, T>
188where
189 I: Index,
190 T: ComplexField + Debug + Sync,
191{
192 fn transpose_apply_scratch(&self, _rhs_ncols: usize, _par: Par) -> StackReq {
193 StackReq::EMPTY
194 }
195
196 fn transpose_apply(
197 &self,
198 mut out: MatMut<'_, T>,
199 rhs: MatRef<'_, T>,
200 par: Par,
201 _stack: &mut MemStack,
202 ) {
203 out.copy_from(rhs);
204 apply::solve_in_place(self, true, false, out, par);
205 }
206
207 fn adjoint_apply(
208 &self,
209 mut out: MatMut<'_, T>,
210 rhs: MatRef<'_, T>,
211 par: Par,
212 _stack: &mut MemStack,
213 ) {
214 out.copy_from(rhs);
215 apply::solve_in_place(self, true, true, out, par);
216 }
217}
218
219impl<I, T> BiPrecond<T> for Ssor<I, T>
220where
221 I: Index,
222 T: ComplexField + Debug + Sync,
223{
224 fn transpose_apply_in_place_scratch(&self, _rhs_ncols: usize, _par: Par) -> StackReq {
225 StackReq::EMPTY
226 }
227
228 fn transpose_apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, _stack: &mut MemStack) {
229 apply::solve_in_place(self, true, false, rhs, par);
230 }
231
232 fn adjoint_apply_in_place(&self, rhs: MatMut<'_, T>, par: Par, _stack: &mut MemStack) {
233 apply::solve_in_place(self, true, true, rhs, par);
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use faer::sparse::{SparseColMat, Triplet};
241 use faer::{Mat, MatRef, mat};
242
243 fn assert_close(lhs: MatRef<'_, f64>, rhs: MatRef<'_, f64>, tol: f64) {
244 assert_eq!(lhs.nrows(), rhs.nrows());
245 assert_eq!(lhs.ncols(), rhs.ncols());
246 for j in 0..lhs.ncols() {
247 for i in 0..lhs.nrows() {
248 let diff = (*lhs.get(i, j) - *rhs.get(i, j)).abs();
249 assert!(
250 diff <= tol,
251 "mismatch at ({i}, {j}): lhs={}, rhs={}, diff={diff}",
252 *lhs.get(i, j),
253 *rhs.get(i, j),
254 );
255 }
256 }
257 }
258
259 fn to_dense(a: &SparseColMat<usize, f64>) -> Mat<f64> {
260 let n = a.nrows();
261 let mut out = Mat::<f64>::zeros(n, a.ncols());
262 let a_ref = a.as_ref();
263 for j in 0..a.ncols() {
264 let rows = a_ref.symbolic().row_idx_of_col_raw(j);
265 let vals = a_ref.val_of_col(j);
266 for (r, v) in rows.iter().zip(vals.iter()) {
267 *out.as_mut().get_mut(*r, j) = *v;
268 }
269 }
270 out
271 }
272
273 fn diagonal(diag: &[f64]) -> SparseColMat<usize, f64> {
274 let mut triplets = Vec::new();
275 for (i, &v) in diag.iter().enumerate() {
276 triplets.push(Triplet::new(i, i, v));
277 }
278 SparseColMat::try_new_from_triplets(diag.len(), diag.len(), &triplets).unwrap()
279 }
280
281 fn tridiagonal(n: usize, diag: f64, sub: f64, sup: f64) -> SparseColMat<usize, f64> {
282 let mut triplets = Vec::new();
283 for i in 0..n {
284 triplets.push(Triplet::new(i, i, diag));
285 if i > 0 {
286 triplets.push(Triplet::new(i, i - 1, sub));
287 triplets.push(Triplet::new(i - 1, i, sup));
288 }
289 }
290 SparseColMat::try_new_from_triplets(n, n, &triplets).unwrap()
291 }
292
293 fn laplacian_2d(grid: usize) -> SparseColMat<usize, f64> {
294 let n = grid * grid;
295 let mut triplets = Vec::new();
296 for gy in 0..grid {
297 for gx in 0..grid {
298 let idx = gy * grid + gx;
299 triplets.push(Triplet::new(idx, idx, 4.0));
300 if gx > 0 {
301 triplets.push(Triplet::new(idx, idx - 1, -1.0));
302 }
303 if gx + 1 < grid {
304 triplets.push(Triplet::new(idx, idx + 1, -1.0));
305 }
306 if gy > 0 {
307 triplets.push(Triplet::new(idx, idx - grid, -1.0));
308 }
309 if gy + 1 < grid {
310 triplets.push(Triplet::new(idx, idx + grid, -1.0));
311 }
312 }
313 }
314 SparseColMat::try_new_from_triplets(n, n, &triplets).unwrap()
315 }
316
317 fn residual_ratio(a: &SparseColMat<usize, f64>, pc: &Ssor<usize, f64>, b: &Mat<f64>) -> f64 {
318 let a_dense = to_dense(a);
319 let mut x = b.clone();
320 pc.apply_in_place(x.as_mut(), Par::Seq, MemStack::new(&mut []));
321 let residual = &a_dense * &x - b;
322 let b_norm: f64 = b.as_ref().col(0).iter().map(|v| v * v).sum::<f64>().sqrt();
323 let r_norm: f64 = residual
324 .as_ref()
325 .col(0)
326 .iter()
327 .map(|v| v * v)
328 .sum::<f64>()
329 .sqrt();
330 r_norm / b_norm
331 }
332
333 #[test]
334 fn sgs_on_diagonal_is_exact_inverse() {
335 let a = diagonal(&[2.0, 4.0, 8.0]);
337 let pc = Ssor::try_new(a.as_ref(), SsorParams::default()).unwrap();
338
339 let mut x = mat![[2.0_f64], [8.0], [16.0]];
340 pc.apply_in_place(x.as_mut(), Par::Seq, MemStack::new(&mut []));
341 let expected = mat![[1.0_f64], [2.0], [2.0]];
342 assert_close(x.as_ref(), expected.as_ref(), 1e-12);
343 }
344
345 #[test]
346 fn symmetric_input_makes_transpose_equal_apply() {
347 let a = tridiagonal(6, 4.0, -1.0, -1.0);
349 let pc = Ssor::try_new(a.as_ref(), SsorParams { omega: 1.3 }).unwrap();
350 let rhs = mat![[1.0_f64], [-2.0], [3.0], [0.5], [-1.0], [2.0]];
351
352 let mut fwd = rhs.clone();
353 pc.apply_in_place(fwd.as_mut(), Par::Seq, MemStack::new(&mut []));
354 let mut tr = rhs.clone();
355 pc.transpose_apply_in_place(tr.as_mut(), Par::Seq, MemStack::new(&mut []));
356 assert_close(fwd.as_ref(), tr.as_ref(), 1e-12);
357 }
358
359 #[test]
360 fn sgs_reduces_residual_on_laplacian() {
361 let a = laplacian_2d(8);
362 let n = a.nrows();
363 let pc = Ssor::try_new(a.as_ref(), SsorParams::default()).unwrap();
364 let b = Mat::<f64>::from_fn(n, 1, |i, _| (i % 7) as f64 - 3.0);
365 let ratio = residual_ratio(&a, &pc, &b);
366 assert!(ratio < 0.7, "SGS residual ratio {ratio} too large");
367 }
368
369 #[test]
370 fn out_of_place_matches_in_place() {
371 let a = tridiagonal(7, 4.0, -2.0, -1.0);
372 let pc = Ssor::try_new(a.as_ref(), SsorParams { omega: 1.2 }).unwrap();
373 let rhs = Mat::<f64>::from_fn(7, 2, |i, j| ((i + 2 * j) % 5) as f64 - 2.0);
374
375 let mut out = Mat::<f64>::zeros(7, 2);
376 pc.apply(out.as_mut(), rhs.as_ref(), Par::Seq, MemStack::new(&mut []));
377 let mut inplace = rhs.clone();
378 pc.apply_in_place(inplace.as_mut(), Par::Seq, MemStack::new(&mut []));
379 assert_close(out.as_ref(), inplace.as_ref(), 1e-12);
380 }
381
382 #[test]
383 fn refactorize_matches_fresh_construction() {
384 let a1 = tridiagonal(7, 4.0, -1.0, -1.0);
385 let a2 = tridiagonal(7, 5.0, -2.0, -1.5);
386 let params = SsorParams { omega: 1.4 };
387
388 let fresh = Ssor::try_new(a2.as_ref(), params).unwrap();
389 let mut reused = Ssor::try_new(a1.as_ref(), params).unwrap();
390 reused.refactorize(a2.as_ref()).unwrap();
391
392 assert_eq!(fresh.l_values.len(), reused.l_values.len());
393 for (a, b) in fresh.l_values.iter().zip(reused.l_values.iter()) {
394 assert!((a - b).abs() < 1e-14);
395 }
396 for (a, b) in fresh.u_values.iter().zip(reused.u_values.iter()) {
397 assert!((a - b).abs() < 1e-14);
398 }
399 for (a, b) in fresh.scaled_diag.iter().zip(reused.scaled_diag.iter()) {
400 assert!((a - b).abs() < 1e-14);
401 }
402 }
403
404 #[test]
405 fn rejects_invalid_omega() {
406 let a = tridiagonal(3, 4.0, -1.0, -1.0);
407 assert_eq!(
408 Ssor::try_new(a.as_ref(), SsorParams { omega: 0.0 }).unwrap_err(),
409 SsorError::InvalidOmega
410 );
411 assert_eq!(
412 Ssor::try_new(a.as_ref(), SsorParams { omega: 2.0 }).unwrap_err(),
413 SsorError::InvalidOmega
414 );
415 }
416
417 #[test]
418 fn rejects_zero_diagonal() {
419 let a = diagonal(&[1.0, 0.0, 1.0]);
420 assert_eq!(
421 Ssor::try_new(a.as_ref(), SsorParams::default()).unwrap_err(),
422 SsorError::ZeroDiagonal { col: 1 }
423 );
424 }
425
426 #[test]
427 fn rejects_non_square() {
428 let mut triplets = Vec::new();
429 for i in 0..3 {
430 triplets.push(Triplet::new(i, i, 1.0));
431 }
432 let a = SparseColMat::<usize, f64>::try_new_from_triplets(3, 4, &triplets).unwrap();
433 assert_eq!(
434 Ssor::try_new(a.as_ref(), SsorParams::default()).unwrap_err(),
435 SsorError::NonSquareMatrix { nrows: 3, ncols: 4 }
436 );
437 }
438}