outram_foam_basic_lib/linear_operator.rs
1// SPDX-License-Identifier: GPL-3.0-only
2// Copyright (C) 2026 OUTRAM PARK contributors
3//
4// This file is part of OUTRAM PARK.
5//
6// OUTRAM PARK is free software: you can redistribute it and/or modify it
7// under the terms of the GNU General Public License as published by the
8// Free Software Foundation, either version 3 of the License, or (at your
9// option) any later version.
10//
11// OUTRAM PARK is distributed in the hope that it will be useful, but
12// WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14// General Public License for more details.
15//
16// You should have received a copy of the GNU General Public License along
17// with OUTRAM PARK. If not, see <https://www.gnu.org/licenses/>.
18
19//! Discretisation-agnostic linear-algebra contract, plus Krylov solvers written
20//! against it.
21//!
22//! # What belongs in this module
23//!
24//! The [`LinearOperator`] contract — "I am an `n x n` linear map you can apply
25//! to a vector, and I can hand you my diagonal" — and the
26//! [`OperatorPreconditioner`] contract, together with the three matrix-free
27//! Krylov drivers [`cg_op`], [`bicgstab_op`] and [`gmres_op`] that need nothing
28//! more than those two contracts.
29//!
30//! # What does NOT belong here
31//!
32//! Any storage format. This module stores no coefficients of its own: it is the
33//! seam at which a *representation* (LDU for the face-addressed finite-volume
34//! side, CSR for a finite-element side) meets the *solvers*.
35//!
36//! # Why it exists
37//!
38//! Before this module every Krylov entry point in the crate
39//! ([`crate::krylov::gmres`], [`crate::krylov::bicgstab`],
40//! [`crate::ldu_matrix::solvers::conjugate_gradient`], GAMG, Gauss-Seidel) took
41//! a concrete `&LduMatrix`. `LduMatrix` is a *good* representation for a
42//! finite-volume mesh — one coefficient per internal face, owner/neighbour
43//! addressing straight off the mesh — and a poor one for a finite-element
44//! stiffness matrix, whose degree-of-freedom graph is denser than the cell graph
45//! and has no face to hang a coefficient on.
46//!
47//! A finite-element crate therefore had exactly two bad options: reformulate
48//! structural mechanics as finite volume so it could reuse the solvers, or
49//! reimplement the solvers so it could keep its own matrix. This module is the
50//! third option (GitHub issue #175): **one Krylov layer, two matrix layouts.**
51//!
52//! # Additive by construction
53//!
54//! Nothing in this module changes any existing function. The existing
55//! finite-volume solvers keep their `&LduMatrix` signatures and their exact
56//! behaviour; the functions here are *new* generic siblings. [`LduMatrix`]
57//! implements [`LinearOperator`], so the finite-volume side may use them too,
58//! but is not obliged to.
59//!
60//! # No trait objects
61//!
62//! Per the workspace design rules, [`LinearOperator`] and
63//! [`OperatorPreconditioner`] are used **only as generic bounds**
64//! (`fn cg_op<A: LinearOperator, M: OperatorPreconditioner>`), never as
65//! `Box<dyn LinearOperator>`. Each call site monomorphises to one concrete
66//! matrix type, so the matrix-vector product inlines exactly as it would have
67//! done in a hand-written solver.
68//!
69//! # Units
70//!
71//! Everything here is dimensionless `f64`. A Krylov subspace mixes residuals,
72//! search directions and solution increments that share no single physical
73//! dimension, so no `uom` typing is applied — exactly as in
74//! [`crate::krylov`]. Apply units at the layer that assembles the matrix.
75//!
76//! # Example
77//!
78//! ```rust
79//! use outram_foam_basic_lib::krylov::KrylovSettings;
80//! use outram_foam_basic_lib::ldu_matrix::LduMatrix;
81//! use outram_foam_basic_lib::linear_operator::{cg_op, JacobiOperatorPreconditioner};
82//!
83//! // Symmetric positive-definite 3-cell chain.
84//! let mut a = LduMatrix::new(3, vec![0, 1], vec![1, 2]);
85//! a.diag = vec![4.0, 4.0, 4.0];
86//! a.lower = vec![-1.0, -1.0];
87//! a.upper = vec![-1.0, -1.0];
88//! let b = vec![1.0, 2.0, 3.0];
89//!
90//! let m = JacobiOperatorPreconditioner::new(&a);
91//! let settings = KrylovSettings { tolerance: 1e-12, max_iter: 100, restart: 30 };
92//! let (x, result) = cg_op(&a, &b, None, &m, &settings);
93//!
94//! assert!(result.converged);
95//! let mut ax = vec![0.0; 3];
96//! outram_foam_basic_lib::linear_operator::LinearOperator::apply(&a, &x, &mut ax);
97//! for i in 0..3 {
98//! assert!((ax[i] - b[i]).abs() < 1e-9);
99//! }
100//! ```
101
102use crate::krylov::{KrylovResult, KrylovSettings};
103use crate::ldu_matrix::LduMatrix;
104
105/// A square linear map `A` that can be applied to a vector.
106///
107/// This is the single contract the discretisation-agnostic Krylov solvers in
108/// this module need. Implement it for any sparse (or dense, or matrix-free)
109/// representation and [`cg_op`] / [`bicgstab_op`] / [`gmres_op`] work on it
110/// unchanged.
111///
112/// # Contract
113///
114/// - [`n_rows`](Self::n_rows) and [`n_cols`](Self::n_cols) must be equal and
115/// constant for the lifetime of the operator: these solvers are square-system
116/// solvers.
117/// - [`apply`](Self::apply) must be **linear** and **deterministic**: the same
118/// `x` must give bitwise the same `y`, and `A(u + v) = Au + Av` to rounding.
119/// A Krylov method that is handed a non-linear or noisy operator does not
120/// merely converge slowly, it converges to the wrong answer.
121/// - [`diagonal`](Self::diagonal) must write `A[i][i]` into `d[i]`.
122///
123/// # Units
124///
125/// Dimensionless. `x` and `y` are bare `f64` slices in whatever units the
126/// assembling layer chose; this contract neither knows nor enforces them.
127///
128/// # Implemented by
129///
130/// [`LduMatrix`] in this crate (face-addressed, finite-volume), and
131/// `farrer_park::sparse::CsrMatrix` (row-compressed, finite-element).
132#[diagnostic::on_unimplemented(
133 message = "`{Self}` cannot be used as a Krylov system matrix: it does not implement `LinearOperator`",
134 label = "not a linear operator",
135 note = "implemented by `outram_foam_basic_lib::ldu_matrix::LduMatrix` (finite volume) and `farrer_park::sparse::CsrMatrix` (finite element)",
136 note = "to add your own, implement `n_rows`, `n_cols`, `apply` and `diagonal`"
137)]
138pub trait LinearOperator {
139 /// Number of rows of `A` (dimensionless count). Equals the length of `y` in
140 /// [`apply`](Self::apply).
141 fn n_rows(&self) -> usize;
142
143 /// Number of columns of `A` (dimensionless count). Equals the length of `x`
144 /// in [`apply`](Self::apply). Must equal [`n_rows`](Self::n_rows).
145 fn n_cols(&self) -> usize;
146
147 /// Write the matrix-vector product `y = A x`.
148 ///
149 /// `x` has length [`n_cols`](Self::n_cols), `y` has length
150 /// [`n_rows`](Self::n_rows) and is **fully overwritten** (implementations
151 /// must not accumulate into it).
152 fn apply(&self, x: &[f64], y: &mut [f64]);
153
154 /// Write the main diagonal `A[i][i]` into `d`, which has length
155 /// [`n_rows`](Self::n_rows).
156 ///
157 /// Used to build a Jacobi preconditioner without exposing the storage
158 /// format. A structurally-zero diagonal entry must be written as `0.0`;
159 /// [`JacobiOperatorPreconditioner`] is the layer that decides what to do
160 /// about it.
161 fn diagonal(&self, d: &mut [f64]);
162
163 /// Write the residual `r = b - A x`.
164 ///
165 /// Provided so an implementation with a fused kernel can override it; the
166 /// default applies the operator and subtracts. `x`, `b` and `r` all have
167 /// length `n`. `r` is fully overwritten.
168 fn residual(&self, x: &[f64], b: &[f64], r: &mut [f64]) {
169 self.apply(x, r);
170 for i in 0..r.len() {
171 r[i] = b[i] - r[i];
172 }
173 }
174}
175
176/// An approximate inverse `M^{-1} ~ A^{-1}` usable by the generic Krylov
177/// drivers.
178///
179/// # Contract
180///
181/// [`apply`](Self::apply) writes `z = M^{-1} r`, with `r` and `z` the same
182/// length as the system. It must be **linear** and **deterministic** for the
183/// same reasons [`LinearOperator::apply`] must be, and for [`cg_op`] it must
184/// additionally be **symmetric positive definite** — preconditioned CG is only
185/// a conjugate-gradient method in the `M^{-1}`-inner product, and an
186/// unsymmetric `M` silently destroys that.
187///
188/// # Units
189///
190/// Dimensionless, as [`LinearOperator`].
191#[diagnostic::on_unimplemented(
192 message = "`{Self}` cannot be used as a Krylov preconditioner: it does not implement `OperatorPreconditioner`",
193 label = "not a preconditioner",
194 note = "implemented by `IdentityPreconditioner`, `JacobiOperatorPreconditioner`, and `outram_foam_basic_lib::krylov::Preconditioner`",
195 note = "pass `&IdentityPreconditioner` for an unpreconditioned solve"
196)]
197pub trait OperatorPreconditioner {
198 /// Write `z = M^{-1} r`. Both slices have the system length; `z` is fully
199 /// overwritten.
200 fn apply(&self, r: &[f64], z: &mut [f64]);
201}
202
203/// No preconditioning: `M = I`, so `z = r`.
204///
205/// The correct choice when the system is already well conditioned, and the
206/// baseline any other preconditioner must beat.
207#[derive(Debug, Clone, Copy, Default)]
208pub struct IdentityPreconditioner;
209
210impl OperatorPreconditioner for IdentityPreconditioner {
211 fn apply(&self, r: &[f64], z: &mut [f64]) {
212 z.copy_from_slice(r);
213 }
214}
215
216/// Diagonal (Jacobi) preconditioning built from any [`LinearOperator`]:
217/// `z[i] = r[i] / A[i][i]`.
218///
219/// Stores the reciprocal diagonal, so applying it is one multiply per row.
220/// Cannot break down: a diagonal entry whose magnitude is below `1e-300` is
221/// treated as `1.0`, matching [`crate::krylov::JacobiPreconditioner`].
222///
223/// # Units
224///
225/// The stored reciprocals carry the inverse of whatever units the matrix
226/// entries had; treated as dimensionless `f64` here.
227#[derive(Debug, Clone)]
228pub struct JacobiOperatorPreconditioner {
229 /// `1 / A[i][i]` per row, length `n`.
230 inv_diag: Vec<f64>,
231}
232
233impl JacobiOperatorPreconditioner {
234 /// Extract and invert the diagonal of `a`.
235 ///
236 /// Cost: one [`LinearOperator::diagonal`] call plus `n` divisions.
237 pub fn new<A: LinearOperator + ?Sized>(a: &A) -> Self {
238 let n = a.n_rows();
239 let mut d = vec![0.0; n];
240 a.diagonal(&mut d);
241 for v in d.iter_mut() {
242 *v = if v.abs() > 1.0e-300 { 1.0 / *v } else { 1.0 };
243 }
244 Self { inv_diag: d }
245 }
246
247 /// The stored reciprocal diagonal `1 / A[i][i]`, length `n`.
248 pub fn reciprocal_diagonal(&self) -> &[f64] {
249 &self.inv_diag
250 }
251}
252
253impl OperatorPreconditioner for JacobiOperatorPreconditioner {
254 fn apply(&self, r: &[f64], z: &mut [f64]) {
255 for i in 0..z.len() {
256 z[i] = r[i] * self.inv_diag[i];
257 }
258 }
259}
260
261impl LinearOperator for LduMatrix {
262 fn n_rows(&self) -> usize {
263 self.n_cells
264 }
265
266 fn n_cols(&self) -> usize {
267 self.n_cells
268 }
269
270 /// Face-addressed sparse multiply, delegating to
271 /// [`LduMatrix::multiply`] so there is exactly one implementation of the
272 /// finite-volume kernel.
273 fn apply(&self, x: &[f64], y: &mut [f64]) {
274 let prod = self.multiply(x);
275 y.copy_from_slice(&prod);
276 }
277
278 fn diagonal(&self, d: &mut [f64]) {
279 d.copy_from_slice(&self.diag);
280 }
281
282 /// Delegates to [`LduMatrix::residual`], the crate's existing fused kernel.
283 fn residual(&self, x: &[f64], b: &[f64], r: &mut [f64]) {
284 let res = LduMatrix::residual(self, x, b);
285 r.copy_from_slice(&res);
286 }
287}
288
289impl OperatorPreconditioner for crate::krylov::Preconditioner {
290 /// Bridges the existing finite-volume preconditioner enum onto the generic
291 /// contract, so an `LduMatrix` caller can use [`cg_op`] / [`gmres_op`] /
292 /// [`bicgstab_op`] with ILU(0) as well as Jacobi.
293 fn apply(&self, r: &[f64], z: &mut [f64]) {
294 crate::krylov::Preconditioner::apply(self, r, z)
295 }
296}
297
298// ── shared helpers ──────────────────────────────────────────────────────────
299
300/// Euclidean norm, summed flat (no blocking), so results are reproducible
301/// independent of problem size.
302fn norm2(v: &[f64]) -> f64 {
303 v.iter().map(|x| x * x).sum::<f64>().sqrt()
304}
305
306/// Euclidean inner product, summed flat.
307fn dot(a: &[f64], b: &[f64]) -> f64 {
308 a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
309}
310
311/// `y += alpha * x`.
312fn axpy(alpha: f64, x: &[f64], y: &mut [f64]) {
313 for i in 0..y.len() {
314 y[i] += alpha * x[i];
315 }
316}
317
318/// The trivial `b == 0` answer shared by all three drivers.
319fn zero_rhs_result(n: usize) -> (Vec<f64>, KrylovResult) {
320 (
321 vec![0.0; n],
322 KrylovResult {
323 n_iterations: 0,
324 final_residual: 0.0,
325 converged: true,
326 },
327 )
328}
329
330/// Compute the true relative residual of `x`, and the work buffer it used.
331fn true_relative_residual<A: LinearOperator + ?Sized>(
332 a: &A,
333 x: &[f64],
334 b: &[f64],
335 bnorm: f64,
336 work: &mut [f64],
337) -> f64 {
338 a.residual(x, b, work);
339 norm2(work) / bnorm
340}
341
342// ── generic Krylov drivers ──────────────────────────────────────────────────
343
344/// Preconditioned conjugate gradients for a **symmetric positive-definite**
345/// operator.
346///
347/// The right solver for a finite-element stiffness matrix, which is symmetric
348/// by construction (`K = integral of B^T D B` with `D` symmetric), and for the
349/// finite-volume pressure Poisson equation.
350///
351/// # Not a replacement for [`crate::ldu_matrix::solvers::conjugate_gradient`]
352///
353/// That function is the finite-volume path and keeps its DIC preconditioner and
354/// its `&LduMatrix` signature unchanged. This one is generic over the operator
355/// and takes the preconditioner from the caller. Neither calls the other.
356///
357/// # Arguments
358///
359/// - `a` — the SPD system operator. Behaviour on a non-symmetric or indefinite
360/// operator is undefined (CG may stagnate or diverge); use [`gmres_op`] or
361/// [`bicgstab_op`] there.
362/// - `b` — right-hand side, length `a.n_rows()`, caller's units.
363/// - `x0` — optional initial guess; `None` means the zero vector. Pass the
364/// previous Newton iterate to warm-start.
365/// - `precond` — SPD preconditioner `M^{-1}`.
366/// - `settings` — `tolerance` (relative, on `||b - A x||_2 / ||b||_2`) and
367/// `max_iter`. `restart` is ignored.
368///
369/// # Returns
370///
371/// `(x, result)`. `result.final_residual` is the **true** relative residual of
372/// the returned iterate, recomputed from `a` and `b`, never an internal
373/// estimate. An exactly-zero `b` returns `x = 0`, converged, `0` iterations.
374///
375/// # Cost
376///
377/// One [`LinearOperator::apply`] and one [`OperatorPreconditioner::apply`] per
378/// iteration, plus `O(n)` vector work. Storage `O(n)`.
379pub fn cg_op<A, M>(
380 a: &A,
381 b: &[f64],
382 x0: Option<&[f64]>,
383 precond: &M,
384 settings: &KrylovSettings,
385) -> (Vec<f64>, KrylovResult)
386where
387 A: LinearOperator + ?Sized,
388 M: OperatorPreconditioner + ?Sized,
389{
390 let n = a.n_rows();
391 debug_assert_eq!(b.len(), n, "cg_op: rhs length mismatch");
392 let bnorm = norm2(b);
393 if bnorm == 0.0 {
394 return zero_rhs_result(n);
395 }
396
397 let mut x = match x0 {
398 Some(g) => g.to_vec(),
399 None => vec![0.0; n],
400 };
401 let mut r = vec![0.0; n];
402 a.residual(&x, b, &mut r);
403
404 let mut rel = norm2(&r) / bnorm;
405 if rel <= settings.tolerance {
406 return (
407 x,
408 KrylovResult {
409 n_iterations: 0,
410 final_residual: rel,
411 converged: true,
412 },
413 );
414 }
415
416 let mut z = vec![0.0; n];
417 precond.apply(&r, &mut z);
418 let mut p = z.clone();
419 let mut rz = dot(&r, &z);
420 let mut ap = vec![0.0; n];
421
422 let mut n_iter = 0usize;
423 let mut converged = false;
424
425 for iter in 0..settings.max_iter {
426 a.apply(&p, &mut ap);
427 let pap = dot(&p, &ap);
428 if !pap.is_finite() || pap.abs() < 1.0e-300 {
429 n_iter = iter;
430 break;
431 }
432 let alpha = rz / pap;
433 axpy(alpha, &p, &mut x);
434 axpy(-alpha, &ap, &mut r);
435
436 rel = norm2(&r) / bnorm;
437 n_iter = iter + 1;
438 if !rel.is_finite() {
439 break;
440 }
441 if rel <= settings.tolerance {
442 converged = true;
443 break;
444 }
445
446 precond.apply(&r, &mut z);
447 let rz_new = dot(&r, &z);
448 if rz.abs() < 1.0e-300 {
449 break;
450 }
451 let beta = rz_new / rz;
452 rz = rz_new;
453 for i in 0..n {
454 p[i] = z[i] + beta * p[i];
455 }
456 }
457
458 let final_rel = true_relative_residual(a, &x, b, bnorm, &mut r);
459 let final_rel = if final_rel.is_finite() {
460 final_rel
461 } else {
462 f64::INFINITY
463 };
464 (
465 x,
466 KrylovResult {
467 n_iterations: n_iter,
468 final_residual: final_rel,
469 converged: converged && final_rel <= settings.tolerance,
470 },
471 )
472}
473
474/// Preconditioned BiCGStab for a general (possibly non-symmetric) operator.
475///
476/// The generic sibling of [`crate::krylov::bicgstab`]: same algorithm and same
477/// breakdown guards, but over [`LinearOperator`] instead of `&LduMatrix`, and
478/// with the preconditioner supplied by the caller rather than built from an
479/// `LduMatrix`. The existing function is untouched.
480///
481/// # Arguments and returns
482///
483/// As [`cg_op`], except that `a` need not be symmetric. `settings.restart` is
484/// ignored.
485///
486/// # Cost
487///
488/// Two [`LinearOperator::apply`] and two [`OperatorPreconditioner::apply`] per
489/// iteration; fixed `O(n)` storage.
490pub fn bicgstab_op<A, M>(
491 a: &A,
492 b: &[f64],
493 x0: Option<&[f64]>,
494 precond: &M,
495 settings: &KrylovSettings,
496) -> (Vec<f64>, KrylovResult)
497where
498 A: LinearOperator + ?Sized,
499 M: OperatorPreconditioner + ?Sized,
500{
501 let n = a.n_rows();
502 debug_assert_eq!(b.len(), n, "bicgstab_op: rhs length mismatch");
503 let bnorm = norm2(b);
504 if bnorm == 0.0 {
505 return zero_rhs_result(n);
506 }
507
508 let mut x = match x0 {
509 Some(g) => g.to_vec(),
510 None => vec![0.0; n],
511 };
512 let mut r = vec![0.0; n];
513 a.residual(&x, b, &mut r);
514 let r0 = r.clone();
515
516 let mut rho = 1.0;
517 let mut alpha = 1.0;
518 let mut omega = 1.0;
519 let mut p = vec![0.0; n];
520 let mut v = vec![0.0; n];
521 let mut y = vec![0.0; n];
522 let mut z = vec![0.0; n];
523 let mut s = vec![0.0; n];
524 let mut t = vec![0.0; n];
525
526 let mut n_iter = 0usize;
527 let mut converged = norm2(&r) / bnorm <= settings.tolerance;
528
529 if !converged {
530 for iter in 0..settings.max_iter {
531 n_iter = iter + 1;
532 let rho_new = dot(&r0, &r);
533 if rho_new.abs() < 1.0e-300 || !rho_new.is_finite() {
534 break; // breakdown
535 }
536 let beta = (rho_new / rho) * (alpha / omega);
537 rho = rho_new;
538 for i in 0..n {
539 p[i] = r[i] + beta * (p[i] - omega * v[i]);
540 }
541 precond.apply(&p, &mut y);
542 a.apply(&y, &mut v);
543 let r0v = dot(&r0, &v);
544 if r0v.abs() < 1.0e-300 || !r0v.is_finite() {
545 break;
546 }
547 alpha = rho / r0v;
548 for i in 0..n {
549 s[i] = r[i] - alpha * v[i];
550 }
551 axpy(alpha, &y, &mut x);
552 if norm2(&s) / bnorm <= settings.tolerance {
553 converged = true;
554 break;
555 }
556 precond.apply(&s, &mut z);
557 a.apply(&z, &mut t);
558 let tt = dot(&t, &t);
559 if tt.abs() < 1.0e-300 || !tt.is_finite() {
560 break;
561 }
562 omega = dot(&t, &s) / tt;
563 axpy(omega, &z, &mut x);
564 for i in 0..n {
565 r[i] = s[i] - omega * t[i];
566 }
567 let rel = norm2(&r) / bnorm;
568 if !rel.is_finite() {
569 break;
570 }
571 if rel <= settings.tolerance {
572 converged = true;
573 break;
574 }
575 if omega.abs() < 1.0e-300 {
576 break;
577 }
578 }
579 }
580
581 let final_rel = true_relative_residual(a, &x, b, bnorm, &mut r);
582 let final_rel = if final_rel.is_finite() {
583 final_rel
584 } else {
585 f64::INFINITY
586 };
587 (
588 x,
589 KrylovResult {
590 n_iterations: n_iter,
591 final_residual: final_rel,
592 converged: converged && final_rel <= settings.tolerance,
593 },
594 )
595}
596
597/// Restarted right-preconditioned GMRES(m) for a general operator.
598///
599/// The generic sibling of [`crate::krylov::gmres`], algorithmically identical
600/// (modified Gram-Schmidt Arnoldi, Givens rotations, happy-breakdown guard at
601/// `1e-300`) but over [`LinearOperator`]. The existing function is untouched.
602///
603/// # Arguments and returns
604///
605/// As [`cg_op`], plus `settings.restart` = the subspace dimension `m`; `0` is
606/// treated as `m = max_iter` (unrestarted up to the cap).
607///
608/// # Cost
609///
610/// One [`LinearOperator::apply`] and one [`OperatorPreconditioner::apply`] per
611/// inner iteration; storage `O(m n)` for the Arnoldi basis.
612pub fn gmres_op<A, M>(
613 a: &A,
614 b: &[f64],
615 x0: Option<&[f64]>,
616 precond: &M,
617 settings: &KrylovSettings,
618) -> (Vec<f64>, KrylovResult)
619where
620 A: LinearOperator + ?Sized,
621 M: OperatorPreconditioner + ?Sized,
622{
623 /// Below this an Arnoldi vector norm is a happy breakdown. Same value as
624 /// `crate::krylov::gmres::HAPPY_TOL`.
625 const HAPPY_TOL: f64 = 1.0e-300;
626
627 let n = a.n_rows();
628 debug_assert_eq!(b.len(), n, "gmres_op: rhs length mismatch");
629 let bnorm = norm2(b);
630 if bnorm == 0.0 {
631 return zero_rhs_result(n);
632 }
633
634 let m = if settings.restart == 0 {
635 settings.max_iter.max(1)
636 } else {
637 settings.restart
638 };
639
640 let mut x = match x0 {
641 Some(g) => g.to_vec(),
642 None => vec![0.0; n],
643 };
644 let tol = settings.tolerance;
645 let mut total_iters = 0usize;
646 let mut converged = false;
647 let mut mz = vec![0.0; n];
648 let mut r = vec![0.0; n];
649
650 while total_iters < settings.max_iter {
651 a.residual(&x, b, &mut r);
652 let beta = norm2(&r);
653 if beta / bnorm <= tol {
654 converged = true;
655 break;
656 }
657
658 let mut v: Vec<Vec<f64>> = Vec::with_capacity(m + 1);
659 let mut v0 = r.clone();
660 for e in v0.iter_mut() {
661 *e /= beta;
662 }
663 v.push(v0);
664
665 let mut cs = vec![0.0f64; m];
666 let mut sn = vec![0.0f64; m];
667 let mut g = vec![0.0f64; m + 1];
668 g[0] = beta;
669 let mut hcols: Vec<Vec<f64>> = Vec::with_capacity(m);
670 let mut k = 0usize;
671
672 for j in 0..m {
673 if total_iters >= settings.max_iter {
674 break;
675 }
676 total_iters += 1;
677
678 precond.apply(&v[j], &mut mz);
679 let mut w = vec![0.0; n];
680 a.apply(&mz, &mut w);
681
682 let mut hcol = vec![0.0f64; j + 2];
683 for i in 0..=j {
684 let hij = dot(&w, &v[i]);
685 hcol[i] = hij;
686 axpy(-hij, &v[i], &mut w);
687 }
688 let hnext = norm2(&w);
689 hcol[j + 1] = hnext;
690
691 for i in 0..j {
692 let temp = cs[i] * hcol[i] + sn[i] * hcol[i + 1];
693 hcol[i + 1] = -sn[i] * hcol[i] + cs[i] * hcol[i + 1];
694 hcol[i] = temp;
695 }
696
697 let (c, s) = {
698 let (h1, h2) = (hcol[j], hcol[j + 1]);
699 if h2 == 0.0 {
700 (1.0, 0.0)
701 } else {
702 let den = (h1 * h1 + h2 * h2).sqrt();
703 (h1 / den, h2 / den)
704 }
705 };
706 cs[j] = c;
707 sn[j] = s;
708 hcol[j] = c * hcol[j] + s * hcol[j + 1];
709 hcol[j + 1] = 0.0;
710 let g_temp = c * g[j] + s * g[j + 1];
711 g[j + 1] = -s * g[j] + c * g[j + 1];
712 g[j] = g_temp;
713
714 hcols.push(hcol);
715 k = j + 1;
716
717 let resid = g[j + 1].abs() / bnorm;
718 if hnext > HAPPY_TOL {
719 let mut vnew = w;
720 for e in vnew.iter_mut() {
721 *e /= hnext;
722 }
723 v.push(vnew);
724 }
725 if resid <= tol || hnext <= HAPPY_TOL {
726 break;
727 }
728 }
729
730 let mut y = vec![0.0f64; k];
731 for i in (0..k).rev() {
732 let mut sum = g[i];
733 for l in (i + 1)..k {
734 sum -= hcols[l][i] * y[l];
735 }
736 let diag = hcols[i][i];
737 y[i] = if diag.abs() > HAPPY_TOL {
738 sum / diag
739 } else {
740 0.0
741 };
742 }
743
744 let mut zc = vec![0.0f64; n];
745 for i in 0..k {
746 axpy(y[i], &v[i], &mut zc);
747 }
748 precond.apply(&zc, &mut mz);
749 axpy(1.0, &mz, &mut x);
750
751 let true_rel = true_relative_residual(a, &x, b, bnorm, &mut r);
752 if !true_rel.is_finite() {
753 break;
754 }
755 if true_rel <= tol {
756 converged = true;
757 break;
758 }
759 }
760
761 let final_rel = true_relative_residual(a, &x, b, bnorm, &mut r);
762 let final_rel = if final_rel.is_finite() {
763 final_rel
764 } else {
765 f64::INFINITY
766 };
767 (
768 x,
769 KrylovResult {
770 n_iterations: total_iters,
771 final_residual: final_rel,
772 converged: converged && final_rel <= tol,
773 },
774 )
775}
776
777#[cfg(test)]
778mod tests {
779 use super::*;
780 use crate::krylov::{bicgstab, gmres, Preconditioner};
781
782 /// SPD tridiagonal LduMatrix helper.
783 fn spd_tridiag(n: usize, d: f64, e: f64) -> LduMatrix {
784 let owner: Vec<usize> = (0..n - 1).collect();
785 let neigh: Vec<usize> = (1..n).collect();
786 let mut a = LduMatrix::new(n, owner, neigh);
787 a.diag = vec![d; n];
788 a.lower = vec![e; n - 1];
789 a.upper = vec![e; n - 1];
790 a
791 }
792
793 fn nonsym_tridiag(n: usize, d: f64, lo: f64, up: f64) -> LduMatrix {
794 let owner: Vec<usize> = (0..n - 1).collect();
795 let neigh: Vec<usize> = (1..n).collect();
796 let mut a = LduMatrix::new(n, owner, neigh);
797 a.diag = vec![d; n];
798 a.lower = vec![lo; n - 1];
799 a.upper = vec![up; n - 1];
800 a
801 }
802
803 fn rel_err(x: &[f64], y: &[f64]) -> f64 {
804 let num: f64 = x.iter().zip(y).map(|(a, b)| (a - b) * (a - b)).sum();
805 let den: f64 = y.iter().map(|b| b * b).sum();
806 (num / den).sqrt()
807 }
808
809 /// `LduMatrix`'s `LinearOperator::apply` must agree bit-for-bit with its
810 /// own `multiply`, so routing through the contract cannot perturb the
811 /// finite-volume path.
812 #[test]
813 fn ldu_operator_apply_matches_multiply_bitwise() {
814 let a = nonsym_tridiag(7, 4.0, -1.0, -2.0);
815 let x: Vec<f64> = (0..7).map(|i| 0.3 * i as f64 - 1.1).collect();
816 let direct = a.multiply(&x);
817 let mut via = vec![0.0; 7];
818 LinearOperator::apply(&a, &x, &mut via);
819 assert_eq!(direct, via);
820
821 let b: Vec<f64> = (0..7).map(|i| i as f64).collect();
822 let direct_r = LduMatrix::residual(&a, &x, &b);
823 let mut via_r = vec![0.0; 7];
824 LinearOperator::residual(&a, &x, &b, &mut via_r);
825 assert_eq!(direct_r, via_r);
826 }
827
828 /// `cg_op` on an SPD system reproduces the known solution.
829 #[test]
830 fn cg_op_spd_known_solution() {
831 let n = 25;
832 let a = spd_tridiag(n, 2.0, -1.0);
833 let x_true: Vec<f64> = (0..n).map(|i| ((i + 1) as f64).sqrt()).collect();
834 let b = a.multiply(&x_true);
835 let s = KrylovSettings {
836 tolerance: 1e-12,
837 max_iter: 500,
838 restart: 30,
839 };
840 let m = JacobiOperatorPreconditioner::new(&a);
841 let (x, r) = cg_op(&a, &b, None, &m, &s);
842 assert!(r.converged, "cg_op did not converge: {:?}", r);
843 assert!(
844 rel_err(&x, &x_true) < 1e-9,
845 "rel err {}",
846 rel_err(&x, &x_true)
847 );
848 }
849
850 /// The generic drivers must reach the same answer as the existing
851 /// finite-volume ones on a non-symmetric system.
852 #[test]
853 fn generic_drivers_agree_with_existing_fv_solvers() {
854 let n = 30;
855 let a = nonsym_tridiag(n, 4.0, -1.0, -2.0);
856 let b: Vec<f64> = (0..n).map(|i| (i as f64) - 12.5).collect();
857 let s = KrylovSettings {
858 tolerance: 1e-10,
859 max_iter: 500,
860 restart: 30,
861 };
862
863 let pc = Preconditioner::ilu0(&a);
864 let (x_old_b, r_old_b) = bicgstab(&a, &b, None, &pc, &s);
865 let (x_new_b, r_new_b) = bicgstab_op(&a, &b, None, &pc, &s);
866 assert!(r_old_b.converged && r_new_b.converged);
867 assert!(rel_err(&x_new_b, &x_old_b) < 1e-8);
868
869 let (x_old_g, r_old_g) = gmres(&a, &b, None, &pc, &s);
870 let (x_new_g, r_new_g) = gmres_op(&a, &b, None, &pc, &s);
871 assert!(r_old_g.converged && r_new_g.converged);
872 assert!(rel_err(&x_new_g, &x_old_g) < 1e-8);
873 }
874
875 /// Zero right-hand side short-circuits in all three drivers.
876 #[test]
877 fn zero_rhs_shortcircuits() {
878 let a = spd_tridiag(6, 3.0, -1.0);
879 let b = vec![0.0; 6];
880 let s = KrylovSettings::default();
881 let m = IdentityPreconditioner;
882 for (x, r) in [
883 cg_op(&a, &b, None, &m, &s),
884 bicgstab_op(&a, &b, None, &m, &s),
885 gmres_op(&a, &b, None, &m, &s),
886 ] {
887 assert!(r.converged && r.n_iterations == 0);
888 assert!(x.iter().all(|v| *v == 0.0));
889 }
890 }
891
892 /// A warm start that is already the answer costs zero iterations.
893 #[test]
894 fn warm_start_is_honoured() {
895 let a = spd_tridiag(10, 2.0, -0.5);
896 let x_true: Vec<f64> = (0..10).map(|i| i as f64 * 0.25).collect();
897 let b = a.multiply(&x_true);
898 let s = KrylovSettings {
899 tolerance: 1e-10,
900 max_iter: 100,
901 restart: 20,
902 };
903 let m = JacobiOperatorPreconditioner::new(&a);
904 let (_, r) = cg_op(&a, &b, Some(&x_true), &m, &s);
905 assert!(r.converged);
906 assert_eq!(r.n_iterations, 0);
907 }
908}