Skip to content

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_y schedule (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:

  1. 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.
  2. Plenum pressure — pulse damping: exhaust plenum sees discrete pulses, silencer almost flat (it's doing its job).
  3. Per-cylinder fuel inventory — sawtooth: injection adds mass, exhaust valve open removes it via blowback.
  4. Plenum fuel accumulation — exhaust plenum's m_fuel ramps; silencer's stays tiny.
  5. 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).
  6. 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.5 on 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 — see examples/59_wastegate_boost_ctrl.toml and the closures reference for the full controller catalogue (boost, wastegate, DPF heater, DOC sulfur).

  • Detect surge / choke. After a run, the Compressor.last_regime field reads as "normal" / "idle" / "backflow" / "surge" / "choke"; last_surge_margin and last_choke_margin give 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.reaction from hc_oxidation to doc_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 as theta_S accumulates from the Y_SO2_in setpoint, and re-activate during a high-T active-regen window with clean fuel (drop Y_SO2_in = 0 during regen). See examples/61_doc_sulfur_poisoning.toml for 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 use aeolus1d.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_metrics on 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.toml case 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 — see aeolus1d optimize.

  • Custom physics via closures. The py_reaction / py_friction / py_porous / py_junction_bc seams let you plug in arbitrary Python rate / closure callables. See Closures. The aeolus1d.closures.pinn module (Phase 7h) shows how to drop in a trained ML surrogate.

  • FMI export. aeolus1d.fmi can wrap a case as a Functional Mock-up Unit for co-simulation with Simulink / Modelica / GT-SUITE — see Interop.

  • Acoustic modal analysis. aeolus1d modes runs 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.