include/pops/numerics/elliptic/linear/generic_krylov.hpp Source File

adc_cpp: include/pops/numerics/elliptic/linear/generic_krylov.hpp Source File
adc_cpp 0.3.0
Model-free C++23 core for coupled hyperbolic-elliptic systems on adaptive (AMR) meshes, with MPI and GPU (Kokkos) backends
generic_krylov.hpp
Go to the documentation of this file.
1#pragma once
2
48
52#include <pops/numerics/elliptic/linear/krylov_result.hpp> // pops::KrylovResult (shared)
53
54#include <cmath>
55#include <functional>
56#include <stdexcept>
57#include <vector>
58
59namespace pops {
60
61// KrylovResult is shared via krylov_result.hpp (included above), so this header and the
62// GeometricMG-coupled krylov_solver.hpp never carry hand-synchronised copies of the struct.
63
67using ApplyFn = std::function<void(MultiFab& out, const MultiFab& in)>;
68
69namespace detail {
70
76inline Real krylov_dot(const MultiFab& x, const MultiFab& y) {
77 return x.ncomp() > 1 ? dot_all(x, y) : dot(x, y);
78}
79
82inline Real krylov_l2_norm(const MultiFab& x) {
83 return std::sqrt(krylov_dot(x, x));
84}
85
87inline void require_max_iter(int max_iters) {
88 if (max_iters <= 0)
89 throw std::invalid_argument("dynamic solver loops require max_iter");
90}
91
93inline constexpr Real kKrylovTiny = Real(1e-300);
94
95} // namespace detail
96
108inline KrylovResult richardson_solve(const ApplyFn& A, MultiFab& phi, const MultiFab& rhs,
109 Real omega, Real rel_tol, int max_iters) {
110 detail::require_max_iter(max_iters);
111 // Scratch allocated ONCE, co-distributed with phi; reused every iteration (no in-loop alloc).
112 MultiFab r(phi.box_array(), phi.dmap(), phi.ncomp(), phi.n_grow());
113
114 const Real bnorm = detail::krylov_l2_norm(rhs); // COLLECTIVE
115 const Real norm0 = bnorm > Real(0) ? bnorm : Real(1); // zero rhs -> absolute base
116
117 A(r, phi); // r = A(phi)
118 lincomb(r, Real(1), rhs, Real(-1), r); // r = b - A(phi)
119 Real rnorm = detail::krylov_l2_norm(r); // COLLECTIVE
120 KrylovResult res;
121 res.rel_residual = rnorm / norm0;
122 if (rnorm <= rel_tol * norm0) { // already converged (warm start)
123 res.converged = true;
124 return res;
125 }
126
127 for (int k = 1; k <= max_iters; ++k) {
128 saxpy(phi, omega, r); // phi <- phi + omega r
129 A(r, phi); // r = A(phi)
130 lincomb(r, Real(1), rhs, Real(-1), r); // r = b - A(phi)
131 rnorm = detail::krylov_l2_norm(r); // COLLECTIVE (all ranks, same value)
132 res.iters = k;
133 res.rel_residual = rnorm / norm0;
134 if (rnorm <= rel_tol * norm0) {
135 res.converged = true;
136 return res;
137 }
138 }
139 return res; // max_iters reached: best effort (converged=false)
140}
141
155inline KrylovResult cg_solve(const ApplyFn& A, MultiFab& phi, const MultiFab& rhs, Real rel_tol,
156 int max_iters) {
157 detail::require_max_iter(max_iters);
158 // Scratch allocated ONCE, co-distributed with phi; reused every iteration. p carries phi's ghost
159 // width because A(Ap, p) may read p's ghosts; r and Ap need none but share the layout for clarity.
160 MultiFab r(phi.box_array(), phi.dmap(), phi.ncomp(), phi.n_grow());
161 MultiFab p(phi.box_array(), phi.dmap(), phi.ncomp(), phi.n_grow());
162 MultiFab Ap(phi.box_array(), phi.dmap(), phi.ncomp(), phi.n_grow());
163
164 const Real bnorm = detail::krylov_l2_norm(rhs); // COLLECTIVE
165 const Real norm0 = bnorm > Real(0) ? bnorm : Real(1);
166
167 A(r, phi); // r = A(phi)
168 lincomb(r, Real(1), rhs, Real(-1), r); // r = b - A(phi)
169 Real rnorm = detail::krylov_l2_norm(r); // COLLECTIVE
170 KrylovResult res;
171 res.rel_residual = rnorm / norm0;
172 if (rnorm <= rel_tol * norm0) { // already converged (warm start)
173 res.converged = true;
174 return res;
175 }
176
177 lincomb(p, Real(1), r, Real(0), r); // p_0 = r_0 (copy via lincomb)
178 Real rs_old = detail::krylov_dot(r, r); // COLLECTIVE: ||r||^2 (all components if ncomp>1)
179
180 for (int k = 1; k <= max_iters; ++k) {
181 A(Ap, p); // Ap = A(p)
182 const Real pAp = detail::krylov_dot(p, Ap); // COLLECTIVE (all components if ncomp>1)
183 if (std::fabs(pAp) < detail::kKrylovTiny) { // breakdown (A not SPD / null direction)
184 res.iters = k - 1;
185 res.rel_residual = rnorm / norm0;
186 return res;
187 }
188 const Real alpha = rs_old / pAp;
189 saxpy(phi, alpha, p); // phi <- phi + alpha p
190 saxpy(r, -alpha, Ap); // r <- r - alpha Ap
191 const Real rs_new = detail::krylov_dot(r, r); // COLLECTIVE: new ||r||^2 (all components)
192 rnorm = std::sqrt(rs_new);
193 res.iters = k;
194 res.rel_residual = rnorm / norm0;
195 if (rnorm <= rel_tol * norm0) {
196 res.converged = true;
197 return res;
198 }
199 const Real beta = rs_new / rs_old;
200 lincomb(p, Real(1), r, beta, p); // p <- r + beta p
201 rs_old = rs_new;
202 }
203 return res; // max_iters reached: best effort (converged=false)
204}
205
223inline KrylovResult bicgstab_solve(const ApplyFn& A, const ApplyFn& precond, MultiFab& phi,
224 const MultiFab& rhs, Real rel_tol, int max_iters) {
225 detail::require_max_iter(max_iters);
226 const bool has_precond = static_cast<bool>(precond);
227
228 // Scratch allocated ONCE, co-distributed with phi; reused every iteration (mirror of
229 // TensorKrylovSolver's fixed fields). The matvec inputs (p / s, or phat / shat when preconditioned)
230 // carry phi's ghost width since A may read their ghosts; the pointwise scratch (r, rhat, v, t)
231 // shares the layout. phat/shat are only allocated when a preconditioner is supplied.
232 const BoxArray& ba = phi.box_array();
233 const DistributionMapping& dm = phi.dmap();
234 const int nc = phi.ncomp();
235 const int ng = phi.n_grow();
236 MultiFab r(ba, dm, nc, ng), rhat(ba, dm, nc, ng), p(ba, dm, nc, ng);
237 MultiFab v(ba, dm, nc, ng), s(ba, dm, nc, ng), t(ba, dm, nc, ng);
238 MultiFab phat, shat;
239 if (has_precond) {
240 phat = MultiFab(ba, dm, nc, ng);
241 shat = MultiFab(ba, dm, nc, ng);
242 }
243
244 const Real bnorm = detail::krylov_l2_norm(rhs); // COLLECTIVE
245 const Real norm0 = bnorm > Real(0) ? bnorm : Real(1);
246
247 A(v, phi); // v = A(phi) (scratch reuse for r0)
248 lincomb(r, Real(1), rhs, Real(-1), v); // r = b - A(phi)
249 Real rnorm = detail::krylov_l2_norm(r); // COLLECTIVE
250 KrylovResult res;
251 res.rel_residual = rnorm / norm0;
252 if (rnorm <= rel_tol * norm0) { // already converged (warm start)
253 res.converged = true;
254 return res;
255 }
256
257 lincomb(rhat, Real(1), r, Real(0), r); // rhat = frozen r0 (shadow vector)
258 p.set_val(Real(0));
259 v.set_val(Real(0));
260 Real rho_prev = Real(1), alpha = Real(1), omega = Real(1);
261
262 for (int k = 1; k <= max_iters; ++k) {
263 const Real rho = detail::krylov_dot(rhat, r); // COLLECTIVE (all components if ncomp>1)
264 if (std::fabs(rho) < detail::kKrylovTiny || std::fabs(omega) < detail::kKrylovTiny) {
265 res.iters = k - 1; // breakdown: best effort
266 res.rel_residual = rnorm / norm0;
267 return res;
268 }
269 const Real beta = (rho / rho_prev) * (alpha / omega);
270 lincomb(p, Real(1), p, -omega, v); // p <- p - omega v
271 lincomb(p, beta, p, Real(1), r); // p <- r + beta p
272 MultiFab& phat_ref = has_precond ? phat : p; // identity precond: phat = p
273 if (has_precond)
274 precond(phat, p); // phat = M^{-1} p
275 A(v, phat_ref); // v = A(phat)
276 const Real rhat_dot_v = detail::krylov_dot(rhat, v); // COLLECTIVE (all components if ncomp>1)
277 if (std::fabs(rhat_dot_v) < detail::kKrylovTiny) {
278 res.iters = k - 1;
279 res.rel_residual = rnorm / norm0;
280 return res;
281 }
282 alpha = rho / rhat_dot_v;
283 lincomb(s, Real(1), r, -alpha, v); // s <- r - alpha v
284 saxpy(phi, alpha, phat_ref); // phi <- phi + alpha phat (partial)
285 const Real snorm = detail::krylov_l2_norm(s); // COLLECTIVE
286 if (snorm <= rel_tol * norm0) { // mid-iteration convergence
287 rnorm = snorm;
288 res.iters = k;
289 res.rel_residual = rnorm / norm0;
290 res.converged = true;
291 return res;
292 }
293 MultiFab& shat_ref = has_precond ? shat : s; // identity precond: shat = s
294 if (has_precond)
295 precond(shat, s); // shat = M^{-1} s
296 A(t, shat_ref); // t = A(shat)
297 const Real tt = detail::krylov_dot(t, t); // COLLECTIVE (all components if ncomp>1)
298 omega = tt > detail::kKrylovTiny ? detail::krylov_dot(t, s) / tt : Real(0); // COLLECTIVE
299 saxpy(phi, omega, shat_ref); // phi <- phi + omega shat
300 lincomb(r, Real(1), s, -omega, t); // r <- s - omega t
301 rnorm = detail::krylov_l2_norm(r); // COLLECTIVE
302 res.iters = k;
303 res.rel_residual = rnorm / norm0;
304 if (rnorm <= rel_tol * norm0) {
305 res.converged = true;
306 return res;
307 }
308 rho_prev = rho;
309 }
310 return res; // max_iters reached: best effort (converged=false)
311}
312
344inline KrylovResult gmres_solve(const ApplyFn& A, const ApplyFn& precond, MultiFab& phi,
345 const MultiFab& rhs, Real rel_tol, int max_iters,
346 int restart = 30) {
347 detail::require_max_iter(max_iters);
348 constexpr int kGmresRestartMax = 50; // caps the stack Hessenberg system (restart+1 entries)
349 const int m = restart < 1 ? 1 : (restart > kGmresRestartMax ? kGmresRestartMax : restart);
350 const bool has_precond = static_cast<bool>(precond);
351
352 const BoxArray& ba = phi.box_array();
353 const DistributionMapping& dm = phi.dmap();
354 const int nc = phi.ncomp();
355 const int ng = phi.n_grow();
356
357 // Scratch allocated ONCE, co-distributed with phi; reused across every restart cycle. The Arnoldi
358 // basis V holds m+1 vectors; w is the matvec / MGS workspace (carries phi's ghost width since A reads
359 // its ghosts); r is the (preconditioned) residual; Mb is M^{-1} b for the relative-residual base.
360 std::vector<MultiFab> V;
361 V.reserve(static_cast<std::size_t>(m) + 1);
362 for (int i = 0; i <= m; ++i)
363 V.emplace_back(ba, dm, nc, ng);
364 MultiFab w(ba, dm, nc, ng);
365 MultiFab r(ba, dm, nc, ng);
366 MultiFab Mb;
367 if (has_precond)
368 Mb = MultiFab(ba, dm, nc, ng);
369
370 // Preconditioned right-hand-side norm: the left-preconditioned residual is M^{-1}(b - A x), so its
371 // base is ||M^{-1} b||. With the identity Mb aliases rhs and this is the true ||b||, matching CG.
372 if (has_precond)
373 precond(Mb, rhs);
374 const MultiFab& Mb_ref = has_precond ? Mb : rhs;
375 const Real bnorm = detail::krylov_l2_norm(Mb_ref); // COLLECTIVE
376 const Real norm0 = bnorm > Real(0) ? bnorm : Real(1);
377
378 // Fixed-size stack Hessenberg least-squares: H column-major (m columns of m+1 rows), the Givens
379 // rotation cosines/sines, the rotated rhs g (beta e1 after rotations), and the back-substituted y.
380 // restart <= kGmresRestartMax keeps every array bounded; no heap, no Eigen / LAPACK.
381 Real H[kGmresRestartMax + 1][kGmresRestartMax];
382 Real cs[kGmresRestartMax];
383 Real sn[kGmresRestartMax];
384 Real g[kGmresRestartMax + 1];
385 Real y[kGmresRestartMax];
386
387 KrylovResult res;
388 int total_iters = 0;
389
390 while (total_iters < max_iters) {
391 // r = M^{-1}(b - A phi): residual of the current iterate, then left-preconditioned.
392 A(w, phi); // w = A(phi)
393 lincomb(w, Real(1), rhs, Real(-1), w); // w = b - A(phi)
394 if (has_precond)
395 precond(r, w); // r = M^{-1}(b - A phi)
396 else
397 lincomb(r, Real(1), w, Real(0), w); // r = b - A phi (copy via lincomb)
398 Real beta = detail::krylov_l2_norm(r); // COLLECTIVE
399 res.rel_residual = beta / norm0;
400 if (beta <= rel_tol * norm0) { // converged (also the warm-start early-out)
401 res.converged = true;
402 return res;
403 }
404
405 lincomb(V[0], Real(1) / beta, r, Real(0), r); // v_0 = r / ||r||
406 g[0] = beta;
407 for (int i = 1; i <= m; ++i)
408 g[i] = Real(0);
409
410 int k = 0; // Krylov steps taken this cycle
411 for (; k < m && total_iters < max_iters; ++k) {
412 ++total_iters;
413 // Arnoldi: w = M^{-1} A v_k, orthogonalised against v_0..v_k by MODIFIED Gram-Schmidt.
414 if (has_precond) {
415 A(r, V[k]); // r = A v_k (reuse r as the unpreconditioned matvec scratch)
416 precond(w, r); // w = M^{-1} A v_k
417 } else {
418 A(w, V[k]); // w = A v_k
419 }
420 for (int i = 0; i <= k; ++i) {
421 H[i][k] = detail::krylov_dot(w, V[i]); // COLLECTIVE (all components if ncomp>1)
422 saxpy(w, -H[i][k], V[i]); // w <- w - H[i][k] v_i
423 }
424 H[k + 1][k] = detail::krylov_l2_norm(w); // COLLECTIVE
425 if (H[k + 1][k] > detail::kKrylovTiny)
426 lincomb(V[k + 1], Real(1) / H[k + 1][k], w, Real(0), w); // v_{k+1} = w / h
427 // else: lucky breakdown (the exact solution lies in the current subspace); v_{k+1} stays unused.
428
429 // Apply the previous Givens rotations to the new Hessenberg column, then a fresh rotation to
430 // annihilate H[k+1][k]; carry the rotation through g (the rotated beta e1).
431 for (int i = 0; i < k; ++i) {
432 const Real t = cs[i] * H[i][k] + sn[i] * H[i + 1][k];
433 H[i + 1][k] = -sn[i] * H[i][k] + cs[i] * H[i + 1][k];
434 H[i][k] = t;
435 }
436 const Real denom = std::sqrt(H[k][k] * H[k][k] + H[k + 1][k] * H[k + 1][k]);
437 if (denom > detail::kKrylovTiny) {
438 cs[k] = H[k][k] / denom;
439 sn[k] = H[k + 1][k] / denom;
440 } else { // degenerate column: identity rotation (no division by ~0)
441 cs[k] = Real(1);
442 sn[k] = Real(0);
443 }
444 H[k][k] = cs[k] * H[k][k] + sn[k] * H[k + 1][k];
445 H[k + 1][k] = Real(0);
446 const Real g_next = -sn[k] * g[k];
447 g[k] = cs[k] * g[k];
448 g[k + 1] = g_next;
449
450 const Real resid = std::fabs(g[k + 1]); // ||M^{-1}(b - A x)|| WITHOUT a matvec
451 res.iters = total_iters;
452 res.rel_residual = resid / norm0;
453 if (resid <= rel_tol * norm0) {
454 ++k; // include this step in the back-substitution
455 break;
456 }
457 }
458
459 // Back-substitution: solve the k x k upper-triangular R y = g, then phi <- phi + sum y_i v_i.
460 for (int i = k - 1; i >= 0; --i) {
461 Real sum = g[i];
462 for (int j = i + 1; j < k; ++j)
463 sum -= H[i][j] * y[j];
464 y[i] = std::fabs(H[i][i]) > detail::kKrylovTiny ? sum / H[i][i] : Real(0);
465 }
466 for (int i = 0; i < k; ++i)
467 saxpy(phi, y[i], V[i]); // phi <- phi + y_i v_i
468
469 // True residual of the updated iterate decides convergence / continuation (the rotated estimate
470 // g[k] guides the inner cycle; this recompute keeps the stopping test honest across restarts).
471 A(w, phi); // w = A(phi)
472 lincomb(w, Real(1), rhs, Real(-1), w); // w = b - A(phi)
473 if (has_precond)
474 precond(r, w);
475 else
476 lincomb(r, Real(1), w, Real(0), w);
477 const Real rnorm = detail::krylov_l2_norm(r); // COLLECTIVE
478 res.rel_residual = rnorm / norm0;
479 if (rnorm <= rel_tol * norm0) {
480 res.converged = true;
481 return res;
482 }
483 }
484 return res; // max_iters reached: best effort (converged=false)
485}
486
487} // namespace pops
Ordered list of boxes tiling a level.
Definition box_array.hpp:22
Owning MPI rank of each box, indexed by GLOBAL box index (parallel to a BoxArray).
Definition distribution_mapping.hpp:19
Field distributed over a level: decomposition (BoxArray) + distribution (DistributionMapping) + ncomp...
Definition multifab.hpp:33
int ncomp() const
Number of components.
Definition multifab.hpp:60
const DistributionMapping & dmap() const
GLOBAL distribution (owner rank per box).
Definition multifab.hpp:58
int n_grow() const
Number of ghost layers.
Definition multifab.hpp:62
const BoxArray & box_array() const
GLOBAL decomposition of the level (all boxes, all ranks).
Definition multifab.hpp:56
void set_val(Real v)
Fills all cells (valid + ghosts) of every local fab with v.
Definition multifab.hpp:85
KrylovResult – the shared result type of every Krylov solve in include/pops/numerics/elliptic/linear.
MultiFab arithmetic (saxpy, lincomb, norm_inf, dot) over VALID cells.
MultiFab: a field DISTRIBUTED over a level (equivalent of AMReX's MultiFab).
constexpr Real kKrylovTiny
Tiny breakdown guard for the BiCGStab scalar recurrences (division by ~0).
Definition generic_krylov.hpp:93
Real krylov_l2_norm(const MultiFab &x)
GLOBAL L2 norm sqrt(sum x.x), collective (all_reduce_sum).
Definition generic_krylov.hpp:82
Real krylov_dot(const MultiFab &x, const MultiFab &y)
Krylov inner product x.y, COLLECTIVE (all_reduce_sum).
Definition generic_krylov.hpp:76
void require_max_iter(int max_iters)
Guards a dynamic solver loop: a non-positive iteration budget is a configuration error.
Definition generic_krylov.hpp:87
Definition amr_hierarchy.hpp:29
KrylovResult gmres_solve(const ApplyFn &A, const ApplyFn &precond, MultiFab &phi, const MultiFab &rhs, Real rel_tol, int max_iters, int restart=30)
Left-preconditioned restarted GMRES(m), solving A x = b for a GENERAL (possibly NON-symmetric) operat...
Definition generic_krylov.hpp:344
Real dot_all(const MultiFab &x, const MultiFab &y)
FULL-component dot Sum_{cells, c} x(.,.,c) * y(.,.,c) over ALL components, reduced over ALL ranks (al...
Definition mf_arith.hpp:182
double Real
Definition types.hpp:30
KrylovResult cg_solve(const ApplyFn &A, MultiFab &phi, const MultiFab &rhs, Real rel_tol, int max_iters)
Conjugate Gradient, solving A x = b for an SPD operator A.
Definition generic_krylov.hpp:155
Real dot(const MultiFab &x, const MultiFab &y, int comp=0)
Dot product Sum_cells x.y over component comp, reduced over ALL ranks (all_reduce).
Definition mf_arith.hpp:163
KrylovResult richardson_solve(const ApplyFn &A, MultiFab &phi, const MultiFab &rhs, Real omega, Real rel_tol, int max_iters)
Richardson iteration x <- x + omega (b - A x), solving A x = b.
Definition generic_krylov.hpp:108
void saxpy(MultiFab &y, Real a, const MultiFab &x)
y <- y + a x over ALL components of the valid cells. Identical layouts required.
Definition mf_arith.hpp:102
Real sum(const MultiFab &mf, int comp=0)
Sum of the VALID cells of component comp, reduced over ALL ranks (all_reduce).
Definition multifab.hpp:121
void lincomb(MultiFab &z, Real a, const MultiFab &x, Real b, const MultiFab &y)
z <- a x + b y over ALL components of the valid cells. Identical layouts; aliasing safe.
Definition mf_arith.hpp:133
std::function< void(MultiFab &out, const MultiFab &in)> ApplyFn
Matrix-free operator callback: out <- A(in).
Definition generic_krylov.hpp:67
KrylovResult bicgstab_solve(const ApplyFn &A, const ApplyFn &precond, MultiFab &phi, const MultiFab &rhs, Real rel_tol, int max_iters)
Preconditioned BiCGStab, solving A x = b for a general (possibly non-symmetric) operator A.
Definition generic_krylov.hpp:223
Outcome of a Krylov solve: iterations performed, final relative residual, convergence flag.
Definition krylov_result.hpp:16
int iters
number of iterations performed
Definition krylov_result.hpp:17
Real rel_residual
||r_final|| / ||b|| (global L2 norm; base 1 when ||b|| == 0)
Definition krylov_result.hpp:18
bool converged
true if ||r|| <= rel_tol * ||b|| was reached
Definition krylov_result.hpp:19
Base scalar types and the POPS_HD macro (host+device portability).