Skip to main content

faer_precond/adapters/
solve.rs

1//! Use an exact factorisation as a preconditioner.
2//!
3//! [`SolvePrecond`] wraps any faer factorisation implementing `SolveCore`
4//! (`Llt`, `Ldlt`, `Lu`, `Qr`, ...) and exposes it through the preconditioner
5//! traits. Each apply is a forward/back substitution against the stored
6//! factors, so "apply the preconditioner" becomes "solve with this
7//! factorisation".
8//!
9//! # When to use it
10//!
11//! This is an *adapter*, not a factorisation method — it does no incomplete or
12//! approximate work of its own. The useful pattern is to factorise something
13//! *cheaper than the full `A`* and precondition `A` with it:
14//!
15//! - a lower-fidelity discretisation of the same problem,
16//! - a frozen- or averaged-coefficient version of `A`,
17//! - a single dominant block of a larger system,
18//! - or any factorisation you already have lying around for another reason.
19//!
20//! The Krylov method then only has to correct for whatever the approximation
21//! left out. (Factorising the *full* `A` exactly and preconditioning `A` with
22//! it converges in one step — but then you may as well have called the solver
23//! directly.)
24
25use core::fmt::Debug;
26
27use dyn_stack::{MemStack, StackReq};
28use faer::{
29    Conj, MatMut, MatRef, Par,
30    linalg::solvers::{ShapeCore, SolveCore},
31    matrix_free::{BiLinOp, BiPrecond, LinOp, Precond},
32};
33use faer_traits::ComplexField;
34
35/// Adapter exposing a faer factorisation `S` as a preconditioner.
36///
37/// See the [module documentation](self) for the intended use. `S` is any
38/// factorisation implementing `SolveCore` (`Llt`, `Ldlt`, `Lu`, `Qr`, ...).
39#[derive(Debug, Clone)]
40pub struct SolvePrecond<S> {
41    solver: S,
42}
43
44impl<S> SolvePrecond<S> {
45    /// Wrap a factorisation as a preconditioner.
46    pub fn new(solver: S) -> Self {
47        Self { solver }
48    }
49
50    /// Borrow the wrapped factorisation.
51    pub fn inner(&self) -> &S {
52        &self.solver
53    }
54
55    /// Unwrap and return the factorisation.
56    pub fn into_inner(self) -> S {
57        self.solver
58    }
59}
60
61impl<T, S> LinOp<T> for SolvePrecond<S>
62where
63    T: ComplexField,
64    S: ShapeCore + SolveCore<T> + Sync + Debug,
65{
66    fn apply_scratch(&self, _rhs_ncols: usize, _par: Par) -> StackReq {
67        StackReq::EMPTY
68    }
69
70    fn nrows(&self) -> usize {
71        self.solver.nrows()
72    }
73
74    fn ncols(&self) -> usize {
75        self.solver.ncols()
76    }
77
78    fn apply(&self, mut out: MatMut<'_, T>, rhs: MatRef<'_, T>, _par: Par, _stack: &mut MemStack) {
79        assert_eq!(
80            rhs.nrows(),
81            self.ncols(),
82            "rhs row count must match operator input dimension"
83        );
84        assert_eq!(
85            out.nrows(),
86            self.nrows(),
87            "out row count must match operator output dimension"
88        );
89        assert_eq!(
90            out.ncols(),
91            rhs.ncols(),
92            "out and rhs must have the same number of columns"
93        );
94
95        out.copy_from(rhs);
96        self.solver.solve_in_place_with_conj(Conj::No, out);
97    }
98
99    fn conj_apply(
100        &self,
101        mut out: MatMut<'_, T>,
102        rhs: MatRef<'_, T>,
103        _par: Par,
104        _stack: &mut MemStack,
105    ) {
106        assert_eq!(
107            rhs.nrows(),
108            self.ncols(),
109            "rhs row count must match operator input dimension"
110        );
111        assert_eq!(
112            out.nrows(),
113            self.nrows(),
114            "out row count must match operator output dimension"
115        );
116        assert_eq!(
117            out.ncols(),
118            rhs.ncols(),
119            "out and rhs must have the same number of columns"
120        );
121
122        out.copy_from(rhs);
123        self.solver.solve_in_place_with_conj(Conj::Yes, out);
124    }
125}
126
127impl<T, S> Precond<T> for SolvePrecond<S>
128where
129    T: ComplexField,
130    S: ShapeCore + SolveCore<T> + Sync + Debug,
131{
132    fn apply_in_place_scratch(&self, _rhs_ncols: usize, _par: Par) -> StackReq {
133        StackReq::EMPTY
134    }
135
136    fn apply_in_place(&self, rhs: MatMut<'_, T>, _par: Par, _stack: &mut MemStack) {
137        self.solver.solve_in_place_with_conj(Conj::No, rhs);
138    }
139
140    fn conj_apply_in_place(&self, rhs: MatMut<'_, T>, _par: Par, _stack: &mut MemStack) {
141        self.solver.solve_in_place_with_conj(Conj::Yes, rhs);
142    }
143}
144
145impl<T, S> BiLinOp<T> for SolvePrecond<S>
146where
147    T: ComplexField,
148    S: ShapeCore + SolveCore<T> + Sync + Debug,
149{
150    fn transpose_apply_scratch(&self, _rhs_ncols: usize, _par: Par) -> StackReq {
151        StackReq::EMPTY
152    }
153
154    fn transpose_apply(
155        &self,
156        mut out: MatMut<'_, T>,
157        rhs: MatRef<'_, T>,
158        _par: Par,
159        _stack: &mut MemStack,
160    ) {
161        assert_eq!(
162            rhs.nrows(),
163            self.ncols(),
164            "rhs row count must match operator input dimension"
165        );
166        assert_eq!(
167            out.nrows(),
168            self.nrows(),
169            "out row count must match operator output dimension"
170        );
171        assert_eq!(
172            out.ncols(),
173            rhs.ncols(),
174            "out and rhs must have the same number of columns"
175        );
176
177        out.copy_from(rhs);
178        self.solver
179            .solve_transpose_in_place_with_conj(Conj::No, out);
180    }
181
182    fn adjoint_apply(
183        &self,
184        mut out: MatMut<'_, T>,
185        rhs: MatRef<'_, T>,
186        _par: Par,
187        _stack: &mut MemStack,
188    ) {
189        assert_eq!(
190            rhs.nrows(),
191            self.ncols(),
192            "rhs row count must match operator input dimension"
193        );
194        assert_eq!(
195            out.nrows(),
196            self.nrows(),
197            "out row count must match operator output dimension"
198        );
199        assert_eq!(
200            out.ncols(),
201            rhs.ncols(),
202            "out and rhs must have the same number of columns"
203        );
204
205        out.copy_from(rhs);
206        self.solver
207            .solve_transpose_in_place_with_conj(Conj::Yes, out);
208    }
209}
210
211impl<T, S> BiPrecond<T> for SolvePrecond<S>
212where
213    T: ComplexField,
214    S: ShapeCore + SolveCore<T> + Sync + Debug,
215{
216    fn transpose_apply_in_place_scratch(&self, _rhs_ncols: usize, _par: Par) -> StackReq {
217        StackReq::EMPTY
218    }
219
220    fn transpose_apply_in_place(&self, rhs: MatMut<'_, T>, _par: Par, _stack: &mut MemStack) {
221        self.solver
222            .solve_transpose_in_place_with_conj(Conj::No, rhs);
223    }
224
225    fn adjoint_apply_in_place(&self, rhs: MatMut<'_, T>, _par: Par, _stack: &mut MemStack) {
226        self.solver
227            .solve_transpose_in_place_with_conj(Conj::Yes, rhs);
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use core::mem::MaybeUninit;
234
235    use super::*;
236    use faer::{
237        Mat, MatRef, Side,
238        linalg::solvers::Llt,
239        mat,
240        matrix_free::{BiLinOp, BiPrecond, LinOp, Precond},
241    };
242
243    fn with_stack(req: StackReq, f: impl FnOnce(&mut MemStack)) {
244        let nbytes = req.unaligned_bytes_required().max(1);
245        let mut buf = vec![MaybeUninit::<u8>::uninit(); nbytes].into_boxed_slice();
246        f(MemStack::new(&mut buf));
247    }
248
249    fn assert_close(lhs: MatRef<'_, f64>, rhs: MatRef<'_, f64>, tol: f64) {
250        assert_eq!(lhs.nrows(), rhs.nrows());
251        assert_eq!(lhs.ncols(), rhs.ncols());
252
253        for j in 0..lhs.ncols() {
254            for i in 0..lhs.nrows() {
255                let diff = (*lhs.get(i, j) - *rhs.get(i, j)).abs();
256                assert!(
257                    diff <= tol,
258                    "mismatch at ({i}, {j}): lhs={}, rhs={}, diff={diff}",
259                    *lhs.get(i, j),
260                    *rhs.get(i, j),
261                );
262            }
263        }
264    }
265
266    fn test_solver() -> SolvePrecond<Llt<f64>> {
267        let a = mat![[4.0, 1.0], [1.0, 3.0f64],];
268        let llt = Llt::new(a.as_ref(), Side::Lower).expect("matrix should be SPD");
269        SolvePrecond::new(llt)
270    }
271
272    #[test]
273    fn exposes_dimensions() {
274        let pc = test_solver();
275        assert_eq!(pc.nrows(), 2);
276        assert_eq!(pc.ncols(), 2);
277    }
278
279    #[test]
280    fn apply_solves_system() {
281        let pc = test_solver();
282        let rhs = mat![[1.0], [2.0f64],];
283        let mut out = Mat::<f64>::zeros(2, 1);
284
285        with_stack(pc.apply_scratch(rhs.ncols(), Par::Seq), |stack| {
286            pc.apply(out.as_mut(), rhs.as_ref(), Par::Seq, stack);
287        });
288
289        let expected = mat![[1.0 / 11.0], [7.0 / 11.0f64],];
290        assert_close(out.as_ref(), expected.as_ref(), 1e-12);
291    }
292
293    #[test]
294    fn apply_in_place_matches_apply() {
295        let pc = test_solver();
296        let rhs = mat![[1.0], [2.0f64],];
297
298        let mut out = Mat::<f64>::zeros(2, 1);
299        with_stack(pc.apply_scratch(rhs.ncols(), Par::Seq), |stack| {
300            pc.apply(out.as_mut(), rhs.as_ref(), Par::Seq, stack);
301        });
302
303        let mut inplace = rhs.to_owned();
304        with_stack(
305            pc.apply_in_place_scratch(inplace.ncols(), Par::Seq),
306            |stack| {
307                pc.apply_in_place(inplace.as_mut(), Par::Seq, stack);
308            },
309        );
310
311        assert_close(out.as_ref(), inplace.as_ref(), 1e-12);
312    }
313
314    #[test]
315    fn transpose_apply_is_usable() {
316        let pc = test_solver();
317        let rhs = mat![[1.0], [2.0f64],];
318        let mut out = Mat::<f64>::zeros(2, 1);
319
320        with_stack(pc.transpose_apply_scratch(rhs.ncols(), Par::Seq), |stack| {
321            pc.transpose_apply(out.as_mut(), rhs.as_ref(), Par::Seq, stack);
322        });
323
324        let expected = mat![[1.0 / 11.0], [7.0 / 11.0f64],];
325        assert_close(out.as_ref(), expected.as_ref(), 1e-12);
326    }
327
328    #[test]
329    fn adjoint_apply_is_usable() {
330        let pc = test_solver();
331        let rhs = mat![[1.0], [2.0f64],];
332        let mut out = Mat::<f64>::zeros(2, 1);
333
334        with_stack(pc.transpose_apply_scratch(rhs.ncols(), Par::Seq), |stack| {
335            pc.adjoint_apply(out.as_mut(), rhs.as_ref(), Par::Seq, stack);
336        });
337
338        let expected = mat![[1.0 / 11.0], [7.0 / 11.0f64],];
339        assert_close(out.as_ref(), expected.as_ref(), 1e-12);
340    }
341
342    #[test]
343    fn transpose_and_adjoint_in_place_are_usable() {
344        let pc = test_solver();
345
346        let mut rhs_t = mat![[1.0], [2.0f64],];
347        with_stack(
348            pc.transpose_apply_in_place_scratch(rhs_t.ncols(), Par::Seq),
349            |stack| {
350                pc.transpose_apply_in_place(rhs_t.as_mut(), Par::Seq, stack);
351            },
352        );
353
354        let mut rhs_h = mat![[1.0], [2.0f64],];
355        with_stack(
356            pc.transpose_apply_in_place_scratch(rhs_h.ncols(), Par::Seq),
357            |stack| {
358                pc.adjoint_apply_in_place(rhs_h.as_mut(), Par::Seq, stack);
359            },
360        );
361
362        let expected = mat![[1.0 / 11.0], [7.0 / 11.0f64],];
363        assert_close(rhs_t.as_ref(), expected.as_ref(), 1e-12);
364        assert_close(rhs_h.as_ref(), expected.as_ref(), 1e-12);
365    }
366
367    #[test]
368    fn inner_accessors_work() {
369        let pc = test_solver();
370        assert_eq!(pc.inner().nrows(), 2);
371
372        let pc2 = test_solver();
373        let solver = pc2.into_inner();
374        assert_eq!(solver.nrows(), 2);
375    }
376}