Skip to content

Absorption: Spectral Line Shapes

The purpose of this tutorial is to demonstrate how to compute line-shapes using the vSmartMOM Julia package. Here, we focus on rotational-vibrational transition lines, what processes determine line-shapes, and how the line-shapes depend on pressure and temperature.

Basic Tools

We are making use of the HITRAN database, see a list of tutorials here.

There are other spectroscopic linelists, but we are focusing on HITRAN only here. In addition, there are more complex line-shapes that we are not treating here, including collisional narrowing and line-mixing, collision-induced absorption (CIA). In the future, these might be added to vSmartMOM.

Line-shape:

If we have a line-strength S (in cm cm/molecule) for a specific transition at , we can compute the cross section as:

where   denotes the line-shape function (in 1/cm), which is normalized to 1:

There are several processes that affect the shape and width of , and we will walk through the most important ones here.

Doppler Broadening

Doppler broadening is caused by a simple doppler shift of emitted and absorbed frequencies, caused by the relative velocities of the molecules along the line of sight. A doppler shifted apparent frequency from the centroid frequency can be described as:

where is the relative velocity of the absorbing photon along the line of sight. The doppler shift is then simply

Let us take a simple example with the satellite flying at 7km/s and either staring into the flight direction (technically, it wouldn't see the atmosphere then, but let's ignore this) or looking into the back:


julia
# Speed of light (in m/s)
c = 299792458.0

# Relative velocity (in m/s)
vᵣ = 7000.0

# Center wavenumber (say 1600nm, which is 1e7/1600=6250 cm$^{-1}$)
v₀ = 6250.0

# Doppler shift:
Δ_ν = ( v₀ * vᵣ) / c

# Just writing out doppler shift in wavenumbers and wavelengths
println("Doppler shift = $Δ_ν cm-1")
println("Doppler shift = $(1e7/(v₀-Δ_ν)-1e7/v₀) nm")

Random movements of molecules in gases lead to doppler broadening effects

In the one-dimensional case – say along the x-axis –, the speed of molecules is distributed according to the Maxwell-Boltzmann distribution:

where is the particle mass, is the Boltzmann constant, and T is thermodynamic temperature.

We can thus define a Doppler width as

which yields the following line-shape:

which is a Gaussian distribution with representing the standard deviation.

Let us put in some numbers with R=8.3144598 J/K/mol at 6000cm:

T = 220K, 290K

M = 16g/mol (CH) or 44g/mol (CO)


Multitply with about 1.6585 (2) to get the full-width half-maximum (FWHM) of the spectral line.


Natural broadening

The Heisenberg uncertainty principle tells us that there is an uncertainty in the Energy:

As is , we can write:

The natural line-width is defined by using the resulting radiative lifetime, but this is mostly negligible as the natural lifetime of the upper state is usually much much smaller than the "perturbed" lifetime in the presence of quencher (e.g. through collisions or doppler broadening). Again, there are exceptions.

Collisional broadening

Collisions between molecules quench the excited state and thus reduce the lifetime of the upper state, resulting in a widening of the line width. This behavor gives rise to the so-called Lorentz lineshape.

depends linearly on the number density of the perturbing molecules and the relative speed of the collision partners (thus scales linearly with pressure and with , basically both density and velocity affect the number of collisions per time). We often call this type of broadening pressure broadening, caused by collisions between molecules or atoms, which can supply or remove small amounts of energy during radiative transitions, thereby allowing photons with a broader range of frequencies to produce a particular transition of a molecule.


Voigt lineshape

The Voigt line-shape is the combination of Doppler and Lorentz broadening (convolution of the two) but cannot be evaluated analytically. However, there are numerical routines to compute it efficiently, multiple of which are implemented in vSmartMOM.


Other more complex lineshapes

Once you dig deeper, there are various other more complex line-shapes (and line-mixing effects), which we ignore for now as the Voigt line-shape can provide very reasonable results. See, for instance, here.

The generated docs include a compact Plotly preview of the same line-shape families. The Voigt trace is computed as a numerical convolution of the Doppler and Lorentz profiles on the displayed grid.

Import the required tools:

julia
using CairoMakie
using Pkg.Artifacts
using vSmartMOM
using vSmartMOM.Absorption

