iterative_solvers/
cg.rs

1//! Conjugate Gradient (CG) method.
2//!
3//! # Copyright
4//!
5//! Portions of this implementation are derived from [IterativeSolvers.jl](https://github.com/JuliaLinearAlgebra/IterativeSolvers.jl).
6//!
7//! Copyright (c) 2013--2016 The Julia Language.
8//!
9//! Licensed under the MIT License.
10
11use std::ops::Mul;
12
13use super::MatrixOp;
14#[cfg(not(feature = "ndarray"))]
15use crate::utils::is_vector;
16use crate::{
17    IterSolverError, IterSolverResult,
18    ops::Vector,
19    utils::{axpy, dot, norm_l2, zeros},
20};
21
22/// Conjugate Gradient (CG) method for solving linear systems Ax = b.
23///
24/// The Conjugate Gradient method is an iterative algorithm for solving systems of linear
25/// equations where the coefficient matrix A is symmetric and positive definite. It is
26/// particularly effective for large sparse systems.
27///
28/// # Algorithm Overview
29///
30/// The CG method generates a sequence of approximations to the solution by minimizing
31/// the quadratic form associated with the linear system. At each iteration, it computes
32/// a search direction that is conjugate to all previous search directions with respect
33/// to the matrix A.
34///
35/// # Convergence
36///
37/// For a symmetric positive definite matrix, the CG method is guaranteed to converge
38/// to the exact solution in at most n iterations (where n is the size of the matrix),
39/// though in practice it often converges much faster, especially for well-conditioned
40/// systems.
41///
42/// # Examples
43///
44/// **With nalgebra feature:**
45///
46/// ```rust
47/// # #[cfg(feature = "nalgebra")]
48/// # {
49/// use nalgebra::DVector;
50/// use iterative_solvers::CG;
51///
52/// let n = 1024;
53/// let h = 1.0 / (n as f64);
54/// let a = vec![2.0 / (h * h); n - 1];
55/// let b = vec![-1.0 / (h * h); n - 2];
56/// let mat = symmetric_tridiagonal(&a, &b).unwrap();
57/// let rhs: Vec<_> = (1..n)
58///     .map(|i| PI * PI * (i as f64 * h * PI).sin())
59///     .collect();
60/// let rhs = DVector::from_vec(rhs);
61/// let abstol = 1e-10;
62/// let reltol = 1e-8;
63///
64/// let mut cg = CG::new(&mat, &rhs, abstol, reltol).unwrap();
65/// let solution = cg.solve();
66/// # }
67/// ```
68///
69/// **With faer feature:**
70///
71/// ```rust
72/// # #[cfg(feature = "faer")]
73/// # {
74/// use faer::Mat;
75/// use iterative_solvers::CG;
76/// use iterative_solvers::utils::dense::symmetric_tridiagonal;
77/// use std::f64::consts::PI;
78///
79/// let n = 1024;
80/// let h = 1.0 / (n as f64);
81/// let a = vec![2.0 / (h * h); n - 1];
82/// let b = vec![-1.0 / (h * h); n - 2];
83/// let mat = symmetric_tridiagonal(&a, &b).unwrap();
84/// let rhs: Vec<_> = (1..n)
85///     .map(|i| PI * PI * (i as f64 * h * PI).sin())
86///     .collect();
87/// let rhs = Mat::from_fn(n - 1, 1, |i, _| rhs[i]);
88/// let abstol = 1e-10;
89/// let reltol = 1e-8;
90///
91/// let mut cg = CG::new(&mat, &rhs, abstol, reltol).unwrap();
92/// let solution = cg.solve();
93/// # }
94/// ```
95///
96/// **With ndarray feature:**
97///
98/// ```rust
99/// # #[cfg(feature = "ndarray")]
100/// # {
101/// use ndarray::arr1;
102/// use iterative_solvers::CG;
103/// use iterative_solvers::utils::dense::symmetric_tridiagonal;
104/// use std::f64::consts::PI;
105///
106/// let n = 1024;
107/// let h = 1.0 / (n as f64);
108/// let a = vec![2.0 / (h * h); n - 1];
109/// let b = vec![-1.0 / (h * h); n - 2];
110/// let mat = symmetric_tridiagonal(&a, &b).unwrap();
111/// let rhs: Vec<_> = (1..n)
112///     .map(|i| PI * PI * (i as f64 * h * PI).sin())
113///     .collect();
114/// let rhs = arr1(&rhs);
115/// let abstol = 1e-10;
116/// let reltol = 1e-8;
117///
118/// let mut cg = CG::new(&mat, &rhs, abstol, reltol).unwrap();
119/// let solution = cg.solve();
120/// # }
121/// ```
122#[derive(Debug, Clone)]
123pub struct CG<'mat, Mat: MatrixOp> {
124    mat: &'mat Mat,
125    solution: Vector<f64>,
126    residual: f64,
127    iteration: usize,
128    r: Vector<f64>,
129    c: Vector<f64>,
130    u: Vector<f64>,
131    tol: f64,
132    prev_residual: f64,
133}
134
135impl<'mat, Mat: MatrixOp> CG<'mat, Mat> {
136    /// Create a new `CG` solver with the given matrix, right-hand side, and tolerance.
137    ///
138    /// # Arguments
139    ///
140    /// * `mat` - The coefficient matrix A.
141    /// * `rhs` - The right-hand side vector b.
142    /// * `abstol` - The absolute tolerance for the residual.
143    /// * `reltol` - The relative tolerance for the residual.
144    ///
145    /// # Tolerance
146    ///
147    /// The tolerance is the stopping criterion for the CG method. It is the maximum of the absolute tolerance and the relative tolerance.
148    ///
149    /// The stopping criterion is `|r_k| ≤ max(reltol * |r_0|, abstol)`,
150    /// 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.
151    pub fn new(
152        mat: &'mat Mat,
153        rhs: &'mat Vector<f64>,
154        abstol: f64,
155        reltol: f64,
156    ) -> IterSolverResult<Self> {
157        if !mat.is_square() {
158            return Err(IterSolverError::DimensionError(format!(
159                "The matrix is not square, whose shape is ({}, {})",
160                mat.nrows(),
161                mat.ncols()
162            )));
163        }
164        #[cfg(feature = "faer")]
165        if !is_vector(rhs) {
166            return Err(IterSolverError::DimensionError(format!(
167                "The `rhs` should be a vector, but got a matrix with shape ({}, {}).",
168                rhs.nrows(),
169                rhs.ncols()
170            )));
171        }
172        if mat.nrows() != rhs.len() {
173            return Err(IterSolverError::DimensionError(format!(
174                "The matrix with order {}, and the rhs with length {}, do not match",
175                mat.nrows(),
176                rhs.len()
177            )));
178        }
179
180        let n = mat.nrows();
181        let x = zeros(n);
182
183        let r = rhs.clone();
184
185        let c = zeros(n);
186
187        let u = zeros(n);
188
189        let residual = norm_l2(&r);
190
191        let prev_residual = residual;
192        let iteration = 0;
193        let tol = abstol.max(reltol * residual);
194        Ok(Self {
195            mat,
196            solution: x,
197            residual,
198            iteration,
199            r,
200            c,
201            u,
202            tol,
203            prev_residual,
204        })
205    }
206
207    /// Create a new `CG` solver with the given matrix, right-hand side, tolerance, and initial guess.
208    ///
209    /// # Arguments
210    ///
211    /// * `mat` - The coefficient matrix A.
212    /// * `rhs` - The right-hand side vector b.
213    /// * `abstol` - The absolute tolerance for the residual.
214    /// * `reltol` - The relative tolerance for the residual.
215    /// * `initial_guess` - The initial guess for the solution.
216    ///
217    /// # Tolerance
218    ///
219    /// The tolerance is the stopping criterion for the CG method. It is the maximum of the absolute tolerance and the relative tolerance.
220    ///
221    /// The stopping criterion is `|r_k| ≤ max(reltol * |r_0|, abstol)`,
222    /// 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.
223    ///
224    /// # Initial Guess
225    ///
226    /// The initial guess is the starting point for the CG method. It is used to compute the initial residual.
227    pub fn new_with_initial_guess(
228        mat: &'mat Mat,
229        rhs: &'mat Vector<f64>,
230        initial_guess: Vector<f64>,
231        abstol: f64,
232        reltol: f64,
233    ) -> IterSolverResult<Self>
234    where
235        &'mat Mat: Mul<Vector<f64>, Output = Vector<f64>>,
236    {
237        if !mat.is_square() {
238            return Err(IterSolverError::DimensionError(format!(
239                "The matrix is not square, whose shape is ({}, {})",
240                mat.nrows(),
241                mat.ncols()
242            )));
243        }
244        #[cfg(not(feature = "ndarray"))]
245        if !is_vector(rhs) {
246            return Err(IterSolverError::DimensionError(format!(
247                "The `rhs` should be a vector, but got a matrix with shape ({}, {}).",
248                rhs.nrows(),
249                rhs.ncols()
250            )));
251        }
252
253        if mat.nrows() != rhs.len() {
254            return Err(IterSolverError::DimensionError(format!(
255                "The matrix with order {}, and the rhs with length {}, do not match",
256                mat.nrows(),
257                rhs.len()
258            )));
259        }
260        #[cfg(not(feature = "ndarray"))]
261        if !is_vector(&initial_guess) {
262            return Err(IterSolverError::DimensionError(format!(
263                "The `initial_guess` should be a vector, but got a matrix with shape ({}, {}).",
264                initial_guess.nrows(),
265                initial_guess.ncols()
266            )));
267        }
268
269        if initial_guess.len() != mat.nrows() {
270            return Err(IterSolverError::DimensionError(format!(
271                "The initial guess with length {}, and the matrix with order {}, do not match",
272                initial_guess.len(),
273                mat.nrows()
274            )));
275        }
276        let n = mat.nrows();
277        let r = rhs - mat * initial_guess.clone();
278
279        let c = zeros(n);
280
281        let u = zeros(n);
282
283        let residual = norm_l2(&r);
284
285        let prev_residual = residual;
286        let iteration = 0;
287        let tol = abstol.max(reltol * residual);
288        Ok(Self {
289            mat,
290            solution: initial_guess,
291            residual,
292            iteration,
293            r,
294            c,
295            u,
296            tol,
297            prev_residual,
298        })
299    }
300
301    /// Check if the solver has converged.
302    #[inline]
303    fn converged(&self) -> bool {
304        self.residual <= self.tol
305    }
306
307    /// Check if the solver has reached the maximum number of iterations.
308    #[inline]
309    fn done(&self) -> bool {
310        (self.iteration >= self.max_iter()) || self.converged()
311    }
312
313    /// Get the maximum number of iterations.
314    #[inline]
315    fn max_iter(&self) -> usize {
316        self.solution.len()
317    }
318
319    /// Consume the solver and return the solved result.
320    pub fn solve(mut self) -> Self {
321        self.by_ref().count();
322        self
323    }
324
325    /// Get the solution.
326    pub fn solution(&self) -> &Vector<f64> {
327        &self.solution
328    }
329
330    /// Get the residual.
331    pub fn residual(&self) -> f64 {
332        self.residual
333    }
334
335    /// Get the iteration.
336    pub fn iteration(&self) -> usize {
337        self.iteration
338    }
339
340    /// Get the matrix.
341    pub fn mat(&self) -> &Mat {
342        self.mat
343    }
344
345    /// Get the residual vector
346    pub fn residual_vector(&self) -> &Vector<f64> {
347        &self.r
348    }
349
350    /// Get the conjugate direction
351    pub fn conjugate_direction(&self) -> &Vector<f64> {
352        &self.u
353    }
354}
355
356impl<'mat, Mat: MatrixOp> Iterator for CG<'mat, Mat> {
357    type Item = f64;
358
359    fn next(&mut self) -> Option<Self::Item> {
360        if self.done() {
361            return None;
362        }
363
364        // u = r + beta * u
365        let beta = self.residual.powi(2) / self.prev_residual.powi(2);
366
367        axpy(&mut self.u, 1.0, &self.r, beta);
368
369        // c = A * u
370        self.mat.gemv(1.0, &self.u, 0.0, &mut self.c);
371
372        // update solution and residual
373        // x = x + alpha * u
374        // r = r - alpha * c
375        let alpha = self.residual.powi(2) / dot(&self.u, &self.c).unwrap();
376        axpy(&mut self.solution, alpha, &self.u, 1.0);
377        axpy(&mut self.r, -alpha, &self.c, 1.0);
378
379        self.prev_residual = self.residual;
380
381        self.residual = norm_l2(&self.r);
382
383        self.iteration += 1;
384
385        Some(self.residual)
386    }
387}
388
389/// Solve a linear system Ax = b using the Conjugate Gradient method.
390///
391/// # Arguments
392///
393/// * `mat` - The coefficient matrix A.
394/// * `rhs` - The right-hand side vector b.
395/// * `abstol` - The absolute tolerance for the residual.
396/// * `reltol` - The relative tolerance for the residual.
397///
398/// # Tolerance
399///
400/// The tolerance is the stopping criterion for the CG method. It is the maximum of the absolute tolerance and the relative tolerance.
401///
402/// The stopping criterion is `|r_k| ≤ max(reltol * |r_0|, abstol)`,
403/// 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.
404///
405/// # Examples
406///
407/// **With nalgebra feature:**
408///
409/// ```rust
410/// # #[cfg(feature = "nalgebra")]
411/// # {
412/// use nalgebra::DVector;
413/// use iterative_solvers::cg;
414///
415/// let n = 1024;
416/// let h = 1.0 / (n as f64);
417/// let a = vec![2.0 / (h * h); n - 1];
418/// let b = vec![-1.0 / (h * h); n - 2];
419/// let mat = symmetric_tridiagonal(&a, &b).unwrap();
420/// let rhs: Vec<_> = (1..n)
421///     .map(|i| PI * PI * (i as f64 * h * PI).sin())
422///     .collect();
423/// let rhs = DVector::from_vec(rhs);
424/// let abstol = 1e-10;
425/// let reltol = 1e-8;
426///
427/// let solution = cg(&mat, &rhs, abstol, reltol).unwrap();
428/// # }
429/// ```
430///
431/// **With faer feature:**
432///
433/// ```rust
434/// # #[cfg(feature = "faer")]
435/// # {
436/// use faer::Mat;
437/// use iterative_solvers::cg;
438/// use iterative_solvers::utils::dense::symmetric_tridiagonal;
439/// use std::f64::consts::PI;
440///
441/// let n = 1024;
442/// let h = 1.0 / (n as f64);
443/// let a = vec![2.0 / (h * h); n - 1];
444/// let b = vec![-1.0 / (h * h); n - 2];
445/// let mat = symmetric_tridiagonal(&a, &b).unwrap();
446/// let rhs: Vec<_> = (1..n)
447///     .map(|i| PI * PI * (i as f64 * h * PI).sin())
448///     .collect();
449/// let rhs = Mat::from_fn(n - 1, 1, |i, _| rhs[i]);
450/// let solution: Vec<_> = (1..n).map(|i| (i as f64 * h * PI).sin()).collect();
451/// let solution = Mat::from_fn(n - 1, 1, |i, _| solution[i]);
452/// let abstol = 1e-10;
453/// let reltol = 1e-8;
454///
455/// let solver = cg(&mat, &rhs, abstol, reltol).unwrap();
456/// let e = (solution - solver.solution()).norm_l2();
457/// println!("error: {}", e);
458/// # }
459/// ```
460///
461/// **With ndarray feature:**
462///
463/// ```rust
464/// # #[cfg(feature = "ndarray")]
465/// # {
466/// use ndarray::arr1;
467/// use iterative_solvers::cg;
468/// use iterative_solvers::utils::dense::symmetric_tridiagonal;
469/// use std::f64::consts::PI;
470///
471/// let n = 1024;
472/// let h = 1.0 / (n as f64);
473/// let a = vec![2.0 / (h * h); n - 1];
474/// let b = vec![-1.0 / (h * h); n - 2];
475/// let mat = symmetric_tridiagonal(&a, &b).unwrap();
476/// let rhs: Vec<_> = (1..n)
477///     .map(|i| PI * PI * (i as f64 * h * PI).sin())
478///     .collect();
479/// let rhs = arr1(&rhs);
480/// let solution: Vec<_> = (1..n).map(|i| (i as f64 * h * PI).sin()).collect();
481/// let solution = arr1(&solution);
482/// let abstol = 1e-10;
483/// let reltol = 1e-8;
484///
485/// let solver = cg(&mat, &rhs, abstol, reltol).unwrap();
486/// let e = (solution - solver.solution()).norm_l2();
487/// println!("error: {}", e);
488/// # }
489/// ```
490pub fn cg<'mat, Mat: MatrixOp>(
491    mat: &'mat Mat,
492    rhs: &'mat Vector<f64>,
493    abstol: f64,
494    reltol: f64,
495) -> IterSolverResult<CG<'mat, Mat>> {
496    let mut solver = CG::new(mat, rhs, abstol, reltol)?;
497    solver.by_ref().count();
498    Ok(solver)
499}
500
501/// Solve a linear system Ax = b using the Conjugate Gradient method.
502///
503/// # Arguments
504///
505/// * `mat` - The coefficient matrix A.
506/// * `rhs` - The right-hand side vector b.
507/// * `abstol` - The absolute tolerance for the residual.
508/// * `reltol` - The relative tolerance for the residual.
509/// * `initial_guess` - The initial guess for the solution.
510///
511/// # Tolerance
512///
513/// The tolerance is the stopping criterion for the CG method. It is the maximum of the absolute tolerance and the relative tolerance.
514///
515/// The stopping criterion is `|r_k| ≤ max(reltol * |r_0|, abstol)`,
516/// 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.
517///
518/// # Initial Guess
519///
520/// The initial guess is the starting point for the CG method. It is used to compute the initial residual.
521///
522/// # Examples
523///
524/// **With nalgebra feature:**
525///
526/// ```rust
527/// # #[cfg(feature = "nalgebra")]
528/// # {
529/// use nalgebra::DVector;
530/// use iterative_solvers::cg_with_initial_guess;
531///
532/// let n = 1024;
533/// let h = 1.0 / (n as f64);
534/// let a = vec![2.0 / (h * h); n - 1];
535/// let b = vec![-1.0 / (h * h); n - 2];
536/// let mat = symmetric_tridiagonal(&a, &b).unwrap();
537/// let rhs: Vec<_> = (1..n)
538///     .map(|i| PI * PI * (i as f64 * h * PI).sin())
539///     .collect();
540/// let rhs = DVector::from_vec(rhs);
541/// let abstol = 1e-10;
542/// let reltol = 1e-8;
543/// let initial_guess = DVector::from_vec(vec![1.0; n-1]);
544///
545/// let solution = cg_with_initial_guess(&mat, &rhs, initial_guess, abstol, reltol).unwrap();
546/// # }
547/// ```
548///
549/// **With faer feature:**
550///
551/// ```rust
552/// # #[cfg(feature = "faer")]
553/// # {
554/// use faer::Mat;
555/// use iterative_solvers::cg_with_initial_guess;
556/// use iterative_solvers::utils::dense::symmetric_tridiagonal;
557/// use std::f64::consts::PI;
558///
559/// let n = 1024;
560/// let h = 1.0 / (n as f64);
561/// let a = vec![2.0 / (h * h); n - 1];
562/// let b = vec![-1.0 / (h * h); n - 2];
563/// let mat = symmetric_tridiagonal(&a, &b).unwrap();
564/// let rhs: Vec<_> = (1..n)
565///     .map(|i| PI * PI * (i as f64 * h * PI).sin())
566///     .collect();
567/// let rhs = Mat::from_fn(n - 1, 1, |i, _| rhs[i]);
568/// let solution: Vec<_> = (1..n).map(|i| (i as f64 * h * PI).sin()).collect();
569/// let solution = Mat::from_fn(n - 1, 1, |i, _| solution[i]);
570/// let abstol = 1e-10;
571/// let reltol = 1e-8;
572/// let initial_guess = Mat::from_fn(n - 1, 1, |i, _| 1.0);
573///
574/// let solver = cg_with_initial_guess(&mat, &rhs, initial_guess, abstol, reltol).unwrap();
575/// let e = (solution - solver.solution()).norm_l2();
576/// println!("error: {}", e);
577/// # }
578/// ```
579///
580/// **With ndarray feature:**
581///
582/// ```rust
583/// # #[cfg(feature = "ndarray")]
584/// # {
585/// use ndarray::arr1;
586/// use iterative_solvers::cg_with_initial_guess;
587/// use iterative_solvers::utils::dense::symmetric_tridiagonal;
588/// use std::f64::consts::PI;
589///
590/// let n = 1024;
591/// let h = 1.0 / (n as f64);
592/// let a = vec![2.0 / (h * h); n - 1];
593/// let b = vec![-1.0 / (h * h); n - 2];
594/// let mat = symmetric_tridiagonal(&a, &b).unwrap();
595/// let rhs: Vec<_> = (1..n)
596///     .map(|i| PI * PI * (i as f64 * h * PI).sin())
597///     .collect();
598/// let rhs = arr1(&rhs);
599/// let solution: Vec<_> = (1..n).map(|i| (i as f64 * h * PI).sin()).collect();
600/// let solution = arr1(&solution);
601/// let abstol = 1e-10;
602/// let reltol = 1e-8;
603/// let initial_guess = arr1(&vec![1.0; n-1]);
604///
605/// let solver = cg_with_initial_guess(&mat, &rhs, initial_guess, abstol, reltol).unwrap();
606/// let e = (solution - solver.solution()).norm_l2();
607/// println!("error: {}", e);
608/// # }
609/// ```
610pub fn cg_with_initial_guess<'mat, Mat: MatrixOp>(
611    mat: &'mat Mat,
612    rhs: &'mat Vector<f64>,
613    initial_guess: Vector<f64>,
614    abstol: f64,
615    reltol: f64,
616) -> IterSolverResult<CG<'mat, Mat>>
617where
618    &'mat Mat: Mul<Vector<f64>, Output = Vector<f64>>,
619{
620    let mut solver = CG::new_with_initial_guess(mat, rhs, initial_guess, abstol, reltol)?;
621    solver.by_ref().count();
622    Ok(solver)
623}
624
625#[cfg(test)]
626mod tests {
627    use std::f64::consts::PI;
628
629    use super::*;
630    use crate::utils::{dense::symmetric_tridiagonal, sparse::symmetric_tridiagonal_csc};
631
632    #[test]
633    #[cfg(feature = "nalgebra")]
634    fn test_cg_dense() {
635        let n = 1024;
636        let h = 1.0 / (n as f64);
637        let a = vec![2.0 / (h * h); n - 1];
638        let b = vec![-1.0 / (h * h); n - 2];
639        let mat = symmetric_tridiagonal(&a, &b).unwrap();
640        let rhs: Vec<_> = (1..n)
641            .map(|i| PI * PI * (i as f64 * h * PI).sin())
642            .collect();
643        let solution: Vec<_> = (1..n).map(|i| (i as f64 * h * PI).sin()).collect();
644        let solution = Vector::from_vec(solution);
645        let rhs = Vector::from_vec(rhs);
646        let solver = cg(&mat, &rhs, 1e-10, 1e-8).unwrap();
647        let e = (solution - solver.solution()).norm();
648        assert!(e < 1e-4);
649    }
650
651    #[test]
652    #[cfg(feature = "faer")]
653    fn test_cg_dense() {
654        let n = 1024;
655        let h = 1.0 / (n as f64);
656        let a = vec![2.0 / (h * h); n - 1];
657        let b = vec![-1.0 / (h * h); n - 2];
658        let mat = symmetric_tridiagonal(&a, &b).unwrap();
659        let rhs: Vec<_> = (1..n)
660            .map(|i| PI * PI * (i as f64 * h * PI).sin())
661            .collect();
662        let solution: Vec<_> = (1..n).map(|i| (i as f64 * h * PI).sin()).collect();
663        let solution = faer::Mat::from_fn(n - 1, 1, |i, _| solution[i]);
664        let rhs = faer::Mat::from_fn(n - 1, 1, |i, _| rhs[i]);
665        let solver = cg(&mat, &rhs, 1e-10, 1e-8).unwrap();
666        let e = (solution - solver.solution()).norm_l2();
667        assert!(e < 1e-4);
668    }
669
670    #[test]
671    #[cfg(feature = "ndarray")]
672    fn test_cg_dense() {
673        use ndarray::arr1;
674        use ndarray_linalg::Norm;
675
676        let n = 1024;
677        let h = 1.0 / (n as f64);
678        let a = vec![2.0 / (h * h); n - 1];
679        let b = vec![-1.0 / (h * h); n - 2];
680        let mat = symmetric_tridiagonal(&a, &b).unwrap();
681        let rhs: Vec<_> = (1..n)
682            .map(|i| PI * PI * (i as f64 * h * PI).sin())
683            .collect();
684        let solution: Vec<_> = (1..n).map(|i| (i as f64 * h * PI).sin()).collect();
685        let solution = arr1(&solution);
686        let rhs = arr1(&rhs);
687        let solver = cg(&mat, &rhs, 1e-10, 1e-8).unwrap();
688        let e = (solution - solver.solution()).norm_l2();
689        assert!(e < 1e-4);
690    }
691
692    #[test]
693    #[cfg(feature = "nalgebra")]
694    fn test_cg_sparse() {
695        let n = 1024;
696        let h = 1.0 / (n as f64);
697        let a = vec![2.0 / (h * h); n - 1];
698        let b = vec![-1.0 / (h * h); n - 2];
699        let mat = symmetric_tridiagonal_csc(&a, &b).unwrap();
700        let rhs: Vec<_> = (1..n)
701            .map(|i| PI * PI * (i as f64 * h * PI).sin())
702            .collect();
703        let solution: Vec<_> = (1..n).map(|i| (i as f64 * h * PI).sin()).collect();
704        let solution = Vector::from_vec(solution);
705        let rhs = Vector::from_vec(rhs);
706        let solver = cg(&mat, &rhs, 1e-10, 1e-8).unwrap();
707        let e = (solution - solver.solution()).norm();
708        assert!(e < 1e-4);
709    }
710
711    #[test]
712    #[cfg(feature = "faer")]
713    fn test_cg_sparse() {
714        let n = 1024;
715        let h = 1.0 / (n as f64);
716        let a = vec![2.0 / (h * h); n - 1];
717        let b = vec![-1.0 / (h * h); n - 2];
718        let mat = symmetric_tridiagonal_csc(&a, &b).unwrap();
719        let rhs: Vec<_> = (1..n)
720            .map(|i| PI * PI * (i as f64 * h * PI).sin())
721            .collect();
722        let solution: Vec<_> = (1..n).map(|i| (i as f64 * h * PI).sin()).collect();
723        let solution = faer::Mat::from_fn(n - 1, 1, |i, _| solution[i]);
724        let rhs = faer::Mat::from_fn(n - 1, 1, |i, _| rhs[i]);
725        let solver = cg(&mat, &rhs, 1e-10, 1e-8).unwrap();
726        let e = (solution - solver.solution()).norm_l2();
727        assert!(e < 1e-4);
728    }
729
730    #[test]
731    #[cfg(feature = "ndarray")]
732    fn test_cg_sparse() {
733        use ndarray::arr1;
734        use ndarray_linalg::Norm;
735
736        let n = 1024;
737        let h = 1.0 / (n as f64);
738        let a = vec![2.0 / (h * h); n - 1];
739        let b = vec![-1.0 / (h * h); n - 2];
740        let mat = symmetric_tridiagonal_csc(&a, &b).unwrap();
741        let rhs: Vec<_> = (1..n)
742            .map(|i| PI * PI * (i as f64 * h * PI).sin())
743            .collect();
744        let solution: Vec<_> = (1..n).map(|i| (i as f64 * h * PI).sin()).collect();
745        let solution = arr1(&solution);
746        let rhs = arr1(&rhs);
747        let solver = cg(&mat, &rhs, 1e-10, 1e-8).unwrap();
748        let e = (solution - solver.solution()).norm_l2();
749        assert!(e < 1e-4);
750    }
751}