include/pops/runtime/system/system_field_solver.hpp Source FileΒΆ

adc_cpp: include/pops/runtime/system/system_field_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
system_field_solver.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <pops/core/state/state.hpp> // kAuxBaseComps (base component of the aux channel)
4#include <pops/core/foundation/types.hpp> // Real
5#include <pops/mesh/storage/multifab.hpp> // MultiFab, Array4, ConstArray4
6#include <pops/mesh/index/box2d.hpp> // Box2D
7#include <pops/mesh/execution/for_each.hpp> // device_fence
8#include <pops/mesh/boundary/physical_bc.hpp> // BCRec, fill_ghosts, fill_boundary
11#include <pops/numerics/elliptic/polar/polar_poisson_solver.hpp> // PolarPoissonSolver (direct polar Poisson)
12#include <pops/parallel/comm.hpp> // n_ranks() (FFT MPI guard)
13#include <pops/runtime/builders/block/block_builder_polar.hpp> // derive_aux_polar (polar aux in local basis)
14#include <pops/runtime/context/wall_predicate.hpp> // detail::wall_predicate
15
16#include <cstdio> // POPS_TRACE_SOLVE_FIELDS: device diagnostic trace (env-gated, inert by default)
17#include <cstdlib> // getenv
18#include <functional>
19#include <map> // named_aux_: NAMED aux fields (comp -> field), re-applied after channel realloc
20#include <optional>
21#include <stdexcept>
22#include <string>
23#include <type_traits>
24#include <variant>
25#include <vector>
26
58
59namespace pops {
60namespace field_solver {
61
66inline bool pops_trace_sf() {
67 static const bool on = std::getenv("POPS_TRACE_SOLVE_FIELDS") != nullptr;
68 return on;
69}
71inline void pops_sf_mark(const char* w) {
72 if (pops_trace_sf()) {
73 std::fprintf(stderr, "[sf] %s\n", w);
74 std::fflush(stderr);
75 }
76}
77
82template <class Impl>
84 public:
86 explicit SystemFieldSolver(Impl* owner) : owner_(owner) {}
87
89 static constexpr int kTeComp = kAuxBaseComps + 1; // = 4
90
97 bool provides_aux(const std::string& name) const {
98 if (name == "B_z") {
99 return !bz_field_.empty();
100 }
101 if (name == "T_e") {
102 return te_src_ >= 0;
103 }
104 return true;
105 }
106
107 // --- OWNED state (elliptic solve + coefficient fields + application buffers) --------
108 // Poisson configuration (elliptic solver built lazily).
109 std::string p_rhs = "charge_density";
110 std::string p_solver = "geometric_mg";
111 std::string p_bc = "auto";
112 std::string p_wall = "none";
113 double p_wall_radius = 0.0;
114 Real p_eps_ = 1; // CONSTANT permittivity: div(eps grad phi) = f <=> lap phi = f/eps
115 // ABSOLUTE floor of the GeometricMG V-cycle stopping criterion (same units as the residual). Default 0:
116 // purely relative criterion (historical bit-identical behavior). Set > 0 (problem scale),
117 // it makes the off-step solve_fields exit WITHOUT cycling on an already converged state (diagnostics, oracles,
118 // restart). Inert for the FFT solver (direct, without iterative tolerance).
120 bool has_eps_field_ = false; // VARIABLE permittivity eps(x) provided (carried by the operator)
121 std::vector<double> p_eps_field_; // field eps(x), n*n row-major (if has_eps_field_)
123 false; // ANISOTROPIC permittivity eps_x(x), eps_y(x) (operator div(diag(eps_x,eps_y) grad phi))
124 std::vector<double>
125 p_eps_x_field_; // field eps_x(x), n*n row-major (faces normal to x; if has_eps_xy_field_)
126 std::vector<double>
127 p_eps_y_field_; // field eps_y(x), n*n row-major (faces normal to y; if has_eps_xy_field_)
128 bool has_kappa_field_ = false; // REACTION term kappa(x) provided: div(eps grad phi) - kappa phi
129 std::vector<double> p_kappa_field_; // field kappa(x), n*n row-major (if has_kappa_field_)
130 // GAUSS POLICY (restart-free Gauss-evolution option). Default "restart":
131 // solve_fields re-solves -Delta phi = f (Gauss) on EVERY call -- historical behavior,
132 // BIT-IDENTICAL. "evolve": after the FIRST solve (phi^0), solve_fields NO LONGER re-solves the
133 // Poisson; it only DERIVES the aux (phi, grad phi) from the CURRENT phi -- the one that the condensed
134 // source stage (Schur) evolves IN-PLACE in ell_phi() (cf. run_source_stage). Thus it gives a
135 // restart-free evolution of -Delta phi (the Gauss constraint is only imposed at t=0).
136 // INERT without a Schur stage (phi would stay frozen after t=0). The lock gauss_solved_once_ guarantees that
137 // the first solve (the init of phi^0) always solves, whatever the policy.
138 bool gauss_evolve_ = false;
139 bool gauss_solved_once_ = false;
140 std::optional<std::variant<GeometricMG, PoissonFFTSolver, RemappedFFTSolver>> ell_;
141 // Direct POLAR Poisson solver (FFT-in-theta + tridiag-in-r), built lazily when
142 // polar_ (cf. ensure_elliptic_polar). SEPARATE from ell_ (geom() returns a PolarGeometry, not a
143 // Geometry): the cartesian path is never touched. INERT (nullopt) in cartesian.
144 std::optional<PolarPoissonSolver> pell_;
145 // phi buffer of the POLAR condensed SOURCE STAGE (Path A step 2c). The direct PolarPoissonSolver
146 // (pell_->phi()) is WITHOUT ghost (valid box = allocation); but the PolarCondensedSchurSourceStepper
147 // needs a phi WITH 1 ghost (fill_ghosts + apply_polar_tensor + centered grad + write of
148 // phi^{n+1}). So we pass it this dedicated buffer (1 ghost), fed with phi^n (= aux[0] after
149 // solve_fields_polar) before the source stage, which carries phi^{n+1} on output (warm start of the next
150 // step). In CARTESIAN this buffer is INERT (nullopt): ell_phi() routes to ell_->phi() as
151 // before, BIT-IDENTICAL.
152 std::optional<MultiFab> phi_src_polar_;
153 std::vector<Real> bz_field_; // field B_z(x) n*n row-major (empty if not provided)
154 int te_src_ = -1; // index of the fluid block source of T_e (-1 = none)
155 // NAMED aux fields (ADC-70 phase 1) provided by the user via System::set_aux_field: key =
156 // canonical component (>= kAuxNamedBase = 5), value = field n*n (cartesian) / nr*ntheta (polar)
157 // row-major. PERSISTENT like bz_field_: solve_fields touches ONLY components 0..2 (phi,
158 // grad) and 4 (T_e via apply_te), so components >= 5 survive from one step to the next; but a
159 // REALLOCATION of the aux channel (ensure_aux_width) starts again from a zeroed MultiFab -> we re-apply
160 // them then (apply_named_aux), exactly like apply_bz / apply_te.
161 std::map<int, std::vector<Real>> named_aux_;
162 // Per-field aux HALO policy (ADC-369): key = canonical component (>= kAuxNamedBase), value = the
163 // uniform boundary policy declared via pops.AuxHalo. Applied by apply_named_aux_bc() AFTER the shared
164 // aux ghost fill, overriding only that component's PHYSICAL-face ghosts (periodic faces -- Cartesian
165 // periodic, polar theta -- keep their wrap). Empty -> shared aux BC for every field, bit-identical.
166 std::map<int, AuxHaloPolicy> named_aux_bc_;
167
168 // NAMED multi-elliptic fields (ADC-428): a SECOND elliptic solve (beyond the default Poisson) for a
169 // user-named field m.elliptic_field("phi2", rhs=..., aux=[...]). Each named field owns:
170 // - phi_comp / gx_comp / gy_comp: the aux channel components (>= kAuxNamedBase) its solution and
171 // centered gradient are written to (the model declares them as aux_field slots; a source then
172 // reads them like any other named aux). gx_comp / gy_comp < 0 => the field declares fewer than 3
173 // aux slots and the gradient is not derived (only phi is written).
174 // - a DEDICATED elliptic solver instance (cartesian GeometricMG / FFT), built lazily the SAME way
175 // as the default ell_ (ensure_named_elliptic mirrors ensure_elliptic's poisson operator), so the
176 // native solver is REUSED, not reimplemented. The default Poisson path (ell_) is untouched.
177 // The RHS = sum over blocks of s.named_poisson_rhs[name] (the per-field brick, ADC-428), assembled
178 // exactly like assemble_poisson_rhs but reading the named closures.
179 struct NamedField {
180 int phi_comp = -1;
181 int gx_comp = -1;
182 int gy_comp = -1;
183 std::optional<std::variant<GeometricMG, PoissonFFTSolver, RemappedFFTSolver>> ell;
184 };
185 std::map<std::string, NamedField> named_fields_;
186
192 void register_named_field(const std::string& field, int phi_comp, int gx_comp, int gy_comp) {
193 NamedField nf;
194 nf.phi_comp = phi_comp;
195 nf.gx_comp = gx_comp;
196 nf.gy_comp = gy_comp;
197 named_fields_[field] = std::move(nf); // solver built lazily by ensure_named_elliptic
198 }
199
204 if (named_aux_bc_.empty())
205 return; // hot-path fast exit (parity with the AMR counterparts)
206 for (const auto& kv : named_aux_bc_) {
207 if (kv.first >= owner_->aux_ncomp_)
208 continue;
209 fill_physical_bc(owner_->aux, owner_->dom, aux_halo_override(owner_->bc_, kv.second),
210 kv.first);
211 }
212 }
213
217 void apply_bz() {
218 if (bz_field_.empty() || owner_->aux_ncomp_ <= kAuxBaseComps)
219 return;
220 // ROW WIDTH (fast axis i) of the row-major array bz_field_: n in cartesian (square n x n,
221 // BIT-IDENTICAL), nr in POLAR (ring nr x ntheta, i = r of size nr, cf. set_magnetic_field).
222 // The index stays flat[j * row + i]: in cartesian row == n (unchanged); in polar row == nr.
223 const int row = owner_->polar_ ? owner_->aux.box(0).nx() : owner_->cfg.n;
224 // LOCAL population on the owner rank (cf. solve_fields): iteration over the local fabs of the
225 // aux channel instead of fab(0) hardcoded (no-op on a rank without a local box at np>1, bit-identical to the
226 // owner).
227 for (int li = 0; li < owner_->aux.local_size(); ++li) {
228 Array4 a = owner_->aux.fab(li).array();
229 const Box2D v = owner_->aux.box(li);
230 for (int j = v.lo[1]; j <= v.hi[1]; ++j)
231 for (int i = v.lo[0]; i <= v.hi[0]; ++i)
232 a(i, j, kAuxBaseComps) = bz_field_[static_cast<std::size_t>(j) * row + i];
233 }
234 }
235
240 void apply_te() {
241 if (te_src_ < 0 || owner_->aux_ncomp_ <= kTeComp)
242 return;
243 const auto& s = owner_->sp[static_cast<std::size_t>(te_src_)];
244 const Real gm1 = Real(s.gamma) - Real(1);
245 // LOCAL population on the owner rank (cf. solve_fields): we iterate over the local fabs of the
246 // aux channel instead of fab(0) hardcoded (no-op on a rank without a local box at np>1, bit-identical to the
247 // owner). s.U and aux share the same DistributionMapping -> same local indexing.
248 for (int li = 0; li < owner_->aux.local_size(); ++li) {
249 const ConstArray4 us = s.U.fab(li).const_array();
250 Array4 a = owner_->aux.fab(li).array();
251 const Box2D v = owner_->aux.box(li);
252 for (int j = v.lo[1]; j <= v.hi[1]; ++j)
253 for (int i = v.lo[0]; i <= v.hi[0]; ++i) {
254 const Real rho = us(i, j, 0), mx = us(i, j, 1), my = us(i, j, 2), E = us(i, j, 3);
255 const Real p = gm1 * (E - Real(0.5) * (mx * mx + my * my) / rho);
256 a(i, j, kTeComp) = p / rho; // T = p / rho
257 }
258 }
259 }
260
267 void apply_named_aux_one(int comp, const std::vector<Real>& field) {
268 if (field.empty() || owner_->aux_ncomp_ <= comp)
269 return;
270 // ROW WIDTH (fast axis i): n in cartesian (square n x n), nr in polar (ring nr x
271 // ntheta). Index flat[j * row + i], identical to apply_bz / set_density.
272 const int row = owner_->polar_ ? owner_->aux.box(0).nx() : owner_->cfg.n;
273 for (int li = 0; li < owner_->aux.local_size(); ++li) {
274 Array4 a = owner_->aux.fab(li).array();
275 const Box2D v = owner_->aux.box(li);
276 for (int j = v.lo[1]; j <= v.hi[1]; ++j)
277 for (int i = v.lo[0]; i <= v.hi[0]; ++i)
278 a(i, j, comp) = field[static_cast<std::size_t>(j) * row + i];
279 }
280 }
281
285 for (const auto& kv : named_aux_)
286 apply_named_aux_one(kv.first, kv.second);
287 }
288
289 // --- elliptic solver (system Poisson) -----------------------------
293 std::string mode = p_bc;
294 if (mode == "auto")
295 mode = (p_wall == "circle" || !owner_->cfg.periodic) ? "dirichlet" : "periodic";
296 BCRec b;
297 if (mode == "periodic")
298 return b;
299 if (mode == "dirichlet") {
300 b.xlo = b.xhi = b.ylo = b.yhi = BCType::Dirichlet;
301 return b;
302 }
303 if (mode == "neumann") {
304 b.xlo = b.xhi = b.ylo = b.yhi = BCType::Foextrap;
305 return b;
306 }
307 throw std::runtime_error("System::set_poisson: unknown bc '" + mode + "'");
308 }
311 std::function<bool(Real, Real)> wall_active() {
312 return detail::wall_predicate(p_wall, p_wall_radius, owner_->cfg.L, "System::set_poisson");
313 }
320 if (ell_)
321 return;
322 // The system right-hand side is ALWAYS f = Sum_s elliptic_rhs_s(u_s), assembled by
323 // solve_fields from the elliptic brick of EACH block (charge q n, background alpha (n-n0),
324 // gravity coupling 4piG (rho-rho0)). The token is thus NOT a computation mode but a LABEL
325 // of this composite right-hand side. "composite" names this behavior honestly; "charge_density"
326 // stays the historical alias (default, bit-identical) since the usual case is a charge block.
327 if (p_rhs != "charge_density" && p_rhs != "composite")
328 throw std::runtime_error("System::set_poisson: unknown rhs '" + p_rhs +
329 "' (charge_density|composite; the right-hand side = sum of the "
330 "per-block elliptic bricks)");
331 const BCRec pbc = poisson_bc();
332 std::function<bool(Real, Real)> active = wall_active();
333 if (p_solver == "fft" || p_solver == "fft_spectral") {
334 // The FFT path is CONSTANT-coefficient and PERIODIC only: reject walls, variable / anisotropic
335 // permittivity and reaction kappa FIRST (each guard fires identically on all ranks -> no
336 // deadlock), so only the pure periodic constant-coefficient case reaches the layout switch below.
337 if (active)
338 throw std::runtime_error("System: solver '" + p_solver +
339 "' incompatible with a wall -> 'geometric_mg'");
340 if (has_eps_field_)
341 throw std::runtime_error("System: solver '" + p_solver +
342 "' has a CONSTANT coefficient, incompatible with a "
343 "variable eps(x) field -> use solver='geometric_mg'");
345 throw std::runtime_error(
346 "System: solver '" + p_solver +
347 "' has a CONSTANT coefficient, incompatible with an "
348 "ANISOTROPIC permittivity eps_x(x), eps_y(x) -> use solver='geometric_mg'");
350 throw std::runtime_error("System: solver '" + p_solver +
351 "' (pure Poisson) incompatible with a "
352 "reaction term kappa(x) -> use solver='geometric_mg'");
353 // 'fft_spectral': same plumbing, CONTINUOUS symbol -(kx^2+ky^2) (fidelity to the spectral
354 // references, e.g. poisson_fft.m of RIEMOM2D); 'fft' keeps the discrete stencil (bit-identical).
355 const bool spectral = (p_solver == "fft_spectral");
356 if (n_ranks() > 1) {
357 // ADC-287: distributed periodic FFT via a box<->slab remap. System distributes ONE box
358 // round-robin (DistributionMapping(1, n_ranks())), so a direct PoissonFFTSolver would
359 // dereference a nonexistent fab(0) on a rank without a box (SIGSEGV). RemappedFFTSolver presents
360 // the SAME single-box layout outward (rhs()/phi() on owner_->ba/dm, aligned with the aux) and
361 // hides a scatter/gather around PoissonFFT inside solve() -> the field-solve path is untouched.
362 // COLLECTIVE: every rank constructs the same type; its Ny % n_ranks() guard throws on all ranks
363 // (no deadlock). NOTE: pending the ADC-273 design vote (structural change to the ell_ variant).
364 ell_.emplace(std::in_place_type<RemappedFFTSolver>, owner_->geom, owner_->ba, pbc, active,
365 spectral);
366 } else {
367 // Single-rank: the proven direct FFT on the System box. PoissonFFTSolver keeps a hard guard in
368 // its constructor (rejects n_ranks()>1 / ba.size()!=1).
369 ell_.emplace(std::in_place_type<PoissonFFTSolver>, owner_->geom, owner_->ba, pbc, active,
370 spectral);
371 }
372 } else if (p_solver == "geometric_mg") {
373 ell_.emplace(std::in_place_type<GeometricMG>, owner_->geom, owner_->ba, pbc,
374 std::move(active));
375 std::get<GeometricMG>(*ell_).set_abs_tol(
376 p_abs_tol_); // absolute floor of the V-cycle (0 = relative only)
377 if (has_eps_field_)
378 apply_epsilon_field(); // operator div(eps grad phi) with variable eps(x)
380 apply_epsilon_anisotropic_field(); // div(diag(eps_x, eps_y) grad phi)
382 apply_reaction_field(); // term - kappa phi (screened Poisson / Helmholtz)
383 // Guard: with kappa and a CONSTANT permittivity eps != 1 (without an eps(x) field), the rhs
384 // would be scaled by 1/eps (shortcut lap phi = f/eps) -- inconsistent with the term -kappa phi.
385 // We then require eps = 1 or an eps(x) field (carried by the operator, without scaling).
387 throw std::runtime_error(
388 "System: reaction term kappa(x) + CONSTANT permittivity eps != 1 "
389 "unsupported; use eps = 1 or an eps(x) field (set_epsilon_field)");
390 } else {
391 throw std::runtime_error("System::set_poisson: unknown solver '" + p_solver +
392 "' (geometric_mg|fft|fft_spectral)");
393 }
394 }
399 GeometricMG& mg = std::get<GeometricMG>(*ell_);
400 MultiFab eps_fine(owner_->ba, owner_->dm, 1, 0);
401 const int n = owner_->cfg.n;
402 // Filling of the source field LOCAL to the owner rank (iteration over the local fabs, never
403 // fab(0) hardcoded): no-op on a rank without a local box at np>1, identical to before on the
404 // owner. mg.set_epsilon is then COLLECTIVE (local copy + MPI-safe restriction).
405 for (int li = 0; li < eps_fine.local_size(); ++li) {
406 Array4 e = eps_fine.fab(li).array();
407 const Box2D v = eps_fine.box(li);
408 for (int j = v.lo[1]; j <= v.hi[1]; ++j)
409 for (int i = v.lo[0]; i <= v.hi[0]; ++i)
410 e(i, j, 0) = static_cast<Real>(p_eps_field_[static_cast<std::size_t>(j) * n + i]);
411 }
412 mg.set_epsilon(
413 eps_fine); // copy on the fine level + restriction (average_down) to the coarse ones
414 }
420 GeometricMG& mg = std::get<GeometricMG>(*ell_);
421 MultiFab eps_x_fine(owner_->ba, owner_->dm, 1, 0), eps_y_fine(owner_->ba, owner_->dm, 1, 0);
422 const int n = owner_->cfg.n;
423 // LOCAL filling on the owner rank (cf. apply_epsilon_field): iteration over the local fabs
424 // (no-op on an empty rank at np>1). eps_x_fine and eps_y_fine share ba/dm -> same
425 // local indexing. mg.set_epsilon_anisotropic is then COLLECTIVE (copy + restriction).
426 for (int li = 0; li < eps_x_fine.local_size(); ++li) {
427 Array4 ex = eps_x_fine.fab(li).array();
428 Array4 ey = eps_y_fine.fab(li).array();
429 const Box2D v = eps_x_fine.box(li);
430 for (int j = v.lo[1]; j <= v.hi[1]; ++j)
431 for (int i = v.lo[0]; i <= v.hi[0]; ++i) {
432 const std::size_t k = static_cast<std::size_t>(j) * n + i;
433 ex(i, j, 0) = static_cast<Real>(p_eps_x_field_[k]);
434 ey(i, j, 0) = static_cast<Real>(p_eps_y_field_[k]);
435 }
436 }
437 mg.set_epsilon_anisotropic(eps_x_fine,
438 eps_y_fine); // faces x <- eps_x, faces y <- eps_y (+ restriction)
439 }
444 GeometricMG& mg = std::get<GeometricMG>(*ell_);
445 MultiFab kappa_fine(owner_->ba, owner_->dm, 1, 0);
446 const int n = owner_->cfg.n;
447 // LOCAL filling on the owner rank (cf. apply_epsilon_field): iteration over the local fabs
448 // (no-op on an empty rank at np>1). mg.set_reaction is then COLLECTIVE.
449 for (int li = 0; li < kappa_fine.local_size(); ++li) {
450 Array4 k = kappa_fine.fab(li).array();
451 const Box2D v = kappa_fine.box(li);
452 for (int j = v.lo[1]; j <= v.hi[1]; ++j)
453 for (int i = v.lo[0]; i <= v.hi[0]; ++i)
454 k(i, j, 0) = static_cast<Real>(p_kappa_field_[static_cast<std::size_t>(j) * n + i]);
455 }
456 mg.set_reaction(kappa_fine);
457 }
460 return std::visit([](auto& e) -> MultiFab& { return e.rhs(); }, *ell_);
461 }
469 if (owner_->polar_) {
470 // Allocate lazily (1 ghost) on the System layout, then copy phi^n from aux[0].
471 if (!phi_src_polar_)
472 phi_src_polar_.emplace(owner_->ba, owner_->dm, 1, 1);
473 for (int li = 0; li < phi_src_polar_->local_size(); ++li) {
474 const ConstArray4 a = owner_->aux.fab(li).const_array();
475 Array4 p = phi_src_polar_->fab(li).array();
476 const Box2D v = phi_src_polar_->box(li);
477 for (int j = v.lo[1]; j <= v.hi[1]; ++j)
478 for (int i = v.lo[0]; i <= v.hi[0]; ++i)
479 p(i, j, 0) = a(i, j, 0); // aux[0] = phi^n
480 }
481 return *phi_src_polar_;
482 }
483 return std::visit([](auto& e) -> MultiFab& { return e.phi(); }, *ell_);
484 }
487 void ell_solve() {
488 pops_sf_mark("ell_solve: before std::visit");
489 std::visit(
490 [](auto& e) {
491 using T = std::decay_t<decltype(e)>;
492 if (pops_trace_sf())
493 pops_sf_mark(std::is_same_v<T, GeometricMG> ? "ell_solve: GeometricMG::solve() start"
494 : "ell_solve: FFT solver::solve() start");
495 e.solve();
496 pops_sf_mark("ell_solve: solve() return");
497 },
498 *ell_);
499 pops_sf_mark("ell_solve: after std::visit");
500 }
501
502 // --- ELLIPTIC-SOLVER PROFILING COUNTERS (Spec 5 sec.13.11.1, ADC-479 criteria 42/43) ----------
503 // Read-back accessors for the per-solve stats of the ACTIVE cartesian elliptic solver, queried at
504 // the System solve_fields seam AFTER ell_solve()+device_fence() to populate the native profiler
505 // counters (mg_cycles / krylov_iters / mg_levels / elliptic_bottom). LOW-INVASIVE by design: the
506 // deep numerics never sees the profiler -- GeometricMG caches the stats per solve (chrono only) and
507 // we visit the variant here. Solvers that lack a notion (FFT: direct, no cycles/levels/iters)
508 // return 0 HONESTLY -- not every solver has these. nullopt ell_ (never solved) -> 0.
509 //
510 // krylov_iters is 0 on the cartesian field-solve path: the default Poisson uses GeometricMG (or a
511 // direct FFT), never a Krylov solver (the Krylov path lives in the condensed Schur SOURCE stage,
512 // which is not the ell_ elliptic solve). The counter is emitted for completeness / future Krylov
513 // elliptic backends; it stays an honest 0 here.
514 int last_mg_cycles() const {
515 if (!ell_)
516 return 0;
517 return std::visit(
518 [](const auto& e) -> int {
519 using T = std::decay_t<decltype(e)>;
520 if constexpr (std::is_same_v<T, GeometricMG>)
521 return e.last_cycles();
522 else
523 return 0; // direct FFT solver: no V-cycles
524 },
525 *ell_);
526 }
527 int last_krylov_iters() const {
528 return 0; // cartesian ell_ = GeometricMG / FFT, never a Krylov elliptic solver (see note above)
529 }
530 int last_num_levels() const {
531 if (!ell_)
532 return 0;
533 return std::visit(
534 [](const auto& e) -> int {
535 using T = std::decay_t<decltype(e)>;
536 if constexpr (std::is_same_v<T, GeometricMG>)
537 return e.num_levels();
538 else
539 return 0; // direct FFT solver: no multigrid hierarchy
540 },
541 *ell_);
542 }
543 double last_bottom_seconds() const {
544 if (!ell_)
545 return 0.0;
546 return std::visit(
547 [](const auto& e) -> double {
548 using T = std::decay_t<decltype(e)>;
549 if constexpr (std::is_same_v<T, GeometricMG>)
550 return e.last_bottom_seconds();
551 else
552 return 0.0; // direct FFT solver: no coarsest-grid bottom solve
553 },
554 *ell_);
555 }
556
557 // --- direct POLAR Poisson (PolarPoissonSolver) -------------------------
565 if (pell_)
566 return;
567 if (p_rhs != "charge_density" && p_rhs != "composite")
568 throw std::runtime_error("System::set_poisson (polar): unknown rhs '" + p_rhs +
569 "' (charge_density|composite)");
570 if (p_solver != "geometric_mg" && p_solver != "polar")
571 throw std::runtime_error(
572 "System::set_poisson (polar): solver '" + p_solver +
573 "' unsupported on a ring; the polar Poisson is direct (FFT-in-theta + tridiag-in-r). "
574 "Leave the default ('geometric_mg') or request 'polar'.");
576 throw std::runtime_error(
577 "System::set_poisson (polar): variable / anisotropic permittivity / reaction unsupported "
578 "by the direct polar Poisson (Phase 2b; operator (1/r) d_r(r d_r) + (1/r^2) d_theta^2)");
579 // MULTI-BOX GUARD (ADC-67): the DIRECT polar Poisson (FFT-in-theta + tridiag-in-r) requires
580 // complete theta ROWS and radial COLUMNS on ONE box (PolarPoissonSolver already rejects
581 // ba.size()!=1). We raise a clear UPSTREAM message HERE -- before the construction of the solver, so from the
582 // 1st solve_fields / potential / set_potential of a System with theta_boxes > 1 -- rather than letting
583 // the low-level rejection of PolarPoissonSolver bubble up. The theta SPLITTING (theta_boxes > 1) only serves
584 // the TRANSPORT; for a multi-box electrostatic field, go through the tensor Schur stage.
585 if (owner_->ba.size() != 1)
586 throw std::runtime_error(
587 "System: DIRECT polar Poisson incompatible with the theta splitting (theta_boxes=" +
588 std::to_string(owner_->ba.size()) +
589 " boxes); it requires a single-box grid (theta_boxes=1). For a multi-box theta "
590 "splitting, "
591 "use the polar tensor Schur stage (pops.Split + pops.CondensedSchur), multi-box, or "
592 "go back to theta_boxes=1.");
593 // Radial BC: Dirichlet/Neumann from poisson_bc() (xlo/xhi). theta always periodic.
594 const BCRec pbc = poisson_bc();
595 pell_.emplace(owner_->pgeom_, owner_->ba, pbc);
596 }
597
606 void assemble_poisson_rhs(MultiFab& rhs, int target_block = -1,
607 const MultiFab* U_stage = nullptr) {
608 rhs.set_val(Real(0));
609 for (std::size_t b = 0; b < owner_->sp.size(); ++b) {
610 auto& s = owner_->sp[b];
611 const bool override_here = (U_stage != nullptr && static_cast<int>(b) == target_block);
612 s.add_poisson_rhs(override_here ? *U_stage : s.U, rhs);
613 }
614 }
615
628 const std::vector<const MultiFab*>& U_stages) {
629 if (U_stages.size() != owner_->sp.size())
630 throw std::invalid_argument(
631 "assemble_poisson_rhs_from_blocks: U_stages size " + std::to_string(U_stages.size()) +
632 " != block count " + std::to_string(owner_->sp.size()) +
633 " (index U_stages by block index; nullptr = use the block's live state)");
634 rhs.set_val(Real(0));
635 for (std::size_t b = 0; b < owner_->sp.size(); ++b) {
636 auto& s = owner_->sp[b];
637 s.add_poisson_rhs(U_stages[b] != nullptr ? *U_stages[b] : s.U, rhs);
638 }
639 }
640
647 void solve_fields_polar(int target_block = -1, const MultiFab* U_stage = nullptr) {
649 MultiFab& rhs = pell_->rhs();
650 assemble_poisson_rhs(rhs, target_block, U_stage);
651 // CONSTANT permittivity eps != 1: lap phi = f/eps (1/eps scaling of the rhs), like the
652 // cartesian. (variable/aniso eps(x) is refused by ensure_elliptic_polar.)
653 if (p_eps_ != Real(1)) {
654 const Real inv = Real(1) / p_eps_;
655 for (int li = 0; li < rhs.local_size(); ++li) {
656 Array4 r = rhs.fab(li).array();
657 const Box2D v = rhs.box(li);
658 for (int j = v.lo[1]; j <= v.hi[1]; ++j)
659 for (int i = v.lo[0]; i <= v.hi[0]; ++i)
660 r(i, j, 0) *= inv;
661 }
662 }
663 pell_->solve();
664 device_fence();
665 // Derivation (phi, grad_r, grad_theta) in the local basis (e_r, e_theta) via the SAME helper as the C++
666 // test (derive_aux_polar of block_builder_polar.hpp). phi is WITHOUT ghost (direct single-box solver):
667 // the helper thus never reads an out-of-domain index (radial OFFSET at the walls, theta WRAPPED in
668 // periodic) -- that was the bug: the centered difference read phi(lo-1)/phi(hi+1)/phi(.,jlo-1) out of
669 // allocation -> spurious gradient -> divergent velocity -> nan.
670 derive_aux_polar(pell_->phi(), owner_->aux, owner_->pgeom_);
671 apply_te(); // inert in polar ExB (no fluid block source of T_e), kept by symmetry
672 // Aux ghosts: theta PERIODIC (joint 0/2pi), r PHYSICAL (extrapolation at the boundary). fill_ghosts
673 // already routes through bc_ (xlo/xhi Foextrap, ylo/yhi Periodic) -> correct periodic azimuthal halo.
674 fill_ghosts(owner_->aux, owner_->dom, owner_->bc_);
675 apply_named_aux_bc(); // ADC-369: per-field halo override on the RADIAL faces (theta stays periodic)
676 }
677
689 void solve_fields(int target_block = -1, const MultiFab* U_stage = nullptr) {
690 if (owner_->polar_)
691 // ring: polar Poisson + aux in local basis (e_r, e_theta)
692 return solve_fields_polar(target_block, U_stage);
693 pops_sf_mark("solve_fields: start");
695 pops_sf_mark("solve_fields: after ensure_elliptic");
696 // GAUSS POLICY: "restart" (default, gauss_evolve_==false) re-solves -Delta phi = f on
697 // EVERY call (bit-identical to history). "evolve": after the FIRST solve (phi^0), we SKIP
698 // the RHS assembly + the elliptic solve -> ell_phi() keeps the current phi (the one that the
699 // Schur source stage evolves in-place), giving a restart-free -Delta phi evolution (Gauss imposed only at t=0).
700 // The derivation of the aux (phi, grad phi) below ALWAYS runs, on the current phi.
702 MultiFab& rhs = ell_rhs();
703 // f = Sum_s elliptic_rhs_s(U_s). By default the CURRENT state of each block; with a
704 // target_block / U_stage override the target block reads U_stage (per-stage field solve, ADC-409).
705 assemble_poisson_rhs(rhs, target_block, U_stage);
706 pops_sf_mark("solve_fields: after add_poisson_rhs");
707 // CONSTANT permittivity: div(eps grad phi) = f <=> lap phi = f/eps, so we scale the rhs by
708 // 1/eps. With a VARIABLE or ANISOTROPIC eps(x) field we DO NOT do it: the GeometricMG
709 // operator carries eps directly (apply_epsilon_field / apply_epsilon_anisotropic_field), the
710 // rhs stays f as is.
711 if (!has_eps_field_ && !has_eps_xy_field_ && p_eps_ != Real(1)) {
712 const Real inv = Real(1) / p_eps_;
713 for (int li = 0; li < rhs.local_size(); ++li) {
714 Array4 r = rhs.fab(li).array();
715 const Box2D v = rhs.box(li);
716 for (int j = v.lo[1]; j <= v.hi[1]; ++j)
717 for (int i = v.lo[0]; i <= v.hi[0]; ++i)
718 r(i, j, 0) *= inv;
719 }
720 }
721 pops_sf_mark("solve_fields: before ell_solve");
722 ell_solve();
723 gauss_solved_once_ = true;
724 pops_sf_mark("solve_fields: after ell_solve, before device_fence");
725 }
726 device_fence();
727 pops_sf_mark("solve_fields: after device_fence (aux derivation)");
728 const Real dx = owner_->geom.dx(), dy = owner_->geom.dy();
729 // Per-cell derivation (phi, grad phi) -> aux channel: LOCAL to the owner rank. System
730 // distributes ONE box (round-robin DistributionMapping(1, n_ranks())), so at np>1 a single rank
731 // owns it; the others have local_size()==0 and HAVE NO fab to derive. We iterate over the LOCAL
732 // fabs (never fab(0) hardcoded): no-op on an empty rank, identical to before on the owner
733 // (loop executed once, bit-identical to np=1). ell_phi() and aux share the same
734 // DistributionMapping -> same local indexing.
735 MultiFab& phi_mf = ell_phi();
736 for (int li = 0; li < owner_->aux.local_size(); ++li) {
737 const ConstArray4 p = phi_mf.fab(li).const_array();
738 Array4 a = owner_->aux.fab(li).array();
739 const Box2D v = owner_->aux.box(li);
740 for (int j = v.lo[1]; j <= v.hi[1]; ++j)
741 for (int i = v.lo[0]; i <= v.hi[0]; ++i) {
742 a(i, j, 0) = p(i, j);
743 a(i, j, 1) = (p(i + 1, j) - p(i - 1, j)) / (2 * dx);
744 a(i, j, 2) = (p(i, j + 1) - p(i, j - 1)) / (2 * dy);
745 }
746 }
747 pops_sf_mark("solve_fields: after aux derivation (phi, grad phi)");
748 apply_te(); // T_e = p/rho of the fluid block source, recomputed on each solve (B_z, comp 3, preserved)
749 pops_sf_mark("solve_fields: after apply_te");
750 if (owner_->periodic_)
751 fill_boundary(owner_->aux, owner_->dom, owner_->per_);
752 else
753 fill_ghosts(owner_->aux, owner_->dom,
754 owner_->bc_); // extrapolation at the boundary (wall / free outflow)
755 apply_named_aux_bc(); // ADC-369: per-field halo override (after the shared fill; no-op if none)
756 pops_sf_mark("solve_fields: end (fill ghosts aux)");
757 }
758
767 void solve_fields_from_state(int block_idx, const MultiFab& U_stage) {
768 if (block_idx < 0 || block_idx >= static_cast<int>(owner_->sp.size()))
769 throw std::out_of_range("solve_fields_from_state: block index " + std::to_string(block_idx) +
770 " out of range (" + std::to_string(owner_->sp.size()) + " blocks)");
771 solve_fields(block_idx, &U_stage);
772 }
773
780 void solve_fields_polar_from_blocks(const std::vector<const MultiFab*>& U_stages) {
782 MultiFab& rhs = pell_->rhs();
784 if (p_eps_ != Real(1)) {
785 const Real inv = Real(1) / p_eps_;
786 for (int li = 0; li < rhs.local_size(); ++li) {
787 Array4 r = rhs.fab(li).array();
788 const Box2D v = rhs.box(li);
789 for (int j = v.lo[1]; j <= v.hi[1]; ++j)
790 for (int i = v.lo[0]; i <= v.hi[0]; ++i)
791 r(i, j, 0) *= inv;
792 }
793 }
794 pell_->solve();
795 device_fence();
796 derive_aux_polar(pell_->phi(), owner_->aux, owner_->pgeom_);
797 apply_te();
798 fill_ghosts(owner_->aux, owner_->dom, owner_->bc_);
800 }
801
813 void solve_fields_from_blocks(const std::vector<const MultiFab*>& U_stages) {
814 if (owner_->polar_)
815 return solve_fields_polar_from_blocks(U_stages);
817 // Coupled multi-block solve always re-solves the Poisson (it is requested explicitly with the
818 // stage states); it is NOT subject to the gauss_evolve_ skip (that policy is for the single
819 // default head solve). The aux derivation below runs on the freshly solved phi.
820 {
821 MultiFab& rhs = ell_rhs();
823 if (!has_eps_field_ && !has_eps_xy_field_ && p_eps_ != Real(1)) {
824 const Real inv = Real(1) / p_eps_;
825 for (int li = 0; li < rhs.local_size(); ++li) {
826 Array4 r = rhs.fab(li).array();
827 const Box2D v = rhs.box(li);
828 for (int j = v.lo[1]; j <= v.hi[1]; ++j)
829 for (int i = v.lo[0]; i <= v.hi[0]; ++i)
830 r(i, j, 0) *= inv;
831 }
832 }
833 ell_solve();
834 gauss_solved_once_ = true;
835 }
836 device_fence();
837 const Real dx = owner_->geom.dx(), dy = owner_->geom.dy();
838 MultiFab& phi_mf = ell_phi();
839 for (int li = 0; li < owner_->aux.local_size(); ++li) {
840 const ConstArray4 p = phi_mf.fab(li).const_array();
841 Array4 a = owner_->aux.fab(li).array();
842 const Box2D v = owner_->aux.box(li);
843 for (int j = v.lo[1]; j <= v.hi[1]; ++j)
844 for (int i = v.lo[0]; i <= v.hi[0]; ++i) {
845 a(i, j, 0) = p(i, j);
846 a(i, j, 1) = (p(i + 1, j) - p(i - 1, j)) / (2 * dx);
847 a(i, j, 2) = (p(i, j + 1) - p(i, j - 1)) / (2 * dy);
848 }
849 }
850 apply_te();
851 if (owner_->periodic_)
852 fill_boundary(owner_->aux, owner_->dom, owner_->per_);
853 else
854 fill_ghosts(owner_->aux, owner_->dom, owner_->bc_);
856 }
857
858 // --- NAMED multi-elliptic field (ADC-428) ----------------------------------
866 if (nf.ell)
867 return;
868 const BCRec pbc = poisson_bc();
869 std::function<bool(Real, Real)> active = wall_active();
870 if (p_solver == "fft" || p_solver == "fft_spectral") {
871 if (active)
872 throw std::runtime_error("System: named elliptic field solver '" + p_solver +
873 "' incompatible with a wall -> 'geometric_mg'");
874 const bool spectral = (p_solver == "fft_spectral");
875 if (n_ranks() > 1)
876 nf.ell.emplace(std::in_place_type<RemappedFFTSolver>, owner_->geom, owner_->ba, pbc, active,
877 spectral);
878 else
879 nf.ell.emplace(std::in_place_type<PoissonFFTSolver>, owner_->geom, owner_->ba, pbc, active,
880 spectral);
881 } else if (p_solver == "geometric_mg") {
882 nf.ell.emplace(std::in_place_type<GeometricMG>, owner_->geom, owner_->ba, pbc,
883 std::move(active));
884 std::get<GeometricMG>(*nf.ell).set_abs_tol(p_abs_tol_);
885 } else {
886 throw std::runtime_error("System: named elliptic field solver '" + p_solver +
887 "' unsupported (geometric_mg|fft|fft_spectral)");
888 }
889 }
890
896 void assemble_named_poisson_rhs(const std::string& field, MultiFab& rhs, int target_block,
897 const MultiFab* U_stage) {
898 rhs.set_val(Real(0));
899 bool any = false;
900 for (std::size_t b = 0; b < owner_->sp.size(); ++b) {
901 auto& s = owner_->sp[b];
902 auto it = s.named_poisson_rhs.find(field);
903 if (it == s.named_poisson_rhs.end() || !it->second)
904 continue;
905 const bool override_here = (U_stage != nullptr && static_cast<int>(b) == target_block);
906 it->second(override_here ? *U_stage : s.U, rhs);
907 any = true;
908 }
909 if (!any)
910 throw std::runtime_error("System: named elliptic field '" + field +
911 "' has no contributing block (declare m.elliptic_field on the block "
912 "model)");
913 }
914
922 void solve_named_field_from_state(const std::string& field, int block_idx,
923 const MultiFab& U_stage) {
924 if (block_idx < 0 || block_idx >= static_cast<int>(owner_->sp.size()))
925 throw std::out_of_range("solve_fields_from_state (named): block index " +
926 std::to_string(block_idx) + " out of range (" +
927 std::to_string(owner_->sp.size()) + " blocks)");
928 auto it = named_fields_.find(field);
929 if (it == named_fields_.end())
930 throw std::runtime_error("System: unknown named elliptic field '" + field +
931 "' (register it via m.elliptic_field + the compiled block)");
932 if (owner_->polar_)
933 throw std::runtime_error("System: named elliptic field '" + field +
934 "' on a polar (ring) grid is not supported yet (cartesian only)");
935 NamedField& nf = it->second;
936 if (nf.phi_comp < 0 || nf.phi_comp >= owner_->aux_ncomp_)
937 throw std::runtime_error(
938 "System: named elliptic field '" + field +
939 "' aux component out of the channel width (add the block that declares "
940 "its aux fields before solving)");
942 MultiFab& rhs = std::visit([](auto& e) -> MultiFab& { return e.rhs(); }, *nf.ell);
943 assemble_named_poisson_rhs(field, rhs, block_idx, &U_stage);
944 // CONSTANT permittivity eps != 1: lap phi = f/eps (1/eps scaling), like the default Poisson. The
945 // variable / anisotropic eps(x) and reaction kappa of the DEFAULT Poisson are NOT applied to a
946 // named field (plain Laplacian operator; see ensure_named_elliptic).
947 if (p_eps_ != Real(1)) {
948 const Real inv = Real(1) / p_eps_;
949 for (int li = 0; li < rhs.local_size(); ++li) {
950 Array4 r = rhs.fab(li).array();
951 const Box2D v = rhs.box(li);
952 for (int j = v.lo[1]; j <= v.hi[1]; ++j)
953 for (int i = v.lo[0]; i <= v.hi[0]; ++i)
954 r(i, j, 0) *= inv;
955 }
956 }
957 std::visit([](auto& e) { e.solve(); }, *nf.ell);
958 device_fence(); // CRITICAL: the V-cycle must finish before phi is read (same invariant as ell_)
959 MultiFab& phi_mf = std::visit([](auto& e) -> MultiFab& { return e.phi(); }, *nf.ell);
960 const Real dx = owner_->geom.dx(), dy = owner_->geom.dy();
961 const int cphi = nf.phi_comp, cgx = nf.gx_comp, cgy = nf.gy_comp;
962 const bool grad =
963 (cgx >= 0 && cgx < owner_->aux_ncomp_ && cgy >= 0 && cgy < owner_->aux_ncomp_);
964 for (int li = 0; li < owner_->aux.local_size(); ++li) {
965 const ConstArray4 p = phi_mf.fab(li).const_array();
966 Array4 a = owner_->aux.fab(li).array();
967 const Box2D v = owner_->aux.box(li);
968 for (int j = v.lo[1]; j <= v.hi[1]; ++j)
969 for (int i = v.lo[0]; i <= v.hi[0]; ++i) {
970 a(i, j, cphi) = p(i, j);
971 if (grad) {
972 a(i, j, cgx) = (p(i + 1, j) - p(i - 1, j)) / (2 * dx);
973 a(i, j, cgy) = (p(i, j + 1) - p(i, j - 1)) / (2 * dy);
974 }
975 }
976 }
977 // Ghost-fill the named field's aux components: the shared aux fill (same routing as solve_fields)
978 // then the per-field halo override (ADC-369). This re-fills ALL components -- the shared phi/grad
979 // were last written by the default solve_fields, so their valid cells are unchanged and only the
980 // halos are recomputed (idempotent for those components).
981 if (owner_->periodic_)
982 fill_boundary(owner_->aux, owner_->dom, owner_->per_);
983 else
984 fill_ghosts(owner_->aux, owner_->dom, owner_->bc_);
986 }
987
988 private:
989 Impl* owner_;
990};
991
992} // namespace field_solver
993} // namespace pops
POLAR counterpart of block_builder.hpp: builds a block's closures (time advance + residual + Poisson ...
Box2D: the integer index space of a 2D cell-centered Cartesian grid.
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
Definition geometric_mg.hpp:79
void set_epsilon(std::function< Real(Real, Real)> eps_fn)
Definition geometric_mg.hpp:249
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
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
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
SystemFieldSolver<Impl>: see contract above.
Definition system_field_solver.hpp:83
void solve_fields(int target_block=-1, const MultiFab *U_stage=nullptr)
Solves the system Poisson then DERIVES the aux = (phi, grad phi[, B_z, T_e]).
Definition system_field_solver.hpp:689
int te_src_
Definition system_field_solver.hpp:154
bool has_kappa_field_
Definition system_field_solver.hpp:128
BCRec poisson_bc()
Resolves the BC mode into a BCRec: "auto" -> dirichlet if wall/non-periodic, otherwise periodic; "per...
Definition system_field_solver.hpp:292
bool has_eps_xy_field_
Definition system_field_solver.hpp:122
std::vector< double > p_eps_x_field_
Definition system_field_solver.hpp:125
void solve_fields_from_blocks(const std::vector< const MultiFab * > &U_stages)
Coupled multi-block field solve (Spec 3 criterion 24, ADC-457): SAME elliptic solve + aux derivation ...
Definition system_field_solver.hpp:813
std::optional< std::variant< GeometricMG, PoissonFFTSolver, RemappedFFTSolver > > ell_
Definition system_field_solver.hpp:140
std::optional< MultiFab > phi_src_polar_
Definition system_field_solver.hpp:152
bool has_eps_field_
Definition system_field_solver.hpp:120
std::string p_wall
Definition system_field_solver.hpp:112
void solve_fields_from_state(int block_idx, const MultiFab &U_stage)
Per-stage field solve (ADC-409): SAME solve + derive-aux as solve_fields(), but the target block bloc...
Definition system_field_solver.hpp:767
double p_wall_radius
Definition system_field_solver.hpp:113
void apply_te()
Populates the T_e component (electron temperature) = p/rho of the fluid block source te_src_.
Definition system_field_solver.hpp:240
void apply_bz()
Populates the B_z component (index kAuxBaseComps) of the shared channel from bz_field_,...
Definition system_field_solver.hpp:217
MultiFab & ell_phi()
Potential phi read (and rewritten) by the condensed source stage.
Definition system_field_solver.hpp:468
std::vector< double > p_eps_y_field_
Definition system_field_solver.hpp:127
std::string p_bc
Definition system_field_solver.hpp:111
void ensure_elliptic_polar()
Builds the direct POLAR Poisson (PolarPoissonSolver, single-rank, single box covering the ring) LAZIL...
Definition system_field_solver.hpp:564
SystemFieldSolver(Impl *owner)
Definition system_field_solver.hpp:86
void ell_solve()
Solves the active cartesian Poisson (GeometricMG V-cycle or direct FFT).
Definition system_field_solver.hpp:487
void register_named_field(const std::string &field, int phi_comp, int gx_comp, int gy_comp)
Register a named elliptic field (ADC-428): records the aux output components (where the field's solve...
Definition system_field_solver.hpp:192
static constexpr int kTeComp
Canonical component of T_e (after phi/grad/B_z); cf. pops::Aux and AUX_CANONICAL on the DSL side.
Definition system_field_solver.hpp:89
void apply_named_aux_bc()
Re-applies the per-field aux HALO policies (ADC-369) onto the shared channel, AFTER the shared fill_g...
Definition system_field_solver.hpp:203
bool gauss_evolve_
Definition system_field_solver.hpp:138
Real p_abs_tol_
Definition system_field_solver.hpp:119
std::optional< PolarPoissonSolver > pell_
Definition system_field_solver.hpp:144
void apply_named_aux_one(int comp, const std::vector< Real > &field)
Populates ONE NAMED aux component (canonical index comp >= kAuxNamedBase) of the shared channel from ...
Definition system_field_solver.hpp:267
std::vector< Real > bz_field_
Definition system_field_solver.hpp:153
void solve_fields_polar_from_blocks(const std::vector< const MultiFab * > &U_stages)
POLAR coupled multi-block solve (Spec 3 criterion 24, ADC-457): same solve + aux derivation as solve_...
Definition system_field_solver.hpp:780
int last_mg_cycles() const
Definition system_field_solver.hpp:514
std::map< std::string, NamedField > named_fields_
Definition system_field_solver.hpp:185
std::string p_rhs
Definition system_field_solver.hpp:109
Real p_eps_
Definition system_field_solver.hpp:114
int last_num_levels() const
Definition system_field_solver.hpp:530
void apply_reaction_field()
Installs the reaction term kappa(x) (n*n row-major) on the GeometricMG: the operator becomes div(eps ...
Definition system_field_solver.hpp:443
void assemble_named_poisson_rhs(const std::string &field, MultiFab &rhs, int target_block, const MultiFab *U_stage)
Assembles the RIGHT-HAND SIDE of named field field into rhs: f = Sum_s named_poisson_rhs_s[field](U_s...
Definition system_field_solver.hpp:896
double last_bottom_seconds() const
Definition system_field_solver.hpp:543
void assemble_poisson_rhs_from_blocks(MultiFab &rhs, const std::vector< const MultiFab * > &U_stages)
Assembles the system Poisson RIGHT-HAND SIDE into rhs for a SIMULTANEOUS coupled multi-block solve (S...
Definition system_field_solver.hpp:627
void ensure_named_elliptic(NamedField &nf)
Builds the DEDICATED cartesian elliptic solver of named field nf, the SAME poisson operator as the de...
Definition system_field_solver.hpp:865
bool provides_aux(const std::string &name) const
True if the named aux field is provided at install time, for Spec-2 criterion 24 install-time require...
Definition system_field_solver.hpp:97
std::vector< double > p_kappa_field_
Definition system_field_solver.hpp:129
std::vector< double > p_eps_field_
Definition system_field_solver.hpp:121
std::map< int, AuxHaloPolicy > named_aux_bc_
Definition system_field_solver.hpp:166
void apply_epsilon_field()
Installs the eps(x) field (n*n row-major) on the GeometricMG: the operator becomes div(eps grad phi) ...
Definition system_field_solver.hpp:398
void apply_named_aux()
Re-applies ALL the stored named aux fields (cf.
Definition system_field_solver.hpp:284
bool gauss_solved_once_
Definition system_field_solver.hpp:139
std::map< int, std::vector< Real > > named_aux_
Definition system_field_solver.hpp:161
void apply_epsilon_anisotropic_field()
Installs the eps_x(x), eps_y(x) fields (n*n row-major each) on the GeometricMG: the operator becomes ...
Definition system_field_solver.hpp:419
void assemble_poisson_rhs(MultiFab &rhs, int target_block=-1, const MultiFab *U_stage=nullptr)
Assembles the system Poisson RIGHT-HAND SIDE into rhs: f = Sum_s elliptic_rhs_s(U_s),...
Definition system_field_solver.hpp:606
std::function< bool(Real, Real)> wall_active()
"Conductor interior" predicate from p_wall / p_wall_radius / cfg.L (cf.
Definition system_field_solver.hpp:311
void ensure_elliptic()
Builds the cartesian elliptic solver (ell_) LAZILY according to p_solver: GeometricMG (carries eps(x)...
Definition system_field_solver.hpp:319
int last_krylov_iters() const
Definition system_field_solver.hpp:527
std::string p_solver
Definition system_field_solver.hpp:110
void solve_named_field_from_state(const std::string &field, int block_idx, const MultiFab &U_stage)
Solves named field field's SECOND elliptic problem from block block_idx's stage state U_stage and wri...
Definition system_field_solver.hpp:922
void solve_fields_polar(int target_block=-1, const MultiFab *U_stage=nullptr)
POLAR solve_fields: assembles f = Sum_s elliptic_rhs_s(U_s) (host loop per cell), solves the polar Po...
Definition system_field_solver.hpp:647
MultiFab & ell_rhs()
Right-hand side f of the active cartesian elliptic solver (GeometricMG or FFT), via std::visit.
Definition system_field_solver.hpp:459
Parallel seam: minimal MPI abstraction (rank/size + collectives) with serial fallback.
for_each_cell and reductions: the parallelism SEAM over the cells of a Box2D; sync_host / sync_device...
GeometricMG: in-house geometric multigrid (V-cycle) for the elliptic operator, Gauss-Seidel smoother ...
MultiFab: a field DISTRIBUTED over a level (equivalent of AMReX's MultiFab).
std::function< bool(Real, Real)> wall_predicate(const std::string &wall, double wall_radius, double L, const std::string &err_context)
Builds the "inside the conductor" predicate (embedded wall for the Poisson solver) from the wall mode...
Definition wall_predicate.hpp:30
bool pops_trace_sf()
True if the DIAGNOSTIC trace of the solve_fields path is active (environment variable POPS_TRACE_SOLV...
Definition system_field_solver.hpp:66
void pops_sf_mark(const char *w)
Writes the marker w to stderr (with flush) ONLY if pops_trace_sf(); no-op otherwise.
Definition system_field_solver.hpp:71
Definition amr_hierarchy.hpp:29
constexpr int kAuxBaseComps
Definition state.hpp:147
double Real
Definition types.hpp:30
int n_ranks()
Definition comm.hpp:139
BCRec aux_halo_override(const BCRec &shared, const AuxHaloPolicy &p)
Builds the effective override BCRec for a per-field aux halo: starts from the SHARED aux BC shared (s...
Definition physical_bc.hpp:204
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 fill_boundary(MultiFab &mf, const Box2D &domain, Periodicity per={})
BLOCKING halo exchange: begin then end immediately (no overlap).
Definition fill_boundary.hpp:333
void fill_physical_bc(MultiFab &mf, const Box2D &domain, const BCRec &bc)
Fills the physical-face ghosts of ALL components per bc (historical entry point, bit-identical).
Definition physical_bc.hpp:176
void derive_aux_polar(const MultiFab &phi, MultiFab &aux, const PolarGeometry &g)
Derives the POLAR aux in the local basis (e_r, e_theta) from the potential phi resolved by PolarPoiss...
Definition block_builder_polar.hpp:231
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).
DIRECT EllipticSolver backends via spectral FFT (periodic BCs), MultiFab wrappers around PoissonFFT: ...
Direct POLAR Poisson solver on an annulus (PolarGeometry), Phase 2a.
Pointwise types of the physics layer: StateVec<N> (conserved state) and Aux (auxiliary fields from th...
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
BCType xlo
Definition physical_bc.hpp:30
BCType ylo
Definition physical_bc.hpp:31
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
Definition system_field_solver.hpp:179
int gy_comp
Definition system_field_solver.hpp:182
int phi_comp
Definition system_field_solver.hpp:180
std::optional< std::variant< GeometricMG, PoissonFFTSolver, RemappedFFTSolver > > ell
Definition system_field_solver.hpp:183
int gx_comp
Definition system_field_solver.hpp:181
Base scalar types and the POPS_HD macro (host+device portability).
ELLIPTIC conductor-wall predicate shared by the System and AmrSystem runtimes (the wall acts on the P...