Read the HITRAN database for CO, using artifacts in Julia:

julia
co2_par      = Absorption.read_hitran(artifact("CO2"), mol=2, iso=1, ν_min=6214.4, ν_max=6214.8);
nothing #hide

Create a Voigt model (all in CPU mode)

julia
line_voigt   = make_hitran_model(co2_par, Voigt(), architecture=CPU())

Create a Doppler model

julia
line_doppler = make_hitran_model(co2_par, Doppler(), architecture=CPU())

Create a Lorentz model

julia
line_lorentz = make_hitran_model(co2_par, Lorentz(), architecture=CPU())
# Specify our wavenumber grid
ν = 6210:0.001:6220;
nothing #hide

Here we compute the cross sections at different pressures (in hPa, 3rd argument) and temperatures (in K, 4th argument)

julia
cs_co2_1atm   = absorption_cross_section(line_voigt, ν, 1013.0     , 296.0);
cs_co2_075atm = absorption_cross_section(line_voigt, ν, 0.75*1013.0, 296.0);
cs_co2_05atm  = absorption_cross_section(line_voigt, ν, 0.5*1013.0 , 296.0);
cs_co2_025atm = absorption_cross_section(line_voigt, ν, 0.25*1013.0, 296.0);
cs_co2_01atm  = absorption_cross_section(line_voigt, ν, 0.1*1013.0 , 296.0);
nothing #hide

Get some more line-shapes just for Doppler and Voigt to better compare the different line shapes

julia
cs_co2_01atm    = absorption_cross_section(line_voigt,   ν, 0.1*1013.0 , 296.0);
cs_co2_doppler  = absorption_cross_section(line_doppler, ν, 0.1*1013.0 , 296.0);
cs_co2_lorentz  = absorption_cross_section(line_lorentz, ν, 0.1*1013.0 , 296.0);
nothing #hide

Using a scaling factor for display purposes:

julia
ff = 1e20;
fig = Figure(size=(700, 450))
ax = Axis(fig[1,1],
    xlabel = "Wavenumber (cm⁻¹)",
    ylabel = "Absorption cross section (10⁻²⁰ cm²/molecule)")
lines!(ax, collect(ν), ff .* cs_co2_1atm,   label="Voigt, 1 atm")
lines!(ax, collect(ν), ff .* cs_co2_075atm, label="Voigt, 0.75 atm")
lines!(ax, collect(ν), ff .* cs_co2_05atm,  label="Voigt, 0.5 atm")
lines!(ax, collect(ν), ff .* cs_co2_025atm, label="Voigt, 0.25 atm")
xlims!(ax, 6214, 6215.2)
axislegend(ax, position=:rt)
fig

From these figures we can see how strongly the line-width is impacted by pressure broadening at ranges from about 250 hPa to 1013 hPa. Especially the line wings are substantially widened, which is typical of Lorentz line-shapes.

julia
fig = Figure(size=(700, 450))
ax = Axis(fig[1,1],
    xlabel = "Wavenumber (cm⁻¹)",
    ylabel = "Normalized absorption cross section")
lines!(ax, collect(ν), cs_co2_01atm   ./ maximum(cs_co2_01atm), label="Voigt, 296K, 0.1 atm")
lines!(ax, collect(ν), cs_co2_doppler ./ maximum(cs_co2_01atm), label="Doppler, 296K")
lines!(ax, collect(ν), cs_co2_lorentz ./ maximum(cs_co2_01atm), label="Lorentz, 296K, 0.1 atm")
xlims!(ax, 6214.4, 6214.8)
axislegend(ax, position=:rt)
fig

We can see the individual impacts of Doppler and Pressure broadening at about 100 hPa, at which Doppler broadening becomes important too. Most of the shape of the wings is still dominated by pressure broadening but the center has substantial contributions from Doppler broadening as well. The Voigt shape shown here is the convolution of the Lorentz and Doppler shape.

From an individual line to a band

Here, we will compute an entire band of CO (a few to be precise) and look at some simple behavior, e.g. the re-distribution of individual lines in the P and R branch with changing temperature.

Load a wider spectral range

julia
co2_par_band = Absorption.read_hitran(artifact("CO2"), mol=2, iso=1, ν_min=6000.0, ν_max=6400.0);
nothing #hide

