Python API

This page documents the stable user-facing shape of pops. The implementation also contains runtime seams used by tests and bindings; those are not the authoring path.

For the rules, see public API contract.

Main flow

import pops
from pops.codegen import Production

compiled = pops.compile(case, backend=Production())
sim = pops.bind(compiled, state=initial_state, params=params)
sim.run(t_final=1.0, cfl=0.4)

pops.compile validates and lowers the Case. pops.bind attaches data and constructs the C++ runtime from the case layout.

Top-level assembly

class pops.Case(layout=None, name=None)[source]

Bases: object

A typed, inert top-level assembly: layout + blocks + fields + params + aux + outputs.

Case(layout=Uniform(CartesianMesh()), name="plasma") then chained:

case = (pops.Case(name="plasma")
        .block("ne", physics=model, spatial=pops.FiniteVolume())
        .field(pops.fields.FieldProblem(unknown=phi, equation=eq, solver=mg))
        .time(pops.time.Program(...)))

Each assembly setter RETURNS the case so calls chain. The default layout is a single-level Uniform over a default CartesianMesh. validate() runs structural checks; multi-block assemblies, named non-Poisson elliptic fields and output / checkpoint policies all lower (C3 / C1-System / C4); compile / bind flow them onto the runtime. AMR per-level output (Plotfile / level filtering) is the one remaining deferral (ADC-511).

A Case CONTAINS descriptors (the layout, the blocks’ physics, the field problems) but is NOT itself a pops.descriptors.Descriptor (Spec 5 sec.6 table / sec.15). It exposes the same inspectable surface – requirements / capabilities / options / available / validate / inspect / lower – implemented DIRECTLY here, so it duck-types as a route-describing object without inheriting a descriptor identity.

property amr

The AMR refinement-policy handle (raises ValueError if layout is not AMR).

aux(name, value=None)[source]

Declare a static aux input name (e.g. a background field). Chains.

available(context=None)[source]

An EXPLAINABLE availability status, computed from the parts (no runtime).

block(name, physics, spatial=None)[source]

Declare a physics block name (its physics model is REQUIRED). Chains.

explain_routes()[source]

Return a printable route matrix sourced from the C++ authoritative facts (sec.13.12.1).

