This notebook is part of the pyglenn Labbook collection. ⬇ Download .ipynb

06 - Adiabatic Flame Temperature

When a fuel burns in an insulated, constant-pressure chamber, all of the chemical energy released goes into heating the products. The resulting temperature is the adiabatic flame temperature \(T_\mathrm{ad}\) — a key figure for combustor and turbine design.

Because pyglenn’s standardized enthalpy already contains each species’ formation enthalpy, the adiabatic, isobaric energy balance is simply conservation of total enthalpy:

\[\sum_{\text{reactants}} n_i\,H^\circ_i(T_\mathrm{in}) \;=\; \sum_{\text{products}} n_j\,H^\circ_j(T_\mathrm{ad}).\]

We solve this nonlinear equation for \(T_\mathrm{ad}\). Air is modelled as \(1\ \mathrm{O_2} + 3.76\ \mathrm{N_2}\).

Note on the ideal-gas standard state: The NASA polynomial properties represent the ideal-gas standard state (1 bar). At high pressures or for condensed-phase mixtures, non-ideality corrections (e.g. fugacity coefficients, equations of state) should be applied — the raw h_relative values from pyglenn are the ideal-gas limit.

[1]:
from pyglenn import ThermochemicalCalculator, R

print("Universal gas constant R =", R, "J/(mol.K)")

Universal gas constant R = 8.314462618 J/(mol.K)
[2]:
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd

plt.rcParams["figure.figsize"] = (8, 4.5)
plt.rcParams["axes.grid"] = True
pd.set_option("display.float_format", lambda v: f"{v:,.3f}")

from scipy.optimize import brentq

1. Mixture enthalpy and the energy balance

mixture_enthalpy sums \(n_i H^\circ_i(T)\) over a {species: moles} dict. adiabatic_flame_T finds the product temperature at which the product enthalpy equals the reactant enthalpy, using a robust bracketed root find (brentq). Product enthalpy increases monotonically with \(T\), so the root is unique.

[3]:
def mixture_enthalpy(calc, counts, T):
    """Total standardized enthalpy of a {species: moles} mixture, J."""
    return sum(n * calc.calculate_properties(calc.get_available_species(name, exact_match=True)[0]["id"], T)["h_relative"]
               for name, n in counts.items())

def adiabatic_flame_T(calc, reactants, products, T_in=298.15, T_hi=6000.0):
    """Constant-pressure adiabatic flame temperature, K."""
    H_react = mixture_enthalpy(calc, reactants, T_in)
    f = lambda T: mixture_enthalpy(calc, products, T) - H_react
    return brentq(f, T_in, T_hi)

2. Stoichiometric methane/air

\[\mathrm{CH_4} + 2(\mathrm{O_2} + 3.76\,\mathrm{N_2}) \rightarrow \mathrm{CO_2} + 2\,\mathrm{H_2O} + 7.52\,\mathrm{N_2}.\]

The nitrogen is inert but carries away a large share of the released energy.

[4]:
reactants = {"CH4": 1.0, "O2": 2.0, "N2": 2 * 3.76}
products  = {"CO2": 1.0, "H2O": 2.0, "N2": 2 * 3.76}

with ThermochemicalCalculator() as calc:
    Tad = adiabatic_flame_T(calc, reactants, products, T_in=298.15)
print(f"Adiabatic flame temperature (CH4/air, stoichiometric): {Tad:.1f} K "
      f"= {Tad-273.15:.0f} C")
print("Ballpark without dissociation: ~2320 K.")
Adiabatic flame temperature (CH4/air, stoichiometric): 2325.7 K = 2053 C
Ballpark without dissociation: ~2320 K.

3. Visualising the balance

Plotting the product enthalpy against temperature and drawing the (constant) reactant enthalpy as a horizontal line, the intersection is \(T_\mathrm{ad}\).

[5]:
Tgrid = np.linspace(298.15, 3200, 80)
with ThermochemicalCalculator() as calc:
    H_prod = np.array([mixture_enthalpy(calc, products, T) for T in Tgrid]) / 1000.0
    H_react = mixture_enthalpy(calc, reactants, 298.15) / 1000.0

fig, ax = plt.subplots()
ax.plot(Tgrid, H_prod, label="products $H(T)$")
ax.axhline(H_react, color="C1", ls="--", label=r"reactants $H(T_\mathrm{in})$")
ax.axvline(Tad, color="0.5", ls=":")
ax.scatter([Tad], [H_react], color="red", zorder=5, label=rf"$T_\mathrm{{ad}}$ = {Tad:.0f} K")
ax.set_xlabel("Temperature [K]")
ax.set_ylabel("Mixture enthalpy [kJ]")
ax.set_title("Adiabatic flame temperature as an enthalpy balance")
ax.legend()
plt.show()
_images/06_adiabatic_flame_temperature_8_0.png

4. Effect of the equivalence ratio

