Skip to content

[[cylinder]] reference

The exhaustive field reference for the cylinder block and its sub-tables: [cylinder.init], [cylinder.species_init], [[cylinder.port]], [[cylinder.injector]], [cylinder.combustion] and [cylinder.fmep].

This is the [[cylinder]] counterpart of run-settings.md, which covers the case-level [case] flags — including the three that govern how a cylinder's ports exchange mass and energy with their runners (cylinder_orifice_valve_flow, binary_valve_apply_cd, valve_stagnation_recovery). Read that section too; the valve coupling is a case-level decision, not a per-cylinder one.

For the physics behind these fields see theory §6, and for what the models do not cover see scope-and-limitations.

tests/unit/test_cylinder_reference_doc_coverage.py asserts that every field of every spec below appears here, so a new knob fails CI until it is documented.

[[cylinder]] — geometry and kinematics

Field Type Default Meaning
id str required Identifier. Used by controllers (cylinder_id), by the HDF5 writer for the per-cylinder group name, and by the GUI.
bore float required Cylinder bore \(D\) [m].
stroke float required Piston stroke [m]. The crank radius is stroke / 2.
con_rod_length float required Connecting-rod length [m]. Enters the slider-crank displacement directly — it is not a ratio.
clearance_volume float required Volume at TDC [m³]. For a target compression ratio, V_disp / (CR - 1).
omega_crank float 0.0 Crank speed [rad/s]. Optional when the case carries a [crankshaft] block with rpm — the build path then derives every cylinder's ω from 2π·rpm/60. A per-cylinder value > 0 still wins (legacy override), and the build path warns when the two disagree.
theta_init_deg float 0.0 Crank angle at t = 0, in the user's CAD convention. This is how firing order is set on a multi-cylinder engine: an inline-4 firing 1-3-4-2 uses offsets 0 / 180 / 540 / 360.
cad_at_tdc float 0.0 Which CAD value corresponds to TDC. 0 (the historic default) means θ = 0 is TDC. Set e.g. 360 to declare that the user's θ = 0 is intake-stroke TDC and firing TDC sits at CAD 360. Read by the kinematics and by the GUI's "show time as CAD" readout.

[crankshaft] — global engine speed and phasing

An engine has one crankshaft, so engine speed belongs to the case rather than to each cylinder. This optional top-level block centralises it. It is a singular [crankshaft] table, not a list.

Field Type Default Meaning
rpm float 0.0 Engine speed [rev/min]. The build path converts to ω = 2π·rpm/60 and assigns it to every cylinder's omega_crank. 0.0 means "leave each cylinder's own omega_crank alone", which is why adding this block to an existing case cannot change it.
theta0_deg float 0.0 Global crank-angle offset added on top of each cylinder's theta_init_deg. Re-phases the whole engine without editing every cylinder.
firing_order list[str] [] Cylinder IDs in the order their power strokes fire. When set, the parser derives each named cylinder's theta_init_deg from the cumulative firing intervals — unless any named cylinder writes theta_init_deg itself, in which case nothing is derived and the case loads exactly as before. See Firing order and phasing.
firing_interval_deg list[float] [] Crank-angle gap before each firing-order position, so entry 0 is always 0.0. Left empty it is materialised as [0, 720/n, …] (even fire, four-stroke). Write it explicitly for an odd-fire engine — a 90° V6 is [0, 90, 150, 90, 150, 90].
[crankshaft]
rpm = 7000.0
theta0_deg = 0.0
# firing_order = ["cyl1", "cyl5", "cyl3", "cyl6", "cyl2", "cyl4"]

A per-cylinder omega_crank > 0 still wins over rpm (legacy override), and the build path warns when the two disagree rather than silently picking one.

Firing order and phasing

Set firing_order and the phases are derived for you:

[crankshaft]
rpm = 1500.0
firing_order = ["cyl1", "cyl5", "cyl3", "cyl6", "cyl2", "cyl4"]

gives 0, −120, −240, −360, −480, −600 — the cumulative-negative form the GT importer uses. That is the same set of phases a hand-written file expresses as 0, 600, 480, 360, 240, 120, because theta_init_deg enters a 720°-periodic cycle (−120 ≡ 600). Either spelling runs identically.

