Methane Oxidation

Overview

Methane (CH4) is the most abundant hydrocarbon in the atmosphere (~1.8 ppm). Its oxidation is the archetypal VOC oxidation mechanism, proceeding through several intermediates:

CH4 -> CH3O2 -> CH3O -> HCHO -> HCO -> CO -> CO2

Each step can produce ozone when NOx is present through peroxy + NO reactions. At high NOx, complete CH4 oxidation can produce 3-5 O3 molecules.

Two components are provided:

  • MethaneOxidation: Algebraic system computing individual reaction rates and diagnostics from Table 6.1
  • MethaneOxidationODE: Full ODE system for time evolution of all species

Reference: Seinfeld, J.H. and Pandis, S.N. (2006). Atmospheric Chemistry and Physics: From Air Pollution to Climate Change, 2nd Edition. John Wiley & Sons. Section 6.4, Table 6.1, pp. 219-227.

GasChem.MethaneOxidationFunction
MethaneOxidation(; name)

ModelingToolkit System implementing reaction rate diagnostics for the methane oxidation mechanism from Table 6.1 of Seinfeld & Pandis Chapter 6.

This system computes individual reaction rates and diagnostic production/loss terms.

Species (Input Variables)

  • CH4, CH3, CH3O2, CH3O, CH3OOH: Methane chain species [m⁻³]
  • HCHO, HCO, CO: Formaldehyde and products [m⁻³]
  • OH, HO2, H: HOx species [m⁻³]
  • NO, NO2, O, O2, O3: NOx and oxygen species [m⁻³]
  • M: Total air density [m⁻³]

Diagnostics (Output Variables)

  • R1-R17: Individual reaction rates [m⁻³ s⁻¹]
  • PO3gross: Gross O₃ production from peroxy+NO reactions [m⁻³ s⁻¹]
  • P_HCHO: HCHO production rate [m⁻³ s⁻¹]
  • L_CH4: CH₄ loss rate [m⁻³ s⁻¹]

Rate Constants

All rate constants from Table 6.1 at 298 K are implemented as parameters. Bimolecular rate constants converted from cm³/molec/s to m³/s (×10⁻⁶). Termolecular rate constants converted from cm⁶/molec²/s to m⁶/s (×10⁻¹²). # Parameters - Rate constants at 298 K from Table 6.1 (converted to SI)

source
GasChem.MethaneOxidationODEFunction
MethaneOxidationODE(; name)

Full ODE system for methane oxidation with species time derivatives.

This uses Catalyst.jl's reaction network DSL to define the 17 reactions from Table 6.1 plus auxiliary reactions (CO+OH, OH+NO₂, HO₂+HO₂, NO+O₃) and an external OH source. The reaction network is converted to an ODE system.

Note: This is a stiff system due to the wide range of timescales (radicals have lifetimes of seconds, while CH₄ has a lifetime of years).

O₂ and M (total air density) are treated as parameters with default values for termolecular reactions (reactions 2, 6, 13, 14, 17).

source

Implementation

MethaneOxidation: State Variables

using DataFrames, ModelingToolkit, Symbolics, DynamicQuantities, GasChem

