Spectroscopy¶
Cross-section look-up tables σ(ν; T, p): per-layer optical depth for a layered atmosphere (opacity: the OpacityProvider protocol, TableOpacity, and SumOpacity, the sum of several providers on one grid), single-slab interpolation and Beer–Lambert transmittance (tables), v0's table cache and loader (table_cache), the synthetic methane spectra for docs, notebooks and tests (synthetic), the solar irradiance on the model grid from the TSIS-1 HSRS, downloaded once into the table cache, and the Earth–Sun distance (solar), and the offline HITRAN table builder (hitran, the [hitran] extra; python -m radtrax.spectroscopy.hitran CH4 H2O CO2 N2O CO builds v0's tables into the cache). Theory: HITRAN tables.
spectroscopy
¶
Absorption cross sections per layer.
tables interpolates a cross-section look-up table σ(ν; T, p) and applies
Beer–Lambert transmittance, including the differential-ratio form used for
plume-enhancement retrievals. hitran builds those tables offline from
HITRAN through HAPI (the [hitran] extra).
opacity turns them into per-layer optical depth for a layered
atmosphere: the OpacityProvider protocol and TableOpacity, bilinear
in (ln p, T) (radtrax#33), and SumOpacity, the sum of several
providers on one grid (radtrax#184). Line-by-line opacity arrives in v2 of the
roadmap. synthetic builds the synthetic methane spectra the docs,
notebooks and tests use without HITRAN (radtrax#115). table_cache
locates and loads v0's built tables (radtrax#34). solar gives the solar
irradiance on the model grid from the TSIS-1 HSRS, fetched once into the same
cache, and the Earth–Sun distance (radtrax#107).
opacity¶
Per-layer optical depth: the OpacityProvider protocol and table opacity.
The optical depth of each layer at each wavenumber, τ (L, G), is
radtrax's canonical interface between the atmosphere and a solver
(radtrax · 3). v0 computes it from cross-section tables σ_g(ν; p, T),
interpolated bilinearly in (ln p, T) at each layer's pressure and
temperature:
.. math:: σ_{g,l}(ν) = \operatorname{bilinear}(σ_g;\ \ln p_l, T_l), \qquad τ_l(ν) = \sum_g σ_{g,l}(ν)\, n_l\, x_{g,l}\, Δz_l ,
with n_l the air number density, x_{g,l} the gas's mixing ratio and
Δz_l the thickness (:class:~radtrax.atmosphere.AtmosphericState).
Sources of optical depth add: :class:SumOpacity holds several providers on
one grid and returns Σ_k τ^{(k)} (radtrax#184).
Ported from the radtrax · 3 design notebook's TableOpacity and
generalised to a dict of per-gas tables mapped with jax.tree.map, so
the gas list lives in the data. The interpolation is
:class:jax.scipy.interpolate.RegularGridInterpolator. Shapes: L
layers, G wavenumbers, P and T table pressures and
temperatures.
OpacityProvider
¶
Bases: Module
Protocol: an atmospheric state in, per-layer optical depth out.
An abstract Equinox module (an eqx.AbstractVar field and an
abc-abstract __call__), so filter_jit compiles each concrete
provider and a forward model can hold any of them.
Attributes:
| Name | Type | Description |
|---|---|---|
nu_cm |
AbstractVar[Float[Array, ' G']]
|
The wavenumber grid [cm⁻¹], |
CrossSectionTable
¶
Bases: Module
One gas's cross sections σ(ν; p, T) on a (ln p, T) grid.
Attributes:
| Name | Type | Description |
|---|---|---|
log_p_pa |
Float[Array, ' P']
|
Natural log of the table pressures [ln Pa], strictly
increasing, |
t_k |
Float[Array, ' T']
|
Table temperatures [K], strictly increasing, |
sigma_cm2 |
Float[Array, 'P T G']
|
Cross sections [cm²/molecule], |
TableOpacity
¶
TableOpacity(nu_cm: Float[Array, ' G'], tables: dict[str, CrossSectionTable])
Bases: OpacityProvider
Optical depth from per-gas cross-section tables.
τ_l(ν) = Σ_g σ_g(ν; p_l, T_l) n_l x_{g,l} Δz_l. The tables are a
dict keyed by gas and mapped with jax.tree.map; every table
gas must be in the state's vmr (gases only in the state are
transparent here). Tables are leaves: to keep them out of a gradient,
split them off with eqx.partition. Table opacity needs float64
(jax_enable_x64); a float32 grid warns.
Attributes:
| Name | Type | Description |
|---|---|---|
nu_cm |
Float[Array, ' G']
|
Wavenumber grid [cm⁻¹], |
tables |
dict[str, CrossSectionTable]
|
One :class: |
Examples:
>>> import jax.numpy as jnp
>>> from radtrax.atmosphere import layers_from_levels
>>> table = CrossSectionTable(
... log_p_pa=jnp.log(jnp.array([1e4, 1e5])),
... t_k=jnp.array([200.0, 300.0]),
... sigma_cm2=jnp.full((2, 2, 3), 1e-21),
... )
>>> opacity = TableOpacity(
... jnp.array([4300.0, 4300.1, 4300.2]), {"CH4": table}
... )
>>> atm = layers_from_levels(
... jnp.array([1e5, 5e4]),
... jnp.array([288.0, 255.0]),
... {"CH4": jnp.full(2, 1.9e-6)},
... )
>>> opacity(atm).shape
(1, 3)
Source code in src/radtrax/spectroscopy/opacity.py
from_datasets
classmethod
¶
from_datasets(datasets: dict[str, Dataset], *, var: str | dict[str, str] = 'absorption_cross_section') -> TableOpacity
Build from per-gas look-up tables in radtrax's HITRAN format.
Each dataset is what :func:radtrax.spectroscopy.hitran.build_lut_dataset
writes: var on (wavenumber, temperature, pressure) with the
pressure in atm, in any order (pressure and temperature are sorted).
var may be a dict per gas, e.g. {"CH4": "CH4_cross_section"}
for the table of :func:radtrax.spectroscopy.hitran.create_combined_lut.
Every gas must share one wavenumber grid.
Raises:
| Type | Description |
|---|---|
KeyError
|
If a dataset has no such variable. |
ValueError
|
If the wavenumber grids differ. |
Source code in src/radtrax/spectroscopy/opacity.py
SumOpacity
¶
SumOpacity(providers: Sequence[OpacityProvider])
Bases: OpacityProvider
Optical depth from several sources on one wavenumber grid.
τ_l(ν) = Σ_k τ_l^{(k)}(ν): lines, continua and collision-induced
absorption are separate sources whose temperature and pressure
dependences differ, and they add at the optical-depth level (Korkin et
al. 2026, §1, §6). Each provider stays a leaf of the module, so a
gradient flows through every one, and each stays testable alone.
Attributes:
| Name | Type | Description |
|---|---|---|
nu_cm |
Float[Array, ' G']
|
Wavenumber grid [cm⁻¹], |
providers |
tuple[OpacityProvider, ...]
|
The opacity providers whose optical depths add. |
Raises:
| Type | Description |
|---|---|
ParameterError
|
With no providers. |
GridError
|
If a provider's grid differs from the first's in length, or in value when the grids are concrete (under tracing only the lengths are checked). |
Examples:
>>> import jax.numpy as jnp
>>> from radtrax.atmosphere import layers_from_levels
>>> def table(sigma):
... return CrossSectionTable(
... log_p_pa=jnp.log(jnp.array([1e4, 1e5])),
... t_k=jnp.array([200.0, 300.0]),
... sigma_cm2=jnp.full((2, 2, 3), sigma),
... )
>>> nu = jnp.array([4300.0, 4300.1, 4300.2])
>>> lines = TableOpacity(nu, {"CH4": table(1e-21)})
>>> other = TableOpacity(nu, {"H2O": table(2e-23)})
>>> atm = layers_from_levels(
... jnp.array([1e5, 5e4]),
... jnp.array([288.0, 255.0]),
... {"CH4": jnp.full(2, 1.9e-6), "H2O": jnp.full(2, 1e-2)},
... )
>>> total = SumOpacity((lines, other))
>>> bool(jnp.allclose(total(atm), lines(atm) + other(atm)))
True
Source code in src/radtrax/spectroscopy/opacity.py
solar¶
The solar irradiance at the top of the atmosphere, on the model grid.
:func:solar_irradiance gives \(E_0(\nu)\) at 1 AU in W m⁻² (cm⁻¹)⁻¹, the
solar_irradiance that :class:~radtrax.solvers.BeerLambert takes, on the
wavenumber grid before the band integration, so that the Fraunhofer lines
are integrated by the instrument rather than averaged into the band
beforehand (radtrax#107).
The one source today is the TSIS-1 Hybrid Solar Reference Spectrum, version
2 (Coddington et al. 2021, Geophys. Res. Lett. 48, e2020GL091709;
LASP, file hybrid_reference_spectrum_c2022-11-30_with_unc.nc): 202 to
2730 nm in vacuum wavelength at 0.001 nm sampling, in W m⁻² nm⁻¹, at 1 AU.
It is 61 MB, so it doesn't ship with radtrax: :func:fetch_solar_spectrum
downloads it once into the table cache (:func:~radtrax.spectroscopy.table_cache_dir)
and checks its SHA-256, and :func:solar_irradiance fetches it on first use.
Per unit wavenumber, with \(\lambda\) in nm and \(\nu = 10^7 / \lambda\) in cm⁻¹,
At a date other than the mean distance, divide by \(d^2\), with \(d\) from
:func:earth_sun_distance.
SolarSource
dataclass
¶
A published solar reference spectrum that radtrax can download.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
The key :func: |
url |
str
|
Where the file is downloaded from. |
filename |
str
|
The file's name in the cache. |
sha256 |
str
|
The file's SHA-256, checked after every download. |
citation |
str
|
The reference to cite. |
solar_spectrum_path
¶
The cache file of a solar spectrum (it may not exist yet).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
str
|
A key of :data: |
'tsis1-hsrs'
|
cache_dir
|
Path | str | None
|
The cache directory; :func: |
None
|
Returns:
| Type | Description |
|---|---|
Path
|
|
Raises:
| Type | Description |
|---|---|
ParameterError
|
If |
Examples:
>>> from radtrax.spectroscopy.solar import solar_spectrum_path
>>> solar_spectrum_path(cache_dir="/cache").name
'hybrid_reference_spectrum_c2022-11-30_with_unc.nc'
Source code in src/radtrax/spectroscopy/solar.py
fetch_solar_spectrum
¶
Download a solar spectrum into the cache, once, and check its SHA-256.
A file already in the cache is returned without a download.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
str
|
A key of :data: |
'tsis1-hsrs'
|
cache_dir
|
Path | str | None
|
The cache directory; :func: |
None
|
Returns:
| Type | Description |
|---|---|
Path
|
The path of the cached file. |
Raises:
| Type | Description |
|---|---|
ParameterError
|
If |
MissingTableError
|
If the download fails (offline, or the server refuses); the message names the URL and the command to retry. |
TableBuildError
|
If the downloaded file's SHA-256 is not the registered one; nothing is written to the cache. |
Source code in src/radtrax/spectroscopy/solar.py
solar_irradiance
¶
solar_irradiance(nu_cm: Float[ArrayLike, ' G'], *, source: str = 'tsis1-hsrs', cache_dir: Path | str | None = None, download: bool = True) -> Float[Array, ' G']
Solar irradiance \(E_0(\nu)\) at 1 AU, interpolated onto a wavenumber grid.
Linear in wavenumber between the reference's samples (0.001 nm, i.e.
0.0019 cm⁻¹ at 2.3 µm and 0.0037 cm⁻¹ at 1.6 µm, so finer than v0's
0.01 cm⁻¹ grid: the grid samples the spectrum rather than averaging
it). The spectrum is read with NumPy, off the gradient path: call this
once, outside jit, and pass the array to
:class:~radtrax.solvers.BeerLambert.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
nu_cm
|
Float[ArrayLike, ' G']
|
The model's wavenumbers [cm⁻¹], |
required |
source
|
str
|
A key of :data: |
'tsis1-hsrs'
|
cache_dir
|
Path | str | None
|
The cache directory; :func: |
None
|
download
|
bool
|
Fetch the spectrum if it is not cached
(:func: |
True
|
Returns:
| Type | Description |
|---|---|
Float[Array, ' G']
|
\(E_0\) [W m⁻² (cm⁻¹)⁻¹], |
Float[Array, ' G']
|
(:func: |
Raises:
| Type | Description |
|---|---|
ParameterError
|
If |
MissingTableError
|
If the spectrum is not cached and |
GridError
|
If any wavenumber is outside the source's range. |
Source code in src/radtrax/spectroscopy/solar.py
earth_sun_distance
¶
The Earth–Sun distance [AU] on a day of the year.
with \(t\) the day of the year (1 on 1 January; fractional days allowed):
the first-order eccentricity term with perihelion on 4 January, the
formula georeader's earth_sun_distance_correction_factor uses for
EMIT, Sentinel-2 and PRISMA reflectance. The irradiance on that day is
\(E_0 / d^2\).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
day_of_year
|
Real[ArrayLike, '*shape']
|
The day of the year, any shape. |
required |
Returns:
| Type | Description |
|---|---|
Float[Array, '*shape']
|
\(d\) [AU], same shape. |
Examples:
>>> from radtrax.spectroscopy.solar import earth_sun_distance
>>> print(f"{float(earth_sun_distance(4.0)):.5f}")
0.98327
>>> print(f"{float(earth_sun_distance(186.0)):.5f}")
1.01673
Source code in src/radtrax/spectroscopy/solar.py
synthetic¶
Synthetic methane spectra for docs, notebooks and tests, without HITRAN.
Two spectra, each built deterministically from a seed rather than shipped:
- :func:
synthetic_table: radtrax · 3's cross-section table, 150 Voigt lines between 4252 and 4348 cm⁻¹ on 4230–4370 cm⁻¹ at 0.01 cm⁻¹, ten pressures and six temperatures, in the format of :func:radtrax.spectroscopy.hitran.build_lut_dataset(about a second to build). Line strengths are log-normal at 296 K, lower-state energies 0–600 cm⁻¹, Lorentz half-widthsγ_air (p / 1 atm)(296 / T)^0.75and Doppler widths of CH₄ at the line's temperature. - :func:
methane_cross_section: radtrax · 1 and · 2's monochromatic cross-section on :data:WL_NM[m²/kg]: Lorentz lines drawn at random in the 1.66 and 2.3 μm bands, scaled so that the averages under two band responses (Sentinel-2's B11 and B12 in those notebooks) are chosen values.
They were tests/_synthetic_table.py here and
notebooks/geomodels/synthetic_ch4.py in geostack; both are bit for bit
the same here (radtrax#115). SciPy (the Voigt profile) is imported inside
:func:synthetic_table.
Shapes: G wavenumbers or wavelengths; T temperatures; P pressures.
synthetic_table
¶
radtrax · 3's σ(ν; T, p) [cm²/molecule] on (wavenumber, temperature, pressure [atm]).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
seed
|
int
|
Seed of the line draw ( |
0
|
Returns:
| Type | Description |
|---|---|
Dataset
|
|
Dataset
|
data: |
Examples:
>>> table = synthetic_table()
>>> dict(table.sizes)
{'wavenumber': 14000, 'temperature': 6, 'pressure': 10}
Source code in src/radtrax/spectroscopy/synthetic.py
band_mean
¶
band_mean(x: Float[ndarray, '*batch G'], f: Float[ndarray, ' G']) -> float | Float[ndarray, '*batch']
⟨x⟩_b = ∫ x f dλ / ∫ f dλ on a shared uniform grid.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
Float[ndarray, '*batch G']
|
Values on the grid, |
required |
f
|
Float[ndarray, ' G']
|
A band response on the same grid, |
required |
Returns:
| Type | Description |
|---|---|
float | Float[ndarray, '*batch']
|
The response-weighted mean over the last axis (a float for a 1-D |
Source code in src/radtrax/spectroscopy/synthetic.py
methane_cross_section
¶
methane_cross_section(rng: Generator, f11: Float[ndarray, ' G'], f12: Float[ndarray, ' G'], sig12: float = 0.4, ratio11: float = 0.1) -> Float[ndarray, ' G']
radtrax · 1 and · 2's synthetic CH₄ cross-section σ(λ) on :data:WL_NM [m²/kg].
Draws 3,000 lines in the 2.3 μm band, then 600 in the 1.66 μm band, from
rng (in that order, so a caller's later draws are unchanged), and
scales them so that ⟨σ⟩₁₂ = sig12 and ⟨σ⟩₁₁ = ratio11 · sig12
under the band responses f12 and f11.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
rng
|
Generator
|
The caller's generator, advanced by the draws. |
required |
f11
|
Float[ndarray, ' G']
|
The 1.66 μm band's response on :data: |
required |
f12
|
Float[ndarray, ' G']
|
The 2.3 μm band's response on :data: |
required |
sig12
|
float
|
The 2.3 μm band's mean cross-section [m²/kg]. |
0.4
|
ratio11
|
float
|
The 1.66 μm band's mean over the 2.3 μm band's. |
0.1
|
Returns:
| Type | Description |
|---|---|
Float[ndarray, ' G']
|
σ(λ) [m²/kg], |
Examples:
>>> f11 = ((WL_NM >= 1569) & (WL_NM <= 1658)).astype(float)
>>> f12 = ((WL_NM >= 2113) & (WL_NM <= 2286)).astype(float)
>>> sigma = methane_cross_section(np.random.default_rng(0), f11, f12)
>>> (
... round(float(band_mean(sigma, f12)), 12),
... round(float(band_mean(sigma, f11)), 12),
... )
(0.4, 0.04)
Source code in src/radtrax/spectroscopy/synthetic.py
table_cache¶
The cross-section tables of v0: where they live and how to load them.
The tables are hundreds of MB, so they don't ship with radtrax (decision
D6). Each is built once with the [hitran] extra,
.. code-block:: console
python -m radtrax.spectroscopy.hitran CH4 H2O CO2 N2O CO
and cached under the user cache directory, keyed by gas, grid, HITRAN
version and table version; :func:load_table and :func:load_opacity read them back, and a
missing table raises :class:~radtrax.MissingTableError naming that
command. Loading needs no extra (xarray and netCDF4 are base dependencies).
V0_GRID is the grid v0 builds on: 4000–6500 cm⁻¹ (Sentinel-2's bands
11 and 12 and the 1.6 and 2.3 µm windows of imaging spectrometers) at
0.01 cm⁻¹, 16 pressures from 0.01 to 1.1 atm (the surface to ~31 km, log
spaced, ratio 1.37) and 8 temperatures from 180 to 320 K: 128 MB per gas as
float32. It is a proposal that gate V2 checks: band transmittance within
0.1 % of HAPI through the AFGL US Standard atmosphere
(tests/spectroscopy/test_v2_hapi.py, which also reports the
monochromatic difference). A single-line study puts the monochromatic error
of this grid near 4.5e-3 and that of 32 pressures × 15 temperatures near
1e-3 (480 MB per gas); refine the grid if the band gate fails.
TableGrid
dataclass
¶
TableGrid(name: str, nu_min_cm: float, nu_max_cm: float, nu_step_cm: float, p_atm: tuple[float, ...], t_k: tuple[float, ...])
The (ν, p, T) grid a cross-section table is built on.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
A short name for messages and the CLI ( |
nu_min_cm |
float
|
First wavenumber [cm⁻¹]. |
nu_max_cm |
float
|
Last wavenumber [cm⁻¹]. |
nu_step_cm |
float
|
Wavenumber step [cm⁻¹]. |
p_atm |
tuple[float, ...]
|
Table pressures [atm], increasing. |
t_k |
tuple[float, ...]
|
Table temperatures [K], increasing. |
key
¶
A short hash of the grid's values (not its name), for file names.
Source code in src/radtrax/spectroscopy/table_cache.py
table_cache_dir
¶
Where tables are cached.
$RADTRAX_TABLE_DIR if set, else $XDG_CACHE_HOME/radtrax/tables,
else ~/.cache/radtrax/tables.
Source code in src/radtrax/spectroscopy/table_cache.py
table_path
¶
table_path(gas: str, grid: TableGrid = V0_GRID, *, hitran_version: str = HITRAN_VERSION, cache_dir: Path | str | None = None) -> Path
The cache file of gas on grid: <gas>_<grid key>_<HITRAN>_v<table>.nc.
Source code in src/radtrax/spectroscopy/table_cache.py
build_command
¶
build_command(gases: Sequence[str], grid: TableGrid = V0_GRID) -> str
The shell command that builds gases on grid.
Source code in src/radtrax/spectroscopy/table_cache.py
load_table
¶
load_table(gas: str, grid: TableGrid = V0_GRID, *, hitran_version: str = HITRAN_VERSION, cache_dir: Path | str | None = None) -> Dataset
Open gas's cached table (lazily, with xarray).
Raises:
| Type | Description |
|---|---|
MissingTableError
|
If it isn't built; the message names the command. |
Source code in src/radtrax/spectroscopy/table_cache.py
load_opacity
¶
load_opacity(gases: Sequence[str], grid: TableGrid = V0_GRID, *, hitran_version: str = HITRAN_VERSION, cache_dir: Path | str | None = None) -> TableOpacity
A :class:~radtrax.spectroscopy.TableOpacity from cached tables.
Raises:
| Type | Description |
|---|---|
MissingTableError
|
Naming every gas that isn't built, with one command that builds them all. |
Source code in src/radtrax/spectroscopy/table_cache.py
tables¶
LUT interpolation + Beer-Lambert transmittance for one-shot forward models.
This module closes the loop between an offline-generated σ(ν, T, P) LUT and the naïve single-layer radiative-transfer model used in the tutorial notebooks. The physics is:
N_total = p / (k_B T) [molecules/cm³] (ideal gas)
N_gas = N_total · VMR_gas [molecules/cm³]
α(ν) = σ(ν; T, p) · N_gas [1/cm]
AMF = 1/cos(SZA) + 1/cos(VZA) [plane-parallel, two-way]
τ(ν) = exp[ -α(ν) · L_vert · AMF ] [Beer-Lambert]
For plume-enhancement retrievals the differential form is
τ_ratio(ν) = τ_total(ν) / τ_background(ν)
= exp[ -σ(ν; T, p) · N_total · L_vert · AMF · (VMR_total - VMR_bg) ]
— any factor common to the "plume" and "background" pixel (solar spectrum, broadband albedo, aerosol) cancels, leaving the narrow-band gas signature.
Units: the LUT is in HAPI's HITRAN convention (σ in cm², p in atm). We work
in cgs throughout — pressures are converted to Pa only to evaluate number
density via the ideal-gas law with k_B = 1.380649e-23 J/K.
Ported from plumax 0.1.2 at 28f257e (src/plumax/hapi_lut/beers.py); only import paths changed.
number_density
¶
number_density(p_atm: Real[ArrayLike, '*#shape'], T_K: Real[ArrayLike, '*#shape']) -> Float[ArrayLike, '*#shape']
Total number density from the ideal-gas law [molecules / cm³].
with \(p\) converted from atm to Pa and the result from m⁻³ to cm⁻³.
Element-wise on arrays; no validation (radtrax.slab.number_density_cm3
adds it for scalar inputs).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
p_atm
|
Real[ArrayLike, '*#shape']
|
Pressure [atm]. |
required |
T_K
|
Real[ArrayLike, '*#shape']
|
Temperature [K]. |
required |
Returns:
| Type | Description |
|---|---|
Float[ArrayLike, '*#shape']
|
Number density [molecules / cm³], the broadcast shape of the inputs. |
Examples:
Source code in src/radtrax/atmosphere/gas.py
air_mass_factor
¶
air_mass_factor(sza_deg: Real[ArrayLike, '*#shape'], vza_deg: Real[ArrayLike, '*#shape']) -> Float[ArrayLike, '*#shape']
Plane-parallel two-way air-mass factor, sun → surface → sensor.
with \(\theta_0\) the solar and \(\theta\) the viewing zenith angle. The plane-parallel form breaks down past about 75°, where a spherical (Chapman) correction is needed; that arrives with v1 of the roadmap.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sza_deg
|
Real[ArrayLike, '*#shape']
|
Solar zenith angle [degrees]. |
required |
vza_deg
|
Real[ArrayLike, '*#shape']
|
Viewing zenith angle [degrees]. |
required |
Returns:
| Type | Description |
|---|---|
Float[ArrayLike, '*#shape']
|
The two-way air-mass factor (dimensionless). |
Examples:
Source code in src/radtrax/atmosphere/geometry.py
interpolate_cross_section
¶
interpolate_cross_section(ds: Dataset, T_K: float, p_atm: float, *, var: str = 'absorption_cross_section') -> DataArray
Return σ(ν) at a given (T, p) via bilinear LUT interpolation.
The LUT dataset must carry temperature [K] and pressure [atm]
coords as in build_lut_dataset. No extrapolation — callers should
clip to the LUT range before interpolating.
Source code in src/radtrax/spectroscopy/tables.py
absorption_coefficient
¶
absorption_coefficient(sigma: DataArray | Float[ndarray, '*shape'], *, vmr: float, p_atm: float, T_K: float) -> DataArray | Float[ndarray, '*shape']
α(ν) = σ(ν) · N_gas [1/cm].
Source code in src/radtrax/spectroscopy/tables.py
transmittance
¶
transmittance(alpha: DataArray | Float[ndarray, '*shape'], *, l_vert_cm: float, sza_deg: float, vza_deg: float) -> DataArray | Float[ndarray, '*shape']
τ(ν) = exp(-α · L_vert · AMF).
Source code in src/radtrax/spectroscopy/tables.py
beers_law_from_lut
¶
beers_law_from_lut(ds: Dataset, *, vmr: float, T_K: float, p_atm: float, l_vert_cm: float, sza_deg: float, vza_deg: float, var: str = 'absorption_cross_section') -> DataArray
End-to-end: σ-from-LUT → α → τ(ν) for a single homogeneous layer.
Returns an xarray.DataArray carrying the wavenumber + wavelength
coords so callers can plot τ(λ) without re-introducing units.
Source code in src/radtrax/spectroscopy/tables.py
plume_ratio_spectrum
¶
plume_ratio_spectrum(ds: Dataset, *, vmr_background: float, vmr_total: float, T_K: float, p_atm: float, l_vert_cm: float, sza_deg: float, vza_deg: float, var: str = 'absorption_cross_section') -> DataArray
Differential-Beer-Lambert ratio τ_total(ν) / τ_bg(ν).
Callers typically pass vmr_total = vmr_background * (1 + enhancement)
where enhancement is the fitting parameter in the plume retrieval.
Source code in src/radtrax/spectroscopy/tables.py
HITRAN table building¶
Needs the [hitran] extra; HAPI is imported inside the builders.
hitran
¶
Offline HITRAN cross-section table building through HAPI.
Submodules
config—GasConfig/LUTGridConfig/ATMOSPHERIC_GASES.generator— single-gas fetch / compute / wrap / save pipeline.multi— multi-gas orchestration (separate-per-gas + combined).build— v0's tables into the cache, and its command line,python -m radtrax.spectroscopy.hitran GAS [GAS ...](radtrax#34).
HAPI (hitran-api, the [hitran] extra) is imported lazily inside the
generator and multi pipelines: importing this subpackage does not need HAPI,
only building or fetching a table does. Interpolating a built table is
radtrax.spectroscopy.tables.
Ported from plumax 0.1.2 at 28f257e (plumax.hapi_lut).
config¶
Gas + grid configuration for HAPI absorption-cross-section look-up tables.
A LUT is an N-dimensional array of absorption cross-sections
σ(ν, T, P) = Σ_l S_l(T) f_V(ν − ν_l; Γ_D, Γ_L) · 1[|ν − ν_l| ≤ w] [cm² / molecule]
computed line-by-line via HAPI's Voigt routine, over the lines l of every
isotopologue of the gas, each cut at w = ±25 cm⁻¹ from its centre, the
convention continua are defined against (Korkin et al. 2025, eqs. 16–18).
HITRAN's intensities S_l are already weighted by each isotopologue's natural
abundance, so the sum needs no rescaling. GasConfig identifies the
HITRAN line set (molecule + isotopologues + spectral range) and
LUTGridConfig describes the common (T, P, ν-step, cut-off) grid used
across gases.
The ATMOSPHERIC_GASES registry covers the six species most commonly needed
for SWIR/NIR remote sensing (CH₄, CO₂, H₂O, O₂, N₂O, CO). Add entries for
other HITRAN species by constructing a GasConfig directly.
Ported from plumax 0.1.2 at 28f257e (src/plumax/hapi_lut/config.py); every
isotopologue and the ±25 cm⁻¹ cut-off since radtrax#172.
GasConfig
dataclass
¶
GasConfig(name: str, molecule_id: int, nu_min: float, nu_max: float, description: str = '', *, isotopologue_ids: tuple[int, ...] | None = None, isotopologue_id: int | None = None)
Configuration for a single HITRAN gas species.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
Short name used as HAPI SourceTable id (e.g. 'CH4'). |
molecule_id |
int
|
HITRAN molecule id (see |
nu_min |
float
|
Lower wavenumber bound [cm⁻¹]. |
nu_max |
float
|
Upper wavenumber bound [cm⁻¹]. |
description |
str
|
Free-text description for dataset metadata. |
isotopologue_ids |
tuple[int, ...] | None
|
HITRAN global isotopologue ids (the |
isotopologue_id |
int | None
|
Deprecated: one local isotopologue number
(1 = most abundant); use |
Raises:
| Type | Description |
|---|---|
ParameterError
|
If both |
resolve_isotopologue_ids
¶
The HITRAN global isotopologue ids to fetch (needs [hitran]).
isotopologue_ids as given; else the deprecated local
isotopologue_id mapped through hapi.ISO; else every global id
whose hapi.ISO entry has this molecule_id, in HITRAN's
isotopologue order.
Raises:
| Type | Description |
|---|---|
MissingExtraError
|
If the ids must come from |
KeyError
|
If |
Source code in src/radtrax/spectroscopy/hitran/config.py
LUTGridConfig
dataclass
¶
LUTGridConfig(T_grid: ndarray = (lambda: array([200, 220, 240, 260, 280, 300, 320], dtype=float))(), P_grid: ndarray = (lambda: array([0.1, 0.2, 0.4, 0.6, 0.8, 1.0], dtype=float))(), nu_step: float = 0.01, diluent_composition: dict[str, float] | None = None, wing_cm: float = 25.0, wing_halfwidths: float = 0.0)
Grid configuration shared across gases for a single LUT build.
Attributes:
| Name | Type | Description |
|---|---|---|
T_grid |
ndarray
|
Temperature knots [K]. Should bracket the atmospheric range of interest (upper-troposphere ~200 K to surface ~320 K). |
P_grid |
ndarray
|
Pressure knots [atm]. Should bracket the altitude range of interest (upper-tropo ~0.1 atm to surface ~1.0 atm). |
nu_step |
float
|
Spectral step [cm⁻¹] on the output wavenumber grid. HITRAN lines have HWHMs down to ~0.005 cm⁻¹ at low pressure — step ≤ 0.01 cm⁻¹ for hyperspectral work, 0.05 cm⁻¹ for tutorials. |
diluent_composition |
dict[str, float] | None
|
Optional override for the air diluent fractions. If None, each gas gets {'air': 1-VMR, 'self': VMR}. |
wing_cm |
float
|
Absolute line cut-off w [cm⁻¹]: each line is evaluated
within ±w of its centre (HAPI's |
wing_halfwidths |
float
|
Relative cut-off in half-widths max(Γ_L, Γ_D)
(HAPI's |
get_diluent_for_gas
¶
Return the HAPI Diluent dict for a given gas.
If an explicit diluent_composition override was supplied at
construction time it is returned verbatim; otherwise a standard
{'air': 1 - VMR, 'self': VMR} split is built from
DEFAULT_VMR_NOMINAL[gas_name] (falling back to 1e-6).
Source code in src/radtrax/spectroscopy/hitran/config.py
generator¶
Single-gas absorption-cross-section LUT builder.
Given a GasConfig and a LUTGridConfig this module fetches HITRAN line
data (cached under a user-provided directory), evaluates a Voigt-profile
line-by-line sum on every (T, P) grid cell, and wraps the result as a
CF-conformant xarray.Dataset ready for NetCDF persistence.
HAPI is imported lazily inside the functions that need it so import
radtrax.spectroscopy.hitran does not require hitran-api to be installed.
Ported from plumax 0.1.2 at 28f257e (src/plumax/hapi_lut/generator.py);
every isotopologue (fetch_by_ids) and the ±25 cm⁻¹ cut-off since
radtrax#172.
default_cache_dir
¶
Return the HITRAN line-data cache directory.
Resolution order
$HAPI_CACHE_DIRif set.~/.cache/radtrax/hitran— a per-user cache, so that invoking the generator from anywhere on disk does not leave unignored artefacts in the repository tree. Theprojects/plumax/data/hitran_cachepath used by the notebooks is always passed explicitly.
Source code in src/radtrax/spectroscopy/hitran/generator.py
fetch_hitran_data
¶
fetch_hitran_data(gas_config: GasConfig, cache_dir: Path | str | None = None) -> Path
Fetch HITRAN line parameters for gas_config into cache_dir.
Fetches the lines of :meth:GasConfig.resolve_isotopologue_ids
<radtrax.spectroscopy.hitran.GasConfig.resolve_isotopologue_ids>, every
isotopologue of the molecule by default, with HAPI's fetch_by_ids.
HAPI writes a <GasConfig.name>.data + .header pair under
cache_dir, overwriting any existing pair (HAPI does not no-op on
a cache hit). A successful fetch also records the fetched ν range and
isotopologue ids in a <name>.range.json sidecar.
When the fetch fails (no network, API error) the previously-cached pair is
only reused if its recorded range covers the requested
[nu_min, nu_max] and it holds the same isotopologues — otherwise a
narrowed cache (e.g. left by
:func:radtrax.spectroscopy.hitran.multi.create_combined_lut, which
fetches over the gas intersection range) or one isotopologue's lines would
silently serve a truncated line list.
Raises:
| Type | Description |
|---|---|
RuntimeError
|
if the fetch raises and either no cached pair is present or its recorded range or isotopologues do not match the request. The underlying HAPI exception is chained so the network / API error is visible. |
Returns:
| Type | Description |
|---|---|
Path
|
The resolved cache directory. |
Source code in src/radtrax/spectroscopy/hitran/generator.py
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 | |
compute_absorption_lut
¶
compute_absorption_lut(gas_config: GasConfig, grid_config: LUTGridConfig, *, cache_dir: Path | str | None = None, progress: bool = True) -> tuple[Float[ndarray, ' G'], Float[ndarray, 'G T P'], Float[ndarray, ' G']]
Evaluate σ(ν, T, P) on the full LUT grid.
σ(ν) = Σ_l S_l(T) f_V(ν − ν_l) · 1[|ν − ν_l| ≤ w] over every line in
the fetched table, each cut at w = max(wing_cm, wing_halfwidths ·
max(Γ_L, Γ_D)) from its centre (HAPI's rule; ±25 cm⁻¹ by default).
Assumes fetch_hitran_data has already populated the cache, over the
grid plus w so lines just outside it add their wings. Fails
fast — a HAPI failure at any (T, P) knot, or any non-finite
cross-section in the result, raises rather than leaving silent NaN
holes in the LUT.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
gas_config
|
GasConfig
|
Target gas (molecule + isotopologues + ν range). |
required |
grid_config
|
LUTGridConfig
|
Common T/P/ν-step grid and line cut-off. |
required |
cache_dir
|
Path | str | None
|
Where the fetched HITRAN |
None
|
progress
|
bool
|
Emit per-(T, P) log lines at INFO. |
True
|
Raises:
| Type | Description |
|---|---|
RuntimeError
|
if |
Returns:
| Type | Description |
|---|---|
Float[ndarray, ' G']
|
|
Float[ndarray, 'G T P']
|
|
Float[ndarray, ' G']
|
|
tuple[Float[ndarray, ' G'], Float[ndarray, 'G T P'], Float[ndarray, ' G']]
|
|
Source code in src/radtrax/spectroscopy/hitran/generator.py
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 | |
build_lut_dataset
¶
build_lut_dataset(gas_config: GasConfig, grid_config: LUTGridConfig, nu_grid: Float[ndarray, ' G'], sigma_lut: Float[ndarray, 'G T P'], wavelength_nm: Float[ndarray, ' G']) -> Dataset
Wrap the raw LUT arrays in a CF-conformant xarray.Dataset.
The attributes record the line set and its cut-off: isotopologue_ids
(HITRAN global ids, from
:meth:~radtrax.spectroscopy.hitran.GasConfig.resolve_isotopologue_ids,
so it needs [hitran]), line_wing_cm and line_wing_halfwidths.
Source code in src/radtrax/spectroscopy/hitran/generator.py
328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 | |
save_lut
¶
save_lut(ds: Dataset, output_dir: Path | str, gas_name: str, *, filename: str | None = None) -> Path
Persist ds to <output_dir>/<filename> (zlib, σ as float32).
filename defaults to <gas_name>_absorption_lut.nc. The table is
written to a temporary file in output_dir and renamed into place, so
a crashed or concurrent write never leaves a half table under the name.
Source code in src/radtrax/spectroscopy/hitran/generator.py
generate_single_gas_lut
¶
generate_single_gas_lut(gas_config: GasConfig, grid_config: LUTGridConfig | None = None, *, cache_dir: Path | str | None = None, output_dir: Path | str | None = None, save: bool = False) -> Dataset
End-to-end single-gas pipeline: fetch → compute → wrap → (optionally) save.
Source code in src/radtrax/spectroscopy/hitran/generator.py
build¶
Build v0's cross-section tables into the cache (the [hitran] extra).
:func:build_table runs the existing pipeline
(:func:~radtrax.spectroscopy.hitran.generate_single_gas_lut: fetch the
lines from HITRAN, compute σ with HAPI, wrap them) on a
:class:~radtrax.spectroscopy.table_cache.TableGrid, over the grid's
wavenumber range for every gas so their tables share one grid, and writes
the result where :func:~radtrax.spectroscopy.table_cache.load_table looks.
python -m radtrax.spectroscopy.hitran GAS [GAS ...] is its command line.
gas_config_for
¶
gas's HITRAN configuration over grid's wavenumber range.
Every gas is built on the grid's range, not its own, so the tables share
one wavenumber grid (:class:~radtrax.spectroscopy.TableOpacity needs it).
Raises:
| Type | Description |
|---|---|
KeyError
|
If |
Source code in src/radtrax/spectroscopy/hitran/build.py
fetch_lines
¶
fetch_lines(gas: str, grid: TableGrid = V0_GRID, *, line_cache_dir: Path | str | None = None) -> None
Download gas's HITRAN lines over the grid plus a margin ([hitran]).
Source code in src/radtrax/spectroscopy/hitran/build.py
build_table
¶
build_table(gas: str, grid: TableGrid = V0_GRID, *, hitran_version: str = HITRAN_VERSION, cache_dir: Path | str | None = None, line_cache_dir: Path | str | None = None, overwrite: bool = False) -> Path
Build gas's table on grid and write it to the table cache.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
gas
|
str
|
A gas of :data: |
required |
grid
|
TableGrid
|
The table grid. |
V0_GRID
|
hitran_version
|
str
|
Recorded in the file and its cache key. |
HITRAN_VERSION
|
cache_dir
|
Path | str | None
|
Table cache directory (default
:func: |
None
|
line_cache_dir
|
Path | str | None
|
Where HAPI keeps the downloaded line lists (default
:func: |
None
|
overwrite
|
bool
|
Rebuild a table that already exists. |
False
|
Returns:
| Type | Description |
|---|---|
Path
|
The table's path. |
Raises:
| Type | Description |
|---|---|
KeyError
|
If |
ValueError
|
If the line cut-off exceeds :data: |
ImportError
|
Without the |
Source code in src/radtrax/spectroscopy/hitran/build.py
main
¶
python -m radtrax.spectroscopy.hitran GAS [GAS ...] [--grid v0].
Source code in src/radtrax/spectroscopy/hitran/build.py
multi¶
Multi-gas LUT orchestration — separate-per-gas and combined builds.
Ported from plumax 0.1.2 at 28f257e (src/plumax/hapi_lut/multi.py); only import paths changed.
create_multi_gas_luts
¶
create_multi_gas_luts(gases: list[str], grid_config: LUTGridConfig | None = None, *, output_dir: Path | str, cache_dir: Path | str | None = None, force_recompute: bool = False) -> dict[str, Path]
Build one separate <gas>_absorption_lut.nc per gas.
Gases are looked up in ATMOSPHERIC_GASES. Unknown names are skipped
with a warning rather than raising — lets users pass a superset list.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
gases
|
list[str]
|
List of gas names (e.g. |
required |
grid_config
|
LUTGridConfig | None
|
Common grid; defaults to |
None
|
output_dir
|
Path | str
|
Required. Where to write NetCDF LUTs. No default
is provided to avoid dropping unignored artefacts
into the current working directory; callers should
point this at the project-local git-ignored
|
required |
cache_dir
|
Path | str | None
|
HITRAN cache dir; defaults to |
None
|
force_recompute
|
bool
|
If False, skip gases whose NetCDF already exists. |
False
|
Returns:
| Type | Description |
|---|---|
dict[str, Path]
|
Mapping |
Source code in src/radtrax/spectroscopy/hitran/multi.py
create_combined_lut
¶
create_combined_lut(gases: list[str], grid_config: LUTGridConfig | None = None, *, output_dir: Path | str, cache_dir: Path | str | None = None, filename: str = 'combined_atmospheric_lut.nc') -> Path
Build a single NetCDF with one cross-section variable per gas.
The shared wavenumber axis is the intersection of the per-gas ranges:
[max(nu_min), min(nu_max)]. All variables therefore live on one
(wavenumber, temperature, pressure) coordinate system — convenient
for matched-filter retrievals where CH₄ and CO₂ share the same SWIR window.
output_dir has no default for the same reason as
:func:create_multi_gas_luts — forcing an explicit path keeps
generated artefacts out of the current working directory.
Source code in src/radtrax/spectroscopy/hitran/multi.py
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | |