Write a model with the physics DSL

Use pops.physics.Model when you want to author equations directly instead of choosing a ready-made model from pops.lib.models.

pops.physics lowers to pops.model. It does not compile by itself and it does not run numerical loops.

Declare fields and rates

from pops.physics import Model
from pops.math import div, ddt, grad, laplacian
from pops.solvers.elliptic import GeometricMG

m = Model("diocotron")
U = m.state("U", components=["ne"], roles={"ne": "density"})
ne = U[0]

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)
F = m.flux("F", on=U, x=[ne * E.y], y=[ne * (-E.x)], waves={"x": [E.y], "y": [-E.x]})
explicit_rate = m.rate("explicit_rate", ddt(U) == -div(F))

model = m.lower()

The names U, ne, phi, and explicit_rate are user-facing identifiers. The solver choice is the typed GeometricMG() descriptor.

Compose with numerical descriptors

from pops.numerics.riemann import Rusanov
from pops.numerics.reconstruction import MUSCL
from pops.numerics.reconstruction.limiters import Minmod

spatial = pops.FiniteVolume(
    riemann=Rusanov(),
    reconstruction=MUSCL(limiter=Minmod()),
)

Compile through a case

from pops.mesh import CartesianMesh
from pops.mesh.layouts import Uniform
from pops.time import Program
from pops.lib.time import ssprk2
from pops.codegen import Production

program = Program("ssprk2")
ssprk2(program, "ne")

case = (
    pops.Case(layout=Uniform(CartesianMesh(n=96, L=1.0, periodic=True)))
    .block("ne", physics=model, spatial=spatial)
    .time(program)
)

compiled = pops.compile(case, backend=Production())
sim = pops.bind(compiled, state={"ne": ne0})
sim.run(t_final=0.1, cfl=0.4)

Use layout=AMR(...) for adaptive runs. The model and time program stay the same when their descriptors declare AMR support.