Tutorial 3 — Engine simulation end-to-end¶
Time: ~1 hour (plus simulation wall time). Prerequisites:
Tutorials 1 and 2 finished. Anchored on:
examples/39_4cyl_engine_turbo_scr.toml.
This is the deep end. A 4-cylinder turbocharged diesel engine with realistic cam profiles, fuel injection, blowback into the exhaust, a turbocharger, a silencer plenum, a catalytic SCR brick that oxidises unburnt fuel, and finally a probe that exports the pressure trace as a playable WAV file. Every Phase 25 / 26 / 7 feature aeolus1d ships meets in this one TOML.
After this tutorial you'll have run a full engine simulation, read fuel transport metrics from the result, plotted the breathing pulses + plenum dynamics + catalyst conversion, and listened to the audible exhaust note.
The case¶
examples/39_4cyl_engine_turbo_scr.toml describes:
atm → intake_atm → Compressor → comp_out
│
▼
intake_plenum (1 L Junction)
╱ │ ╲ ╲
▼ ▼ ▼ ▼
in_run_1 in_run_2 in_run_3 in_run_4
│ │ │ │
▼ ▼ ▼ ▼
cyl1 cyl2 cyl3 cyl4 (4-stroke,
│ │ │ │ 1800 rpm,
▼ ▼ ▼ ▼ firing 1-3-4-2)
ex_run_1 ex_run_2 ex_run_3 ex_run_4
╲ │ ╱ ╱
╲ │ ╱ ╱
▼ ▼ ▼ ▼
exhaust_plenum (1 L Junction)
│
▼
plen_to_turbine
│
▼
Turbine
│
▼
silencer_in
│
▼
silencer (2 L Junction
│ expansion chamber)
▼
scr_brick (porous + 2-phase
│ catalyst + HC ox)
▼
atm
13 pipes, 3 plena, 4 cylinders, 2 turbos, 1 shaft. The
[[species]] table at the top of the case declares fuel
once with its molar mass + LHV; per-pipe blocks reference by
name and inherit (Phase 26).
Step 1 — Skim the case¶
Don't try to read every block. Look for:
- The four cylinder blocks, all identical except for
theta_init_deg: 0, 180, 540, 360. Those offsets phase the firing order to 1-3-4-2 (the standard inline-4 order). - Cosine cam profiles on every cylinder port via the
lift_cad/lift_yschedule (Phase 25 named-CAD cam input). - Fuel injection — each cylinder has
inject_cad/inject_rate/m_fuel_per_cycle = 5e-5(50 mg/cycle). Inject just before exhaust valve open so blowback is visible. - HC oxidation on the SCR brick —
species.reaction = "hc_oxidation"plus the[[species]]table at the top supplies the LHV. - Junction species tracking — Phase 25p means the exhaust plenum and silencer accumulate fuel mass that's being convected from the runners.
Step 2 — Run it¶
The default t_end = 0.040 s covers one full 720° engine
cycle at 1800 rpm — enough to see firing pulses and verify
the chain is working, fast enough to finish in ~30 s wall.
Run it whichever way is more convenient.
Option A — Terminal¶
aeolus1d run examples/39_4cyl_engine_turbo_scr.toml --out /tmp/engine.h5 --trace-dt 4.5e-5
The --trace-dt 4.5e-5 flag samples every ~22 kHz so the
output carries a usable pressure trace for audio (Tutorial 1
didn't need this — the Sod test only needed final-state).
Expected output (abbreviated — one summary line per pipe/junction):
Running case '4cyl_engine_turbo_scr' to t_end=0.04 s ...
finished at t = 0.04 s
wall time = 28.7 s (717.5x realtime)
pipe 'intake_atm' : rho=[...] u=[...] p=[...]
... (13 pipes total)
junction[0]: V=0.001 m^3, p=... Pa, T=... K
junction[1]: V=0.001 m^3, p=... Pa, T=... K
junction[2]: V=0.002 m^3, p=... Pa, T=... K
time-series samples: 889
-> wrote /tmp/engine.h5
Wall time: ~30 s on a modern laptop.
Option B — GUI¶
aeolus1d gui
File → Open → examples/39_4cyl_engine_turbo_scr.toml. The
canvas paints the network — 13 pipes, 3 plena, 4 cylinders, 2
turbos, 1 shaft — with the wiring you'd expect from the ASCII
diagram above.
Open the Runner dock's settings strip (above the run button)
and set trace-dt = 4.5e-5 so the run records a 22 kHz
time series of every pipe — needed for the audio in Step 5.
The other settings (t_end, cfl, scheme, RK integrator)
default off the case's [case] block; override them inline
if you want a quicker / longer run.
Click Run (or F5). The Runner dock streams the subprocess console output. On clean exit the post-processing tab opens the HDF5 and paints each pipe as a coloured tube — every cell across all 13 pipes shares a single colour scale. Click a pipe to focus the 2D Plot dock on its profile; switch to time-series mode to see the cylinder pressure rise / firing / blowdown sequence per cycle.
Step 3 — Inspect the fuel chain¶
The Phase 25 chain delivers fuel from cylinder injectors all the way to the SCR catalyst. Check each link:
import h5py, numpy as np
with h5py.File("/tmp/engine.h5") as f:
pipes = f["pipes"]
# Cylinder blowback shows up as Y_fuel in the runners.
for i in range(1, 5):
Y_max = pipes[f"ex_run_{i}/Y"][:].max()
print(f"ex_run_{i}: max Y_fuel = {Y_max:.3e}")
# Plenum accumulators (Phase 25p)
for jname in ("intake_plenum", "exhaust_plenum", "silencer"):
# plenum-id ordering matches case.junctions order; in this
# case index 0=intake, 1=exhaust, 2=silencer.
pass # m_fuel isn't currently in the HDF5 attrs; needs Python API
For the plenum fuel-inventory check, use the Python API:
from aeolus1d.io.case import load_case, run_case
case = load_case("examples/39_4cyl_engine_turbo_scr.toml")
case.case.t_end = 0.05
pipes, junctions, _ = run_case(case)
from aeolus1d.components.junction import Junction
for spec, j in zip(case.junctions, [
x for x in junctions if isinstance(x, Junction)
]):
print(f"{spec.id}: m_fuel = {j.volume.m_fuel * 1e6:.2f} mg")
# SCR conversion across the catalyst
scr = pipes["scr_brick"]
y_in, y_out = float(scr.Y[0]), float(scr.Y[-1])
conversion = 1.0 - y_out / y_in
print(f"SCR: Y_in={y_in:.3e} Y_out={y_out:.3e} conv={conversion:.1%}")
print(f"SCR substrate: {scr.T_solid.min():.1f} → {scr.T_solid.max():.1f} K")
Expected:
intake_plenum: m_fuel = 0.00 mg (intake side stays clean)
exhaust_plenum: m_fuel ≈ 100 mg (Phase 25p tracking the blowback cloud)
silencer: m_fuel ≈ 2-3 mg (downstream of turbine; 25q pass-through)
SCR: Y_in≈1.8e-3 Y_out≈1.1e-3 conv≈38%
SCR substrate: 550.0 → 551.4 K (HC ox heat release warming substrate)
That's the full chain delivering as expected. The exhaust plenum is the main fuel reservoir; ~2.5 % of the injected mass reaches the catalyst (most stays trapped upstream over this short window); 38% of what arrives gets oxidised.
Step 4 — Plot the chain¶
python examples/plots/plot_39_4cyl_engine.py
Saves /tmp/aeolus_example_39.png with six panels:
- Per-cylinder pressure — the firing-order signature. cyl1 (theta_init=0) fires first; cyl3, cyl4, cyl2 follow at 180° intervals. Peaks reach ~30-50 atm during compression.
- Plenum pressure — pulse damping: exhaust plenum sees discrete pulses, silencer almost flat (it's doing its job).
- Per-cylinder fuel inventory — sawtooth: injection adds mass, exhaust valve open removes it via blowback.
- Plenum fuel accumulation — exhaust plenum's m_fuel ramps; silencer's stays tiny.
- Y_fuel along the exhaust train — mass fraction profile from runners → plenum → turbine → silencer → SCR. Visible step-down at the SCR inlet (oxidation consuming fuel).
- SCR substrate temperature — small but measurable rise from the HC-oxidation heat release.
Step 5 — Listen to it¶
The pressure trace at any probe location can be exported as
audio. The bundled demo_engine_audio.py script automates
the full chain:
python examples/plots/demo_engine_audio.py
This re-runs the case (overriding t_end to longer for
audible content) with a 22 kHz tracer and writes two WAV
files: one probed at the SCR brick (post-silencer, distant
exhaust drone) and one at the exhaust manifold (raw firing
pulses).
The default T_END = 2.0 s in the script gives you 2
seconds of audio; bump it for longer clips. Wall time at
T_END = 2.0 s is around 15 minutes; higher T_END runs
proportionally.
The script prints a status line every PROGRESS_DT seconds of sim time so you can monitor a long run:
running 2.00 s of '4cyl_engine_turbo_scr'...
t = 0.250 / 2.00 s ( 12.5%) wall = 100.3 s ETA = 700.0 s tracer samples = 5513
t = 0.500 / 2.00 s ( 25.0%) wall = 201.4 s ETA = 600.0 s tracer samples = 11025
…
done in 800.0 s wall, 44102 tracer samples.
wrote /tmp/aeolus_engine_silencer.wav: 2.00 s, peak=0.95, RMS=-12.0 dBFS, audible=True
When it finishes, open the files. On Linux:
xdg-open /tmp/aeolus_engine_silencer.wav
xdg-open /tmp/aeolus_engine_manifold.wav
On WSL:
explorer.exe $(wslpath -w /tmp/aeolus_engine_silencer.wav)
explorer.exe $(wslpath -w /tmp/aeolus_engine_manifold.wav)
Or do it directly from the GUI: open the case, run, click on
scr_brick in the canvas, switch the plot dock to
time-series mode, click "🔊 Audio…". The plot dock's WSL-aware
fallback opens the saved WAV in Windows Media Player.
The two probes sound different: the manifold side is harsh and pulsed (you can count the firings); the silencer side is muffled, the firings smear into a drone. That's the silencer plenum doing its physical job.
Step 6 — Modify and rerun¶
Real engineering experiments to try:
- Add Wiebe combustion to one cylinder. Phase B22 (2026-
05-06) added a
[cylinder.combustion]sub-table:
[cylinder.combustion]
theta_start_deg = 360.0 # TDC of the firing stroke
duration_deg = 60.0
a = 5.0
m = 2.0
m_fuel = 5.0e-5 # 50 mg/cycle
LHV = 4.35e7 # diesel-grade (matches the case fuel)
Rerun. Now the injected fuel gets burnt before exhaust
opens; blowback drops dramatically; the cylinder peak
pressure spikes (50 atm → 80+ atm). The Phase 25l fuel-burn
coupling means Wiebe automatically tracks injected mass —
set m_fuel only when you want a fixed-mass burn study
decoupled from injection. For closed-loop knock-retard
control, also set the flat knock_* fields on the
[[cylinder]] block (defaults match the Douaud-Eyzat
gasoline calibration); see examples/52_knock_event.toml.
-
Open the wastegate. Set
bypass_open = 0.5on the[[turbo]] kind = "turbine"block. Rerun. Less shaft power extracted; intake boost drops; cylinders breathe less. To make this closed-loop, add a[[controller]] kind = "wastegate"block targeting the intake manifold pressure — seeexamples/59_wastegate_boost_ctrl.tomland the closures reference for the full controller catalogue (boost, wastegate, DPF heater, DOC sulfur). -
Detect surge / choke. After a run, the
Compressor.last_regimefield reads as"normal"/"idle"/"backflow"/"surge"/"choke";last_surge_marginandlast_choke_margingive fractional distances to the map's PR-grid edges. Useful when an off-design transient has the compressor walking off its envelope — the diagnostics tell you whether you've exceeded the surge line or saturated at choke. See closures § Compressor surge / choke diagnostics. -
Add a sulfur-poisoning DOC. Switch the SCR brick's
species.reactionfromhc_oxidationtodoc_oxidation_S(same HC + CO oxidation branches, plus a reversible sulfur-deactivation factor) and add a paired[[controller]] kind = "doc_sulfur"block. The brick will gradually deactivate astheta_Saccumulates from theY_SO2_insetpoint, and re-activate during a high-T active-regen window with clean fuel (dropY_SO2_in = 0during regen). Seeexamples/61_doc_sulfur_poisoning.tomlfor the full storage + active-regen demo cycle. -
Switch to ammonia fuel. Change the case-level
[[species]]block:M = 17.03e-3,LHV = 1.86e7(or useaeolus1d.fuels.get_fuel_preset("ammonia").LHV). No other change needed — the SCR brick's HC oxidation picks up the new LHV from the[[species]]table automatically. Heat release across the catalyst will be ~2.3× lower (LHV ratio). -
Compute IMEP. Use
aeolus1d.engine_post.engine_metricson the cylinder pressure trace from the tracer to get cycle-averaged IMEP, BSFC, η_thermal:
from aeolus1d.engine_post import engine_metrics
# … run with --trace-dt small enough to resolve the cycle …
# cylinder p, V come from a custom on_step recorder; see
# examples/plots/plot_39_4cyl_engine.py for the pattern.
What you've done¶
- Read a 13-pipe + 4-cylinder + 2-turbo TOML and made sense of every block.
- Run it via the CLI with audio-rate tracing.
- Used the Python API to inspect plenum fuel inventories that aren't exposed in the HDF5 yet.
- Plotted six aspects of the result simultaneously.
- Listened to the audible engine note.
- Made physically-meaningful modifications and saw the expected effects.
Where to go from here¶
-
Optimization workflow. The
examples/optimize/scr/scr_full.tomlcase fits the SCR rate constants from a measured Y_NH3(t) + Y_NO(t) trace via Nelder-Mead. The same machinery works for any cylinder / turbo / catalyst parameter — seeaeolus1d optimize. -
Custom physics via closures. The
py_reaction/py_friction/py_porous/py_junction_bcseams let you plug in arbitrary Python rate / closure callables. See Closures. Theaeolus1d.closures.pinnmodule (Phase 7h) shows how to drop in a trained ML surrogate. -
FMI export.
aeolus1d.fmican wrap a case as a Functional Mock-up Unit for co-simulation with Simulink / Modelica / GT-SUITE — see Interop. -
Acoustic modal analysis.
aeolus1d modesruns a 1-D high-order FEM eigensolver to extract acoustic eigenfrequencies of any pipe — see Mode analysis.
The rest of the documentation site is now reachable. Pick whichever direction your problem sits closest to.