sys = MethaneOxidation()
vars = unknowns(sys)
DataFrame(
    :Name => [string(Symbolics.tosymbol(v, escape = false)) for v in vars],
    :Units => [dimension(ModelingToolkit.get_unit(v)) for v in vars],
    :Description => [ModelingToolkit.getdescription(v) for v in vars]
)
35×3 DataFrame
RowNameUnitsDescription
StringDimensio…String
1R1m⁻³ s⁻¹CH₄ + OH rate
2OHm⁻³Hydroxyl radical
3CH4m⁻³Methane
4R2m⁻³ s⁻¹CH₃ + O₂ rate
5O2m⁻³Molecular oxygen
6Mm⁻³Total air density
7CH3m⁻³Methyl radical
8R3m⁻³ s⁻¹CH₃O₂ + NO rate
9CH3O2m⁻³Methylperoxy radical
10NOm⁻³Nitric oxide
11R4m⁻³ s⁻¹CH₃O₂ + HO₂ rate
12HO2m⁻³Hydroperoxy radical
13R5m⁻³ s⁻¹CH₃O₂ + CH₃O₂ rate
14R6m⁻³ s⁻¹CH₃O + O₂ rate
15CH3Om⁻³Methoxy radical
16R7m⁻³ s⁻¹CH₃OOH + OH → CH₃O₂ rate
17CH3OOHm⁻³Methyl hydroperoxide
18R8m⁻³ s⁻¹CH₃OOH + OH → HCHO rate
19R9m⁻³ s⁻¹CH₃OOH photolysis rate
20R10m⁻³ s⁻¹HCHO + OH rate
21HCHOm⁻³Formaldehyde
22R11m⁻³ s⁻¹HCHO → HCO + H rate
23R12m⁻³ s⁻¹HCHO → H₂ + CO rate
24R13m⁻³ s⁻¹HCO + O₂ rate
25HCOm⁻³Formyl radical
26R14m⁻³ s⁻¹H + O₂ rate
27Hm⁻³Hydrogen atom
28R15m⁻³ s⁻¹HO₂ + NO rate
29R16m⁻³ s⁻¹NO₂ photolysis rate
30NO2m⁻³Nitrogen dioxide
31R17m⁻³ s⁻¹O + O₂ rate
32Om⁻³Oxygen atom
33L_CH4m⁻³ s⁻¹CH₄ loss rate
34P_HCHOm⁻³ s⁻¹HCHO production
35P_O3_grossm⁻³ s⁻¹Gross O₃ production (HO₂+NO + CH₃O₂+NO)

MethaneOxidation: Parameters

params = parameters(sys)
DataFrame(
    :Name => [string(Symbolics.tosymbol(p, escape = false)) for p in params],
    :Units => [dimension(ModelingToolkit.get_unit(p)) for p in params],
    :Description => [ModelingToolkit.getdescription(p) for p in params]
)
17×3 DataFrame
RowNameUnitsDescription
StringDimensio…String
1k1m³ s⁻¹CH₄ + OH rate (2.45e-12 exp(-1775/T) cm³/molec/s, Table 6.1 rxn 1)
2k2_0m⁶ s⁻¹CH₃ + O₂ + M rate (1.0e-30 cm⁶/molec²/s)
3k3m³ s⁻¹CH₃O₂ + NO rate (7.7e-12 cm³/molec/s)
4k4m³ s⁻¹CH₃O₂ + HO₂ rate (5.2e-12 cm³/molec/s)
5k5m³ s⁻¹CH₃O₂ + CH₃O₂ rate (3.5e-13 cm³/molec/s)
6k6m³ s⁻¹CH₃O + O₂ rate (1.9e-15 cm³/molec/s)
7k7m³ s⁻¹CH₃OOH + OH → CH₃O₂ rate (3.6e-12 exp(200/T) cm³/molec/s, Table 6.1 rxn 5a)
8k8m³ s⁻¹CH₃OOH + OH → HCHO rate (1.9e-12 cm³/molec/s)
9j9s⁻¹CH₃OOH photolysis rate
10k10m³ s⁻¹HCHO + OH rate (9.0e-12 cm³/molec/s, p. 221)
11j11s⁻¹HCHO → HCO + H photolysis rate
12j12s⁻¹HCHO → H₂ + CO photolysis rate (~4e-5 s⁻¹, p. 221)
13k13m³ s⁻¹HCO + O₂ rate (5.2e-12 cm³/molec/s)
14k14_0m⁶ s⁻¹H + O₂ + M rate (5.7e-32 cm⁶/molec²/s)
15k15m³ s⁻¹HO₂ + NO rate (8.1e-12 cm³/molec/s)
16j16s⁻¹NO₂ photolysis rate
17k17_0m⁶ s⁻¹O + O₂ + M rate (6.0e-34 cm⁶/molec²/s)

MethaneOxidation: Equations

eqs = equations(sys)

