SystemStepper< Impl > Class Template ReferenceΒΆ

adc_cpp: pops::stepper::SystemStepper< Impl > Class Template Reference
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
pops::stepper::SystemStepper< Impl > Class Template Reference

SystemStepper<Impl>: see the contract above. More...

#include <system_stepper.hpp>

+ Collaboration diagram for pops::stepper::SystemStepper< Impl >:

Public Member Functions

 SystemStepper (Impl *owner)
 
void set_scheme (SplitScheme scheme)
 Chooses the time splitting policy (default Lie = bit-identical). See SplitScheme.
 
SplitScheme scheme () const
 
void apply_couplings (Real dt)
 Inter-species COUPLING sources: applied by SPLITTING (one explicit additive step of dt) AFTER the transport of each block.
 
void apply_coupled_freq_expr_bounds (double cfl, double &dt, std::string *reason) const
 Step bound from PER-CELL COUPLED FREQUENCIES (CoupledSource.frequency with an Expr, refinement of the CONSTANT frequency).
 
Real cfl_grid_h () const
 MIN physical step of the grid, shared by step_cfl / step_adaptive: Cartesian = min(dx, dy); POLAR = min(dr, r_min * dtheta) – the azimuthal physical step r*dtheta is minimal at the inner radius r_min of the ring (the most constraining edge for the CFL).
 
void apply_global_dt_bounds (double &dt, std::string *reason) const
 GLOBAL step bounds (System::add_dt_bound): multi-block coupling, Schur/Poisson, AMR/scheduler.
 
void apply_projections ()
 PROJECTION PONCTUELLE post-pas (ADC-177) : U <- project(U, aux) par bloc, appliquee UNE fois a la FIN de chaque macro-pas ENTIER (apres transport + etage source + couplages ; jamais par etage RK), sur les cellules VALIDES seulement.
 
