Skip to article frontmatterSkip to article content

Beer-Lambert's Law - HAPI

United Nations Environmental Programme

Introduction: HAPI as a Radiative Transfer Model (RTM)

For your goal of gas retrieval, you need a “forward model”—a tool that simulates a spectrum given a set of atmospheric conditions (like gas concentration, temperature, pressure). This forward model is your Radiative Transfer Model (RTM).

HAPI (HITRAN Application Programming Interface) is a powerful Python library that functions as the core of an RTM.

ELI5 (Explain Like I’m 5): 🧠 Think of the HITRAN database as a giant, hyper-detailed “fingerprint” library for every gas. HAPI is the expert “forensic analyst” that knows how to read this library. You give HAPI a “case” (a specific temperature, pressure, and gas amount), and it generates the exact fingerprint (the spectrum) you should see.

The Retrieval Goal: Your retrieval problem is the reverse. You have a “mystery fingerprint” (your measured transmittance spectrum) and you ask HAPI to generate thousands of “known fingerprints” by changing the gas amount parameter until the HAPI-generated spectrum perfectly matches your measurement. The gas amount that creates the match is your retrieved concentration.

This report will walk through the step-by-step logic of how HAPI builds that spectrum, from the fundamental physics to the practical code.

1. The “Line-by-Line” (LBL) Method

This is the core philosophy behind HAPI and the most accurate RTM method.

ELI5: A gas’s absorption spectrum isn’t a smooth band. It’s a “forest” made of thousands of individual “trees” 🌳 (the absorption lines). Instead of just estimating the size of the forest, the LBL method meticulously calculates the size, shape, and position of every single tree and adds them all up.

Theory (Continuous Equation): The total absorption coefficient α\alpha at any wavenumber ν\nu is the linear sum of the absorption coefficients from all (ii) nearby lines.

α(ν)=iαi(ν)\alpha(\nu) = \sum_{i} \alpha_i(\nu)

HAPI Implementation (Discretized): HAPI does this sum automatically. When you call a function like hapi.absorptionCrossSection_Voigt, it fetches all the lines in your specified range and performs this summation across your WavenumberGrid (a NumPy array).

Assumptions:


2. Line Intensity (Si(T)S_i(T))

This is the first of two components for every line. It defines the “strength” or “height” of the line.

ELI5: This is the “volume” of a single note 🎵. This volume changes with temperature. A hot gas “plays” some notes (lines) louder and others softer than a cold gas.

Theory (Continuous Equation): HAPI scales the reference intensity SrefS_{ref} (from HITRAN, at Tref=296T_{ref}=296 K) to your desired temperature TT using this equation from statistical mechanics:

Si(T)=Si(Tref)Q(Tref)Q(T)exp(c2Ei/T)exp(c2Ei/Tref)[1exp(c2ν0,i/T)][1exp(c2ν0,i/Tref)]S_i(T) = S_i(T_{ref}) \cdot \frac{Q(T_{ref})}{Q(T)} \cdot \frac{\exp(-c_2 E_i''/T)}{\exp(-c_2 E_i''/T_{ref})} \cdot \frac{[1 - \exp(-c_2 \nu_{0,i}/T)]}{[1 - \exp(-c_2 \nu_{0,i}/T_{ref})]}

HAPI Implementation: This complex calculation is done internally when you pass the TT parameter.

# T is part of the 'Environment' dictionary
env = {'T': 300.0, 'p': 1.0}
# HAPI automatically scales all S_ref from 296K to 300K
sigma = hapi.absorptionCrossSection_Voigt(..., Environment=env) 

Variables & Units:

Assumptions:


3. Line Shape Function (fi(ν)f_i(\nu))

This is the second component. It defines the “shape” or “width” of the line.