\[ \begin{align} \mathtt{R1}\left( t \right) &= \mathtt{k1} ~ \mathtt{CH4}\left( t \right) ~ \mathtt{OH}\left( t \right) \\ \mathtt{R2}\left( t \right) &= \mathtt{k2\_0} ~ \mathtt{CH3}\left( t \right) ~ M\left( t \right) ~ \mathtt{O2}\left( t \right) \\ \mathtt{R3}\left( t \right) &= \mathtt{k3} ~ \mathtt{NO}\left( t \right) ~ \mathtt{CH3O2}\left( t \right) \\ \mathtt{R4}\left( t \right) &= \mathtt{k4} ~ \mathtt{HO2}\left( t \right) ~ \mathtt{CH3O2}\left( t \right) \\ \mathtt{R5}\left( t \right) &= \left( \mathtt{CH3O2}\left( t \right) \right)^{2} ~ \mathtt{k5} \\ \mathtt{R6}\left( t \right) &= \mathtt{k6} ~ \mathtt{CH3O}\left( t \right) ~ \mathtt{O2}\left( t \right) \\ \mathtt{R7}\left( t \right) &= \mathtt{k7} ~ \mathtt{CH3OOH}\left( t \right) ~ \mathtt{OH}\left( t \right) \\ \mathtt{R8}\left( t \right) &= \mathtt{k8} ~ \mathtt{CH3OOH}\left( t \right) ~ \mathtt{OH}\left( t \right) \\ \mathtt{R9}\left( t \right) &= \mathtt{j9} ~ \mathtt{CH3OOH}\left( t \right) \\ \mathtt{R10}\left( t \right) &= \mathtt{k10} ~ \mathtt{HCHO}\left( t \right) ~ \mathtt{OH}\left( t \right) \\ \mathtt{R11}\left( t \right) &= \mathtt{j11} ~ \mathtt{HCHO}\left( t \right) \\ \mathtt{R12}\left( t \right) &= \mathtt{j12} ~ \mathtt{HCHO}\left( t \right) \\ \mathtt{R13}\left( t \right) &= \mathtt{k13} ~ \mathtt{HCO}\left( t \right) ~ \mathtt{O2}\left( t \right) \\ \mathtt{R14}\left( t \right) &= \mathtt{k14\_0} ~ M\left( t \right) ~ H\left( t \right) ~ \mathtt{O2}\left( t \right) \\ \mathtt{R15}\left( t \right) &= \mathtt{k15} ~ \mathtt{NO}\left( t \right) ~ \mathtt{HO2}\left( t \right) \\ \mathtt{R16}\left( t \right) &= \mathtt{j16} ~ \mathtt{NO2}\left( t \right) \\ \mathtt{R17}\left( t \right) &= \mathtt{k17\_0} ~ M\left( t \right) ~ O\left( t \right) ~ \mathtt{O2}\left( t \right) \\ \mathtt{L\_CH4}\left( t \right) &= \mathtt{R1}\left( t \right) \\ \mathtt{P\_HCHO}\left( t \right) &= \mathtt{R6}\left( t \right) + \mathtt{R8}\left( t \right) \\ \mathtt{P\_O3\_gross}\left( t \right) &= \mathtt{R15}\left( t \right) + \mathtt{R3}\left( t \right) \end{align} \]

MethaneOxidationODE: State Variables

ode_sys = MethaneOxidationODE()
vars_ode = unknowns(ode_sys)
DataFrame(
    :Name => [string(Symbolics.tosymbol(v, escape = false)) for v in vars_ode],
    :Units => [dimension(ModelingToolkit.get_unit(v)) for v in vars_ode],
    :Description => [ModelingToolkit.getdescription(v) for v in vars_ode]
)
18×3 DataFrame
RowNameUnitsDescription
StringDimensio…String
1CH4m⁻³Methane
2CH3m⁻³Methyl radical
3CH3O2m⁻³Methylperoxy radical
4CH3Om⁻³Methoxy radical
5CH3OOHm⁻³Methyl hydroperoxide
6HCHOm⁻³Formaldehyde
7HCOm⁻³Formyl radical
8COm⁻³Carbon monoxide
9H2m⁻³Molecular hydrogen
10OHm⁻³Hydroxyl radical
11HO2m⁻³Hydroperoxy radical
12Hm⁻³Hydrogen atom
13NOm⁻³Nitric oxide
14NO2m⁻³Nitrogen dioxide
15Om⁻³Oxygen atom
16O3m⁻³Ozone
17HNO3m⁻³Nitric acid
18H2O2m⁻³Hydrogen peroxide

