Skip to content

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_mech
  • src/aeolus1d/chemistry/importers/chemkin_mech.py::parse_chemkin_mech
  • src/aeolus1d/chemistry/importers/chemkin_mech.py::_reactions_header_units
  • src/aeolus1d/chemistry/importers/chemkin_mech.py::_parse_reaction_line
  • src/aeolus1d/chemistry/importers/chemkin_mech.py::_detect_third_body_and_falloff
  • src/aeolus1d/chemistry/importers/chemkin_mech.py::_apply_continuation
  • src/aeolus1d/chemistry/importers/chemkin_mech.py::_drop_note
  • src/aeolus1d/chemistry/importers/cantera_yaml.py::import_cantera_yaml
  • src/aeolus1d/chemistry/importers/cantera_yaml.py::import_cantera_yaml_as_mechanism
  • src/aeolus1d/chemistry/importers/cantera_yaml.py::_parse_species
  • src/aeolus1d/chemistry/importers/cantera_yaml.py::_parse_reaction
  • src/aeolus1d/chemistry/mech_db.py::save_mechanism
  • src/aeolus1d/chemistry/mech_db.py::load_mechanism
  • src/aeolus1d/chemistry/mech_db.py::_reaction_from_dict
  • src/aeolus1d/chemistry/importers/nasa7.py::import_thermo_file
  • src/aeolus1d/chemistry/importers/nasa7.py::parse_nasa7_block
  • src/aeolus1d/chemistry/burcat_db.py::ensure_burcat_db
  • src/aeolus1d/chemistry/importers/burcat.py::parse_burcat_file
  • src/aeolus1d/chemistry/importers/janaf.py::import_janaf_table
  • src/aeolus1d/chemistry/importers/janaf.py::fit_nasa7_to_table
  • src/aeolus1d/chemistry/db.py::save_species
  • src/aeolus1d/chemistry/db.py::save_many
  • src/aeolus1d/chemistry/db.py::load_species
  • src/aeolus1d/io/build.py::_attach_reactions
  • src/aeolus1d/io/build.py::_attach_mechanism_catalyst
  • src/aeolus1d/chemistry/kinetics.py::MechanismKinetics.__post_init__
  • src/aeolus1d/chemistry/kinetics.py::MechanismKinetics._build_jit_arrays
  • src/aeolus1d/chemistry/kinetics.py::MechanismKinetics.make_py_reaction_multi
  • src/aeolus1d/chemistry/kinetics.py::MechanismKinetics.per_cell_rates_array
  • src/aeolus1d/chemistry/kinetics.py::MechanismKinetics.per_cell_rates
  • src/aeolus1d/chemistry/kinetics.py::MechanismKinetics._effective_kf
  • src/aeolus1d/chemistry/kinetics.py::MechanismKinetics._effective_kr
  • src/aeolus1d/chemistry/kinetics_njit.py::per_cell_rates_array_njit
  • src/aeolus1d/chemistry/kinetics_njit.py::per_cell_rates_array_njit_full
  • src/aeolus1d/chemistry/mixture.py::build_mixture_table
  • src/aeolus1d/chemistry/mixture.py::MixtureThermoTable.gamma
  • src/aeolus1d/chemistry/mixture.py::MixtureThermoTable.T_from_e
  • src/aeolus1d/chemistry/mixture.py::mixture_gamma
  • src/aeolus1d/chemistry/mixture.py::build_scalar_thermo
  • src/aeolus1d/chemistry/mixture.py::ScalarMixtureThermo.T_from_e