Conservative and primitive variables

A hyperbolic model in pops carries its state in two complementary layouts: the conservative variables U, which the scheme evolves and conserves, and the primitive variables P, which the closures and the wave speeds read. This page explains why both exist and where each is used.

Two views of the same state

The conservative variables are the quantities the conservation law evolves in time. For a compressible fluid they are density rho, momenta rho*u, rho*v, and total energy E. They are the natural variables of the divergence form \(\partial_t U + \nabla\cdot F(U, \mathrm{aux}) = S(U, \mathrm{aux})\): the finite-volume update moves U from one cell to its neighbor, and what one cell loses at a face the next cell gains. That balance is exact only because the state is held in conserved form.

The primitive variables are the quantities the physics is written with: density, velocity u, v, and pressure p. The flux formula, the pressure closure, and the eigenvalues (characteristic speeds) are expressed cleanly in primitives. A velocity is rho*u divided by rho; you do not want to redo that division everywhere the model needs a velocity.

So the two layouts are not redundant: each is the convenient frame for a different job. U is what is transported and conserved; P is what closures and spectra are read from.

The two conversions

A model that evolves more than one variable ties the two layouts together with a pair of pointwise conversions, cons_to_prim (to_primitive) and prim_to_cons (to_conservative). The first reads U and returns P; the second reads P and returns U. They are inverse maps of each other.

For a scalar transported density there is nothing to convert: the primitive equals the conservative, and both conversions are the identity. The distinction only earns its keep once a variable like momentum has to be divided by density to recover a velocity.

A model that satisfies the full hyperbolic contract (pops::HyperbolicPhysicalModel) carries the variable names, the roles, and these two conversions together with its flux, because the flux is written for one specific variable layout. Variables, conversions, and flux are physically linked and travel as one brick, whether you write it natively or in the DSL. See the models section.

Why the scheme reconstructs in primitives

At order two and above, the spatial operator does not use cell averages directly; it reconstructs a left and a right state at each face before calling the Riemann solver. That reconstruction can run on the conserved variables or on the primitives, and the choice matters.

Reconstructing the conserved state can push the reconstructed values out of the admissible domain at a strong shock: a limited slope on rho*u and E does not guarantee a positive density or pressure on either side of the face. Reconstructing in primitives limits rho, u, and p directly, which keeps positivity of density and pressure and is more stable for Euler with shocks.

This is why a fluid block typically selects primitive reconstruction. The choice is exposed through the variables="primitive" option of pops.FiniteVolume; the operator then calls to_primitive before reconstructing and to_conservative after, and the conserved update at the end stays exact.

Roles, not indices

Because couplings between species need to find a quantity by meaning and not by a literal slot, each variable carries a physical role (Density, MomentumX, Energy, and so on). Canonical names map to roles automatically (rho or n to Density, E to Energy); an unrecognized name stays Custom. A coupling that needs the density of another block asks for the role, so it keeps working when two models order their variables differently.

# fluid block: conservative U, primitives P, and the inverse map
(rho, mx, my, E) = m.conservative_vars("rho", "mx", "my", "E")
u = m.primitive("u", mx / rho)            # cons -> prim, pointwise
v = m.primitive("v", my / rho)
m.primitive_vars(rho=rho, u=u, v=v, p=p)  # fixes the ordered P layout
m.conservative_from([rho, rho * u, rho * v, E])  # prim -> cons (you give the inverse)

The DSL cannot invert your primitives symbolically, so you state the inverse explicitly with conservative_from; it generates to_conservative. For the discretization that consumes these variables (reconstruction, Riemann flux), see ALGORITHMS.