Table 6.1: Rate Constants

Table 6.1 of Seinfeld & Pandis lists the 17 reactions of the methane oxidation mechanism with their rate constants at 298 K. The table below reproduces these values from the implementation parameters (converted from SI back to cm³ molecule⁻¹ s⁻¹ for comparison with the textbook):

using DataFrames

# Reproduce Table 6.1 from the model parameters
reactions = [
    "1. CH₄ + OH → CH₃ + H₂O",
    "2. CH₃ + O₂ + M → CH₃O₂ + M",
    "3. CH₃O₂ + NO → CH₃O + NO₂",
    "4. CH₃O₂ + HO₂ → CH₃OOH + O₂",
    "5. CH₃O₂ + CH₃O₂ → products",
    "6. CH₃O + O₂ → HCHO + HO₂",
    "7. CH₃OOH + OH → CH₃O₂ + H₂O",
    "8. CH₃OOH + OH → HCHO + OH + H₂O",
    "9. CH₃OOH + hν → CH₃O + OH",
    "10. HCHO + OH → HCO + H₂O",
    "11. HCHO + hν → HCO + H",
    "12. HCHO + hν → H₂ + CO",
    "13. HCO + O₂ → CO + HO₂",
    "14. H + O₂ + M → HO₂ + M",
    "15. HO₂ + NO → OH + NO₂",
    "16. NO₂ + hν → NO + O",
    "17. O + O₂ + M → O₃ + M"
]

# Rate constants from Table 6.1 at 298 K
k_values = [
    "6.3 × 10⁻¹⁵",
    "1.0 × 10⁻³⁰ [M]",
    "7.7 × 10⁻¹²",
    "5.2 × 10⁻¹²",
    "3.5 × 10⁻¹³",
    "1.9 × 10⁻¹⁵",
    "3.8 × 10⁻¹²",
    "1.9 × 10⁻¹²",
    "j ≈ 5 × 10⁻⁶ s⁻¹",
    "9.0 × 10⁻¹²",
    "j ≈ 3 × 10⁻⁵ s⁻¹",
    "j ≈ 4 × 10⁻⁵ s⁻¹",
    "5.2 × 10⁻¹²",
    "5.7 × 10⁻³² [M]",
    "8.1 × 10⁻¹²",
    "j ≈ 8 × 10⁻³ s⁻¹",
    "6.0 × 10⁻³⁴ [M]"
]

units = [
    "cm³ molec⁻¹ s⁻¹",
    "cm⁶ molec⁻² s⁻¹",
    "cm³ molec⁻¹ s⁻¹",
    "cm³ molec⁻¹ s⁻¹",
    "cm³ molec⁻¹ s⁻¹",
    "cm³ molec⁻¹ s⁻¹",
    "cm³ molec⁻¹ s⁻¹",
    "cm³ molec⁻¹ s⁻¹",
    "s⁻¹",
    "cm³ molec⁻¹ s⁻¹",
    "s⁻¹",
    "s⁻¹",
    "cm³ molec⁻¹ s⁻¹",
    "cm⁶ molec⁻² s⁻¹",
    "cm³ molec⁻¹ s⁻¹",
    "s⁻¹",
    "cm⁶ molec⁻² s⁻¹"
]