The equivalence ratio \(\phi\) is the actual fuel/air ratio divided by the stoichiometric one. Lean mixtures (\(\phi<1\)) contain excess air that absorbs heat without releasing more, lowering \(T_\mathrm{ad}\). Assuming complete combustion, for \(\phi \le 1\) the products are CO₂, H₂O, the leftover O₂ and all the N₂.

[6]:
def flame_T_phi(calc, phi, T_in=298.15):
    """CH4/air adiabatic flame temperature at equivalence ratio phi (<=1)."""
    # Fix 1 mol fuel; stoichiometric O2 = 2. Lean => more air => O2_supplied = 2/phi.
    o2 = 2.0 / phi
    n2 = o2 * 3.76
    reac = {"CH4": 1.0, "O2": o2, "N2": n2}
    prod = {"CO2": 1.0, "H2O": 2.0, "O2": o2 - 2.0, "N2": n2}
    prod = {k: v for k, v in prod.items() if v > 0}
    return adiabatic_flame_T(calc, reac, prod, T_in)

phis = np.linspace(0.5, 1.0, 26)
with ThermochemicalCalculator() as calc:
    Tad_phi = [flame_T_phi(calc, p) for p in phis]

fig, ax = plt.subplots()
ax.plot(phis, Tad_phi)
ax.set_xlabel(r"equivalence ratio $\phi$")
ax.set_ylabel(r"$T_\mathrm{ad}$ [K]")
ax.set_title("Leaner methane/air mixtures burn cooler")
plt.show()
print(f"stoichiometric (phi=1.0): {Tad_phi[-1]:.0f} K")
print(f"lean         (phi=0.5): {Tad_phi[0]:.0f} K")
_images/06_adiabatic_flame_temperature_10_0.png
stoichiometric (phi=1.0): 2326 K
lean         (phi=0.5): 1481 K

5. Effect of reactant preheat

Preheating the incoming air (heat recuperation, common in gas turbines) raises the reactant enthalpy and therefore \(T_\mathrm{ad}\). Here we sweep the inlet temperature for the stoichiometric mixture.

[7]:
T_in_grid = np.linspace(298.15, 800, 25)
with ThermochemicalCalculator() as calc:
    Tad_preheat = [adiabatic_flame_T(calc, reactants, products, T_in=Ti)
                   for Ti in T_in_grid]

fig, ax = plt.subplots()
ax.plot(T_in_grid, Tad_preheat)
ax.set_xlabel(r"inlet temperature $T_\mathrm{in}$ [K]")
ax.set_ylabel(r"$T_\mathrm{ad}$ [K]")
ax.set_title("Preheating the reactants raises the flame temperature")
plt.show()
_images/06_adiabatic_flame_temperature_12_0.png

6. Comparing fuels

Stoichiometric flame temperatures in air (inlet 298.15 K) for four fuels. All use their own oxygen demand; hydrogen and the hydrocarbons cluster together once diluted by atmospheric nitrogen.

[8]:
FUEL_STOICH = {
    "H2":     ({"O2": 0.5}, {"CO2": 0, "H2O": 1}),
    "CH4":    ({"O2": 2.0}, {"CO2": 1, "H2O": 2}),
    "C3H8":   ({"O2": 5.0}, {"CO2": 3, "H2O": 4}),
    "C2H5OH": ({"O2": 3.0}, {"CO2": 2, "H2O": 3}),
}

rows = []
with ThermochemicalCalculator() as calc:
    for fuel, (o2d, prod) in FUEL_STOICH.items():
        nO2 = o2d["O2"]
        nN2 = nO2 * 3.76
        reac = {fuel: 1.0, "O2": nO2, "N2": nN2}
        products_f = {k: v for k, v in {**prod, "N2": nN2}.items() if v > 0}
        Tad_f = adiabatic_flame_T(calc, reac, products_f, T_in=298.15)
        rows.append({"fuel": fuel, "T_ad [K]": Tad_f})

fuel_df = pd.DataFrame(rows).set_index("fuel")
print(fuel_df.to_string())

fig, ax = plt.subplots()
ax.bar(fuel_df.index, fuel_df["T_ad [K]"], color="#c0392b")
ax.set_ylabel(r"$T_\mathrm{ad}$ [K]")
ax.set_ylim(2000, 2500)
ax.set_title("Stoichiometric flame temperature in air")
plt.show()
        T_ad [K]
fuel
H2     2,519.601
CH4    2,325.684
C3H8   2,391.472
C2H5OH 2,351.714
_images/06_adiabatic_flame_temperature_14_1.png

Caveat: dissociation

This model assumes complete combustion to CO₂ and H₂O. Real flames above ~2000 K partially dissociate the products (into CO, OH, H, O, NO, …), which absorbs energy and caps the true flame temperature 100–300 K below these estimates. Notebook 08 quantifies dissociation through chemical equilibrium.

Next: notebook 07 applies these combustion tools to compare biofuels.