PhysicalModel¶
A model in pops is the equation, not the run: it is the set of pointwise laws that
describe transport, sources, and the elliptic coupling, and it names no scenario. This page
explains what the PhysicalModel concept captures, why it is kept this narrow, and how a model
emerges as a composition of generic bricks.
What a PhysicalModel is¶
PhysicalModel (pops::PhysicalModel, in core/physical_model.hpp) is the contract for “what to
compute”. A type that satisfies it exposes a handful of pure functions of pointwise states:
flux(U, aux, dir): the physical flux in directiondir(0 = x, 1 = y);max_wave_speed(U, aux, dir): the largest characteristic speed, for the CFL bound and the Riemann solver;source(U, aux): the pointwise source term;elliptic_rhs(U): the contribution to the right-hand side of the elliptic equation (charge or mass density, depending on the model).
That is the whole interface. A PhysicalModel sees a single cell at a time. It has no access to
storage, to the mesh, or to parallelism, and the methods called in kernels carry POPS_HD so they
remain callable on a GPU device. There is no allocation in hot loops, no std::function, no dynamic
polymorphism. This locality is why the same model runs unchanged on Serial, OpenMP, Kokkos Cuda,
MPI, and AMR.
A complete hyperbolic brick additionally satisfies pops::HyperbolicPhysicalModel: it carries its
conservative and primitive variables and the conversions to_primitive / to_conservative, because
the variable layout and the flux are physically linked. A flux is written for one specific layout.
Why it stays this narrow¶
The core is model-agnostic on purpose. It defines bricks; it never names a physical case. Diocotron, Euler-Poisson, and two-fluid are not types in the library. They are compositions assembled on the application side.
Keeping PhysicalModel to local laws separates three concerns that change for different reasons:
what to compute: the pointwise laws, the
PhysicalModelaxis;where and how to iterate: the mesh and the execution seams;
in what order: the time integrator and the coupler.
A high layer expresses the problem; a low layer executes it. A high layer never depends on an execution detail. Because the model knows nothing of boxes, ranks, or loop policy, you can refine the mesh, switch the backend, or change the time scheme without touching a single flux formula.
The three orthogonal axes¶
A model is built along three independent axes. You pick one choice per axis, and the choices compose freely.
axis |
concept |
example bricks |
|---|---|---|
transport |
|
ExB drift, compressible Euler, isothermal Euler |
numerical flux |
|
Rusanov, HLL, HLLC, Roe |
elliptic coupling |
|
geometric multigrid, Poisson FFT |
The first axis is the physics: the exact flux F(U, aux) and its wave speeds. The second is a purely
numerical choice, the Riemann solver that turns two interface states into one flux; it lives in the
discretization layer (numerics/numerical_flux.hpp), not in the model. The third is the elliptic
solver that closes the Poisson coupling. None of the three knows the internals of the others.
The link between the two hyperbolic axes is the aux channel. flux and source both receive
pops::Aux, which carries the potential phi, its gradient grad_x / grad_y, and the optional
extended fields B_z and T_e. The same spatial operator can therefore host a drift transport, where
aux is read inside the flux, and a self-gravitating compressible fluid, where aux is read inside
the source.
A model is a composition of bricks¶
You obtain a PhysicalModel by composing generic bricks, not by hand-writing a monolithic equation.
In C++ this is CompositeModel<Hyperbolic, Source, Elliptic> (physics/composite.hpp), a model
assembled on three slots: transport, source, and elliptic. Each slot is a small, reusable struct from
physics/bricks.hpp.
The reduced diocotron model is one such composition: a scalar density (Scalar) advected by the ExB
drift (ExBVelocity), with no source (NoSource) and a neutralizing background as the elliptic
right-hand side (BackgroundDensity). Swap the transport brick for CompressibleFlux and the
elliptic brick for GravityCoupling, and the same machinery describes a self-gravitating gas. The
scenario lives in the choice of bricks, never in the core.
From Python you assemble the same object three ways, all producing the same compiled C++ model: native
bricks (pops.Model), symbolic formulas compiled to a .so (pops.physics.facade.Model), or a mix of the two
within one model (pops.CompositeModel). The cell-by-cell computation stays compiled either way, so
MPI, AMR, and GPU are preserved.
Where to go next¶
For the composition mechanics and the catalog of bricks, see the models guide. For the numerical flux and reconstruction policies behind the second axis, and the multigrid Poisson behind the third, see ALGORITHMS. For the layered architecture that keeps the three axes orthogonal, see ARCHITECTURE.