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}