Shared hierarchy¶
All blocks live on a single AMR hierarchy: same boxes, same MPI distribution
(DistributionMapping), same space steps per level. This is the model “a common hierarchy
carrying several fields”, never one hierarchy per species. The current version
carries two levels (refinement ratio 2: the fine level has a step dx/2).
Single-block (a single
add_block): historical pathAmrCouplerMP, with dynamic regrid and conservative reflux. Bit-identical to what it has always produced.Multi-block (two or more
add_block): N blocks co-located on the shared hierarchy (engineAmrRuntime). A single auxiliary channel per level (phi,grad phi) and a single coarse Poisson whose right-hand side is the co-located sum of the elliptic bricks of the blocks (f = somme_b q_b n_b, read at the same cells). Conservation is ensured per block (reflux + average-down). In multi-block, the block name indexesset_density(name),mass(name)anddensity(name).
A guard (same_layout_or_throw) verifies at construction that all blocks share
exactly the same layout per level (boxes, order, distribution, dx/dy): this is the
precondition of the single aux and the single Poisson. Detail: ARCHITECTURE.md
section 8, AMR_MULTIBLOCK_DESIGN.md sections 1-2, and the core
include/pops/runtime/amr_system.hpp.
import numpy as np
import pops
from pops.physics import Model
from pops.math import laplacian, grad, div, ddt
from pops.mesh.cartesian import CartesianMesh
from pops.mesh.layouts import AMR
from pops.mesh.amr import Refine, RegridEvery
from pops.time import Program
from pops.lib.time import ssprk2
from pops.fields import PoissonProblem
from pops.fields.bcs import Periodic
from pops.fields.rhs import ChargeDensity
from pops.solvers.elliptic import GeometricMG
from pops.codegen import Production
n, L = 96, 1.0
ne0 = np.ones((n, n)) # initial density (n, n), row-major
m = Model("diocotron")
U = m.state("U", components=["ne"], roles={"ne": "density"})
(ne,) = U
phi = m.field("phi")
m.solve_field("fields_from_state", equation=(-laplacian(phi) == ne),
outputs={"phi": phi, "grad_x": grad(phi).x, "grad_y": grad(phi).y},
solver=GeometricMG())
E = m.vector_field("E", x=-grad(phi).x, y=-grad(phi).y)
flux = m.flux("F", on=U, x=[ne * E.y], y=[ne * (-E.x)], waves={"x": [E.y], "y": [-E.x]})
m.rate("explicit_rate", ddt(U) == -div(flux))
m.check()
poisson = PoissonProblem(name="phi", unknown="phi",
equation=(-laplacian("phi") == ChargeDensity.from_blocks("ne")),
bcs=(Periodic(),), solver=GeometricMG())
layout = AMR(CartesianMesh(n=n, L=L, periodic=True), max_levels=2, ratio=2, regrid=RegridEvery(8))
program = Program("ssprk2")
ssprk2(program, "ne")
case = (pops.Case(layout=layout).block("ne", physics=m.lower()).field(poisson).time(program))
case.amr.refine(Refine.on("density").above(0.05)) # refine where the density exceeds the threshold
compiled = pops.compile(case, backend=Production())
sim = pops.bind(compiled, state={"ne": ne0})
sim.run(0.25, cfl=0.4) # CFL on the coarse-level step
print("fine patches:", sim.n_patches(), "| mass:", sim.mass("ne"))
rho = sim.density("ne") # coarse density (n, n)
pops.bind builds the AmrSystem from a config derived from the AMR layout (regrid cadence from
RegridEvery(...), patch settings from a PatchLayout) and flows the typed refinement
(case.amr.refine(...)) and the Poisson field onto it before installing the block.