from importlib.resources import files
import plotly.graph_objects as go
import nefes
from nefes.chem import equivalence_ratio_mixture
from nefes.elements import catalog as cat
from nefes.plotting import COLORWAY, use_nefes_theme
from nefes.thermo import SpeciesSet, SpeciesDatabase
# Use the bundled theme instead of plotly's default.
use_nefes_theme()
# A stoichiometric hydrogen/air feed. With no library, equivalence_ratio_mixture looks the
# named species up in the packaged data, so the blend itself needs no library.
premix = equivalence_ratio_mixture({"H2": 1.0}, {"O2": 0.21, "N2": 0.79}, phi=1.0)
def h2_air_flame(gas):
"""A premixed hydrogen/air flame: one inlet, an equilibrium flame, one outlet."""
return nefes.Network(
gas,
nodes=[
cat.mass_flow_inlet(0.1, 300.0, composition=premix, name="premix"),
cat.equilibrium_flame(name="flame"),
cat.pressure_outlet(1.0e5, 300.0, name="out"),
],
edges=[(0, 1, 0.01), (1, 2, 0.01)],
).solve()Species libraries: automatic, explicit, and Cantera mechanisms
Source notebook: examples/combustion/species_sets.ipynb. This page is executed from it at build time.
A reacting network resolves its chemistry from a species set: the candidate species the equilibrium closure works over. A species set can be supplied in three ways, from the least to the most explicit, and this example builds the same hydrogen/air flame with each.
- Automatic — name no species at all; the product slate is derived from the feeds over the packaged data.
- An explicit pool — pin a chosen list of species from the packaged data.
- A Cantera mechanism — load a species set from a Cantera-format YAML file.
The physics is identical in all three; what differs is how the species set is chosen.
The automatic species set (the default)
equilibrium() with no argument leaves the species open. The network is assembled from the feed compositions alone, and the product slate is derived when the network is compiled: every gas-phase species the packaged data can form from the fed-in elements, reduced to the non-trace ones when that candidate set is large. Nothing here names a species, yet the resolved slate and the reduction that produced it stay inspectable on net.gas after the build.
auto = h2_air_flame(nefes.equilibrium())
print(f"converged: {auto.converged}, flame temperature: {auto.edge(1)['T']:.0f} K")
print(f"resolved species ({auto.network.gas.n_species}): {sorted(auto.network.gas.species_names)}")
report = auto.network.gas.species_set.reduction_report
print(f"reduction: kept {report['n_kept']} of {report['n_candidates']} candidates")converged: True, flame temperature: 2377 K
resolved species (30): ['H', 'H2', 'H2O', 'H2O2', 'HNO', 'HNO2', 'HNO3', 'HO2', 'N', 'N2', 'N2H2', 'N2H4', 'N2O', 'N2O3', 'N2O4', 'N2O5', 'N3', 'N3H', 'NH', 'NH2', 'NH2NO2', 'NH2OH', 'NH3', 'NO', 'NO2', 'NO3', 'O', 'O2', 'O3', 'OH']
reduction: kept 30 of 30 candidates
An explicit species pool
Passing a species set pins the species. SpeciesDatabase() with no argument opens the packaged database, and .select([...]) selects an explicit list from it. This is the form to use when a fixed, reproducible species set matters, since the automatic reduction can change as its policy improves. A lean pool of the major hydrogen/air species reaches the same flame temperature as the broad automatic slate, because the species it omits are present only in trace amounts.
pool = SpeciesDatabase().select(["H2", "O2", "N2", "H2O", "OH", "H", "O", "HO2", "NO"])
explicit = h2_air_flame(nefes.equilibrium(pool))
print(f"pinned species ({pool.n_species}): {pool.species_names}")
print(f"flame temperature: {explicit.edge(1)['T']:.0f} K")pinned species (9): ['H2', 'O2', 'N2', 'H2O', 'OH', 'H', 'O', 'HO2', 'NO']
flame temperature: 2377 K
A Cantera-subset mechanism
A species set can also come from a Cantera-format YAML file, read directly with no Cantera installation. The packaged hydrogen/oxygen mechanism is used here; a path to your own file works the same way. The thermodynamic data are the mechanism’s own, so the flame temperature differs slightly from the packaged-data results.
mech = str(files("nefes.thermo") / "data" / "h2o2.yaml")
cantera_lib = SpeciesSet.from_cantera(mech)
cantera = h2_air_flame(nefes.equilibrium(cantera_lib))
print(f"mechanism species ({cantera_lib.n_species}): {cantera_lib.species_names}")
print(f"flame temperature: {cantera.edge(1)['T']:.0f} K")mechanism species (10): ['H2', 'H', 'O', 'O2', 'OH', 'H2O', 'HO2', 'H2O2', 'AR', 'N2']
flame temperature: 2386 K
The three side by side
Each bar is the same flame under a different species set. The automatic and explicit results share the packaged thermodynamic data and agree exactly; the Cantera mechanism carries its own data and lands a few kelvin apart.
labels = ["automatic", "explicit pool", "Cantera h2o2"]
temps = [auto.edge(1)["T"], explicit.edge(1)["T"], cantera.edge(1)["T"]]
counts = [auto.network.gas.n_species, pool.n_species, cantera_lib.n_species]
fig = go.Figure(
go.Bar(
x=labels,
y=temps,
marker_color=COLORWAY[0],
text=[f"{n} species" for n in counts],
textposition="outside",
)
)
fig.update_layout(
title="Hydrogen/air flame temperature by species-set choice",
yaxis_title="flame temperature [K]",
yaxis_range=[0, max(temps) * 1.15],
showlegend=False,
)
fig.show()Sizing the automatic set
The automatic set is deliberately broad. Three keyword dials on equilibrium() size it without hand-listing species: reducer (with "none" keeping every candidate), reduce_threshold (the trace mole-fraction cutoff below which a species is dropped), and reduce_above (the candidate count above which the reduction runs at all). Hydrogen/air has few candidates, so the reduction is off by default; lowering the gate engages it, and the trace cutoff then trims further, all at the same flame temperature.
# Hydrogen/air has few candidates, so the reducer is off by default -- lower the gate to engage it.
default = h2_air_flame(nefes.equilibrium())
trimmed = h2_air_flame(nefes.equilibrium(reduce_above=0))
tight = h2_air_flame(nefes.equilibrium(reduce_above=0, reduce_threshold=1e-3))
for label, sol in [
("default (no reduction)", default),
("reduce_above=0", trimmed),
("+ reduce_threshold=1e-3", tight),
]:
print(f"{label:24s}: {sol.network.gas.n_species:2d} species, T = {sol.edge(1)['T']:.0f} K")default (no reduction) : 30 species, T = 2377 K
reduce_above=0 : 19 species, T = 2377 K
+ reduce_threshold=1e-3 : 10 species, T = 2377 K
Which to use
- Automatic keeps a model short and asks for no species knowledge; it is the natural default for exploratory work.
- An explicit pool pins the species for a reproducible model, or trims the slate to speed up a large sweep.
- A Cantera mechanism brings in an external species set together with its thermodynamic data.
Whichever route supplies the species set, the resolved species are inspectable after the build through net.gas.species_names.