How to add a throttle controller¶
A throttle is the motor-driven orifice between the airbox and the
intake manifold. In aeolus1d it's modeled as an inlet BC of kind
throttle — a stagnation reservoir on the airbox side, an orifice
whose effective area is the open-fraction × A_max × Cd (Phase E1).
The open fraction defaults to 1.0 (wide-open) at build time and is
mutable at runtime through a small Throttle state object.
The fully-built-in closed-loop story (a [[controller]] kind =
"throttle" block writing the open fraction from a manifold-pressure
setpoint) is not in place today — it's a BACKLOG follow-up. This
tutorial walks through the part that does work now: dropping a
throttle BC into a case and mutating its open fraction either
statically (one value at build time) or dynamically (from a small
Python control script running alongside the solver).
Open the demo case¶
The bundled examples/72_throttle_demo.toml is a minimal pipe
network with a throttle BC + a mass-flow extraction at the
downstream end — the kind of layout a control script would
exercise without engine baggage.
aeolus1d run examples/72_throttle_demo.toml --out /tmp/throttle.h5 --trace-dt 1e-3
Open the trace in the GUI (aeolus1d-gui /tmp/throttle.h5) and
plot the manifold pipe's mid-pipe pressure time series. At
WOT (open=1.0), the manifold sits near atmospheric — the
0.02 kg/s extraction at the right end produces only small
pressure-wave dynamics.
Step 1 — Static partial-throttle via the build-time API¶
The throttle's open fraction lives on a Throttle dataclass
(defined in aeolus1d.bc.throttle) that build_network records
in the aeolus1d.io.build._THROTTLES registry under the
(pipe_id, end) key. The public accessor is
get_throttles_registry() in the same module. To start
partially closed, build the case in Python and write the open
fraction before the timeloop starts:
from aeolus1d.io.case import load_case
from aeolus1d.io.build import build_network, get_throttles_registry
from aeolus1d.solver.network_timeloop import advance_network
case = load_case("examples/72_throttle_demo.toml")
pipes, junctions, far_bcs = build_network(case)
throttles = get_throttles_registry()
# Set the throttle to 50 % open before any solver step runs.
throttles[("intake_manifold", "left")].open = 0.5
advance_network(
list(pipes.values()), junctions, far_bcs,
t_end=case.case.t_end, cfl=case.case.cfl,
)
The half-open throttle restricts mass flow — the manifold pressure drops noticeably below atmospheric for the same 0.02 kg/s draw, then recovers when the wave bounces.
Step 2 — Dynamic control via the on_step hook¶
For a closed-loop demo, pass an on_step(t) callback to
advance_network that reads the manifold pressure and writes
the throttle open fraction:
throttle = throttles[("intake_manifold", "left")]
manifold = pipes["intake_manifold"]
P_TARGET = 60_000.0 # 60 kPa
KP, KI = 1.0e-6, 5.0e-4
integ = 0.0
def control(t):
global integ
# pipe.primitives() -> (rho, u, p) arrays over the interior;
# sample mid-pipe pressure.
_, _, p = manifold.primitives()
p_mid = float(p[manifold.N // 2])
err = P_TARGET - p_mid
integ += err * 1e-4 # rough dt — pull from the actual step in real code
new_open = max(0.05, min(1.0, 0.5 + KP * err + KI * integ))
throttle.open = new_open
advance_network(
list(pipes.values()), junctions, far_bcs,
t_end=case.case.t_end, cfl=case.case.cfl,
on_step=control,
)
This shape — solver thread + Python control callback — mirrors
how the boost-pressure controller works internally; it'll move
into a first-class [[controller]] kind = "throttle" block once
the BACKLOG follow-up lands.
Step 3 — When the closed-loop controller block ships¶
A future [[controller]] entry will look like:
[[controller]]
id = "throttle_ctrl"
kind = "throttle" # not yet implemented; tracked on BACKLOG
throttle_pipe_id = "intake_manifold"
throttle_end = "left"
probe_pipe_id = "intake_manifold"
p_target = 60.0e3
Kp = 1.0e-6
Ki = 5.0e-4
open_min = 0.05
open_max = 1.0
The schema follows the same pattern as egr_valve /
fuel_injector / spark_timing — drop in the block, run, the
controller owns the throttle's open fraction for the rest of
the case.
Reference¶
examples/72_throttle_demo.toml— the runnable case this tutorial walks through.aeolus1d.bc.throttle— the Phase-E1 throttle BC +Throttlestate dataclass (make_throttle_bc).aeolus1d.io.build.get_throttles_registry()/._THROTTLES— the(pipe, end) → Throttleruntime registry populated bybuild_network(cleared at the start of each build).- See Closures — reference table for the full BC kind list.
- See How to model EGR with O₂ feedback for a worked closed-loop example using a controller kind that IS in place today.