include/pops/runtime/amr/amr_runtime.hpp Source FileΒΆ

adc_cpp: include/pops/runtime/amr/amr_runtime.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
amr_runtime.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <pops/amr/regridding/regrid.hpp> // tag_cells, grow_tags (per-block tags + phi for the union regrid)
4#include <pops/amr/tagging/tag_box.hpp> // TagBox, tag_union (cell-by-cell OR of the tags of all blocks)
5#include <pops/core/state/state.hpp> // kAuxBaseComps
6#include <pops/core/state/variables.hpp> // VariableSet, VariableRole, role_from_name (role -> component of coupled sources)
7#include <pops/coupling/amr/amr_coupler_mp.hpp> // detail::coupler_inject_aux_mb (aux injection coarse->fine)
8#include <pops/coupling/amr/amr_regrid_coupler.hpp> // regrid_compute_fine_layout + regrid_field_on_layout (split bricks)
9#include <pops/coupling/system/amr_system_coupler.hpp> // detail::same_layout_or_throw (shared-layout guard)
10#include <pops/coupling/base/aux_fill.hpp> // detail::derive_aux_bc (BC of the aux channel)
11#include <pops/coupling/source/coupled_source_program.hpp> // CoupledSourceKernel + CsProgram (flat ABI, P5 bytecode)
12#include <pops/numerics/elliptic/interface/elliptic_problem.hpp> // field_postprocess, FieldPostProcess
14#include <pops/numerics/time/amr/reflux/amr_reflux_mf.hpp> // AmrLevelMP, mf_average_down_mb
15#include <pops/numerics/time/integrators/implicit_stepper.hpp> // NewtonReport (OPT-IN IMEX diagnostics, aggregated per block)
18#include <pops/mesh/layout/patch_box.hpp> // PatchBox: index-space signature of a fine patch (patch_boxes())
24#include <pops/parallel/comm.hpp> // n_ranks() / comm_active(): MPI message+reduction counts (Spec 5 criterion 43)
25#include <pops/runtime/program/profiler.hpp> // Profiler / ProfileScope: AMR phase timings (Spec 5 criterion 43, ADC-479)
26
27#include <algorithm> // std::max (substeps/stride-aware CFL step)
28#include <chrono> // AmrPhaseScope wall-clock timing (Spec 5 criterion 43)
29#include <cmath> // std::isfinite (reject a degenerate dt)
30#include <cstddef>
31#include <functional>
32#include <limits> // std::numeric_limits (initial dt = +inf, min over the blocks)
33#include <map> // named_aux_: model-named aux fields (comp -> coarse field), re-applied each solve
34#include <memory>
35#include <stdexcept>
36#include <string>
37#include <utility>
38#include <vector>
39
90
91namespace pops {
92
99 std::string name;
100 int ncomp = 1;
101 double gamma = 1.4;
105 int substeps = 1;
115 int stride = 1;
120
128
133 std::shared_ptr<std::vector<AmrLevelMP>> levels;
134
141 std::function<void(std::vector<AmrLevelMP>&, const Box2D&, Real, Periodicity, bool)> advance;
142
149 bool imex = false;
150
168 std::function<void(std::vector<AmrLevelMP>&, const Box2D&, Real, Periodicity, bool)> imex_advance;
169
174 std::function<void(std::vector<AmrLevelMP>&)> project_per_level;
175
185 bool newton_diagnostics = false;
186 std::shared_ptr<NewtonReport> newton_report;
187
191 std::function<void(const MultiFab&, MultiFab&)> add_elliptic_rhs;
192
199 std::map<std::string, std::function<void(const MultiFab&, MultiFab&)>> named_elliptic_rhs;
200
204 std::function<Real(const MultiFab&, const MultiFab&)> max_speed;
205
212 std::function<Real(const MultiFab&, const MultiFab&)> source_frequency;
213 std::function<Real(const MultiFab&, const MultiFab&)> stability_dt;
214
216 std::function<Real()> mass;
217
219 std::function<std::vector<double>()> density;
220
223 std::function<std::vector<double>(const MultiFab&)> potential;
224};
225
230 public:
238 AmrRuntime(const Geometry& geom, const BoxArray& ba_coarse, const BCRec& bcPhi,
239 std::vector<AmrRuntimeBlock> blocks, Periodicity base_per = Periodicity{true, true},
240 bool replicated_coarse = true, std::function<bool(Real, Real)> active = {})
241 : geom_(geom),
242 dom_(geom.domain),
243 base_per_(base_per),
244 bcPhi_(bcPhi),
245 aux_bc_(detail::derive_aux_bc(bcPhi)),
246 replicated_coarse_(replicated_coarse),
247 mg_(geom, ba_coarse, bcPhi, active, replicated_coarse),
248 ba_coarse_(ba_coarse),
249 wall_active_(std::move(active)), // copy already consumed by mg_ (earlier in decl order)
250 blocks_(std::move(blocks)) {
251 if (blocks_.empty())
252 throw std::runtime_error("AmrRuntime : at least one block required");
253 for (const auto& b : blocks_)
254 if (!b.levels || b.levels->empty())
255 throw std::runtime_error(
256 "AmrRuntime : each block must carry at least one level "
257 "(coarse) on the shared layout");
258 nlev_ = static_cast<int>(blocks_[0].levels->size());
259
260 // EXACT layout consistency between blocks (the aux is shared per level): same number of levels,
261 // and per level same BoxArray (boxes AND order), same DistributionMapping, same dx/dy. SAME guard
262 // as AmrSystemCoupler (detail::same_layout_or_throw): all blocks live on ALL patches of the
263 // UNIQUE shared hierarchy. A single block matches itself trivially (the loop over the other blocks
264 // is empty).
265 {
266 std::vector<std::vector<AmrLevelMP>> ref;
267 ref.reserve(blocks_.size());
268 for (const auto& b : blocks_)
269 ref.push_back(*b.levels);
271 }
272
273 // Width of the SHARED aux channel: max of the blocks' aux_comps (>= kAuxBaseComps). Counterpart of
274 // AmrSystemCoupler::system_aux_comps: a block reading an extra field (B_z, T_e) has the room at
275 // each level, a base block ignores the extra components. PR1 does not POPULATE multi-block B_z (no
276 // bz_ here), but we size the channel to the widest anyway so that load_aux<aux_comps<Model>> never
277 // reads out of bounds. Without an extra-field block -> kAuxBaseComps (3) -> allocation strictly
278 // identical to the base case.
279 aux_ncomp_ = kAuxBaseComps;
280 for (const auto& b : blocks_)
281 if (b.aux_ncomp > aux_ncomp_)
282 aux_ncomp_ = b.aux_ncomp;
283
284 // SHARED aux: one MultiFab (phi, grad phi) per level, on the common grid. Sized once -> stable
285 // addresses for the blocks' aux pointers. The shared layout is that of block 0
286 // (same_layout_or_throw guard: identical for all).
287 aux_.resize(nlev_);
288 const auto& L0 = *blocks_[0].levels;
289 for (int k = 0; k < nlev_; ++k)
290 aux_[k] = MultiFab(L0[k].U.box_array(), L0[k].U.dmap(), aux_ncomp_, 1);
291 for (auto& b : blocks_)
292 for (int k = 0; k < nlev_; ++k)
293 (*b.levels)[k].aux = &aux_[k];
294
295 // Tag predicates of the union regrid: one empty slot per block (set_block_tag_predicate fills
296 // them). Empty by default -> no tag -> frozen hierarchy (regrid is not called anyway as long as
297 // set_regrid has not activated regrid_every_ > 0).
298 block_tag_.resize(blocks_.size());
299 }
300
301 int nlev() const { return nlev_; }
302 std::size_t n_blocks() const { return blocks_.size(); }
307 const VariableSet& block_cons_vars(std::size_t b) const {
308 if (b >= blocks_.size())
309 throw std::runtime_error("AmrRuntime::block_cons_vars : block index out of bounds");
310 return blocks_[b].cons_vars;
311 }
312 std::size_t n_coupled_sources() const { return coupled_sources_.size(); }
313 MultiFab& phi() { return mg_.phi(); }
314 // System Poisson right-hand side after the last solve_fields: f = Sum_b elliptic_rhs_b(U_b) on the
315 // shared coarse. Exposed to check the CO-LOCATED SUM (PR1 test); same grid as the coarse (the
316 // blocks' contributions are accumulated there at the same cells).
317 MultiFab& poisson_rhs() { return mg_.rhs(); }
318 const MultiFab& aux(int k) const { return aux_[k]; }
319 std::vector<AmrLevelMP>& levels(std::size_t b) { return *blocks_[b].levels; }
320 Real mass(std::size_t b) const { return blocks_[b].mass(); }
321 std::vector<double> density(std::size_t b) const { return blocks_[b].density(); }
322 int solve_count() const { return solve_count_; }
323 int regrid_count() const { return regrid_count_; }
324
330 using TagPredicate = std::function<bool(const ConstArray4&, int, int)>;
331
339 void set_regrid(int every, int grow = 2, int margin = 2) {
340 if (every < 0)
341 throw std::runtime_error("AmrRuntime::set_regrid : regrid_every >= 0");
342 regrid_every_ = every;
343 regrid_grow_ = grow;
344 regrid_margin_ = margin;
345 }
346
353 void set_block_tag_predicate(std::size_t b, TagPredicate crit) {
354 if (b >= blocks_.size())
355 throw std::runtime_error("AmrRuntime::set_block_tag_predicate : block index out of bounds");
356 block_tag_[b] = std::move(crit);
357 }
358
363 void set_phi_tag_predicate(TagPredicate crit) { phi_tag_ = std::move(crit); }
364
373 void set_named_aux(int comp, std::vector<Real> field) {
374 named_aux_[comp] = std::move(field);
375 if (!aux_.empty())
376 apply_named_aux(); // reflect immediately if the hierarchy already exists
377 }
378
384 void set_named_aux_bc(int comp, AuxHaloPolicy policy) { named_aux_bc_[comp] = policy; }
385
402 void register_named_field(const std::string& field, int phi_comp, int gx_comp, int gy_comp) {
403 NamedField nf;
404 nf.phi_comp = phi_comp;
405 nf.gx_comp = gx_comp;
406 nf.gy_comp = gy_comp;
407 named_fields_[field] = std::move(nf); // solver built lazily by ensure_named_elliptic
408 }
411 void set_block_named_elliptic_rhs(std::size_t b, const std::string& field,
412 std::function<void(const MultiFab&, MultiFab&)> rhs) {
413 if (b >= blocks_.size())
414 throw std::runtime_error(
415 "AmrRuntime::set_block_named_elliptic_rhs : block index out of bounds");
416 blocks_[b].named_elliptic_rhs[field] = std::move(rhs);
417 }
419 std::size_t n_named_fields() const { return named_fields_.size(); }
421 bool has_named_field(const std::string& field) const {
422 return named_fields_.find(field) != named_fields_.end();
423 }
428 std::vector<double> named_field_values(const std::string& field) {
429 auto it = named_fields_.find(field);
430 if (it == named_fields_.end())
431 throw std::runtime_error("AmrRuntime::named_field_values : unknown named elliptic field '" +
432 field + "' (register it via m.elliptic_field + the compiled block)");
433 solve_fields(); // up-to-date phi (counterpart of potential()); named solve runs inside
434 return coarse_aux_component(it->second.phi_comp);
435 }
437
458 void add_coupled_source(const std::vector<std::string>& in_blocks,
459 const std::vector<std::string>& in_roles,
460 const std::vector<double>& consts,
461 const std::vector<std::string>& out_blocks,
462 const std::vector<std::string>& out_roles,
463 const std::vector<int>& prog_ops, const std::vector<int>& prog_args,
464 const std::vector<int>& prog_lens) {
465 const int n_in = static_cast<int>(in_blocks.size());
466 const int n_const = static_cast<int>(consts.size());
467 const int n_terms = static_cast<int>(out_blocks.size());
468 // --- form validation (before any step, EXPLICIT errors); mirror of System::add_coupled_source.
469 if (n_terms == 0)
470 throw std::runtime_error(
471 "AmrRuntime::add_coupled_source : no source term (out_blocks empty)");
472 if (static_cast<int>(in_roles.size()) != n_in)
473 throw std::runtime_error(
474 "AmrRuntime::add_coupled_source : in_blocks / in_roles of different sizes");
475 if (static_cast<int>(out_roles.size()) != n_terms ||
476 static_cast<int>(prog_lens.size()) != n_terms)
477 throw std::runtime_error(
478 "AmrRuntime::add_coupled_source : out_blocks / out_roles / prog_lens of "
479 "different sizes");
480 if (prog_ops.size() != prog_args.size())
481 throw std::runtime_error(
482 "AmrRuntime::add_coupled_source : prog_ops / prog_args of different sizes");
483 if (n_in + n_const > kCsMaxReg)
484 throw std::runtime_error(
485 "AmrRuntime::add_coupled_source : too many registers (inputs + constants > " +
486 std::to_string(kCsMaxReg) + ")");
487 if (n_terms > kCsMaxTerms)
488 throw std::runtime_error("AmrRuntime::add_coupled_source : too many source terms (> " +
489 std::to_string(kCsMaxTerms) + ")");
490 // Resolves (block, role) -> (block index, component) by the block CONSERVATIVE descriptor, like
491 // System (#181). An unknown block throws immediately; an unknown (non-canonical) role too.
492 auto resolve = [&](const std::string& block, const std::string& role) -> std::pair<int, int> {
493 const int b = block_index(block);
494 if (b < 0)
495 throw std::runtime_error("AmrRuntime::add_coupled_source : no block named '" + block + "'");
496 // STRICT (no silent fallback; mirror of System::add_coupled_source #181): a DSL coupled source
497 // targets a (block, role) EXPLICITLY requested by the user. The role is addressed BY NAME: a
498 // canonical role name OR a user-defined role label (index_of(string), ADC-292). If the block does
499 // NOT expose this role, a fallback to component 0 would apply the source to the wrong field
500 // SILENTLY (the false-positive identified at the Lot E review). We throw, listing what the block
501 // exposes.
502 const VariableSet& vs = blocks_[static_cast<std::size_t>(b)].cons_vars;
503 const int comp = vs.index_of(role);
504 if (comp < 0)
505 throw std::runtime_error(
506 "AmrRuntime::add_coupled_source : block '" + block + "' does not expose role '" + role +
507 "' (roles: " + (vs.roles.empty() ? std::string("<none>") : roles_csv(vs)) +
508 ", no silent fallback to component 0)");
509 return {b, comp};
510 };
511 // Inputs: (block, component) read per cell. Captured by INDEX -> we rebuild the Array4 at EACH
512 // application (the fabs live in the level stack, repointed per level in the splitting).
513 std::vector<CsRef> ins(static_cast<std::size_t>(n_in));
514 for (int c = 0; c < n_in; ++c) {
515 auto [b, comp] =
516 resolve(in_blocks[static_cast<std::size_t>(c)], in_roles[static_cast<std::size_t>(c)]);
517 ins[static_cast<std::size_t>(c)] = {b, comp, CsProgram{}};
518 }
519 std::vector<CsRef> outs(static_cast<std::size_t>(n_terms));
520 int off = 0;
521 for (int t = 0; t < n_terms; ++t) {
522 auto [b, comp] =
523 resolve(out_blocks[static_cast<std::size_t>(t)], out_roles[static_cast<std::size_t>(t)]);
524 const int len = prog_lens[static_cast<std::size_t>(t)];
525 if (len < 0 || len > kCsMaxProg)
526 throw std::runtime_error("AmrRuntime::add_coupled_source : program of term " +
527 std::to_string(t) + " too long (> " + std::to_string(kCsMaxProg) +
528 ")");
529 if (off + len > static_cast<int>(prog_ops.size()))
530 throw std::runtime_error(
531 "AmrRuntime::add_coupled_source : prog_lens inconsistent with prog_ops");
532 CsProgram pg;
533 pg.len = len;
534 for (int k = 0; k < len; ++k) {
535 const int opc = prog_ops[static_cast<std::size_t>(off + k)];
536 const int a = prog_args[static_cast<std::size_t>(off + k)];
537 if (opc < 0 || opc > static_cast<int>(CsOp::Sqrt))
538 throw std::runtime_error("AmrRuntime::add_coupled_source : invalid opcode");
539 if (opc == static_cast<int>(CsOp::PushReg) && (a < 0 || a >= n_in + n_const))
540 throw std::runtime_error(
541 "AmrRuntime::add_coupled_source : register out of bounds in the program");
542 pg.op[k] = opc;
543 pg.arg[k] = a;
544 }
545 outs[static_cast<std::size_t>(t)] = {b, comp, pg};
546 off += len;
547 }
548 std::vector<Real> kconsts(consts.begin(), consts.end());
549 coupled_sources_.push_back(CoupledSourceSpec{std::move(ins), std::move(outs),
550 std::move(kconsts), n_in, n_const, n_terms});
551 }
552
572 if (coupled_sources_.empty())
573 return; // opt-in: no source -> bit-identical path
574 solve_fields(); // aux per level up to date (a term may read phi/grad via a future input)
575 for (const auto& cs : coupled_sources_) {
576 // PER-LEVEL application: at each level k, the blocks share EXACTLY the same layout
577 // (same_layout_or_throw guard), so same local_size() and same local indexing -> we iterate in
578 // parallel over the local fabs. local_size()==0 on a rank without a box -> empty loop (MPI-safe).
579 for (int k = 0; k < nlev_; ++k) {
580 const int sref = cs.n_in > 0 ? cs.ins[0].block : cs.outs[0].block;
581 MultiFab& Uref = (*blocks_[static_cast<std::size_t>(sref)].levels)[k].U;
582 for (int li = 0; li < Uref.local_size(); ++li) {
584 kern.dt = dt;
585 kern.n_in = cs.n_in;
586 kern.n_const = cs.n_const;
587 kern.n_terms = cs.n_terms;
588 for (int c = 0; c < cs.n_in; ++c) {
589 kern.in[c] =
590 (*blocks_[static_cast<std::size_t>(cs.ins[static_cast<std::size_t>(c)].block)]
591 .levels)[k]
592 .U.fab(li)
593 .array();
594 kern.in_comp[c] = cs.ins[static_cast<std::size_t>(c)].comp;
595 }
596 for (int c = 0; c < cs.n_const; ++c)
597 kern.consts[c] = cs.kconsts[static_cast<std::size_t>(c)];
598 for (int t = 0; t < cs.n_terms; ++t) {
599 kern.out[t] =
600 (*blocks_[static_cast<std::size_t>(cs.outs[static_cast<std::size_t>(t)].block)]
601 .levels)[k]
602 .U.fab(li)
603 .array();
604 kern.out_comp[t] = cs.outs[static_cast<std::size_t>(t)].comp;
605 kern.prog[t] = cs.outs[static_cast<std::size_t>(t)].prog;
606 }
607 for_each_cell(Uref.box(li),
608 kern); // NAMED functor (device-clean), additive forward-Euler
609 }
610 }
611 // Restore the consistency of the covered coarse cells (cf. COVERAGE INVARIANT above).
612 for (auto& b : blocks_)
613 for (int k = nlev_ - 1; k >= 1; --k)
614 mf_average_down_mb((*b.levels)[k].U, (*b.levels)[k - 1].U);
615 }
616 }
617
623 ++solve_count_;
624 // 1. average_down per block (fine -> coarse) over the whole hierarchy. AMR PROFILING (Spec 5
625 // criterion 43): time the restriction cascade into the "average_down" scope + bump its per-solve
626 // count. The scope is per-solve_fields (NOT per-cell), so a profiled run pays one clock pair here;
627 // an unprofiled run constructs nothing (profiler_ null or disabled). See profile_amr_scope below.
628 {
629 auto _ad = profile_amr_scope("average_down");
630 if (profiler_ != nullptr)
631 profiler_->count("average_down");
632 for (auto& b : blocks_) {
633 auto& L = *b.levels;
634 for (int k = nlev_ - 1; k >= 1; --k)
635 mf_average_down_mb(L[k].U, L[k - 1].U);
636 }
637 }
638
639 // 2. SUMMED and CO-LOCATED system RHS: f = Sum_b elliptic_rhs_b(U_b) on the coarse. We reset to
640 // zero then each block ACCUMULATES (+=) its contribution on the SAME cells of the shared coarse
641 // (mg_.rhs() shares the coarse layout).
642 mg_.rhs().set_val(Real(0));
643 for (auto& b : blocks_)
644 b.add_elliptic_rhs((*b.levels)[0].U, mg_.rhs());
645 mg_.solve();
646
647 // 3. coarse aux = (phi, grad phi) via the SAME clean path as AmrSystemCoupler: fill the ghosts of
648 // phi according to bcPhi_, field_postprocess (phi + grad), fill the ghosts of aux according to
649 // aux_bc_ (derived from bcPhi_). Handles the non-periodic case (Foextrap).
650 fill_ghosts_profiled(mg_.phi(), dom_, bcPhi_);
651 const Real cx = Real(1) / (2 * geom_.dx()), cy = Real(1) / (2 * geom_.dy());
652 field_postprocess(mg_.phi(), aux_[0], cx, cy,
653 FieldPostProcess{FieldPostProcess::GradSign::Plus, true});
654 // 3b. model-NAMED aux (ADC-291): re-apply the static named fields onto the coarse valid cells
655 // BEFORE fill_ghosts (so their ghosts are filled) and the injection (so they reach every level).
656 // No-op when no named field was set; field_postprocess wrote only comps 0..2, so this never clobbers
657 // phi/grad. This is what makes named aux survive a regrid (regrid re-solves -> re-applies).
658 apply_named_aux();
659 fill_ghosts_profiled(aux_[0], dom_, aux_bc_);
660 apply_named_aux_bc(); // ADC-369: per-field halo override on the coarse physical ghosts (after the
661 // shared fill, before injection); no-op when no policy declared.
662 // 4. coarse->fine injection of the aux (parent replicated only at level 1 if coarse replicated).
663 for (int k = 1; k < nlev_; ++k)
664 detail::coupler_inject_aux_mb(aux_[k - 1], aux_[k],
665 /*replicated_parent=*/(k == 1) && replicated_coarse_);
666
667 // 5. NAMED multi-elliptic fields (ADC-428): a SECOND elliptic solve per user-named field, written to
668 // the field's OWN aux components and injected to the fine levels. No-op when none is registered ->
669 // the default-Poisson trajectory above is strictly bit-identical.
671 }
672
679 if (named_fields_.empty())
680 return;
681 const Real dx = geom_.dx(), dy = geom_.dy();
682 for (auto& [field, nf] : named_fields_) {
683 if (nf.phi_comp < 0 || nf.phi_comp >= aux_ncomp_)
684 throw std::runtime_error("AmrRuntime : named elliptic field '" + field +
685 "' aux component out of the channel width (add the block that "
686 "declares its aux fields)");
687 ensure_named_elliptic(nf);
688 // SUMMED + CO-LOCATED RHS on the coarse: f = Sum_b named_elliptic_rhs_b[field](U_b), exactly like
689 // the default Poisson (mg_.rhs()), but reading the per-field block closures. A field with no
690 // contributing block solves a zero RHS -> reject loud (mirror of the System named path).
691 MultiFab& rhs = nf.mg->rhs();
692 rhs.set_val(Real(0));
693 bool any = false;
694 for (auto& b : blocks_) {
695 auto it = b.named_elliptic_rhs.find(field);
696 if (it == b.named_elliptic_rhs.end() || !it->second)
697 continue;
698 it->second((*b.levels)[0].U, rhs);
699 any = true;
700 }
701 if (!any)
702 throw std::runtime_error(
703 "AmrRuntime : named elliptic field '" + field +
704 "' has no contributing block (declare m.elliptic_field on the block model)");
705 nf.mg->solve();
706 device_fence(); // CRITICAL: the V-cycle must finish before phi is read (same invariant as mg_)
707 // Write phi (+ centered grad) into the field's OWN aux components on the coarse valid cells. The
708 // default field_postprocess hardcodes comps 0..2, so we write the named comps with a dedicated
709 // loop (mirror of SystemFieldSolver::solve_named_field_from_state). Per-local-fab (MPI-safe).
710 MultiFab& phi_mf = nf.mg->phi();
711 const int cphi = nf.phi_comp, cgx = nf.gx_comp, cgy = nf.gy_comp;
712 const bool grad = (cgx >= 0 && cgx < aux_ncomp_ && cgy >= 0 && cgy < aux_ncomp_);
713 for (int li = 0; li < aux_[0].local_size(); ++li) {
714 const ConstArray4 p = phi_mf.fab(li).const_array();
715 Array4 a = aux_[0].fab(li).array();
716 const Box2D v = aux_[0].box(li);
717 for (int j = v.lo[1]; j <= v.hi[1]; ++j)
718 for (int i = v.lo[0]; i <= v.hi[0]; ++i) {
719 a(i, j, cphi) = p(i, j);
720 if (grad) {
721 a(i, j, cgx) = (p(i + 1, j) - p(i - 1, j)) / (2 * dx);
722 a(i, j, cgy) = (p(i, j + 1) - p(i, j - 1)) / (2 * dy);
723 }
724 }
725 }
726 }
727 // Ghost-fill the named components (shared aux fill: same routing as the default) + per-field halo
728 // override (ADC-369), then inject coarse->fine so the named field reaches every level. We re-fill the
729 // WHOLE aux: the default comps 0..2 were just written by the Poisson block, so their valid cells are
730 // unchanged -- only ghosts are refreshed (idempotent). Cheap (one extra fill per solve_fields when a
731 // named field exists; none otherwise).
732 fill_ghosts_profiled(aux_[0], dom_, aux_bc_);
733 apply_named_aux_bc();
734 for (int k = 1; k < nlev_; ++k)
735 detail::coupler_inject_aux_mb(aux_[k - 1], aux_[k],
736 /*replicated_parent=*/(k == 1) && replicated_coarse_);
737 }
738
746 void regrid() {
747 if (nlev_ < 2)
748 return; // 2 levels required (D5): nothing to re-grid in single-level
749 const int fk = nlev_ - 1, pk = fk - 1; // fine + its parent (pk == 0 in v1 with 2 levels)
750
751 // AMR PROFILING (Spec 5 criterion 43): time the WHOLE regrid attempt (tag + cluster + prolong +
752 // re-solve) into the "regrid" scope. RAII -> the scope covers EVERY early-return path below (empty
753 // tags / nothing to refine), so the timing reflects the real regrid cost. The per-run "regrid"
754 // COUNT is bumped at the tail (++regrid_count_) only when a regrid actually completed -- a no-op
755 // attempt times itself but does not inflate the count. Null/disabled profiler -> no scope object.
756 auto _rg = profile_amr_scope("regrid");
757
758 // (R0) PRECONDITION: fields up to date (aux per level, for the |grad phi| criterion). The per-block
759 // mass snapshot is NOT needed by the engine (conservation is checked test-side V1).
760 solve_fields();
761
762 // (R1)+(R2) PER-BLOCK TAGS (on the block U at the parent level) + PHI TAGS (on the shared aux).
763 const int PNX = dom_.nx() << pk, PNY = dom_.ny() << pk;
764 const Box2D pdom = Box2D::from_extents(PNX, PNY);
765 std::vector<TagBox> parts;
766 parts.reserve(blocks_.size() + 1);
767 for (std::size_t b = 0; b < blocks_.size(); ++b) {
768 const TagPredicate& crit = block_tag_[b];
769 if (!crit)
770 continue; // block without a criterion: tags nothing on its side (re-gridded as background)
771 parts.push_back(tag_cells((*blocks_[b].levels)[pk].U, pdom, crit));
772 }
773 if (phi_tag_)
774 parts.push_back(tag_cells(aux_[pk], pdom, phi_tag_));
775 if (parts.empty())
776 return; // no active criterion -> no tagged cell -> grid unchanged
777
778 // (R3) UNION (OR) of the tags + dilation (nesting + anticipation of the structures moving).
779 TagBox grown = grow_tags(tag_union(parts), regrid_grow_, pdom);
780
781 // (R4)+(R5) cross-rank collective reduction (if coarse distributed) + UNIQUE clustering -> SHARED
782 // fine layout. all_reduce_or_inplace is called INSIDE regrid_compute_fine_layout for distributed
783 // pk==0: all ranks start from the SAME tag grid -> IDENTICAL fb/dmap per rank (otherwise MPI
784 // desync).
785 auto [fb, dmap] =
786 regrid_compute_fine_layout(std::move(grown), pdom, pk, regrid_margin_, replicated_coarse_);
787#ifdef POPS_HAS_MPI
788 // MPI COLLECTIVE COUNT (Spec 5 criterion 43): regrid_compute_fine_layout issues ONE
789 // all_reduce_or_inplace over the tag grid when the coarse is distributed (multi-rank) -- every rank
790 // must cluster from the SAME gathered tags. Count it as one reduction (np>1 only; serial / single
791 // rank issues no collective). np==1 is bit-identical with no count.
792 if (profiler_ != nullptr && n_ranks() > 1)
793 profiler_->count("mpi_reductions");
794#endif
795 if (fb.size() == 0)
796 return; // nothing to refine: we keep the current grid (no-op)
797
798 // (R6) COHERENT PROLONG / RESTRICT of ALL blocks on the SAME fb/dmap (including the blocks held by
799 // their stride: their frozen state is present everywhere and contributes to the Poisson, D3). The
800 // ghost width is INHERITED per block (a MUSCL order-2 block carries 2 ghosts; a Minmod block and a
801 // VanLeer one may differ), so the scheme does not read out of bounds at the next step (V2 / risk
802 // X4).
803 for (auto& b : blocks_) {
804 auto& L = *b.levels;
805 const int ngf = L[fk].U.n_grow();
806 L[fk].U = regrid_field_on_layout(fb, dmap, L[pk].U, L[fk].U, pk, ngf, replicated_coarse_);
807 }
808
809 // (R7) REBUILD OF THE SHARED AUX (one only, width aux_ncomp_) on the new layout + RE-WIRING of the
810 // aux pointer of EACH block. The address &aux_[fk] stays stable (in-place reallocation of the
811 // MultiFab in the existing std::vector) -> the pointers of the other levels do not move.
812 aux_[fk] = MultiFab(fb, dmap, aux_ncomp_, 1);
813 for (auto& b : blocks_)
814 (*b.levels)[fk].aux = &aux_[fk];
815
816 // (V3) SHARED-LAYOUT INVARIANT: all blocks MUST live on EXACTLY the same fb/dmap (boxes, order,
817 // rank per box) after the regrid. Collective guard (cross-block); catches any inconsistent
818 // reconstruction before it corrupts the shared aux / the summed Poisson.
819 {
820 std::vector<std::vector<AmrLevelMP>> ref;
821 ref.reserve(blocks_.size());
822 for (const auto& b : blocks_)
823 ref.push_back(*b.levels);
825 }
826
827 // (R8) RESTORATION OF THE COVERAGE INVARIANT: re-solve so that phi / grad phi are consistent with
828 // the new grid AND to trigger the fine -> coarse cascade (mf_average_down_mb, in solve_fields) that
829 // restores the covered coarse cells (otherwise a mass diagnostic, sum of the coarse only, would
830 // count a phantom coarse value under the new patch, X5).
831 solve_fields();
832 ++regrid_count_;
833 // AMR PROFILING (Spec 5 criterion 43): a regrid COMPLETED -> bump the per-run "regrid" counter
834 // (parity with regrid_count_). The "regrid" TIMING scope (_rg above) already covered the whole
835 // attempt; this counts only the regrids that actually rebuilt the hierarchy.
836 if (profiler_ != nullptr)
837 profiler_->count("regrid");
838 }
839
862 void step(Real dt) {
863 solve_count_ = 0;
864 // UNION-TAGS REGRID (capstone Phase 2, C.6; D2: BEFORE the macro-step's step, consistent with the
865 // single-block amr_dsl_block.hpp:108). regrid_every_ cadence in MACRO-STEPS, OUTSIDE the substep
866 // loops and the stride windows (macro-step granularity ONLY, D3). regrid_every_ == 0 -> FROZEN
867 // hierarchy, regrid never called -> BIT-IDENTICAL trajectory to the historical one. The guard
868 // macro_step_ > 0 (like the single-block) avoids a regrid at the very first step (the initial grid
869 // is already the build one). The regrid sits BEFORE solve_fields below: it does its own
870 // solve_fields (R0/R8), then the step's solve_fields recomputes phi on the re-gridded grid.
871 if (regrid_every_ > 0 && macro_step_ > 0 && macro_step_ % regrid_every_ == 0)
872 regrid();
873 // System Poisson solved ONCE on the current state (OncePerStep cadence). A HELD block (stride > 1,
874 // outside end-of-window) contributed with its FROZEN state since its last advance: loose coupling
875 // assumed by the multirate, exactly like System::step / AmrSystemCoupler in OncePerStep. phi stays
876 // frozen during the blocks' advance (no per-substep re-solve here). When reached from step_cfl this
877 // re-solves an unchanged state (a second solve), kept on purpose; see the ADC-318 note in step_cfl.
878 solve_fields();
879 for (auto& b : blocks_) {
880 // HOLD-THEN-CATCH-UP cadence (cf. AmrRuntimeBlock::stride, #140): the block is HELD as long as
881 // (macro_step_+1) % stride != 0, then CATCHES UP at end-of-window by an effective step stride*dt.
882 // The end-of-window catch-up keeps the block temporally consistent with the fast ones at the
883 // coupling point (never in the future). stride=1: always true -> every step, bit-identical.
884 if ((macro_step_ + 1) % b.stride != 0)
885 continue;
886 // NEWTON DIAGNOSTICS (OPT-IN): RESET of the report at the HEAD of the block advance (parity with
887 // System::AdvanceImex::operator() which resets nreport before its substep loop). The report then
888 // AGGREGATES over all the levels AND substeps of THIS advance (imex_advance accumulates per level
889 // via backward_euler_source; step() calls imex_advance substeps times without re-resetting).
890 // Placed AFTER the stride skip: a HELD block keeps the report of its LAST advance ("last advance"
891 // semantics of System). No-op for a block without diagnostics (newton_report null).
892 if (b.newton_diagnostics && b.newton_report)
893 b.newton_report->reset();
894 const Real bdt = dt * static_cast<Real>(b.stride); // catch-up: effective step stride*dt
895 // substeps equal substeps of bdt/substeps. The chosen closure does ONE advance per call;
896 // substeps=1 -> a single advance of bdt (bit-identical to the single-substep case). Per-block
897 // treatment SELECTION: IMEX (source-free transport + implicit stiff source, mirrors the IMEX
898 // branch of AmrSystemCoupler::step) if b.imex, otherwise EXPLICIT (transport + forward-Euler
899 // source). The test is PER BLOCK and stable: a single IMEX block changes nothing for the
900 // neighboring explicit blocks.
901 // NOTE substeps>1: the loop below calls step_block substeps times for BOTH treatments, so the
902 // IMEX splitting is SUB-CYCLED (K Lie steps over bdt/K). The compile-time, for its part, applies
903 // its IMEX only once over bdt (it ignores substeps on its IMEX branch): divergence INTENTIONAL
904 // and sound for substeps>1 (cf. IMEX SEMANTICS UNDER substeps in the file header).
905 const Real h = bdt / static_cast<Real>(b.substeps);
906 auto& step_block = b.imex ? b.imex_advance : b.advance;
907 for (int s = 0; s < b.substeps; ++s)
908 step_block(*b.levels, dom_, h, base_per_, replicated_coarse_);
909 // PROJECTION PONCTUELLE post-pas (ADC-177) : par niveau, APRES substeps + reflux/cascade.
910 // Cell-local + idempotente -> conservation preservee (flux-registres deja regles). No-op si vide.
911 if (b.project_per_level)
912 b.project_per_level(*b.levels);
913 }
914 // Inter-species coupled sources AFTER the transport (same order as AmrSystemCoupler: transport then
915 // coupled_source_step), by forward-Euler splitting. No-op if no source registered -> bit-identical
916 // trajectory to the historical one (the feature is opt-in).
918 ++macro_step_;
919 }
920
930 // NOTE (ADC-318): this pre-solve plus step(dt)'s own head solve below is a DOUBLE Poisson solve on
931 // the SAME unchanged state (regrid_every=0 freezes the grid in between). It looks redundant but is
932 // NOT, and is INTENTIONALLY kept. GeometricMG::solve() is warm-started and iterates to a RELATIVE
933 // tolerance (rel_tol 1e-8; abs_tol 0 by default, so its off-step early-exit never fires here), so the
934 // second solve does not recompute identical phi: starting from the first solve's iterate it
935 // over-converges it by ~rel_tol. Skipping the second solve would therefore NOT be bit-identical; it
936 // drifts the trajectory by ~3e-10 over 20 steps (below the solver tolerance and far below the O(dt^2)
937 // scheme error, but nonzero). The de-dup was declined to preserve the exact historical bit-stream
938 // (SystemStepper::step_cfl avoids the double solve by INLINING its advance, not by skipping a solve).
939 solve_fields(); // aux up to date: each block's max_speed reads it on the current coarse
940 Real dt = std::numeric_limits<Real>::infinity();
941 last_dt_reason_ = "degenerate";
942 for (auto& b : blocks_) {
943 const Real w = std::max(b.max_speed((*b.levels)[0].U, aux_[0]), kCflSpeedFloor);
944 Real dt_b = cfl * h * static_cast<Real>(b.substeps) / (static_cast<Real>(b.stride) * w);
945 const char* why = "transport";
946 // OPTIONAL block BOUNDS (AMR StabilityPolicy, audit 2026-06): same substeps/stride formulas as
947 // SystemStepper::step_cfl, evaluated on the COARSE. Empty closures (model without the trait) ->
948 // not queried, transport bound only (bit-identical).
949 if (b.source_frequency) {
950 const Real mu = b.source_frequency((*b.levels)[0].U, aux_[0]);
951 if (mu > Real(0)) {
952 const Real dt_src =
953 cfl * static_cast<Real>(b.substeps) / (static_cast<Real>(b.stride) * mu);
954 if (dt_src < dt_b) {
955 dt_b = dt_src;
956 why = "source_frequency";
957 }
958 }
959 }
960 if (b.stability_dt) {
961 const Real db = b.stability_dt((*b.levels)[0].U, aux_[0]);
962 if (db > Real(0)) {
963 const Real dt_adm = db * static_cast<Real>(b.substeps) / static_cast<Real>(b.stride);
964 if (dt_adm < dt_b) {
965 dt_b = dt_adm;
966 why = "stability_dt";
967 }
968 }
969 }
970 if (dt_b < dt) {
971 dt = dt_b;
972 last_dt_reason_ = std::string(why) + ":" + b.name;
973 }
974 }
975 // Declared frequencies of the coupled sources (CoupledSource.frequency): bound on the MACRO-step
976 // (the couplings apply once per macro-step), dt <= cfl / mu, without substeps/stride.
977 for (const auto& cs : coupled_freqs_) {
978 const Real dt_cs = cfl / cs.mu;
979 if (dt_cs < dt) {
980 dt = dt_cs;
981 last_dt_reason_ = "coupled_source:" + cs.label;
982 }
983 }
984 // PER-CELL frequencies (CoupledSource.frequency with an Expr): mu(U) reduced (MAX) on the COARSE
985 // level of the input blocks (where the AMR CFL lives), GLOBAL all_reduce_max (ALL ranks, neutral
986 // without a local box), bound dt <= cfl / max(mu). Same reason "coupled_source:<label>" as the
987 // constant. No per-cell source -> empty loop (bit-identical). The Array4 are rebuilt at EACH step
988 // (the hierarchy fabs are repointed by the regrid), like coupled_source_step.
989 for (const auto& ce : coupled_freq_exprs_) {
990 Real m = 0;
991 if (ce.n_in > 0) {
992 auto& Uref =
993 (*blocks_[static_cast<std::size_t>(ce.ins[0].block)].levels)[0].U; // coarse (lev 0)
994 for (int li = 0; li < Uref.local_size(); ++li) {
996 kern.n_in = ce.n_in;
997 kern.n_const = ce.n_const;
998 for (int c = 0; c < ce.n_in; ++c) {
999 kern.in[c] =
1000 (*blocks_[static_cast<std::size_t>(ce.ins[static_cast<std::size_t>(c)].block)]
1001 .levels)[0]
1002 .U.fab(li)
1003 .array();
1004 kern.in_comp[c] = ce.ins[static_cast<std::size_t>(c)].comp;
1005 }
1006 for (int c = 0; c < ce.n_const; ++c)
1007 kern.consts[c] = ce.kconsts[static_cast<std::size_t>(c)];
1008 kern.prog = ce.prog;
1009 m = std::max(m, reduce_max_cell(Uref.box(li), kern));
1010 }
1011 } else {
1012 // Program WITHOUT an input field (constant in bytecode): evaluated once on the constants.
1013 Real reg[kCsMaxReg];
1014 for (int c = 0; c < ce.n_const; ++c)
1015 reg[c] = ce.kconsts[static_cast<std::size_t>(c)];
1016 const Real mu0 = ce.prog.eval(reg);
1017 if (mu0 > Real(0))
1018 m = mu0;
1019 }
1020 const double mu = all_reduce_max(static_cast<double>(m)); // ALL ranks (collective symmetry)
1021#ifdef POPS_HAS_MPI
1022 // MPI COLLECTIVE COUNT (Spec 5 criterion 43): one all_reduce_max per per-cell coupled-frequency
1023 // bound, multi-rank only (serial all_reduce_max is an identity, no collective).
1024 if (profiler_ != nullptr && n_ranks() > 1)
1025 profiler_->count("mpi_reductions");
1026#endif
1027 if (mu > 0.0) {
1028 const Real dt_cs = cfl / static_cast<Real>(mu);
1029 if (dt_cs < dt) {
1030 dt = dt_cs;
1031 last_dt_reason_ = "coupled_source:" + ce.label;
1032 }
1033 }
1034 }
1035 // GLOBAL bounds (AmrRuntime::add_dt_bound, parity with System::add_dt_bound): evaluated PER RANK
1036 // then reduced all_reduce_min (dt identical on all ranks; <= 0/non-finite = inert).
1037 for (const auto& g : dt_bounds_) {
1038 if (!g.fn)
1039 continue;
1040 double v = g.fn();
1041 if (!(v > 0.0) || !std::isfinite(v))
1042 v = std::numeric_limits<double>::infinity();
1043 v = all_reduce_min(v);
1044#ifdef POPS_HAS_MPI
1045 // MPI COLLECTIVE COUNT (Spec 5 criterion 43): one all_reduce_min per registered global dt bound,
1046 // multi-rank only (the global min keeps dt identical on all ranks). Serial -> identity, no count.
1047 if (profiler_ != nullptr && n_ranks() > 1)
1048 profiler_->count("mpi_reductions");
1049#endif
1050 if (static_cast<Real>(v) < dt) {
1051 dt = static_cast<Real>(v);
1052 last_dt_reason_ = "global:" + g.label;
1053 }
1054 }
1055 if (!std::isfinite(dt)) {
1056 dt = cfl * h / kCflSpeedFloor; // guard (no block: impossible here)
1057 last_dt_reason_ = "degenerate";
1058 }
1059 step(dt);
1060 return dt;
1061 }
1062
1065 int macro_step() const { return macro_step_; }
1069 void set_macro_step(int s) { macro_step_ = s; }
1070
1079 void set_profiler(runtime::program::Profiler* prof) { profiler_ = prof; }
1080
1083 void add_dt_bound(const std::string& label, std::function<double()> fn) {
1084 dt_bounds_.push_back(GlobalDtBound{label, std::move(fn)});
1085 }
1086
1090 void add_coupled_frequency(const std::string& label, Real mu) {
1091 if (mu > Real(0))
1092 coupled_freqs_.push_back(CoupledFreqDecl{label, mu});
1093 }
1094
1103 void add_coupled_frequency_expr(const std::string& label,
1104 const std::vector<std::string>& in_blocks,
1105 const std::vector<std::string>& in_roles,
1106 const std::vector<double>& consts,
1107 const std::vector<int>& freq_prog_ops,
1108 const std::vector<int>& freq_prog_args) {
1109 if (freq_prog_ops.empty() && freq_prog_args.empty())
1110 return; // no per-cell frequency
1111 const int n_in = static_cast<int>(in_blocks.size());
1112 const int n_const = static_cast<int>(consts.size());
1113 if (static_cast<int>(in_roles.size()) != n_in)
1114 throw std::runtime_error(
1115 "AmrRuntime::add_coupled_frequency_expr : in_blocks / in_roles of different sizes");
1116 if (n_in + n_const > kCsMaxReg)
1117 throw std::runtime_error(
1118 "AmrRuntime::add_coupled_frequency_expr : too many registers (inputs + constants > " +
1119 std::to_string(kCsMaxReg) + ")");
1120 if (freq_prog_ops.size() != freq_prog_args.size())
1121 throw std::runtime_error(
1122 "AmrRuntime::add_coupled_frequency_expr : freq_prog_ops / freq_prog_args of different "
1123 "sizes");
1124 if (static_cast<int>(freq_prog_ops.size()) > kCsMaxProg)
1125 throw std::runtime_error(
1126 "AmrRuntime::add_coupled_frequency_expr : frequency program too long (> " +
1127 std::to_string(kCsMaxProg) + ")");
1128 // Resolves (block, role) -> (block index, component), STRICT (mirror of add_coupled_source).
1129 std::vector<CsRef> ins(static_cast<std::size_t>(n_in));
1130 for (int c = 0; c < n_in; ++c) {
1131 const std::string& block = in_blocks[static_cast<std::size_t>(c)];
1132 const std::string& role = in_roles[static_cast<std::size_t>(c)];
1133 const int b = block_index(block);
1134 if (b < 0)
1135 throw std::runtime_error("AmrRuntime::add_coupled_frequency_expr : no block named '" +
1136 block + "'");
1137 // Role addressed BY NAME: a canonical role name OR a user-defined role label (ADC-292), STRICT.
1138 const VariableSet& vs = blocks_[static_cast<std::size_t>(b)].cons_vars;
1139 const int comp = vs.index_of(role);
1140 if (comp < 0)
1141 throw std::runtime_error("AmrRuntime::add_coupled_frequency_expr : block '" + block +
1142 "' does not expose role '" + role + "' (roles: " +
1143 (vs.roles.empty() ? std::string("<none>") : roles_csv(vs)) +
1144 ", no silent fallback to component 0)");
1145 ins[static_cast<std::size_t>(c)] = {b, comp, CsProgram{}};
1146 }
1147 CsProgram pg;
1148 pg.len = static_cast<int>(freq_prog_ops.size());
1149 for (int k = 0; k < pg.len; ++k) {
1150 const int opc = freq_prog_ops[static_cast<std::size_t>(k)];
1151 const int a = freq_prog_args[static_cast<std::size_t>(k)];
1152 if (opc < 0 || opc > static_cast<int>(CsOp::Sqrt))
1153 throw std::runtime_error(
1154 "AmrRuntime::add_coupled_frequency_expr : invalid opcode in the frequency");
1155 if (opc == static_cast<int>(CsOp::PushReg) && (a < 0 || a >= n_in + n_const))
1156 throw std::runtime_error(
1157 "AmrRuntime::add_coupled_frequency_expr : register out of bounds in the frequency");
1158 pg.op[k] = opc;
1159 pg.arg[k] = a;
1160 }
1161 std::vector<Real> kconsts(consts.begin(), consts.end());
1162 coupled_freq_exprs_.push_back(
1163 CoupledFreqExprDecl{label, std::move(ins), pg, n_in, n_const, std::move(kconsts)});
1164 }
1165
1168 const std::string& last_dt_bound() const { return last_dt_reason_; }
1169
1174 const NewtonReport& newton_report(const std::string& name) const {
1175 const int b = block_index(name);
1176 if (b < 0)
1177 throw std::runtime_error("AmrRuntime::newton_report : no block named '" + name + "'");
1178 const AmrRuntimeBlock& blk = blocks_[static_cast<std::size_t>(b)];
1179 if (!blk.newton_diagnostics || !blk.newton_report)
1180 throw std::runtime_error(
1181 "AmrRuntime::newton_report : Newton diagnostics not enabled for block '" + name +
1182 "' ; add the block with newton_diagnostics=True (pops.IMEX(newton_diagnostics=True))");
1183 return *blk.newton_report;
1184 }
1185
1188 std::vector<double> potential() {
1189 solve_fields();
1190 return blocks_[0].potential(aux_[0]);
1191 }
1192
1195 solve_fields();
1196 Real w = Real(1e-12);
1197 for (auto& b : blocks_) {
1198 const Real wb = b.max_speed((*b.levels)[0].U, aux_[0]);
1199 if (wb > w)
1200 w = wb;
1201 }
1202 return w;
1203 }
1204
1205 int n_patches() const {
1206 const auto& L = *blocks_[0].levels;
1207 return L.size() >= 2 ? static_cast<int>(L[1].U.box_array().size()) : 0;
1208 }
1209
1210 // Index-space signatures of the fine patches (level + inclusive lo/hi corners), for ALL fine levels.
1211 // Read-only of the GLOBAL BoxArray (all boxes/all ranks) already stored -> rank-independent, zero
1212 // communication, NO hot-path cost (query between steps). Mirror of n_patches(): the same box_array()
1213 // that gives the COUNT gives the BOXES. Block 0 representative (SHARED layout, same_layout_or_throw
1214 // guard). Loop k = 1..nlev-1: a single fine level today (ratio 2), correct if a future adds levels
1215 // (the level field disambiguates the spacing dx = L / (n << level) Python-side).
1216 std::vector<PatchBox> patch_boxes() const {
1217 const auto& L = *blocks_[0].levels;
1218 std::vector<PatchBox> out;
1219 for (int k = 1; k < static_cast<int>(L.size()); ++k) {
1220 const auto& bxs = L[k].U.box_array().boxes();
1221 for (const Box2D& b : bxs)
1222 out.push_back(PatchBox{k, b.lo[0], b.lo[1], b.hi[0], b.hi[1]});
1223 }
1224 return out;
1225 }
1226
1227 // COARSE-level (base) box counts (ADC-319, MPI ownership diagnostic). Block 0 is the SHARED layout
1228 // (same_layout_or_throw), so its level-0 MultiFab carries the base BoxArray + DistributionMapping
1229 // common to all blocks. local_size() = base boxes OWNED by this rank; box_array().size() = total base
1230 // boxes (all ranks). Mirror of n_patches(): a query between steps, no communication, no hot-path cost.
1231 int coarse_local_boxes() const { return (*blocks_[0].levels)[0].U.local_size(); }
1232 int coarse_total_boxes() const { return (*blocks_[0].levels)[0].U.box_array().size(); }
1233
1234 // ----------------------------------------------------------------------------------------------
1235 // MULTI-BLOCK AMR CHECKPOINT / RESTART (ADC-509). PER-BLOCK PER-LEVEL state accessors + the
1236 // level-0 phi (multigrid warm-start), counterpart of AmrCouplerMP::level_state on the SHARED
1237 // hierarchy. The shared layout is FROZEN at build (make_shared_amr_layout: a deterministic central
1238 // fine patch, regrid_every==0): replaying the SAME composition reproduces the SAME hierarchy, so a
1239 // restart only needs to restore each block's valid cells + phi (no set_hierarchy on the runtime).
1240 // The _global variants all_reduce_sum the per-rank LOCAL fabs into the complete field (np>1
1241 // gather), MIRROR of System::state_global / gather_global; mono-rank they are the identity. @p b:
1242 // block index, @p k: level (0 = coarse, >= 1 = fine).
1243 // ----------------------------------------------------------------------------------------------
1244
1245 // Conserved components of block @p b (Model::n_vars, carried by the AmrRuntimeBlock).
1246 int block_n_vars(std::size_t b) const {
1247 if (b >= blocks_.size())
1248 throw std::runtime_error("AmrRuntime::block_n_vars : block index out of bounds");
1249 return blocks_[b].ncomp;
1250 }
1251
1252 // FULL conservative state (all components) of block @p b at level @p k, flat component-major
1253 // c*nf*nf + j*nf + i (nf = nx << k); zeros outside the patches at the fine level. LOCAL fabs only
1254 // (no gather): the facade calls this mono-rank. Mirror of AmrCouplerMP::level_state.
1255 std::vector<double> block_level_state(std::size_t b, int k) const {
1256 if (b >= blocks_.size())
1257 throw std::runtime_error("AmrRuntime::block_level_state : block index out of bounds");
1258 const std::vector<AmrLevelMP>& L = *blocks_[b].levels;
1259 if (k < 0 || k >= static_cast<int>(L.size()))
1260 throw std::runtime_error("AmrRuntime::block_level_state : level out of bounds");
1261 const MultiFab& U = L[k].U;
1262 const int nc = U.ncomp();
1263 const std::size_t nf = static_cast<std::size_t>(dom_.nx()) << k;
1264 std::vector<double> out(static_cast<std::size_t>(nc) * nf * nf, 0.0);
1265 device_fence();
1266 fill_level_state(U, nc, nf, out);
1267 return out;
1268 }
1269
1270 // Same as block_level_state but all_reduce_sum the per-rank contributions -> every rank holds the
1271 // complete field (np>1 gather, AMR reflux pattern). COLLECTIVE: all ranks MUST call it.
1272 std::vector<double> block_level_state_global(std::size_t b, int k) const {
1273 std::vector<double> out = block_level_state(b, k);
1274 all_reduce_sum_inplace(out.data(), static_cast<int>(out.size()));
1275 return out;
1276 }
1277
1278 // Restores block @p b at level @p k from @p s (same layout as block_level_state). Writes ONLY the
1279 // VALID cells of the local fabs (the ghosts are redone at the next solve_fields/step, like after a
1280 // regrid). NO re-prolongation: restored AS-IS. Mirror of AmrCouplerMP::set_level_state.
1281 void set_block_level_state(std::size_t b, int k, const std::vector<double>& s) {
1282 if (b >= blocks_.size())
1283 throw std::runtime_error("AmrRuntime::set_block_level_state : block index out of bounds");
1284 std::vector<AmrLevelMP>& L = *blocks_[b].levels;
1285 if (k < 0 || k >= static_cast<int>(L.size()))
1286 throw std::runtime_error("AmrRuntime::set_block_level_state : level out of bounds");
1287 MultiFab& U = L[k].U;
1288 const int nc = U.ncomp();
1289 const std::size_t nf = static_cast<std::size_t>(dom_.nx()) << k;
1290 if (s.size() != static_cast<std::size_t>(nc) * nf * nf)
1291 throw std::runtime_error("AmrRuntime::set_block_level_state : state size != ncomp*nf*nf");
1292 device_fence();
1293 for (int li = 0; li < U.local_size(); ++li) {
1294 Array4 u = U.fab(li).array();
1295 const Box2D v = U.box(li);
1296 for (int j = v.lo[1]; j <= v.hi[1]; ++j)
1297 for (int i = v.lo[0]; i <= v.hi[0]; ++i)
1298 for (int c = 0; c < nc; ++c)
1299 u(i, j, c) = s[static_cast<std::size_t>(c) * nf * nf +
1300 static_cast<std::size_t>(j) * nf + static_cast<std::size_t>(i)];
1301 }
1302 }
1303
1304 // Potential phi of level @p k, flat nf*nf row-major, zeros outside patches. Level 0: the multigrid
1305 // WARM-START mg_.phi() (the state reused by the next solve -> bit-identical restart). Level >= 1:
1306 // shared aux comp 0 (recomputed at solve_fields). Mirror of AmrCouplerMP::level_potential; the phi
1307 // is SHARED by all blocks (single aux), so it carries no block index. NON-const like
1308 // AmrRuntime::potential() (GeometricMG::phi() returns a mutable warm-start reference).
1309 std::vector<double> level_potential(int k) {
1310 if (k < 0 || k >= nlev_)
1311 throw std::runtime_error("AmrRuntime::level_potential : level out of bounds");
1312 const std::size_t nf = static_cast<std::size_t>(dom_.nx()) << k;
1313 std::vector<double> out(nf * nf, 0.0);
1314 device_fence();
1315 const MultiFab& P = (k == 0) ? mg_.phi() : aux_[k];
1316 fill_level_phi(P, nf, out);
1317 return out;
1318 }
1319
1320 // Same as level_potential but all_reduce_sum (np>1 gather). COLLECTIVE: all ranks MUST call it.
1321 std::vector<double> level_potential_global(int k) {
1322 std::vector<double> out = level_potential(k);
1323 all_reduce_sum_inplace(out.data(), static_cast<int>(out.size()));
1324 return out;
1325 }
1326
1327 // Restores phi of level @p k. Level 0: warm-start mg_.phi() -> bit-identical restart (1st
1328 // post-restart solve starts from the same guess). Level >= 1: shared aux comp 0 (idempotent,
1329 // recomputed at solve_fields). Mirror of AmrCouplerMP::set_level_potential.
1330 void set_level_potential(int k, const std::vector<double>& p) {
1331 if (k < 0 || k >= nlev_)
1332 throw std::runtime_error("AmrRuntime::set_level_potential : level out of bounds");
1333 const std::size_t nf = static_cast<std::size_t>(dom_.nx()) << k;
1334 if (p.size() != nf * nf)
1335 throw std::runtime_error("AmrRuntime::set_level_potential : phi size != nf*nf");
1336 device_fence();
1337 MultiFab& P = (k == 0) ? mg_.phi() : aux_[k];
1338 for (int li = 0; li < P.local_size(); ++li) {
1339 Array4 q = P.fab(li).array();
1340 const Box2D v = P.box(li);
1341 for (int j = v.lo[1]; j <= v.hi[1]; ++j)
1342 for (int i = v.lo[0]; i <= v.hi[0]; ++i)
1343 q(i, j, 0) = p[static_cast<std::size_t>(j) * nf + static_cast<std::size_t>(i)];
1344 }
1345 }
1346
1347 private:
1348 // Fills @p out (zero-initialized, size nc*nf*nf) from the LOCAL valid cells of @p U at GLOBAL
1349 // component-major indices c*nf*nf + j*nf + i. Shared by block_level_state and its _global gather
1350 // variant (the loop is verbatim with AmrCouplerMP::level_state -> bit-identical layout).
1351 static void fill_level_state(const MultiFab& U, int nc, std::size_t nf,
1352 std::vector<double>& out) {
1353 for (int li = 0; li < U.local_size(); ++li) {
1354 const ConstArray4 u = U.fab(li).const_array();
1355 const Box2D v = U.box(li);
1356 for (int j = v.lo[1]; j <= v.hi[1]; ++j)
1357 for (int i = v.lo[0]; i <= v.hi[0]; ++i)
1358 for (int c = 0; c < nc; ++c)
1359 out[static_cast<std::size_t>(c) * nf * nf + static_cast<std::size_t>(j) * nf +
1360 static_cast<std::size_t>(i)] = u(i, j, c);
1361 }
1362 }
1363
1364 // Fills @p out (zero-initialized, size nf*nf) from the LOCAL valid cells of @p P (comp 0) at GLOBAL
1365 // row-major indices j*nf + i. Shared by level_potential and its _global gather variant.
1366 static void fill_level_phi(const MultiFab& P, std::size_t nf, std::vector<double>& out) {
1367 for (int li = 0; li < P.local_size(); ++li) {
1368 const ConstArray4 p = P.fab(li).const_array();
1369 const Box2D v = P.box(li);
1370 for (int j = v.lo[1]; j <= v.hi[1]; ++j)
1371 for (int i = v.lo[0]; i <= v.hi[0]; ++i)
1372 out[static_cast<std::size_t>(j) * nf + static_cast<std::size_t>(i)] = p(i, j, 0);
1373 }
1374 }
1375
1376 // Re-applies the model-NAMED aux fields (ADC-291) onto the COARSE shared aux valid cells. Mirror of
1377 // SystemFieldSolver::apply_named_aux_one (cartesian System): per LOCAL fab (MPI-safe), valid cells
1378 // only, global flat index j*nx+i. The coarse layout is frozen across regrid (only fine levels are
1379 // rebuilt), so the stored coarse field stays valid; solve_fields runs the coarse->fine injection
1380 // right after, carrying the named comps to every level. No-op without a named field.
1381 void apply_named_aux() {
1382 if (named_aux_.empty() || aux_.empty())
1383 return;
1384 const int row = dom_.nx();
1385 for (const auto& [comp, field] : named_aux_) {
1386 if (field.empty() || comp >= aux_ncomp_)
1387 continue;
1388 for (int li = 0; li < aux_[0].local_size(); ++li) {
1389 Array4 a = aux_[0].fab(li).array();
1390 const Box2D v = aux_[0].box(li);
1391 for (int j = v.lo[1]; j <= v.hi[1]; ++j)
1392 for (int i = v.lo[0]; i <= v.hi[0]; ++i)
1393 a(i, j, comp) = field[static_cast<std::size_t>(j) * row + i];
1394 }
1395 }
1396 }
1397
1398 // NAMED multi-elliptic field (ADC-428): a field name's aux output components + a DEDICATED coarse
1399 // GeometricMG (built lazily, REUSING the native solver; the operator is never reimplemented).
1400 // shared_ptr<GeometricMG>: GeometricMG owns Fabs (non-copyable/non-movable), and named_fields_ is a
1401 // std::map (stable nodes), so a heap GeometricMG gives a stable address without making NamedField
1402 // movable. Defined here (before ensure_named_elliptic, whose parameter type must be visible).
1403 struct NamedField {
1404 int phi_comp = -1;
1405 int gx_comp = -1;
1406 int gy_comp = -1;
1407 std::shared_ptr<GeometricMG>
1408 mg; // dedicated coarse solver, built lazily by ensure_named_elliptic
1409 };
1410
1411 // Builds a NAMED elliptic field's dedicated coarse GeometricMG (ADC-428), lazily, IDENTICAL to the
1412 // default mg_ (same coarse geometry / BoxArray / Poisson BC / wall predicate / replication). REUSES the
1413 // native solver -- no operator is reimplemented. The variable / anisotropic permittivity of the default
1414 // Poisson is NOT carried onto a named field (a named field is a plain Laplacian, like the System named
1415 // path). No-op if already built.
1416 void ensure_named_elliptic(NamedField& nf) {
1417 if (nf.mg)
1418 return;
1419 nf.mg =
1420 std::make_shared<GeometricMG>(geom_, ba_coarse_, bcPhi_, wall_active_, replicated_coarse_);
1421 }
1422
1423 // Reads aux component @p comp of the COARSE level as a GLOBAL n*n row-major field (diagnostic /
1424 // read-back). Same marshaling as detail::coupler_read_coarse_phi (the default potential read-back) but
1425 // for an arbitrary component: local n*n buffer, all_reduce_sum_inplace when the coarse is DISTRIBUTED
1426 // (disjoint boxes -> exact recompose; serial / replicated is identity). Used by named_field_values.
1427 std::vector<double> coarse_aux_component(int comp) const {
1428 device_fence();
1429 const int nx = dom_.nx(), ny = dom_.ny();
1430 std::vector<double> out(static_cast<std::size_t>(nx) * ny, 0.0);
1431 for (int li = 0; li < aux_[0].local_size(); ++li) {
1432 const ConstArray4 a = aux_[0].fab(li).const_array();
1433 const Box2D v = aux_[0].box(li);
1434 for (int j = v.lo[1]; j <= v.hi[1]; ++j)
1435 for (int i = v.lo[0]; i <= v.hi[0]; ++i)
1436 out[static_cast<std::size_t>(j) * nx + i] = static_cast<double>(a(i, j, comp));
1437 }
1438 if (!replicated_coarse_)
1439 all_reduce_sum_inplace(out.data(), static_cast<int>(out.size()));
1440 return out;
1441 }
1442
1443 // Per-field aux HALO override (ADC-369) on the COARSE aux, AFTER the shared fill_ghosts. Overrides
1444 // only each declared component's physical-face ghosts (aux_halo_override keeps periodic faces
1445 // periodic). No-op without a policy. Mirror of SystemFieldSolver::apply_named_aux_bc.
1446 void apply_named_aux_bc() {
1447 if (named_aux_bc_.empty() || aux_.empty())
1448 return;
1449 for (const auto& [comp, policy] : named_aux_bc_) {
1450 if (comp >= aux_ncomp_)
1451 continue;
1452 fill_physical_bc(aux_[0], dom_, aux_halo_override(aux_bc_, policy), comp);
1453 }
1454 }
1455
1456 // Index of the block named @p name in the registry (-1 if absent). Counterpart of
1457 // AmrSystem::Impl::block_index (the facade names the blocks; the coupled sources target them by name,
1458 // resolved once at registration).
1459 int block_index(const std::string& name) const {
1460 for (std::size_t i = 0; i < blocks_.size(); ++i)
1461 if (blocks_[i].name == name)
1462 return static_cast<int>(i);
1463 return -1;
1464 }
1465
1466 // Resolved reference of a coupled-source field: (block index, component) + the term bytecode program
1467 // (empty for an input). Inputs carry only block/comp; outputs carry in addition the postfix program
1468 // evaluated per cell. We capture the block INDEX (not a fab pointer): the Array4 are rebuilt at each
1469 // application, per level.
1470 struct CsRef {
1471 int block;
1472 int comp;
1473 CsProgram prog; // outputs: term program; inputs: unused (CsProgram{})
1474 };
1475 // A registered coupled source: its inputs, its output terms and its constants, ready to be marshaled
1476 // into a CoupledSourceKernel per level / per fab at application.
1477 struct CoupledSourceSpec {
1478 std::vector<CsRef> ins;
1479 std::vector<CsRef> outs;
1480 std::vector<Real> kconsts;
1481 int n_in = 0;
1482 int n_const = 0;
1483 int n_terms = 0;
1484 };
1485
1486 Geometry geom_;
1487 Box2D dom_;
1488 Periodicity base_per_;
1489 BCRec bcPhi_, aux_bc_;
1490 bool replicated_coarse_;
1491 GeometricMG mg_;
1492 // Coarse BoxArray + conductive-wall predicate stashed at the ctor so a NAMED elliptic field (ADC-428)
1493 // can build its own coarse GeometricMG identical to mg_ (ensure_named_elliptic). mg_ consumes them at
1494 // its construction but does not expose them, so we keep a copy here (cheap; the coarse layout is small).
1495 BoxArray ba_coarse_;
1496 std::function<bool(Real, Real)> wall_active_;
1497 std::vector<AmrRuntimeBlock> blocks_;
1498 // GLOBAL step bounds (add_dt_bound, parity with System) + ACTIVE bound of the last step_cfl.
1499 struct GlobalDtBound {
1500 std::string label;
1501 std::function<double()> fn;
1502 };
1503 std::vector<GlobalDtBound> dt_bounds_;
1504 // Declared frequencies of the coupled sources (bound dt <= cfl/mu on the macro-step, wave 3).
1505 struct CoupledFreqDecl {
1506 std::string label;
1507 Real mu;
1508 };
1509 std::vector<CoupledFreqDecl> coupled_freqs_;
1510 // PER-CELL frequencies of the coupled sources (CoupledSource.frequency with an Expr): bytecode
1511 // program mu(U) evaluated on the coarse at each step_cfl (MAX + all_reduce_max -> dt <= cfl/max(mu)).
1512 // ins = (block, comp) of the inputs (prog unused); kconsts = constants (same as the source).
1513 struct CoupledFreqExprDecl {
1514 std::string label;
1515 std::vector<CsRef> ins;
1516 CsProgram prog;
1517 int n_in = 0;
1518 int n_const = 0;
1519 std::vector<Real> kconsts;
1520 };
1521 std::vector<CoupledFreqExprDecl> coupled_freq_exprs_;
1522 std::string last_dt_reason_;
1523 std::vector<MultiFab> aux_; // [level], shared by all blocks
1524 // Model-NAMED aux fields (ADC-291): component (>= kAuxNamedBase) -> coarse base-level field
1525 // (n*n row-major). STATIC user fields re-applied by solve_fields each macro-step (so they persist
1526 // across regrid). Empty by default -> bit-identical. cf. set_named_aux / apply_named_aux.
1527 std::map<int, std::vector<Real>> named_aux_;
1528 // Per-field aux HALO policy (ADC-369): component -> uniform boundary policy, applied to the coarse aux
1529 // after the shared fill (apply_named_aux_bc). Empty by default -> bit-identical.
1530 std::map<int, AuxHaloPolicy> named_aux_bc_;
1531 // NAMED multi-elliptic fields (ADC-428): field name -> its aux output components + a DEDICATED coarse
1532 // GeometricMG. The NamedField struct itself is defined higher up (before ensure_named_elliptic, which
1533 // takes it by reference: a parameter type must be visible at the function declaration, unlike a member
1534 // body). Empty default -> bit-identical (the solve_named_fields loop early-returns).
1535 std::map<std::string, NamedField> named_fields_;
1536 std::vector<CoupledSourceSpec>
1537 coupled_sources_; // registered coupled sources (applied after transport)
1538 // UNION-TAGS REGRID (capstone Phase 2, C.6). regrid_every_ == 0 -> FROZEN hierarchy (default,
1539 // bit-identical). block_tag_: PER-BLOCK tag predicate (D1; same size as blocks_, empty = this block
1540 // tags nothing on its side). phi_tag_: phi tag predicate on |grad phi| (D4; empty = phi does not
1541 // contribute to the union).
1542 std::vector<TagPredicate> block_tag_;
1543 TagPredicate phi_tag_;
1544 int regrid_every_ = 0;
1545 int regrid_grow_ = 2;
1546 int regrid_margin_ = 2;
1547 int aux_ncomp_ = kAuxBaseComps;
1548 int nlev_ = 0;
1549 int macro_step_ = 0;
1550 mutable int solve_count_ = 0;
1551 int regrid_count_ = 0;
1552 // AMR / MPI PROFILING (Spec 5 criterion 43, ADC-479): non-owning pointer to the AmrSystem-owned
1553 // Profiler (lifetime guaranteed by the facade). Null by default -> the engine never profiles
1554 // (zero overhead). Set via set_profiler after build (parity with System::profiler_).
1555 runtime::program::Profiler* profiler_ = nullptr;
1556
1557 // RAII phase-timing scope for an AMR phase (regrid / average_down / fill_boundary). Mirrors
1558 // runtime::program::ProfileScope but over a NULLABLE profiler pointer: it reads the clock and
1559 // records only when profiler_ is non-null AND enabled. A null/disabled run constructs a cheap inert
1560 // scope (one pointer copy + one clock read) -- the granularity is per-phase, not per-cell, so this
1561 // is off the hot path. Returned BY VALUE from profile_amr_scope (movable: only POD members).
1562 class AmrPhaseScope {
1563 public:
1564 AmrPhaseScope(runtime::program::Profiler* prof, const char* name)
1565 : prof_(prof != nullptr && prof->enabled() ? prof : nullptr),
1566 name_(name),
1567 t0_(std::chrono::steady_clock::now()) {}
1568 AmrPhaseScope(AmrPhaseScope&& o) noexcept : prof_(o.prof_), name_(o.name_), t0_(o.t0_) {
1569 o.prof_ = nullptr; // the moved-from scope must not record
1570 }
1571 AmrPhaseScope(const AmrPhaseScope&) = delete;
1572 AmrPhaseScope& operator=(const AmrPhaseScope&) = delete;
1573 AmrPhaseScope& operator=(AmrPhaseScope&&) = delete;
1574 ~AmrPhaseScope() {
1575 if (prof_ == nullptr)
1576 return;
1577 const auto t1 = std::chrono::steady_clock::now();
1578 try {
1579 prof_->record(name_, std::chrono::duration<double>(t1 - t0_).count());
1580 } catch (...) { // NOLINT(bugprone-empty-catch) -- a profiler never throws out of a scope
1581 }
1582 }
1583
1584 private:
1585 runtime::program::Profiler* prof_;
1586 const char* name_;
1587 std::chrono::steady_clock::time_point t0_;
1588 };
1589
1590 // Build an AMR phase scope (no-op when profiling is off). Used at the head of regrid /
1591 // average_down / fill_boundary.
1592 AmrPhaseScope profile_amr_scope(const char* name) { return AmrPhaseScope(profiler_, name); }
1593
1594 // fill_ghosts wrapped in the "fill_boundary" timing scope + per-call count (Spec 5 criterion 43).
1595 // Under MPI np>1 the ghost fill is a cross-rank halo exchange -> also count one "mpi_messages" (a
1596 // point-to-point round, distinct from the all_reduce collectives counted as "mpi_reductions").
1597 // Serial / single rank: no message count (the fill is a local copy). Off-profiling: inert.
1598 void fill_ghosts_profiled(MultiFab& mf, const Box2D& dom, const BCRec& bc) {
1599 auto _fb = profile_amr_scope("fill_boundary");
1600 if (profiler_ != nullptr) {
1601 profiler_->count("fill_boundary");
1602#ifdef POPS_HAS_MPI
1603 if (n_ranks() > 1)
1604 profiler_->count("mpi_messages");
1605#endif
1606 }
1607 fill_ghosts(mf, dom, bc);
1608 }
1609};
1610
1611} // namespace pops
AmrCouplerMP: MULTI-PATCH E x B AMR coupler (coarse Poisson -> aux = grad phi -> fine injection -> co...
Umbrella for the AMR MultiFab stack: includes the numerics/time sub-headers in dependency order (flux...
amr_regrid_finest: Berger-Rigoutsos regrid of the finest level (responsibility b).
AmrSystemCoupler: multi-species SYSTEM coupler on AMR (milestone 2.3).
Helpers shared by the three couplers (single-block Coupler, SystemAssembler, AmrSystemCoupler) for th...
Box2D: the integer index space of a 2D cell-centered Cartesian grid.
BoxArray: the set of boxes tiling a level (disjoint, covering).
AMR multi-block engine at runtime.
Definition amr_runtime.hpp:229
std::vector< double > level_potential(int k)
Definition amr_runtime.hpp:1309
void set_profiler(runtime::program::Profiler *prof)
AMR / MPI PROFILING SEAM (Spec 5 sec.12.5, ADC-479 criterion 43).
Definition amr_runtime.hpp:1079
void solve_fields()
sync_down (per block) + system coarse Poisson (CO-LOCATED SUMMED RHS) + coarse aux + fine injection.
Definition amr_runtime.hpp:622
int coarse_total_boxes() const
Definition amr_runtime.hpp:1232
Real step_cfl(Real cfl, Real h)
substeps/stride-aware CFL step (runtime counterpart of System::step_cfl, EXACT mirror of its formula)...
Definition amr_runtime.hpp:929
std::vector< AmrLevelMP > & levels(std::size_t b)
Definition amr_runtime.hpp:319
std::function< bool(const ConstArray4 &, int, int)> TagPredicate
Tag predicate of the union regrid: (ConstArray4 of the read field, i, j) -> should we refine ?...
Definition amr_runtime.hpp:330
std::size_t n_coupled_sources() const
Definition amr_runtime.hpp:312
Real mass(std::size_t b) const
Definition amr_runtime.hpp:320
const MultiFab & aux(int k) const
Definition amr_runtime.hpp:318
std::vector< PatchBox > patch_boxes() const
Definition amr_runtime.hpp:1216
int block_n_vars(std::size_t b) const
Definition amr_runtime.hpp:1246
void set_block_tag_predicate(std::size_t b, TagPredicate crit)
Registers the TAG PREDICATE of block b (D1: PER-BLOCK union criterion).
Definition amr_runtime.hpp:353
Real max_speed()
Max SYSTEM wave speed (max over the blocks) on the current coarse. Requires the aux up to date.
Definition amr_runtime.hpp:1194
std::vector< double > density(std::size_t b) const
Definition amr_runtime.hpp:321
AmrRuntime(const Geometry &geom, const BoxArray &ba_coarse, const BCRec &bcPhi, std::vector< AmrRuntimeBlock > blocks, Periodicity base_per=Periodicity{true, true}, bool replicated_coarse=true, std::function< bool(Real, Real)> active={})
Definition amr_runtime.hpp:238
void coupled_source_step(Real dt)
Applies ALL the registered coupled sources of a step dt, by forward-Euler splitting.
Definition amr_runtime.hpp:571
const NewtonReport & newton_report(const std::string &name) const
NEWTON REPORT (OPT-IN IMEX diagnostics) of block name, AGGREGATED over the levels and substeps of its...
Definition amr_runtime.hpp:1174
void register_named_field(const std::string &field, int phi_comp, int gx_comp, int gy_comp)
Definition amr_runtime.hpp:402
std::vector< double > potential()
Coarse potential (component 0 of the shared aux) as an n*n row-major field.
Definition amr_runtime.hpp:1188
std::vector< double > level_potential_global(int k)
Definition amr_runtime.hpp:1321
void set_named_aux_bc(int comp, AuxHaloPolicy policy)
Registers a per-field aux HALO policy (ADC-369) for the named component comp: solve_fields applies it...
Definition amr_runtime.hpp:384
void add_coupled_frequency(const std::string &label, Real mu)
DECLARED frequency of a coupled source (CoupledSource.frequency, wave-3 audit): step bound dt <= cfl ...
Definition amr_runtime.hpp:1090
void add_coupled_source(const std::vector< std::string > &in_blocks, const std::vector< std::string > &in_roles, const std::vector< double > &consts, const std::vector< std::string > &out_blocks, const std::vector< std::string > &out_roles, const std::vector< int > &prog_ops, const std::vector< int > &prog_args, const std::vector< int > &prog_lens)
Registers an inter-species COUPLED SOURCE (DSL CoupledSource, P5 bytecode) on the runtime facade,...
Definition amr_runtime.hpp:458
int n_patches() const
Definition amr_runtime.hpp:1205
MultiFab & poisson_rhs()
Definition amr_runtime.hpp:317
void set_block_named_elliptic_rhs(std::size_t b, const std::string &field, std::function< void(const MultiFab &, MultiFab &)> rhs)
Attaches named field's RHS contribution closure (rhs += elliptic_field_rhs(U_b)) to block b.
Definition amr_runtime.hpp:411
void regrid()
UNION-TAGS REGRID (capstone Phase 2, C.6; docs/AMR_REGRID_UNION_TAGS_DESIGN.md, steps R0-R8).
Definition amr_runtime.hpp:746
int solve_count() const
Definition amr_runtime.hpp:322
void set_macro_step(int s)
RESTORES the macro-step counter (IO v1, reserved for restart via AmrSystem::set_clock): without it th...
Definition amr_runtime.hpp:1069
void set_named_aux(int comp, std::vector< Real > field)
Registers a model-NAMED aux field (ADC-291) at shared-channel component comp (= kAuxNamedBase.
Definition amr_runtime.hpp:373
int coarse_local_boxes() const
Definition amr_runtime.hpp:1231
std::size_t n_blocks() const
Definition amr_runtime.hpp:302
std::vector< double > block_level_state(std::size_t b, int k) const
Definition amr_runtime.hpp:1255
std::vector< double > named_field_values(const std::string &field)
Solved potential of named field as a COARSE n*n row-major field (diagnostic / read-back).
Definition amr_runtime.hpp:428
void solve_named_fields()
Solves every registered NAMED elliptic field (ADC-428) on the coarse, writes phi (+ centered grad) in...
Definition amr_runtime.hpp:678
void step(Real dt)
Advances the system by one macro-step dt.
Definition amr_runtime.hpp:862
int nlev() const
Definition amr_runtime.hpp:301
bool has_named_field(const std::string &field) const
True if field is a registered named elliptic field.
Definition amr_runtime.hpp:421
void set_phi_tag_predicate(TagPredicate crit)
Registers the PHI TAG PREDICATE (D4: SEPARATE phi criterion, on |grad phi|).
Definition amr_runtime.hpp:363
int macro_step() const
MACRO-STEP counter of the engine (regrid + hold-then-catch-up stride cadence: regrid when macro_step_...
Definition amr_runtime.hpp:1065
MultiFab & phi()
Definition amr_runtime.hpp:313
const VariableSet & block_cons_vars(std::size_t b) const
Conservative VariableSet (names + physical roles, Model::conservative_vars()) of block b.
Definition amr_runtime.hpp:307
void add_dt_bound(const std::string &label, std::function< double()> fn)
GLOBAL step bound (AMR counterpart of System::add_dt_bound): fn() evaluated once per step_cfl,...
Definition amr_runtime.hpp:1083
const std::string & last_dt_bound() const
ACTIVE bound of the last step_cfl ("transport:<block>" / "source_frequency:<block>" / "stability_dt:<...
Definition amr_runtime.hpp:1168
void add_coupled_frequency_expr(const std::string &label, const std::vector< std::string > &in_blocks, const std::vector< std::string > &in_roles, const std::vector< double > &consts, const std::vector< int > &freq_prog_ops, const std::vector< int > &freq_prog_args)
PER-CELL COUPLED frequency (CoupledSource.frequency with an Expr, refinement of the CONSTANT frequenc...
Definition amr_runtime.hpp:1103
void set_regrid(int every, int grow=2, int margin=2)
Activates the UNION-TAGS REGRID at the cadence every (in macro-steps): every every macro-steps,...
Definition amr_runtime.hpp:339
std::size_t n_named_fields() const
Number of registered named elliptic fields (diagnostic / test).
Definition amr_runtime.hpp:419
void set_block_level_state(std::size_t b, int k, const std::vector< double > &s)
Definition amr_runtime.hpp:1281
int regrid_count() const
Definition amr_runtime.hpp:323
std::vector< double > block_level_state_global(std::size_t b, int k) const
Definition amr_runtime.hpp:1272
void set_level_potential(int k, const std::vector< double > &p)
Definition amr_runtime.hpp:1330
Ordered list of boxes tiling a level.
Definition box_array.hpp:22
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
int solve(Real rel_tol, int max_cycles, Real abs_tol=Real(0))
Definition geometric_mg.hpp:407
MultiFab & rhs()
Definition geometric_mg.hpp:222
MultiFab & phi()
Definition geometric_mg.hpp:221
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
int ncomp() const
Number of components.
Definition multifab.hpp:60
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
Definition profiler.hpp:27
void count(const std::string &name, std::int64_t by=1)
Definition profiler.hpp:69
Parallel seam: minimal MPI abstraction (rank/size + collectives) with serial fallback.
Generic COUPLED SOURCE interpreter: postfix bytecode evaluated on device (P5 phase 1).
DistributionMapping: maps each box (by global index) to its owning MPI rank.
DESCRIPTIVE types of the elliptic stage: EllipticProblem (problem definition) and FieldPostProcess (f...
fill_boundary: INTRA-level halo exchange (fills ghosts from neighbors).
GeometricMG: in-house geometric multigrid (V-cycle) for the elliptic operator, Gauss-Seidel smoother ...
Geometry: index-space (Box2D) <-> Cartesian physical-space mapping; PolarGeometry: SIBLING for a glob...
Implicit / IMEX block step as a named CONTRACT.
MultiFab: a field DISTRIBUTED over a level (equivalent of AMReX's MultiFab).
void coupler_inject_aux_mb(const MultiFab &parent, MultiFab &child, bool replicated_parent=true)
Definition amr_coupler_mp.hpp:60
void same_layout_or_throw(const std::vector< std::vector< AmrLevelMP > > &block_levels)
Definition amr_system_coupler.hpp:115
Definition amr_hierarchy.hpp:29
constexpr Real kCflSpeedFloor
Speed FLOOR for the CFL step policies (audit 2026-06, explicit constant instead of the scattered lite...
Definition types.hpp:38
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
constexpr int kCsMaxTerms
Definition coupled_source_program.hpp:43
constexpr int kAuxBaseComps
Definition state.hpp:147
double Real
Definition types.hpp:30
void mf_average_down_mb(const MultiFab &Uf, MultiFab &Uc)
Definition amr_subcycling.hpp:305
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
std::pair< BoxArray, DistributionMapping > regrid_compute_fine_layout(TagBox grown, const Box2D &pdom, int pk, int margin, bool coarse_replicated=true)
Compute the fine layout (BoxArray + DistributionMapping) of a Berger-Rigoutsos regrid from the grown ...
Definition amr_regrid_coupler.hpp:53
void all_reduce_sum_inplace(double *, int)
Definition comm.hpp:155
TagBox tag_union(const std::vector< TagBox > &parts)
Union (cell-by-cell logical OR) of several TagBox sharing EXACTLY the same box.
Definition tag_box.hpp:74
constexpr int kCsMaxReg
Definition coupled_source_program.hpp:40
void device_fence()
Device barrier: waits for in-flight kernels to finish before a HOST access to unified memory.
Definition kokkos_env.hpp:43
Real reduce_max_cell(const Box2D &b, F f)
MAX reduction with a REDUCING FUNCTOR: f receives (i, j, Real& acc) and updates acc,...
Definition for_each.hpp:240
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
double all_reduce_max(double x)
Definition comm.hpp:146
std::string roles_csv(const VariableSet &vs)
CSV of a VariableSet's roles (role_name, separator ',').
Definition variables.hpp:172
double all_reduce_min(double x)
Definition comm.hpp:149
void field_postprocess(const MultiFab &phi, MultiFab &out, Real cx, Real cy, FieldPostProcess spec)
Definition elliptic_problem.hpp:104
TagBox grow_tags(const TagBox &in, int n, const Box2D &domain)
Grows the tags by n cells (square neighborhood), staying within the domain.
Definition regrid.hpp:58
MultiFab regrid_field_on_layout(const BoxArray &fb, const DistributionMapping &dmap, const MultiFab &par, const MultiFab &old, int pk, int ngf, bool coarse_replicated=true)
Rebuild ONE fine MultiFab on the IMPOSED layout fb / dmap (the same one for all blocks in multi-block...
Definition amr_regrid_coupler.hpp:86
constexpr int kCsMaxProg
Definition coupled_source_program.hpp:42
TagBox tag_cells(const MultiFab &mf, const Box2D &domain, Crit crit)
Marks the valid cells where the predicate is true, on a TagBox covering the domain.
Definition regrid.hpp:40
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
PatchBox: index-space footprint of an AMR fine patch, exposed read-only to Python.
PHYSICAL boundary conditions at the domain edge (BCType, BCRec, fill_physical_bc, fill_ghosts).
Dynamic regrid: (re)builds a fine level from the tagging of a coarse level.
Pointwise types of the physics layer: StateVec<N> (conserved state) and Aux (auxiliary fields from th...
Type-erased closures of ONE AMR block, placed on the shared hierarchy.
Definition amr_runtime.hpp:98
std::function< std::vector< double >(const MultiFab &)> potential
Coarse potential read from the shared aux (component 0) as an n*n row-major field (diagnostic).
Definition amr_runtime.hpp:223
int stride
HOLD-THEN-CATCH-UP cadence of the block (multirate).
Definition amr_runtime.hpp:115
std::string name
Definition amr_runtime.hpp:99
bool newton_diagnostics
NEWTON DIAGNOSTICS (OPT-IN, wave 3: AMR counterpart of System::newton_report).
Definition amr_runtime.hpp:185
std::function< void(std::vector< AmrLevelMP > &, const Box2D &, Real, Periodicity, bool)> advance
Advances the block by ONE substep of size dt: AMR transport (Berger-Oliger + conservative reflux.
Definition amr_runtime.hpp:141
int ncomp
Definition amr_runtime.hpp:100
double gamma
Definition amr_runtime.hpp:101
std::function< std::vector< double >()> density
Coarse density (component 0) of the block as a global n*n row-major field (diagnostic).
Definition amr_runtime.hpp:219
std::shared_ptr< std::vector< AmrLevelMP > > levels
Level stack of the block (level 0 = coarse, > 0 = fine patches), ON the shared layout.
Definition amr_runtime.hpp:133
int substeps
EXPLICIT substeps of the block within ITS effective macro-step: the effective step (stride * dt) is s...
Definition amr_runtime.hpp:105
std::function< void(std::vector< AmrLevelMP > &)> project_per_level
POINTWISE PROJECTION post-pas (ADC-177) : U <- project(U, aux) appliquee PAR NIVEAU a la FIN de l'ava...
Definition amr_runtime.hpp:174
std::shared_ptr< NewtonReport > newton_report
Definition amr_runtime.hpp:186
std::function< Real(const MultiFab &, const MultiFab &)> max_speed
Speed driving the block CFL on the coarse.
Definition amr_runtime.hpp:204
std::function< void(const MultiFab &, MultiFab &)> add_elliptic_rhs
Contribution of the block to the Poisson right-hand side: rhs += elliptic_rhs_b(U_b) on the coarse.
Definition amr_runtime.hpp:191
std::function< Real(const MultiFab &, const MultiFab &)> source_frequency
OPTIONAL STEP BOUNDS of the block (AMR StabilityPolicy, audit 2026-06): evaluated on the COARSE (leve...
Definition amr_runtime.hpp:212
std::map< std::string, std::function< void(const MultiFab &, MultiFab &)> > named_elliptic_rhs
Per-NAMED-field elliptic right-hand-side contributions of the block (ADC-428): field name -> closure ...
Definition amr_runtime.hpp:199
std::function< Real()> mass
Mass of component 0 of the block coarse (sum u*dV; cross-rank reduced if distributed).
Definition amr_runtime.hpp:216
int aux_ncomp
Width of the aux channel READ by the block model (aux_comps<Model>(); >= kAuxBaseComps).
Definition amr_runtime.hpp:119
std::function< void(std::vector< AmrLevelMP > &, const Box2D &, Real, Periodicity, bool)> imex_advance
IMEX advance of the block by ONE substep of size dt: (1) EXPLICIT TRANSPORT on the SOURCE-FREE model ...
Definition amr_runtime.hpp:168
bool imex
TEMPORAL TREATMENT of the block: false (default) = EXPLICIT (forward-Euler source,...
Definition amr_runtime.hpp:149
std::function< Real(const MultiFab &, const MultiFab &)> stability_dt
Definition amr_runtime.hpp:213
VariableSet cons_vars
Descriptor of the model CONSERVATIVE variables (names + physical ROLES, Model::conservative_vars()).
Definition amr_runtime.hpp:127
WRITE POD handle (raw pointer + strides) over a Fab2D buffer, indexed by (i, j, c) IN GLOBAL INDICES ...
Definition fab2d.hpp:29
Per-field aux halo policy (ADC-369): a UNIFORM boundary policy for ONE model-named aux component,...
Definition physical_bc.hpp:195
Boundary conditions for the FOUR faces of the domain (type + associated Dirichlet value).
Definition physical_bc.hpp:29
2D integer index space, cell-centered.
Definition box2d.hpp:37
static Box2D from_extents(int nx, int ny)
Box [0, nx-1] x [0, ny-1] covering nx*ny cells from the index origin.
Definition box2d.hpp:42
POPS_HD int nx() const
Width (direction 0). POPS_HD (called from Geometry::dx() in a device kernel).
Definition box2d.hpp:50
int hi[2]
Definition box2d.hpp:39
POPS_HD int ny() const
Height (direction 1). POPS_HD (called from Geometry::dy() in a device kernel).
Definition box2d.hpp:52
int lo[2]
Definition box2d.hpp:38
READ-only handle (const counterpart of Array4): same layout and same contract (POD device-copyable,...
Definition fab2d.hpp:44
Definition coupled_source_program.hpp:175
CsProgram prog
Definition coupled_source_program.hpp:183
Real consts[kCsMaxReg]
Definition coupled_source_program.hpp:180
int in_comp[kCsMaxReg]
Definition coupled_source_program.hpp:177
int n_const
Definition coupled_source_program.hpp:181
int n_in
Definition coupled_source_program.hpp:178
Array4 in[kCsMaxReg]
Definition coupled_source_program.hpp:176
Device functor applying ONE coupled source over a box: captures the PODs by VALUE (input/output Array...
Definition coupled_source_program.hpp:133
Array4 out[kCsMaxTerms]
Definition coupled_source_program.hpp:141
int out_comp[kCsMaxTerms]
Definition coupled_source_program.hpp:142
int n_terms
Definition coupled_source_program.hpp:144
Real dt
Definition coupled_source_program.hpp:146
Real consts[kCsMaxReg]
Definition coupled_source_program.hpp:138
Array4 in[kCsMaxReg]
Definition coupled_source_program.hpp:134
int n_const
Definition coupled_source_program.hpp:139
int n_in
Definition coupled_source_program.hpp:136
int in_comp[kCsMaxReg]
Definition coupled_source_program.hpp:135
CsProgram prog[kCsMaxTerms]
Definition coupled_source_program.hpp:143
Fixed-capacity postfix program (POD device-copyable): len opcodes, arg read only by PushReg (register...
Definition coupled_source_program.hpp:47
int len
Definition coupled_source_program.hpp:48
int arg[kCsMaxProg]
Definition coupled_source_program.hpp:50
int op[kCsMaxProg]
Definition coupled_source_program.hpp:49
Definition elliptic_problem.hpp:73
Cartesian geometry of a level: index domain + physical bounds [xlo, xhi] x [ylo, yhi].
Definition geometry.hpp:20
POPS_HD Real dy() const
Grid spacing in y (= (yhi - ylo) / domain.ny()). POPS_HD.
Definition geometry.hpp:33
POPS_HD Real dx() const
Grid spacing in x (= (xhi - xlo) / domain.nx()). POPS_HD.
Definition geometry.hpp:31
AGGREGATED report (whole block, all substeps of one advance) of the implicit-source Newton.
Definition implicit_stepper.hpp:165
INDEX-SPACE footprint of an AMR fine patch, exposed to Python by AmrSystem::patch_boxes().
Definition patch_box.hpp:30
Per-direction periodicity: halo wrapping in x and/or y during the exchange (false = open edge,...
Definition fill_boundary.hpp:37
Dense grid of 0/1 markers over a box, input to Berger-Rigoutsos clustering.
Definition tag_box.hpp:35
A model's variable set: kind (cons/prim), names, size, canonical roles (optional, parallel to names; ...
Definition variables.hpp:58
std::vector< VariableRole > roles
parallel to names; empty = roles not provided
Definition variables.hpp:62
int index_of(VariableRole role) const
Index of the component carrying role (first occurrence), -1 if absent.
Definition variables.hpp:67
TagBox: dense grid of markers (0/1) over a region, input to Berger-Rigoutsos clustering.
Descriptor of a model's variables (Vars).