iterative_solvers/nalgebra/
cg.rs

1//! Conjugate Gradient (CG) method.
2
3use std::ops::Mul;
4
5use super::MatrixOp;
6use crate::{IterSolverError, IterSolverResult};
7use nalgebra::DVector;
8
9/// Conjugate Gradient (CG) method for solving linear systems Ax = b.
10///
11/// The Conjugate Gradient method is an iterative algorithm for solving systems of linear
12/// equations where the coefficient matrix A is symmetric and positive definite. It is
13/// particularly effective for large sparse systems.
14///
15/// # Algorithm Overview
16///
17/// The CG method generates a sequence of approximations to the solution by minimizing
18/// the quadratic form associated with the linear system. At each iteration, it computes
19/// a search direction that is conjugate to all previous search directions with respect
20/// to the matrix A.
21///
22/// # Convergence
23///
24/// For a symmetric positive definite matrix, the CG method is guaranteed to converge
25/// to the exact solution in at most n iterations (where n is the size of the matrix),
26/// though in practice it often converges much faster, especially for well-conditioned
27/// systems.
28///
29/// # Examples
30///
31/// ```rust
32/// use nalgebra::{DMatrix, DVector};
33/// use iterative_solvers::CG;
34///
35/// // Create a simple 2x2 symmetric positive definite system
36/// let mat = DMatrix::from_row_slice(2, 2, &[4.0, 1.0, 1.0, 3.0]);
37/// let rhs = DVector::from_vec(vec![1.0, 2.0]);
38/// let abstol = 1e-10;
39/// let reltol = 1e-8;
40///
41/// let mut cg = CG::new(&mat, &rhs, abstol, reltol).unwrap();
42/// let solution = cg.solve();
43/// ```
44#[derive(Debug, Clone)]
45pub struct CG<'mat, Mat: MatrixOp> {
46    mat: &'mat Mat,
47    solution: DVector<f64>,
48    residual: f64,
49    iteration: usize,
50    r: DVector<f64>,
51    c: DVector<f64>,
52    u: DVector<f64>,
53    tol: f64,
54    prev_residual: f64,
55}
56
57impl<'mat, Mat: MatrixOp> CG<'mat, Mat> {
58    /// Create a new `CG` solver with the given matrix, right-hand side, and tolerance.
59    ///
60    /// # Arguments
61    ///
62    /// * `mat` - The coefficient matrix A.
63    /// * `rhs` - The right-hand side vector b.
64    /// * `abstol` - The absolute tolerance for the residual.
65    /// * `reltol` - The relative tolerance for the residual.
66    ///
67    /// # Tolerance
68    ///
69    /// The tolerance is the stopping criterion for the CG method. It is the maximum of the absolute tolerance and the relative tolerance.
70    ///
71    /// The stopping criterion is `|r_k| ≤ max(reltol * |r_0|, abstol)`,
72    /// where `r_k ≈ A x_k - b` is the residual at the `k`th iteration, and `r_0 ≈ A x_0 - b` is the initial residual.
73    ///
74    /// # Examples
75    ///
76    /// ```rust
77    /// use nalgebra::{DMatrix, DVector};
78    /// use iterative_solvers::CG;
79    ///
80    /// // Create a simple 2x2 symmetric positive definite system
81    /// let mat = DMatrix::from_row_slice(2, 2, &[4.0, 1.0, 1.0, 3.0]);
82    /// let rhs = DVector::from_vec(vec![1.0, 2.0]);
83    /// let abstol = 1e-10;
84    /// let reltol = 1e-8;
85    ///
86    /// let mut cg = CG::new(&mat, &rhs, abstol, reltol).unwrap();
87    /// let solution = cg.solve();
88    /// ```
89    pub fn new(
90        mat: &'mat Mat,
91        rhs: &'mat DVector<f64>,
92        abstol: f64,
93        reltol: f64,
94    ) -> IterSolverResult<Self> {
95        if !mat.is_square() {
96            return Err(IterSolverError::DimensionError(format!(
97                "The matrix is not square, whose shape is ({}, {})",
98                mat.nrows(),
99                mat.ncols()
100            )));
101        }
102        if mat.nrows() != rhs.len() {
103            return Err(IterSolverError::DimensionError(format!(
104                "The matrix with order {}, and the rhs with length {}, do not match",
105                mat.nrows(),
106                rhs.len()
107            )));
108        }
109        let n = mat.nrows();
110        let x = DVector::zeros(n);
111        let r = rhs.clone();
112        let c = DVector::zeros(n);
113        let u = DVector::zeros(n);
114        let residual = r.norm();
115        let prev_residual = residual;
116        let iteration = 0;
117        let tol = abstol.max(reltol * residual);
118        Ok(Self {
119            mat,
120            solution: x,
121            residual,
122            iteration,
123            r,
124            c,
125            u,
126            tol,
127            prev_residual,
128        })
129    }
130
131    /// Create a new `CG` solver with the given matrix, right-hand side, tolerance, and initial guess.
132    ///
133    /// # Arguments
134    ///
135    /// * `mat` - The coefficient matrix A.
136    /// * `rhs` - The right-hand side vector b.
137    /// * `abstol` - The absolute tolerance for the residual.
138    /// * `reltol` - The relative tolerance for the residual.
139    /// * `initial_guess` - The initial guess for the solution.
140    ///
141    /// # Tolerance
142    ///
143    /// The tolerance is the stopping criterion for the CG method. It is the maximum of the absolute tolerance and the relative tolerance.
144    ///
145    /// The stopping criterion is `|r_k| ≤ max(reltol * |r_0|, abstol)`,
146    /// where `r_k ≈ A x_k - b` is the residual at the `k`th iteration, and `r_0 ≈ A x_0 - b` is the initial residual.
147    ///
148    /// # Initial Guess
149    ///
150    /// The initial guess is the starting point for the CG method. It is used to compute the initial residual.
151    ///
152    /// # Examples
153    ///
154    /// ```rust
155    /// use nalgebra::{DMatrix, DVector};
156    /// use iterative_solvers::CG;
157    ///
158    /// // Create a simple 2x2 symmetric positive definite system
159    /// let mat = DMatrix::from_row_slice(2, 2, &[4.0, 1.0, 1.0, 3.0]);
160    /// let rhs = DVector::from_vec(vec![1.0, 2.0]);
161    /// let abstol = 1e-10;
162    /// let reltol = 1e-8;
163    /// let initial_guess = DVector::from_vec(vec![0.0, 0.0]);
164    ///
165    /// let mut cg = CG::new_with_initial_guess(&mat, &rhs, initial_guess, abstol, reltol).unwrap();
166    /// let solution = cg.solve();
167    /// ```
168    pub fn new_with_initial_guess(
169        mat: &'mat Mat,
170        rhs: &'mat DVector<f64>,
171        initial_guess: DVector<f64>,
172        abstol: f64,
173        reltol: f64,
174    ) -> IterSolverResult<Self>
175    where
176        &'mat Mat: Mul<DVector<f64>, Output = DVector<f64>>,
177    {
178        if !mat.is_square() {
179            return Err(IterSolverError::DimensionError(format!(
180                "The matrix is not square, whose shape is ({}, {})",
181                mat.nrows(),
182                mat.ncols()
183            )));
184        }
185        if mat.nrows() != rhs.len() {
186            return Err(IterSolverError::DimensionError(format!(
187                "The matrix with order {}, and the rhs with length {}, do not match",
188                mat.nrows(),
189                rhs.len()
190            )));
191        }
192        if initial_guess.len() != mat.nrows() {
193            return Err(IterSolverError::DimensionError(format!(
194                "The initial guess with length {}, and the matrix with order {}, do not match",
195                initial_guess.len(),
196                mat.nrows()
197            )));
198        }
199        let n = mat.nrows();
200        let r = rhs - mat * initial_guess.clone();
201        let c = DVector::zeros(n);
202        let u = DVector::zeros(n);
203        let residual = r.norm();
204        let prev_residual = residual;
205        let iteration = 0;
206        let tol = abstol.max(reltol * residual);
207        Ok(Self {
208            mat,
209            solution: initial_guess,
210            residual,
211            iteration,
212            r,
213            c,
214            u,
215            tol,
216            prev_residual,
217        })
218    }
219
220    /// Check if the solver has converged.
221    #[inline]
222    fn converged(&self) -> bool {
223        self.residual <= self.tol
224    }
225
226    /// Check if the solver has reached the maximum number of iterations.
227    #[inline]
228    fn done(&self) -> bool {
229        (self.iteration >= self.max_iter()) || self.converged()
230    }
231
232    /// Get the maximum number of iterations.
233    #[inline]
234    fn max_iter(&self) -> usize {
235        self.solution.len()
236    }
237
238    /// Consume the solver and return the solved result.
239    pub fn solve(mut self) -> Self {
240        self.by_ref().count();
241        self
242    }
243
244    /// Get the solution.
245    pub fn solution(&self) -> &DVector<f64> {
246        &self.solution
247    }
248
249    /// Get the residual.
250    pub fn residual(&self) -> f64 {
251        self.residual
252    }
253
254    /// Get the iteration.
255    pub fn iteration(&self) -> usize {
256        self.iteration
257    }
258
259    /// Get the matrix.
260    pub fn mat(&self) -> &Mat {
261        self.mat
262    }
263
264    /// Get the residual vector
265    pub fn residual_vector(&self) -> &DVector<f64> {
266        &self.r
267    }
268
269    /// Get the conjugate direction
270    pub fn conjugate_direction(&self) -> &DVector<f64> {
271        &self.u
272    }
273}
274
275impl<'mat, Mat: MatrixOp> Iterator for CG<'mat, Mat> {
276    type Item = f64;
277
278    fn next(&mut self) -> Option<Self::Item> {
279        if self.done() {
280            return None;
281        }
282
283        // u = r + beta * u
284        let beta = self.residual.powi(2) / self.prev_residual.powi(2);
285        self.u.axpy(1.0, &self.r, beta);
286
287        // c = A * u
288        self.mat.gemv(1.0, &self.u, 0.0, &mut self.c);
289
290        // update solution and residual
291        // x = x + alpha * u
292        // r = r - alpha * c
293        let alpha = self.residual.powi(2) / self.u.dot(&self.c);
294        self.solution.axpy(alpha, &self.u, 1.0);
295        self.r.axpy(-alpha, &self.c, 1.0);
296
297        self.prev_residual = self.residual;
298        self.residual = self.r.norm();
299
300        self.iteration += 1;
301
302        Some(self.residual)
303    }
304}
305
306/// Solve a linear system Ax = b using the Conjugate Gradient method.
307///
308/// # Arguments
309///
310/// * `mat` - The coefficient matrix A.
311/// * `rhs` - The right-hand side vector b.
312/// * `abstol` - The absolute tolerance for the residual.
313/// * `reltol` - The relative tolerance for the residual.
314///
315/// # Tolerance
316///
317/// The tolerance is the stopping criterion for the CG method. It is the maximum of the absolute tolerance and the relative tolerance.
318///
319/// The stopping criterion is `|r_k| ≤ max(reltol * |r_0|, abstol)`,
320/// where `r_k ≈ A x_k - b` is the residual at the `k`th iteration, and `r_0 ≈ A x_0 - b` is the initial residual.
321///
322/// # Examples
323///
324/// ```rust
325/// use nalgebra::{DMatrix, DVector};
326/// use iterative_solvers::cg;
327///
328/// // Create a simple 2x2 symmetric positive definite system
329/// let mat = DMatrix::from_row_slice(2, 2, &[4.0, 1.0, 1.0, 3.0]);
330/// let rhs = DVector::from_vec(vec![1.0, 2.0]);
331/// let abstol = 1e-10;
332/// let reltol = 1e-8;
333///
334/// let solution = cg(&mat, &rhs, abstol, reltol).unwrap();
335/// ```
336pub fn cg<'mat, Mat: MatrixOp>(
337    mat: &'mat Mat,
338    rhs: &'mat DVector<f64>,
339    abstol: f64,
340    reltol: f64,
341) -> IterSolverResult<CG<'mat, Mat>> {
342    let mut solver = CG::new(mat, rhs, abstol, reltol)?;
343    solver.by_ref().count();
344    Ok(solver)
345}
346
347/// Solve a linear system Ax = b using the Conjugate Gradient method.
348///
349/// # Arguments
350///
351/// * `mat` - The coefficient matrix A.
352/// * `rhs` - The right-hand side vector b.
353/// * `abstol` - The absolute tolerance for the residual.
354/// * `reltol` - The relative tolerance for the residual.
355/// * `initial_guess` - The initial guess for the solution.
356///
357/// # Tolerance
358///
359/// The tolerance is the stopping criterion for the CG method. It is the maximum of the absolute tolerance and the relative tolerance.
360///
361/// The stopping criterion is `|r_k| ≤ max(reltol * |r_0|, abstol)`,
362/// where `r_k ≈ A x_k - b` is the residual at the `k`th iteration, and `r_0 ≈ A x_0 - b` is the initial residual.
363///
364/// # Initial Guess
365///
366/// The initial guess is the starting point for the CG method. It is used to compute the initial residual.
367///
368/// # Examples
369///
370/// ```rust
371/// use nalgebra::{DMatrix, DVector};
372/// use iterative_solvers::cg_with_initial_guess;
373///
374/// // Create a simple 2x2 symmetric positive definite system
375/// let mat = DMatrix::from_row_slice(2, 2, &[4.0, 1.0, 1.0, 3.0]);
376/// let rhs = DVector::from_vec(vec![1.0, 2.0]);
377/// let abstol = 1e-10;
378/// let reltol = 1e-8;
379/// let initial_guess = DVector::from_vec(vec![0.0, 0.0]);
380///
381/// let solution = cg_with_initial_guess(&mat, &rhs, initial_guess, abstol, reltol).unwrap();
382/// ```
383pub fn cg_with_initial_guess<'mat, Mat: MatrixOp>(
384    mat: &'mat Mat,
385    rhs: &'mat DVector<f64>,
386    initial_guess: DVector<f64>,
387    abstol: f64,
388    reltol: f64,
389) -> IterSolverResult<CG<'mat, Mat>>
390where
391    &'mat Mat: Mul<DVector<f64>, Output = DVector<f64>>,
392{
393    let mut solver = CG::new_with_initial_guess(mat, rhs, initial_guess, abstol, reltol)?;
394    solver.by_ref().count();
395    Ok(solver)
396}
397
398#[cfg(test)]
399mod tests {
400    use std::f64::consts::PI;
401
402    use super::*;
403    use crate::utils::{dense::symmetric_tridiagonal, sparse::symmetric_tridiagonal_csc};
404
405    #[test]
406    fn test_cg_dense() {
407        let n = 1024;
408        let h = 1.0 / 1024.0;
409        let a = vec![2.0 / (h * h); n - 1];
410        let b = vec![-1.0 / (h * h); n - 2];
411        let mat = symmetric_tridiagonal(&a, &b).unwrap();
412        let rhs: Vec<_> = (1..n)
413            .map(|i| PI * PI * (i as f64 * h * PI).sin())
414            .collect();
415        let solution: Vec<_> = (1..n).map(|i| (i as f64 * h * PI).sin()).collect();
416        let solution = DVector::from_vec(solution);
417        let rhs = DVector::from_vec(rhs);
418        let solver = cg(&mat, &rhs, 1e-10, 1e-8).unwrap();
419        let e = (solution - solver.solution()).norm();
420        assert!(e < 1e-4);
421    }
422
423    #[test]
424    fn test_cg_sparse() {
425        let n = 1024;
426        let h = 1.0 / 1024.0;
427        let a = vec![2.0 / (h * h); n - 1];
428        let b = vec![-1.0 / (h * h); n - 2];
429        let mat = symmetric_tridiagonal_csc(&a, &b).unwrap();
430        let rhs: Vec<_> = (1..n)
431            .map(|i| PI * PI * (i as f64 * h * PI).sin())
432            .collect();
433        let solution: Vec<_> = (1..n).map(|i| (i as f64 * h * PI).sin()).collect();
434        let solution = DVector::from_vec(solution);
435        let rhs = DVector::from_vec(rhs);
436        let solver = cg(&mat, &rhs, 1e-10, 1e-8).unwrap();
437        let e = (solution - solver.solution()).norm();
438        assert!(e < 1e-4);
439    }
440}