Skip to content

ADR-0012 — The plenum port is a reservoir mouth, not a piston face

Status: Accepted

Context

A Junction couples each port to a Pipe by writing a ghost state and letting HLLC resolve the face. From Milestone 1 until 2026-08-02 that ghost was the plenum's rest state, (ρ_V, 0, p_V) — _fill_plenum_ghost.

docs/theory.md §5.1 described this as "exact for mean-flow mass and energy". It is not, and the error is not small.

The mechanism

Writing u = 0 puts a velocity jump on the port face. A Godunov face between a state at rest and a moving state does not resolve a contraction; it resolves a piston. A piston's pressure response is the acoustic impedance ρ·a·Δu. A contraction's is the dynamic head ½ρu². Their ratio is

ρ·a·u / (½ρu²)  =  2a/u  =  2/M

so the port invented 2/M times the correct loss — about 15× at the M ≈ 0.13 of an engine exhaust manifold. The error is worst at low Mach, which is backwards from every ordinary discretisation error, and is a large part of why it survived so long in a code exercised mostly at high port velocities.

The measurement

Steady frictionless constant-area discharge from a reservoir (p₀, T₀) into a pipe held at static pressure p has a closed form:

T = T₀ (p/p₀)^((γ−1)/γ)      u = √(2 cp (T₀ − T))      ρ = p/(R T)

A correct port coupling must leave that state alone — the flux the face returns has to equal the pipe's own interior flux, or the port is pushing the solution away from an answer that is already exact. No time integration is needed; the momentum residual is the spurious port force, in Pa.

Measured on the old ghost across p/p₀ ∈ [0.53, 1.0]:

quantity old ghost reservoir coupling
mass flux error −39 % to −50 % ≤ 1e-15
momentum residual 2.2 – 12.8 × dynamic head ≤ 1e-15

The port was throttling every plenum in every case by roughly a factor of two.

What was already known, and what was missed

Two prior investigations found this exact ghost and stopped short of the general fix.

  • The EHR junction (aeolus1d/components/junction_ehr.py, Phase-7 / Phase-14) opens by calling the plenum ghost "a first-order low-pass" that "cannot sustain a Helmholtz mode". Correct — but framed purely as an acoustic defect, and fixed by giving each port an extra ODE state u_n. Nobody asked what it did to the mean flow.
  • ADR-0008 found it on the cylinder ports and quantified it: "the ghost was filled with zero velocity (_fill_plenum_ghost), so even with the area bug fixed the HLLC face could not represent the accelerated jet through the valve throat (~60 % of the correct orifice flow)". It replaced the cylinder coupling with a compressible-orifice model and made that the default. Its remark that "the plenum/junction coupling was not affected" is true of the area bug it was chiefly about — but the zero-velocity ghost it names as a secondary issue was left in place on plenum ports.

So the defect had been seen twice, measured once at ~60 % on a sibling code path, and never generalised. ADR-0008's ~60 % and the −39…−50 % above are the same number.

Decision

Treat the port as what it physically is — the mouth of a stagnation reservoir — with the flow direction deciding whether the dynamic head is recovered or destroyed. Both branches carry u_ghost = u_int, which is the point: no velocity jump, hence no spurious ρ·a·Δu.

Feed (volume → pipe). The volume is a stagnation reservoir; gas accelerates from rest to the port velocity isentropically, so the port static pressure sits below p_V:

a² = a₀² − ½(γ−1)u²            a₀² = γ·p_V/ρ_V
p  = p_V·(a²/a₀²)^(γ/(γ−1))    ρ = ρ_V·(a²/a₀²)^(1/(γ−1))

clamped at a² = a₀²·2/(γ+1) — the port chokes at M = 1 and cannot pass more, which is what a blowdown port actually does.

Dump (pipe → volume). A sudden expansion: the dynamic head is dissipated inside the volume, not at the face, so the port static pressure is p_V and the gas is the pipe's own — its entropy, not the plenum's:

p = p_V                        ρ = ρ_int·(p_V/p_int)^(1/γ)

This is deliberately the same physics ADR-0008 adopted for cylinder ports, so the two couplings no longer disagree about what a port is.

Why no opt-out flag

The old behaviour is not a modelling choice with a defensible regime; it is wrong against a closed-form answer by a factor of two, everywhere. A flag would preserve a mode whose only property is being incorrect. The migration risk it would hedge is covered instead by the identity below.

At u_int == 0 the feed branch returns (ρ_V, 0, p_V) bit-identically — not to a tolerance. Every case starts at rest, so nothing moves until flow develops, and the change cannot perturb initialisation, resume, or any static-state test. This is pinned by test_a_quiescent_port_is_bit_identical_to_the_legacy_ghost over Ng ∈ {1,2,3} and both ends.

The py_junction_bc seam remains free to write any ghost it likes, including the legacy static state; that is guarded by test_py_bc_can_still_write_the_legacy_static_ghost.

Consequences

  • Every plenum port in every case changes once flow develops. Validation impact is recorded in the MR and in each suite's README.
  • Ports no longer fabricate a large pressure loss, so cases previously tuned on top of that loss need re-baselining. The EPW plenum wall-heat calibration (MR !387, merged the same day) is the known instance.
  • The isentropic relations are ideal-gas forms evaluated at pipe.gamma — the same approximation class as the p/(γ−1) they replaced. Ghost energy still routes through _ghost_rho_E, so real-gas pipes keep an EOS-consistent ρ·E.
  • Neck ports (neck_cells > 0) inherit the fix through _fill_neck_far_ghost_from_volume; the legacy binary-valve cylinder path inherits it too. The default cylinder path is unaffected — it has used the orifice model since ADR-0008.
  • Port.lodi was investigated as the alternative and is measured pathological: it pins p_b = p_V with no Poinsot–Lele relaxation coefficient, converting any pipe↔plenum pressure mismatch into velocity at a gain of 2/(γ−1) = 5. On a short EPW run it produced ghost velocities of 719 m/s (default: 0) and ran >10× slower. It is left in place, documented, and not recommended; adding the relaxation term K = σ(1−M²)a/L is a separate change with its own verification.

Alternatives considered

Extrapolate velocity but keep p_ghost = p_V in both directions. Simpler, and removes most of the impedance — but it does not admit the correct steady state on the feed branch, because it omits the reservoir's isentropic acceleration. Measured residual 0.6–1.0 dynamic heads, versus machine precision for the adopted form. Rejected: a coupling that is 10× better but still not a fixed point invites exactly the kind of calibration-on-top-of-error this ADR exists to undo.

Fix lodi and make it the default. It is the textbook characteristic BC and would handle transients on principle rather than by construction. Rejected for now on scope: it is broken as written (above), so adopting it means fixing it first, and the steady-state defect that motivates this ADR is better settled by a coupling whose fixed point can be checked in closed form.

Insert a neck pipe at every port. Already available (neck_cells > 0) and resolves the port geometrically. Rejected as a default: it adds cells and a timestep constraint per port, and it does not by itself fix the far-side ghost — which was also (ρ_V, 0, p_V).