For an odd-fire engine, supply the intervals:

firing_interval_deg = [0.0, 90.0, 150.0, 90.0, 150.0, 90.0]   # 90° V6

Explicit phases always win. If any cylinder named in firing_order writes its own theta_init_deg, nothing is derived and the case is bit-identical to one without firing_order — validate notes that the field is redundant for phasing there. This is deliberate: phases are relative, so one cylinder is always the reference at 0.0, and a rule that treated "zero" as "unset" would silently overwrite a hand-phased engine. The test for "did the user set this?" is whether the key is present, not what its value is.

Errors you will get rather than silence: an ID in firing_order that matches no cylinder, an interval list whose length differs from the firing order, a first interval that is not 0.0, a negative interval, or intervals summing past 720°.

[cylinder.init] — initial charge state

Same body as a pipe's [pipe.init] (a UniformInit): set p plus one of T / rho, optionally u. Omitted, the cylinder starts at 1 atm / 300 K.

[cylinder.species_init] — initial charge composition

A plain name = Y table of mass fractions, e.g.

[cylinder.species_init]
"N2" = 0.767
"O2" = 0.233
Field Type Default Meaning
species_init dict[str, float] {} (empty) Initial mass fraction of each non-fuel species column. Empty means seed from the runners: on the first step the inventory is set from the port-average of the connected pipes' boundary composition, so the cylinder starts in equilibrium with them instead of writing a spurious Y = 0 step-down into them. Naming a species no connected pipe carries raises — a mass fraction that goes nowhere is a config error, not a no-op. The fuel column is not settable here; it is owned by m_fuel_per_cycle / volume.m_fuel.

[[cylinder]] — combustion products

