Gas-phase chemistry¶
How a mechanism file and a species thermo file become the rates and the
mixture thermodynamics a run uses: the importers, the species and reaction
objects, the compiled kinetics, the mixture tables, and where each refusal
happens. Written 2026-09-30 with the contracts of chemistry/mixture.py,
kinetics.py, db.py, mech_db.py, burcat_db.py and importers/; the
standard is Function contracts and flow charts. What the
solver does with the rates each step is in Chemistry split.
What enters. Three kinds of file. A CHEMKIN .inp or a Cantera .yaml
mechanism (reactions, plus species names); a species thermo file (CHEMKIN
NASA-7 blocks, Burcat's BURCAT.THR, or a JANAF table); and the two stores
the run reads back, the species DB (one NAME.npz per species, NASA-7) and
the mechanism DB (one NAME.json per mechanism). What leaves. A py_reaction_multi
callable per reacting pipe, and a MixtureThermoTable or
ScalarMixtureThermo per pipe or volume that needs a composition-dependent
gamma, R or temperature.
Units and bases, once. A is stored in the SOURCE units, CHEMKIN cgs
(cm-mol-s), by both importers (the Cantera importer converts from the file's
length / quantity units); Ea is in J/mol; the kinetics converts A to
SI by reaction order, with one more power for a three-body reaction and for
the LOW limit of a falloff. SpeciesThermo.M_w is g/mol (= kg/kmol); every
mass-basis helper divides by it, so a species with M_w <= 0 is refused
where it would divide (MechanismKinetics, build_mixture_table). NASA-7
coefficients are in Kelvin, the low set below T_mid and the high set at
and above it, clamped into [T_min, T_max]; the entropy reference is 1 bar,
and Kc = Kp (1e5 / RT)^dnu. Rate constants and Kc are evaluated at
max(T, 200 K). Mixture energies are sensible energies with e(0 K) = 0.
From a mechanism file to a Mechanism¶
Both importers end in the same object. Anything they cannot carry is either refused (the rate law would be silently wrong) or dropped with a warning; nothing is dropped without one.
flowchart TD
F1[".inp file"] --> IC["import_chemkin_mech: read UTF-8"]
IC -- "absent / not UTF-8" --> X0["FileNotFoundError / UnicodeDecodeError"]
IC --> P["parse_chemkin_mech: sections, comments, SITE blocks"]
P -- "a reaction repeated without DUPLICATE on both copies" --> XD["ValueError"]
P --> H{"_reactions_header_units"}
H -- "MOLECULES" --> X1["NotImplementedError"]
H -- "unknown unit token" --> W0["UserWarning + note, cal/mol kept"]
H --> RL{"_parse_reaction_line"}
RL -- "not 3 trailing numbers, no or 2 arrows, empty side" --> W1["line dropped: note + UserWarning (_drop_note)"]
RL -- "two different (+X) colliders" --> X2["ValueError"]
RL --> CT{"_apply_continuation"}
CT -- "FORD RORD UNITS COV ENRGY" --> X3["NotImplementedError"]
CT -- "non-numeric LOW TROE SRI PLOG REV" --> X4["ValueError naming the line"]
CT -- "wrong count, orphan, bad efficiency" --> W1
CT -- "HIGH CHEB" --> W2["UserWarning: main-line Arrhenius kept"]
CT --> MECH["Mechanism: species deduped, A cgs, Ea J/mol, notes"]
F2[".yaml file"] --> IY{"import_cantera_yaml"}
IY -- "not a mapping / species, reactions not lists / unknown element" --> X5["CanteraYamlImportError"]
IY -- "orders, coverage-dependencies" --> X6["NotImplementedError"]
IY -- "undeclared repeat, non-boolean duplicate:" --> X5
IY --> SP{"_parse_species"}
SP -- "not NASA7, bad ranges or coefficients, repeat name" --> W3["species skipped: CanteraYamlImportWarning"]
IY --> RX{"_parse_reaction"}
RX -- "bad rate, Troe / SRI key, efficiencies, PLOG pressure, two different falloff colliders, a term that is not a number then a species" --> W4["reaction skipped: CanteraYamlImportWarning"]
RX -- "unknown type, Chebyshev, empty side" --> W4
SP --> IM2["import_cantera_yaml_as_mechanism: gas species, surface sites"]
RX --> IM2
IM2 --> MECH
MECH --> SV["save_mechanism: NAME.json, written atomically"]
SV -- "name is a path" --> X7["ValueError"]
SV --> LM["load_mechanism"]
LM -- "not JSON / not an object" --> X8["ValueError naming the file"]
LM --> REAC["_reaction_from_dict: Ea_units to J/mol"]
REAC -- "unknown unit, wrong-length list, missing key" --> X9["ValueError / KeyError"]
The two routes into the solver differ. A [[reaction]] block names a JSON
mechanism, so _attach_reactions calls load_mechanism; a catalyst
kind = "mechanism" names a .inp or .yaml, which
_attach_mechanism_catalyst imports directly. Neither path checks at import
that every species a reaction names is in the species list; MechanismKinetics
does. The .inp and .yaml importers refuse a repeated reaction unless both
copies carry DUPLICATE / duplicate: true (since 2026-10-11); a JSON
mechanism read by load_mechanism is not checked, and both copies of a repeat
there are kept and added.
From a thermo file to the species DB¶
flowchart TD
T1["CHEMKIN thermo file"] --> IT["import_thermo_file"]
IT --> PB{"parse_nasa7_block: 4 lines, name, fields"}
PB -- "blank or bad number, fractional or unknown element, bad T range, no name" --> SK["record skipped, listed in ONE UserWarning"]
PB --> DUP{"repeat name?"}
DUP -- yes --> SK
DUP -- no --> SPT["SpeciesThermo: M_w from elements (0.0 if none)"]
IT -- "final record short of 4 lines" --> SK
T2["BURCAT.THR"] --> EN["ensure_burcat_db: cache, atomic write"]
EN -- "every mirror fails or serves HTML" --> X1["RuntimeError naming each mirror"]
EN --> PBF["parse_burcat_file: records by column 80"]
PBF -- "malformed record" --> SKS["skipped, NO warning"]
PBF --> SPT
T3["JANAF table"] --> IJ["import_janaf_table: rows of 4 floats"]
IJ -- "no data row" --> X2["ValueError"]
IJ --> FIT{"fit_nasa7_to_table"}
FIT -- "shape, under 5 rows, NaN or T not positive, T_mid outside, under 5 per range" --> X3["ValueError"]
FIT --> SPT
SPT --> SS["save_species / save_many: NAME.npz, atomic"]
SS -- "name is a path" --> X4["ValueError"]
SS --> LS["load_species"]
LS -- "absent" --> X5["FileNotFoundError naming the directory"]
LS -- "pickled entry, temperatures out of order" --> X6["ValueError"]
LS --> OUT["SpeciesThermo, M_w g/mol"]
A species with M_w = 0.0 (no elements in a NASA-7 record, no M_w in a
JANAF header) is not refused here; it is refused at the first place that
divides by it.
From Mechanism and species to a rate¶
flowchart TD
M["Mechanism + species_db"] --> K{"MechanismKinetics.__post_init__"}
K -- "no reactions, species missing from the DB, M_w not positive" --> XK1["ValueError"]
K -- "two rate forms, collider without falloff or not in the mechanism, falloff without LOW" --> XK1
K -- "surface or undeclared species in a reaction" --> XK1
K --> J["_build_jit_arrays: A to SI, alpha, LOW, PLOG in Pa, REV"]
J --> FAC{"make_py_reaction_multi(tracked names)"}
FAC -- "empty, repeated, unknown, untracked reactive species or [M] collider" --> XK2["ValueError"]
FAC --> CB["reaction(T, Y, Y_extras, rho)"]
CB --> PA["per_cell_rates_array"]
PA -- "use_jit" --> NJ["per_cell_rates_array_njit / _njit_full"]
PA -- "numpy" --> PC["per_cell_rates per cell"]
PC --> KF["_effective_kf: k_forward, [M_eff], falloff F, PLOG"]
PC --> KR["_effective_kr: kf / Kc, or REV"]
KF --> OUT["dY/dt [1/s], s_E [W/m3]"]
KR --> OUT
NJ --> OUT
per_cell_rates refuses nothing: a bad input gives a bad number. Negative
mass fractions are clipped to zero on both backends, rho is floored at
1e-12 in the divide, and the two backends agree to round-off on valid states.
Mixture thermo¶
flowchart TD
SD["species_db + tracked names"] --> BT{"build_mixture_table"}
BT -- "name missing" --> XM1["KeyError"]
BT -- "M_w not positive" --> XM2["ValueError naming the species"]
BT --> TAB["MixtureThermoTable: R_mix, cp_mix, cv_mix, gamma, e_sens, T_from_e"]
TAB --> G{"gamma: cv above 0?"}
G -- "no (zero, negative, NaN composition)" --> GF["gamma = 1.4 + one RuntimeWarning per table"]
G -- yes --> GV["gamma = cp / cv"]
TAB --> TE{"T_from_e: Newton on e_sens"}
TE -- "max_iter below 1" --> XM3["ValueError"]
TE -- "no convergence" --> XM4["RuntimeError, names the likely cause"]
TE --> TV["T [K]"]
TAB --> BS{"build_scalar_thermo"}
BS -- "Y size or dissociation table wrong" --> XM5["ValueError"]
BS --> SC["ScalarMixtureThermo: cp, e_sens, e_cal, T_from_e"]
SC -- "no convergence" --> XM6["RuntimeError"]
The two gamma seams differ on purpose (ADR-0014): the scalar
mixture_gamma raises on a degenerate composition, the table returns 1.4 so
one transient cell does not kill a run.
Functions¶
src/aeolus1d/chemistry/importers/chemkin_mech.py::import_chemkin_mechsrc/aeolus1d/chemistry/importers/chemkin_mech.py::parse_chemkin_mechsrc/aeolus1d/chemistry/importers/chemkin_mech.py::_reactions_header_unitssrc/aeolus1d/chemistry/importers/chemkin_mech.py::_parse_reaction_linesrc/aeolus1d/chemistry/importers/chemkin_mech.py::_detect_third_body_and_falloffsrc/aeolus1d/chemistry/importers/chemkin_mech.py::_apply_continuationsrc/aeolus1d/chemistry/importers/chemkin_mech.py::_drop_notesrc/aeolus1d/chemistry/importers/cantera_yaml.py::import_cantera_yamlsrc/aeolus1d/chemistry/importers/cantera_yaml.py::import_cantera_yaml_as_mechanismsrc/aeolus1d/chemistry/importers/cantera_yaml.py::_parse_speciessrc/aeolus1d/chemistry/importers/cantera_yaml.py::_parse_reactionsrc/aeolus1d/chemistry/mech_db.py::save_mechanismsrc/aeolus1d/chemistry/mech_db.py::load_mechanismsrc/aeolus1d/chemistry/mech_db.py::_reaction_from_dictsrc/aeolus1d/chemistry/importers/nasa7.py::import_thermo_filesrc/aeolus1d/chemistry/importers/nasa7.py::parse_nasa7_blocksrc/aeolus1d/chemistry/burcat_db.py::ensure_burcat_dbsrc/aeolus1d/chemistry/importers/burcat.py::parse_burcat_filesrc/aeolus1d/chemistry/importers/janaf.py::import_janaf_tablesrc/aeolus1d/chemistry/importers/janaf.py::fit_nasa7_to_tablesrc/aeolus1d/chemistry/db.py::save_speciessrc/aeolus1d/chemistry/db.py::save_manysrc/aeolus1d/chemistry/db.py::load_speciessrc/aeolus1d/io/build.py::_attach_reactionssrc/aeolus1d/io/build.py::_attach_mechanism_catalystsrc/aeolus1d/chemistry/kinetics.py::MechanismKinetics.__post_init__src/aeolus1d/chemistry/kinetics.py::MechanismKinetics._build_jit_arrayssrc/aeolus1d/chemistry/kinetics.py::MechanismKinetics.make_py_reaction_multisrc/aeolus1d/chemistry/kinetics.py::MechanismKinetics.per_cell_rates_arraysrc/aeolus1d/chemistry/kinetics.py::MechanismKinetics.per_cell_ratessrc/aeolus1d/chemistry/kinetics.py::MechanismKinetics._effective_kfsrc/aeolus1d/chemistry/kinetics.py::MechanismKinetics._effective_krsrc/aeolus1d/chemistry/kinetics_njit.py::per_cell_rates_array_njitsrc/aeolus1d/chemistry/kinetics_njit.py::per_cell_rates_array_njit_fullsrc/aeolus1d/chemistry/mixture.py::build_mixture_tablesrc/aeolus1d/chemistry/mixture.py::MixtureThermoTable.gammasrc/aeolus1d/chemistry/mixture.py::MixtureThermoTable.T_from_esrc/aeolus1d/chemistry/mixture.py::mixture_gammasrc/aeolus1d/chemistry/mixture.py::build_scalar_thermosrc/aeolus1d/chemistry/mixture.py::ScalarMixtureThermo.T_from_e