include/pops/numerics/elliptic/polar/polar_tensor_operator.hpp Source File

adc_cpp: include/pops/numerics/elliptic/polar/polar_tensor_operator.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
polar_tensor_operator.hpp
Go to the documentation of this file.
1#pragma once
2
8#include <pops/mesh/geometry/geometry.hpp> // PolarGeometry
13
14#include <cassert>
15#include <cmath>
16#include <concepts>
17#include <cstddef>
18#include <stdexcept>
19#include <vector>
20
125
126namespace pops {
127
128namespace detail {
129
135POPS_HD inline Real polar_radial_div(const ConstArray4& p, const ConstArray4& arr, bool hrt,
136 const ConstArray4& art, int i, int j, Real ri, Real rfm,
137 Real rfp, Real idr, Real idth) {
138 // Face a_rr coefficients (arithmetic mean of the adjacent centers).
139 const Real arr_p = Real(0.5) * (arr(i, j) + arr(i + 1, j));
140 const Real arr_m = Real(0.5) * (arr(i, j) + arr(i - 1, j));
141 // DIAGONAL radial term: (1/r_i) [ r_{i+1/2} a_rr_p (phi_{i+1}-phi_i)/dr - r_{i-1/2} a_rr_m (phi_i-phi_{i-1})/dr ] / dr.
142 Real out = (rfp * arr_p * (p(i + 1, j) - p(i, j)) - rfm * arr_m * (p(i, j) - p(i - 1, j))) *
143 (idr * idr / ri);
144 if (hrt) { // CROSS radial term a_rt (1/r_face) d_theta phi at face i+-1/2 (d_theta averaged over 4 corners).
145 const Real art_p = Real(0.5) * (art(i, j) + art(i + 1, j));
146 const Real art_m = Real(0.5) * (art(i, j) + art(i - 1, j));
147 // (d_theta phi)_face averaged over the 4 corners of the face: 1/(r_face) because grad_theta = (1/r) d_theta phi.
148 const Real dth_p =
149 (p(i, j + 1) + p(i + 1, j + 1) - p(i, j - 1) - p(i + 1, j - 1)) * (Real(0.25) * idth);
150 const Real dth_m =
151 (p(i - 1, j + 1) + p(i, j + 1) - p(i - 1, j - 1) - p(i, j - 1)) * (Real(0.25) * idth);
152 // (1/r_i)(1/dr) [ r_{i+1/2} F_cross_p - r_{i-1/2} F_cross_m ], F_cross = a_rt (1/r_face) d_theta phi.
153 // r_face * (1/r_face) = 1: the r_face metric cancels the 1/r_face of the azimuthal gradient -> factor 1.
154 out += (art_p * dth_p - art_m * dth_m) * (idr / ri);
155 }
156 return out;
157}
158
162POPS_HD inline Real polar_azimuthal_div(const ConstArray4& p, const ConstArray4& att, bool htr,
163 const ConstArray4& atr, int i, int j, Real ri, Real idr,
164 Real idth) {
165 const Real inv_r2 = Real(1) / (ri * ri);
166 const Real att_p = Real(0.5) * (att(i, j) + att(i, j + 1));
167 const Real att_m = Real(0.5) * (att(i, j) + att(i, j - 1));
168 // DIAGONAL azimuthal term: a_tt^{j+-1/2} (phi_{j+1}-2 phi+phi_{j-1}) / (r_i^2 dtheta^2), flux form.
169 Real out =
170 (att_p * (p(i, j + 1) - p(i, j)) - att_m * (p(i, j) - p(i, j - 1))) * (idth * idth * inv_r2);
171 if (htr) { // CROSS azimuthal term a_tr d_r phi at face j+-1/2 (d_r averaged over 4 corners).
172 const Real atr_p = Real(0.5) * (atr(i, j) + atr(i, j + 1));
173 const Real atr_m = Real(0.5) * (atr(i, j) + atr(i, j - 1));
174 const Real dr_p =
175 (p(i + 1, j) + p(i + 1, j + 1) - p(i - 1, j) - p(i - 1, j + 1)) * (Real(0.25) * idr);
176 const Real dr_m =
177 (p(i + 1, j - 1) + p(i + 1, j) - p(i - 1, j - 1) - p(i - 1, j)) * (Real(0.25) * idr);
178 // (1/r_i)(1/dtheta) [ F_cross_p - F_cross_m ], F_cross = a_tr d_r phi. Metric 1/r_i (face weight dr).
179 out += (atr_p * dr_p - atr_m * dr_m) * (idth / ri);
180 }
181 return out;
182}
183
188POPS_HD inline Real polar_diag(const ConstArray4& arr, const ConstArray4& att, int i, int j, Real ri,
189 Real rfm, Real rfp, Real idr, Real idth) {
190 const Real arr_p = Real(0.5) * (arr(i, j) + arr(i + 1, j));
191 const Real arr_m = Real(0.5) * (arr(i, j) + arr(i - 1, j));
192 const Real att_p = Real(0.5) * (att(i, j) + att(i, j + 1));
193 const Real att_m = Real(0.5) * (att(i, j) + att(i, j - 1));
194 const Real inv_r2 = Real(1) / (ri * ri);
195 const Real rad = (rfp * arr_p + rfm * arr_m) * (idr * idr / ri);
196 const Real azi = (att_p + att_m) * (idth * idth * inv_r2);
197 return -(rad + azi); // diagonal coefficient of L_int (the neighbors have positive coefficients)
198}
199
208 bool hrt, htr;
211 POPS_HD void operator()(int i, int j) const {
212 const Real ri = r_min + (i + Real(0.5)) * dr; // r_cell(i)
213 const Real rfm = r_min + i * dr; // r_face(i) = r_{i-1/2}
214 const Real rfp = r_min + (i + 1) * dr; // r_face(i+1) = r_{i+1/2}
215 L(i, j) = polar_radial_div(p, arr, hrt, art, i, j, ri, rfm, rfp, idr, idth) +
216 polar_azimuthal_div(p, att, htr, atr, i, j, ri, idr, idth);
217 }
218};
219
228 ConstArray4 idiag; // 1 / diagonal coefficient of L_int (negative), precomputed
229 POPS_HD void operator()(int i, int j) const { out(i, j) = z(i, j) * idiag(i, j); }
230};
231
237 POPS_HD void operator()(int i, int j) const { d(i, j) = s(i, j, 0); }
238};
239
245 POPS_HD void operator()(int i, int j) const {
246 const Real ri = r_min + (i + Real(0.5)) * dr;
247 const Real rfm = r_min + i * dr;
248 const Real rfp = r_min + (i + 1) * dr;
249 const Real d = polar_diag(arr, att, i, j, ri, rfm, rfp, idr, idth);
250 idiag(i, j) = d != Real(0) ? Real(1) / d : Real(0);
251 }
252};
253
254} // namespace detail
255
259inline void apply_polar_tensor(const MultiFab& phi, const PolarGeometry& geom, MultiFab& lap,
260 const MultiFab* a_rr, const MultiFab* a_tt, const MultiFab* a_rt,
261 const MultiFab* a_tr) {
262 // CONTRACT: a_rr/a_tt required (the diagonal coefficients of the stencil are always read). An
263 // isotropic case provides fields CONSTANT at 1 (PolarTensorKrylovSolver does this via its internal
264 // stores). a_rt/a_tr optional (cross terms). We cannot deref a nullptr in the kernel -> guard at entry.
265 if (!a_rr || !a_tt)
266 throw std::runtime_error(
267 "apply_polar_tensor: a_rr and a_tt required (fields at 1 if isotropic)");
268 const Real dr = geom.dr();
269 const Real idr = Real(1) / dr;
270 const Real idth = Real(1) / geom.dtheta();
271 for (int li = 0; li < phi.local_size(); ++li) {
272 const ConstArray4 p = phi.fab(li).const_array();
273 Array4 L = lap.fab(li).array();
274 const Box2D v = lap.box(li);
275 const ConstArray4 arr = a_rr->fab(li).const_array();
276 const ConstArray4 att = a_tt->fab(li).const_array();
277 const bool hrt = a_rt != nullptr;
278 const bool htr = a_tr != nullptr;
279 const ConstArray4 art = hrt ? a_rt->fab(li).const_array() : ConstArray4{};
280 const ConstArray4 atr = htr ? a_tr->fab(li).const_array() : ConstArray4{};
282 v, detail::PolarApplyKernel{p, L, arr, att, hrt, htr, art, atr, geom.r_min, dr, idr, idth});
283 }
284}
285
290template <class S>
291concept PolarLinearSolver = requires(S s, Real tol, int it) {
292 { s.rhs() } -> std::same_as<MultiFab&>;
293 { s.phi() } -> std::same_as<MultiFab&>;
294 s.solve();
295 { s.residual() } -> std::convertible_to<Real>;
296 { s.geom() } -> std::convertible_to<const PolarGeometry&>;
297 s.solve(tol, it);
298 requires !std::same_as<decltype(s.solve(tol, it)), void>;
299};
300
305 int iters = 0;
307 bool converged = false;
308};
309
320
333 public:
339 : geom_(geom),
340 bc_(force_theta_periodic(bc)),
341 precond_(precond),
342 dm_(ba.size(), n_ranks()),
343 phi_(ba, dm_, 1, 1),
344 rhs_(ba, dm_, 1, 0),
345 r_(ba, dm_, 1, 0),
346 rhat_(ba, dm_, 1, 0),
347 p_(ba, dm_, 1, 0),
348 v_(ba, dm_, 1, 0),
349 s_(ba, dm_, 1, 0),
350 t_(ba, dm_, 1, 0),
351 phat_(ba, dm_, 1, 1),
352 shat_(ba, dm_, 1, 1),
353 idiag_(ba, dm_, 1, 0),
354 op_offset_(ba, dm_, 1, 0),
355 a_rr_store_(ba, dm_, 1, 1),
356 a_tt_store_(ba, dm_, 1, 1) {
357 // LAYOUT GUARD (replaces the single-rank guard): the RadialLine preconditioner solves a RADIAL
358 // tridiagonal per theta line via a SEQUENTIAL Thomas sweep in r. This sweep must stay LOCAL to a
359 // box (it cannot cross a box/rank boundary). We therefore REQUIRE that each box of the BoxArray
360 // cover the FULL RADIAL RANGE [domain.lo[0], domain.hi[0]] (splitting in theta only). The Jacobi
361 // fallback (per cell) has no such constraint -> no check. Single-rank single box: the box covers
362 // the whole ring, the check passes trivially (path unchanged).
363 if (precond_ == PolarPrecond::RadialLine)
364 check_radial_columns(ba);
365 // Default diagonal coefficients = 1 (isotropic): internal fields at 1, ghosts filled Foextrap.
366 a_rr_store_.set_val(Real(1));
367 a_tt_store_.set_val(Real(1));
368 a_rr_ = &a_rr_store_;
369 a_tt_ = &a_tt_store_;
370 }
371
372 // RULE OF FIVE (C.21): the current pointers a_rr_/a_tt_ alias the internal stores
373 // a_rr_store_/a_tt_store_ or external fields. A DEFAULT copy/move would leave these pointers aiming
374 // at the SOURCE object's stores (dangling/UB). The solver is ALWAYS used as a LOCAL scope variable
375 // (never copied, moved, stored in a container, nor returned by value): we DELETE the four
376 // operations rather than write a move re-pointing the stores (useless here).
381
382 // --- PolarEllipticSolver / PolarLinearSolver contract ---
383 MultiFab& rhs() { return rhs_; }
384 MultiFab& phi() { return phi_; }
385 const PolarGeometry& geom() const { return geom_; }
386
391 void set_coefficients(MultiFab* a_rr, MultiFab* a_tt, MultiFab* a_rt = nullptr,
392 MultiFab* a_tr = nullptr) {
393 a_rr_ = a_rr ? a_rr : &a_rr_store_;
394 a_tt_ = a_tt ? a_tt : &a_tt_store_;
395 a_rt_ = a_rt;
396 a_tr_ = a_tr;
397 fill_coeff_ghosts();
398 coeffs_ready_ = false; // idiag to be recomputed
399 }
400
403 ensure_coeffs();
404 apply_operator(phi_, r_);
405 lincomb(r_, Real(1), rhs_, Real(-1), r_);
406 return l2_norm(r_);
407 }
408
409 void solve() { solve(Real(1e-10), 400); }
410
414 PolarKrylovResult solve(Real rel_tol, int max_iters) {
415 ensure_coeffs();
416 prepare_offset(); // c_bc = apply_operator(0) (inhomogeneous part of Dirichlet boundary) once
418 // MULTI-RANK MPI: ALL ranks (including those without a box, local_size()==0) execute the SAME
419 // BiCGStab body. The per-fab operations (lincomb/saxpy/copy/apply_precond/apply_polar_tensor) are
420 // no-ops on an empty rank; the COLLECTIVES (dot/l2_norm/project_mean -> all_reduce_sum) are called
421 // by ALL ranks in the SAME order (an empty rank contributes 0). The stopping criteria rely only on
422 // these GLOBAL scalars (identical everywhere) -> no desynchronization, no separate "run_empty_rank"
423 // path to maintain in parallel. fill_ghosts (fill_boundary) is a PAIRWISE exchange (paired
424 // Isend/Irecv, not a collective): an empty rank posts nothing and nobody sends to it -> no deadlock.
425 // r0 = rhs - L_int(phi) (AFFINE operator: the Dirichlet data is folded into the residual).
426 apply_operator(phi_, v_);
427 lincomb(r_, Real(1), rhs_, Real(-1), v_);
428 // SINGULAR OPERATOR (pure radial Neumann + periodic theta, no reaction): the constant is in the
429 // kernel of L_int. BiCGStab then diverges (the residual keeps an undamped constant component). We
430 // FIX THE GAUGE by projection onto the zero-mean subspace: we remove the mean of r0 (the RHS must
431 // be compatible: zero mean in the FV sense) and of phi (gauge), then of each preconditioned
432 // correction direction in the loop. This is the iterative counterpart of the mode-0 pinning of the
433 // direct PolarPoissonSolver, without perturbing the stencil. Dirichlet case (>= one boundary):
434 // pin_gauge_ stays false -> PATH UNCHANGED, bit-identical.
435 if (pin_gauge_) {
436 project_mean(r_);
437 project_mean(phi_);
438 }
439 const Real bnorm = l2_norm(rhs_);
440 const Real norm0 = bnorm > Real(0) ? bnorm : Real(1);
441 Real rnorm = l2_norm(r_);
442 res.rel_residual = rnorm / norm0;
443 if (rnorm <= rel_tol * norm0) {
444 res.converged = true;
445 return res;
446 }
447
448 copy_into(rhat_, r_);
449 p_.set_val(Real(0));
450 v_.set_val(Real(0));
451 Real rho_prev = Real(1), alpha = Real(1), omega = Real(1);
452
453 for (int k = 1; k <= max_iters; ++k) {
454 const Real rho = dot(rhat_, r_); // COLLECTIVE
455 if (std::fabs(rho) < kTiny || std::fabs(omega) < kTiny) {
456 res.iters = k - 1;
457 res.rel_residual = rnorm / norm0;
458 return res;
459 }
460 const Real beta = (rho / rho_prev) * (alpha / omega);
461 lincomb(p_, Real(1), p_, -omega, v_); // p <- p - omega v
462 lincomb(p_, beta, p_, Real(1), r_); // p <- r + beta p
463 apply_precond(p_, phat_); // phat = M^{-1} p
464 if (pin_gauge_)
465 project_mean(phat_); // gauge: zero-mean correction direction
466 apply_operator_lin(phat_, v_); // v = L_lin(phat) (LINEAR matvec)
467 const Real rhat_dot_v = dot(rhat_, v_);
468 if (std::fabs(rhat_dot_v) < kTiny) {
469 res.iters = k - 1;
470 res.rel_residual = rnorm / norm0;
471 return res;
472 }
473 alpha = rho / rhat_dot_v;
474 lincomb(s_, Real(1), r_, -alpha, v_); // s <- r - alpha v
475 saxpy(phi_, alpha, phat_); // phi <- phi + alpha phat
476 const Real snorm = l2_norm(s_);
477 if (snorm <= rel_tol * norm0) {
478 rnorm = snorm;
479 res.iters = k;
480 res.rel_residual = rnorm / norm0;
481 res.converged = true;
482 return res;
483 }
484 apply_precond(s_, shat_); // shat = M^{-1} s
485 if (pin_gauge_)
486 project_mean(shat_); // gauge: zero-mean correction direction
487 apply_operator_lin(shat_, t_); // t = L_lin(shat)
488 const Real tt = dot(t_, t_);
489 omega = tt > kTiny ? dot(t_, s_) / tt : Real(0);
490 saxpy(phi_, omega, shat_); // phi <- phi + omega shat
491 lincomb(r_, Real(1), s_, -omega, t_); // r <- s - omega t
492 rnorm = l2_norm(r_);
493 res.iters = k;
494 res.rel_residual = rnorm / norm0;
495 if (rnorm <= rel_tol * norm0) {
496 res.converged = true;
497 return res;
498 }
499 rho_prev = rho;
500 }
501 return res; // max_iters reached: best effort (converged=false)
502 }
503
504 private:
505 static constexpr Real kTiny = Real(1e-300);
506
511 void check_radial_columns(const BoxArray& ba) const {
512 const Box2D dom = geom_.domain;
513 for (int b = 0; b < ba.size(); ++b) {
514 if (ba[b].lo[0] != dom.lo[0] || ba[b].hi[0] != dom.hi[0])
515 throw std::runtime_error(
516 "PolarTensorKrylovSolver (precond RadialLine): the BoxArray must be split in THETA "
517 "only "
518 "(each box covers the full radial range). The Thomas sweep in r cannot cross a box "
519 "boundary. Use an azimuthal splitting, or the PolarPrecond::Jacobi fallback (per cell, "
520 "without layout constraint) if r must be cut.");
521 }
522 }
523
530 static BCRec force_theta_periodic(const BCRec& bc) {
531 BCRec b = bc;
532 b.ylo = BCType::Periodic;
533 b.yhi = BCType::Periodic;
534 b.ylo_val = Real(0);
535 b.yhi_val = Real(0);
536 return b;
537 }
538
540 BCRec coeff_bc() const {
541 auto fo = [](BCType t) { return t == BCType::Periodic ? t : BCType::Foextrap; };
542 BCRec b;
543 b.xlo = fo(bc_.xlo);
544 b.xhi = fo(bc_.xhi);
545 b.ylo = BCType::Periodic;
546 b.yhi = BCType::Periodic; // theta always periodic
547 return b;
548 }
549
550 void fill_coeff_ghosts() {
551 const BCRec eb = coeff_bc();
552 device_fence();
553 fill_ghosts(*a_rr_, geom_.domain, eb);
554 fill_ghosts(*a_tt_, geom_.domain, eb);
555 if (a_rt_)
556 fill_ghosts(*a_rt_, geom_.domain, eb);
557 if (a_tr_)
558 fill_ghosts(*a_tr_, geom_.domain, eb);
559 }
560
561 void ensure_coeffs() {
562 if (coeffs_ready_)
563 return;
564 // idiag = 1/diag of the diagonal stencil (for Jacobi).
565 const Real dr = geom_.dr();
566 const Real idr = Real(1) / dr;
567 const Real idth = Real(1) / geom_.dtheta();
568 for (int li = 0; li < idiag_.local_size(); ++li) {
569 for_each_cell(idiag_.box(li), detail::PolarInvDiagKernel{
570 a_rr_->fab(li).const_array(), a_tt_->fab(li).const_array(),
571 idiag_.fab(li).array(), geom_.r_min, dr, idr, idth});
572 }
573 if (precond_ == PolarPrecond::RadialLine)
574 build_radial_lines();
575 coeffs_ready_ = true;
576 }
577
589 void build_radial_lines() {
590 a_rr_->sync_host();
591 a_tt_->sync_host();
592 const int nr = geom_.domain.nx();
593 const Box2D dom = geom_.domain;
594 const Real dr = geom_.dr();
595 const Real idr2 = Real(1) / (dr * dr);
596 const Real idth2 = Real(1) / (geom_.dtheta() * geom_.dtheta());
597 const bool dir_lo = bc_.xlo == BCType::Dirichlet;
598 const bool dir_hi = bc_.xhi == BCType::Dirichlet;
599 nr_ = nr;
600 const int nloc = idiag_.local_size();
601 line_b_.resize(static_cast<std::size_t>(nloc));
602 line_sub_.resize(static_cast<std::size_t>(nloc));
603 line_sup_.resize(static_cast<std::size_t>(nloc));
604 for (int li = 0; li < nloc; ++li) {
605 const Box2D vb = idiag_.box(li); // local VALID box (full r range, theta sub-range)
606 const int nth_l = vb.ny(); // number of theta lines LOCAL to this box
607 const ConstArray4 arr = a_rr_->fab(li).const_array();
608 const ConstArray4 att = a_tt_->fab(li).const_array();
609 std::vector<Real>& lb = line_b_[li];
610 std::vector<Real>& lsub = line_sub_[li];
611 std::vector<Real>& lsup = line_sup_[li];
612 lb.assign(static_cast<std::size_t>(nth_l) * static_cast<std::size_t>(nr), Real(0));
613 lsub.assign(static_cast<std::size_t>(nth_l) * static_cast<std::size_t>(nr), Real(0));
614 lsup.assign(static_cast<std::size_t>(nth_l) * static_cast<std::size_t>(nr), Real(0));
615 for (int jl = 0; jl < nth_l; ++jl) {
616 const int jg = vb.lo[1] + jl; // GLOBAL theta index
617 for (int i = 0; i < nr; ++i) {
618 const int ig = dom.lo[0] + i;
619 const Real ri = geom_.r_cell(i);
620 const Real rfm = geom_.r_face(i);
621 const Real rfp = geom_.r_face(i + 1);
622 const Real arr_p = Real(0.5) * (arr(ig, jg) + arr(ig + 1, jg));
623 const Real arr_m = Real(0.5) * (arr(ig, jg) + arr(ig - 1, jg));
624 const Real att_p = Real(0.5) * (att(ig, jg) + att(ig, jg + 1));
625 const Real att_m = Real(0.5) * (att(ig, jg) + att(ig, jg - 1));
626 const Real ai = rfm * arr_m * (idr2 / ri); // coeff of p_{i-1} in L_int
627 const Real ci = rfp * arr_p * (idr2 / ri); // coeff of p_{i+1}
628 const Real azi_diag =
629 (att_p + att_m) * (idth2 / (ri * ri)); // azimuthal part of -diag (lumped)
630 Real bi = -(ai + ci) - azi_diag; // FULL diagonal of L_int (radial + azimuthal)
631 Real sub = ai, sup = ci;
632 if (i ==
633 0) { // HOMOGENEOUS low BC fold: Dirichlet b -= a, Neumann b += a; sub-diag zeroed
634 bi += dir_lo ? -ai : ai;
635 sub = Real(0);
636 }
637 if (i == nr - 1) { // HOMOGENEOUS high BC fold
638 bi += dir_hi ? -ci : ci;
639 sup = Real(0);
640 }
641 const std::size_t idx = static_cast<std::size_t>(jl) * static_cast<std::size_t>(nr) +
642 static_cast<std::size_t>(i);
643 lb[idx] = bi;
644 lsub[idx] = sub;
645 lsup[idx] = sup;
646 }
647 }
648 }
649 }
650
653 void apply_operator(MultiFab& in, MultiFab& out) {
654 device_fence();
655 fill_ghosts(in, geom_.domain, bc_);
656 apply_polar_tensor(in, geom_, out, a_rr_, a_tt_, a_rt_, a_tr_);
657 }
658
661 void apply_operator_lin(MultiFab& in, MultiFab& out) {
662 apply_operator(in, out);
663 if (has_op_offset_)
664 lincomb(out, Real(1), out, Real(-1), op_offset_);
665 }
666
670 void apply_precond(MultiFab& in, MultiFab& out) {
671 if (precond_ == PolarPrecond::Jacobi) {
672 for (int li = 0; li < out.local_size(); ++li)
673 for_each_cell(out.box(li),
674 detail::PolarJacobiApplyKernel{in.fab(li).const_array(), out.fab(li).array(),
675 idiag_.fab(li).const_array()});
676 return;
677 }
678 apply_precond_radial_line(in, out);
679 }
680
688 void apply_precond_radial_line(MultiFab& in, MultiFab& out) {
689 if (in.local_size() == 0)
690 return;
691 in.sync_host();
692 const int r_lo = geom_.domain.lo[0];
693 const std::size_t N = static_cast<std::size_t>(nr_);
694 cthom_.assign(N, Real(0)); // Thomas working super-diagonal
695 xthom_.assign(N, Real(0)); // working solution
696 for (int li = 0; li < in.local_size(); ++li) {
697 const Box2D vb = in.box(li);
698 const ConstArray4 z = in.fab(li).const_array();
699 Array4 o = out.fab(li).array();
700 const std::vector<Real>& lb = line_b_[li];
701 const std::vector<Real>& lsub = line_sub_[li];
702 const std::vector<Real>& lsup = line_sup_[li];
703 const int nth_l = vb.ny();
704 for (int jl = 0; jl < nth_l; ++jl) {
705 const int jg = vb.lo[1] + jl;
706 const std::size_t base = static_cast<std::size_t>(jl) * N;
707 // Thomas in r: a = lsub, b = lb, c = lsup (base + i), rhs = z(., jg).
708 Real beta = lb[base + 0];
709 xthom_[0] = (beta != Real(0) ? z(r_lo, jg) / beta : Real(0));
710 for (std::size_t i = 1; i < N; ++i) {
711 cthom_[i] = lsup[base + i - 1] / beta;
712 beta = lb[base + i] - lsub[base + i] * cthom_[i];
713 const Real zi = z(r_lo + static_cast<int>(i), jg);
714 xthom_[i] = (beta != Real(0) ? (zi - lsub[base + i] * xthom_[i - 1]) / beta : Real(0));
715 }
716 for (int i = static_cast<int>(N) - 2; i >= 0; --i)
717 xthom_[static_cast<std::size_t>(i)] -=
718 cthom_[static_cast<std::size_t>(i + 1)] * xthom_[static_cast<std::size_t>(i + 1)];
719 for (std::size_t i = 0; i < N; ++i)
720 o(r_lo + static_cast<int>(i), jg) = xthom_[i];
721 }
722 }
723 }
724
730 void prepare_offset() {
731 has_op_offset_ = (bc_.xlo == BCType::Dirichlet && bc_.xlo_val != Real(0)) ||
732 (bc_.xhi == BCType::Dirichlet && bc_.xhi_val != Real(0));
733 pin_gauge_ = (bc_.xlo != BCType::Dirichlet) && (bc_.xhi != BCType::Dirichlet);
734 if (has_op_offset_) {
735 // Called on ALL ranks (apply_operator is a no-op on an empty rank except for the pairwise
736 // fill_ghosts): no local_size() branch that would unpair the cross-rank halo exchange.
737 phat_.set_val(Real(0));
738 apply_operator(phat_, op_offset_); // op_offset_ <- L_int(0) = c_bc
739 }
740 }
741
750 void project_mean(MultiFab& x) {
751 x.sync_host();
752 const int r_lo = geom_.domain.lo[0];
753 const Real dr = geom_.dr(), dth = geom_.dtheta();
754 Real sum = 0, vol = 0;
755 for (int li = 0; li < x.local_size(); ++li) {
756 const Box2D vb = x.box(li);
757 const ConstArray4 a = x.fab(li).const_array();
758 for (int j = vb.lo[1]; j <= vb.hi[1]; ++j)
759 for (int i = vb.lo[0]; i <= vb.hi[0]; ++i) {
760 const Real w = geom_.r_cell(i - r_lo) * dr * dth; // i - r_lo = 0-based radial offset
761 sum += a(i, j) * w;
762 vol += w;
763 }
764 }
765 // COLLECTIVE all-reduce (over all ranks, including empty ones): GLOBAL mean identical everywhere.
766 sum = static_cast<Real>(all_reduce_sum(static_cast<double>(sum)));
767 vol = static_cast<Real>(all_reduce_sum(static_cast<double>(vol)));
768 const Real mean = vol > Real(0) ? sum / vol : Real(0);
769 for (int li = 0; li < x.local_size(); ++li) {
770 const Box2D vb = x.box(li);
771 Array4 a = x.fab(li).array();
772 for (int j = vb.lo[1]; j <= vb.hi[1]; ++j)
773 for (int i = vb.lo[0]; i <= vb.hi[0]; ++i)
774 a(i, j) -= mean;
775 }
776 }
777
778 Real l2_norm(const MultiFab& x) { return std::sqrt(dot(x, x)); }
779
780 void copy_into(MultiFab& dst, const MultiFab& src) {
781 for (int li = 0; li < dst.local_size(); ++li) {
782 Array4 d = dst.fab(li).array();
783 const ConstArray4 s = src.fab(li).const_array();
784 for_each_cell(dst.box(li), detail::PolarCopyKernel{d, s});
785 }
786 }
787
788 PolarGeometry geom_;
789 BCRec bc_;
790 PolarPrecond precond_;
791 DistributionMapping dm_;
792 MultiFab phi_, rhs_;
793 MultiFab r_, rhat_, p_, v_, s_, t_;
794 MultiFab phat_, shat_;
795 MultiFab idiag_;
796 MultiFab op_offset_;
797 MultiFab a_rr_store_, a_tt_store_;
798 MultiFab* a_rr_ = nullptr;
799 MultiFab* a_tt_ = nullptr;
800 MultiFab* a_rt_ = nullptr;
801 MultiFab* a_tr_ = nullptr;
802 bool coeffs_ready_ = false;
803 bool has_op_offset_ = false;
804 bool pin_gauge_ =
805 false;
806 // RadialLine preconditioner: radial tridiagonal of the diagonal block per theta line. PER LOCAL BOX
807 // (line_b_/sub_/sup_[li]), each stored [jl*nr + i] where jl = theta index LOCAL to the box, i = global
808 // radius 0..nr-1 (a_rr may depend on theta -> coeffs per (i, jl)). The splitting being in THETA only,
809 // M^{-1} is block-diagonal per box (no cross-box radial coupling) -> each box is inverted
810 // independently. cthom_/xthom_ = Thomas working buffers (reused, length nr). HOST.
811 std::vector<std::vector<Real>> line_b_, line_sub_,
812 line_sup_;
813 mutable std::vector<Real> cthom_, xthom_;
814 int nr_ = 0;
815};
816
817static_assert(PolarLinearSolver<PolarTensorKrylovSolver>,
818 "PolarTensorKrylovSolver must model PolarLinearSolver");
819
820} // namespace pops
BoxArray: the set of boxes tiling a level (disjoint, covering).
Ordered list of boxes tiling a level.
Definition box_array.hpp:22
int size() const
Number of boxes in the tiling.
Definition box_array.hpp:42
ConstArray4 const_array() const
READ handle (POD device-copyable) over this Fab. Valid as long as the Fab lives.
Definition fab2d.hpp:96
Array4 array()
WRITE handle (POD device-copyable) over this Fab. Valid as long as the Fab lives.
Definition fab2d.hpp:91
Field distributed over a level: decomposition (BoxArray) + distribution (DistributionMapping) + ncomp...
Definition multifab.hpp:33
Fab2D & fab(int li)
Local fab at index li (0 <= li < local_size()), for writing.
Definition multifab.hpp:67
void sync_host()
Makes the HOST residence valid (before a host access: operator(), loop, set_val).
Definition multifab.hpp:79
const Box2D & box(int li) const
VALID box of local fab li.
Definition multifab.hpp:71
void set_val(Real v)
Fills all cells (valid + ghosts) of every local fab with v.
Definition multifab.hpp:85
int local_size() const
Number of fabs OWNED by this rank (bound on local indices).
Definition multifab.hpp:65
MATRIX-FREE BiCGStab Krylov solver for the FULL-tensor POLAR elliptic operator L_int(phi) = div(A gra...
Definition polar_tensor_operator.hpp:332
PolarTensorKrylovSolver & operator=(PolarTensorKrylovSolver &&)=delete
PolarTensorKrylovSolver(const PolarTensorKrylovSolver &)=delete
void set_coefficients(MultiFab *a_rr, MultiFab *a_tt, MultiFab *a_rt=nullptr, MultiFab *a_tr=nullptr)
Sets the tensor A coefficients.
Definition polar_tensor_operator.hpp:391
PolarTensorKrylovSolver & operator=(const PolarTensorKrylovSolver &)=delete
MultiFab & rhs()
Definition polar_tensor_operator.hpp:383
Real residual()
Current GLOBAL L2 residual ||rhs - L_int(phi)|| (collective). Prepares once if needed.
Definition polar_tensor_operator.hpp:402
const PolarGeometry & geom() const
Definition polar_tensor_operator.hpp:385
PolarTensorKrylovSolver(PolarTensorKrylovSolver &&)=delete
PolarKrylovResult solve(Real rel_tol, int max_iters)
MATRIX-FREE BiCGStab preconditioned by precond_ (RadialLine by default, Jacobi as fallback); fixes th...
Definition polar_tensor_operator.hpp:414
MultiFab & phi()
Definition polar_tensor_operator.hpp:384
void solve()
Definition polar_tensor_operator.hpp:409
PolarTensorKrylovSolver(const PolarGeometry &geom, const BoxArray &ba, const BCRec &bc, PolarPrecond precond=PolarPrecond::RadialLine)
Definition polar_tensor_operator.hpp:337
Parallel seam: minimal MPI abstraction (rank/size + collectives) with serial fallback.
Contract of the iterative POLAR elliptic operators: same shape as PolarEllipticSolver (cf.
Definition polar_tensor_operator.hpp:291
DistributionMapping: maps each box (by global index) to its owning MPI rank.
Fab2D: single-grid data on a Box2D (in-house equivalent of AMReX's FArrayBox); Array4 / ConstArray4: ...
for_each_cell and reductions: the parallelism SEAM over the cells of a Box2D; sync_host / sync_device...
Geometry: index-space (Box2D) <-> Cartesian physical-space mapping; PolarGeometry: SIBLING for a glob...
MultiFab arithmetic (saxpy, lincomb, norm_inf, dot) over VALID cells.
MultiFab: a field DISTRIBUTED over a level (equivalent of AMReX's MultiFab).
POPS_HD Real polar_diag(const ConstArray4 &arr, const ConstArray4 &att, int i, int j, Real ri, Real rfm, Real rfp, Real idr, Real idth)
Diagonal (coefficient of phi_{i,j}) of the diagonal POLAR stencil (radial + azimuthal),...
Definition polar_tensor_operator.hpp:188
POPS_HD Real polar_radial_div(const ConstArray4 &p, const ConstArray4 &arr, bool hrt, const ConstArray4 &art, int i, int j, Real ri, Real rfm, Real rfp, Real idr, Real idth)
RADIAL FACE contribution to the diagonal + cross stencil, at (i, j).
Definition polar_tensor_operator.hpp:135
POPS_HD Real polar_azimuthal_div(const ConstArray4 &p, const ConstArray4 &att, bool htr, const ConstArray4 &atr, int i, int j, Real ri, Real idr, Real idth)
AZIMUTHAL FACE contribution to the diagonal + cross stencil, at (i, j).
Definition polar_tensor_operator.hpp:162
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
int n_ranks()
Definition comm.hpp:139
double all_reduce_sum(double x)
Definition comm.hpp:143
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 device_fence()
Device barrier: waits for in-flight kernels to finish before a HOST access to unified memory.
Definition kokkos_env.hpp:43
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 apply_polar_tensor(const MultiFab &phi, const PolarGeometry &geom, MultiFab &lap, const MultiFab *a_rr, const MultiFab *a_tt, const MultiFab *a_rt, const MultiFab *a_tr)
Applies L_int(phi) = div(A grad phi) in polar over the whole MultiFab.
Definition polar_tensor_operator.hpp:259
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
PolarPrecond
Choice of the SIMPLE BiCGStab PRECONDITIONER (NO MG V-cycle – stagnation on polar 1/r^2,...
Definition polar_tensor_operator.hpp:319
BCType
Boundary condition type for a face: Periodic (handled by fill_boundary), Foextrap (zero gradient,...
Definition physical_bc.hpp:25
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).
WRITE POD handle (raw pointer + strides) over a Fab2D buffer, indexed by (i, j, c) IN GLOBAL INDICES ...
Definition fab2d.hpp:29
Boundary conditions for the FOUR faces of the domain (type + associated Dirichlet value).
Definition physical_bc.hpp:29
BCType xlo
Definition physical_bc.hpp:30
Real xlo_val
Definition physical_bc.hpp:32
Real xhi_val
Definition physical_bc.hpp:32
BCType xhi
Definition physical_bc.hpp:30
2D integer index space, cell-centered.
Definition box2d.hpp:37
POPS_HD int nx() const
Width (direction 0). POPS_HD (called from Geometry::dx() in a device kernel).
Definition box2d.hpp:50
int hi[2]
Definition box2d.hpp:39
POPS_HD int ny() const
Height (direction 1). POPS_HD (called from Geometry::dy() in a device kernel).
Definition box2d.hpp:52
int lo[2]
Definition box2d.hpp:38
READ-only handle (const counterpart of Array4): same layout and same contract (POD device-copyable,...
Definition fab2d.hpp:44
Definition geometry.hpp:59
POPS_HD Real r_face(int i) const
Radius at radial FACE i (face between cells i-1 and i; i = 0 -> r_min, i = nr -> r_max).
Definition geometry.hpp:75
POPS_HD Real r_cell(int i) const
Radius at the CENTER of radial cell i (i = 0 -> r_min + dr/2).
Definition geometry.hpp:73
POPS_HD Real dr() const
Definition geometry.hpp:70
Real r_min
Definition geometry.hpp:61
Box2D domain
nx() = nr (radial cells), ny() = ntheta (azimuthal cells)
Definition geometry.hpp:60
POPS_HD Real dtheta() const
Definition geometry.hpp:71
Result of a polar BiCGStab solve: iterations, relative residual, convergence.
Definition polar_tensor_operator.hpp:304
bool converged
Definition polar_tensor_operator.hpp:307
Real rel_residual
Definition polar_tensor_operator.hpp:306
int iters
Definition polar_tensor_operator.hpp:305
L_int(phi) = div(A grad phi) in polar (apply).
Definition polar_tensor_operator.hpp:204
ConstArray4 atr
Definition polar_tensor_operator.hpp:209
ConstArray4 att
Definition polar_tensor_operator.hpp:207
Array4 L
Definition polar_tensor_operator.hpp:206
Real dr
Definition polar_tensor_operator.hpp:210
ConstArray4 art
Definition polar_tensor_operator.hpp:209
bool htr
Definition polar_tensor_operator.hpp:208
POPS_HD void operator()(int i, int j) const
Definition polar_tensor_operator.hpp:211
bool hrt
Definition polar_tensor_operator.hpp:208
ConstArray4 p
Definition polar_tensor_operator.hpp:205
Real idth
Definition polar_tensor_operator.hpp:210
Real idr
Definition polar_tensor_operator.hpp:210
ConstArray4 arr
Definition polar_tensor_operator.hpp:207
Real r_min
Definition polar_tensor_operator.hpp:210
Copy component 0 (dst <- src).
Definition polar_tensor_operator.hpp:234
POPS_HD void operator()(int i, int j) const
Definition polar_tensor_operator.hpp:237
Array4 d
Definition polar_tensor_operator.hpp:235
ConstArray4 s
Definition polar_tensor_operator.hpp:236
Computes idiag = 1 / diag of the diagonal polar stencil (for Jacobi). diag = polar_diag (< 0).
Definition polar_tensor_operator.hpp:241
Array4 idiag
Definition polar_tensor_operator.hpp:243
POPS_HD void operator()(int i, int j) const
Definition polar_tensor_operator.hpp:245
Real idr
Definition polar_tensor_operator.hpp:244
ConstArray4 arr
Definition polar_tensor_operator.hpp:242
Real dr
Definition polar_tensor_operator.hpp:244
Real idth
Definition polar_tensor_operator.hpp:244
Real r_min
Definition polar_tensor_operator.hpp:244
ConstArray4 att
Definition polar_tensor_operator.hpp:242
out = (f - L0 phi) / |diag| – one Jacobi iteration (point-by-point relaxation) on the DIAGONAL polar ...
Definition polar_tensor_operator.hpp:225
POPS_HD void operator()(int i, int j) const
Definition polar_tensor_operator.hpp:229
ConstArray4 idiag
Definition polar_tensor_operator.hpp:228
Array4 out
Definition polar_tensor_operator.hpp:227
ConstArray4 z
Definition polar_tensor_operator.hpp:226
Base scalar types and the POPS_HD macro (host+device portability).
#define POPS_HD
Definition types.hpp:25