void run_source_stage (typename Impl::Species &s, Real eff_dt)
 Schur-CONDENSED SOURCE STAGE (OPT-IN, cf.
 
void advance_transport_n (typename Impl::Species &s, Real dt, int n)
 TRANSPORT ADVANCE of block s over dt in n substeps, DISPATCHED by the System geometry mode (worksite T5-PR3).
 
void advance_transport (typename Impl::Species &s, Real eff_dt)
 TRANSPORT ADVANCE of block s over eff_dt in s.substeps substeps, dispatched by the mode (cf.
 
void advance_transport_half (typename Impl::Species &s, Real eff_dt)
 HALF transport advance (Strang): dispatch of advance_transport over half_dt = eff_dt/2 – the disk mode ALSO honors the Strang path (H(dt/2) S(dt) H(dt/2)).
 
void advance_due_blocks (double dt)
 FULL-STEP advance of every DUE block over dt: effective step eff_dt = stride * dt (cadence catch-up), then transport (dispatched by the geometry mode, cf.
 
void run_program_cadence (double dt)
 Runs ONE macro-step of length dt through an INSTALLED compiled time Program (epic ADC-399): the SYSTEM-level cadence (substeps + stride, ADC-411) wrapped around the opaque program closure, then the clock tick.
 
void step (double dt)
 One macro-step of length dt.
 
void step_strang (double dt)
 One STRANG macro-step (symmetric, 2nd order): H(dt/2); S(dt); H(dt/2).
 
void advance (double dt, int nsteps)
 Advances by nsteps macro-steps of length dt (loop over step).
 
double step_cfl (double cfl)
 One macro-step at CFL dt: dt = min over the evolving blocks of the block step BOUNDS, then advances like step.
 
double step_adaptive (double cfl)
 One MULTIRATE macro-step: the macro-step = stable step of the SLOWEST block; each faster block is subcycled n_b = ceil(stride_b * w_b / w_min) times.
 
const std::string & last_dt_reason () const
 Name of the ACTIVE bound (the one that fixed dt) of the last step_cfl: "transport:<block>", "source_frequency:<block>", "stability_dt:<block>", "global:<label>", "degenerate", or "" if no step_cfl has run yet.
 

Static Public Member Functions

static bool stride_due (int macro_step, int stride)
 True if a block of cadence stride CATCHES UP at this macro-step (END of window).
 

Detailed Description

template<class Impl>
class pops::stepper::SystemStepper< Impl >

SystemStepper<Impl>: see the contract above.

All methods are MEMBERS because they share the step orchestration; accesses to the SHARED state of Impl go through owner_-> verbatim. Templated on Impl to stay free of any dependency on the (private) definition of System::Impl.

Constructor & Destructor Documentation

◆ SystemStepper()

template<class Impl >
pops::stepper::SystemStepper< Impl >::SystemStepper ( Impl *  owner)
inlineexplicit
Parameters
ownerback-pointer to System::Impl (lifetime subordinate to that of Impl).

Member Function Documentation

◆ advance()

template<class Impl >
void pops::stepper::SystemStepper< Impl >::advance ( double  dt,
int  nsteps 
)
inline

Advances by nsteps macro-steps of length dt (loop over step).

+ Here is the call graph for this function:

◆ advance_due_blocks()

template<class Impl >
void pops::stepper::SystemStepper< Impl >::advance_due_blocks ( double  dt)
inline

FULL-STEP advance of every DUE block over dt: effective step eff_dt = stride * dt (cadence catch-up), then transport (dispatched by the geometry mode, cf.

advance_transport) and the OPT-IN Schur source stage (no-op otherwise, cf. run_source_stage). Frozen blocks (!evolve) and HELD blocks (outside their stride window, cf. stride_due) are skipped. This is the verbatim per-block loop shared by step (Lie) and step_cfl. The two OTHER macro-steps cannot reuse it: step_strang splits transport into two HALF advances around a re-solved source stage (3 interleaved sub-loops), and step_adaptive subcycles each block n_b times (advance_transport_n). It introduces NO NEW collective: the underlying transport (halo exchanges) and Schur source stage (fill_ghosts + all_reduce in the condensed solve) DO communicate, but this helper iterates the same blocks in the same order, so each rank issues the SAME sequence of collectives as the inlined loops did – no MPI desync, bit-identical.

+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ advance_transport()

template<class Impl >
void pops::stepper::SystemStepper< Impl >::advance_transport ( typename Impl::Species &  s,
Real  eff_dt 
)
inline

TRANSPORT ADVANCE of block s over eff_dt in s.substeps substeps, dispatched by the mode (cf.

advance_transport_n). Reuses s.substeps as the former s.advance of the step / step_cfl steps.

+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ advance_transport_half()

template<class Impl >
void pops::stepper::SystemStepper< Impl >::advance_transport_half ( typename Impl::Species &  s,
Real  eff_dt 
)
inline

HALF transport advance (Strang): dispatch of advance_transport over half_dt = eff_dt/2 – the disk mode ALSO honors the Strang path (H(dt/2) S(dt) H(dt/2)).

+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ advance_transport_n()

template<class Impl >
void pops::stepper::SystemStepper< Impl >::advance_transport_n ( typename Impl::Species &  s,
Real  dt,
int  n 
)
inline

TRANSPORT ADVANCE of block s over dt in n substeps, DISPATCHED by the System geometry mode (worksite T5-PR3).

This is the SOLE wiring point of the disk in the step (the 4 steps – step, step_strang, step_cfl, step_adaptive – go through here):

  • None (default): s.advance (assemble_rhs, full Cartesian). BIT-IDENTICAL.
  • Staircase, fixed disk: s.advance_masked (assemble_rhs_masked, 0/1 mask).
  • CutCell, fixed disk: s.advance_eb (assemble_rhs_eb, cut-cell EB). An embedded-boundary mode requested WITHOUT a fixed domain (eb_set_ == false) FALLS BACK to s.advance: the mode alone (without set_disc_domain) must not change the transport. A mode with a fixed domain but on a block that DID NOT build the embedded-boundary advance (e.g. polar block / loaded from an earlier .so) raises an EXPLICIT error rather than SILENTLY playing the full path (the T2 footgun: believing the boundary active while the transport ignores it). The embedded-boundary advances MIMIC s.advance (same RK / IMEX scheme, same limiter / flux); only the transport residual is dispatched.
+ Here is the caller graph for this function:

◆ apply_coupled_freq_expr_bounds()

template<class Impl >
void pops::stepper::SystemStepper< Impl >::apply_coupled_freq_expr_bounds ( double  cfl,
double &  dt,
std::string *  reason 
) const
inline

Step bound from PER-CELL COUPLED FREQUENCIES (CoupledSource.frequency with an Expr, refinement of the CONSTANT frequency).

For each registered program: reduces the MAX of mu(U) over the LOCAL fabs of the FIRST input block (CoupledFreqKernel, named device-clean functor; same MPI-safe convention as apply_couplings), GLOBAL all_reduce_max, then dt <= cfl / max(mu). Updates dt (and reason if non-null) if the bound is tighter. max(mu) <= 0 = no bound this step. Reason "coupled_source:<label>" – SAME prefix as the constant frequency, for a uniform diagnostic. Per-cell counterpart of the constant loop of step_cfl / step_adaptive; no per-cell source registered -> empty loop, bit-identical trajectory.

MPI: all_reduce_max is called by ALL ranks, the SAME number of times (coupled_freq_exprs_ is identical on all ranks) -> symmetric collective, identical dt everywhere (no deadlock). A rank with no local box reduces m=0 (neutral for MAX). WARNING: the Array4 are rebuilt at EACH step (the fabs may be reallocated), like apply_couplings.

+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ apply_couplings()

template<class Impl >
void pops::stepper::SystemStepper< Impl >::apply_couplings ( Real  dt)
inline

Inter-species COUPLING sources: applied by SPLITTING (one explicit additive step of dt) AFTER the transport of each block.

Each coupling is a for_each_cell (DEVICE kernel) reading / updating several blocks at the same point; they order after the transport on the same execution space, hence no prior device_fence (no more host access).

+ Here is the caller graph for this function:

◆ apply_global_dt_bounds()

template<class Impl >
void pops::stepper::SystemStepper< Impl >::apply_global_dt_bounds ( double &  dt,
std::string *  reason 
) const
inline

GLOBAL step bounds (System::add_dt_bound): multi-block coupling, Schur/Poisson, AMR/scheduler.

One HOST evaluation per step and per bound; <= 0 or non-finite = does not constrain this step (neutralized to +inf BEFORE the global min). ALL_REDUCE_MIN mandatory: the callback is evaluated PER RANK (it may read a rank-local state); without the global min each rank would choose a different dt -> desynchronized step collectives (Krylov / fill_boundary) -> MPI deadlock. In serial all_reduce_min is the identity (bit-identical). reason, if non-null, is set to "global:<label>" for the winning bound (step_cfl tracks it; step_adaptive passes nullptr). MPI: dt_bounds_ is identical on all ranks and if(!g.fn) is rank-uniform, so the collective is symmetric (same count/order on every rank) – factoring keeps the deadlock-safety unchanged.

+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ apply_projections()

template<class Impl >
void pops::stepper::SystemStepper< Impl >::apply_projections ( )
inline

PROJECTION PONCTUELLE post-pas (ADC-177) : U <- project(U, aux) par bloc, appliquee UNE fois a la FIN de chaque macro-pas ENTIER (apres transport + etage source + couplages ; jamais par etage RK), sur les cellules VALIDES seulement.

Les GHOSTS ne sont pas projetes : chaque consommateur de ghosts (residu de transport) refait fill_ghosts en tete d'evaluation (cf. BlockRhsEval), donc l'etat fantome est reconstruit du valide projete au pas suivant – aucun fill_boundary ici. Appliquee a TOUS les blocs evolutifs munis d'une projection, y compris les blocs TENUS par leur cadence stride : leur etat peut avoir change via les couplages, et une projection etant IDEMPOTENTE par contrat (cf. HasPointwiseProjection), la re-application sur un etat deja projete est neutre. Bloc sans projection (s.project vide) : jamais interroge – cout nul, les 4 pas (step / step_strang / step_cfl / step_adaptive) restent bit-identiques a l'historique.

+ Here is the caller graph for this function:

◆ cfl_grid_h()

template<class Impl >
Real pops::stepper::SystemStepper< Impl >::cfl_grid_h ( ) const
inline

MIN physical step of the grid, shared by step_cfl / step_adaptive: Cartesian = min(dx, dy); POLAR = min(dr, r_min * dtheta) – the azimuthal physical step r*dtheta is minimal at the inner radius r_min of the ring (the most constraining edge for the CFL).

Reads rank-local geometry only (no collective).

+ Here is the caller graph for this function:

◆ last_dt_reason()

template<class Impl >
const std::string & pops::stepper::SystemStepper< Impl >::last_dt_reason ( ) const
inline

Name of the ACTIVE bound (the one that fixed dt) of the last step_cfl: "transport:<block>", "source_frequency:<block>", "stability_dt:<block>", "global:<label>", "degenerate", or "" if no step_cfl has run yet.

Diagnostic (System::last_dt_bound).

◆ run_program_cadence()

template<class Impl >
void pops::stepper::SystemStepper< Impl >::run_program_cadence ( double  dt)
inline

Runs ONE macro-step of length dt through an INSTALLED compiled time Program (epic ADC-399): the SYSTEM-level cadence (substeps + stride, ADC-411) wrapped around the opaque program closure, then the clock tick.

Shared by step() (Lie path) and step_cfl() (CFL path) so both route a compiled program through the SAME cadence, keeping them consistent. The Program OWNS the whole step body (solve_fields, RHS, combine, commit – all via ProgramContext); the runtime adds no implicit solve_fields / couplings / projections here (cf. step()): they are the Program's job.

SUBSTEPS + STRIDE (ADC-411), mirroring the native per-block advance loop (advance_due_blocks):

  • stride M: GLOBAL hold-then-catch-up. The whole program is HELD on the macro-steps where stride_due is false, then runs ONCE with the effective step eff_dt = M*dt at the window end. A compiled program is ONE whole-system closure, so the stride is GLOBAL (whole-system); this equals native per-block stride ONLY for a single-block system (or all blocks sharing M).
  • substeps n: subdivides the EFFECTIVE step into n calls program_step_(eff_dt/n), mirroring native advance_transport_n(s, eff_dt, n) -> eff_dt/n. BUT program_step_(h) re-runs the WHOLE program (its solve_fields included), whereas native substeps subdivides ONLY the transport (solve_fields + source run ONCE per macro-step). So n>1 here is bit-exact vs native substeps ONLY for an UNCOUPLED / transport-only program (solve_fields inert). The clock ticks EVERY macro-step (held steps included), matching native. Default cadence 1/1 is byte-identical to the single program_step_(dt) call: stride_due(_, 1) is always true and n == 1 collapses the loop to one call with h == dt.
+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ run_source_stage()

template<class Impl >
void pops::stepper::SystemStepper< Impl >::run_source_stage ( typename Impl::Species &  s,
Real  eff_dt 
)
inline

Schur-CONDENSED SOURCE STAGE (OPT-IN, cf.

set_source_stage). No-op if the block has no source stage (s.schur == nullptr): the default path stays BIT-IDENTICAL. Otherwise, AFTER the hyperbolic transport of the block (already played by s.advance), we play the AUTONOMOUS source stage (CondensedSchurSourceStepper, #126) on the post-transport state:

  • state = s.U (rho frozen in the source, mom/E updated);
  • phi = the system Poisson potential (ell_phi(), warm start phi^n from solve_fields at the head of step) – the stage solves its OWN condensed operator and WRITES phi^{n+1} into it, it does NOT call solve_fields again (no duplication);
  • B_z = aux channel at index kAuxBaseComps (populated + ghosts filled by solve_fields). theta/dt of the theta-scheme; dt = eff_dt (stride factor already included by the caller, like s.advance).
+ Here is the caller graph for this function:

◆ scheme()

template<class Impl >
SplitScheme pops::stepper::SystemStepper< Impl >::scheme ( ) const
inline
+ Here is the caller graph for this function:

◆ set_scheme()

template<class Impl >
void pops::stepper::SystemStepper< Impl >::set_scheme ( SplitScheme  scheme)
inline

Chooses the time splitting policy (default Lie = bit-identical). See SplitScheme.

+ Here is the call graph for this function:

◆ step()

template<class Impl >
void pops::stepper::SystemStepper< Impl >::step ( double  dt)
inline

One macro-step of length dt.

ORDER INVARIANT per scheme (cf. SplitScheme):

  • Lie (default, bit-identical): solve_fields; for each DUE block (stride cadence honored) advance(dt) then run_source_stage(dt) interleaved; couplings; t += dt; ++macro_step.
  • Strang: H(dt/2); S(dt); H(dt/2), with a solve_fields RE-SOLVED between each stage (cf. step_strang() and docs/HOFFART_STEP_SEQUENCE.md).
+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ step_adaptive()

template<class Impl >
double pops::stepper::SystemStepper< Impl >::step_adaptive ( double  cfl)
inline

One MULTIRATE macro-step: the macro-step = stable step of the SLOWEST block; each faster block is subcycled n_b = ceil(stride_b * w_b / w_min) times.

aux frozen over the macro-step (coupling once-per-step).

Returns
the macro-step.

OPTIONAL BOUNDS (audit 2026-06): like step_cfl, the macro-step is then REDUCED by the block bounds (source_frequency / stability_dt, applied to the effective substep stride_b*macro_dt/n_b – n_b does not depend on dt, so the clamp is exact) and by the GLOBAL bounds (P->dt_bounds_). Without optional bounds, macro_dt is STRICTLY historical.

+ Here is the call graph for this function:

◆ step_cfl()

template<class Impl >
double pops::stepper::SystemStepper< Impl >::step_cfl ( double  cfl)
inline

One macro-step at CFL dt: dt = min over the evolving blocks of the block step BOUNDS, then advances like step.

Returns
the dt used. SUBSTEPS-AWARE (post-#121): bit-identical to the old formula only for substeps=1 (cf. backward-compatibility note).

STEP POLICY (audit 2026-06, step_cfl worksite): the historical TRANSPORT bound dt <= cfl*h*substeps_b/(stride_b*w_b) stays the base, but the step now AGGREGATES, per block:

  • the SOURCE FREQUENCY bound (s.source_frequency, HasSourceFrequency trait): effective substep stride*dt/substeps <= cfl/mu -> dt <= cfl*substeps/(stride*mu), WITHOUT h (a local source bounds in 1/time, not in length/time);
  • the direct ADMISSIBLE STEP (s.stability_dt, HasStabilityDt trait): stride*dt/substeps <= dt_adm -> dt <= dt_adm*substeps/stride, WITHOUT cfl (the model already declares an admissible step);
  • the CFL speed itself can be the declared STABILITY speed (HasStabilitySpeed trait): s.max_speed is then wired onto stability_speed (cf. make_max_speed). Then the GLOBAL bounds (P->dt_bounds_: multi-block coupling, Schur/Poisson, AMR/scheduler, set by System::add_dt_bound): dt <= fn() each, one HOST evaluation per step (no per-cell callback). A block / a system WITHOUT optional bounds keeps a step STRICTLY identical to history (empty functions are not queried). The ACTIVE bound of the last step is consultable via last_dt_bound() ("transport:<block>", "source_frequency:<block>", "stability_dt:<block>", "global:<label>", "degenerate"). 2D CONVENTION NOTE (audit ADC-182): the per-cell speed is w = max(wx, wy) – NOT the sum. In unsplit 2D the effective Courant number thus reaches 2*cfl when wx ~ wy: cfl = 0.4 (case default) stays < 1 (safe), this is also the convention of the sweep references (HLL step); cfl >= 0.5 in unsplit 2D is MARGINAL – to avoid without study.
+ Here is the call graph for this function:

◆ step_strang()

template<class Impl >
void pops::stepper::SystemStepper< Impl >::step_strang ( double  dt)
inline

One STRANG macro-step (symmetric, 2nd order): H(dt/2); S(dt); H(dt/2).

Reuses s.advance for the HALF transport advances and run_source_stage for the FULL source stage (no new stepper).

phi CONSISTENCY (critical point, cf. docs/HOFFART_STEP_SEQUENCE.md): the potential phi / the aux fields (grad phi) that the transport READS are populated by solve_fields from the CURRENT density. The SINGLE solve_fields at the head of the step (sufficient for Lie splitting, where a single transport advance follows) DOES NOT SUFFICE here: between the 1st half-advance and the 2nd, the density has changed (1st half-advance + source stage), so the 2nd half-advance would read a STALE phi. We thus RE-SOLVE solve_fields BEFORE each stage that consumes phi:

  1. solve_fields() -> phi consistent with rho^n (for H(dt/2))
  2. H(dt/2) (s.advance over eff_dt/2)
  3. solve_fields() -> phi consistent with rho after H(dt/2) (for S(dt): warm start / aux field)
  4. S(dt) (run_source_stage over eff_dt; the Schur stage WRITES phi^{n+1})
  5. solve_fields() -> phi consistent with rho after S(dt) (for the 2nd H(dt/2))
  6. H(dt/2) (s.advance over eff_dt/2) Without step 5, the 2nd half-advance would read either the Schur phi (overwritten at the next step anyway) or a phi of stale rho: the Strang symmetry (thus the 2nd order) would be broken. Steps 1/3/5 are SYSTEM solve_fields (sum over all blocks), outside the block loop.

stride CADENCE: evaluated ONCE per macro-step (stride_due at the START), so that the two half-advances and the source stage of one macro-step concern the SAME set of DUE blocks. The effective step eff_dt = dt * stride (catch-up) is identical to Lie; only the transport is split into two halves eff_dt/2.

+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ stride_due()

template<class Impl >
static bool pops::stepper::SystemStepper< Impl >::stride_due ( int  macro_step,
int  stride 
)
inlinestatic

True if a block of cadence stride CATCHES UP at this macro-step (END of window).

STRIDE SEMANTICS = HOLD-THEN-CATCH-UP (catch-up at the END of the window). A block of cadence M is HELD (not advanced) on the macro-steps where (macro_step + 1) % M != 0, then advances by an effective step M*dt at the macro-step where (macro_step + 1) % M == 0, i.e. at the END of its window of M macro-steps. At macro-step k, the system time is (k+1)*dt and the block that CATCHES UP has then advanced by the same cumulative (k+1)*dt: it is temporally CONSISTENT with the fast blocks, never "in the future". (The old semantics advanced at the START of the window, macro_step % M == 0: at k=0 the block already advanced M*dt while the system advanced only dt -> anticipated block, wrong Poisson/source coupling.)

+ Here is the caller graph for this function:

The documentation for this class was generated from the following file: