Closures¶
Physics that is not gas-dynamic transport (friction, wall heat
transfer, equation of state, junction loss) lives behind a thin dispatch
layer. Each closure is identified by an integer kind code plus a small
float parameter array, so the @njit residual kernel can branch on the
kind without calling back into Python.
For the implicit drivers (Implicit Euler / BDF2 / Radau IIA(3)), closure sources also contribute a closed-form Jacobian block to the Newton matrix — see Analytic Jacobians below. Phases B16 → B22 (2026-05-06) brought every kind shipped today onto the analytic path; the FD fallback only fires for Python-callable overrides.
Looking for a specific component?
The Component index maps every closure kind to its model, theory section, a runnable example, and its tutorial — all in one table.
Friction¶
s_mom = -τ_w · P_w / A
For a circular cross-section of hydraulic diameter D_h, P_w / A = 4 / D_h
and Darcy-Weisbach gives τ_w = (1/8) ρ |u| u f with Darcy friction
factor f. The resulting momentum source is
s_mom = -(f / (2 D_h)) · ρ · |u| · u
s_E = 0 (adiabatic Fanno flow — see below)
The total-energy source is zero. The wall is stationary, so it does
no work on the fluid and (adiabatically) carries away no heat; in the
total-energy formulation the kinetic energy removed by s_mom is
converted to internal energy automatically by conservation (adiabatic
Fanno flow, stagnation enthalpy preserved). An earlier s_E = u · s_mom
sink double-debited that energy, silently shedding the frictional heat and
driving the gas below the Fanno prediction — that was corrected in the
2026-07 closures audit. Deliberate non-adiabatic wall heat loss belongs
in the heat closure, not here.
| kind | code | params | analytic Jacobian |
|---|---|---|---|
NONE |
0 | unused | n/a |
DARCY_CONST |
1 | (f, D_h) |
Phase B16 |
COLEBROOK |
2 | (eps_over_D, D_h, nu) |
Phase B16-ext |
MINOR_LOSS |
3 | (K_loss, L_loss, …) |
Phase B16 (distributes K over L) |
MINOR_LOSS models a discrete minor loss (bend, junction, sudden
area change) as a distributed Darcy-equivalent friction spread over a
length L_loss. Dropping ½ K ρ u² over L_loss gives the per-volume
momentum sink s_mom = -(K_loss/(2·L_loss))·ρ·|u|·u — structurally
identical to constant-f Darcy friction with the coefficient
K_loss/(2·L_loss) in place of f/(2·D_h), so it reuses that momentum
form and its analytic Jacobian. If K_loss <= 0 at build time,
K_loss is derived from the geometry inputs bend_r_over_d /
bend_angle_deg via the Idelchik bend correlation
(aeolus1d.closures.bend_loss); those two geometry fields round-trip
through the TOML/GUI but are not kernel params.
COLEBROOK uses the Haaland explicit approximation of the
Colebrook-White equation in the turbulent regime (Re > ~4000) with a
laminar f = 64/Re branch below Re ~ 2300 and a linear blend in the
transition zone — accurate to ~1-2 % vs the implicit Colebrook-White.
Friction depends only on (ρ, u) — never on T or p — so the
analytic Jacobian is EOS-agnostic (Phase B16-real-gas pinned this
contract): the closed form fires unchanged on real-gas pipes (Peng-
Robinson, Redlich-Kwong) at the same (ρ, u).
Wall heat transfer¶
q_w = h · (T_wall - T)
s_E = q_w · 4 / D_h (per unit volume, circular pipe)
with T = p/(ρ·R_gas) recovered from the live cell state.
| kind | code | params | analytic Jacobian |
|---|---|---|---|
NONE |
0 | unused | n/a |
CONSTANT_TWALL |
1 | (h, T_wall, D_h, R_gas) |
Phase B17 (ideal) / Phase B17-real-gas |
EGR_COOLER |
2 | (T_coolant, UA, C_coolant) |
py_heat override → FD |
CHARGE_AIR_COOLER |
3 | (T_coolant, UA, C_coolant) |
py_heat override → FD |
FLOW_NU |
4 | (T_wall, D_h, k_gas, mu_gas, Pr, correlation, laminar_nu) |
py_heat overlay → FD |
The EGR_COOLER kind is implemented as a Python callable
(make_egr_cooler_py_heat) using the lumped counter-flow ε-NTU model
across the whole pipe; it always routes through the FD Jacobian path.
CHARGE_AIR_COOLER (make_charge_air_cooler_py_heat) is the same
ε-NTU model under a sister name for the intake side.
FLOW_NU is a py_heat overlay (the integer kernel stays HEAT_NONE):
instead of a hand-tuned h, it computes the wall coefficient per cell
from the local flow via h = Nu · k_gas / D_h, Re = |ρ·u|·D_h/μ,
s_E = h·(T_wall − T)·4/D_h. Below Re ≈ 2300 the flow is laminar
(Nu = laminar_nu, default 3.66); above it the selected turbulent
correlation applies, floored at the laminar value. The change at
Re = 2300 is a step (for dittus_boelter at Pr = 0.7, Nu jumps from
3.66 to 9.75), not a blend. Supported
correlations (correlation): dittus_boelter, colburn,
sieder_tate, gnielinski. Because it needs the local velocity, it
routes through the FD Jacobian path like the other py_heat overrides.
For the CONSTANT_TWALL analytic path on real-gas pipes (Phase
B17-real-gas), the chain rule differentiates T_app = p_eos/(ρ·R)
(the apparent T the heat kernel uses, NOT the EOS-converged
temperature) via the EOS-derivative helpers from Phase B15.4.
Plenum wall heat (junctions)¶
Separate from the pipe closures above: a plenum junction can drain heat
from its 0-D volume into surrounding metal. Junction.accumulate_rates
subtracts a Newton-cooling term from dE/dt,
Q_wall = h · A · (T_gas − T_wall)
and the term is skipped entirely unless both wall_h_conv and wall_A
are > 0. Only the product h·A is identifiable — the two never appear
separately — so calibrations conventionally set wall_A = 1.0 and carry
the whole lumped conductance in wall_h_conv (which then reads in W/K, not
W/m²K).
Unlike the pipe-side FLOW_NU overlay, a junction has no local Reynolds
number to work from, so h cannot be computed from the flow. Instead the
two coefficients may be declared as weakly-linear functions of the engine
operating point:
h·A|eff = wall_h_conv · (1 + wall_h_conv_speed_slope · (S_p/S_p_ref − 1))
T_wall|eff = wall_T + wall_T_load_slope · (φ/φ_ref − 1)
| field | units | default | meaning |
|---|---|---|---|
wall_h_conv |
W/(m²·K) | 0.0 |
convective coefficient at the reference point; 0 disables the closure |
wall_A |
m² | 0.0 |
internal surface area; 0 disables the closure |
wall_T |
K | 400.0 |
metal temperature at the reference point |
wall_h_conv_speed_ref |
m/s | 0.0 |
mean piston speed S_p_ref; 0 disables the speed law |
wall_h_conv_speed_slope |
— | 0.0 |
local Nusselt exponent; 0.8 reproduces Dittus-Boelter to first order |
wall_T_load_ref |
kg | 0.0 |
fuel per cycle per cylinder φ_ref; 0 disables the load law |
wall_T_load_slope |
K | 0.0 |
kelvin per unit relative load |
Both laws are identities at the reference point, so wall_h_conv and
wall_T keep their plain meaning. A power law x^a linearised about
x = 1 is 1 + a(x−1), which is why the speed slope is the local Nusselt
exponent rather than a free fitting constant.
The drivers are read from the case's cylinder specs (stroke,
omega_crank, and m_fuel_per_cycle or combustion.m_fuel), averaged
over cylinders, and the laws are evaluated once at build time — the hot
loop is untouched. Two consequences worth knowing:
- This is a steady-operating-point closure. On a transient the drivers are frozen at their declared values; the laws do not track a changing speed or fuelling within a run.
- A nonzero
_refwith no cylinder to read (or zero fuelling) is a build error, not a silent fallback. A linear law extrapolated far enough also goes negative, so a non-positive effectiveh·AorT_wallis likewise a build error rather than a clamp.
These coefficients are calibrated, not derived
h·A and T_wall are not jointly identifiable from an exhaust
temperature. Matching a measured T_EXH pins the total heat removed,
Q = ṁ·c_p·(T_ad − T_meas), leaving h·A·(T_gas − T_wall) = const —
one equation in two unknowns, where every hotter wall simply buys a
larger h·A. An optimiser handed both will run to whatever bound it
is given, and the fit residual will not object. Pin one of them on
physical grounds (a Nusselt exponent, a defensible metal temperature)
and fit the other. The AVL EPW diesel case documents a worked example
of this trap in
examples/validation/avl_epw_diesel_steady/base_diesel.toml.
Porous media (Darcy-Forchheimer)¶
A porous region (catalyst brick, DPF, muffler, flow straightener) opposes the gas flow with a per-volume momentum sink built from two empirical coefficients — viscous (Darcy) and inertial (Forchheimer). aeolus1d uses the AVL Cruise M / GT-Power tabulated form,
|dP/dx| = α · ½ · ρ · u² + β · μ(T) · u
so the volumetric source (sign-aware via |u| · u) is
s_mom = -(α · ½ · ρ · |u| + β · μ(T)) · u
s_E = 0 (adiabatic: the pressure-drop work is
dissipated to internal energy automatically
by total-energy conservation — stagnation
enthalpy is conserved across an adiabatic
restriction, like J-T throttling)
α (1/m) is the Forchheimer coefficient — the kernel multiplies it
by the live ρ at each cell. β (1/m²) is the Darcy coefficient
— the kernel multiplies it by μ(T). Both are pure geometry once the
operating-temperature viscosity is fixed; they're typically read off
the porous-media catalogue of the upstream commercial tool.
| kind | code | params (7 floats) |
|---|---|---|
NONE |
0 | unused |
DARCY_FORCHHEIMER |
1 | (α, β, μ_ref, viscosity_model, T_ref, S, R_gas) |
DARCY_FORCHHEIMER_KC |
2 | (K, C_F, μ_ref, viscosity_model, T_ref, S, R_gas) |
DARCY_FORCHHEIMER_KC is an alternate parameterisation matching the
permeability-and-Forchheimer-coefficient convention used by some
porous-media catalogues; it is converged-state-equivalent to
DARCY_FORCHHEIMER with β = 1/K and α = 2·C_F/√K.
viscosity_model is 0 for constant μ (= μ_ref everywhere) or 1
for Sutherland (μ scales with cell T).
Analytic Jacobian coverage¶
| (kind, viscosity_model) | analytic path |
|---|---|
DARCY_FORCHHEIMER / DARCY_FORCHHEIMER_KC + constant μ |
Phase B18 (ideal + real gas; same closed form) |
DARCY_FORCHHEIMER / DARCY_FORCHHEIMER_KC + Sutherland μ + ideal-gas EOS |
Phase B18-ext (μ(T) chain via ideal-gas T) |
DARCY_FORCHHEIMER / DARCY_FORCHHEIMER_KC + Sutherland μ + real-gas EOS |
Phase B18-ext-real-gas (EOS-derivative chain) |
Constant-μ porous reads only (ρ, u) from the cell state — like
friction, the closed form is EOS-agnostic. Sutherland-μ porous adds
the μ(T) chain rule; on ideal-gas pipes this routes through the
ideal-gas pressure recovery T = (γ−1)/R · (E_total − u²/2), while
real-gas pipes route through the EOS chain (matching the same T_app
= p_eos/(ρ·R) convention used by Phase B17-real-gas heat).
Mapping from older conventions¶
aeolus1d's pre-α/β storage is no longer accepted, but the migration formulas are simple and the parser raises a clean error pointing at them:
| Pre-2026-04-29 v2 | New |
|---|---|
K (m²) |
β = 1 / K |
inertial_length (m) |
α = 2 / inertial_length |
| Pre-2026-04-29 v1 | New |
|---|---|
mu_over_K (Pa·s/m²) |
β = mu_over_K / μ_ref |
C_F (1/m) |
α = 2 · C_F |
Sutherland viscosity¶
Setting viscosity_model = "sutherland" activates a per-cell
viscosity correction that follows the textbook Sutherland law
μ(T) = μ_ref · (T / T_ref)^(3/2) · (T_ref + S) / (T + S)
with T = p / (ρ · R_gas) recovered from the live cell state.
μ_ref is then μ at the reference temperature T_ref, not the
operating-T value. For air the textbook constants
T_ref = 273.15 K, S = 110.4 K reproduce experimental viscosity
to ~1 % from 200 K to 1500 K — useful for hot catalysts where μ
rises ~T0.7 across the brick.
Sutherland constants for common gases:
| gas | T_ref (K) | S (K) | μ_ref (Pa·s, T_ref) |
|---|---|---|---|
| air | 273.15 | 110.4 | 1.716e-5 |
| nitrogen | 273.15 | 111.0 | 1.663e-5 |
| oxygen | 273.15 | 127.0 | 1.919e-5 |
| water vapour | 350.0 | 659.0 | 1.16e-5 |
When viscosity_model = "constant" (the default) the four
Sutherland-related slots are ignored and the Darcy term reads
β · μ_ref everywhere — fastest, fine when the brick runs near
the reference temperature.
Gas EOS¶
aeolus1d ships four EOS implementations under the same dispatch contract (ideal + two cubics + a tabulated lookup):
| kind | code | params | source |
|---|---|---|---|
EOS_IDEAL |
0 | (R_gas, _, _, _, cv) |
textbook ideal gas (default) |
EOS_REDLICH_KWONG |
1 | (R_gas, Tc, Pc, _, cv) |
RK two-parameter cubic |
EOS_PENG_ROBINSON |
2 | (R_gas, Tc, Pc, ω, cv) |
PR three-parameter cubic |
EOS_TABULATED |
3 | 11-float header + 5 flat (n_T × n_rho) panels |
bilinear interpolation on (T, ρ) |
The tabulated path is the ML-substitution / NIST-quality EOS pattern:
generate a precomputed (T, ρ) table and load it; everything else
in the residual stays unchanged. The pack (closures/eos_tabulated.py)
reuses the first five cubic-EOS slots
([R_gas, Tc=0, Pc=0, omega=0, cv_ref]) so generic callers keep
working, then adds a header describing the grid
(n_T, n_rho, T_min, T_max, rho_min, rho_max, slots 5–10) and five
row-major (n_T × n_rho) panels in order: p, e, ∂p/∂T,
∂p/∂ρ, cv. The cv panel holds the local EOS-corrected
cv = ∂e/∂T|_ρ (not the scalar cv_ref header slot), because the
thermodynamic sound speed needs the true local cv — a single scalar
biases a by several percent at dense / near-critical states (the
tabulated analog of audit finding #12). cv_ref is retained only as
the Newton initial-guess scale for the T(ρ, e) inversion. The
default 64×64 grid packs to 11 + 5·4096 = 20 491 float64 (~160 kB
per pipe). Panels are built at network-construction time from CoolProp
via make_table_from_coolprop (optional [real-gas] extra) or from
user callables via make_table_from_callables.
Real-gas sound speed (audit finding #12)¶
The thermodynamic sound speed for a real gas with frozen composition is
where the specific heat is the EOS-corrected
cv_real = ∂e/∂T|_ρ = cv_ideal + departure, not the ideal-gas
cv_ideal header value. The departure is small at moderate pressure
but comparable to cv_ideal at dense / near-critical states, so using
cv_ideal biased the correction term (and hence a) by several
percent — corrected in the 2026-07 closures/EOS audit (#12). For the
cubics cv_real is a closed form (RK: + 0.75·a/(b·T^1.5)·ln(1+b·ρ);
PR: an analogous second-α-derivative term); for the tabulated EOS it
is read from the dedicated cv panel. The pure-Python
eos_sound_speed and the @njit eos_sound_speed_jit mirror each other
bit-for-bit.
Real-gas analytic Jacobians (Phase B15.4 + B20)¶
The implicit Newton solver chains pressure and sound-speed gradients through the EOS using closed-form helpers:
eos_dpdT_dpdrho_jit(T, ρ)— first partials(∂p/∂T|ρ, ∂p/∂ρ|T). Closed form for ideal / RK / PR; for tabulated it reads the precomputed∂p/∂T/∂p/∂ρpanels directly (bilinear lookup — the Phase Perf-1 direct-panel path replaced the earlier 4-call central-FD fallback).eos_dedT_dedrho_jit(T, ρ)— internal-energy partials. Same pattern; the tabulated branch differentiates the bilinear interpolant of the energy panel (Phase Perf-1) rather than finite-differencing.eos_dadT_dadrho_jit(T, ρ)— sound-speed partials. Phase B20 added closed form for RK and PR (the messy second-derivative algebra of the cubic α(T) factor); Phase B23 added closed form for tabulated EOS by differentiating the bilinear interpolant of the precomputed ∂p/∂T and ∂p/∂ρ panels.
These get plumbed through the HLLC analytic Jacobian
(hllc_jac._eos_primitive_gradients) so the Newton matrix is
closed-form on every face for ideal, RK, and PR runs.
Analytic Jacobians¶
The implicit drivers (Implicit Euler / BDF2 / Radau IIA(3)) augment
the Newton matrix with a closed-form ∂S_closure/∂U block per cell
for every closure family that has analytic coverage. The fallback
is a 6-point central-FD perturbation of _cell_closure_source
(roughly 7-8× slower per cell — see the Phase B19 bench).
| Phase | Coverage |
|---|---|
| Phase B16 | DARCY_CONST friction (closed form in (ρ, u)) |
| Phase B16-ext | COLEBROOK friction (Haaland chain through Re-blend) |
| Phase B16-real-gas | Friction is EOS-agnostic; same closed form on real-gas pipes |
| Phase B17 | HEAT_CONSTANT_TWALL on ideal-gas pipes |
| Phase B17-real-gas | HEAT_CONSTANT_TWALL on real-gas pipes via EOS-derivative chain |
| Phase B18 | DARCY_FORCHHEIMER / KC + constant μ (both ideal + real gas) |
| Phase B18-ext | DARCY_FORCHHEIMER / KC + Sutherland μ on ideal-gas pipes |
| Phase B18-ext-real-gas | DARCY_FORCHHEIMER / KC + Sutherland μ on real-gas pipes |
| Phase B19 | Consolidated analytic-vs-FD bench (@pytest.mark.slow); 7 scenarios |
| Phase B20 | Closed-form ∂a/∂T, ∂a/∂ρ for cubic EOS (RK + PR) |
| Phase B21 | Shared T_app chain across real-gas heat + Sutherland porous (saves one Newton solve / cell) |
| Phase B22 | Wiebe combustion + closed-loop knock-controller TOML support; new 52_knock_event.toml |
| Phase B23 | Closed-form ∂a/∂T, ∂a/∂ρ for tabulated EOS via differentiated bilinear interpolant |
| Phase Perf-1 | EOS / solver hot-path optimization sweep — wires B23 panels into the Jacobian directly (replaces 4-call FD on eos_dpdT_dpdrho_jit); closed-form ∂e/∂T, ∂e/∂ρ from the bilinear interpolant of the energy panel; batched _bilinear_two / _bilinear_grad_two helpers share the (T, ρ) cell-index pass; fused eos_a_and_derivs_jit returns (a, ∂p, ∂a) in one dispatch; fused tabulated (p, a) recovery in primitives_from_U_eos_jit; block-Thomas _inv3x3_into writes through a hoisted scratch buffer (zero per-cell allocs); Picard-loop snapshot bookkeeping skipped on picard_max_iter == 1; Tikhonov regularisation on the chemistry-BS 2×2 Cramer solve. |
The Solver Internals dock in the GUI (under View → Solver Internals)
shows a per-pipe table with a ✓ marker on any closure cell handled
by the analytic path and fd on cells that fall through. Hover any
cell for the phase-by-phase tooltip.
Aftertreatment closures¶
Beyond the per-cell physics closures above, several aftertreatment
brick models ship as py_reaction_multi-shaped closures that the
TOML schema attaches via the species.reaction selector on a pipe.
These run on the chemistry sub-step (Strang-split) rather than the
advection Newton step, so they are independent of the analytic-vs-FD
Jacobian routing.
species.reaction |
Module | What it models |
|---|---|---|
arrhenius_scr_demo |
closures/reaction_demo.py |
Single-Arrhenius standard-SCR demo |
arrhenius_scr_multi_demo |
closures/reaction_demo.py |
NH₃ + NO consumed in M_NO/M_NH3 ratio |
nh3_ads_des |
closures/scr_kinetics.py |
Cu-zeolite NH₃ Langmuir adsorption / desorption |
nh3_scr_kinetics |
closures/scr_kinetics.py |
Above + NH₃ oxidation + standard-SCR with NO |
nh3_slip_catalyst |
closures/scr_kinetics.py |
Same mechanism with V₂O₅-WO₃/TiO₂ defaults |
hc_oxidation |
closures/hc_oxidation.py |
First-order Arrhenius HC oxidation on Pt-coated DOC |
doc_oxidation |
closures/hc_oxidation.py |
Two-branch HC + CO oxidation on Pt/Pd DOC (CO lights off ~150 °C, HC ~250 °C) |
doc_oxidation_S |
closures/doc_sulfur.py |
DOC oxidation + sulfate-poisoning deactivation factor f_S = 1 − α_S · θ_S. Pair with [[controller]] kind = "doc_sulfur" to integrate θ_S |
doc_oxidation_voltz |
closures/hc_oxidation.py |
DOC oxidation with the Voltz/Pattas surface-inhibition denominator (the AVL Cruise-M DOC form): same HC (primary) + CO (extras[0]) Arrhenius numerators as doc_oxidation, divided by a shared coverage-inhibition term G(T, c) built from three adsorption groups, giving a steep light-off knee (high apparent activation energy) plain doc_oxidation cannot |
hnco_hydrolysis |
closures/hnco_hydrolysis.py |
Urea-water HNCO → NH₃ hydrolysis on a urea-injector brick |
twc |
closures/twc.py |
Three-Way Catalyst, Voltz-Morgan-Liederman three-pathway (CO + ½O₂, C₃H₆ + 4½O₂, NO + CO) with Langmuir-Hinshelwood CO-inhibition |
twc_brinkmeier |
closures/twc_brinkmeier.py |
Full Brinkmeier (Stuttgart 2006) 11-reaction TWC + cerium O₂ storage, plus two opt-in appended channels (both default-off → inert → bit-identical with the 11-reaction baseline): (1) passive-SCR NH₃ generation (A_nh3_form_brnk > 0; NO + 2.5 H₂ → NH₃ + H₂O), whose super-linear H₂ dependence is the dimensionless richness gate (c_H2/c_H2_ref)^(order-1) (c_H2_ref = 1 mol/m³), so A_nh3_form_brnk is a proper bimolecular rate constant (m³/(mol·s)) like the other Brinkmeier constants; selectivity validated against the ORNL CLEERS λ-trend; and (2) C₃H₆ (propene) total oxidation (A_c3h6_ox_brnk > 0; C₃H₆ + 4.5 O₂ → 3 CO₂ + 3 H₂O, Voltz-Morgan-Liederman kinetics, Ea = 110 kJ/mol) — the legacy mechanism consumed propylene only via NO reduction, so it was near-inert on lean / low-NO feeds; the channel's light-off T50 is calibrated for the Brinkmeier inhibition denominator (A_c3h6_ox_brnk = 1e13 → T50 ≈ 240 °C, validated in examples/validation/doc_propylene_lightoff/). Neither channel is modelled on the bs_brinkmeier chem solver (build refuses that combination). |
mechanism |
closures/mechanism.py |
Generic spec-driven mass-action mechanism: mechanism_file (TOML/YAML) declares species + [[reaction]] blocks (+ optional molar_mass), compiled to a py_reaction_multi callable by make_mechanism. The mechanism's species[0] must be the block's primary name and species[1:] its extras (by name) |
DPF (diesel particulate filter) physics is wired through
pipe.filter (a FilterState sub-spec on the pipe), not the
species-reaction selector, because it carries per-cell soot-loading
+ cake-thermal state outside the gas species: the trap kernel
(closures/filter.py), the cake-Δp pressure-drop overlay, the
active-O₂ regen rate, and the Phase 8g cake solid-phase thermal
mass T_cake field all live there. A [[controller]] kind =
"dpf_heater" block can drive pipe.filter.heater_power_w in
closed loop on mean cake T (DPFHeaterController).
By default the cake-Darcy Δp (μ_cake·soot·resistance) only enters the
explicit / implicit drivers' source overlay, not the quasi-steady pressure
balance. Setting qs_drag = true on the cake sub-spec folds it into the QS
driver's junction/flow solve so a DPF running under solver = "quasi_steady"
reproduces an AVL-comparable back-pressure (e.g. the C02006 drive cycle);
default false keeps the pre-feature QS behaviour.
Closed-loop controllers¶
aeolus1d.control ships a family of velocity-form PI controllers as
on_step= callables; seven of them are wireable via [[controller]]
kind = … blocks in TOML:
| Kind | Class | Actuates | Reads |
|---|---|---|---|
boost_pressure |
BoostController |
Turbine.vane_open (VGT vane) |
mean p in a manifold pipe |
wastegate |
WastegateController |
Turbine.bypass_open (parallel-bypass fraction) |
mean p in a manifold pipe |
dpf_heater |
DPFHeaterController |
pipe.filter.heater_power_w |
mean DPF cake T |
doc_sulfur |
DOCSulfurController |
the bound DOCSulfurOxidation closure's theta_S |
mean DOC gas T |
egr_valve |
EGRValveController |
EGRValve.open on the egr_valve-kind BC |
mean species fraction (default O₂) at a probe pipe |
fuel_injector |
FuelInjectorController |
Cylinder.m_fuel_per_cycle (per-injector ratios preserved) |
mean species fraction (λ feedback, default O₂) at a probe pipe |
spark_timing |
SparkTimingController |
WiebeCombustion.theta_start_deg (retards on knock) |
Cylinder.last_knock_integral (Livengood-Wu) |
WastegateController is sign-inverted relative to BoostController:
when boost overshoots target, the wastegate opens to bleed exhaust
around the turbine; when underboost, it closes. The two coexist on
the same Turbine (a VGT-with-wastegate stage uses both controllers
on the same turbine instance, actuating two independent surfaces).
DOCSulfurController integrates a lumped sulfate balance against
the bound DOC pipe's mean gas T:
Storage Ea defaults are low (proceeds at exhaust T); reduction Ea
defaults are high (meaningful only above ~870 K) — this matches the
literature DOC desulfation window. The closure's deactivation factor
f_S = 1 − α_S · θ_S is shared with the HC and CO oxidation rate
constants so the brick gradually loses activity as sulfate
accumulates.
Compressor surge / choke diagnostics¶
Compressor.last_regime exposes the operating-point regime as one
of "normal" / "idle" / "flow_through" / "surge" / "choke",
plus normalised last_surge_margin / last_choke_margin
fractional distances to the top / bottom edges of the map's PR grid
(positive when inside the envelope, ≤ 0 at or past the edge). (The
sibling Turbine.last_regime uses "backflow" for its own PR < 1
branch.)
| Condition | Regime |
|---|---|
omega <= 0 |
"idle" |
PR < 1.0 |
"flow_through" (un-boosted / windmilling — PR clamped to the choke-most edge, flow passed through with zero compression work) |
PR > pr_grid[-1] |
"surge" (operator demands more PR than the map covers) |
PR < pr_grid[0] |
"choke" (PR clipped to right edge of speed line) |
| else | "normal" |
Only the idle branch wall-closes both pipe ends. The PR < 1
(under-boost) case is not wall-closed: an earlier version blocked
all airflow through the compressor whenever boost had not yet built,
starving the engine; the compressor now clamps the map lookup to the
choke-most PR and passes the flow through at T0_out = T0_in (audit
2026-06-10 A1.10), reporting "flow_through". The margin fields are
pure diagnostics that controllers and post-processors read to detect
when an operating point has crossed an envelope edge.
ML substitution pattern¶
A learned closure replaces an @njit kernel with a NumPy-mode Python
callable that has the identical signature. The solver detects the
substitution via a per-pipe flag (pipe.py_friction /
pipe.py_heat / pipe.py_porous) and routes the affected pipe
through a Python residual wrapper — interior cells are still
evaluated in bulk so the NumPy path is not ruinously slow.
In live mode of the Solver Internals dock (the "Refresh with built
network" button), the dock scans pipe.py_* after build_network
runs and shows which cells have been overridden. Python-callable
closures always route through the FD path on the implicit Jacobian
— that's the design intent (the closure can be anything; the
solver shouldn't assume differentiability).