Mixture properties: air and combustion products
This notebook introduces mixture support in pyglenn — computing thermodynamic properties of a gas mixture with a known composition, such as air or combustion products.
pyglenn does not solve chemical equilibrium here: the composition is an input (mole fractions), not a result.
Key API used:
Mixture— immutable(species_id, mole_fraction)pairs;mixture_properties(mixture, T)— returnsMixedPropertieswith both molar (per mole of mixture) and mass (per kg of mixture) values;CompositionError— raised when the mole fractions are inconsistent.
from pyglenn import CompositionError, Mixture, ThermochemicalCalculator
print('pyglenn imported successfully')
pyglenn imported successfully
Building a mixture: dry air
Air is approximately 79% N₂ and 21% O₂ by mole. We build a
Mixture from (species_id, mole_fraction) pairs and validate it.
with ThermochemicalCalculator() as calc:
n2_id = calc.get_available_species('N2', exact_match=True)[0].id
o2_id = calc.get_available_species('O2', exact_match=True)[0].id
air = Mixture(((n2_id, 0.79), (o2_id, 0.21)))
air.validate() # raises CompositionError if inconsistent
print('air mixture is valid:', air.species)
air mixture is valid: ((861, 0.79), (931, 0.21))
Computing mixture properties
mixture_properties returns a MixedProperties dataclass. The molar
properties are the mole-fraction-weighted sum of the components’
properties; the mass properties are the molar values divided by the
mean molar mass.
with ThermochemicalCalculator() as calc:
mix = calc.mixture_properties(air, 1000.0)
print(f"temperature : {mix.temperature:.1f} K")
print(f"mean molar mass : {mix.molar_mass * 1000.0:.4f} g/mol")
print()
print(f"cp_molar = {mix.cp_molar:.3f} J/(mol·K)")
print(f"cp_mass = {mix.cp_mass:.3f} J/(kg·K)")
print()
print(f"h_molar = {mix.h_molar:.1f} J/mol")
print(f"h_mass = {mix.h_mass:.1f} J/kg")
print()
print(f"s_molar = {mix.s_molar:.3f} J/(mol·K)")
print(f"s_mass = {mix.s_mass:.3f} J/(kg·K)")
temperature : 1000.0 K
mean molar mass : 28.8503 g/mol
cp_molar = 33.156 J/(mol·K)
cp_mass = 1149.225 J/(kg·K)
h_molar = 21723.7 J/mol
h_mass = 752979.5 J/kg
s_molar = 231.408 J/(mol·K)
s_mass = 8020.988 J/(kg·K)
Mass vs molar basis
The mean molar mass of a mixture is the mole-fraction-weighted arithmetic mean: M = Σ xᵢ Mᵢ. For air this is about 28.85 g/mol — heavier than N₂ (28.01) and lighter than O₂ (32.00), as expected.
with ThermochemicalCalculator() as calc:
n2 = calc.get_available_species('N2', exact_match=True)[0]
o2 = calc.get_available_species('O2', exact_match=True)[0]
m_air = 0.79 * n2.molecular_weight + 0.21 * o2.molecular_weight
mix = calc.mixture_properties(air, 1000.0)
print(f"hand-calculated M = {m_air:.4f} g/mol")
print(f"mixture M = {mix.molar_mass * 1000.0:.4f} g/mol")
print(f"cp_mass * M = {mix.cp_mass * mix.molar_mass:.3f} J/(mol·K)")
print(f"cp_molar = {mix.cp_molar:.3f} J/(mol·K)")
hand-calculated M = 28.8503 g/mol
mixture M = 28.8503 g/mol
cp_mass * M = 33.156 J/(mol·K)
cp_molar = 33.156 J/(mol·K)
Invalid compositions
Mixture.validate (and mixture_properties) raises CompositionError
when the mole fractions do not sum to 1.0 (within 1e-9), are negative,
or contain duplicate species ids.
bad = Mixture(((n2_id, 0.5), (o2_id, 0.49))) # sums to 0.99, not 1.0
try:
bad.validate()
except CompositionError as exc:
print('CompositionError:', exc)
CompositionError: mole fractions sum to 0.99, expected 1.0 (tolerance 1e-09)
Combustion products (stoichiometric methane/air)
For CH₄ + 2 O₂ → CO₂ + 2 H₂O burned in air, the nitrogen that accompanies the O₂ ends up in the products. For every mole of CH₄:
CO₂: 1 mol
H₂O: 2 mol
N₂: 2 × (0.79 / 0.21) ≈ 7.52 mol
n2_in = 2.0 * (0.79 / 0.21) # mol of N2 per mol of CH4 burned
moles = {'CO2': 1.0, 'H2O': 2.0, 'N2': n2_in}
total = sum(moles.values())
with ThermochemicalCalculator() as calc:
ids = {
name: calc.get_available_species(name, exact_match=True)[0].id
for name in moles
}
flue = Mixture(tuple((ids[name], moles[name] / total) for name in moles))
props = calc.mixture_properties(flue, 1500.0)
print("flue-gas composition (mole fractions):")
for name in moles:
print(f" {name:>4}: {moles[name] / total:.4f}")
print()
print(f"at 1500 K: cp_mass = {props.cp_mass:.2f} J/(kg·K), "
f"h_mass = {props.h_mass / 1000.0:.2f} kJ/kg")
flue-gas composition (mole fractions):
CO2: 0.0950
H2O: 0.1900
N2: 0.7149
at 1500 K: cp_mass = 1427.59 J/(kg·K), h_mass = -1478.95 kJ/kg