Skip to main content

faer_precond/
jacobi.rs

1//! Point-Jacobi (diagonal) preconditioner.
2//!
3//! The simplest preconditioner there is: `M = diag(A)`. Applying it divides
4//! each entry of the residual by the matching diagonal entry of `A`. There is
5//! no factorisation and no fill-in — construction inverts the diagonal once,
6//! and every apply is a single element-wise multiply, costing `O(n)`.
7//!
8//! # When to use it
9//!
10//! Jacobi helps only when the diagonal actually carries information about how
11//! the problem is scaled — that is, when `A` is diagonally dominant or its rows
12//! differ widely in magnitude. Variable-coefficient PDEs, reaction terms, and
13//! badly-scaled unknowns all fit. Because it costs almost nothing to build or
14//! apply, it is the natural first thing to try on any new system.
15//!
16//! It does **nothing** when the diagonal is constant: scaling every equation by
17//! the same number leaves the spectrum unchanged, so the iterative solver takes
18//! exactly as many steps as it would with no preconditioner. The constant-
19//! coefficient Laplacian is the textbook example. For those problems use
20//! [`crate::Ic0`] (symmetric) or [`crate::Ilu0`] (general) instead.
21
22use core::fmt::Debug;
23
24use dyn_stack::{MemStack, StackReq};
25use faer::{
26    MatMut, MatRef, Par,
27    matrix_free::{BiLinOp, BiPrecond, LinOp, Precond},
28    prelude::ReborrowMut,
29};
30use faer_traits::ComplexField;
31use faer_traits::math_utils::{abs2, conj, copy, mul, recip, zero};
32
33/// Error returned when building a [`JacobiPrecond`].
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum JacobiError {
36    /// The source matrix was not square.
37    NonSquareMatrix { nrows: usize, ncols: usize },
38    /// Diagonal entry `index` was zero, so it cannot be inverted.
39    ZeroDiagonalEntry { index: usize },
40}
41
42/// Point-Jacobi preconditioner: `M^{-1} y = diag(A)^{-1} y`.
43///
44/// Stores the reciprocals of `A`'s diagonal and multiplies them into the
45/// right-hand side. Apply is `O(n)` and allocates nothing. See the
46/// [module documentation](self) for when this is the right choice.
47#[derive(Debug, Clone)]
48pub struct JacobiPrecond<T> {
49    inv_diag: Vec<T>,
50}
51
52impl<T> JacobiPrecond<T> {
53    /// Build directly from the already-inverted diagonal `1 / diag(A)`.
54    ///
55    /// No checking is done — use this only when you have computed the
56    /// reciprocals yourself. Prefer [`Self::try_from_diagonal`] otherwise.
57    pub fn from_inverse_diagonal(inv_diag: Vec<T>) -> Self {
58        Self { inv_diag }
59    }
60
61    /// The stored inverse diagonal, `1 / diag(A)`.
62    pub fn inverse_diagonal(&self) -> &[T] {
63        &self.inv_diag
64    }
65
66    /// Dimension `n` of the preconditioner.
67    pub fn dim(&self) -> usize {
68        self.inv_diag.len()
69    }
70
71    /// `true` if the preconditioner has dimension zero.
72    pub fn is_empty(&self) -> bool {
73        self.inv_diag.is_empty()
74    }
75}
76
77impl<T: ComplexField> JacobiPrecond<T> {
78    /// Build from a slice holding the diagonal of `A`.
79    ///
80    /// # Errors
81    ///
82    /// Returns [`JacobiError::ZeroDiagonalEntry`] if any entry is zero.
83    pub fn try_from_diagonal(diag: &[T]) -> Result<Self, JacobiError> {
84        let mut inv_diag = Vec::with_capacity(diag.len());
85
86        for (index, value) in diag.iter().enumerate() {
87            if abs2(value) == zero::<T::Real>() {
88                return Err(JacobiError::ZeroDiagonalEntry { index });
89            }
90            inv_diag.push(recip(value));
91        }
92
93        Ok(Self { inv_diag })
94    }
95
96    /// Build by reading the diagonal of a dense matrix `mat`.
97    ///
98    /// # Errors
99    ///
100    /// Returns [`JacobiError::NonSquareMatrix`] if `mat` is not square, or
101    /// [`JacobiError::ZeroDiagonalEntry`] if any diagonal entry is zero.
102    pub fn try_from_matrix_diagonal(mat: MatRef<'_, T>) -> Result<Self, JacobiError> {
103        if mat.nrows() != mat.ncols() {
104            return Err(JacobiError::NonSquareMatrix {
105                nrows: mat.nrows(),
106                ncols: mat.ncols(),
107            });
108        }
109
110        let mut diag = Vec::with_capacity(mat.nrows());
111        for i in 0..mat.nrows() {
112            diag.push(copy(mat.get(i, i)));
113        }
114
115        Self::try_from_diagonal(&diag)
116    }
117
118    #[inline]
119    fn check_dims(&self, out_nrows: usize, rhs_nrows: usize, rhs_ncols: usize, out_ncols: usize) {
120        assert_eq!(
121            rhs_nrows,
122            self.dim(),
123            "rhs row count must match preconditioner dimension"
124        );
125        assert_eq!(
126            out_nrows,
127            self.dim(),
128            "out row count must match preconditioner dimension"
129        );
130        assert_eq!(
131            out_ncols, rhs_ncols,
132            "out and rhs must have the same number of columns"
133        );
134    }
135
136    #[inline]
137    fn apply_scale_to_out(&self, mut out: MatMut<'_, T>, rhs: MatRef<'_, T>, conjugate_diag: bool) {
138        self.check_dims(out.nrows(), rhs.nrows(), rhs.ncols(), out.ncols());
139
140        for j in 0..rhs.ncols() {
141            for i in 0..rhs.nrows() {
142                let scale = if conjugate_diag {
143                    conj(&self.inv_diag[i])
144                } else {
145                    copy(&self.inv_diag[i])
146                };
147                *out.rb_mut().get_mut(i, j) = mul(&scale, rhs.get(i, j));
148            }
149        }
150    }
151
152    #[inline]
153    fn apply_scale_in_place(&self, mut rhs: MatMut<'_, T>, conjugate_diag: bool) {
154        assert_eq!(
155            rhs.nrows(),
156            self.dim(),
157            "rhs row count must match preconditioner dimension"
158        );
159
160        for j in 0..rhs.ncols() {
161            for i in 0..rhs.nrows() {
162                let scale = if conjugate_diag {
163                    conj(&self.inv_diag[i])
164                } else {
165                    copy(&self.inv_diag[i])
166                };
167                let elem = rhs.rb_mut().get_mut(i, j);
168                *elem = mul(&scale, elem);
169            }
170        }
171    }
172}
173
174impl<T> LinOp<T> for JacobiPrecond<T>
175where
176    T: ComplexField + Debug + Sync,
177{
178    fn apply_scratch(&self, _rhs_ncols: usize, _par: Par) -> StackReq {
179        StackReq::EMPTY
180    }
181
182    fn nrows(&self) -> usize {
183        self.dim()
184    }
185
186    fn ncols(&self) -> usize {
187        self.dim()
188    }
189
190    fn apply(&self, out: MatMut<'_, T>, rhs: MatRef<'_, T>, _par: Par, _stack: &mut MemStack) {
191        self.apply_scale_to_out(out, rhs, false);
192    }
193
194    fn conj_apply(&self, out: MatMut<'_, T>, rhs: MatRef<'_, T>, _par: Par, _stack: &mut MemStack) {
195        self.apply_scale_to_out(out, rhs, true);
196    }
197}
198
199impl<T> Precond<T> for JacobiPrecond<T>
200where
201    T: ComplexField + Debug + Sync,
202{
203    fn apply_in_place_scratch(&self, _rhs_ncols: usize, _par: Par) -> StackReq {
204        StackReq::EMPTY
205    }
206
207    fn apply_in_place(&self, rhs: MatMut<'_, T>, _par: Par, _stack: &mut MemStack) {
208        self.apply_scale_in_place(rhs, false);
209    }
210
211    fn conj_apply_in_place(&self, rhs: MatMut<'_, T>, _par: Par, _stack: &mut MemStack) {
212        self.apply_scale_in_place(rhs, true);
213    }
214}
215
216impl<T> BiLinOp<T> for JacobiPrecond<T>
217where
218    T: ComplexField + Debug + Sync,
219{
220    fn transpose_apply_scratch(&self, _rhs_ncols: usize, _par: Par) -> StackReq {
221        StackReq::EMPTY
222    }
223
224    fn transpose_apply(
225        &self,
226        out: MatMut<'_, T>,
227        rhs: MatRef<'_, T>,
228        _par: Par,
229        _stack: &mut MemStack,
230    ) {
231        // diagonal operator: transpose is identical
232        self.apply_scale_to_out(out, rhs, false);
233    }
234
235    fn adjoint_apply(
236        &self,
237        out: MatMut<'_, T>,
238        rhs: MatRef<'_, T>,
239        _par: Par,
240        _stack: &mut MemStack,
241    ) {
242        // diagonal operator: adjoint conjugates the diagonal
243        self.apply_scale_to_out(out, rhs, true);
244    }
245}
246
247impl<T> BiPrecond<T> for JacobiPrecond<T>
248where
249    T: ComplexField + Debug + Sync,
250{
251    fn transpose_apply_in_place_scratch(&self, _rhs_ncols: usize, _par: Par) -> StackReq {
252        StackReq::EMPTY
253    }
254
255    fn transpose_apply_in_place(&self, rhs: MatMut<'_, T>, _par: Par, _stack: &mut MemStack) {
256        self.apply_scale_in_place(rhs, false);
257    }
258
259    fn adjoint_apply_in_place(&self, rhs: MatMut<'_, T>, _par: Par, _stack: &mut MemStack) {
260        self.apply_scale_in_place(rhs, true);
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use core::mem::MaybeUninit;
267
268    use super::*;
269    use faer::{
270        Mat, MatRef, mat,
271        matrix_free::{BiLinOp, BiPrecond, LinOp, Precond},
272    };
273
274    fn with_stack(req: StackReq, f: impl FnOnce(&mut MemStack)) {
275        let nbytes = req.unaligned_bytes_required().max(1);
276        let mut buf = vec![MaybeUninit::<u8>::uninit(); nbytes].into_boxed_slice();
277        f(MemStack::new(&mut buf));
278    }
279
280    fn assert_close(lhs: MatRef<'_, f64>, rhs: MatRef<'_, f64>, tol: f64) {
281        assert_eq!(lhs.nrows(), rhs.nrows());
282        assert_eq!(lhs.ncols(), rhs.ncols());
283
284        for j in 0..lhs.ncols() {
285            for i in 0..lhs.nrows() {
286                let diff = (*lhs.get(i, j) - *rhs.get(i, j)).abs();
287                assert!(
288                    diff <= tol,
289                    "mismatch at ({i}, {j}): lhs={}, rhs={}, diff={diff}",
290                    *lhs.get(i, j),
291                    *rhs.get(i, j),
292                );
293            }
294        }
295    }
296
297    #[test]
298    fn builds_from_diagonal() {
299        let pc = JacobiPrecond::try_from_diagonal(&[2.0, 4.0, 8.0]).unwrap();
300        assert_eq!(pc.dim(), 3);
301        assert_eq!(pc.inverse_diagonal(), &[0.5, 0.25, 0.125]);
302    }
303
304    #[test]
305    fn rejects_zero_diagonal() {
306        let err = JacobiPrecond::try_from_diagonal(&[2.0, 0.0, 8.0]).unwrap_err();
307        assert_eq!(err, JacobiError::ZeroDiagonalEntry { index: 1 });
308    }
309
310    #[test]
311    fn builds_from_matrix_diagonal() {
312        let a = mat![[2.0, 9.0, 0.0], [1.0, 4.0, 5.0], [0.0, 7.0, 8.0f64],];
313
314        let pc = JacobiPrecond::try_from_matrix_diagonal(a.as_ref()).unwrap();
315        assert_eq!(pc.inverse_diagonal(), &[0.5, 0.25, 0.125]);
316    }
317
318    #[test]
319    fn apply_matches_expected_multiple_rhs() {
320        let pc = JacobiPrecond::try_from_diagonal(&[2.0, 4.0]).unwrap();
321        let rhs = mat![[2.0, 8.0], [4.0, 12.0f64],];
322        let mut out = Mat::<f64>::zeros(2, 2);
323
324        let req = pc.apply_scratch(rhs.ncols(), Par::Seq);
325        with_stack(req, |stack| {
326            pc.apply(out.as_mut(), rhs.as_ref(), Par::Seq, stack);
327        });
328
329        let expected = mat![[1.0, 4.0], [1.0, 3.0f64],];
330        assert_close(out.as_ref(), expected.as_ref(), 1e-12);
331    }
332
333    #[test]
334    fn apply_in_place_matches_apply() {
335        let pc = JacobiPrecond::try_from_diagonal(&[2.0, 4.0]).unwrap();
336        let rhs = mat![[2.0, 8.0], [4.0, 12.0f64],];
337
338        let mut out = Mat::<f64>::zeros(2, 2);
339        with_stack(pc.apply_scratch(rhs.ncols(), Par::Seq), |stack| {
340            pc.apply(out.as_mut(), rhs.as_ref(), Par::Seq, stack);
341        });
342
343        let mut inplace = rhs.to_owned();
344        with_stack(
345            pc.apply_in_place_scratch(inplace.ncols(), Par::Seq),
346            |stack| {
347                pc.apply_in_place(inplace.as_mut(), Par::Seq, stack);
348            },
349        );
350
351        assert_close(out.as_ref(), inplace.as_ref(), 1e-12);
352    }
353
354    #[test]
355    fn transpose_and_adjoint_are_usable() {
356        let pc = JacobiPrecond::try_from_diagonal(&[2.0, 4.0]).unwrap();
357        let rhs = mat![[2.0], [4.0f64],];
358
359        let mut out_t = Mat::<f64>::zeros(2, 1);
360        with_stack(pc.transpose_apply_scratch(rhs.ncols(), Par::Seq), |stack| {
361            pc.transpose_apply(out_t.as_mut(), rhs.as_ref(), Par::Seq, stack);
362        });
363
364        let mut out_h = Mat::<f64>::zeros(2, 1);
365        with_stack(pc.transpose_apply_scratch(rhs.ncols(), Par::Seq), |stack| {
366            pc.adjoint_apply(out_h.as_mut(), rhs.as_ref(), Par::Seq, stack);
367        });
368
369        let expected = mat![[1.0], [1.0f64],];
370        assert_close(out_t.as_ref(), expected.as_ref(), 1e-12);
371        assert_close(out_h.as_ref(), expected.as_ref(), 1e-12);
372    }
373
374    #[test]
375    fn transpose_and_adjoint_in_place_are_usable() {
376        let pc = JacobiPrecond::try_from_diagonal(&[2.0, 4.0]).unwrap();
377
378        let mut rhs_t = mat![[2.0], [4.0f64],];
379        with_stack(
380            pc.transpose_apply_in_place_scratch(rhs_t.ncols(), Par::Seq),
381            |stack| {
382                pc.transpose_apply_in_place(rhs_t.as_mut(), Par::Seq, stack);
383            },
384        );
385
386        let mut rhs_h = mat![[2.0], [4.0f64],];
387        with_stack(
388            pc.transpose_apply_in_place_scratch(rhs_h.ncols(), Par::Seq),
389            |stack| {
390                pc.adjoint_apply_in_place(rhs_h.as_mut(), Par::Seq, stack);
391            },
392        );
393
394        let expected = mat![[1.0], [1.0f64],];
395        assert_close(rhs_t.as_ref(), expected.as_ref(), 1e-12);
396        assert_close(rhs_h.as_ref(), expected.as_ref(), 1e-12);
397    }
398}