Atmosphere¶
The layered atmosphere every forward model starts from: AtmosphericState
(state), layers from levels by hydrostatic balance (hydrostatics) and the
AFGL (1986) US Standard profile (afgl); and the ideal-gas constants and
number density (gas) and the air-mass factor and ViewingGeometry, with
its spherical-shell layer air mass (geometry), the one
implementation of each in radtrax (radtrax.spectroscopy.tables and
radtrax.slab.config re-export them).
atmosphere
¶
The atmospheric state and its physics.
state: :class:AtmosphericState, the layered input of every forward model (radtrax#32);hydrostatics: :func:layers_from_levels, layers from levels by hydrostatic balance;afgl: :func:afgl_us_standard, the AFGL (1986) US Standard profile;gas: the ideal-gas constants and number density, andgeometry: the air-mass factor, each the one implementation in radtrax (radtrax#27), and :class:ViewingGeometry, the geometry surfaces and solvers take (radtrax#35).
state¶
The layered atmospheric state: the input of every forward model.
AtmosphericState holds, per layer, the pressure, temperature, altitude
and thickness, and a volume mixing ratio per gas. Everything is a JAX array
leaf, so a forward model differentiates with respect to any of it, and the
gases are a dict keyed by name, so the gas list lives in the data rather
than in the code (radtrax · 3). Build one from levels with
:func:radtrax.atmosphere.layers_from_levels, or take the AFGL US Standard
profile, :func:radtrax.atmosphere.afgl_us_standard.
Shapes: L layers.
AtmosphericState
¶
Bases: Module
Pressure, temperature, altitude, thickness and gases, per layer.
Layers are ordered from the surface up. Every field is a leaf: choose
what is not differentiated with eqx.partition or
jax.lax.stop_gradient.
Attributes:
| Name | Type | Description |
|---|---|---|
p_pa |
Real[Array, ' L']
|
Layer pressure [Pa], |
t_k |
Real[Array, ' L']
|
Layer temperature [K], |
z_m |
Real[Array, ' L']
|
Altitude of the layer's middle [m], |
dz_m |
Real[Array, ' L']
|
Layer thickness [m], |
vmr |
dict[str, Float[Array, ' L']]
|
Volume mixing ratio [mol/mol] per gas, each |
Examples:
>>> import jax.numpy as jnp
>>> slab = AtmosphericState(
... p_pa=jnp.array([101325.0]),
... t_k=jnp.array([273.15]),
... z_m=jnp.array([500.0]),
... dz_m=jnp.array([1000.0]),
... vmr={"CH4": jnp.array([1.9e-6])},
... )
>>> f"{float(slab.column('CH4')):.4g}"
'5.105e+18'
number_density
¶
Air number density per layer [molecules/cm³], n = p / (k_B T).
air_column
¶
column
¶
Total column of gas [molecules/cm²], Σ_l n_l x_l Δz_l.
Raises:
| Type | Description |
|---|---|
KeyError
|
If the state carries no |
add_column
¶
add_column(gas: str, delta_column_mol_m2: Float[ArrayLike, ''], profile: Float[ArrayLike, ' L'] | None = None) -> AtmosphericState
A copy with ΔX [mol/m²] more gas, spread over the layers.
Layer l gains w_l ΔX with w = profile / Σ profile, so the
column grows by exactly ΔX:
Δx_l = w_l ΔX N_A / (n_l Δz_l) with the air column n_l Δz_l
in molecules/m². Differentiable in ΔX and profile; a plume's
unit-absorption target is ∂ ln y / ∂ΔX through this.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
gas
|
str
|
The gas, which the state must carry. |
required |
delta_column_mol_m2
|
Float[ArrayLike, '']
|
Column enhancement |
required |
profile
|
Float[ArrayLike, ' L'] | None
|
Relative weight per layer, |
None
|
Raises:
| Type | Description |
|---|---|
KeyError
|
If the state carries no |
ValueError
|
If |
Examples:
>>> import jax.numpy as jnp
>>> from radtrax.atmosphere import (
... AVOGADRO_PER_MOL,
... CM2_PER_M2,
... afgl_us_standard,
... )
>>> atm = afgl_us_standard(z_top_km=3.0, gases=["CH4"])
>>> plume = atm.add_column("CH4", 0.1)
>>> added = (plume.column("CH4") - atm.column("CH4")) * CM2_PER_M2
>>> round(float(added / AVOGADRO_PER_MOL), 4) # mol/m²
0.1
Source code in src/radtrax/atmosphere/state.py
with_vmr
¶
with_vmr(gas: str, vmr: float | Float[Array, ' L']) -> AtmosphericState
A copy with gas set to vmr [mol/mol] (a scalar fills every layer).
Source code in src/radtrax/atmosphere/state.py
hydrostatics¶
Layers from levels by hydrostatic balance.
A profile is given on L + 1 levels (pressure, temperature and mixing
ratios at layer boundaries, surface first); radiative transfer wants L
layers. Between levels k (bottom) and k + 1 (top), with T̄ the
mean of the two level temperatures and m_air the mass of an air
molecule, hydrostatic balance dp/dz = −p m_air g / (k_B T) gives the
thickness
.. math:: Δz = \frac{k_B \bar T}{m_{air} g_0} \ln\frac{p_k}{p_{k+1}} ,
and the layer pressure is the log-mean
.. math:: \bar p = \frac{p_k - p_{k+1}}{\ln(p_k / p_{k+1})} ,
the choice that makes the ideal-gas column n̄ Δz = p̄ Δz / (k_B T̄)
equal the hydrostatic one, (p_k − p_{k+1}) / (m_air g_0), exactly and
whatever the temperature. Mixing ratios are the mean of the two levels.
g_0 is constant (standard gravity), so the altitudes are geopotential
(short of geometric altitude by ~0.3 % per 20 km) and the column is
Δp / (m_air g_0): about 0.2 % below an integration with g(z) over the
US Standard profile, and surface gravity varies with latitude by ±0.27 %.
Inputs are cast to floating point (integer pressures would truncate the
means). Pressures must be positive and strictly decreasing: checked eagerly
with a GridError, and under jit / vmap with
eqx.error_if, which raises a RuntimeError at run time.
layers_from_levels
¶
layers_from_levels(p_levels_pa: Real[ArrayLike, ' L1'], t_levels_k: Real[ArrayLike, ' L1'], vmr_levels: dict[str, Float[Array, ' L1']], *, z_surface_m: float | Float[Array, ''] = 0.0) -> AtmosphericState
An :class:AtmosphericState of L layers from L + 1 levels.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
p_levels_pa
|
Real[ArrayLike, ' L1']
|
Level pressures [Pa], surface first, strictly decreasing,
|
required |
t_levels_k
|
Real[ArrayLike, ' L1']
|
Level temperatures [K], |
required |
vmr_levels
|
dict[str, Float[Array, ' L1']]
|
Level mixing ratios [mol/mol] per gas, each |
required |
z_surface_m
|
float | Float[Array, '']
|
Altitude of the lowest level [m]. |
0.0
|
Returns:
| Type | Description |
|---|---|
AtmosphericState
|
The layers: log-mean pressure, mean temperature and mixing ratios, |
AtmosphericState
|
hydrostatic thickness, and the altitude of each layer's middle. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the shapes disagree, there are fewer than two levels, or (for concrete inputs) the pressures are not strictly decreasing and positive. |
Examples:
>>> import jax.numpy as jnp
>>> atm = layers_from_levels(
... jnp.array([101325.0, 50662.5]), jnp.array([288.15, 255.0]), {}
... )
>>> f"{float(atm.dz_m[0]):.1f}"
'5510.2'
Source code in src/radtrax/atmosphere/hydrostatics.py
afgl¶
AFGL reference atmospheres (Anderson et al., 1986).
The US Standard profile of AFGL Atmospheric Constituent Profiles
(0–120 km), AFGL-TR-86-0110: 50 levels from 0 to 120 km with pressure,
temperature and the mixing ratios of H₂O, CO₂, O₃, N₂O, CO and CH₄. The
table ships with radtrax (atmosphere/data/afgl_us_standard.csv); its
header says where each column comes from. The other five AFGL atmospheres
can follow the same way.
afgl_us_standard
¶
afgl_us_standard(*, z_top_km: float = 120.0, gases: Sequence[str] = AFGL_GASES) -> AtmosphericState
The AFGL US Standard atmosphere as layers between its levels.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z_top_km
|
float
|
Highest level kept [km]; the layers run from the surface to the last table level at or below it (levels are 1 km apart to 25 km, 2.5 km to 50 km, 5 km above). |
120.0
|
gases
|
Sequence[str]
|
The gases to carry, from :data: |
AFGL_GASES
|
Returns:
| Type | Description |
|---|---|
AtmosphericState
|
The state from :func: |
AtmosphericState
|
hydrostatic thicknesses, log-mean layer pressures, mean temperatures |
AtmosphericState
|
and mixing ratios [mol/mol]. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If a gas is not in the table or fewer than two levels are kept. |
Examples:
>>> atm = afgl_us_standard(gases=["CH4"])
>>> atm.n_layers
49
>>> f"{float(atm.with_vmr('CH4', 1.9e-6).column('CH4')):.3g}"
'4.08e+19'
Source code in src/radtrax/atmosphere/afgl.py
gas¶
Ideal-gas constants and the total number density.
One home for the constants and the number density that plumax defined twice,
in hapi_lut.beers and radtran.config (radtrax#27).
radtrax.spectroscopy.tables and radtrax.slab.config re-export them.
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
geometry¶
When the spherical air mass matters: by how much the plane-parallel solar air mass 1/μ₀ exceeds the spherical-shell one of a 1 km layer (ViewingGeometry.layer_air_mass), by solar zenith angle and the layer's height. BeerLambert(spherical=True) uses the spherical one.
| SZA | z = 0.5 km | z = 5 km | z = 15 km |
|---|---|---|---|
| 60° | +0.02 % | +0.23 % | +0.70 % |
| 70° | +0.06 % | +0.59 % | +1.76 % |
| 75° | +0.11 % | +1.09 % | +3.22 % |
| 80° | +0.25 % | +2.49 % | +7.28 % |
| 85° | +1.01 % | +9.76 % | +27.00 % |
| 89° | +21.24 % | +147.69 % | +304.93 % |
Viewing geometry: the air-mass factor and the geometry a solver sees.
One home for the air-mass factor that plumax defined twice, in
hapi_lut.beers.air_mass_factor and radtran.config.ObservationGeometry
(radtrax#27). radtrax.spectroscopy.tables.air_mass_factor re-exports it,
and ObservationGeometry.air_mass_factor calls it. It is NumPy, in
degrees, for the single-slab models.
:class:ViewingGeometry is the same geometry for the layered forward model
(radtrax#35): JAX leaves (cosines and the relative azimuth), so a retrieval
differentiates through them, and :meth:ViewingGeometry.air_mass is the
air-mass factor on the gradient path. Surfaces and solvers take one
ViewingGeometry, so the azimuth the v1 kernel BRDFs need is already
there.
:meth:ViewingGeometry.layer_air_mass is the spherical-shell (Chapman)
air mass of each layer (radtrax#104): the straight slant path through the
shell between the layer's bottom and top, z_b and z_t, over its
thickness, one way at zenith angle θ (μ = cos θ):
written in the second form, which does not cancel. It tends to 1/μ as
z/R → 0 and stays finite at θ = 90° for a layer above the surface. It
ignores refraction.
ViewingGeometry
¶
ViewingGeometry(mu0: Real[ArrayLike, ''], mu: Real[ArrayLike, ''], relative_azimuth_rad: Real[ArrayLike, ''] = 0.0)
Bases: Module
Sun and sensor directions as seen from the surface.
Every field is a leaf, so a forward model differentiates through the
geometry. The cosines are checked on construction, also under jit
(eqx.error_if raises equinox.EquinoxRuntimeError): each must be
in (0, 1] (up to rounding), and NaN fails. The plane-parallel
geometry breaks down past about 75° (see :func:air_mass_factor).
Attributes:
| Name | Type | Description |
|---|---|---|
mu0 |
Float[Array, '']
|
Cosine of the solar zenith angle. |
mu |
Float[Array, '']
|
Cosine of the viewing zenith angle. |
relative_azimuth_rad |
Float[Array, '']
|
Sensor azimuth minus solar azimuth [rad]; no v0 surface uses it, kernel BRDFs (v1) will. |
Examples:
Source code in src/radtrax/atmosphere/geometry.py
from_angles
classmethod
¶
from_angles(sza_deg: Real[ArrayLike, ''], vza_deg: Real[ArrayLike, ''], raa_deg: Real[ArrayLike, ''] = 0.0) -> ViewingGeometry
From solar and viewing zenith angles and relative azimuth [degrees].
Source code in src/radtrax/atmosphere/geometry.py
air_mass
¶
layer_air_mass
¶
layer_air_mass(z_m: Float[ArrayLike, ' L'], dz_m: Float[ArrayLike, ' L'], *, earth_radius_m: float = EARTH_MEAN_RADIUS_M) -> Float[Array, ' L']
Spherical-shell two-way air mass of each layer, m(θ₀) + m(θ).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z_m
|
Float[ArrayLike, ' L']
|
Height of each layer's middle above the surface [m], |
required |
dz_m
|
Float[ArrayLike, ' L']
|
Thickness of each layer [m], |
required |
earth_radius_m
|
float
|
Radius of the sphere through the surface [m]; the
default is the IUGG mean Earth radius
( |
EARTH_MEAN_RADIUS_M
|
Returns:
| Type | Description |
|---|---|
Float[Array, ' L']
|
The layer air masses, |
Float[Array, ' L']
|
as |
Examples:
>>> import jax.numpy as jnp
>>> geometry = ViewingGeometry.from_angles(60.0, 0.0)
>>> m = geometry.layer_air_mass(jnp.array([500.0]), jnp.array([1000.0]))
>>> round(float(m[0]), 4) # just under the plane-parallel 3
2.9995
Source code in src/radtrax/atmosphere/geometry.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: