include/pops/numerics/elliptic/mg/geometric_mg.hpp Source FileΒΆ

adc_cpp: include/pops/numerics/elliptic/mg/geometric_mg.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
geometric_mg.hpp
Go to the documentation of this file.
1#pragma once
2
30
42
43#include <chrono> // last_bottom_seconds(): self-time the coarsest (bottom) GS solve (Spec 5, ADC-479)
44#include <cstdio> // POPS_TRACE_SOLVE_FIELDS: device diagnostic trace (#93), inert by default
45#include <cstdlib> // getenv
46#include <functional>
47#include <utility>
48#include <vector>
49
50namespace pops {
51
52// DIAGNOSTIC trace of the MG V-cycle (milestone #93). Active only if POPS_TRACE_SOLVE_FIELDS is set;
53// stderr + immediate flush to locate the last marker before a device crash. INERT by default.
54namespace detail {
55inline void mg_trace_mark(const char* w) {
56 static const bool on = std::getenv("POPS_TRACE_SOLVE_FIELDS") != nullptr;
57 if (on) {
58 std::fprintf(stderr, "[mg] %s\n", w);
59 std::fflush(stderr);
60 }
61}
62
63// Copy component 0 of a fine field (discretized eps/eps_y/kappa) onto the MG fine level.
64// NAMED FUNCTOR (not an POPS_HD lambda): same device-clean recipe as the rest (#93). Identical
65// body -> bit-identical. Inert on the constant-eps path, exercised as soon as a field is wired.
69 POPS_HD void operator()(int i, int j) const { d(i, j) = s(i, j, 0); }
70};
71} // namespace detail
72
73inline BCRec homogeneous(const BCRec& b) {
74 BCRec h = b;
75 h.xlo_val = h.xhi_val = h.ylo_val = h.yhi_val = 0;
76 return h;
77}
78
80 public:
81 // active(x, y): optional "active cell" predicate (interior of the conductor).
82 // Empty => everything active (no embedded wall).
83 // replicated: if true, each level (mono-box covering the domain) is REPLICATED on
84 // all ranks (dmap = my_rank() everywhere) instead of the default round-robin. Each rank
85 // then solves the SAME coarse Poisson redundantly, WITHOUT communication (per-fab V-cycle,
86 // fill_boundary on a box covering the domain is local, and current_residual reduced by
87 // norm_inf = all_reduce_MAX, idempotent under replication). This is what the AMR coupler
88 // expects (level 0 replicated). In serial my_rank()=0 -> bit-for-bit identical to round-robin.
89 //
90 // cut_cell + levelset: ORDER-2 embedded boundary (Shortley-Weller) instead of
91 // the staircase. levelset(x, y) is a level-set function (< 0 inside, sign of
92 // the boundary); for the conducting circle, levelset = hypot(x - cx, y - cy) - Rwall.
93 // Each active cell receives 5 coefficients computed from the distances to the
94 // boundary (cut fraction theta per direction). active is then deduced from the sign of
95 // levelset if it is not provided. cut_cell=false => historical staircase stencil (bit-identical).
96 //
97 // V-cycle parameters (proven defaults):
98 // min_coarse (default 2): minimal size of a grid dimension below which we STOP
99 // coarsening. Coarsening grows the domain by 2 as long as nx/2 and ny/2
100 // stay >= min_coarse (and the boxes coarsen cleanly); the
101 // coarsest grid (the bottom) thus keeps >= min_coarse cells per axis.
102 // nu1 (default 2): number of PRE-smoothing Gauss-Seidel sweeps (before descending to the
103 // coarse grid), at each non-bottom level.
104 // nu2 (default 2): number of POST-smoothing Gauss-Seidel sweeps (after ascending and adding
105 // the prolonged correction), at each non-bottom level.
106 // nbottom (default 50): number of Gauss-Seidel sweeps at the coarsest level (bottom solve);
107 // this long smoothing stands in for an exact solve on the small bottom grid.
108 // (solve_robust LOCALLY doubles nu1/nu2 if the embedded boundary makes the cycle diverge, then restores them.)
109 GeometricMG(const Geometry& geom, const BoxArray& ba, const BCRec& bc,
110 std::function<bool(Real, Real)> active = {}, bool replicated = false,
111 int min_coarse = 2, int nu1 = 2, int nu2 = 2, int nbottom = 50, bool cut_cell = false,
112 std::function<Real(Real, Real)> levelset = {})
113 : bc_(bc),
114 active_(std::move(active)),
115 nu1_(nu1),
116 nu2_(nu2),
117 nbottom_(nbottom),
118 replicated_(replicated),
119 cut_cell_(cut_cell),
120 levelset_(std::move(levelset)) {
121 if (cut_cell_ && levelset_ && !active_)
122 active_ = [ls = levelset_](Real x, Real y) { return ls(x, y) < Real(0); };
123 add_level(geom, ba);
124 while (true) {
125 const Geometry g = lev_.back().geom;
126 if (g.domain.nx() % 2 || g.domain.ny() % 2)
127 break;
128 if (g.domain.nx() / 2 < min_coarse || g.domain.ny() / 2 < min_coarse)
129 break;
130 // Stop if a box of the current level does not coarsen CLEANLY: on a MULTI-BOX domain
131 // (max_grid_size < n), the boxes shrink by 2 at each level and
132 // end up at 1 cell; coarsen(ba, 2) would then make SEVERAL distinct fine
133 // boxes fall onto the SAME coarse cell -> DEGENERATE coarse BoxArray (duplicate boxes
134 // covering the same cell). average_down reads an r x r block per coarse cell
135 // (F(r*I+a, r*J+b)): for a fine fab of 1 cell (0 ghost) three of the four reads
136 // fall OUT of the buffer bounds (negative indices), i.e. into uninitialized memory.
137 // In serial the heap is stable (deterministic read), but on the MPI path the heap is
138 // shuffled and the read becomes ERRATIC (pointwise deviation up to blow-up). So we keep
139 // the current level as the coarsest grid. refine(coarsen(b)) == b characterizes
140 // exactly the boxes that are aligned AND of even size (exact coarsening, no duplicate or
141 // overflow); mono-box and non-degenerate multi-box never cross this break ->
142 // hierarchy (and result) STRICTLY unchanged on those cases.
143 const BoxArray& cur = lev_.back().ba;
144 bool coarsenable = true;
145 for (int i = 0; i < cur.size(); ++i)
146 if (!(cur[i].coarsen(2).refine(2) == cur[i])) {
147 coarsenable = false;
148 break;
149 }
150 if (!coarsenable)
151 break;
152 Geometry gc{g.domain.coarsen(2), g.xlo, g.xhi, g.ylo, g.yhi};
153 add_level(gc, coarsen(lev_.back().ba, 2));
154 }
155 // V-cycle buffers (corr/cfine) allocated ONCE for each NON-bottom level. cfine adopts the
156 // exact layout that average_down/interpolate would have allocated internally: coarsen(L.ba, 2) on the
157 // FINE dmap (L.dm), 0 ghost. It is REUSED for restriction (average_down(L.res, C.rhs)) AND
158 // prolongation (interpolate(C.phi, L.corr)) of the same level (uses disjoint in time -> a single
159 // buffer suffices). The bottom does not need them (early return from vcycle_rec) and its coarsen would
160 // be degenerate (the very reason coarsening stops) -> not allocated.
161 for (int l = 0; l + 1 < static_cast<int>(lev_.size()); ++l) {
162 lev_[l].corr = MultiFab(lev_[l].ba, lev_[l].dm, 1, 0);
163 lev_[l].cfine = MultiFab(coarsen(lev_[l].ba, 2), lev_[l].dm, 1, 0);
164 }
165 if (active_) {
166 // each level evaluates its own mask from the physical circle
167 for (auto& L : lev_) {
168 L.mask = MultiFab(L.ba, L.dm, 1, 0);
169 for (int li = 0; li < L.mask.local_size(); ++li) {
170 Array4 m = L.mask.fab(li).array();
171 const Geometry& g = L.geom;
172 const Box2D b = L.mask.box(li);
173 // host initialization (std::function predicate not device-callable);
174 // writes unified memory before any kernel.
175 for (int j = b.lo[1]; j <= b.hi[1]; ++j)
176 for (int i = b.lo[0]; i <= b.hi[0]; ++i)
177 m(i, j) = active_(g.x_cell(i), g.y_cell(j)) ? Real(1) : Real(0);
178 }
179 }
180 }
181 if (cut_cell_ && levelset_) {
182 // Shortley-Weller coefficients per active cell, computed per level from
183 // the level-set cut fractions (linear crossing). w_diag grows near the
184 // boundary (cut cell) but the system STAYS diagonally dominant (GS converges):
185 // we only clamp theta at 1e-3 to avoid division by 0, without degrading order 2
186 // (a wider clamp, e.g. 0.05, shifts the worst cut cells and breaks the order).
187 for (auto& L : lev_) {
188 L.coef = MultiFab(L.ba, L.dm, 5, 0);
189 const Geometry& g = L.geom;
190 const Real dx = g.dx(), dy = g.dy();
191 for (int li = 0; li < L.coef.local_size(); ++li) {
192 Array4 c = L.coef.fab(li).array();
193 const ConstArray4 m = L.mask.fab(li).const_array();
194 const Box2D b = L.coef.box(li);
195 // SHARED face-crossing primitive (cut_fraction.hpp): SAME aperture geometry
196 // as the future EB transport. detail::cut_fraction reproduces verbatim the old 'cut'
197 // lambda (cut_distance, same branches and same 1e-3 clamp) and detail::shortley_weller the
198 // formula for the 5 weights -> coef BIT-IDENTICAL to the inline assembly before the refactor.
199 const auto& ls = levelset_;
200 for (int j = b.lo[1]; j <= b.hi[1]; ++j)
201 for (int i = b.lo[0]; i <= b.hi[0]; ++i) {
202 if (m(i, j) == Real(0)) { // conductor: coef unused (cell skipped)
203 for (int k = 0; k < 5; ++k)
204 c(i, j, k) = 0;
205 continue;
206 }
207 const detail::CutFraction cf =
208 detail::cut_fraction(ls, g.x_cell(i), g.y_cell(j), dx, dy);
209 const detail::ShortleyWellerWeights w = detail::shortley_weller(cf);
210 c(i, j, 0) = w.w_xm; // w_xm on p(i-1)
211 c(i, j, 1) = w.w_xp; // w_xp on p(i+1)
212 c(i, j, 2) = w.w_ym; // w_ym on p(i,j-1)
213 c(i, j, 3) = w.w_yp; // w_yp on p(i,j+1)
214 c(i, j, 4) = w.w_diag; // w_diag
215 }
216 }
217 }
218 }
219 }
220
221 MultiFab& phi() { return lev_[0].phi; }
222 MultiFab& rhs() { return lev_[0].rhs; }
223 const Geometry& geom() const { return lev_[0].geom; }
224 int num_levels() const { return static_cast<int>(lev_.size()); }
225
226 // --- PER-SOLVE PROFILING STATS (Spec 5 sec.13.11.1, ADC-479 criteria 42/43) -------------------
227 // Cached by the most recent solve(rel_tol, max_cycles, abs_tol) call (the no-argument concept-level
228 // solve() funnels through it). The System reads these back at the field_solve seam to populate the
229 // elliptic-solver native counters WITHOUT threading a profiler into the deep numerics: chrono-only
230 // here, no profiler / Kokkos dependency. Additive accessors -- no existing path reads them, the
231 // default behavior is unchanged.
232 // last_cycles(): V-cycles performed by the last solve (the value solve() returns).
233 // last_residual(): final residual (infinity norm) reached by the last solve.
234 // last_bottom_seconds(): wall-clock self-time of the coarsest-grid (bottom) Gauss-Seidel solves
235 // summed over the V-cycles of the last solve (steady_clock; host serial /
236 // per-rank; on a device backend a fence would be needed for an exact bottom
237 // time, deferred -- the counter stays an honest host-side measurement).
238 int last_cycles() const { return last_cycles_; }
239 Real last_residual() const { return last_residual_; }
240 double last_bottom_seconds() const { return last_bottom_seconds_; }
241
242 // Activates VARIABLE permittivity eps(x): the operator goes from lap(phi)=f to
243 // div(eps grad phi)=f. eps is a CELL-CENTERED field, evaluated by the
244 // analytic function provided on EACH level of the hierarchy (like the mask
245 // and the cut-cell coefficients), then its ghosts are filled. Evaluating eps level
246 // by level (rather than restricting from the fine level) gives the EXACT permittivity
247 // at each coarse resolution, which preserves order 2. Call once
248 // after construction, before solve. DO NOT call => uniform eps (historical path).
249 void set_epsilon(std::function<Real(Real, Real)> eps_fn) {
250 // 1 ghost (box-boundary neighbors read), ghosts filled (do_fill).
251 sample_per_level(&MGLevel::eps, eps_fn, 1, true, eps_bc());
252 has_eps_ = true;
253 }
254
255 // Overload taking an ALREADY-discretized eps field (1-component MultiFab, defined
256 // on the finest level grid). It is copied onto the fine level then
257 // RESTRICTED (average_down, 2x2 average) to the coarse levels, and its ghosts
258 // are filled at each level. Use it when eps comes from a per-cell field
259 // (not from an analytic formula): this is the entry point for System wiring.
260 void set_epsilon(const MultiFab& eps_fine) {
261 // copy on the fine + restriction to the coarse; 1 ghost, ghosts filled at each level.
262 restrict_and_fill(&MGLevel::eps, eps_fine, 1, true, eps_bc());
263 has_eps_ = true;
264 }
265
266 // Activates ANISOTROPIC permittivity: the operator goes from div(eps grad phi) (scalar
267 // eps) to div(diag(eps_x, eps_y) grad phi). Faces NORMAL TO X use eps_x,
268 // faces NORMAL TO Y use eps_y. eps_x is wired like the isotropic eps (sets
269 // the internal eps field, x faces) and eps_y a SECOND field (y faces). Same conventions
270 // as set_epsilon: CELL-CENTERED field, evaluated PER LEVEL (exact coarse permittivity,
271 // order 2 preserved) then ghosts filled. Use case: anisotropic medium/mesh.
272 // Giving eps_x_fn == eps_y_fn gives back the isotropic operator eps=eps_x. Composable with
273 // set_reaction (kappa). Call once after construction, before solve.
274 void set_epsilon_anisotropic(std::function<Real(Real, Real)> eps_x_fn,
275 std::function<Real(Real, Real)> eps_y_fn) {
276 set_epsilon(std::move(eps_x_fn)); // x faces: reuse the isotropic eps wiring
277 // y faces: second eps_y field, same convention (1 ghost, ghosts filled).
278 sample_per_level(&MGLevel::eps_y, eps_y_fn, 1, true, eps_bc());
279 has_eps_y_ = true;
280 }
281
282 // Overload taking two ALREADY-discretized fields (finest level grid), copied
283 // onto the fine level then RESTRICTED (average_down) to the coarse and ghosts filled,
284 // exactly like set_epsilon(const MultiFab&). Entry point for per-field wiring
285 // (e.g. from System). eps_x carries the x faces, eps_y the y faces.
286 void set_epsilon_anisotropic(const MultiFab& eps_x_fine, const MultiFab& eps_y_fine) {
287 set_epsilon(eps_x_fine); // x faces: reuse the isotropic eps wiring (+ restriction)
288 // y faces: second eps_y field, copy + restriction (1 ghost, ghosts filled at each level).
289 restrict_and_fill(&MGLevel::eps_y, eps_y_fine, 1, true, eps_bc());
290 has_eps_y_ = true;
291 }
292
293 // Activates the REACTION term kappa(x): the operator goes from div(eps grad phi) = f to
294 // div(eps grad phi) - kappa phi = f (SCREENED Poisson / Helmholtz; kappa = 1/lambda_D^2 for
295 // Debye screening). kappa >= 0 makes the operator more diagonally dominant (the multigrid
296 // converges at least as well). It is a PHYSICAL coefficient (unit 1/length^2), DIAGONAL:
297 // read at (i,j) only (no neighbor), so 0 ghost; restricted by average on the coarse
298 // levels (same physical value sampled). DO NOT call => kappa = 0 (Poisson, historical
299 // path strictly unchanged). Composable with set_epsilon (eps(x) and kappa(x) together).
300 // ADC-251: 0 ghost / no fill_ghosts is DELIBERATE (a reaction term is zeroth-order: kappa is never
301 // read at a neighbor, so its ghosts cannot be needed); filling them would be dead work. The
302 // invariant is locked by the VARYING-kappa MMS in tests/test_screened_poisson.cpp (cases D/E),
303 // which a future stencil reading kappa on its unfilled ghosts would break.
304 void set_reaction(std::function<Real(Real, Real)> kappa_fn) {
305 // kappa: DIAGONAL, read at (i,j) only -> 0 ghost and do_fill=false (NO fill_ghosts, historical).
306 // ebc is then unused (BCRec{} never read).
307 sample_per_level(&MGLevel::kappa, kappa_fn, 0, false, BCRec{});
308 has_kappa_ = true;
309 }
310
311 // Overload: ALREADY-discretized kappa field (1-component MultiFab, fine grid), copied onto the
312 // fine level then RESTRICTED (average_down) to the coarse. Entry point for System wiring
313 // (a per-cell kappa field).
314 void set_reaction(const MultiFab& kappa_fine) {
315 // kappa: DIAGONAL -> 0 ghost and do_fill=false (NO fill_ghosts, neither fine nor coarse, historical).
316 restrict_and_fill(&MGLevel::kappa, kappa_fine, 0, false, BCRec{});
317 has_kappa_ = true;
318 }
319
320 // Activates the OFF-DIAGONAL COEFFICIENTS of the FULL tensor A = [[eps_x, Axy], [Ayx, eps_y]]:
321 // the operator goes from div(diag(eps_x, eps_y) grad phi) to div(A grad phi), adding the CROSS
322 // fluxes d_x(Axy d_y phi) + d_y(Ayx d_x phi) (cf. poisson_operator.hpp). A may be NON
323 // symmetric (Axy != Ayx). Same conventions as set_epsilon: CELL-CENTERED fields, evaluated PER
324 // LEVEL (exact coarse coefficient) then ghosts filled (the face average reads the neighbor at
325 // i+-1 / j+-1). Composable with set_epsilon[_anisotropic] and set_reaction. Call once after
326 // construction, before solve. DO NOT call => DIAGONAL block (current path bit-identical).
327 // WARNING: for strongly non-symmetric A the 5-point GS V-cycle (smoother of the DIAGONAL
328 // block, EXPLICIT cross terms) may NOT converge; a Krylov would then be required.
329 void set_cross_terms(std::function<Real(Real, Real)> a_xy_fn,
330 std::function<Real(Real, Real)> a_yx_fn) {
331 const BCRec ebc = eps_bc();
332 for (auto& L : lev_) {
333 L.a_xy = MultiFab(L.ba, L.dm, 1, 1); // 1 ghost: the face average reads the boundary neighbor
334 L.a_yx = MultiFab(L.ba, L.dm, 1, 1);
335 const Geometry& g = L.geom;
336 for (int li = 0; li < L.a_xy.local_size(); ++li) {
337 Array4 fxy = L.a_xy.fab(li).array();
338 Array4 fyx = L.a_yx.fab(li).array();
339 const Box2D b = L.a_xy.box(li);
340 // host initialization (std::function not device-callable); unified memory before kernel
341 for (int j = b.lo[1]; j <= b.hi[1]; ++j)
342 for (int i = b.lo[0]; i <= b.hi[0]; ++i) {
343 const Real x = g.x_cell(i), y = g.y_cell(j);
344 fxy(i, j) = a_xy_fn(x, y);
345 fyx(i, j) = a_yx_fn(x, y);
346 }
347 }
348 fill_ghosts(L.a_xy, g.domain, ebc);
349 fill_ghosts(L.a_yx, g.domain, ebc);
350 }
351 has_cross_ = true;
352 }
353
354 // Overload taking two ALREADY-discretized fields (finest level grid), copied onto the
355 // fine level then RESTRICTED (average_down) to the coarse and ghosts filled, exactly like
356 // set_epsilon_anisotropic(const MultiFab&, const MultiFab&). Entry point for PER-CELL cross
357 // terms (e.g. A = I + c rho B^{-1} from Schur condensation, where rho varies in space, so
358 // a_xy/a_yx are not analytic formulas but fields). The cross coefficients only
359 // serve the residual / the FULL matvec (the GS smoother stays 5-point, diagonal block); their
360 // restriction to the coarse therefore only serves a possible MG residual on the full operator (the
361 // Krylov preconditioner is wired WITHOUT cross terms -> symmetric part). DO NOT call
362 // => DIAGONAL block (current path bit-identical).
363 void set_cross_terms(const MultiFab& a_xy_fine, const MultiFab& a_yx_fine) {
364 const BCRec ebc = eps_bc();
365 for (auto& L : lev_) {
366 L.a_xy = MultiFab(L.ba, L.dm, 1, 1);
367 L.a_yx = MultiFab(L.ba, L.dm, 1, 1);
368 }
369 for (int li = 0; li < lev_[0].a_xy.local_size(); ++li) {
370 Array4 fxy = lev_[0].a_xy.fab(li).array();
371 Array4 fyx = lev_[0].a_yx.fab(li).array();
372 const ConstArray4 sxy = a_xy_fine.fab(li).const_array();
373 const ConstArray4 syx = a_yx_fine.fab(li).const_array();
374 const Box2D b = lev_[0].a_xy.box(li);
377 }
378 fill_ghosts(lev_[0].a_xy, lev_[0].geom.domain, ebc);
379 fill_ghosts(lev_[0].a_yx, lev_[0].geom.domain, ebc);
380 for (int l = 1; l < num_levels(); ++l) {
381 average_down(lev_[l - 1].a_xy, lev_[l].a_xy, 2);
382 average_down(lev_[l - 1].a_yx, lev_[l].a_yx, 2);
383 fill_ghosts(lev_[l].a_xy, lev_[l].geom.domain, ebc);
384 fill_ghosts(lev_[l].a_yx, lev_[l].geom.domain, ebc);
385 }
386 has_cross_ = true;
387 }
388
389 void vcycle() { vcycle_rec(0, bc_); }
390 // ROMEO-ONLY (deferred): a Kokkos::Profiling::pushRegion("mg:vcycle")/popRegion() pair (with a
391 // Kokkos::fence() before popRegion) around this V-cycle would let Nsight attribute the GPU time on
392 // ROMEO. It is intentionally NOT added here: it needs a Kokkos include in a header the profiling
393 // design keeps Kokkos-free (chrono only, last_bottom_seconds()), and the host build (Serial-only
394 // conda Kokkos) gains nothing. Add it at the System/ProgramContext seam if Nsight attribution is
395 // wanted, not in this numerics header.
396
397 // V-cycles until the residual is under the mixed floor (or max_cycles). Returns the number
398 // of cycles performed. phi is kept between calls (warm start).
399 //
400 // MIXED relative/absolute stopping criterion (hypre/AMReX convention):
401 // residual <= max(rel_tol * r0, abs_tol)
402 // abs_tol is an ABSOLUTE floor on the residual norm (SAME units as current_residual(),
403 // so scaled to the problem by the caller who knows it: no magic constant
404 // is baked in here). Default 0 -> max(rel_tol*r0, 0) = rel_tol*r0, i.e. the historical
405 // relative criterion unchanged. The floor avoids over-solving an ALREADY converged state (tiny r0,
406 // typical of an OFF-STEP solve on an unchanged state): early-exit without cycling if r0 is below abs_tol.
407 int solve(Real rel_tol, int max_cycles, Real abs_tol = Real(0)) {
408 detail::mg_trace_mark("solve: before initial current_residual");
409 last_bottom_seconds_ = 0.0; // reset the per-solve bottom self-time (accumulated by vcycle_rec)
410 const Real r0 = current_residual();
411 detail::mg_trace_mark("solve: after initial current_residual");
412 if (r0 <= abs_tol) {
413 last_cycles_ = 0; // already under the floor (or zero); abs_tol=0 -> old test r0<=0
414 last_residual_ = r0;
415 return 0;
416 }
417 const Real stop =
418 (rel_tol * r0 > abs_tol) ? rel_tol * r0 : abs_tol; // max(rel_tol*r0, abs_tol)
419 for (int c = 1; c <= max_cycles; ++c) {
420 detail::mg_trace_mark("solve: before vcycle");
421 vcycle();
422 detail::mg_trace_mark("solve: after vcycle");
423 const Real r = current_residual();
424 if (r <= stop) {
425 last_cycles_ = c;
426 last_residual_ = r;
427 return c;
428 }
429 }
430 last_cycles_ = max_cycles;
431 last_residual_ = current_residual();
432 return max_cycles;
433 }
434
435 // EllipticSolver concept interface: solve() with no argument (default
436 // tolerance) and residual() (alias of current_residual). Lets couplers
437 // depend on the concept, not on GeometricMG directly. Propagates abs_tol_ (absolute
438 // floor, default 0 -> historical relative criterion unchanged) to the mixed criterion.
439 void solve() { solve(Real(1e-8), 50, abs_tol_); }
441
442 // ABSOLUTE floor on the residual used by the no-argument solve() (the EllipticSolver
443 // concept path, taken by the couplers / the runtime). Same units as residual().
444 // Default 0: the criterion stays purely relative (historical behavior bit-identical).
445 // Setting it > 0 (to a value scaled to the problem, e.g. eps * ||rhs||) makes the
446 // OFF-STEP solves on an already-converged state exit without cycling (initial residual under the floor).
447 void set_abs_tol(Real abs_tol) { abs_tol_ = abs_tol; }
448 Real abs_tol() const { return abs_tol_; }
449
450 // HARDENED solve for the embedded boundary at high resolution. On a fine grid, the geometric
451 // V-cycle sometimes diverges near the conducting wall: coarsening is
452 // NON-Galerkin and the circle mask is re-evaluated per level, so the coarse
453 // correction becomes inconsistent with the fine boundary and the nu1=nu2=2 smoothing no longer
454 // dominates it (cycle spectral radius > 1). The potential then diverges on each call (the
455 // warm start propagates the divergence from one step to the next), hence a nan in the field at high
456 // resolution (see docs/HERO_RUN_AMR.md). The divergence is ERRATIC in resolution
457 // (it depends on the alignment of the circle on the grid hierarchy).
458 //
459 // Strategy, BIT-IDENTICAL when the solver already converges (or stalls):
460 // 1. standard cycle at the current smoothing: EXACTLY the body of solve(rel_tol,
461 // max_cycles), so identical to the already-stable runs;
462 // 2. ONLY if the final residual EXCEEDS the initial residual (true divergence,
463 // ratio > 1; not a mere stagnation ratio < 1, which we keep as-is to
464 // stay bit-identical): we harden the smoothing LOCALLY to the solve (nu doubled,
465 // nu1_/nu2_ restored on return, the next steps restart at nominal smoothing) and
466 // RESTART COLD (phi=0, the warm start was carrying the diverged state), until convergence
467 // or nu saturation. More smoothing makes the V-cycle contractive (GS dominates the
468 // inconsistent coarse correction): cf. sweep, nu=2 diverges at nc=640, nu>=4
469 // converges. Any run stable today did NOT diverge (divergence -> nan -> not
470 // recorded), so phase 2 never fires for them: bit-identical.
471 int solve_robust(Real rel_tol, int max_cycles) {
472 const Real r0 = current_residual();
473 if (r0 <= Real(0))
474 return 0;
475 int total = 0;
476 for (int c = 1; c <= max_cycles; ++c) { // phase 1: EXACTLY the body of solve()
477 vcycle();
478 ++total;
479 if (current_residual() <= rel_tol * r0)
480 return total; // -> bit-identical to recorded runs
481 }
482 if (current_residual() <= r0)
483 return total; // stagnation (not divergence): keep as-is
484 // phase 2: V-cycle divergence at the embedded boundary. Smoothing hardening LOCAL to the solve
485 // (nu1_/nu2_ saved then RESTORED before each return): no permanent ratchet on the hot
486 // path, the overhead is paid ONLY by the solve that diverges; the next solves restart at
487 // nominal smoothing (reproducibility preserved, cost independent of history). Cold restart
488 // (phi=0, the warm start was carrying the diverged state). More smoothing makes the cycle contractive.
489 const int nu1_save = nu1_, nu2_save = nu2_;
490 while (nu1_ < 64 || nu2_ < 64) {
491 if (nu1_ < 64)
492 nu1_ *= 2;
493 if (nu2_ < 64)
494 nu2_ *= 2;
495 lev_[0].phi.set_val(Real(0));
496 for (int c = 1; c <= max_cycles; ++c) {
497 vcycle();
498 ++total;
499 if (current_residual() <= rel_tol * r0) {
500 nu1_ = nu1_save;
501 nu2_ = nu2_save;
502 return total;
503 }
504 }
505 }
506 nu1_ = nu1_save;
507 nu2_ = nu2_save;
508 return total; // best effort at maximal smoothing (residual already under r0: no divergence)
509 }
510
511 // Current residual (infinity norm) at the finest level. all_reduce_max MANDATORY for
512 // a DISTRIBUTED MULTI-BOX coarse: without it, norm_inf returns the LOCAL max (different per rank),
513 // so the V-cycle stopping criterion fires at different iterations depending on the rank
514 // -> different number of V-cycles (and fill_boundary calls) -> desynchronization of the
515 // MPI fluxes (MPI_ERR_TRUNCATE). Idempotent under replication (local max = global on each rank) and
516 // identity in serial -> bit-identical to the historical behavior.
518 detail::mg_trace_mark("current_residual: before poisson_residual");
519 poisson_residual(lev_[0].phi, lev_[0].rhs, lev_[0].geom, bc_, lev_[0].res, mask_ptr(0),
520 coef_ptr(0), eps_ptr(0), kappa_ptr(0), eps_y_ptr(0), a_xy_ptr(0), a_yx_ptr(0));
521 detail::mg_trace_mark("current_residual: after poisson_residual, before norm_inf");
522 const Real r = all_reduce_max(norm_inf(lev_[0].res));
523 detail::mg_trace_mark("current_residual: after norm_inf");
524 return r;
525 }
526
527 // ACCESS to the FINE-level (level 0) operator coefficient pointers and to the BC. Expose
528 // EXACTLY what current_residual() passes to poisson_residual: an external caller (the Krylov
529 // solver, which uses apply_laplacian as the matvec and needs a matvec CONSISTENT with the
530 // MG residual) thus reuses the same operator, without duplicating the eps/kappa/Axy field wiring.
531 // nullptr when the corresponding term is inactive (cf. the internal *_ptr). Additive: no existing
532 // path calls them, the default behavior is unchanged.
533 const MultiFab* op_mask() { return mask_ptr(0); }
534 const MultiFab* op_coef() { return coef_ptr(0); }
535 const MultiFab* op_eps() { return eps_ptr(0); }
536 const MultiFab* op_kappa() { return kappa_ptr(0); }
537 const MultiFab* op_eps_y() { return eps_y_ptr(0); }
538 const MultiFab* op_a_xy() { return a_xy_ptr(0); }
539 const MultiFab* op_a_yx() { return a_yx_ptr(0); }
540 const BCRec& bc() const { return bc_; }
541 const BoxArray& box_array() const { return lev_[0].ba; }
542 const DistributionMapping& dmap() const { return lev_[0].dm; }
543
544 private:
545 struct MGLevel {
546 Geometry geom;
547 BoxArray ba;
549 MultiFab phi, rhs, res, mask, coef, eps, kappa, eps_y, a_xy, a_yx;
550 // REUSED V-cycle buffers, allocated once by the constructor for the NON-bottom levels:
551 // corr = prolonged correction (level layout); cfine = "fine coarsened" grid shared by the
552 // restriction (average_down) and the prolongation (interpolate) of the level. The bottom leaves them empty
553 // (vcycle_rec returns before touching them, and its coarsen would be degenerate).
554 MultiFab corr, cfine;
555 };
556
557 const MultiFab* mask_ptr(int l) { return active_ ? &lev_[l].mask : nullptr; }
558 const MultiFab* coef_ptr(int l) { return cut_cell_ ? &lev_[l].coef : nullptr; }
559 const MultiFab* eps_ptr(int l) { return has_eps_ ? &lev_[l].eps : nullptr; }
560 const MultiFab* kappa_ptr(int l) { return has_kappa_ ? &lev_[l].kappa : nullptr; }
561 // eps_y absent => nullptr => isotropic operator (eps_y = eps_x) unchanged.
562 const MultiFab* eps_y_ptr(int l) { return has_eps_y_ ? &lev_[l].eps_y : nullptr; }
563 // cross terms absent => nullptr => DIAGONAL block (current path unchanged).
564 const MultiFab* a_xy_ptr(int l) { return has_cross_ ? &lev_[l].a_xy : nullptr; }
565 const MultiFab* a_yx_ptr(int l) { return has_cross_ ? &lev_[l].a_yx : nullptr; }
566
567 // BC used to fill the eps field ghosts: we keep the periodic but
568 // replace every physical boundary (Dirichlet or outflow of phi) by a
569 // zero-gradient extrapolation (eps_ghost = interior eps), which gives a
570 // face permittivity = eps at the boundary (face on the domain contour).
571 BCRec eps_bc() const {
572 auto fo = [](BCType t) { return t == BCType::Periodic ? t : BCType::Foextrap; };
573 BCRec b;
574 b.xlo = fo(bc_.xlo);
575 b.xhi = fo(bc_.xhi);
576 b.ylo = fo(bc_.ylo);
577 b.yhi = fo(bc_.yhi);
578 return b;
579 }
580
581 void add_level(const Geometry& g, const BoxArray& ba) {
582 DistributionMapping dm = replicated_
583 ? DistributionMapping(std::vector<int>(ba.size(), my_rank()))
584 : DistributionMapping(ba.size(), n_ranks());
585 lev_.push_back(MGLevel{g, ba, dm, MultiFab(ba, dm, 1, 1), MultiFab(ba, dm, 1, 0),
586 MultiFab(ba, dm, 1, 0), MultiFab{}, MultiFab{}, MultiFab{}, MultiFab{},
587 MultiFab{}, MultiFab{}, MultiFab{}, MultiFab{}, MultiFab{}});
588 }
589
590 // FACTORIZATION (operator coefficient wiring, COMMON part): a scalar field
591 // (eps, eps_y, kappa, ...) designated by a pointer-to-MGLevel-member MGLevel::*, either SAMPLED
592 // PER LEVEL from an analytic function (sample_per_level), or COPIED onto the fine level
593 // then RESTRICTED (average_down) to the coarse (restrict_and_fill). Both preserve EXACTLY
594 // the original inline bodies, including the DIFFERENCES between coefficients:
595 // - nghost: 1 for eps/eps_y (face neighbors read), 0 for kappa (diagonal, read at (i,j) only);
596 // - do_fill: eps/eps_y fill their ghosts (fill_ghosts); kappa DOES NOT FILL THEM
597 // (0 ghost, HISTORICAL omission kept unchanged -- NO fill_ghosts added here).
598
599 // Host PER-LEVEL sampling of a field from fn (std::function not device-callable): allocates
600 // MultiFab(L.ba, L.dm, 1, nghost) at each level, writes f(x_cell, y_cell) at the center, then ghosts
601 // (fill_ghosts with ebc) ONLY if do_fill. Body extracted word-for-word from set_epsilon(fn) etc.
602 void sample_per_level(MultiFab MGLevel::* field, const std::function<Real(Real, Real)>& fn,
603 int nghost, bool do_fill, const BCRec& ebc) {
604 for (auto& L : lev_) {
605 MultiFab& F = L.*field;
606 F = MultiFab(L.ba, L.dm, 1, nghost);
607 const Geometry& g = L.geom;
608 for (int li = 0; li < F.local_size(); ++li) {
609 Array4 e = F.fab(li).array();
610 const Box2D b = F.box(li);
611 // host initialization (std::function not device-callable)
612 for (int j = b.lo[1]; j <= b.hi[1]; ++j)
613 for (int i = b.lo[0]; i <= b.hi[0]; ++i)
614 e(i, j) = fn(g.x_cell(i), g.y_cell(j));
615 }
616 if (do_fill)
617 fill_ghosts(F, g.domain, ebc);
618 }
619 }
620
621 // Copy comp 0 of the fine field (already discretized) onto the fine level then RESTRICTION (average_down,
622 // 2x2 average) to the coarse: allocates MultiFab(L.ba, L.dm, 1, nghost) at each level, ghosts
623 // (fill_ghosts with ebc) of the fine level THEN of each coarse level after its average, ONLY
624 // if do_fill. Body extracted word-for-word from set_epsilon(const MultiFab&) / set_reaction(const MultiFab&).
625 void restrict_and_fill(MultiFab MGLevel::* field, const MultiFab& fine, int nghost, bool do_fill,
626 const BCRec& ebc) {
627 for (auto& L : lev_)
628 L.*field = MultiFab(L.ba, L.dm, 1, nghost);
629 for (int li = 0; li < (lev_[0].*field).local_size(); ++li) {
630 Array4 e = (lev_[0].*field).fab(li).array();
631 const ConstArray4 s = fine.fab(li).const_array();
632 const Box2D b = (lev_[0].*field).box(li);
633 for_each_cell(b, detail::CopyComp0Kernel{e, s});
634 }
635 if (do_fill)
636 fill_ghosts(lev_[0].*field, lev_[0].geom.domain, ebc);
637 for (int l = 1; l < num_levels(); ++l) {
638 average_down(lev_[l - 1].*field, lev_[l].*field, 2);
639 if (do_fill)
640 fill_ghosts(lev_[l].*field, lev_[l].geom.domain, ebc);
641 }
642 }
643
644 void vcycle_rec(int l, const BCRec& bc) {
645 MGLevel& L = lev_[l];
646 const MultiFab* mk = mask_ptr(l);
647 const MultiFab* ck = coef_ptr(l);
648 const MultiFab* ep = eps_ptr(l);
649 const MultiFab* kp = kappa_ptr(l);
650 const MultiFab* ey = eps_y_ptr(l); // nullptr => isotropic (eps_y = eps_x)
651 const MultiFab* axy = a_xy_ptr(l); // nullptr => diagonal block (no cross flux)
652 const MultiFab* ayx = a_yx_ptr(l);
653 // NB: gs_smooth stays 5-POINT (diagonal block). The cross terms are EXPLICIT: only the
654 // residual (poisson_residual) carries them. The GS smoother touches only the diagonal -> its diag stays
655 // dominant (kappa>=0, eps>0); the cross coupling is relegated to the residual, per the header
656 // convention. For symmetric-positive-definite A the V-cycle stays contractive; for strongly non-symmetric
657 // A, it may diverge (cf. set_cross_terms, reported observation).
658 if (l == 0)
659 detail::mg_trace_mark("vcycle_rec(0): before gs_smooth(nu1) [first GS kernel]");
660 gs_smooth(L.phi, L.rhs, L.geom, bc, nu1_, mk, ck, ep, kp, ey);
661 if (l == 0)
662 detail::mg_trace_mark("vcycle_rec(0): after gs_smooth(nu1)");
663
664 if (l + 1 == static_cast<int>(lev_.size())) {
665 // BOTTOM solve = long Gauss-Seidel smoothing on the coarsest grid. Self-time it (chrono only,
666 // no profiler dependency here) and accumulate into the per-solve last_bottom_seconds_ (reset at
667 // the top of solve()): the System reads it back to attribute the coarsest-grid cost (Spec 5
668 // sec.13.11.1, ADC-479). Host serial / per-rank; the device-fence for an exact GPU bottom time is
669 // deferred (counter stays an honest host-side measurement).
670 const auto bottom_t0 = std::chrono::steady_clock::now();
671 gs_smooth(L.phi, L.rhs, L.geom, bc, nbottom_, mk, ck, ep, kp, ey); // bottom solve
672 const auto bottom_t1 = std::chrono::steady_clock::now();
673 last_bottom_seconds_ += std::chrono::duration<double>(bottom_t1 - bottom_t0).count();
674 if (mk)
675 zero_conductor(L.phi, L.mask);
676 return;
677 }
678
679 poisson_residual(L.phi, L.rhs, L.geom, bc, L.res, mk, ck, ep, kp, ey, axy, ayx);
680 if (l == 0)
681 detail::mg_trace_mark("vcycle_rec(0): after poisson_residual");
682 MGLevel& C = lev_[l + 1];
683 average_down(L.res, C.rhs, 2, L.cfine); // residual restriction (cfine buffer reused)
684 if (l == 0)
685 detail::mg_trace_mark("vcycle_rec(0): after average_down");
686 C.phi.set_val(0.0);
687 vcycle_rec(l + 1, homogeneous(bc));
688 if (l == 0)
689 detail::mg_trace_mark("vcycle_rec(0): after coarse recursion");
690
691 interpolate(C.phi, L.corr, 2, L.cfine); // correction prolongation (corr/cfine buffers reused)
692 if (l == 0)
693 detail::mg_trace_mark("vcycle_rec(0): after interpolate");
694 saxpy(L.phi, Real(1), L.corr);
695 if (l == 0)
696 detail::mg_trace_mark("vcycle_rec(0): after saxpy");
697 if (mk)
698 zero_conductor(L.phi, L.mask); // re-pin the conductor
699 gs_smooth(L.phi, L.rhs, L.geom, bc, nu2_, mk, ck, ep, kp, ey);
700 if (l == 0)
701 detail::mg_trace_mark("vcycle_rec(0): after gs_smooth(nu2)");
702 }
703
704 BCRec bc_;
705 std::function<bool(Real, Real)> active_;
706 int nu1_, nu2_, nbottom_;
707 bool replicated_ = false;
708 bool cut_cell_ = false;
709 bool has_eps_ = false;
710 bool has_eps_y_ = false;
711 bool has_kappa_ = false;
712 bool has_cross_ = false; // off-diagonal Axy/Ayx coefficients (FULL tensor) active
713 Real abs_tol_ =
714 Real(0); // absolute floor of the no-argument solve() (0 = relative criterion only)
715 // PER-SOLVE PROFILING STATS (read back at the System field_solve seam, ADC-479 criteria 42/43).
716 // last_cycles_/last_residual_ are set by solve(); last_bottom_seconds_ is reset at the top of solve()
717 // and accumulated by vcycle_rec's bottom branch. 0 until the first solve (no cycle recorded yet).
718 int last_cycles_ = 0;
719 Real last_residual_ = Real(0);
720 double last_bottom_seconds_ = 0.0;
721 std::function<Real(Real, Real)> levelset_;
722 std::vector<MGLevel> lev_;
723};
724
725} // namespace pops
BoxArray: the set of boxes tiling a level (disjoint, covering).
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
ConstArray4 const_array() const
READ handle (POD device-copyable) over this Fab. Valid as long as the Fab lives.
Definition fab2d.hpp:96
Definition geometric_mg.hpp:79
int num_levels() const
Definition geometric_mg.hpp:224
int solve(Real rel_tol, int max_cycles, Real abs_tol=Real(0))
Definition geometric_mg.hpp:407
const DistributionMapping & dmap() const
Definition geometric_mg.hpp:542
void vcycle()
Definition geometric_mg.hpp:389
void set_epsilon(std::function< Real(Real, Real)> eps_fn)
Definition geometric_mg.hpp:249
MultiFab & rhs()
Definition geometric_mg.hpp:222
int last_cycles() const
Definition geometric_mg.hpp:238
const MultiFab * op_a_yx()
Definition geometric_mg.hpp:539
Real abs_tol() const
Definition geometric_mg.hpp:448
const MultiFab * op_coef()
Definition geometric_mg.hpp:534
MultiFab & phi()
Definition geometric_mg.hpp:221
void set_epsilon(const MultiFab &eps_fine)
Definition geometric_mg.hpp:260
void set_epsilon_anisotropic(const MultiFab &eps_x_fine, const MultiFab &eps_y_fine)
Definition geometric_mg.hpp:286
const MultiFab * op_kappa()
Definition geometric_mg.hpp:536
void set_reaction(std::function< Real(Real, Real)> kappa_fn)
Definition geometric_mg.hpp:304
void set_epsilon_anisotropic(std::function< Real(Real, Real)> eps_x_fn, std::function< Real(Real, Real)> eps_y_fn)
Definition geometric_mg.hpp:274
Real last_residual() const
Definition geometric_mg.hpp:239
int solve_robust(Real rel_tol, int max_cycles)
Definition geometric_mg.hpp:471
const MultiFab * op_a_xy()
Definition geometric_mg.hpp:538
void set_abs_tol(Real abs_tol)
Definition geometric_mg.hpp:447
const BoxArray & box_array() const
Definition geometric_mg.hpp:541
GeometricMG(const Geometry &geom, const BoxArray &ba, const BCRec &bc, std::function< bool(Real, Real)> active={}, bool replicated=false, int min_coarse=2, int nu1=2, int nu2=2, int nbottom=50, bool cut_cell=false, std::function< Real(Real, Real)> levelset={})
Definition geometric_mg.hpp:109
Real residual()
Definition geometric_mg.hpp:440
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
void set_reaction(const MultiFab &kappa_fine)
Definition geometric_mg.hpp:314
Real current_residual()
Definition geometric_mg.hpp:517
void set_cross_terms(std::function< Real(Real, Real)> a_xy_fn, std::function< Real(Real, Real)> a_yx_fn)
Definition geometric_mg.hpp:329
void set_cross_terms(const MultiFab &a_xy_fine, const MultiFab &a_yx_fine)
Definition geometric_mg.hpp:363
const MultiFab * op_eps_y()
Definition geometric_mg.hpp:537
void solve()
Definition geometric_mg.hpp:439
const MultiFab * op_eps()
Definition geometric_mg.hpp:535
double last_bottom_seconds() const
Definition geometric_mg.hpp:240
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
Parallel seam: minimal MPI abstraction (rank/size + collectives) with serial fallback.
SHARED cut-fraction primitive (cut-cell / embedded boundary).
DistributionMapping: maps each box (by global index) to its owning MPI rank.
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).
void mg_trace_mark(const char *w)
Definition geometric_mg.hpp:55
POPS_HD CutFraction cut_fraction(const LevelSet &ls, Real xc, Real yc, Real dx, Real dy)
Computes the cut geometry of an ACTIVE cell (center (xc, yc) with ls < 0) from a level-set ls evaluat...
Definition cut_fraction.hpp:76
POPS_HD ShortleyWellerWeights shortley_weller(const CutFraction &cf)
Definition cut_fraction.hpp:102
Definition amr_hierarchy.hpp:29
void average_down(const MultiFab &fine, MultiFab &coarse, int r, MultiFab &cfine)
CONSERVATIVE average fine -> coarse (ratio r): coarse(I, J) = average of the r^2 fine cells of the bl...
Definition refinement.hpp:181
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
void zero_conductor(MultiFab &phi, const MultiFab &mask)
Definition poisson_operator.hpp:410
int n_ranks()
Definition comm.hpp:139
BoxArray coarsen(const BoxArray &ba, int r)
Coarsens each box of the BoxArray by a ratio r (coarsen box by box, order preserved).
Definition refinement.hpp:39
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 interpolate(const MultiFab &coarse, MultiFab &fine, int r, MultiFab &cfine)
Interpolation coarse -> fine (ratio r) by piecewise-CONSTANT injection: each fine cell (including the...
Definition refinement.hpp:220
void gs_smooth(MultiFab &phi, const MultiFab &f, const Geometry &geom, const BCRec &bc, int nsweeps, const MultiFab *mask=nullptr, const MultiFab *coef=nullptr, const MultiFab *eps=nullptr, const MultiFab *kappa=nullptr, const MultiFab *eps_y=nullptr)
Definition poisson_operator.hpp:389
BCRec homogeneous(const BCRec &b)
Definition geometric_mg.hpp:73
int my_rank()
Definition comm.hpp:136
double all_reduce_max(double x)
Definition comm.hpp:146
void poisson_residual(MultiFab &phi, const MultiFab &f, const Geometry &geom, const BCRec &bc, MultiFab &res, const MultiFab *mask=nullptr, 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:271
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
Real norm_inf(const MultiFab &mf, int comp=0)
Infinity norm max |f(.,.,comp)| over the valid cells (LOCAL, without MPI all_reduce).
Definition mf_arith.hpp:123
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),...
AMR inter-level transfer operators (integer ratio r) + parallel_copy.
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 yhi
Definition physical_bc.hpp:31
Real ylo_val
Definition physical_bc.hpp:32
BCType xlo
Definition physical_bc.hpp:30
BCType ylo
Definition physical_bc.hpp:31
Real xlo_val
Definition physical_bc.hpp:32
Real yhi_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
int hi[2]
Definition box2d.hpp:39
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
Cartesian geometry of a level: index domain + physical bounds [xlo, xhi] x [ylo, yhi].
Definition geometry.hpp:20
POPS_HD Real x_cell(int i) const
Abscissa at the CENTER of cell index i (i = 0 -> xlo + dx/2; defined for negative i)....
Definition geometry.hpp:35
POPS_HD Real y_cell(int j) const
Ordinate at the CENTER of cell index j. POPS_HD.
Definition geometry.hpp:37
Box2D domain
Definition geometry.hpp:21
Definition geometric_mg.hpp:66
ConstArray4 s
Definition geometric_mg.hpp:68
POPS_HD void operator()(int i, int j) const
Definition geometric_mg.hpp:69
Array4 d
Definition geometric_mg.hpp:67
Base scalar types and the POPS_HD macro (host+device portability).
#define POPS_HD
Definition types.hpp:25