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) — returns MixedProperties with 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