02_iterative_krylov_solvers.cpp¶
CG, preconditioned CG, MINRES and GMRES on sparse and matrix-free operators.
Each solver states its requirement as a concept. CG and PCG need an SPD operator, MINRES needs a self-adjoint one, and GMRES accepts any linear operator. A law is attached to an operator with assume_spd or assume_symmetric, and an operator without the required law does not compile.
#include <cmath>
#include <cstdio>
#include <numerics.hpp>
#include <string_view>
#include <vector>
using namespace num;
namespace {
// Diffusion with a coefficient that grows along the domain, so the diagonal varies.
real coefficient(idx i) {
return 2.01 + (0.05 * static_cast<real>(i));
}
// Tridiagonal (lower, coefficient(i), upper) as a sparse matrix.
spmat tridiagonal(idx n, real lower, real upper) {
array<idx> rows, columns;
array<real> values;
for (idx i = 0; i < n; ++i) {
rows.push_back(i);
columns.push_back(i);
values.push_back(coefficient(i));
if (i > 0) {
rows.push_back(i);
columns.push_back(i - 1);
values.push_back(lower);
}
if (i + 1 < n) {
rows.push_back(i);
columns.push_back(i + 1);
values.push_back(upper);
}
}
return spmat::from_triplets(n, n, rows, columns, values);
}
void report(const char *name, const solver_result &r) {
std::printf("%-30s converged %d iterations %4zu residual %.2e\n", name, r.converged,
static_cast<std::size_t>(r.iterations), r.residual);
}
} // namespace
int main(int argc, char **argv) {
const bool plot = argc > 1 && std::string_view(argv[1]) == "--plot";
constexpr idx n = 400;
const vec<real> b(n, 1.0);
// A symmetric positive definite matrix with a varying diagonal.
const spmat A = tridiagonal(n, -1.0, -1.0);
const operators::sparse_op op(A);
vec<real> x(n, 0.0);
report("CG, sparse", cg(assume_spd(op), b, x, {.tolerance = 1e-10, .max_iterations = 2000}));
x = vec<real>(n, 0.0);
vec<real> inverse_diagonal(n);
for (idx i = 0; i < n; ++i) {
inverse_diagonal[i] = 1.0 / coefficient(i);
}
const jacobi_preconditioner jacobi(inverse_diagonal);
report("PCG, Jacobi",
pcg(assume_spd(op), jacobi, b, x, {.tolerance = 1e-10, .max_iterations = 2000}));
x = vec<real>(n, 0.0);
report("MINRES", minres(assume_symmetric(op), b, x, {.tolerance = 1e-10, .max_iterations = 2000}));
// A nonsymmetric matrix: convection-diffusion. Only GMRES applies.
const spmat N = tridiagonal(n, -1.4, -0.6);
vec<real> y(n, 0.0);
report("GMRES(30), nonsymmetric",
gmres(operators::sparse_op(N), b, y, {.tolerance = 1e-10, .max_iterations = 4000, .restart = 30}));
// A matrix-free operator: the same SPD matrix as a lambda.
const auto laplacian = operators::make_op(
[](const vec<real> &u, vec<real> &Lu) {
const idx m = u.size();
for (idx i = 0; i < m; ++i) {
Lu[i] = (coefficient(i) * u[i]) - (i > 0 ? u[i - 1] : 0.0) - (i + 1 < m ? u[i + 1] : 0.0);
}
},
n);
vec<real> u(n, 0.0);
report("CG, matrix-free", cg(assume_spd(laplacian), b, u, {.tolerance = 1e-10, .max_iterations = 2000}));
std::printf("max |x_sparse - x_matrix_free| = %.1e\n", [&] {
real worst = 0.0;
for (idx i = 0; i < n; ++i) {
worst = std::max(worst, std::abs(x[i] - u[i]));
}
return worst;
}());
if (plot) {
std::vector<double> grid, sol;
for (idx i = 0; i < n; ++i) {
grid.push_back(static_cast<double>(i));
sol.push_back(u[i]);
}
plt::plot(grid, sol, "x", "lines");
plt::title("02 CG solution");
plt::show();
}
}
Possible output:
CG, sparse converged 1 iterations 75 residual 6.06e-11
PCG, Jacobi converged 1 iterations 31 residual 6.11e-11
MINRES converged 1 iterations 74 residual 8.23e-11
GMRES(30), nonsymmetric converged 1 iterations 123 residual 9.70e-11
CG, matrix-free converged 1 iterations 75 residual 6.06e-11
max |x_sparse - x_matrix_free| = 5.5e-12