Skip to content

Python assembly: species and chemistry

Part of the pure-Python assembly ladder. A pipe carries gas dynamics whether or not it carries composition; this page adds the composition.

aeolus1d splits species into a primary column and any number of extras. That is not cosmetic — the primary is the one closures reach for by default (the filter's PM species, the cylinder's fuel), and the extras ride alongside in a single (N, n_extra) array whose column order is an invariant. See ADR-0003 for why: species mis-slotting scrambled stoichiometry twice (MRs !100 / !101), and the layout rule is what closed it.


Declaratively

from aeolus1d.io.build import build_network
from aeolus1d.io.schema import SpeciesExtraSpec, SpeciesSpec

species = SpeciesSpec(
    name="NO", Y_init=1.0e-3, reaction="none",
    extras=[SpeciesExtraSpec(name="NH3", Y_init=0.0),
            SpeciesExtraSpec(name="N2",  Y_init=0.7)],
)
pipes, junctions, far = build_network(case_of(duct(species=species)))
pipe = pipes["duct"]
primary  : 'NO'  Y[0]=0.001
extras   : ['NH3', 'N2']  shape=(24, 2)

pipe.Y is the primary column, length N. pipe.Y_extras is (N, 2) here, with pipe.species_names giving the column order — always read the names, never assume an index.

By hand, no schema

The same pipe built from components. Note the ordering constraint: the primary must exist before extras can be appended.

from aeolus1d.components.pipe import Pipe

pipe = Pipe(L=0.2, N=24, gamma=1.4, Ng=2, A=1.0e-3)
pipe.set_uniform(rho=1.0e5 / (287.05 * 800.0), u=20.0, p=1.0e5)

pipe.set_species(1.0e-3, name="NO")     # primary — must come first
pipe.add_extra_species("NH3", 0.0)      # each call appends one column
pipe.add_extra_species("N2",  0.7)
hand-built: primary='NO' extras=['NH3', 'N2'] Y_extras=(24, 2)

set_species takes a scalar (uniform) or a length-N array, so a non-uniform initial profile is just an array:

import numpy as np
pipe.set_species(np.linspace(0.0, 1.0e-3, pipe.N), name="NO")

Inert transport is a conservation check you can run

With reaction="none" nothing consumes or produces anything, so at steady state the composition must be uniform across the domain and equal to what the inlet carries — first-order upwind advection of a passive scalar is conservative. That makes it a cheap, sharp test of the species plumbing:

pipes, junctions, t = run_case(case_of(duct(species=species)))
pipe = pipes["duct"]
uniform = np.allclose(pipe.Y, pipe.Y[0], rtol=1e-9)
after run: NO uniform? True  Y_NO=0.001  sum(Y)=0.701

sum(Y) = 0.701 is 0.001 (NO) + 0.0 (NH3) + 0.7 (N2) — the mass fractions are carried, not renormalised, so what you put in is what you get out. If this ever comes back non-uniform on an inert run, the defect is in transport or in the junction coupling, not in your chemistry.

tests/integration/test_species_conservation_network.py runs exactly this argument across a two-pipe-plus-junction network, and extends it to a reacting case: with a catalyst the individual species do change, but chemistry only rearranges atoms, so the per-mass element ratios C/N and H/N must stay uniform. That test deliberately gives the downstream pipe a non-canonical column order — the shape the AVL Cruise M importer produces — because that is the layout the mis-slotting bug scrambled.

Reactions

SpeciesSpec.reaction selects the kinetics for the primary column; mechanism files and the surface-reaction seam are their own subject. Start at Chemistry file formats for the file side, and the closures reference for what each reaction kind expects.

Where to go next

Next Page
Catalysts and filters that consume these species Aftertreatment
The extras-layout invariant, in full ADR-0003
Mechanism + species-DB file formats Chemistry file formats