Python assembly: turbochargers, shafts and controllers¶
The last page of the pure-Python assembly ladder. Two families that share a shape: both are components that sit between pipes and carry their own state, and for both the practical Python route is to build from a case and then reach in.
Turbo machinery needs a map, so build it from a case¶
Compressor and Turbine are dataclasses, and their required fields say why
hand-assembly is rarely what you want:
Compressor required fields: ['inlet_pipe', 'inlet_end', 'outlet_pipe',
'outlet_end', 'A_inlet', 'A_outlet', 'map']
Turbine required fields: ['inlet_pipe', 'inlet_end', 'outlet_pipe',
'outlet_end', 'A_inlet', 'A_outlet', 'map']
Six of the seven are pipe plumbing you already have. The seventh, map, is a
TurboMap — a digitised performance map with its own interpolation, surge and
choke handling, and speed-line extrapolation. Constructing one literally in a
tutorial would teach the wrong thing; the map belongs in a file.
So: build the network, then take the components.
from aeolus1d.io.build import build_network
from aeolus1d.io.parsers import load_case
case = load_case("examples/28_turbo_shaft.toml")
pipes, components, far = build_network(case)
pipes : ['exhaust_cool', 'exhaust_hot', 'intake_atm', 'intake_manifold']
components: ['Turbine', 'Compressor', 'Shaft']
Turbine.omega = 1000.0
Turbine.map = TurboMap
Compressor.omega = 1000.0
Compressor.map = TurboMap
Shaft.omega = 1500.0
Shaft.inertia = 1e-05
Note what components is: the second element of build_network's return is
not "junctions" in any narrow sense — it is every non-pipe component the
timeloop advances. Junctions, cylinders, compressors, turbines and shafts all
arrive in that list, which is why advance_network(pipes, components, far, …)
takes it wholesale.
Once you have them, they are ordinary objects:
turbine = next(c for c in components if type(c).__name__ == "Turbine")
compressor = next(c for c in components if type(c).__name__ == "Compressor")
shaft = next(c for c in components if type(c).__name__ == "Shaft")
shaft.inertia = 2.0e-5 # heavier rotor: slower transient response
shaft.omega = 2000.0 # different initial speed
Turbine.omega and Shaft.omega start out disagreeing
1000 rad/s against 1500 rad/s in the listing above, straight from
build_network. That is the case's initial condition, not a bug: the shaft
owns the speed state and the machines are synchronised to it on the first
step. Do not "fix" it by hand-matching them — if you want a different
starting speed, set it on the shaft, which is the one that integrates
I·dω/dt = τ_turbine − τ_compressor.
Controllers¶
Controllers are declared in the case and materialised alongside everything else. Reading one back:
case = load_case("examples/51_vgt_boost_ctrl.toml")
for c in case.controllers:
print(c.kind, c.id)
controllers in case: 1
kind=boost_pressure {'id': 'boost_ctrl', 'kind': 'boost_pressure',
'Kp': 1e-06, 'Ki': 1e-07,
'turbine_id': 'turbine',
'manifold_pipe_id': 'intake_manifold'}
The pattern is the same as every other family: a controller names its plant
by id (turbine_id, manifold_pipe_id) rather than holding a reference, so
the case stays serialisable and the wiring happens at build time. To retune,
change the gains on the spec before building:
case.controllers[0].Kp = 2.0e-6
case.controllers[0].Ki = 5.0e-7
pipes, components, far = build_network(case)
Those gains are small because the plant is a pressure in pascals driving a
normalised actuator — Kp = 1e-6 maps a 1 bar error onto a 0.1 actuator step.
If you are retuning by hand, start from the shipped values and scale, rather
than from a textbook default in different units.
aeolus1d.control.pid_tune and the step-response
helpers exist for doing this properly.
Driving a controlled network¶
Controllers need the same clock discipline as cylinders — they sample at
simulation time — and the drivers accept a live_control object for
pause/stop:
from aeolus1d.solver.network_timeloop import advance_network
t = advance_network(list(pipes.values()), components, far,
t_end=0.2, cfl=0.4)
The 2026-07 audit found the quasi-steady and Strang drivers ignoring
live_control entirely, so pause and checkpoint-and-stop were no-ops on the
two longest-running drivers; test_drivers_accept_control_and_dt_parity_params
now pins that every driver takes the same control and Δt knobs. If you are
writing a harness that needs to interrupt a run, that test is the list of what
is guaranteed.
Where to go next¶
| Next | Page |
|---|---|
| The engine those turbos breathe for | Engines |
| Turbo map file formats and fields | Component index |
| A boost-control recipe in TOML | Recipe: Throttle controller |
| PID tuning helpers | Post-processing |