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 stateu_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 thep/(γ−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.lodiwas investigated as the alternative and is measured pathological: it pinsp_b = p_Vwith no Poinsot–Lele relaxation coefficient, converting any pipe↔plenum pressure mismatch into velocity at a gain of2/(γ−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 termK = σ(1−M²)a/Lis 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).