ELI5: A real musical note isn’t just one pure frequency. It’s a bit “fuzzy.” This function describes the shape of that fuzziness. The fuzziness comes from two things:

  1. Doppler Broadening: Molecules moving away from you sound lower-pitched (red-shift), and those moving toward you sound higher-pitched (blue-shift). This “smears” the line into a Gaussian (bell curve) shape.

  2. Pressure Broadening: Molecules are constantly “bumping” into each other. These collisions interrupt the absorption, smearing the line into a Lorentzian shape. More pressure = more bumps = wider, fuzzier line.

Theory (Continuous Equation): The line shape function ff is not a single simple equation. It’s a profile whose width is determined by the Doppler half-width (γD\gamma_D) and the Pressure (Lorentzian) half-width (γL\gamma_L).

HAPI Implementation: HAPI calculates these widths automatically using TTPP, and the mole_fractions dictionary you provide. This is why passing the concentration (VMR) is so important—it correctly balances self-broadening (γself\gamma_{\text{self}}, CH₄-CH₄ collisions) and air-broadening (γair\gamma_{\text{air}}, CH₄-Air collisions).

env = {'T': T, 'p': P_total, 'mole_fractions': {'CH4': VMR_CH4}}
sigma = hapi.absorptionCrossSection_Voigt(..., Environment=env)

Assumptions: The broadening mechanisms (Doppler, Pressure) are independent.


4. The Voigt Profile

This is the “real” line shape, which combines the two broadening effects.

ELI5: The Voigt profile is simply what you get when you combine the Gaussian shape (from Doppler) and the Lorentzian shape (from Pressure) together.

Theory (Continuous Equation): The Voigt profile fVf_V is the convolution of the Gaussian (GG) and Lorentzian (LL) functions. This is a complex integral.

fV(ν)=(GL)(ν)=G(ν)L(νν)dνf_V(\nu) = (G * L)(\nu) = \int_{-\infty}^{\infty} G(\nu') \cdot L(\nu - \nu') \cdot d\nu'

HAPI Implementation: This is HAPI’s specialty. The _Voigt in absorptionCrossSection_Voigt means HAPI is using highly optimized numerical algorithms (like the Humlíček algorithm) to calculate this difficult convolution for every line at every point in your WavenumberGrid.

Assumptions. The line shape is fully described by this convolution. This is an excellent assumption for most applications.


5. Absorption Cross-Section (σ\sigma)

This is the first major output you calculate. It combines Intensity and Shape into a single, per-molecule property.

ELI5: The “cross-section” is the “target size” of a single molecule 🎯. It combines the “strength” (SS) and the “shape” (ff) of all its lines. A molecule with a large cross-section is very effective at blocking light.

Theory (Continuous Equation): This is the LBL summation (Step 1) of the Intensity (Step 2) multiplied by the Shape (Step 4) for each line.

σ(ν)=iSi(T)fV,i(ν)\sigma(\nu) = \sum_{i} S_i(T) \cdot f_{V,i}(\nu)

HAPI Implementation (Discretized): This is the main HAPI function call.

# nu is a NumPy array, e.g., np.arange(2900, 3100, 0.01)
# sigma will be a NumPy array of the same size as nu

# Get absorption cross-section per molecule
nu, sigma = hapi.absorptionCoefficient_Voigt(
    SourceTables='CH4',
    WavenumberGrid=nu,
    Environment={'T': T, 'p': P_total},
    Diluent={'air': 1.0 - VMR_CH4, 'self': VMR_CH4},
    HITRAN_units=True  # Returns cm²/molecule
)

Variables & Units:


6. Number Density (NN)

Now that we know the “per-molecule” target size, we need to know how many molecules there are.

ELI5: This is simply counting how many molecules are packed into a small box (a cubic centimeter). More pressure or colder temperatures will pack more molecules into the same box.

Theory (Continuous Equation): From the Ideal Gas LawN=P/(kBT)N = P / (k_B T). We calculate the total number density NtotalN_{\text{total}} (all air) and then find the CH₄-specific number density NCH4N_{\text{CH4}} using the concentration.

