Skip to content

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