Skip to content

Python assembly: aftertreatment and the quasi-steady driver

Part of the pure-Python assembly ladder. Aftertreatment is where the driver choice starts to matter as much as the model, so this page is half about bricks and half about which timeloop you hand them to.


A monolith is a pipe with drag and heat

A catalyst or filter substrate is a porous pipe. The two closures that make it one are porous (the pressure drop through the channels) and heat (the substrate exchanging with the gas):

from aeolus1d.io.schema import HeatSpec, PipeSpec, PorousSpec, UniformInit

brick = PipeSpec(
    id="duct", length=0.2, n_cells=24, area=1.0e-3,
    init=UniformInit(p=1.0e5, T=800.0, u=20.0),
    porous=PorousSpec(kind="darcy_forchheimer", alpha=5.0e3, beta=2.0e2),
    heat=HeatSpec(kind="constant_twall", h=300.0, T_wall=400.0, D_h=0.02),
)

alpha is the Darcy (viscous, linear in u) coefficient and beta the Forchheimer (inertial, quadratic) one. Chemistry, soot loading, wash-coat storage and regeneration ride on top through CatalystSpec / FilterSpec — the closures reference is the field-by-field list, and the aftertreatment thermal reference covers the substrate/cake thermal blocks.

Why you would change driver

The explicit timeloop resolves acoustics, so its step is bounded by the acoustic CFL — microseconds. An aftertreatment system's interesting timescales are thermal and chemical — milliseconds to minutes. The quasi-steady driver drops the momentum time-derivative: it re-solves a steady flow field each macro step, then marches the slow states (species, energy, soot, cake and catalyst temperature) forward. That buys roughly two orders of magnitude in step size on a drive cycle.

from aeolus1d.io.build import build_network
from aeolus1d.solver.quasi_steady_network import (
    QSTransientConfig, advance_network_quasi_steady_transient,
)

pipes, junctions, far = build_network(case_of(brick, t_end=0.5))
t = advance_network_quasi_steady_transient(
    list(pipes.values()), junctions, far, t_end=0.5,
    config=QSTransientConfig(dt_max=5.0e-3, dt_init=1.0e-4))

Note the shape: the network is built the same way — build_network does not know or care which driver you will hand it to. Only the final call differs.

The two drivers do not give the same answer, and the gap is worth knowing

On the brick above, run to steady state under each:

t_end = 0.5 s   QS = 493.961 K   explicit = 474.067 K   gap = 19.895 K
t_end = 1.0 s   QS = 493.961 K   explicit = 474.067 K   gap = 19.895 K

Identical at both end times, so both are converged and this is a real model difference rather than one run being caught mid-transient — worth checking before drawing any conclusion from a driver comparison.

19.9 K is large, and it is mostly the porous drag. The cross-driver parity harness (tests/integration/test_driver_steady_parity.py) measures the same comparison on a duct with the heat closure only and finds 1.96 K. Adding Darcy–Forchheimer drag widens it by an order of magnitude, because the QS momentum balance has no momentum-flux term: on a brick dropping tens of kPa, that omission is no longer small. The same harness pins the extreme case — QS reports exactly 0.0 Pa of Rayleigh pressure rise where the compressible drivers report about −100 Pa.

So: the quasi-steady driver is the right tool for a drive cycle and the wrong one for a like-for-like pressure comparison. Choose it for the timescale, not for the speed alone, and if you switch drivers mid-project, re-baseline rather than assuming continuity.

Real-gas pipes are refused, not silently approximated

The QS energy march is ideal-gas-caloric throughout (cv = R/(γ−1)), while its flow solve inverts the real EOS for density. Running a real-gas pipe would mix the two inconsistently, so the driver raises NotImplementedError naming the pipe and telling you to use the explicit or implicit driver instead. That refusal is deliberate — see ADR-0004 for the surrounding design.

Where to go next

Next Page
The species columns these bricks consume Species and chemistry
Every catalyst / filter / regen field Aftertreatment thermal & catalytic blocks
A full chain as a TOML recipe Recipe: Aftertreatment chain
Which driver does what Runtime data flow