N_total=P_totalk_BT\\ \\ N\_{\text{total}} = \frac{P\_{\text{total}}}{k\_B T}
NCH4=NtotalVMRCH4N_{\text{CH4}} = N_{\text{total}} \cdot \text{VMR}_{\text{CH4}}

HAPI Implementation (Discretized):

N_total = hapi.numberDensity(P_total, T)
N_CH4 = N_total * VMR_CH4 

Variables & Units:

Assumptions:


7. Absorption Coefficient (α\alpha)

This is the key physical property of the bulk gas, not just a single molecule.

ELI5: If σ\sigma is the “target size” of one molecule, α\alpha is the total “target size” of all molecules in the box. It’s the “per-molecule” size (σ\sigma) times “how many molecules” (NN).

Theory (Continuous Equation):

α(ν)=σ(ν)NCH4\alpha(\nu) = \sigma(\nu) \cdot N_{\text{CH4}}

HAPI Implementation (Discretized): This is a simple NumPy array-scalar multiplication.

# alpha is a NumPy array, same size as sigma
alpha = sigma * N_CH4

Variables & Units:


8. Path Length (LL) & Air Mass Factor (AMF)

Now we know how much the gas absorbs per cm. Next, we need to know how many centimeters the light travels through.

ELI5: This is the total distance the light travels through your gas layer. If you look straight down (VZA=0°) at a 1 km thick layer, the path is 1 km. If you look at an angle (e.g., VZA=60°), the light has to “slant” through the layer, so the path is longer (2 km). The Air Mass Factor (AMF) is the multiplier (in this case, 2) that accounts for this slant.

Theory (Continuous Equation): For a simple remote sensing case (sunlight comes in, bounces off the ground, goes to the satellite), the total path is the sum of the “in” path and the “out” path.

AMF1cos(SZA)+1cos(VZA)\text{AMF} \approx \frac{1}{\cos(\text{SZA})} + \frac{1}{\cos(\text{VZA})}
L=LvertAMFL = L_{\text{vert}} \cdot \text{AMF}

HAPI Implementation (Discretized): This is standard Python/NumPy math.

SZA_rad = np.deg2rad(SZA)
VZA_rad = np.deg2rad(VZA)
AMF = (1.0 / np.cos(SZA_rad)) + (1.0 / np.cos(VZA_rad))
L = L_vert * AMF 

Variables & Units:

Assumptions:


9. Transmittance (TT)

This is the final simulated spectrum, the “fingerprint” you compare against your measurement.

ELI5: Transmittance is the fraction of light (from 0 to 1) that makes it through the gas. If the absorption coefficient  (the “blockiness”) is high, or the path  (the “distance”) is long, the transmittance will be low.

Theory (Continuous Equation): This is the Beer-Lambert Law.

T(ν)=exp(α(ν)L)\\ \\ T(\nu) = \exp\left(-\alpha(\nu) \cdot L\right)

HAPI Implementation (Discretized): This is a simple NumPy exponential function.

# tau is your final transmittance spectrum, a NumPy array
tau = np.exp(-alpha * L)

Variables & Units:

Assumptions:


Summary: The Full Algorithm for Retrieval

To run your “forward model” and simulate a spectrum for retrieval, you follow this exact chain of logic:

  1. Define Inputs:

    • VMR_CH4 (Your “guess” parameter)

    • TP_total (Assumed atmospheric state)

    • L_vert (Assumed layer thickness)

    • SZAVZA (Known geometry)

    • nu (Your instrument’s spectral grid)

  2. Run HAPI & Physics:

    • sigma = hapi.absorptionCrossSection_Voigt(...) (Calculates Steps 1-5)

    • N_total = hapi.numberDensity(...) (Step 6a)

    • N_CH4 = N_total * VMR_CH4 (Step 6b)

    • alpha = sigma * N_CH4 (Step 7)

    • L = L_vert * AMF(...) (Step 8)

    • tau = np.exp(-alpha * L) (Step 9)

Your retrieval algorithm will then compare this tau to your measured spectrum, and if they don’t match, it will go back to Step 1 and try a new VMR_CH4.