Skip to content

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.toml uses a bare [bc.Y_extras_supply] with just O2 = 0.05 and leaves the primary species ("fuel") absent (Y_supply unset → 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 EGRValve state (valve.open) and is not written to the trace file, so watch the intake Y_O2 settling toward Y_target as the indirect signal. --info-every-n N prints 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 O2 in extras. The controller silently reads zero for the species (_pipe_mean_species returns 0.0 when the pipe doesn't carry it). Since 0 < 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_O2 pushes O₂ up, not down. Double-check Y_extras_supply.
  • Loop unstable in steady-state. Usually Ki too 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 mutable EGRValve state).
  • 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.