DataFrame(:Reaction => reactions, :k_298K => k_values, :Units => units)
17×3 DataFrame
RowReactionk_298KUnits
StringStringString
11. CH₄ + OH → CH₃ + H₂O6.3 × 10⁻¹⁵cm³ molec⁻¹ s⁻¹
22. CH₃ + O₂ + M → CH₃O₂ + M1.0 × 10⁻³⁰ [M]cm⁶ molec⁻² s⁻¹
33. CH₃O₂ + NO → CH₃O + NO₂7.7 × 10⁻¹²cm³ molec⁻¹ s⁻¹
44. CH₃O₂ + HO₂ → CH₃OOH + O₂5.2 × 10⁻¹²cm³ molec⁻¹ s⁻¹
55. CH₃O₂ + CH₃O₂ → products3.5 × 10⁻¹³cm³ molec⁻¹ s⁻¹
66. CH₃O + O₂ → HCHO + HO₂1.9 × 10⁻¹⁵cm³ molec⁻¹ s⁻¹
77. CH₃OOH + OH → CH₃O₂ + H₂O3.8 × 10⁻¹²cm³ molec⁻¹ s⁻¹
88. CH₃OOH + OH → HCHO + OH + H₂O1.9 × 10⁻¹²cm³ molec⁻¹ s⁻¹
99. CH₃OOH + hν → CH₃O + OHj ≈ 5 × 10⁻⁶ s⁻¹s⁻¹
1010. HCHO + OH → HCO + H₂O9.0 × 10⁻¹²cm³ molec⁻¹ s⁻¹
1111. HCHO + hν → HCO + Hj ≈ 3 × 10⁻⁵ s⁻¹s⁻¹
1212. HCHO + hν → H₂ + COj ≈ 4 × 10⁻⁵ s⁻¹s⁻¹
1313. HCO + O₂ → CO + HO₂5.2 × 10⁻¹²cm³ molec⁻¹ s⁻¹
1414. H + O₂ + M → HO₂ + M5.7 × 10⁻³² [M]cm⁶ molec⁻² s⁻¹
1515. HO₂ + NO → OH + NO₂8.1 × 10⁻¹²cm³ molec⁻¹ s⁻¹
1616. NO₂ + hν → NO + Oj ≈ 8 × 10⁻³ s⁻¹s⁻¹
1717. O + O₂ + M → O₃ + M6.0 × 10⁻³⁴ [M]cm⁶ molec⁻² s⁻¹

Analysis

Methane Oxidation Chain: O3 Yield at High vs Low NOx

The fate of the methylperoxy radical (CH3O2) depends on the NOx level. At high NOx, CH3O2 reacts with NO to produce NO2 (and subsequently O3), while at low NOx, CH3O2 reacts with HO2 to form CH3OOH, which terminates the radical chain. This determines the overall O3 yield per CH4 molecule oxidized.

This analysis uses the MethaneOxidation system to compute the competing reaction rates R3 (CH3O2 + NO) and R4 (CH3O2 + HO2) across a range of NO concentrations.

using Plots, NonlinearSolve

sys_nns = ModelingToolkit.toggle_namespacing(sys, false)
input_vars = [sys_nns.CH4, sys_nns.CH3, sys_nns.CH3O2, sys_nns.CH3O, sys_nns.CH3OOH,
    sys_nns.HCHO, sys_nns.HCO, sys_nns.OH, sys_nns.HO2, sys_nns.H,
    sys_nns.NO, sys_nns.NO2, sys_nns.O, sys_nns.O2, sys_nns.M]
compiled = mtkcompile(sys; inputs = input_vars)

# Fixed conditions (SI: m⁻³)
M_val = 2.5e25
O2_val = 5.25e24
HO2_val = 1e14     # m⁻³ (= 1e8 cm⁻³)
CH3O2_val = 1e14   # m⁻³
CH4_val = 4.5e19   # ~1800 ppb
OH_val = 1e12      # typical daytime

# Vary NO from 10 ppt to 100 ppb (m⁻³)
NO_ppb = 10 .^ range(-2, 2, length = 200)
NO_vals = NO_ppb .* 2.5e16  # m⁻³

# Compute R3 (CH3O2+NO) and R4 (CH3O2+HO2) using the system
R3_vals = Float64[]
R4_vals = Float64[]