Field Type Default Meaning
combustion_products bool True Convert burnt fuel into complete-combustion products in the cylinder's species inventory: O₂ consumed, CO₂ and H₂O produced, proportional to the fuel mass the combustion model reports burning. Only columns the case actually carries take part, so this is inert unless a connected pipe declares O₂ / CO₂ / H₂O. Default True is deliberate: with it off, a cylinder that tracks O₂ and burns fuel reports its intake oxygen level at the exhaust port forever. Set False only when the columns are meant as passive tracers.
fuel_H_C_ratio float 1.85 Fuel H/C atom ratio driving those yields, via CH_y + (1 + y/4) O₂ → CO₂ + (y/2) H₂O. 1.85 = diesel surrogate (stoichiometric AFR 14.54, matching the predictive diesel closure's 14.5); 2.25 ≈ gasoline (15.09); 4.0 = methane (17.20). The AFR is an output of this field, so it cannot disagree with the product split. See aeolus1d.chemistry.fuel_stoichiometry.

[[cylinder]] — legacy single-injector fields

Ignored entirely when [[cylinder.injector]] is present. When injectors is empty and m_fuel_per_cycle > 0, the build path migrates these into a one-element injector list, so existing single-injector cases keep working unchanged.

Field Type Default Meaning
inject_cad list[float] [] Injection-rate abscissa in crank degrees. Cyclic over 720°.
inject_rate list[float] [] Normalised rate shape against inject_cad. Dimensionless — only the shape matters; the runtime scales it so the cycle integral equals m_fuel_per_cycle exactly.
m_fuel_per_cycle float 0.0 Absolute fuel mass per 720° cycle [kg]. 0 (default) keeps the cylinder purely air-breathing.
T_fuel float 350.0 Temperature of the arriving fuel [K]. Sets the sensible energy ṁ·c_v·T added to the charge alongside the mass.
cv_fuel float 1700.0 Specific heat of the injected fuel [J/(kg·K)] — diesel-grade hydrocarbon.
h_vap_fuel float 0.0 Latent heat of vaporisation [J/kg]. > 0 turns on evaporative charge cooling: net injected energy becomes ṁ·(c_v·T_fuel − h_vap). Default 0 is bit-identical to no cooling.

[[cylinder.injector]] — multi-injector (split injection, dual fuel)

A list; each entry is one injector. Present entries make the legacy fields above dead.

Field Type Default Meaning
cad list[float] [] This injector's CAD abscissa.
rate list[float] [] This injector's normalised rate shape.
m_fuel_per_cycle float 0.0 This injector's fuel mass per cycle [kg].
T_fuel float 350.0 As above, per injector.
cv_fuel float 1700.0 As above, per injector.
h_vap float 0.0 As h_vap_fuel, per injector — this is where dual-fuel cases differentiate a volatile from a heavy fuel.
fuel_species_name str "fuel" Species column this injector's fuel is routed to on connected pipes. A pipe participates when its primary species ([pipe.species] name) or one of its extras columns matches. "" disables routing for this injector.

[[cylinder.port]] — valve ports

Two mutually exclusive valve modes. Binary (the default) means fully open inside [open_deg, close_deg] modulo 720° and fully closed outside. Continuous lift activates when both lift_cad and lift_y are populated, giving an effective area Cd · A_port · lift(θ) / lift_max. The schedule is cyclic on 720° because the kinematics wrap θ into [0°, 720°) before the lookup.

Field Type Default Meaning
pipe str required Id of the runner pipe this port connects to.
end "left" | "right" required Which end of that pipe the cylinder sits at.
A_port float required Full-open port (curtain) area [m²].
open_deg float required Valve-open crank angle [deg].
close_deg float required Valve-close crank angle [deg]. May be numerically less than open_deg — the window wraps through 720°/0°, which is how overlap at TDC is expressed.
lift_cad list[float] [] Lift-schedule abscissa [deg CAD]. Populating this and lift_y switches the port to continuous-lift mode.
lift_y list[float] [] Lift values against lift_cad, in the same units as lift_max.
lift_max float 1.0 Lift value that corresponds to the full A_port. The area fraction is lift(θ)/lift_max, so lift_y can be given in mm with lift_max in mm.
Cd float 0.7 Discharge coefficient. Always applied on the lift-schedule branch. On a binary port it is applied only when the case sets binary_valve_apply_cd = true; the legacy default uses Cd = 1.0 there and so over-breathes by ~1/Cd.
phase_offset_deg float 0.0 VVT cam-phaser shift applied to both the open/close window and the lift lookup. Positive = advance (valve opens earlier). Mutable at runtime by a controller.
lift_scale float 1.0 Variable-lift multiplier on the final area fraction. 1.0 nominal, < 1.0 reduced lift (VTEC low-cam, AFM half-lift), 0.0 effective cylinder deactivation. Mutable at runtime.

[cylinder.combustion] — heat release

Present ⇒ the cylinder burns; absent ⇒ purely air-breathing (the historical default, and bit-equivalent to omitting the block). The block selects between three models with its kind field: "wiebe" (default), "si_turbulent" (predictive SI turbulent-flame entrainment) and "diesel_predictive". Only the selected kind's parameters are read.

The Wiebe burnt-fraction profile is

\[ x_b(\theta) = 1 - \exp\!\left[-a\left(\frac{\theta - \theta_\text{start}}{\Delta\theta}\right)^{m+1}\right] \]

with gas-side heat release Q̇ = m_fuel · LHV · dx_b/dθ · ω.

Because this block carries three models' parameter sets, its full field list lives with the model that owns it — see theory §6.2 for Wiebe, and WiebeSpec in aeolus1d/io/schema.py for the si_* and diesel_* parameter groups. The fields most cases touch:

Field Type Default Meaning
theta_start_deg float — Start of combustion [deg CAD].
duration_deg float — Burn duration Δθ [deg].
a float — Wiebe efficiency parameter. 5.0 burns ~99 % of the fuel within the window.
m float — Wiebe shape exponent. 2 is standard SI; CI runs higher.
m_fuel float — Fuel mass per cycle for the burn [kg]. Overridden at runtime by the actually-injected mass whenever the cylinder has an injection schedule; set it explicitly with injection off for a fixed-mass burn study.
LHV float — Lower heating value [J/kg]. Inherited from the case-level [[species]] entry of the same name when that is present.
two_zone bool false Maintain a separate burnt-zone temperature for diagnostics.
gamma_burnt, gamma_unburnt float unset Per-zone γ. When both are set the volume's thermodynamic mapping uses γ_eff(x_burnt), which pulls the single-γ Wiebe peak-temperature artefact (~6000 K) toward the physical adiabatic-flame band (~2500 K). Note this affects the volume's E↔T mapping only; the port/HLLC machinery still uses the constant advection γ — see "Two γ's, and the seam between them" below.

Two γ's, and the seam between them

Setting these gives the cylinder two γ's that must not be confused:

  • the advection γ ([gas] gamma, or the cylinder's own) — what the connected runner's HLLC and the orifice mass-flux relation use, and what keeps the neighbouring Riemann problem well-posed;
  • γ_eff(x_burnt) — what the volume's own (E ↔ T, E → p) mapping uses.

Wherever a quantity crosses that seam it must be expressed in a form that does not presume a single γ. Two places where getting it wrong was silent:

  • Enthalpy through the valve. The mass leaving carries h = e + p/ρ, which Volume.h computes as (E + pV)/m — exact under γ_eff, under the caloric e = ∫c_v dT inverse and under a real-gas EOS alike. Building it as cp(γ_advection)·T instead under-removes 1 − cp(γ_adv)/cp(γ_eff) of the outgoing energy (26 % at 1.4 against 1.27) without breaking global conservation, because the same flux is credited to the runner. The cylinder simply keeps energy the exhaust should have taken.
  • x_burnt is composition, not progress. It is the burnt-mass fraction of what is in the cylinder now, maintained as an inventory that survives the cycle wrap and dilutes across the intake stroke. The burn model's last_xb — 0 before ignition, 1 after, reset every cycle — is a progress variable and feeds the inventory as a source. Assigning one to the other makes γ_eff jump at the wrap, and with it p = (γ_eff−1)E/V and T.

Both are measured on the EPW diesel in examples/validation/avl_epw_diesel_steady/README.md.

Three further crossings of the same seam were closed afterwards, each with the same signature — conserved, therefore silent:

  • The valve's mass flux, not just its energy. The gas accelerating through the throat belongs to whichever side is upstream, so the isentropic-nozzle relations take that side's γ and R (gamma_a/gamma_b, R_a/R_b on valve_flow). Built from the neighbour's advection γ, a choked blowdown at γ_eff = 1.27 against 1.4 passes 3.5 % too much mass; 2.3 % in late blowdown, 0.7 % through the exhaust stroke. R cancels identically for a legacy volume — which defines T := p/(ρR) with that same R — and stops cancelling under caloric_thermo, where R_eff = R_mix(Y): +0.8 % alone, +4.3 % compounded with the γ error.
  • Reading the runner back. The runner's conserved state was encoded by the pipe's γ — per cell when per_cell_gamma is on, where gamma_pad at the boundary slot is what the flux kernels themselves decode with. Decoding it with the cylinder's [gas] gamma inflated the pressure the valve sees by 20–35 % on a burnt-gas runner, which then set the flow-direction test, the orifice pressure ratio, and — on inflow, where the cylinder passes no h0_a — the enthalpy carried in. examples/91_cylinder_species_transport.toml is the bundled case where this was live; its documented mid-runner composition moved when it was fixed.
  • The two-zone burnt mass. _update_two_zone_diagnostics split the charge with m_total · last_xb — progress again. The residual is burnt gas; booking it into the unburnt zone at T_u and then dividing the remainder by a too-small m_b inflated T_burnt by +537 K at x_b = 0.1 (×18.6 on the Zeldovich NO rate) with the ~3.5 % residual the EPW diesel carries. Note the subtlety: the inventory is advanced in apply_fuel_step, which runs after the diagnostics, so a diagnostic must read _burnt_mass_now() — inventory plus the increment the current progress implies — and not the raw inventory, which is one step stale.

The 5 % gate that guards the two-zone division still reads last_xb, and so does Woschni's motored reference: both ask a progress question ("has enough burnt for this to mean anything"), which is the distinction the two variables exist to keep apart.

To check the whole class at runtime on your own case, see AEOLUS_PHYSICS_ASSERTS in troubleshooting.md.

[cylinder.fmep] — Chen-Flynn friction

Absent ⇒ friction is unmodelled: the per-cycle accumulator reports FMEP_Pa = 0 and BMEP_Pa = IMEP.

Field Type Default Meaning
A float 75000.0 Constant term [Pa].
B float 0.005 Peak-cylinder-pressure coefficient [-].
C float 90.0 Mean-piston-speed coefficient [Pa·s/m].
D float 1.2 Mean-piston-speed-squared coefficient [Pa·s²/m²].

Wall heat transfer (Woschni)

Flat [[cylinder]] fields. All-zero (the default) = adiabatic walls, the legacy behaviour.

Field Type Default Meaning
T_wall float 0.0 Single lumped wall temperature [K]. > 0 constructs a WoschniHeatTransfer for the cylinder.
T_head float 0.0 Head-zone wall temperature [K].
T_liner float 0.0 Liner-zone wall temperature [K].
T_piston float 0.0 Piston-crown wall temperature [K]. Any of these three being > 0 splits Q̇ across head / liner / piston zones instead of one lumped wall.
woschni_c1 float 0.0 Woschni C₁ override. 0 = use the model default (2.28, Heywood eq. 12.30 for compression/expansion). GT-Suite cases often tune it down (~0.75) or up (~6.18 for gas exchange).
woschni_c2 float 0.0 Woschni combustion-turbulence coefficient [m/(s·K)]. 0 = the C₁-only single-term form (bit-exact); 3.24e-3 is the standard value, which lifts in-cylinder wall heat during combustion.

Knock (Livengood-Wu + closed-loop retard)

Flat [[cylinder]] fields. The defaults are the Douaud-Eyzat values already on the Cylinder dataclass, so a TOML without them is bit-exact.

Field Type Default Meaning
ignition "auto" | "spark" | "compression" "auto" How this cylinder ignites, which decides the knock-controller defaults below. "auto" resolves at parse time: compression ratio ≥ 14 with a combustion kind other than si_turbulent means compression ignition. See Ignition mode and the knock controller.
knock_A float 17.68e-3 Ignition-delay coefficient [s·atm^n].
knock_n float 1.7 Douaud-Eyzat pressure exponent.
knock_B float 3800.0 Arrhenius factor Eₐ/R [K].
knock_retard_deg_per_event float 2.0 Spark retard applied each time knock fires [deg].
knock_retard_max_deg float 10.0 Cap on accumulated retard [deg].
knock_advance_deg_per_cycle float 0.5 Recovery per knock-free cycle [deg]. The knock integral accumulates only over the pre-ignition window [IVC, θ_start], so it fires on genuine pre-spark auto-ignition rather than every cycle.
caloric_thermo bool true Use c_v(T, Y) from the tracked species' NASA-7 polynomials instead of a constant c_v from [gas] gamma. Default since 2026-08-06. See Caloric charge thermodynamics.
ignition_auto_as str (observed) Not a TOML key — what ignition = "auto" (or no ignition key) resolved to when the file was read, "" when the file named a mode. The GUI writer leaves such a mode out of a saved file, so the engine is re-classified from its geometry on the next load instead of being stamped spark / compression. Writing this key in a file does nothing.
caloric_thermo_explicit bool (observed) Not a TOML key — the loader sets it to whether the case file wrote caloric_thermo. A cylinder that asked for caloric thermodynamics but tracks no species gets a RuntimeWarning (the flag cannot engage); a case merely inheriting the true default stays on the silent composition-gated path. Writing this key in a file does nothing: explicitness is observed, not declared.

Ignition mode and the knock controller

The knock criterion is a Livengood–Wu autoignition integral. In a spark-ignition engine, end-gas autoignition is knock, so retarding spark when the integral crosses 1.0 is the right control action.

A diesel burns by autoignition. Its integral crosses the threshold every cycle by design, is read as knock, and retards until it saturates at knock_retard_max_deg — and that retard is applied to every Wiebe stage, so the whole burn executes late. Measured on the EPW HD diesel: all six cylinders pinned at 10°, knock integrals 9–11.6 against a threshold of 1.0, recovery (0.5°/cycle) far slower than re-trigger, costing peak firing pressure −18…−25 %.

So ignition decides the defaults:

ignition resolves to knock_retard_deg_per_event knock_retard_max_deg
"spark" 2.0 10.0
"compression" 0.0 0.0 (controller off)

Two things to know:

  • Writing either knob keeps your value, on any engine. If you are deliberately studying knock on a diesel, set knock_retard_max_deg = 4.0 and it stays 4.0.
  • A high-compression SI engine should say so. The "auto" rule reads CR ≥ 14 as compression ignition. An Atkinson-cycle or high-compression NA petrol engine near that line should write ignition = "spark" explicitly.

aeolus1d validate still warns when a compression-ignition cylinder has an active retard, so an explicit re-enable is visible rather than silent.

Caloric charge thermodynamics

Real γ is not constant. Air is 1.40 at 300 K but 1.34 by 900 K, and burnt gas is ≈1.26 at 2200 K. With caloric_thermo = true (the default since 2026-08-06) the cylinder evaluates c_v(T, Y) from the tracked species' NASA-7 polynomials, so γ responds to temperature, EGR and equivalence ratio on its own. It needs a charge composition — either [cylinder.species_init] or one seeded from the connected pipes — and every tracked species present in the thermo DB.

With caloric_thermo = false the charge is calorically perfect: c_v is fixed by [gas] gamma, and any variation has to be supplied by hand through the combustion spec's gamma_burnt / gamma_unburnt, which blend on burnt-mass fraction. Those two numbers are per-case calibration, and they are inert under the default — Volume.gamma_eff returns c_p/c_v and never reads them.

This changes the model, not a coefficient. Once c_v depends on temperature, e = ∫₀ᵀ c_v dT′ ≠ c_v(T)·T, so the calorically-perfect identity p = (γ−1)ρe no longer holds — it under-predicts pressure by ~3 % at intake temperature and ~14 % at combustion temperature. Pressure is therefore taken from ρRT, with T recovered by inverting the caloric energy, and γ becomes the sound-speed / ratio-of-specific-heats diagnostic rather than the thing that sets pressure. R stays constant, matching the pipes' constant-R / variable-γ convention; a mixture's own R differs from 287.05 J/(kg·K) by only 0.4–1.6 % across air and diesel products.

Why it became the default

It shipped opt-in because it moves gated engine metrics. It is now on by default because the direction it moves them was measured. On the AVL EPW diesel, three anchor operating points, mean absolute error in IMEP_High:

caloric_thermo = false caloric_thermo = true
IMEP_High mean |err| 16.0 % 2.8 %

The mechanism is a level error rather than a trend. The legacy gamma_unburnt = 1.35 implies a c_v about 13 % too high for fresh air, which under-predicts compression pressure at every operating point — so no recalibration of the burn could have fixed it.

Two consequences worth planning for:

  • Cost. Roughly 1.9× wall clock on a cylinder case; the temperature comes from a Newton inverse on every evaluation.
  • Fitted wall closures need re-identifying. Anything calibrated against the old charge model is now fitted to the wrong thing. The EPW case ships the refit — exhaust-plenum h·A 850 → 550 W/K, rms T_EXH error 35.9 K → 7.1 K — and that sweep also confirmed h·A is a clean lever: IMEP_High, PFP and Q_WHT came back identical at every h·A tried, so the refit absorbs no error from other channels.

Setting caloric_thermo = false restores the constant-c_v model unchanged.

The default is composition-gated

Turning the default on does not turn caloric thermodynamics on for a cylinder that carries no composition. The NASA-7 table attaches lazily, once the charge composition is resolved from [cylinder.species_init] or seeded from the connected pipes. A species-less cylinder never switches and runs the legacy constant-c_v path regardless of the flag.

This is why flipping the default moved no golden regression snapshot: 31_cylinder and 38_engine_turbo_scr are both species-less, so both are genuinely unaffected. It is the safe direction — nothing changes silently — but it does mean the default is conditional. If you want the caloric charge model, give the cylinder a composition. Pinned by tests/unit/test_caloric_default_is_composition_gated.py, which asserts the spec says True and the built volume says False, so that "the snapshots did not move" can never be confused with "the flag silently stopped working".

Structural fields

Field Type Default Meaning
init table 1 atm / 300 K The [cylinder.init] sub-table above.
ports list [] The [[cylinder.port]] entries above.
injectors list [] The [[cylinder.injector]] entries above.
combustion table | absent absent The [cylinder.combustion] sub-table above.
fmep table | absent absent The [cylinder.fmep] sub-table above.