Skip to main content

conspire/math/optimize/krylov/
mod.rs

1#[cfg(test)]
2mod test;
3
4use super::{OptimizationError, Precondition};
5use crate::math::{
6    Scalar, Style, StyledError, Tensor, Vector, assert::AssertionError, styled_error,
7};
8use std::mem::replace;
9
10const PATIENCE: usize = 30;
11const PROGRESS: Scalar = 0.9;
12const ACCEPTABLE: Scalar = 1e-3;
13
14/// Available Krylov subspace linear solvers.
15#[derive(Clone, Copy, Debug, Default)]
16pub enum KrylovMethod {
17    /// Conjugate gradients, descending the quadratic the system is the
18    /// stationary point of.
19    ///
20    /// Needs the operator positive definite, and says so when it is not. Where
21    /// that holds it is the cheaper of the two, keeping three vectors rather
22    /// than six.
23    #[default]
24    ConjugateGradients,
25    /// The minimal residual method, shortening the residual over the subspace
26    /// reached so far.
27    ///
28    /// An operator with no minimum still has a residual with a shortest
29    /// length, so this asks only for symmetry and serves the systems conjugate
30    /// gradients has to refuse.
31    Minres,
32    /// The generalized minimal residual method, restarted after the given
33    /// number of iterations.
34    ///
35    /// Asks nothing of the operator, so it serves the nonsymmetric systems the
36    /// other two have to refuse. The preconditioner acts on the right, leaving
37    /// the residual it minimizes the true one, at the cost of keeping a basis
38    /// of up to twice the restart length.
39    Gmres(usize),
40}
41
42/// An iterative linear solver via Krylov subspaces.
43#[derive(Clone, Copy, Debug)]
44pub struct Krylov {
45    /// Maximum number of iterations.
46    pub max_steps: usize,
47    /// Which Krylov method to use.
48    pub method: KrylovMethod,
49    /// Tolerance relative to the initial residual.
50    pub rel_tol: Scalar,
51}
52
53impl Default for Krylov {
54    fn default() -> Self {
55        Self {
56            max_steps: 1_000,
57            method: KrylovMethod::default(),
58            rel_tol: 1e-10,
59        }
60    }
61}
62
63impl Krylov {
64    /// Iteratively solves a linear system given its action on a vector.
65    pub fn solve(
66        &self,
67        apply: impl FnMut(&Vector) -> Vector,
68        preconditioning: impl Precondition,
69        right_hand_side: &Vector,
70    ) -> Result<Vector, KrylovError> {
71        match self.method {
72            KrylovMethod::ConjugateGradients => descend(
73                self.max_steps,
74                self.rel_tol,
75                apply,
76                preconditioning,
77                right_hand_side,
78            ),
79            KrylovMethod::Minres => minimize_residual(
80                self.max_steps,
81                self.rel_tol,
82                apply,
83                preconditioning,
84                right_hand_side,
85            ),
86            KrylovMethod::Gmres(restart) => restarted(
87                self.max_steps,
88                restart,
89                self.rel_tol,
90                apply,
91                preconditioning,
92                right_hand_side,
93            ),
94        }
95    }
96}
97
98fn descend(
99    max_steps: usize,
100    rel_tol: Scalar,
101    mut apply: impl FnMut(&Vector) -> Vector,
102    preconditioning: impl Precondition,
103    right_hand_side: &Vector,
104) -> Result<Vector, KrylovError> {
105    let scale = right_hand_side.norm().value();
106    let mut solution = Vector::zero(right_hand_side.len());
107    if scale == 0.0 {
108        return Ok(solution);
109    }
110    let divide = |residual: &Vector| preconditioning.apply(residual);
111    let mut residual = right_hand_side.clone();
112    let mut preconditioned = divide(&residual);
113    let mut direction = preconditioned.clone();
114    let mut projection = residual.full_contraction(&preconditioned);
115    let mut applied;
116    let mut curvature;
117    let mut next;
118    let mut step;
119    for _ in 0..max_steps {
120        applied = apply(&direction);
121        curvature = direction.full_contraction(&applied);
122        if curvature <= 0.0 {
123            return Err(KrylovError::NotPositiveDefinite(curvature));
124        }
125        step = projection / curvature;
126        solution += &direction * step;
127        residual -= applied * step;
128        if residual.norm().value() <= rel_tol * scale {
129            return Ok(solution);
130        }
131        preconditioned = divide(&residual);
132        next = residual.full_contraction(&preconditioned);
133        direction *= next / projection;
134        direction += &preconditioned;
135        projection = next
136    }
137    Err(KrylovError::MaximumStepsReached(
138        max_steps,
139        residual.norm().value() / scale,
140    ))
141}
142
143fn minimize_residual(
144    max_steps: usize,
145    rel_tol: Scalar,
146    mut apply: impl FnMut(&Vector) -> Vector,
147    preconditioning: impl Precondition,
148    right_hand_side: &Vector,
149) -> Result<Vector, KrylovError> {
150    let size = right_hand_side.len();
151    let mut solution = Vector::zero(size);
152    let divide = |residual: &Vector| preconditioning.apply(residual);
153    let mut previous = right_hand_side.clone();
154    let mut current = previous.clone();
155    let mut preconditioned = divide(&previous);
156    let squared = previous.full_contraction(&preconditioned);
157    if squared < 0.0 {
158        return Err(KrylovError::PreconditionerNotPositiveDefinite(squared));
159    }
160    let scale = squared.sqrt();
161    if scale == 0.0 {
162        return Ok(solution);
163    }
164    let (mut cosine, mut sine) = (-1.0, 0.0);
165    let mut length = scale;
166    let mut off_diagonal = scale;
167    let (mut previous_off, mut carried, mut trailing) = (0.0, 0.0, 0.0 as Scalar);
168    let mut direction = Vector::zero(size);
169    let mut older;
170    let mut old = Vector::zero(size);
171    let mut basis;
172    let load = right_hand_side.norm().value();
173    let mut watched = 1.0;
174    let mut demanded = rel_tol;
175    for step in 0..max_steps {
176        basis = &preconditioned * off_diagonal.recip();
177        preconditioned = apply(&basis);
178        if step > 0 {
179            preconditioned -= &previous * (off_diagonal / previous_off)
180        }
181        let diagonal_entry = basis.full_contraction(&preconditioned);
182        preconditioned -= &current * (diagonal_entry / off_diagonal);
183        previous = replace(&mut current, preconditioned);
184        preconditioned = divide(&current);
185        previous_off = off_diagonal;
186        let squared = current.full_contraction(&preconditioned);
187        if squared < -Scalar::EPSILON * scale * scale {
188            return Err(KrylovError::PreconditionerNotPositiveDefinite(squared));
189        }
190        off_diagonal = squared.max(0.0).sqrt();
191        let reached = carried;
192        let shifted = cosine * trailing + sine * diagonal_entry;
193        let remaining = sine * trailing - cosine * diagonal_entry;
194        carried = sine * off_diagonal;
195        trailing = -cosine * off_diagonal;
196        let rotated = remaining
197            .hypot(off_diagonal)
198            .max(Scalar::EPSILON * scale.max(1.0));
199        cosine = remaining / rotated;
200        sine = off_diagonal / rotated;
201        older = replace(&mut old, direction);
202        direction = (basis - &older * reached - &old * shifted) * rotated.recip();
203        solution += &direction * (cosine * length);
204        length *= sine;
205        let estimate = length.abs() / scale;
206        let checkpoint = step % PATIENCE == PATIENCE - 1;
207        if estimate <= demanded || checkpoint {
208            let truth = (right_hand_side.clone() - apply(&solution)).norm().value() / load;
209            if truth <= rel_tol {
210                return Ok(solution);
211            }
212            if checkpoint {
213                if truth > PROGRESS * watched {
214                    return if truth <= ACCEPTABLE {
215                        Ok(solution)
216                    } else {
217                        Err(KrylovError::StoppedShortening(step + 1, truth))
218                    };
219                }
220                watched = truth
221            }
222            demanded = demanded.min(estimate * 0.1)
223        }
224    }
225    Err(KrylovError::MaximumStepsReached(
226        max_steps,
227        (right_hand_side.clone() - apply(&solution)).norm().value() / load,
228    ))
229}
230
231fn restarted(
232    max_steps: usize,
233    restart: usize,
234    rel_tol: Scalar,
235    mut apply: impl FnMut(&Vector) -> Vector,
236    preconditioning: impl Precondition,
237    right_hand_side: &Vector,
238) -> Result<Vector, KrylovError> {
239    let scale = right_hand_side.norm().value();
240    let mut solution = Vector::zero(right_hand_side.len());
241    if scale == 0.0 {
242        return Ok(solution);
243    }
244    let restart = restart.max(1);
245    let mut steps = 0;
246    let mut relative = 1.0;
247    let mut residual = right_hand_side.clone();
248    while steps < max_steps {
249        let beta = residual.norm().value();
250        let mut basis = vec![&residual * beta.recip()];
251        let mut preconditioned = Vec::new();
252        let mut triangle = Vec::<Vec<Scalar>>::new();
253        let mut rotations = Vec::<(Scalar, Scalar)>::new();
254        let mut projected = vec![beta];
255        let mut inner = 0;
256        while inner < restart && steps < max_steps {
257            preconditioned.push(preconditioning.apply(&basis[inner]));
258            let mut next = apply(&preconditioned[inner]);
259            let mut column = vec![0.0; inner + 2];
260            for (entry, vector) in column.iter_mut().zip(&basis) {
261                *entry = next.full_contraction(vector);
262                next -= vector * *entry;
263            }
264            let subdiagonal = next.norm().value();
265            column[inner + 1] = subdiagonal;
266            for (row, &(cosine, sine)) in rotations.iter().enumerate() {
267                let upper = cosine * column[row] + sine * column[row + 1];
268                column[row + 1] = cosine * column[row + 1] - sine * column[row];
269                column[row] = upper;
270            }
271            let length = column[inner].hypot(column[inner + 1]);
272            let (cosine, sine) = if length == 0.0 {
273                (1.0, 0.0)
274            } else {
275                (column[inner] / length, column[inner + 1] / length)
276            };
277            column[inner] = length;
278            column.truncate(inner + 1);
279            triangle.push(column);
280            rotations.push((cosine, sine));
281            projected.push(-sine * projected[inner]);
282            projected[inner] *= cosine;
283            inner += 1;
284            steps += 1;
285            relative = projected[inner].abs() / scale;
286            if relative <= rel_tol || subdiagonal <= Scalar::EPSILON * beta {
287                break;
288            }
289            basis.push(next * subdiagonal.recip());
290        }
291        let mut coefficients = vec![0.0; inner];
292        for row in (0..inner).rev() {
293            let tail: Scalar = (row + 1..inner)
294                .map(|column| triangle[column][row] * coefficients[column])
295                .sum();
296            coefficients[row] = (projected[row] - tail) / triangle[row][row];
297        }
298        for (coefficient, vector) in coefficients.iter().zip(&preconditioned) {
299            solution += vector * *coefficient;
300        }
301        residual = right_hand_side.clone() - apply(&solution);
302        relative = residual.norm().value() / scale;
303        if relative <= rel_tol {
304            return Ok(solution);
305        }
306    }
307    Err(KrylovError::MaximumStepsReached(max_steps, relative))
308}
309
310/// Possible errors encountered during an iterative linear solve.
311pub enum KrylovError {
312    MaximumStepsReached(usize, Scalar),
313    NotPositiveDefinite(Scalar),
314    PreconditionerNotPositiveDefinite(Scalar),
315    StoppedShortening(usize, Scalar),
316}
317
318impl StyledError for KrylovError {
319    fn message(&self, style: &Style) -> String {
320        let (h, c) = (style.headline, style.frame);
321        match self {
322            Self::MaximumStepsReached(steps, relative) => format!(
323                "{h}Maximum number of iterations ({steps}) reached.{c}\n\
324                Residual relative to the one started from: {relative:?}."
325            ),
326            Self::NotPositiveDefinite(curvature) => format!(
327                "{h}The operator is not positive definite.{c}\n\
328                Curvature along a direction: {curvature:?}."
329            ),
330            Self::PreconditionerNotPositiveDefinite(squared) => format!(
331                "{h}The preconditioner is not positive definite.{c}\n\
332                Squared length of a residual through it: {squared:?}."
333            ),
334            Self::StoppedShortening(steps, relative) => format!(
335                "{h}The residual stopped shortening after {steps} iterations.{c}\n\
336                Residual relative to the one started from: {relative:?}."
337            ),
338        }
339    }
340}
341
342styled_error!(KrylovError);
343
344impl From<KrylovError> for OptimizationError {
345    fn from(error: KrylovError) -> Self {
346        Self::Upstream(error.to_string(), "Krylov".to_string())
347    }
348}
349
350impl From<KrylovError> for AssertionError {
351    fn from(error: KrylovError) -> Self {
352        Self {
353            message: error.to_string(),
354        }
355    }
356}