How to model EGR with O₂ feedback¶
Exhaust gas recirculation (EGR) returns a fraction of the exhaust stream to the intake manifold, displacing fresh charge and reducing peak combustion temperatures (NOx control on diesel; knock margin on SI). The control loop typically targets an intake-manifold O₂ mass fraction — measure with a UEGO sensor, regulate via the EGR valve.
This tutorial wires an egr_valve BC plus the egr_valve
controller that closes the loop on intake O₂ mass fraction.
Bundled example: examples/62_egr_valve_ctrl.toml.
What you'll build¶
[fresh air] → [intake manifold] → [cylinder] → [exhaust manifold] →
↑ │
└──── [EGR valve BC] ──────────┘
↑
[O₂ probe on intake]
│
PI controller
The EGR-loop topology in aeolus1d is built without a closed pipe loop — instead, a stagnation reservoir representing the hot exhaust feeds the EGR BC, and the intake side is the EGR BC's target pipe. Bidirectional flow at a junction would otherwise need a much more careful junction model; the BC-as-source approach captures the dominant physics for control studies.
Step 1 — add the EGR valve BC¶
A egr_valve BC at the right end of the intake-manifold pipe:
[[bc]]
pipe = "intake_manifold"
end = "right"
kind = "egr_valve"
# Stagnation conditions on the EGR side (exhaust). Hot,
# species-rich with low O₂ — the loop reduces intake O₂ via
# this composition.
p0 = 1.5e5
T0 = 850.0
# Orifice geometry — same convention as throttle.
A_max = 5.0e-4
A_pipe = 1.2e-3
Cd = 0.7
# Species composition of the EGR feed. Mass fractions —
# Y_supply is the primary species (typically N2 here so the
# fresh-air O2 fraction drops); Y_extras_supply is a table
# mapping each non-primary species name to its mass fraction.
Y_supply = 0.78 # N2 mass fraction (primary)
[bc.Y_extras_supply]
O2 = 0.05 # 5% O2 in EGR feed (depleted vs intake's 23%)
CO2 = 0.13
H2O = 0.04
The bundled
examples/62_egr_valve_ctrl.tomluses a bare[bc.Y_extras_supply]with justO2 = 0.05and leaves the primary species ("fuel") absent (Y_supplyunset → no primary injected). Only the species the probe watches has to be present in the feed; the rest is optional detail.
When the controller drives the valve open, the BC writes mass-flow-weighted composition into the ghost cells; the species-transport scheme distributes it down the manifold.
Step 2 — pick the species the controller watches¶
The controller measures O₂ on the intake side; the species
ordering in aeolus1d is pipe.species_name (primary) plus
pipe.species_names (extras). For an SI/diesel case with N₂ as
the primary species, O₂ is one of the extras. Extras are
declared as an array-of-tables, each with its own name and
Y_init (there is no separate init_mass_fractions block):
[[pipe]]
id = "intake_manifold"
# ... geometry ...
[pipe.species]
reaction = "none"
name = "N2" # primary species
Y_init = 0.77 # primary mass fraction
[[pipe.species.extras]]
name = "O2"
Y_init = 0.23 # fresh-air O2 baseline
[[pipe.species.extras]]
name = "CO2"
Y_init = 0.0
[[pipe.species.extras]]
name = "H2O"
Y_init = 0.0
Step 3 — close the loop¶
[[controller]]
id = "egr_ctrl"
kind = "egr_valve"
egr_pipe_id = "intake_manifold" # which pipe the EGR BC lives on
egr_end = "right" # which end of that pipe
probe_pipe_id = "intake_manifold" # where O₂ is read (same pipe is fine)
species_name = "O2" # the extras-species name
Y_target = 0.15 # mass fraction target (lower = more EGR)
Kp = 5.0
Ki = 30.0
open_min = 0.0
open_max = 1.0
species_name defaults to "O2" and Ki defaults to 0.0
(pure-P) if omitted; open_min/open_max default to
0.0/1.0.
The controller reads the cell-mean Y_O2 on intake_manifold
every step. It is a velocity-form PI: the increment is
Δopen = Kp·(e − e_prev) + Ki·e·dt, where e = Y_O2 − Y_target.
Note the sign — measured O₂ above target means the intake is
too oxygen-rich, so the controller opens the valve wider (more
inert EGR dilutes the intake, dropping O₂). The output is
clipped to [open_min, open_max].
The EGR valve is built fully closed (open = 0.0): the
build layer hard-sets the initial open fraction, so the loop
always starts from a shut valve regardless of the EGRValve
dataclass default. Because the velocity form needs a prior error
to differentiate, the controller is a no-op on its very first
step — the valve holds closed for one step, then begins to
actuate.
Step 4 — run and inspect¶
aeolus1d run examples/62_egr_valve_ctrl.toml --out /tmp/egr.h5 \
--trace-dt 1e-3
In the GUI, plot:
- Intake-manifold
Y_O2(t)— should settle near the target. - The EGR valve's open fraction lives on the mutable
EGRValvestate (valve.open) and is not written to the trace file, so watch the intakeY_O2settling towardY_targetas the indirect signal.--info-every-n Nprints step/dt/Picard progress lines, not controller state.
If the loop hunts: drop Ki first, then Kp. If it lags: raise
Kp then Ki. The bundled 62_egr_valve_ctrl.toml uses
Kp = 5.0, Ki = 30.0 on a bare intake pipe (mass-flow inlet +
EGR valve, no cylinder); the controller docstring suggests
Kp on the order of tens for a Y-error of a few hundredths of a
mass fraction, so treat these as starting points and retune for
your own manifold volume and flow rate.
Common gotchas¶
- Forgot to list
O2inextras. The controller silently reads zero for the species (_pipe_mean_speciesreturns0.0when the pipe doesn't carry it). Since0 < Y_target, the error stays negative and the valve is driven fully closed — you get no EGR at all and the intake O₂ never moves. Run through schema validation first (typo hints help — see the QW2 unknown-key warnings). - Reservoir composition wrong. EGR with
Y_O2 ≥ intake_Y_O2pushes O₂ up, not down. Double-checkY_extras_supply. - Loop unstable in steady-state. Usually
Kitoo high. Halve it.
Reference¶
examples/62_egr_valve_ctrl.toml— full working case.aeolus1d.bc.egr_valve— the BC implementation (make_egr_valve_bc+ the mutableEGRValvestate).aeolus1d.control.egr_valve_controller.EGRValveController— the velocity-form PI loop.- For the alternative cooler-only path (no controller, just a
heat-loss element), see
examples/44_egr_cooler.toml.