Create a Voigt model for that

julia
band_voigt   = make_hitran_model(co2_par_band , Voigt(), architecture=CPU());
nothing #hide

Define the wavenumber grid and compute cross sections at the same pressure but different temperature:

julia
ν_band = 6300:0.01:6400;
σ_co2_Voigt220 = absorption_cross_section(band_voigt, ν_band, 1013.0 , 220.0);
σ_co2_Voigt290 = absorption_cross_section(band_voigt, ν_band, 1013.0 , 290.0);
nothing #hide
julia
fig = Figure(size=(700, 450))
ax = Axis(fig[1,1],
    xlabel = "Wavenumber (cm⁻¹)",
    ylabel = "σ (10⁻²⁰ cm²/molecule)")
lines!(ax, collect(ν_band), ff .* σ_co2_Voigt220, alpha=0.5, label="220K")
lines!(ax, collect(ν_band), ff .* σ_co2_Voigt290, alpha=0.5, label="290K")
xlims!(ax, 6300, 6380)
axislegend(ax, position=:rt)
fig

This is still a bit hard to see as the differences are relatively subtle. However, we can already see that absorptions near the band center are stronger at lower temperatures, while the opposite is true at higher rotational states. This behavior is caused by the distribution of lower rotational states, which only occupy higher Js at higher temperatures. This picture becomes more clear if we look at differences.

julia
fig = Figure(size=(700, 450))
ax = Axis(fig[1,1],
    xlabel = "Wavenumber (cm⁻¹)",
    ylabel = "Δσ (10⁻²⁰ cm²/molecule)")
lines!(ax, collect(ν_band), ff .* (σ_co2_Voigt220 .- σ_co2_Voigt290), label="220K − 290K")
xlims!(ax, 6300, 6380)
axislegend(ax, position=:rt)
fig

The docs build includes an interactive Plotly summary of this temperature redistribution effect across the band:

Now we can clearly see how the change in the distribution of the lower rotational state leads to a redistribution of cross sections (a shift from lower to higher ground states with higher T)


Animated views

We can use CairoMakie's record function to create animations showing the impact of pressure and temperature:

julia
T = 290.0
fig = Figure(size=(700, 450))
ax = Axis(fig[1,1],
    xlabel = "Wavenumber (cm⁻¹)",
    ylabel = "σ (10⁻²⁰ cm²/molecule)",
    yscale = log10)
record(fig, joinpath(@__DIR__, "absorption_pressure.gif"), 10:10:1100; framerate=15) do p
    empty!(ax)
    σ = absorption_cross_section(band_voigt, ν_band, Float64(p), T)
    lines!(ax, collect(ν_band), ff .* σ, label="p=$(p) hPa")
    ylims!(ax, 1e-7, 1e-1)
    axislegend(ax, position=:rt)
end

When run locally, this writes absorption_pressure.gif next to the tutorial.

julia
p = 900.0
fig = Figure(size=(700, 450))
ax = Axis(fig[1,1],
    xlabel = "Wavenumber (cm⁻¹)",
    ylabel = "σ (10⁻²⁰ cm²/molecule)",
    yscale = log10)
record(fig, joinpath(@__DIR__, "absorption_temperature.gif"), 10:10:320; framerate=15) do T
    empty!(ax)
    σ = absorption_cross_section(band_voigt, ν_band, p, Float64(T))
    lines!(ax, collect(ν_band), ff .* σ, label="T=$(T) K")
    ylims!(ax, 1e-7, 1e-1)
    axislegend(ax, position=:rt)
end

When run locally, this writes absorption_temperature.gif next to the tutorial.

julia
# More extreme case, let's take 10 atmospheres (10,000 hPa)
σ = absorption_cross_section(band_voigt, ν_band, 10000.0, 300.0);
fig = Figure(size=(700, 450))
ax = Axis(fig[1,1],
    xlabel = "Wavenumber (cm⁻¹)",
    ylabel = "σ (10⁻²⁰ cm²/molecule)")
lines!(ax, collect(ν_band), ff .* σ, label="p = 10000 hPa")
axislegend(ax, position=:rt)
fig

We can see that individual lines can be blurred at very high pressures once the broadening becomes wider than the line spacing.


This page was generated using Literate.jl.