Spec 5 sec.13.12.1 (criterion #37): a feature x layout x backend x platform -> status / limitation matrix whose capability VALUES come from the C++ core (_pops.module_capabilities()), NOT a Python-derived walk. Each transport feature (uniform / amr / mpi / gpu / stride / named_fields / partial_imex_mask) is reported with the native available / unavailable status the built module decides, plus the route facts the descriptor catalog adds (the layouts this case can author). Inert: it imports _pops LAZILY and reads metadata only – no compile, bind or run.

When _pops is absent or predates module_capabilities (old build), the matrix reports every native status as unknown with that reason rather than fabricating a value.

field(field_problem)[source]

Register an elliptic FieldProblem (keyed on its name). Chains.

lower(context=None)[source]

The inert lowering record for the assembly (metadata only; no computation).

Mirrors the descriptor lower() shape so a Case duck-types as a route-describing object, without being a pops.descriptors.Descriptor. It runs no numeric loop and touches no runtime; pops.compile does the real lowering through compile_problem.

native_id = None

A Case names a pure-Python assembly, not a single native C++ symbol.

output(policy)[source]

Attach an output / checkpoint policy. Chains.

The policy is stored on the Case; bind() flows it onto the bound System and run() fires it at each policy cadence through the existing write / checkpoint writers (ADC-509). AMR per-level output (Plotfile / level filtering) remains deferred (ADC-511).

param(name, default=None, *, kind=<object object>)[source]

Declare a runtime/const parameter and its default value. Chains.

The KIND is a TYPED param object (Spec 5 sec.7), not a kind= string: case.param(pops.physics.RuntimeParam("alpha", 1.0)) / case.param(pops.physics.ConstParam("gamma", 1.4)) / case.param("alpha", 1.0) (const shorthand). A bare kind="const"/"runtime" keyword is REJECTED – the string route is removed; the typed sugars lower to the SAME {default, kind} record.

time(program)[source]

Attach the time scheme (a pops.time.Program) used at compile. Chains.

validate(context=None)[source]

Structural validation; raise LOUD on a structural error (never fake success).

Checks the layout, that there is at least one block each with a physics model, that every field problem is itself valid, and that no block and field share a name. Multi-block assemblies, named non-Poisson elliptic fields and output / checkpoint policies now LOWER (C3 / C1-System / C4); each output entry is only confirmed to be a real policy descriptor here (a non-policy object is rejected loud). AMR per-level output is deferred (ADC-511).

Runtime facade returned by bind

The object returned by pops.bind is the simulation facade. User code should obtain it from bind, not by hand-building runtime classes.

class pops.System(config=None, mesh=None, **cfg_kw)[source]

Bases: _SystemInstall, _SystemUnifiedInstall, _SystemAuxState, _SystemDiagnostics, _SystemIO

The system/coupler: composes blocks, shares a Poisson, advances the whole.

Low-level runtime. The documented PUBLIC path is the typed pops.Case assembly lowered by pops.compile and wired by pops.bind -> sim.run(...); the per-step step_cfl / step / step_adaptive methods (and add_block / add_equation / set_poisson) are the low-level seam pops.bind builds on and the tests use, not the recommended front door.

add_block takes a composed model (pops.Model(…)) + Spatial / Explicit / IMEX objects. Everything else (set_poisson, set_density, step, step_cfl, step_adaptive, diagnostics, primitives eval_rhs/get_state/set_state) is forwarded to the compiled facade.

GEOMETRY: the choice lives in a MESH object passed as mesh= (pops.CartesianMesh / pops.PolarMesh), NOT in the scheme (pops.FiniteVolume stays reconstruction + Riemann + variables). Default (mesh=None or pops.CartesianMesh) = square domain, bit-identical to the history. pops.PolarMesh (global ring) is WIRED in System.step (Phase 2b): polar ExB transport + polar Poisson + aux in local basis (e_r, e_theta). Limits: scalar ExB transport, single-rank, no cart<->polar coupling.

static abi_key()[source]

Module ABI key (compiler, C++ standard, signature of the pops headers). Compared to that of a native loader by add_native_block. Also exposed as a class attribute (the __getattr__ delegate only covers instances), so pops.System.abi_key() works.

property amr

The AMR runtime inspection surface does not apply to a uniform System.

System is single-level: it carries no AMR hierarchy, so sim.amr (the live patch / regrid / ghost / reflux / checkpoint reports of Spec 5 sec.8.12) is an AmrSystem-only handle. Build an pops.AmrSystem for a refined run, or use the STATIC authoring report pops.inspect_amr(layout) for a layout descriptor. Accessing it raises a clear AttributeError (sourced in __getattr__ so the message is single).

block_names()[source]

Names of the added blocks, in order (useful for a Python integrator).

Delegates to the C++ block registry (single source), so it includes the blocks loaded via add_dynamic_block (.so JIT) and add_compiled_block (.so AOT), not only add_block.

profile(profile=None)[source]

Typed profiling context manager (Spec 5 sec.12.5, criteria 41-44).

Usage:

with sim.profile(pops.Profile.Basic()) as prof:
    sim.run(0.1)
print(prof.summary())

@p profile is a pops.Profile level (Profile.Basic() / Profile.Advanced()); with no argument the level comes from POPS_PROFILE (unset / off -> Basic()). The manager enables the native profiler on entry and disables it on exit, so a plain run (no with sim.profile()) leaves profiling off – the off-by-default contract. prof.summary() returns a pops.PerformanceSummary.

run(t_end, cfl=None, max_steps=1000000, output_dir=None)[source]

Advance up to t_end by CFL steps (sugar: while time() < t_end: step_cfl(cfl)).

@p cfl: Courant number passed to step_cfl. When omitted (None) it defaults to the CFL pinned by an installed CompiledTime(cfl=X) cadence, else 0.4 – so a numeric cadence cfl actually takes effect on a bare sim.run(t_end) rather than being silently ignored. @p max_steps: guard (avoids an infinite loop if dt -> 0). @p output_dir: when output / checkpoint policies were flowed onto this System (pops.bind from a Case with .output(policy)), the directory the run writes them to; each policy fires at its own cadence through the existing write()/checkpoint writers (C4 / ADC-509). Defaults to the current directory when policies are present and output_dir is omitted. Returns the number of steps taken. cf. DSL_MODEL_DESIGN.md section 6.

class pops.AmrSystem(config=None, **cfg_kw)[source]

Bases: _AmrSystemEquation, _AmrSystemIO

Refined counterpart of System : one or SEVERAL blocks carried on an AMR hierarchy.

SINGLE-BLOCK (1 add_block) : historical AmrCouplerMP path (dynamic regrid, reflux). MULTI-BLOCK (>= 2 add_block) : N blocks co-located on ONE SHARED AMR hierarchy (AmrRuntime engine), SYSTEM Poisson with co-located SUMMED right-hand side (Sum_b q_b n_b), conservation PER BLOCK. The blocks may have DIFFERENT SPATIAL SCHEMES, a per-block TEMPORAL TREATMENT (explicit / imex), MULTIRATE (substeps / stride), COUPLED inter-species SOURCES and the multi-block production DSL. In multi-block the block NAME indexes set_density(name) / mass(name) / density(name).

UNION-OF-TAGS REGRID (multi-block + regrid_every > 0) : the shared hierarchy is re-gridded from the UNION of the tags of all blocks. Two criteria compose (cell-by-cell OR) :

  • PER-BLOCK VARIABLE (set_refinement(threshold, variable=, role=)) : refine where the SELECTED variable of a block exceeds threshold. Default = component 0 (historical density), bit-identical ; ADC-296 lets you select it per block by name (variable=) or physical role (role=), resolved against the block’s conserved variables (a block lacking the name/role raises, no silent component-0 fallback). Non-default selector is multi-block only (mono-block / compiled .so : component 0 only) ;

  • grad phi (set_phi_refinement(grad_threshold)) : refine where the norm of the gradient of the electrostatic potential exceeds grad_threshold (diocotron ring edge). Disabled by default (grad_threshold <= 0). MULTI-BLOCK only.

regrid_every == 0 -> FROZEN hierarchy (regrid never called, bit-identical).

add_block(name, model, spatial=None, time=None)[source]

Installs an evolved block composed of NATIVE BRICKS on the shared AMR hierarchy.

Low-level runtime seam. The documented PUBLIC path is the typed pops.Case(layout=AMR(...)) assembly lowered by pops.compile and wired by pops.bind (which calls this internally); add_block stays for that seam and the tests.

Refined counterpart of System.add_block. The 1st add_block opens the single-block path (AmrCouplerMP : dynamic regrid, reflux) ; each subsequent add_block co-locates one more block on THE SAME hierarchy (AmrRuntime engine, system Poisson with summed right-hand side). In multi-block the name indexes set_density(name) / mass(name) / density(name). The arguments are marshaled to the C++ facade (AmrSystem::add_block), which validates the block against the model. For a compiled DSL model (.so) or a dispatch on the model type, use add_equation.

@param name unique name of the block. @param model an pops.Model(…) (ModelSpec : composed native bricks). @param spatial spatial discretization, an pops.Spatial(…) / pops.FiniteVolume(…) (default

minmod + rusanov + conservative). Limiter (none / minmod / vanleer / weno5 ; weno5 = 3 ghosts, the coupler allocates its levels at Limiter::n_ghost and the regrid inherits n_grow()), Riemann flux (rusanov / hll / hllc / roe) and reconstructed variables (conservative / primitive).

@param time temporal treatment, an pops.Explicit (default) / pops.IMEX / pops.SourceImplicit.

Carries substeps, stride (multirate hold-then-catch-up), the implicit mask (implicit_vars / implicit_roles) and the Newton options, threaded to the C++. newton_diagnostics is wired in native multi-block and rejected at the C++ build in single-block (the coupler does not aggregate a report).

@throws TypeError if time is an pops.Split / pops.Strang (Schur-condensed source stage) :

go through add_equation(…, time=pops.Strang(…)) (amr-schur path).

spatial.positivity_floor > 0 (ADC-259) floors the Density-role face states AND the coarse-fine fine ghost means to >= floor on the AMR transport (Zhang-Shu, parity with the uniform System). Guarantee = face / ghost-state Density positivity only (order-1 fallback), NOT updated-mean nor pressure positivity. A model without a Density role rejects it at the first step. The COMPILED .so path carries it too now (ADC-322): a loader regenerated against the current headers marshals the floor (add_equation on a CompiledModel, add_native_block).

add_coupling(coupling)[source]

Add a generic inter-species COUPLED SOURCE (pops.dsl.CoupledSource(…).compile(…)) on the SHARED AMR hierarchy (MULTI-BLOCK), refined counterpart of System.add_coupling. The source is transported as bytecode and interpreted on the C++ side (AmrSystem.add_coupled_source; no per-cell Python callback). The coupling frequency (CoupledSource.frequency) is honored: constant -> dt bound dt <= cfl/mu; Expr -> PER-CELL frequency mu(U) evaluated on the COARSE grid at each step_cfl (the freq_prog_* vectors are forwarded). Must be called BEFORE the first step (the source is frozen then injected at the lazy build of the runtime engine).

property amr

The live AMR runtime inspection handle (Spec 5 sec.8.12), an pops.runtime.amr.AmrRuntimeView.

Bound to THIS built hierarchy: sim.amr.patch_table() / sim.amr.hierarchy_snapshot() / sim.amr.explain_regrid() / explain_ghosts() / explain_reflux() / explain_checkpoint() return short, inert reports of the patches that actually exist, the regrid cadence in force, and the ghost / reflux / checkpoint route limitations. The view READS the runtime (the box accessors + the retained config); it builds / allocates / steps NOTHING.

System.amr does not exist: the inspection surface is AMR-specific (a uniform System carries no hierarchy). Use pops.inspect_amr(layout) for the STATIC authoring report.

coarse_local_boxes()[source]

Number of coarse (base) boxes owned by this MPI rank (ADC-319 diagnostic).

The base level is a MultiFab whose boxes are spread across ranks by a DistributionMapping. Returns this rank’s owned-fab count (level-0 local_size()). With distribute_coarse=True the base is split into several boxes round-robin, so each rank owns a strict subset and the coarse transport is distributed; a replicated or single-box base owns the full count on every rank. Compare with coarse_total_boxes() and pops.n_ranks() to confirm MPI strong-scaling of the base. Triggers the lazy build like n_patches().

coarse_total_boxes()[source]

Total number of coarse (base) boxes across all ranks (ADC-319 diagnostic).

Identical on every rank (BoxArray size, no communication). With distribute_coarse=True this is the number of round-robin base tiles; with a single-box or replicated base it is 1. A rank distributes the coarse transport when coarse_local_boxes() < coarse_total_boxes(). Triggers the lazy build like n_patches().

explain_bind(compiled)[source]

A printable pops.codegen.inspect_report.BindReport of @p compiled vs this AMR sim (Spec 5 sec.12.1, criterion #15). INERT parity with System.explain_bind: reads the artifact’s DECLARED bind inputs (compiled.arguments()) and the blocks / named aux wired on this AmrSystem, then reuses ADC-463 collect_missing_arguments() to report PROVIDED vs still-REQUIRED per group. It binds nothing and mutates nothing – a read-only bind plan.

field(name)[source]

Return the solved potential of a NAMED elliptic field (ADC-428) as an (n, n) array.

Read-back of a second elliptic field declared via m.elliptic_field and lowered on the AMR layout: solves the hierarchy fields if needed (so it is current even before any step) then reads the field’s coarse potential. AMR counterpart of reading System.aux_field(block, name) for an elliptic field. @throws if the field is unregistered (or the system runs the single-block coupler, which carries no named field).

patch_rectangles()[source]

Physical rectangles (x0, y0, width, height) of the current fine patches, in [0, L]^2.

Converts patch_boxes() (index space, inclusive corners) into physical coordinates. The level spacing is dx = L / (n << level) (ratio 2 per level) ; a patch [ilo..ihi] x [jlo..jhi] covers (ihi - ilo + 1) cells in x from x0 = ilo * dx (and likewise in y). Grid convention ne[j, i] -> index 0 = x (i), index 1 = y (j), consistent with density() and an imshow with extent [0, L, 0, L]. Convenient to plot the REAL patches (e.g. matplotlib Rectangle) without rebuilding a density proxy. Returns a list of (x0, y0, w, h), one per fine patch (all fine levels combined). Query (between steps) : triggers the lazy build like n_patches(), no cost on the hot path.

profile(profile=None)[source]

Typed AMR / MPI profiling context manager (Spec 5 sec.12.5, criterion 43).

Usage:

sim.set_refinement(threshold)  # regrid_every > 0 in the config
with sim.profile(pops.Profile.Basic()) as prof:
    for _ in range(n_steps):
        sim.step_cfl(0.4)
print(prof.summary().by_amr_mpi())  # regrid / fill_boundary / average_down timings

@p profile is a pops.Profile level ; with no argument it comes from POPS_PROFILE (unset / off -> Basic()). The manager enables the native AMR profiler on entry and disables it on exit (off-by-default contract). prof.summary().by_amr_mpi() surfaces the AMR phase timings + counters as soon as a regrid / solve fired under the multi-block engine.

The full runtime classes still expose lower-level methods because the C++/pybind layer and tests use them. They are intentionally not examples for user authoring.

Physics authoring

class pops.physics.Model(name)[source]

Bases: _MultiSpeciesMixin

A blackboard-style physical model that lowers to the operator-first IR.

aux(name)[source]

Declare an auxiliary field read by the model (e.g. an imposed B_z).

check()[source]

Validate that every referenced quantity is declared (single-species path).

Multi-species models compose their blocks in a time Program and validate at emit (P.emit_cpp_program / P._check_lowerable), so a model-level check is a single-species notion; it is a no-op for a multi-species model.

property dsl

The underlying pops.dsl.Model (the codegen engine).

field(name)[source]

Declare a solved scalar field (e.g. the potential phi).

field_problem(name, equation, outputs=None, solver=None, bcs=None, coefficients=None)[source]

Author an inspectable elliptic field problem (Spec 5 sec.5.1 / sec.9.6).

The typed-object ergonomic shortcut: it CONSTRUCTS and RETURNS an inert pops.fields.PoissonProblem (or a pops.fields.FieldProblem when coefficients are present, e.g. a screened / anisotropic operator) describing the solve -laplacian(phi) == rhs directly from a pops.math.Equation, and records it on the model’s authoring state so inspect() surfaces it.

Unlike solve_field(), this method is INERT: it lowers ONLY to an inspectable field-problem descriptor; it does NOT touch the dsl model, the elliptic right-hand side, the operator graph, codegen or the runtime. Wiring the problem into the operator graph (a second elliptic operator + aux channel) is the deeper lowering and stays DEFERRED (see solve_field() / the multi-elliptic runtime); this entry point only produces the typed descriptor a user can validate() / inspect() before any run.

Parameters:
  • name – the field-problem name (also the unknown’s display name when not derivable).

  • equation – a pops.math.Equation of the form -laplacian(phi) == rhs.

  • outputs – the produced fields (passed through to the descriptor’s outputs).

  • solver – the elliptic solver descriptor (carried; None leaves it unset so the descriptor’s own available / validate flags the missing solver).

  • bcs – an iterable of field boundary-condition descriptors (pops.fields.bcs).

  • coefficients – an optional operator coefficient; when present the descriptor is a general pops.fields.FieldProblem rather than a PoissonProblem.

finite_volume_rate(name, flux=None, riemann=None, reconstruction=None, sources=())[source]

Declare a rate assembled by the native finite-volume machinery.

Selects the native Riemann solver and reconstruction (by descriptor) and the source terms; lowers to the same rate operator a board equation does. The native-brick hook codegen for a custom Riemann is tracked by ADC-456.

flux(name, on=None, x=None, y=None, waves=None)[source]

Declare the physical flux and (optionally) its characteristic speeds.

x / y are the per-component flux expressions; waves gives the per-direction eigenvalues. Lowers to the model’s default flux.

inspect()[source]

A plain-dict, inert view of the model’s authoring state (Spec 5 sec.12.1).

Reports the declared state / field / flux / source / operator names and the inspectable field problems authored via field_problem() (each as its descriptor’s inspect() dict). Read-only: it touches no numerics, codegen or runtime.

invariant(name, expression=None, over=None)[source]

Declare a generic invariant StateSet -> Scalar from an integral(...).

invariants()[source]

The declared invariants, by name.

local_linear_operator(name, on=None, matrix=None)[source]

Build a local linear operator L: U -> U as a MATH object (not a callable operator). It carries the matrix; register it with operator() (or @module.operator) to obtain a callable operator. Calling the math object directly raises a clear error – see LocalLinearOperatorExpr.

lower()[source]

Lower this writing facade to its pops.model.Module (Spec 5 sec.11).

pops.physics.Model is an AUTHORING facade: it writes the physics (state, primitives, flux, sources, field solves) and lowers to the operator-first IR. It does NOT compile. The documented flow is:

physics_model = pops.physics.Model(...)
model = physics_model.lower()              # -> pops.model.Module
compiled = pops.compile(case_with(model), backend=pops.codegen.Production())

Single-species: the dsl-derived Module (one StateSpace). Multi-species: the multi-block Module this model assembled directly (N StateSpaces + coupled_rate + a multi-block field operator). Identical to :pyattr:`module`; lower is the Spec 5 sec.11 name.

property module

The typed pops.model.Module view (operator-first IR).

Single-species: the dsl-derived Module (one StateSpace). Multi-species: the multi-block Module this model assembled directly (N StateSpaces, a coupled_rate operator, a multi-block field operator) – the SAME operator-first IR a hand-written pops.model.Module would build.

operator(name, handle=None, *, inputs=None, returns=None)[source]

Register a typed, callable operator under name from a math object.

returns (or the positional handle) is the operator body; inputs names its field dependencies (metadata for requirements). A LocalLinearOperatorExpr registers as a local_linear_operator Fields -> LocalLinearOperator(U, U). Returns a CallableOperator.

param(name, value=None, *, kind=<object object>)[source]

Declare a named scalar parameter; returns a usable expression.

The kind is a TYPED param object (Spec 5 sec.7), not a kind= string: param(pops.physics.RuntimeParam("cs2", 1.0)) / param(ConstParam("g", 9.8)) / param("g", 9.8) (const shorthand). A bare kind= keyword is REJECTED.

primitive(name, expr)[source]

Define a primitive quantity by its formula; returns a usable expression.

rate(name, equation)[source]

Declare a rate operator from ddt(U) == -div(F) + sources.

riemann(name, flux=None, pressure=None, velocity=None, sound_speed=None, wave_speeds=None, contact_speed=None, star_state=None)[source]

Select a Riemann solver and validate the model’s capabilities for it.

The native solvers are C++ (pops::RusanovFlux / HLLFlux / HLLCFlux / RoeFlux). HLLC/Roe need model capabilities: a pressure primitive and the fluid roles Density/MomentumX/MomentumY (the dsl enable_hllc / enable_roe then generate the POPS_HD contact_speed / hllc_star_state / roe_dissipation hooks from those roles). Missing capabilities are rejected here with a clear message (Spec 3 criterion 10).

ADC-456: passing an explicit board formula for a capability quantity (e.g. pressure=<pops.math expr>) overrides the role-derived hook with that formula’s codegen (lowered via pops.dsl.Model.set_riemann_hooks()). A capability hook DESCRIPTOR (pops.numerics.riemann.hllc.contact_speed.euler()) or None keeps the role-derived default. A formula referencing a quantity the model cannot provide still raises the clear capability error at codegen.

scalar(name, expr)[source]

Define a named derived scalar (e.g. pressure, sound speed).

solve_field(name, equation=None, outputs=None, solver=None)[source]

Declare an elliptic field solve -laplacian(phi) == rhs.

Lowers to the model’s Poisson coupling; outputs names the produced fields, solver records the required elliptic solver.

source(name, on=None, value=None)[source]

Declare a named local source term; returns a SourceHandle.

species(name, state=(), roles=None)[source]

Declare a named species: a named block instance of its own StateSpace.

Each species lowers to one pops.model.StateSpace and a named block (Spec 3 sections 12, 16). The returned StateHandle unpacks into its component vars and indexes them by name (e["ne"]) for a coupled-rate formula. Arbitrary arity: declare 2, 3, 4, … species. The single-species case is byte-identical to state() (no multi-block Module is created); the multi-block path engages only from the SECOND species, lowering to the existing operator-first multi-block IR (pops.model.Module with N spaces + coupled_rate + solve_fields_from_blocks), never a parallel runtime.

state(name='U', components=(), roles=None)[source]

Declare the conservative state. Returns an unpackable StateHandle.

Board role strings (density / momentum_x / momentum_y / energy / …) are canonicalized to the dsl roles (Density / MomentumX / …) so the native Riemann capabilities (HLLC/Roe role lookup) recognize them.

to_module()

Lower this writing facade to its pops.model.Module (Spec 5 sec.11).

pops.physics.Model is an AUTHORING facade: it writes the physics (state, primitives, flux, sources, field solves) and lowers to the operator-first IR. It does NOT compile. The documented flow is:

physics_model = pops.physics.Model(...)
model = physics_model.lower()              # -> pops.model.Module
compiled = pops.compile(case_with(model), backend=pops.codegen.Production())

Single-species: the dsl-derived Module (one StateSpace). Multi-species: the multi-block Module this model assembled directly (N StateSpaces + coupled_rate + a multi-block field operator). Identical to :pyattr:`module`; lower is the Spec 5 sec.11 name.

vector_field(name, x, y)[source]

Define a named vector field with .x / .y expression components.

The physics facade builds model/operator IR. It does not compile directly in the public flow; the case is compiled by pops.compile.

Native brick composition

Native bricks are already-compiled C++ pieces that can be assembled into a model. They are useful presets, but the public run flow is still Case -> compile -> bind -> run.

pops.Model(state, transport, source, elliptic)[source]

Compose a model (ModelSpec) from state, transport, source, elliptic bricks.

Validates the state <-> transport consistency (Scalar with ExB; compressible FluidState with CompressibleFlux; isothermal with IsothermalFlux) and carries the parameters into the spec.

class pops.Scalar[source]

Bases: object

Scalar state (1 variable, e.g. a transported density).

class pops.FluidState(kind='compressible', gamma=1.4, cs2=0.5, vacuum_floor=0.0)[source]

Bases: object

Fluid state. kind = “compressible” (gamma) or “isothermal” (cs2).

vacuum_floor (isothermal only, ADC-77): quasi-vacuum density floor. When > 0 the model computes the velocity as u = m/max(rho, vacuum_floor), bounding the wave speed and the advective flux where the flow evacuates the background (rho -> ~0). It does NOT modify the conserved state (only the velocity estimate). 0 (default) = inactive (bit-identical). This is independent of the spatial positivity_floor (the Zhang-Shu reconstruction limiter): the two address different failure modes and must be enabled separately. Honored on the native pops.Model(…) / System / AmrSystem path; the compiled/DSL path (pops.CompositeModel / JIT / AOT) does not carry it yet, so set it on the native path.

classmethod compressible(gamma=1.4)[source]

Typed constructor for the COMPRESSIBLE fluid state (Spec 5 sec.14.2.5).

pops.FluidState.compressible(gamma=1.4) is the typed equivalent of pops.FluidState(kind="compressible", gamma=1.4): it builds the SAME inert state object (kind=”compressible”, carrying gamma -> spec.gamma via Model) instead of selecting the kind with a magic string. Pairs with CompressibleFlux (4 variables [rho, rho_u, rho_v, E]).

classmethod isothermal(cs2=0.5, vacuum_floor=0.0)[source]

Typed constructor for the ISOTHERMAL fluid state (Spec 5 sec.14.2.5).

pops.FluidState.isothermal(cs2=0.5, vacuum_floor=0.0) is the typed equivalent of pops.FluidState(kind="isothermal", cs2=0.5, vacuum_floor=0.0): it builds the SAME inert state object (kind=”isothermal”, carrying cs2 -> spec.cs2 and vacuum_floor -> spec.vacuum_floor via Model). Pairs with IsothermalFlux (3 variables [rho, rho_u, rho_v]). See the class docstring for the vacuum_floor (ADC-77) semantics.

class pops.ExB(B0=1.0)[source]

Bases: object

Scalar advection by the E x B drift (magnetic field B0).

class pops.CompressibleFlux[source]

Bases: object

Compressible Euler flux (gamma comes from the FluidState state).

class pops.IsothermalFlux[source]

Bases: object

Isothermal Euler flux (cs2 comes from the FluidState state).

class pops.NoSource[source]

Bases: object

No source.

class pops.PotentialForce(charge=1.0)[source]

Bases: object

Potential force (q/m) rho E on the momentum (+ work if 4 vars).

class pops.GravityForce[source]

Bases: object

Gravitational force rho g (+ work if 4 vars).

class pops.MagneticLorentzForce(charge=1.0)[source]

Bases: object

MAGNETIC Lorentz force q (v x B_z) on the momentum (native C++ brick pops::MagneticLorentzForce, exposed to the Python API by the 2026-06 audit).

EXPLICIT regime (moderate omega_c): pointwise algebraic term, no work (F . v = 0, energy unchanged). Reads B_z from the aux channel (canonical component 3): call sim.set_magnetic_field(Bz) to populate it. Requires a fluid transport >= 3 variables (momentum on 2 axes); rejected on a scalar. The STIFF regime (large omega_c) goes through the condensed stage pops.CondensedSchur (Schur), NOT through this explicit brick.

charge = q/m, sign included (same convention as PotentialForce).

class pops.PotentialMagneticForce(charge=1.0)[source]

Bases: object

Electrostatic force + magnetic Lorentz SUMMED: (q/m) rho E + q (v x B_z) (native C++ brick CompositeSource<PotentialForce, MagneticLorentzForce>, the full magnetized diocotron force). Same q/m for both forces (same species). Reads B_z (set_magnetic_field); requires a fluid transport >= 3 variables. charge = q/m, sign included.

class pops.ChargeDensity(charge=1.0)[source]

Bases: object

Charge density f = q n.

class pops.BackgroundDensity(alpha=1.0, n0=0.0)[source]

Bases: object

Neutralizing background f = alpha (n - n0).

class pops.GravityCoupling(sign=1.0, four_pi_G=1.0, rho0=1.0)[source]

Bases: object

Self-consistent coupling f = sign 4piG (rho - rho0). sign = +1 gravity, -1 plasma.

Mesh and layouts

class pops.mesh.cartesian.CartesianMesh(n=64, L=1.0, periodic=True)[source]

Bases: MeshDescriptor

CARTESIAN mesh (implicit default): square domain [0, L]^2, n x n cells.

pops.System(mesh=pops.mesh.CartesianMesh(n, L, periodic)) is STRICTLY equivalent (bit-identical) to pops.System(n=n, L=L, periodic=periodic). Provided for symmetry with pops.mesh.PolarMesh (the geometry choice is explicit on both sides).

class pops.mesh.layouts.Uniform(mesh, embedded_boundary=None)[source]

Bases: MeshDescriptor

A single-level (uniform) mesh layout.

class pops.mesh.layouts.AMR(base, max_levels=2, ratio=2, regrid=None, patches=None, refine=None, nesting=None, checkpoint=None, output=None)[source]

Bases: MeshDescriptor

An adaptively refined mesh layout (Spec 5 sec.5.10 / sec.8.5).

AMR(base=mesh, max_levels=2, ratio=2, regrid=RegridEvery(20), patches=PatchLayout(...), refine=TagUnion(...), nesting=ProperNesting(...), checkpoint=CheckpointPolicy(...)).

Declares its limitations explicitly: the current native AMR route supports max_levels <= 2 at ratio == 2 (see pops.mesh.amr.NATIVE_MAX_LEVELS / NATIVE_RATIOS); a request beyond that is refused by available() / validate() with a clear message instead of being silently clamped.

AMR policies live under pops.mesh.amr.

Fields

class pops.fields.FieldProblem(name=None, unknown=None, equation=None, inputs=(), coefficients=None, bcs=(), nullspace=None, outputs=None, postprocess=None, solver=None)[source]

Bases: Descriptor

A typed elliptic field problem: an unknown solved from an Equation.

FieldProblem(unknown=phi, equation=(-laplacian(phi) == rhs), solver=GeometricMG()). The inputs / coefficients / bcs / nullspace / outputs / postprocess fields are typed descriptors (from pops.fields); they are stored and surfaced, not interpreted, here. validate() rejects a non-Equation equation (a Python bool produced by == on plain values is the common mistake) and a missing solver.

solve(schedule, policy)[source]

Record an inert field-solve cadence (a schedule + a not-due policy).

Pairs a typed pops.time.Schedule (WHEN to solve – e.g. every(4) to solve every fourth macro-step, or when(cond)) with a typed FieldSolvePolicy (HoldPrevious / Recompute) deciding what happens on a step where the solve is not due. The pair is stored as cadence and surfaced by inspect(); the method returns self so it chains after construction.

This is AUTHORING metadata only. It deliberately does NOT lower the cadence into the Program / codegen: honouring a non-trivial field-solve cadence at runtime (the cached field carry, the residual gate) is the codegen-adjacent follow-up. The cadence is recorded and inspectable, never silently executed.

Parameters:
  • schedule – A typed pops.time.Schedule (built with every / when / always / …). A bare int or str is rejected with a clear TypeError – the cadence is a typed object, not a free string or count.

  • policy – A typed FieldSolvePolicy (HoldPrevious or Recompute). A string such as "hold" is rejected with a clear TypeError.

Returns:

self, so the call chains after construction.

Return type:

FieldProblem

Raises:

TypeError – When @p schedule is not a pops.time.Schedule or @p policy is not a FieldSolvePolicy.

class pops.fields.PoissonProblem(name=None, unknown=None, equation=None, inputs=(), coefficients=None, bcs=(), nullspace=None, outputs=None, postprocess=None, solver=None)[source]

Bases: FieldProblem

A standard Poisson problem -laplacian(phi) == rhs.

Refines FieldProblem by requiring the equation’s principal operator to be an elliptic Laplacian / div(coeff*grad) (so a non-elliptic equation is rejected up front).

class pops.fields.ScreenedPoissonProblem(name=None, unknown=None, equation=None, inputs=(), coefficients=None, bcs=(), nullspace=None, outputs=None, postprocess=None, solver=None)[source]

Bases: PoissonProblem

A screened Poisson problem -laplacian(phi) + k*phi == rhs (Debye screening).

Requires a zeroth-order reaction term (k*phi) on top of the principal operator.

class pops.fields.AnisotropicPoissonProblem(name=None, unknown=None, equation=None, inputs=(), coefficients=None, bcs=(), nullspace=None, outputs=None, postprocess=None, solver=None)[source]

Bases: PoissonProblem

An anisotropic Poisson problem -div(A grad phi) == rhs (variable / tensor coefficient).

Requires a div(coeff*grad(phi)) principal operator (the variable-coefficient form).

NOTE: the form is authorable + validated here, but lowering a variable / tensor coefficient in the elliptic codegen is the coordinated follow-up (ADC-491); today the native elliptic operator solves a constant-coefficient div(eps grad).

Boundary conditions, right-hand-side descriptors, coefficients, nullspaces, and field cadence policies live in pops.fields.bcs, pops.fields.rhs, pops.fields.coefficients, pops.fields.nullspace, and pops.fields.policies.

Numerics

Numerical choices are descriptors.

from pops.numerics.riemann import HLL, HLLC, Roe, Rusanov
from pops.numerics.reconstruction import MUSCL, WENO5Z
from pops.numerics.reconstruction.limiters import Minmod, VanLeer
from pops.numerics.terms import Flux, SourceTerm

The top-level pops.FiniteVolume(...) function remains the current bridge to the runtime spatial brick.

Time

pops.time is the language:

class pops.time.Program(name)[source]

Bases: _ProgramCore, _ProgramLocal, _ProgramSolve, _ProgramAuthoring, _ProgramPasses, _ProgramInspect

A compiled time program (builder mode). Holds the SSA value list and the committed blocks. The Python object only BUILDS the IR; it is never executed numerically during sim.step. Authoring methods come from the mixins; C++ emission is delegated to pops.codegen.program_codegen via emit_cpp_program().

emit_cpp_program(model=None)[source]

Generate the C++ source of a problem.so implementing this Program (codegen).

Thin authoring entry point: delegates to the free function pops.codegen.program_codegen.emit_cpp_program(), imported lazily so the pops.time package never imports pops.codegen / _pops at module scope. See the codegen function for the full lowering contract.

class pops.time.CompiledTime(substeps=1, stride=1, cfl='default')[source]

Bases: object

Record of a compiled Program’s macro-step cadence (substeps / stride).

A compiled Program OWNS the whole step body: it is installed via sim.install_program and driven by sim.step(dt). Its cadence is applied to the System with sim.set_program_cadence(substeps, stride) (call it after install_program); a CompiledTime just records those values. The compiled program is NOT attached via sim.add_equation(time=CompiledTime(…)) – that path is rejected with an explicit error (the transport policy passed to add_equation is a native pops.Explicit/etc.; the compiled program is installed separately). substeps and stride are wired (ADC-411) as a SYSTEM-level orchestration AROUND the opaque program closure (System.set_program_cadence, mirroring the native per-block advance loop): substeps=n runs the program n times over eff_dt/n; stride=M runs the whole program once per M macro-steps with eff_dt = M*dt (GLOBAL hold-then-catch-up, the clock still ticks every macro-step).

Two semantic limits to keep in mind (cf. system_stepper.hpp):
  • substeps > 1 is bit-exact vs native pops.Explicit(substeps=n) ONLY for an UNCOUPLED / transport-only program: program_step_(h) re-runs the WHOLE program (its solve_fields included), whereas native substeps subdivides ONLY the transport (solve_fields runs once).

  • stride here is GLOBAL (a compiled program is one whole-system closure), so it equals native per-block stride only for a single-block system (or all blocks sharing the stride).

A NUMERIC cfl (e.g. cfl=0.5) is wired: it is applied at RUNTIME by sim.run(cfl=…), whose cfl defaults to the installed cadence’s cfl when the caller passes none, so a bare sim.run(t_end) after bind(…, cadence=CompiledTime(cfl=0.5)) advances at cfl=0.5 (the per-block CFL dt is computed in adc_cpp and drives the installed Program). A self-computed cfl SUB-PROGRAM (cfl=”program”) is still deferred (the Program would export its own dt bound); it fails loud rather than being silently ignored.

Ready schemes are in pops.lib.time.

Solvers

from pops.solvers.elliptic import FFT, GeometricMG
from pops.solvers.krylov import BiCGStab, CG, GMRES, Richardson
from pops.solvers.nonlinear import Newton

Users configure provided compiled solvers. They do not author solver loops in Python.

Compilation and inspection

class pops.codegen.loader.CompiledProblem(so_path, program, model, abi_key, cxx, std, libraries=None, problem_hash=None, cache_key=None, compile_command=None, generated_sources=None, codegen_env=None)[source]

Bases: object

Result of pops.compile_problem(...); a generated problem.so (a compiled time Program) plus the metadata to install + reproduce it. Install it with sim.install_program(compiled.so_path) AFTER the physical block has been added (sim.add_equation / sim.add_block); the Program then drives sim.step(dt) entirely in C++ via ProgramContext.

The .so is compiled against the pops headers with the SAME Kokkos toolchain as the loaded _pops module (cf. pops_loader_build_flags), so its ABI key matches and System::install_program accepts it. os.fspath(compiled) returns so_path (it can be passed where a path is expected).

arguments()[source]

The runtime inputs this artifact expects at System.install (Spec 5 sec.12.2, #44-45).

Returns an pops.codegen.inspect_compiled.Arguments listing – WITHOUT any bind or runtime data – the instances (state space / components / required), params (type / kind / required), aux (layout / required), solvers (problem / solver), outputs and the runtime layout the artifact expects. Sourced from the carried Program (the blocks it commits, the field solves it performs) and the physical model (its state / params / aux). It is DISTINCT from requirements()-style compile constraints: arguments lists what you must SUPPLY to bind. It allocates and reads nothing.

property cache_key

The (problem source | abi_key | backend/target) cache key of the .so (sec.12.4, #48).

The sha256 the out-of-source build cache keys the artifact on; reproducing it requires the same program, headers, compiler and C++ standard. None for an externally built handle.

property codegen_dir

Directory the compiled .so (and any generated source) lives in (sec.12.4, #48).

The out-of-source cache directory the .so was written to (os.path.dirname(so_path)); the generated .cpp – when compile_problem(debug=True) wrote one – sits beside it. None only if the handle carries no so_path.

property codegen_env

The resolved codegen POPS_* environment snapshot that governed this compile (sec.12.4).

A pops.codegen.env.CodegenEnv recording the EFFECTIVE settings (env defaults already overridden by any explicit argument): log level, codegen dir, keep-generated, dump-IR / dump-CPP, cache dir, profile, autotune level, and the UNSAFE CodegenEnv.jit_backdoor gate. Surfaced in inspect() so the active env state is never hidden (criterion #47). None for a handle built outside compile_problem.

property compile_command

The REDACTED compiler invocation that built the .so (sec.12.4, #49).

A single command string with the ephemeral temp source replaced by the generated-source path and any secret-looking token masked (cf. _redact_compile_command). None on a cache HIT (the .so was not rebuilt this call – a documented absence, never a fabricated command) or for an externally constructed handle. Recompile with force=True to populate it.

dump_cpp(target)[source]

Write the generated C++ source of the problem .so (REUSES the existing emit).

Calls the EXISTING Program.emit_cpp_program(model=...) codegen (the same source compile_problem compiles) and writes it. @p target is a directory (the source is written as <program_name>.cpp inside it) OR a path ending in .cpp (written verbatim); the parent directory must exist. Returns the written file path. The carried model is passed so a Program whose IR names a model source / linear kernel lowers (without it such a Program raises the SAME NotImplementedError the compile path raises – it is not faked). Raises a clear error if this handle carries no Program.

dump_ir(path=None)[source]

Write the serialized Program IR (JSON) – the SAME serialization _ir_hash digests.

EXPOSES the existing codegen: the lowered pops.time.Program’s _serialize() blob (its nodes, commits, block order, optional dt bound) as indented, sort-keyed JSON – byte-stable run to run, the WHAT the .so was built from. Writes to @p path if given (returns the path), else returns the JSON string. Raises a clear error if this handle carries no Program.

dump_schedule(path=None)[source]

Write the schedule / commit order of the Program (the block advance order).

EXPOSES the lowered schedule WITHOUT running it: the committed blocks in the runtime block index order (_block_indices: the order the Program first declares each block via P.state, the order install_program binds them), each with the IR id of its committed State value. A plain, deterministic text listing. Writes to @p path if given (returns the path), else returns the string. Raises a clear error if this handle carries no Program.

estimate_memory(mesh, *, platform=None, layout=None)[source]

A FORMULA-based memory estimate on mesh (Spec 5 sec.12.3, #46).

Returns an pops.codegen.inspect_compiled.MemoryEstimate: the state / field-output / aux / RHS-scratch / state-scratch / scalar-field / Krylov / multigrid / AMR-patch / halo / MPI-buffer byte budgets, computed as a FORMULA over the mesh shape and the artifact’s static cost (Program.estimate) + component counts. It NEVER allocates a MultiFab; every assumption is in MemoryEstimate.assumptions and the estimate is CONSERVATIVE. @p mesh an pops.mesh.CartesianMesh (or an int / 2-tuple of extents); @p platform an optional hint ("mpi" adds the halo-exchange buffer); @p layout an optional pops.mesh.layouts.AMR / Uniform for an AMR hierarchy estimate (conservative; full-refinement worst case).

property generated_sources

The generated source files written for inspection (sec.12.4, #49).

The .cpp files compile_problem(debug=True) (or POPS_KEEP_GENERATED) persisted next to the .so (the default keeps the source only in a TemporaryDirectory, so this is empty unless one of those was set). A list (possibly empty), never None.

inspect()[source]

A printable pops.codegen.inspect_report.CompiledReport of this artifact (sec.12.1).

The print(compiled.inspect()) summary: name, backend, platform, layout, blocks (+ state / components), fields (+ solver), program (+ commits), the REQUIRED runtime inputs (states / params / aux from arguments()), the on-disk artifacts (so_path / abi_key / cache_key) and the bind-pending status line. It AGGREGATES the metadata this handle already carries (no compile / bind / runtime read); to_dict() serialises it.

inspect_amr(layout=None)[source]

STATIC AMR report on this compiled artifact (Spec 5 sec.8.12 / sec.8.4).

A compiled time Program carries NO AMR layout descriptor (it lowers a whole-system time program, a single-level System concept today – AmrSystem has no install_program seam). So this delegates to the top-level pops.inspect_amr() on an EXPLICIT layout argument (an pops.mesh.layouts.AMR / Uniform descriptor), and with layout=None returns the native AMR envelope report – never a fabricated hierarchy the artifact does not carry. @p layout an optional AMR / Uniform layout descriptor (default: the native envelope).

inspect_capabilities()[source]

The descriptor capability rows relevant to THIS compiled artifact (sec.12.1).

Delegates to the top-level pops.inspect_capabilities() machinery (the descriptor-sourced capability matrix) and SCOPES it to the descriptor categories a compiled time Program can select at bind – the Riemann / reconstruction / limiter / projection bricks and the mesh layouts (the solver / field catalogs are bind inputs, kept too). Returns the same printable pops.CapabilityMatrix. PURE: it imports only the inert authoring catalogs, never _pops (cf. pops.inspect_capabilities()).

list_field_spaces()[source]

Names of the compiled module’s field spaces.

list_operators()[source]

Names of the typed operators of the compiled module (registration order).

list_state_spaces()[source]

Names of the compiled module’s state spaces.

manifest()[source]

The RICH self-describing manifest of this artifact (Spec 5 sec.13.12, #36).

Returns a pops.external.CompiledArtifactManifest: the ABI identity (abi_key / required_headers_sig), the model name, the blocks / variables / roles, the required aux, the const / runtime params, the ghost depth, the field outputs and the supports_* capability flags (uniform / AMR / MPI / GPU known from the backend caps; stride / partial-IMEX mask / named fields honestly None until the C++ codegen emits them). It AGGREGATES the metadata this handle already carries (via arguments() + the carried model + abi_key); it binds, dlopens and runs nothing. The widening of the thin pops.external.CompiledManifest (a brick-id / category list) into the full artifact self-description Spec 5 sec.13.12 requires.

operator_capabilities(name)[source]

The capabilities dict of operator name.

operator_requirements(name)[source]

The requirements dict of operator name.

operator_signature(name)[source]

The pops.model.Signature of operator name in the compiled module.

property problem_hash

Stable hash of the program SOURCE the .so was compiled from (sec.12.4, #48).

The sha256 of the emitted C++ program text – the cache identity (the WHAT). None for a handle built outside compile_problem (it records no source hash); use program_hash for the in-memory Program’s IR hash, which is always available.

requirements()[source]

The COMPILE-TIME constraints of this artifact (sec.12.1), DISTINCT from arguments().

Returns a pops.codegen.inspect_report.RequirementsReport: the model capabilities the lowered route relies on (wave_speeds / hllc_star_state / roe_dissipation, read from the carried model’s emitted flags), the required descriptors (the spatial scheme is a BIND input, reported as such), and the layout / backend / ABI constraints. A piece genuinely unknowable from today’s metadata is stated honestly in unknown, never fabricated. arguments() lists what you SUPPLY at bind; requirements lists what the compiled route NEEDS from the model + toolchain.

runtime_param_routes()[source]

(per_block, defaults) routing the Program’s RUNTIME parameters to the per-PROGRAM-block set_program_params vectors (ADC-510): per_block maps a program block index to its param names in within-block index order (matching the .so metadata + the lowered read), defaults a name to its declaration value. Built via the SAME program_param_entries the codegen emits. No bind.

scratch_plan()[source]

The scratch-buffer liveness plan of this artifact’s time Program (Spec 5 sec.13.11.3, #38).

Returns a pops.codegen.scratch_plan.ScratchPlan: the per-category scratch counts (state / rhs / scalar-field), the PROVABLY-reusable buffers (scratch nodes whose SSA live ranges are disjoint, so the codegen may share one buffer), the REJECTED reuse (with the reason – a still-live occupant or an aux/field barrier) and the PERSISTENT Krylov / multigrid solver buffers. Computed by a liveness analysis over the carried Program IR (Program.scratch_liveness / buffer_reuse_report); the step-body reuse is EXACT, the persistent solver counts are conservative and labelled so. It NEVER binds, dlopens or allocates – it is inspectable BEFORE System.install. Raises a clear error if this handle carries no Program.

class pops.codegen.loader.CompiledModel(so_path, backend, adder, cons_names, cons_roles, prim_names, n_vars, gamma, n_aux, params, caps, abi_key, model_hash, cxx, std, target='system', hllc=False, roe=False, aux_extra_names=None, wave_speeds=False, elliptic_field_names=None)[source]

Bases: object

Result of m.compile(...): packages the produced .so + EVERYTHING needed to wire it correctly (dispatch adder, ABI diagnostic, reproducibility). Replaces the historical pair (str so_path, adder_for(backend)) with a single object.

The metadata is NOT re-read from the .so: Python already holds names/roles/gamma/n_aux/params (the HyperbolicModel carries them); CompiledModel just exposes them for dispatch (add_equation) and diagnostics. cf. DSL_MODEL_DESIGN.md section 3.

check_runtime(n=16, state=None, raise_on_error=True, rtol=1e-08, atol=1e-10)[source]

RUNTIME re-verification of a CompiledModel ALONE (audit balance, GENERICITY pt 9): without the original dsl.Model, the FORMULAS are no longer re-verifiable (symbolic check_model), but the .so itself is – we install it in an EPHEMERAL System (n x n periodic, neutral Poisson, minmod+rusanov) and delegate to System.check_model (finite state, residual -div F + S finite, positivity by roles, round-trip of THE MODEL conversions).

@p state: dict {conservative variable name: ndarray (n, n)} to control the tested state. None -> SMOKE state by ROLES (Density = 1 + gaussian bump, Momentum* = 0, Energy = 2.5, other components = 0.5) – enough to exercise flux/source/conversions; provide state= for a precise physical regime. @return the dict from System.check_model.

inspect_amr(layout=None)[source]

STATIC AMR report on this compiled MODEL (Spec 5 sec.8.12 / sec.8.4).

A CompiledModel is a per-block physics .so; target='amr_system' means it is loadable on the native AMR hierarchy (add_equation), but the model carries NO layout descriptor (the levels / ratio / regrid policy belong to the AmrSystem it is installed in, not to the model). So this delegates to the top-level pops.inspect_amr() on an EXPLICIT layout argument and returns the native AMR envelope report for layout=None; for a target=’system’ model the report still describes the native AMR envelope (a model is AMR-installable only with target=’amr_system’). It never fabricates a hierarchy. @p layout an optional AMR / Uniform layout descriptor (default: the native envelope).

property runtime_param_names

this is the ORDER of the indices on the C++ side (RuntimeParams) AND the order expected by System.set_block_params(name, values) (P7-b). Empty if the model has only const params.

Type:

Names of the model’s RUNTIME parameters (kind=’runtime’), SORTED

runtime_param_values()[source]

DECLARATION values of the runtime params, parallel to runtime_param_names (default as long as no set_block_params has been called).

Useful environment variables are documented in environment variables.

Profiling

class pops.Profile(level='basic')[source]

Bases: object

A typed profiling level (Spec 5 sec.12.5). Inert: it carries no timers.

Use the named constructors rather than a string flag:

with sim.profile(pops.Profile.Basic()) as prof:
    sim.run(0.1)
print(prof.summary())

Basic surfaces the coarse phase timings + the kernel/step counters; Advanced also asks for the per-program-node timings and the scheduler/memory counters (which only populate under a compiled step on a Kokkos build – declared unavailable, never faked, otherwise).

classmethod Advanced()[source]

Per-program-node timings + scheduler / memory counters (Kokkos-gated; honest about gaps).

classmethod Basic()[source]

Coarse phase timings + step / kernel counters.

property advanced

True for the Advanced level (asks for the per-node / scheduler / memory views).

classmethod from_env(default=None)[source]

Resolve the level from POPS_PROFILE (sim.profile() with no argument).

Unset / 0 / off -> @p default (a Basic() when @p default is None); advanced / full -> Advanced(); anything else -> Basic(). Returns None when the env asks for OFF and no @p default is given, so the caller can leave profiling disabled.

class pops.PerformanceSummary(report, profile=None)[source]

Bases: object

A printable, typed wrapper around the native profile report (Spec 5 criteria 41-43).

Built from the string System.profile_report() returns (and the Profile level the run requested). It exposes the report as a structured dict (to_dict() / to_json()) and typed views: by_program_node(), by_native_brick(), by_solver(), by_elliptic(), by_amr_mpi(), by_memory(). Views read the parsed native tables; a view the build does not surface returns an _Unavailable sentinel (bool(view) is False) rather than a faked zero.

by_amr_mpi()[source]

AMR / MPI phase timings + counters: the distributed-runtime dimension (criterion 43).

Spec 5 sec.12.5 requires time attributable to AMR / MPI alongside the program-node, native-brick, solver, and memory views. The distributed AMR runtime spends its non-numeric time in named phases the C++ profiler can scope and count:

  • regrid – rebuilding the patch hierarchy (timing scope + a per-run count);

  • fill_boundary / halo_exchange – the cross-rank ghost-cell halo exchange;

  • reflux – the coarse-fine flux correction at refinement boundaries;

  • average_down – restricting fine-level data onto the coarse level;

  • mpi_reductions / mpi_messages – MPI collective / point-to-point counts.

A scope whose name contains one of the timing tokens is surfaced as a timing entry ({count, total_s, mean_s, min_s, max_s}); a counter whose name contains one of the counter tokens is surfaced as an int. Returns {name: int | timing-dict} with only the phases the run actually produced.

On the common host / serial / non-AMR build NONE of these scopes or counters exist – no C++ path emits them yet (the native regrid / halo / reduction timers are a documented follow-up in include/pops/runtime/program and the AMR runtime). When the report carries none of them this view declares itself _Unavailable honestly rather than fabricating a zero, exactly like by_native_brick() / by_memory() on a host build. It surfaces real numbers as soon as a distributed AMR run under profiling emits the scopes.

by_elliptic()[source]

Elliptic-solver counters: the most actionable view given the elliptic-solve dominance.

The elliptic field solve is 96-99.9% of step cost (Spec 5 sec.13.11.1), yet by_solver only exposes the opaque field_solve phase. This view breaks it down with the native counters the System reads back at the field_solve seam (ADC-479 criteria 42/43):

  • mg_cycles – total geometric-multigrid V-cycles over the run;

  • krylov_iters – total Krylov iterations (0 on the default Poisson path: it uses multigrid / a direct FFT, never a Krylov elliptic solver);

  • mg_levels – multigrid hierarchy depth (a structural constant, reported as the peak);

  • elliptic_bottom – coarsest-grid (bottom) solve self-time, as a timing entry {count, total_s, mean_s, min_s, max_s}.

Returns {name: int | timing-dict} with only the counters / scope the run actually produced. A direct FFT solver yields zeros (no cycles / levels / bottom solve), and a build whose _pops predates these counters (no mg_cycles etc.) declares the view unavailable rather than faking.

by_memory()[source]

Scratch-memory counters: allocation count + the largest single scratch buffer (bytes).

Reads scratch_allocs / scratch_peak_bytes (program_context.hpp count_scratch). These move only under a compiled step on a Kokkos build; on a native host step neither counter is created, so this view declares itself unavailable rather than faking a 0.

by_native_brick()[source]

Per-native-brick timings.

The native runtime times Program nodes and coarse phases, not individual bricks: there is no per-brick scope to read. Declared unavailable rather than faked (the per-brick granularity is a documented follow-up wired through the compiled ProgramContext).

by_program_node()[source]

Per-program-node timings (the node:<name> scopes the compiled step emits).

Keys are the bare node names (rhs2, solve_fields1, …). Empty on a native step (no compiled Program); populated under a compiled .so step.

by_solver()[source]

Solver-attributable timings: the elliptic field-solve phase + any solve_fields node.

Reads the coarse field_solve phase and the node:solve_fields* program nodes. Empty when no field solve ran under profiling.

counters()[source]

All integer counters: {name: int}.

print()[source]

Print the human-readable summary (print(summary) sugar).

property profile

The Profile level the run requested.

property raw_report

The exact string the native profiler returned (the source of truth).

scopes()[source]

All timed scopes: {name: {count, total_s, mean_s, min_s, max_s}}.

to_dict()[source]

The full structured report: level + scopes + counters + total, plus the typed views.

by_native_brick / by_amr_mpi / by_memory serialise their availability honestly (an unavailable view records {"available": False, "reason": ...}).

to_json(path=None)[source]

Serialise to_dict() to JSON. Writes to @p path when given; returns the JSON string.

total_s()[source]

Sum of every scope’s total wall-clock time (seconds).

Profiling is off by default and should be enabled explicitly with sim.profile(...) or POPS_PROFILE.

Experimental namespace

pops.experimental is not production API. Pages may mention it for debugging, but tutorials must not use it as a solver route.