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

adc_cpp: include/pops/numerics/elliptic/linear/krylov_solver.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
krylov_solver.hpp
Go to the documentation of this file.
1#pragma once
2
25
37
38#include <cassert>
39#include <cmath>
40
41namespace pops {
42
43// KrylovResult is defined in krylov_result.hpp (included above), shared with generic_krylov.hpp.
44
45// Matrix-free BiCGStab, preconditioned by N V-cycles of GeometricMG on the SYMMETRIC part.
46//
47// @p op: GeometricMG carrying the FULL operator (configured via set_cross_terms / set_epsilon*
48// / set_reaction as appropriate). Serves (a) as storage for the fine-level phi/rhs fields
49// (phi()/rhs()/geom()), and (b) as the source of the operator coefficients for the matvec
50// (op_eps(), op_a_xy(), ...). This is the object that models the EllipticSolver concept.
51// @p precond: GeometricMG carrying the SYMMETRIC part (same eps/eps_y/kappa, BUT set_cross_terms
52// NOT called -> diagonal block). Its phi()/rhs() serve as a preconditioning buffer.
53// MUST be an object DISTINCT from @p op: apply_precond OVERWRITES precond_.rhs() (copy_into)
54// and precond_.phi() (set_val(0) then V-cycle) at every iteration. If precond_ == op_, these
55// writes would overwrite the iterate phi() and the real rhs() of BiCGStab, destroying the solve.
56// The constructor enforces this via assert(&op != &precond). A SEPARATE object without cross
57// terms is in any case the proper symmetric preconditioner.
59 public:
60 // @p n_precond_vcycles: number N of MG V-cycles per preconditioner application (1 or 2).
61 TensorKrylovSolver(GeometricMG& op, GeometricMG& precond, int n_precond_vcycles = 1)
62 : op_(op),
63 precond_(precond),
64 n_precond_(n_precond_vcycles),
65 ba_(op.box_array()),
66 dm_(op.dmap()),
67 r_(ba_, dm_, 1, 0),
68 rhat_(ba_, dm_, 1, 0),
69 p_(ba_, dm_, 1, 0),
70 v_(ba_, dm_, 1, 0),
71 s_(ba_, dm_, 1, 0),
72 t_(ba_, dm_, 1, 0),
73 phat_(ba_, dm_, 1, 1),
74 shat_(ba_, dm_, 1, 1),
75 op_offset_(ba_, dm_, 1, 0),
76 bc_offset_(ba_, dm_, 1, 0) {
77 // op_ and precond_ MUST be distinct: apply_precond overwrites precond_.rhs()/phi() at every
78 // iteration; confusing them with op_ would overwrite the iterate and the right-hand side of the solve (see header).
79 assert(&op_ != &precond_ && "TensorKrylovSolver: op and precond must be distinct objects");
80 }
81
82 // --- EllipticSolver concept ---
83 MultiFab& phi() { return op_.phi(); }
84 MultiFab& rhs() { return op_.rhs(); }
85 const Geometry& geom() const { return op_.geom(); }
86 // current GLOBAL L2 residual ||rhs - L_int(phi)|| (collective). L2 counterpart of GeometricMG's norm_inf.
88 apply_operator(phi(), r_); // r_ = L_int(phi)
89 lincomb(r_, Real(1), rhs(), Real(-1), r_); // r_ = rhs - L_int(phi)
90 return l2_norm(r_);
91 }
92 void solve() { solve(Real(1e-10), 200); }
93
94 // Preconditioned BiCGStab. phi() is the unknown (warm start: incoming value = starting point);
95 // rhs() the right-hand side. Returns iterations + relative residual + convergence.
96 KrylovResult solve(Real rel_tol, int max_iters) {
97 prepare_solve(); // compute (once) the inhomogeneous Dirichlet BC offsets (matvec + precond)
98 // r0 = rhs - L_int(phi) (true INHOMOGENEOUS residual, warm start respected). Here we KEEP the AFFINE
99 // operator: it folds the Dirichlet data into the residual, exactly what we want for r0. The
100 // equivalent linear system is L_lin x = rhs - c_bc (c_bc = apply_operator(0)); since
101 // r0 = rhs - apply_operator(phi) = (rhs - c_bc) - L_lin(phi), r0 is UNCHANGED. The IN-LOOP matvecs,
102 // by contrast, act on correction DIRECTIONS and must be LINEAR (apply_operator_lin).
103 apply_operator(phi(), v_); // v_ = L_int(phi)
104 lincomb(r_, Real(1), rhs(), Real(-1), v_); // r_ = rhs - L_int(phi)
105 const Real bnorm = l2_norm(rhs());
106 const Real norm0 = bnorm > Real(0) ? bnorm : Real(1); // relative base (zero rhs -> absolute)
107 Real rnorm = l2_norm(r_);
108 KrylovResult res;
109 res.rel_residual = rnorm / norm0;
110 if (rnorm <= rel_tol * norm0) {
111 res.converged = true;
112 return res;
113 } // already converged
114
115 // rhat = frozen r0 (BiCGStab shadow vector); p, v <- 0.
116 copy_into(rhat_, r_);
117 p_.set_val(Real(0));
118 v_.set_val(Real(0));
119 Real rho_prev = Real(1), alpha = Real(1), omega = Real(1);
120
121 for (int k = 1; k <= max_iters; ++k) {
122 const Real rho = dot(rhat_, r_); // COLLECTIVE (all ranks)
123 // guard: BiCGStab breakdown (rho or omega ~ 0). We return the current best effort.
124 if (std::fabs(rho) < kTiny || std::fabs(omega) < kTiny) {
125 res.iters = k - 1;
126 res.rel_residual = rnorm / norm0;
127 return res;
128 }
129 const Real beta = (rho / rho_prev) * (alpha / omega);
130 // p <- r + beta (p - omega v)
131 lincomb(p_, Real(1), p_, -omega, v_); // p <- p - omega v
132 lincomb(p_, beta, p_, Real(1), r_); // p <- r + beta p
133 // phat = M^{-1} p (N MG V-cycles on the symmetric part)
134 apply_precond(p_, phat_);
135 apply_operator_lin(phat_, v_); // v = L_lin(phat) (LINEAR matvec: phat is a direction)
136 const Real rhat_dot_v = dot(rhat_, v_); // COLLECTIVE
137 if (std::fabs(rhat_dot_v) < kTiny) {
138 res.iters = k - 1;
139 res.rel_residual = rnorm / norm0;
140 return res;
141 }
142 alpha = rho / rhat_dot_v;
143 // s <- r - alpha v
144 lincomb(s_, Real(1), r_, -alpha, v_);
145 // phi <- phi + alpha phat (partial correction; buffer before the test on ||s||)
146 saxpy(phi(), alpha, phat_);
147 const Real snorm = l2_norm(s_);
148 if (snorm <= rel_tol * norm0) { // convergence at mid-iteration
149 rnorm = snorm;
150 res.iters = k;
151 res.rel_residual = rnorm / norm0;
152 res.converged = true;
153 return res;
154 }
155 // shat = M^{-1} s; t = L_lin(shat) (LINEAR matvec: shat is a correction direction)
156 apply_precond(s_, shat_);
157 apply_operator_lin(shat_, t_);
158 const Real tt = dot(t_, t_); // COLLECTIVE
159 omega = tt > kTiny ? dot(t_, s_) / tt : Real(0);
160 // phi <- phi + omega shat; r <- s - omega t
161 saxpy(phi(), omega, shat_);
162 lincomb(r_, Real(1), s_, -omega, t_);
163 rnorm = l2_norm(r_);
164 res.iters = k;
165 res.rel_residual = rnorm / norm0;
166 if (rnorm <= rel_tol * norm0) {
167 res.converged = true;
168 return res;
169 }
170 rho_prev = rho;
171 }
172 return res; // max_iters reached without convergence: best effort (converged=false)
173 }
174
175 private:
176 static constexpr Real kTiny = Real(1e-300); // guard against breakdown / division by 0
177
178 // INHOMOGENEOUS MATRIX-FREE matvec: out = L_int(in) = div(A grad in) - kappa in, ghosts of in filled
179 // with op_.bc() (the FULL BC). Reuses the coefficients of op_'s FULL operator (same pointers as
180 // current_residual). AFFINE in in when bcPhi carries a nonzero Dirichlet value: the boundary ghost
181 // equals 2 v - in_interior, so the stencil of boundary cells receives a CONSTANT term c_bc =
182 // apply_operator(0). We use it ONLY for the true residual r0 / residual() (where that constant term
183 // is exactly the Dirichlet data folded into the residual, which is what we want). The IN-LOOP matvecs
184 // (on correction directions) go through apply_operator_lin (LINEAR operator).
185 void apply_operator(MultiFab& in, MultiFab& out) {
186 device_fence(); // a kernel may have written in; wait before the host read of fill_ghosts
187 fill_ghosts(in, op_.geom().domain, op_.bc());
188 apply_laplacian(in, op_.geom(), out, op_.op_coef(), op_.op_eps(), op_.op_kappa(),
189 op_.op_eps_y(), op_.op_a_xy(), op_.op_a_yx());
190 // conductor cells (mask==0): L_int is 0 there (Dirichlet phi=0), like poisson_residual.
191 if (const MultiFab* mk = op_.op_mask())
192 mask_zero(out, *mk);
193 }
194
195 // LINEAR MATRIX-FREE matvec: out = L_lin(in) = apply_operator(in) - c_bc, with c_bc =
196 // apply_operator(0) the inhomogeneous boundary part (constant). BiCGStab applies the matvec to correction
197 // DIRECTIONS (phat = M^{-1} p, shat = M^{-1} s), NOT to the iterate; the operator must be linear there,
198 // otherwise the constant term c_bc injected at each matvec breaks the BiCGStab relations (residual that
199 // oscillates / diverges). We therefore subtract c_bc, just as we subtract d_bc in the preconditioner. Zero
200 // Dirichlet BC => has_op_offset_ stays false => apply_operator_lin == apply_operator, bit-identical.
201 void apply_operator_lin(MultiFab& in, MultiFab& out) {
202 apply_operator(in, out);
203 if (has_op_offset_)
204 lincomb(out, Real(1), out, Real(-1), op_offset_); // out <- L_lin(in)
205 }
206
207 // preconditioner M^{-1}: out = (N MG V-cycles on the symmetric part) applied to in, with HOMOGENEOUS
208 // BC (start out = 0, no warm start: M^{-1} is a LINEAR operator frozen for the BiCGStab iteration).
209 // precond_ does NOT carry the cross terms -> diagonal block.
210 //
211 // The V-cycle of precond_ (precond_.vcycle()) runs at level 0 with its FULL BC bc_. Now if bcPhi
212 // carries a NONZERO Dirichlet value (xlo_val/... != 0), fill_physical_bc fills the ghost by
213 // ghost = 2 v - interior: starting from phi=0, the first pass injects a FIXED source term ~2 v,
214 // INDEPENDENT of in. The raw V-cycle is therefore AFFINE: precond_raw(in) = M^{-1} in + d_bc, with
215 // d_bc = precond_raw(0) (constant offset, function of the BC and coefficients alone, NOT of in).
216 // This offset injected into phat/shat would make phi += alpha phat + omega shat drift by a spurious
217 // term alpha d_bc + omega d_bc at each iteration and would break convergence. We therefore SUBTRACT the offset:
218 // M^{-1} in = precond_raw(in) - precond_raw(0),
219 // which is EXACTLY the V-cycle with HOMOGENEOUS BC (the MG recursion is affine, its homogeneous part does
220 // not depend on v). Bit-identical to an internal vcycle_homogeneous(), without touching GeometricMG.
221 //
222 // Same reason and remedy as apply_operator_lin (linear matvec): the operator AND the preconditioner
223 // are AFFINE under nonzero Dirichlet BC, and BiCGStab applies them to correction DIRECTIONS;
224 // we linearize both by subtracting their respective offset (c_bc for the matvec, d_bc for the precond).
225 // Only the true residual r0 / residual() keeps the operator affine (the Dirichlet data is folded in there).
226 //
227 // d_bc is computed ONCE per solve (prepare_solve). Zero Dirichlet BC (xlo_val=... =0) =>
228 // has_bc_offset_ stays false => path STRICTLY UNCHANGED (no subtraction), bit-identical.
229 void apply_precond(MultiFab& in, MultiFab& out) {
230 precond_raw(in, out);
231 if (has_bc_offset_)
232 lincomb(out, Real(1), out, Real(-1), bc_offset_); // out <- M^{-1} in (homogeneous)
233 }
234
235 // RAW V-cycle of the preconditioner: out = (N MG V-cycles) applied to in, starting from phi=0. AFFINE in in
236 // when bcPhi carries a nonzero Dirichlet value (offset d_bc = precond_raw(0)).
237 void precond_raw(MultiFab& in, MultiFab& out) {
238 copy_into(precond_.rhs(), in);
239 precond_.phi().set_val(Real(0));
240 for (int i = 0; i < n_precond_; ++i)
241 precond_.vcycle();
242 copy_into(out, precond_.phi());
243 }
244
245 // Prepare the inhomogeneous BC offsets, ONCE per solve: c_bc = apply_operator(0) for the matvec and
246 // d_bc = precond_raw(0) for the preconditioner. A BC without a nonzero Dirichlet value leaves both
247 // offsets at 0 (has_*_offset_ = false): apply_operator_lin and apply_precond revert to the historical
248 // path (no subtraction), STRICTLY bit-identical. We detect inhomogeneity on op_.bc()
249 // (matvec) and precond_.bc() (precond) separately. We use phat_ (1 ghost, required by fill_ghosts
250 // of apply_operator) as the NULL input; phat_ is overwritten at the 1st iteration (apply_precond(p_, phat_)).
251 void prepare_solve() {
252 auto inhomog = [](const BCRec& b) {
253 return b.xlo_val != Real(0) || b.xhi_val != Real(0) || b.ylo_val != Real(0) ||
254 b.yhi_val != Real(0);
255 };
256 has_op_offset_ = inhomog(op_.bc());
257 has_bc_offset_ = inhomog(precond_.bc());
258 if (has_op_offset_) {
259 phat_.set_val(Real(0)); // null input (1 ghost for fill_ghosts; phat_ overwritten afterward)
260 apply_operator(
261 phat_,
262 op_offset_); // op_offset_ <- apply_operator(0) = c_bc (inhomogeneous boundary part)
263 }
264 if (has_bc_offset_) {
265 phat_.set_val(Real(0));
266 precond_raw(phat_, bc_offset_); // bc_offset_ <- precond_raw(0) = d_bc
267 }
268 }
269
270 // GLOBAL L2 norm sqrt(sum x^2), collective (dot). Host sqrt, identical on all ranks.
271 Real l2_norm(const MultiFab& x) { return std::sqrt(dot(x, x)); }
272
273 // copy component 0 of the valid cells (src -> dst), named functor (device-clean).
274 void copy_into(MultiFab& dst, const MultiFab& src) {
275 for (int li = 0; li < dst.local_size(); ++li) {
276 Array4 d = dst.fab(li).array();
277 const ConstArray4 s = src.fab(li).const_array();
278 for_each_cell(dst.box(li), detail::CopyComp0Kernel{d, s});
279 }
280 }
281
282 // freeze out=0 on the conductor cells (mask==0), named functor (reuses ZeroConductorKernel).
283 void mask_zero(MultiFab& out, const MultiFab& mask) {
284 for (int li = 0; li < out.local_size(); ++li) {
285 Array4 o = out.fab(li).array();
286 const ConstArray4 m = mask.fab(li).const_array();
287 for_each_cell(out.box(li), detail::ZeroConductorKernel{o, m});
288 }
289 }
290
291 GeometricMG& op_;
292 GeometricMG& precond_;
293 int n_precond_;
294 BoxArray ba_;
295 DistributionMapping dm_;
296 MultiFab r_, rhat_, p_, v_, s_, t_; // 0 ghost: pointwise ops
297 MultiFab phat_, shat_; // 1 ghost: inputs of apply_operator (fill_ghosts)
298 MultiFab op_offset_; // c_bc = apply_operator(0): inhomogeneous boundary part of the matvec
299 MultiFab bc_offset_; // d_bc = precond_raw(0): Dirichlet BC offset of the precond
300 bool has_op_offset_ = false; // true if the operator BC carries a nonzero Dirichlet value
301 bool has_bc_offset_ = false; // true if the preconditioner BC carries a nonzero value
302};
303
304} // namespace pops
BoxArray: the set of boxes tiling a level (disjoint, covering).
Definition geometric_mg.hpp:79
void vcycle()
Definition geometric_mg.hpp:389
MultiFab & rhs()
Definition geometric_mg.hpp:222
const MultiFab * op_a_yx()
Definition geometric_mg.hpp:539
const MultiFab * op_coef()
Definition geometric_mg.hpp:534
MultiFab & phi()
Definition geometric_mg.hpp:221
const MultiFab * op_kappa()
Definition geometric_mg.hpp:536
const MultiFab * op_a_xy()
Definition geometric_mg.hpp:538
const MultiFab * op_mask()
Definition geometric_mg.hpp:533
const BCRec & bc() const
Definition geometric_mg.hpp:540
const Geometry & geom() const
Definition geometric_mg.hpp:223
const MultiFab * op_eps_y()
Definition geometric_mg.hpp:537
const MultiFab * op_eps()
Definition geometric_mg.hpp:535
Field distributed over a level: decomposition (BoxArray) + distribution (DistributionMapping) + ncomp...
Definition multifab.hpp:33
void set_val(Real v)
Fills all cells (valid + ghosts) of every local fab with v.
Definition multifab.hpp:85
Definition krylov_solver.hpp:58
TensorKrylovSolver(GeometricMG &op, GeometricMG &precond, int n_precond_vcycles=1)
Definition krylov_solver.hpp:61
void solve()
Definition krylov_solver.hpp:92
Real residual()
Definition krylov_solver.hpp:87
MultiFab & phi()
Definition krylov_solver.hpp:83
KrylovResult solve(Real rel_tol, int max_iters)
Definition krylov_solver.hpp:96
MultiFab & rhs()
Definition krylov_solver.hpp:84
const Geometry & geom() const
Definition krylov_solver.hpp:85
Parallel seam: minimal MPI abstraction (rank/size + collectives) with serial fallback.
DistributionMapping: maps each box (by global index) to its owning MPI rank.
GeometricMG: in-house geometric multigrid (V-cycle) for the elliptic operator, Gauss-Seidel smoother ...
Geometry: index-space (Box2D) <-> Cartesian physical-space mapping; PolarGeometry: SIBLING for a glob...
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).
Definition amr_hierarchy.hpp:29
void for_each_cell(const Box2D &b, F f)
Applies f to EACH cell (i, j) of box b (bounds inclusive), via Kokkos::parallel_for (Serial / OpenMP ...
Definition for_each.hpp:138
double Real
Definition types.hpp:30
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
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
void apply_laplacian(const MultiFab &phi, const Geometry &geom, MultiFab &lap, const MultiFab *coef=nullptr, const MultiFab *eps=nullptr, const MultiFab *kappa=nullptr, const MultiFab *eps_y=nullptr, const MultiFab *a_xy=nullptr, const MultiFab *a_yx=nullptr)
Definition poisson_operator.hpp:191
void device_fence()
Device barrier: waits for in-flight kernels to finish before a HOST access to unified memory.
Definition kokkos_env.hpp:43
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
void fill_ghosts(MultiFab &mf, const Box2D &domain, const BCRec &bc)
COMPLETE ghost filling: fill_boundary (interior + periodic, periodicity deduced from bc) THEN fill_ph...
Definition physical_bc.hpp:227
PHYSICAL boundary conditions at the domain edge (BCType, BCRec, fill_physical_bc, fill_ghosts).
Free functions of the elliptic operator: apply_laplacian (matvec), poisson_residual (residual),...
Cartesian geometry of a level: index domain + physical bounds [xlo, xhi] x [ylo, yhi].
Definition geometry.hpp:20
Box2D domain
Definition geometry.hpp:21
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).