Skip to content

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_rates credited the cylinder F_hllc · A_eff, where A_eff = A_port · Cd · lift is 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_bcs computes 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_rates adds that ṁ/Ė to the cylinder; the timeloop's _valve_sources_into removes 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=0 ghost, but scale the pipe boundary flux to A_eff and treat A_pipe − A_eff as 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, so h_conv drops to a physical value (see the EPW suite pass_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_rates orifice 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.