ADR-0008 — Compressible-orifice cylinder-valve coupling (mass-conserving)¶
Status: Accepted
Context¶
A Cylinder breathes by coupling each valve port to a runner Pipe through a
ghost cell. The legacy coupling had a mass/energy conservation bug:
Cylinder.accumulate_ratescredited the cylinderF_hllc · A_eff, whereA_eff = A_port · Cd · liftis the throttled valve area;- but the runner pipe's boundary cell drained at the full
F_hllc · A_pipe(the residual kernel is unaware of the valve restriction).
So (1 − Cd·lift) of the mass leaving the runner each step was destroyed —
~35 % even at peak lift. This starved the runner toward vacuum, collapsing the
HLLC face flux, and the cylinder ended its intake stroke at ~0.1 bar: a
volumetric efficiency of ~6 % (measured on the AVL EPW Diesel: 236 mg
trapped vs AVL's ~3290 mg). The prescribed, air-independent Wiebe heat release
masked this in peak pressure (a fixed Q in too little mass still produces
high p), and it is what made the burnt-zone NOx explode.
The plenum/junction coupling was not affected — it uses A_port == A_pipe
on both sides (junction.py), pinned by test_two_pipes_plenum_mass_flow.py.
Only cylinder ports leaked, and there was no cylinder-port conservation test.
A secondary issue: 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).
Decision¶
Replace the cylinder-port coupling with a compressible isentropic-orifice
valve-flow model (Heywood §C.2/§6.3; aeolus1d.components.valve_flow), applied
symmetrically, and make it the default ([case]
cylinder_orifice_valve_flow = true, opt out with false):
- The runner pipe's port face is set to a wall, so the residual transports no mass/energy/species across it.
Cylinder.apply_port_bcscomputes a per-port orificeṁ/Ėbetween the runner boundary cell (upstream when its pressure is higher) and the 0-D cylinder volume — subsonic and choked branches,Ė = ṁ · cp · T_upstream.accumulate_ratesadds thatṁ/Ėto the cylinder; the timeloop's_valve_sources_intoremoves the sameṁ/Ė(and the primary-species flux) from the runner boundary cell, in every RK stage.
Because both sides use one ṁ and one Ė, mass and energy are conserved by
construction. build_network tags each cylinder from the case flag, so every
advance_network caller (run_case, the EPW/KTM/Mendeley validation harnesses,
tests) honours it without per-caller plumbing.
Alternatives considered¶
- Minimal area-weighted flux correction (keep the HLLC +
u=0ghost, but scale the pipe boundary flux toA_effand treatA_pipe − A_effas wall). Rejected: it fixes conservation but not the jet under-pull, and post- correcting the kernel residual and the independently-recomputed species flux is fragile. The orifice model fixes both defects in one explicit, conservative mechanism. - Keep it opt-in (default off). Rejected: the leak is a correctness bug
affecting every
[[cylinder]]case, so correct breathing should be the default. Engine validation numbers are not CI-gated, so the default flip is CI-safe; the validated SI suites are re-baselined against the fix.
Consequences¶
- EPW Diesel: volumetric efficiency 6 % → ~74 % (trapped mass matches AVL's air consumption to ~10 %); IMEP_High and PFP go from −24/−16/+1 % and −6/+4/−12 % to −1/−6/−10 % (near-exact); BMEP becomes physical (−70/−65/−57 % → −23/−33/−29 %).
- The exhaust-plenum wall-heat closure was re-tuned: the legacy aggressive
cooling (
h_conv ≈ 4750) was compensating for the bug — with correct (lean) breathing the exhaust enters the plenum cooler and needs far less wall cooling, soh_convdrops to a physical value (see the EPW suitepass_criteria.toml/ README for the re-tuned T_EXH). - KTM (single-cylinder, closed pipe-pair, no boost plenum to starve) only breathes 5–56 % more with the orifice; its fuel-from-air feed keeps AFR correct, so its torque curve shifts modestly and is re-validated.
- Mendeley SI suite needs re-validation against the fix (its data is local-only; not CI-runnable).
- Conservation is pinned by
tests/integration/test_cylinder_valve_conservation.py(a closed cylinder+pipe conserves total mass to < 1e-10 with the orifice and measurably leaks without it). The flag-off path stays bit-identical.
Update 2026-07: The valve mass/energy sources (_valve_sources_into) are
applied only by the explicit advance_network. advance_network_implicit
(implicit-Euler / BDF2 / Radau-IIA rk) does not apply them, so an
orifice-coupled cylinder would gain mass/energy the walled-off runner pipe never
loses. Both io/build.py and solver/implicit_network.py now fail fast when
cylinder_orifice_valve_flow=True is combined with an implicit network rk —
use an explicit rk for engine breathing, or set the flag false to accept the
legacy non-conservative coupling. No bundled case hits this combination.
References¶
src/aeolus1d/components/valve_flow.py— the orifice flow (+test_valve_flow.py).src/aeolus1d/components/cylinder.py—apply_port_bcs/accumulate_ratesorifice branch.src/aeolus1d/solver/network_timeloop.py—_valve_sources_into(symmetric source).src/aeolus1d/io/build.py— tags cylinders from the case flag.tests/integration/test_cylinder_valve_conservation.py— the conservation pin.examples/validation/avl_epw_diesel_steady/— the case the fix was validated on.