treams_core/linalg/
gmres.rs

1//! Restarted GMRES (generalized minimal residual method) for complex linear systems given only
2//! through matrix-vector products. treams-rs extension.
3//!
4//! Each cycle builds an orthonormal Krylov basis of at most `restart` vectors by
5//! Arnoldi iteration, with two passes of modified Gram-Schmidt per vector, and takes
6//! the least-squares update through Givens rotations. A cycle ends early when the
7//! rotated residual estimate meets the tolerance, the new Krylov vector vanishes, or
8//! the iteration limit is reached. Before it accepts an answer or
9//! restarts, the solver recomputes the true residual `b - A x` with one more operator
10//! application, and only that residual decides convergence.
11#![allow(clippy::indexing_slicing)] // Krylov basis and Hessenberg indices below `restart + 1`.
12
13use crate::{Complex, Error, Result};
14
15/// Accuracy and memory budget of restarted GMRES.
16///
17/// The solve converges when the true residual norm is at most
18/// `max(atol, rtol * |b|)`. The defaults are `rtol = 1e-10`, `atol = 0`,
19/// `restart = 30` and `max_iterations = 300`; the Python constants
20/// `iterative.DEFAULT_RTOL`, `DEFAULT_ATOL`, `DEFAULT_RESTART` and
21/// `DEFAULT_MAX_ITERATIONS` repeat them.
22#[derive(Clone, Copy, Debug)]
23pub struct GmresOptions {
24    /// Relative residual tolerance with respect to the right-hand side norm.
25    pub rtol: f64,
26    /// Absolute residual tolerance, for right-hand sides near zero.
27    pub atol: f64,
28    /// Maximum number of Krylov vectors retained before a restart.
29    pub restart: usize,
30    /// Maximum total operator applications in the Arnoldi iterations.
31    pub max_iterations: usize,
32}
33
34impl Default for GmresOptions {
35    fn default() -> Self {
36        Self {
37            rtol: 1e-10,
38            atol: 0.0,
39            restart: 30,
40            max_iterations: 300,
41        }
42    }
43}
44
45impl GmresOptions {
46    /// Derivative right-hand sides have arbitrary scale, so an absolute tolerance
47    /// would incorrectly erase small tangents or cotangents. Keep the relative
48    /// tolerance, or use the default when the primal requested absolute-only
49    /// convergence; the restart and iteration budgets stay unchanged.
50    pub(crate) fn for_derivative(self) -> Self {
51        Self {
52            rtol: if self.rtol > 0.0 {
53                self.rtol
54            } else {
55                Self::default().rtol
56            },
57            atol: 0.0,
58            ..self
59        }
60    }
61
62    /// Reject non-finite or negative tolerances, two zero tolerances, and a zero
63    /// restart length or iteration limit.
64    pub(crate) fn validate(self) -> Result<()> {
65        if !self.rtol.is_finite()
66            || !self.atol.is_finite()
67            || self.rtol < 0.0
68            || self.atol < 0.0
69            || self.rtol + self.atol == 0.0
70            || self.restart == 0
71            || self.max_iterations == 0
72        {
73            return Err(Error::InvalidInput(
74                "GMRES needs finite nonnegative tolerances, a positive tolerance, restart and iteration limit".into(),
75            ));
76        }
77        Ok(())
78    }
79}
80
81/// Proof of convergence for one right-hand side: the residual `b - A x` recomputed
82/// from the returned answer `x`, not the estimate of the Arnoldi iteration.
83///
84/// `residual_norm <= max(atol, rtol * rhs_norm)` holds for every returned value.
85#[derive(Clone, Copy, Debug)]
86pub struct Convergence {
87    /// Arnoldi iterations performed, including all restarts.
88    pub iterations: usize,
89    /// Euclidean norm of the recomputed residual `b - A x`.
90    pub residual_norm: f64,
91    /// Euclidean norm of the right-hand side.
92    pub rhs_norm: f64,
93}
94
95/// Euclidean norm without overflow or underflow of the squared terms.
96pub(crate) fn norm(vector: &[Complex]) -> f64 {
97    vector.iter().fold(0.0_f64, |sum, z| sum.hypot(z.norm()))
98}
99
100/// Solve `A x = rhs` for the operator that `apply` multiplies with, starting from
101/// `x = 0`. Fails with [`Error::NotConverged`] when the residual is not finite, when
102/// the iterations reach `options.max_iterations`, or when the Arnoldi iteration breaks
103/// down before convergence; errors of `apply` pass through.
104pub(crate) fn gmres(
105    rhs: &[Complex],
106    options: GmresOptions,
107    apply: impl Fn(&[Complex]) -> Result<Vec<Complex>>,
108) -> Result<(Vec<Complex>, Convergence)> {
109    let (answer, reports) = gmres_batch(rhs, 1, options, |x, _| apply(x))?;
110    Ok((answer, reports[0]))
111}
112
113/// Independent GMRES solves of `columns` column-major right-hand sides. Only the
114/// operator applications are shared: each column keeps its own Krylov basis,
115/// rotations, restart decisions and true-residual convergence certificate.
116///
117/// The caller bounds `columns` to set the Krylov memory budget. `apply` receives
118/// only columns that still need that application, in column-major order.
119pub(crate) fn gmres_batch(
120    rhs: &[Complex],
121    columns: usize,
122    options: GmresOptions,
123    apply: impl Fn(&[Complex], usize) -> Result<Vec<Complex>>,
124) -> Result<(Vec<Complex>, Vec<Convergence>)> {
125    let dimension = rhs.len() / columns;
126    let restart = options.restart.min(dimension).min(options.max_iterations);
127    let mut states: Vec<_> = (0..columns)
128        .map(|column| Krylov::new(&rhs[column * dimension..][..dimension], options))
129        .collect();
130    loop {
131        let mut cycle = Vec::with_capacity(columns);
132        for (column, state) in states.iter_mut().enumerate() {
133            if !state.converged(options)? {
134                state.start(restart);
135                cycle.push(column);
136            }
137        }
138        if cycle.is_empty() {
139            break;
140        }
141        let mut extending = cycle.clone();
142        for j in 0..restart {
143            let mut product = apply_columns(
144                &extending,
145                |column| states[column].basis[j].as_slice(),
146                &apply,
147            )?;
148            let mut next = Vec::with_capacity(extending.len());
149            for (&column, w) in extending.iter().zip(product.chunks_exact_mut(dimension)) {
150                if states[column].step(w, j, restart, options)? {
151                    next.push(column);
152                }
153            }
154            extending = next;
155            if extending.is_empty() {
156                break;
157            }
158        }
159        for &column in &cycle {
160            states[column].update(restart);
161        }
162        // Never certify convergence using the Hessenberg estimate alone.
163        let product = apply_columns(&cycle, |column| states[column].answer.as_slice(), &apply)?;
164        for (&column, product) in cycle.iter().zip(product.chunks_exact(dimension)) {
165            states[column].residual = rhs[column * dimension..][..dimension]
166                .iter()
167                .zip(product)
168                .map(|(b, a)| b - a)
169                .collect();
170        }
171    }
172    let reports = states.iter().map(|state| state.report).collect();
173    let answer = if let [state] = states.as_mut_slice() {
174        std::mem::take(&mut state.answer)
175    } else {
176        states.into_iter().flat_map(|state| state.answer).collect()
177    };
178    Ok((answer, reports))
179}
180
181/// Pack active columns only when there is more than one: scalar solves and the
182/// tail of a batch lend their existing Krylov or answer buffer directly.
183fn apply_columns<'a>(
184    columns: &[usize],
185    vector: impl Fn(usize) -> &'a [Complex],
186    apply: &impl Fn(&[Complex], usize) -> Result<Vec<Complex>>,
187) -> Result<Vec<Complex>> {
188    if let [column] = columns {
189        apply(vector(*column), 1)
190    } else {
191        let input: Vec<_> = columns
192            .iter()
193            .flat_map(|&column| vector(column).iter().copied())
194            .collect();
195        apply(&input, columns.len())
196    }
197}
198
199/// One column's independent Arnoldi iteration. Batching changes when its operator
200/// products run, not its arithmetic or convergence decisions.
201struct Krylov {
202    answer: Vec<Complex>,
203    residual: Vec<Complex>,
204    tolerance: f64,
205    report: Convergence,
206    basis: Vec<Vec<Complex>>,
207    h: Vec<Complex>,
208    rotations: Vec<(f64, Complex)>,
209    g: Vec<Complex>,
210    used: usize,
211}
212
213impl Krylov {
214    fn new(rhs: &[Complex], options: GmresOptions) -> Self {
215        let rhs_norm = norm(rhs);
216        Self {
217            answer: vec![Complex::default(); rhs.len()],
218            residual: rhs.to_vec(),
219            tolerance: options.atol.max(options.rtol * rhs_norm),
220            report: Convergence {
221                iterations: 0,
222                residual_norm: rhs_norm,
223                rhs_norm,
224            },
225            basis: Vec::new(),
226            h: Vec::new(),
227            rotations: Vec::new(),
228            g: Vec::new(),
229            used: 0,
230        }
231    }
232
233    fn converged(&mut self, options: GmresOptions) -> Result<bool> {
234        self.report.residual_norm = norm(&self.residual);
235        let Convergence {
236            residual_norm,
237            iterations,
238            ..
239        } = self.report;
240        if residual_norm.is_finite() && residual_norm <= self.tolerance {
241            return Ok(true);
242        }
243        if !residual_norm.is_finite() || iterations >= options.max_iterations {
244            return Err(Error::NotConverged(format!(
245                "GMRES did not converge after {iterations} iterations: residual {residual_norm:e}, tolerance {:e}",
246                self.tolerance
247            )));
248        }
249        Ok(false)
250    }
251
252    fn start(&mut self, restart: usize) {
253        // Initially converged columns need no Krylov workspace, even when the
254        // requested restart budget is large. Reuse it after the first cycle.
255        self.basis.clear();
256        self.basis.reserve(restart + 1);
257        self.basis.push(
258            self.residual
259                .iter()
260                .map(|z| z / self.report.residual_norm)
261                .collect(),
262        );
263        self.h.clear();
264        self.h.resize((restart + 1) * restart, Complex::default());
265        self.rotations.clear();
266        self.rotations.reserve(restart);
267        self.g.clear();
268        self.g.resize(restart + 1, Complex::default());
269        self.g[0] = Complex::new(self.report.residual_norm, 0.0);
270        self.used = 0;
271    }
272
273    /// Advance one Arnoldi step and return whether this cycle needs another.
274    fn step(
275        &mut self,
276        w: &mut [Complex],
277        j: usize,
278        restart: usize,
279        options: GmresOptions,
280    ) -> Result<bool> {
281        // Two modified Gram-Schmidt passes keep orthogonality near resonance.
282        for _ in 0..2 {
283            for i in 0..=j {
284                let dot: Complex = self.basis[i]
285                    .iter()
286                    .zip(&*w)
287                    .map(|(v, w)| v.conj() * w)
288                    .sum();
289                self.h[j * (restart + 1) + i] += dot;
290                for (w, v) in w.iter_mut().zip(&self.basis[i]) {
291                    *w -= dot * v;
292                }
293            }
294        }
295        let next = norm(w);
296        self.h[j * (restart + 1) + j + 1] = Complex::new(next, 0.0);
297        for (i, &(c, s)) in self.rotations.iter().enumerate() {
298            let offset = j * (restart + 1) + i;
299            let a = self.h[offset];
300            let b = self.h[offset + 1];
301            self.h[offset] = c * a + s * b;
302            self.h[offset + 1] = -s.conj() * a + c * b;
303        }
304        let diagonal = j * (restart + 1) + j;
305        let a = self.h[diagonal];
306        let b = self.h[diagonal + 1];
307        let length = a.norm().hypot(b.norm());
308        if !length.is_finite() || length == 0.0 {
309            return Err(Error::NotConverged(
310                "GMRES Arnoldi breakdown before convergence".into(),
311            ));
312        }
313        let phase = if a.norm() == 0.0 {
314            Complex::new(1.0, 0.0)
315        } else {
316            a / a.norm()
317        };
318        let c = a.norm() / length;
319        let s = phase * b.conj() / length;
320        self.h[diagonal] = phase * length;
321        self.h[diagonal + 1] = Complex::default();
322        self.rotations.push((c, s));
323        self.g[j + 1] = -s.conj() * self.g[j];
324        self.g[j] *= c;
325        self.used = j + 1;
326        self.report.iterations += 1;
327        if self.g[j + 1].norm() <= self.tolerance
328            || next == 0.0
329            || self.report.iterations == options.max_iterations
330        {
331            return Ok(false);
332        }
333        self.basis.push(w.iter().map(|z| z / next).collect());
334        Ok(true)
335    }
336
337    fn update(&mut self, restart: usize) {
338        for i in (0..self.used).rev() {
339            let tail: Complex = ((i + 1)..self.used)
340                .map(|j| self.h[j * (restart + 1) + i] * self.g[j])
341                .sum();
342            self.g[i] = (self.g[i] - tail) / self.h[i * (restart + 1) + i];
343            for (x, v) in self.answer.iter_mut().zip(&self.basis[i]) {
344                *x += self.g[i] * v;
345            }
346        }
347    }
348}
349
350/// Convergence, restarts and failures of GMRES against direct solves.
351#[cfg(test)]
352mod tests {
353    use std::cell::RefCell;
354
355    use nalgebra::{DMatrix, DVector};
356    use proptest::{prelude::*, test_runner::TestCaseError};
357
358    use super::{GmresOptions, Krylov, gmres, gmres_batch, norm};
359    use crate::{
360        Complex,
361        test_support::{DEFAULT_CASES, complex_matrix, prop_assert_close},
362    };
363
364    fn options() -> GmresOptions {
365        GmresOptions {
366            rtol: 2e-12,
367            restart: 8,
368            max_iterations: 100,
369            ..GmresOptions::default()
370        }
371    }
372
373    proptest! {
374        #![proptest_config(ProptestConfig::with_cases(DEFAULT_CASES))]
375
376        #[test]
377        fn restarted_gmres_matches_a_direct_solve(
378            (m, rhs, restart) in (1_usize..=24).prop_flat_map(|n| {
379                (complex_matrix(n, n, 1.0), complex_matrix(n, 1, 1.0), 1..=n + 2)
380            }),
381        ) {
382            check_gmres(&m, rhs.as_slice(), restart)?;
383        }
384    }
385
386    /// Restarted GMRES on the nonnormal `A = I + M / (2 ‖M‖)`, whose field of values
387    /// stays at least 1/2 from zero, converges for every restart length to the direct
388    /// solution, reports its true residual within the tolerance, and without restarts
389    /// terminates within `n` iterations.
390    fn check_gmres(
391        m: &DMatrix<Complex>,
392        rhs: &[Complex],
393        restart: usize,
394    ) -> Result<(), TestCaseError> {
395        let n = m.nrows();
396        let shrink = if m.norm() > 0.0 { 0.5 / m.norm() } else { 0.0 };
397        let a = DMatrix::identity(n, n) + m * Complex::from(shrink);
398        let options = GmresOptions {
399            rtol: 1e-10,
400            atol: 0.0,
401            restart,
402            max_iterations: 4000,
403        };
404        let apply = |x: &[Complex]| Ok((&a * DVector::from_column_slice(x)).as_slice().to_vec());
405        let (answer, report) = gmres(rhs, options, apply).unwrap();
406        let b = DVector::from_column_slice(rhs);
407        let direct = a.clone().lu().solve(&b).unwrap();
408        prop_assert_close!(
409            answer.as_slice(),
410            direct.as_slice(),
411            1e-8 * (1.0 + direct.norm())
412        );
413        prop_assert_close!(
414            report.rhs_norm,
415            b.norm(),
416            1e-14 * b.norm() + f64::MIN_POSITIVE
417        );
418        let residual = (&b - &a * DVector::from_column_slice(&answer)).norm();
419        prop_assert_close!(
420            report.residual_norm,
421            residual,
422            1e-14 * b.norm() + f64::MIN_POSITIVE
423        );
424        prop_assert!(report.residual_norm <= options.rtol * report.rhs_norm);
425        if restart >= n {
426            prop_assert!(report.iterations <= n, "{} iterations", report.iterations);
427        }
428        Ok(())
429    }
430
431    #[test]
432    fn gmres_zero_happy_breakdown_restarts_and_failure() {
433        let identity = |x: &[Complex]| Ok(x.to_vec());
434        let (zero, report) = gmres(&[Complex::default(); 3], options(), identity).unwrap();
435        assert_eq!(zero, vec![Complex::default(); 3]);
436        assert_eq!(report.iterations, 0);
437        let rhs = vec![Complex::new(0.2, -0.3); 3];
438        let (answer, report) = gmres(&rhs, options(), identity).unwrap();
439        assert!((norm(&answer) - norm(&rhs)).abs() < 1e-14);
440        assert_eq!(report.iterations, 1);
441        let diagonal = |x: &[Complex]| {
442            Ok(x.iter()
443                .enumerate()
444                .map(|(i, x)| {
445                    Complex::new(1.0 + f64::from(u32::try_from(i).unwrap()) * 0.3, 0.1) * x
446                })
447                .collect())
448        };
449        let limited = GmresOptions {
450            restart: 1,
451            max_iterations: 1,
452            ..options()
453        };
454        assert!(gmres(&rhs, limited, diagonal).is_err());
455        let restarted = GmresOptions {
456            restart: 1,
457            ..options()
458        };
459        let (_, report) = gmres(&rhs, restarted, diagonal).unwrap();
460        assert!(report.iterations > 1);
461        assert!(report.residual_norm <= restarted.rtol * report.rhs_norm);
462        assert!(gmres(&rhs, options(), |_| Ok(vec![Complex::default(); 3])).is_err());
463    }
464
465    /// Sharing operator applications preserves each column's arithmetic, even when
466    /// zero columns skip the solve and eigenvectors finish before the other columns.
467    #[test]
468    fn batched_columns_keep_independent_convergence() {
469        let diagonal = [
470            Complex::new(1.0, 0.1),
471            Complex::new(1.3, 0.1),
472            Complex::new(1.6, 0.1),
473        ];
474        let apply = |input: &[Complex]| {
475            Ok(input
476                .iter()
477                .enumerate()
478                .map(|(i, x)| diagonal[i % 3] * x)
479                .collect::<Vec<_>>())
480        };
481        for (rtol, atol) in [(1e-10, 0.0), (0.0, 1e-10)] {
482            let options = GmresOptions {
483                rtol,
484                atol,
485                restart: 2,
486                max_iterations: 100,
487            };
488            let mut rhs = vec![Complex::default(); 18];
489            rhs[0] = Complex::new(1.0, 2.0);
490            for (column, scale) in [0.7, 1e-120, if rtol > 0.0 { 1e120 } else { 1e-3 }]
491                .into_iter()
492                .enumerate()
493            {
494                for (i, z) in [
495                    Complex::new(0.2, -0.3),
496                    Complex::new(0.5, 0.7),
497                    Complex::new(0.6, -0.1),
498                ]
499                .into_iter()
500                .enumerate()
501                {
502                    rhs[(column + 2) * 3 + i] = z * scale;
503                }
504            }
505            rhs[16] = Complex::new(1.0, -0.3);
506            let widths = RefCell::new(Vec::new());
507            let (answer, reports) = gmres_batch(&rhs, 6, options, |input, columns| {
508                assert_eq!(input.len(), columns * 3);
509                widths.borrow_mut().push(columns);
510                apply(input)
511            })
512            .unwrap();
513            for (column, right) in rhs.as_chunks::<3>().0.iter().enumerate() {
514                let (independent, report) = gmres(right, options, apply).unwrap();
515                assert_eq!(&answer[column * 3..][..3], independent.as_slice());
516                assert_eq!(reports[column].iterations, report.iterations);
517                assert_eq!(
518                    reports[column].rhs_norm.to_bits(),
519                    report.rhs_norm.to_bits()
520                );
521                assert_eq!(
522                    reports[column].residual_norm.to_bits(),
523                    report.residual_norm.to_bits()
524                );
525                assert!(report.residual_norm <= atol.max(rtol * report.rhs_norm));
526            }
527            assert_eq!(reports[1].iterations, 0);
528            assert_eq!(reports[0].iterations, 1);
529            assert!(reports[2].iterations > 2);
530            let widths = widths.into_inner();
531            assert!(widths[0] > 1);
532            assert!(widths.last().unwrap() < &widths[0]);
533            let independent_calls: usize = reports
534                .iter()
535                .map(|report| report.iterations + report.iterations.div_ceil(2))
536                .sum();
537            assert!(widths.len() < independent_calls);
538        }
539    }
540
541    /// Different eigenvector columns each need one iteration. A shared Krylov
542    /// polynomial would incorrectly exhaust this independently sufficient budget.
543    #[test]
544    fn batched_eigenvectors_obey_each_columns_iteration_limit() {
545        let rhs = [1.0, 0.0, 0.0, 1.0].map(Complex::from);
546        let options = GmresOptions {
547            restart: 1,
548            max_iterations: 1,
549            ..options()
550        };
551        let (answer, reports) = gmres_batch(&rhs, 2, options, |input, _| {
552            Ok(input
553                .iter()
554                .enumerate()
555                .map(|(i, x)| x * if i % 2 == 0 { 2.0 } else { 3.0 })
556                .collect())
557        })
558        .unwrap();
559        assert_eq!(answer, [0.5, 0.0, 0.0, 1.0 / 3.0].map(Complex::from));
560        assert!(reports.iter().all(|report| report.iterations == 1));
561        let (answer, reports) = gmres_batch(&[Complex::default(); 8], 4, options, |_, _| {
562            panic!("zero right-hand sides must not apply the operator")
563        })
564        .unwrap();
565        assert_eq!(answer, vec![Complex::default(); 8]);
566        assert!(reports.iter().all(|report| report.iterations == 0));
567    }
568
569    #[test]
570    fn initially_converged_columns_allocate_no_krylov_workspace() {
571        let options = GmresOptions {
572            atol: 1e-12,
573            restart: usize::MAX,
574            max_iterations: usize::MAX,
575            ..options()
576        };
577        for scale in [0.0, 1e-14] {
578            let mut state = Krylov::new(&[Complex::from(scale); 3], options);
579            assert!(state.converged(options).unwrap());
580            assert_eq!(state.basis.capacity(), 0);
581            assert_eq!(state.h.capacity(), 0);
582            assert_eq!(state.rotations.capacity(), 0);
583            assert_eq!(state.g.capacity(), 0);
584        }
585    }
586}