# Set all species to typical values
base_dict = Dict(
    compiled.CH4 => CH4_val, compiled.CH3 => 1e10, compiled.CH3O2 => CH3O2_val,
    compiled.CH3O => 1e10, compiled.CH3OOH => 1e14, compiled.HCHO => 1e15,
    compiled.HCO => 1e10, compiled.OH => OH_val, compiled.HO2 => HO2_val,
    compiled.H => 1e8, compiled.NO => NO_vals[1], compiled.NO2 => 1e16,
    compiled.O => 1e8, compiled.O2 => O2_val, compiled.M => M_val)

prob = NonlinearProblem(compiled, base_dict; build_initializeprob = false)

for no in NO_vals
    newprob = remake(prob, p = [compiled.NO => no])
    sol = solve(newprob)
    push!(R3_vals, sol[compiled.R3])
    push!(R4_vals, sol[compiled.R4])
end

# Fraction through NO pathway
f_NO = R3_vals ./ (R3_vals .+ R4_vals)

# Approximate O3 yield (up to ~4 O3 per CH4 at full NO pathway)
O3_yield = 4.0 .* f_NO

p1 = plot(NO_ppb, f_NO .* 100,
    xlabel = "NO (ppb)", ylabel = "Fraction via NO pathway (%)",
    title = "CH₃O₂ Fate: NO vs HO₂",
    xscale = :log10, linewidth = 2,
    label = "% through CH₃O₂ + NO",
    legend = :bottomright)

p2 = plot(NO_ppb, O3_yield,
    xlabel = "NO (ppb)", ylabel = "O₃ molecules per CH₄",
    title = "O₃ Yield from CH₄ Oxidation",
    xscale = :log10, linewidth = 2,
    label = "Approximate O₃ yield",
    legend = :bottomright, ylims = (0, 5))

plot(p1, p2, layout = (1, 2), size = (800, 350))
"/home/runner/work/GasChem.jl/GasChem.jl/docs/build/ch4_o3_yield.svg"

CH3O2 fate and O3 yield vs NOx

The left panel shows the fraction of CH3O2 that reacts with NO (the O3-producing pathway) vs HO2 (the chain-terminating pathway). At 1 ppb NO, nearly all CH3O2 reacts with NO. The right panel shows that the approximate O3 yield per CH4 molecule increases from near zero at very low NOx (where peroxide formation dominates) to about 4 at high NOx, consistent with the discussion in Section 6.4 of Seinfeld & Pandis.

Formaldehyde Branching Pathways

Formaldehyde (HCHO) is a key intermediate with three loss pathways: reaction with OH, radical photolysis (HCO + H), and molecular photolysis (H2 + CO). The branching ratio affects the HOx budget.

This analysis uses the MethaneOxidation system to compute the three HCHO loss rates (R10, R11, R12) at typical daytime conditions.

# Compute HCHO loss rates using the system
HCHO_val = 1e15   # m⁻³
OH_val_hcho = 1e12  # m⁻³ (= 1e6 cm⁻³)

hcho_prob = remake(prob, p = [
    compiled.HCHO => HCHO_val, compiled.OH => OH_val_hcho])
sol = solve(hcho_prob)

R10_val = sol[compiled.R10]  # HCHO + OH
R11_val = sol[compiled.R11]  # HCHO → HCO + H
R12_val = sol[compiled.R12]  # HCHO → H₂ + CO

L_total = R10_val + R11_val + R12_val
fractions = [R10_val / L_total * 100, R11_val / L_total * 100, R12_val / L_total * 100]
labels_bar = [
    "HCHO + OH\n(radical)", "HCHO + hν → HCO + H\n(radical)", "HCHO + hν → H₂ + CO\n(molecular)"]

bar(labels_bar, fractions,
    ylabel = "Fraction of HCHO Loss (%)",
    title = "Formaldehyde Loss Pathways",
    label = false, color = [:steelblue :orange :green],
    size = (600, 400), ylims = (0, 100))
"/home/runner/work/GasChem.jl/GasChem.jl/docs/build/hcho_branching.svg"

HCHO loss pathway branching

At typical daytime conditions (OH = 10^6 molec/cm3), the molecular photolysis channel (producing H2 + CO) is the largest single loss pathway. Both the OH reaction and radical photolysis channels produce HOx radicals that contribute to further O3 production, while the molecular channel does not produce radicals.