Skip to content

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, and geometry: 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], (L,).

t_k Real[Array, ' L']

Layer temperature [K], (L,).

z_m Real[Array, ' L']

Altitude of the layer's middle [m], (L,).

dz_m Real[Array, ' L']

Layer thickness [m], (L,).

vmr dict[str, Float[Array, ' L']]

Volume mixing ratio [mol/mol] per gas, each (L,), keyed by gas name ("CH4", "H2O", …).

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'

n_layers property

n_layers: int

Number of layers L.

number_density

number_density() -> Float[Array, ' L']

Air number density per layer [molecules/cm³], n = p / (k_B T).

Source code in src/radtrax/atmosphere/state.py
def number_density(self) -> Float[Array, " L"]:
    """Air number density per layer [molecules/cm³], ``n = p / (k_B T)``."""
    return jnp.asarray(number_density(self.p_pa / ATM_TO_PA, self.t_k))

air_column

air_column() -> Float[Array, ' L']

Air column per layer [molecules/cm²], n Δz.

Source code in src/radtrax/atmosphere/state.py
def air_column(self) -> Float[Array, " L"]:
    """Air column per layer [molecules/cm²], ``n Δz``."""
    return self.number_density() * self.dz_m * _CM_PER_M

column

column(gas: str) -> Float[Array, '']

Total column of gas [molecules/cm²], Σ_l n_l x_l Δz_l.

Raises:

Type Description
KeyError

If the state carries no gas.

Source code in src/radtrax/atmosphere/state.py
def column(self, gas: str) -> Float[Array, ""]:
    """Total column of ``gas`` [molecules/cm²], ``Σ_l n_l x_l Δz_l``.

    Raises:
        KeyError: If the state carries no ``gas``.
    """
    return jnp.sum(self.air_column() * self.vmr[gas])

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 ΔX [mol/m²].

required
profile Float[ArrayLike, ' L'] | None

Relative weight per layer, (L,); the default puts all of ΔX in the bottom layer.

None

Raises:

Type Description
KeyError

If the state carries no gas.

ValueError

If profile is not (L,).

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
def add_column(
    self,
    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.

    Args:
        gas: The gas, which the state must carry.
        delta_column_mol_m2: Column enhancement ``ΔX`` [mol/m²].
        profile: Relative weight per layer, ``(L,)``; the default puts
            all of ``ΔX`` in the bottom layer.

    Raises:
        KeyError: If the state carries no ``gas``.
        ValueError: If ``profile`` is not ``(L,)``.

    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
    """
    x = self.vmr[gas]
    if profile is None:
        weights = jnp.zeros_like(x).at[0].set(1.0)
    else:
        weights = jnp.asarray(profile, dtype=x.dtype)
        if weights.shape != x.shape:
            raise ShapeError(
                f"AtmosphericState.add_column: profile must be (L,) = "
                f"{x.shape}, got {weights.shape}."
            )
        weights = weights / jnp.sum(weights)
    # Air column per layer [mol/m²]: n Δz [molecules/cm²] × 1e4 / N_A.
    air_mol_m2 = self.air_column() * CM2_PER_M2 / AVOGADRO_PER_MOL
    return self.with_vmr(gas, x + weights * delta_column_mol_m2 / air_mol_m2)

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
def with_vmr(self, gas: str, vmr: float | Float[Array, " L"]) -> AtmosphericState:
    """A copy with ``gas`` set to ``vmr`` [mol/mol] (a scalar fills every layer)."""
    x = jnp.asarray(vmr)
    value = jnp.broadcast_to(
        x.astype(jnp.result_type(x, self.p_pa, float)), self.p_pa.shape
    )
    return AtmosphericState(
        p_pa=self.p_pa,
        t_k=self.t_k,
        z_m=self.z_m,
        dz_m=self.dz_m,
        vmr={**self.vmr, gas: value},
    )

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, (L + 1,).

required
t_levels_k Real[ArrayLike, ' L1']

Level temperatures [K], (L + 1,).

required
vmr_levels dict[str, Float[Array, ' L1']]

Level mixing ratios [mol/mol] per gas, each (L + 1,).

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
def 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.

    Args:
        p_levels_pa: Level pressures [Pa], surface first, strictly decreasing,
            ``(L + 1,)``.
        t_levels_k: Level temperatures [K], ``(L + 1,)``.
        vmr_levels: Level mixing ratios [mol/mol] per gas, each ``(L + 1,)``.
        z_surface_m: Altitude of the lowest level [m].

    Returns:
        The layers: log-mean pressure, mean temperature and mixing ratios,
        hydrostatic thickness, and the altitude of each layer's middle.

    Raises:
        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'
    """
    p = jnp.asarray(p_levels_pa)
    p = p.astype(jnp.result_type(p, float))
    t = jnp.asarray(t_levels_k, dtype=p.dtype)
    if p.ndim != 1 or p.shape[0] < 2:
        raise ShapeError(
            f"layers_from_levels: need (L + 1,) levels with L ≥ 1, got {p.shape}."
        )
    for name, value in {"t_levels_k": t, **vmr_levels}.items():
        if jnp.shape(value) != p.shape:
            raise ShapeError(
                f"layers_from_levels: {name} has shape {jnp.shape(value)}, "
                f"p_levels_pa has {p.shape}."
            )
    _check_decreasing(p)
    p = eqx.error_if(
        p,
        jnp.any(p[1:] >= p[:-1]) | jnp.any(p <= 0),
        "layers_from_levels: level pressures must be positive and strictly "
        "decreasing upwards (surface first).",
    )
    p_bot, p_top = p[:-1], p[1:]
    gap = (p_bot - p_top) / p_top  # u, relative gap
    log_ratio = jnp.log1p(gap)
    # Log-mean pressure, double-where around the 0/0 at u = 0.
    small = gap < _SERIES_GAP
    safe_log = jnp.where(small, 1.0, log_ratio)
    p_mean = jnp.where(
        small,
        p_top * (1.0 + gap / 2 - gap**2 / 12),
        (p_bot - p_top) / safe_log,
    )
    t_mean = 0.5 * (t[:-1] + t[1:])
    m_air = DRY_AIR_MOLAR_MASS_KG_PER_MOL / AVOGADRO_PER_MOL
    dz = BOLTZMANN_J_PER_K * t_mean / (m_air * STANDARD_GRAVITY_M_PER_S2) * log_ratio
    z_bottom = z_surface_m + jnp.concatenate(
        [jnp.zeros(1, dz.dtype), jnp.cumsum(dz)[:-1]]
    )
    return AtmosphericState(
        p_pa=p_mean,
        t_k=t_mean,
        z_m=z_bottom + 0.5 * dz,
        dz_m=dz,
        vmr={
            gas: 0.5
            * (jnp.asarray(x, dtype=p.dtype)[:-1] + jnp.asarray(x, dtype=p.dtype)[1:])
            for gas, x in vmr_levels.items()
        },
    )

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.

AFGL_GASES

Returns:

Type Description
AtmosphericState

The state from :func:~radtrax.atmosphere.layers_from_levels:

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
def 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.

    Args:
        z_top_km: 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).
        gases: The gases to carry, from :data:`AFGL_GASES`.

    Returns:
        The state from :func:`~radtrax.atmosphere.layers_from_levels`:
        hydrostatic thicknesses, log-mean layer pressures, mean temperatures
        and mixing ratios [mol/mol].

    Raises:
        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'
    """
    unknown = sorted(set(gases) - set(AFGL_GASES))
    if unknown:
        raise ParameterError(
            f"afgl_us_standard: no {unknown} in the table {AFGL_GASES}."
        )
    table = _read_table()
    keep = table["z_km"] <= z_top_km
    if keep.sum() < 2:
        raise GridError(
            f"afgl_us_standard: z_top_km={z_top_km} keeps fewer than two levels."
        )
    return layers_from_levels(
        jnp.asarray(table["p_mb"][keep] * _HPA_TO_PA),
        jnp.asarray(table["t_k"][keep]),
        {gas: jnp.asarray(table[gas][keep] * _PPMV) for gas in gases},
        z_surface_m=float(table["z_km"][0]) * 1e3,
    )

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³].

\[ n = \frac{p}{k_B T} \]

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:

>>> f"{number_density(1.0, 296.0):.4e}"
'2.4794e+19'
Source code in src/radtrax/atmosphere/gas.py
def 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³].

    $$
    n = \\frac{p}{k_B T}
    $$

    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).

    Args:
        p_atm: Pressure [atm].
        T_K: Temperature [K].

    Returns:
        Number density [molecules / cm³], the broadcast shape of the inputs.

    Examples:
        >>> f"{number_density(1.0, 296.0):.4e}"
        '2.4794e+19'
    """
    p_pa = p_atm * ATM_TO_PA
    n_per_m3 = p_pa / (BOLTZMANN_J_PER_K * T_K)
    return n_per_m3 / CM3_PER_M3

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 θ):

\[ m(\theta) = \frac{\sqrt{(R + z_t)^2 - R^2\sin^2\theta} - \sqrt{(R + z_b)^2 - R^2\sin^2\theta}}{z_t - z_b} = \frac{2R + z_t + z_b}{\sqrt{R^2\mu^2 + 2Rz_t + z_t^2} + \sqrt{R^2\mu^2 + 2Rz_b + z_b^2}}, \]

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:

>>> geometry = ViewingGeometry.from_angles(60.0, 0.0)
>>> round(float(geometry.air_mass()), 12)
3.0
Source code in src/radtrax/atmosphere/geometry.py
def __init__(
    self,
    mu0: Real[ArrayLike, ""],
    mu: Real[ArrayLike, ""],
    relative_azimuth_rad: Real[ArrayLike, ""] = 0.0,
) -> None:
    self.mu0 = _cosine("mu0", mu0)
    self.mu = _cosine("mu", mu)
    self.relative_azimuth_rad = jnp.asarray(relative_azimuth_rad, dtype=float)

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
@classmethod
def from_angles(
    cls,
    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]."""
    return cls(
        jnp.cos(jnp.deg2rad(sza_deg)),
        jnp.cos(jnp.deg2rad(vza_deg)),
        jnp.deg2rad(raa_deg),
    )

air_mass

air_mass() -> Float[Array, '']

Plane-parallel two-way air-mass factor m = 1/μ₀ + 1/μ.

Source code in src/radtrax/atmosphere/geometry.py
def air_mass(self) -> Float[Array, ""]:
    """Plane-parallel two-way air-mass factor ``m = 1/μ₀ + 1/μ``."""
    return 1.0 / self.mu0 + 1.0 / self.mu

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], (L,).

required
dz_m Float[ArrayLike, ' L']

Thickness of each layer [m], (L,).

required
earth_radius_m float

Radius of the sphere through the surface [m]; the default is the IUGG mean Earth radius (geonnax.geo.EARTH_MEAN_RADIUS_M).

EARTH_MEAN_RADIUS_M

Returns:

Type Description
Float[Array, ' L']

The layer air masses, (L,); each tends to :meth:air_mass

Float[Array, ' L']

as z/R → 0.

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
def layer_air_mass(
    self,
    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(θ)``.

    Args:
        z_m: Height of each layer's middle above the surface [m], ``(L,)``.
        dz_m: Thickness of each layer [m], ``(L,)``.
        earth_radius_m: Radius of the sphere through the surface [m]; the
            default is the IUGG mean Earth radius
            (``geonnax.geo.EARTH_MEAN_RADIUS_M``).

    Returns:
        The layer air masses, ``(L,)``; each tends to :meth:`air_mass`
        as ``z/R → 0``.

    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
    """
    z, dz = jnp.asarray(z_m, dtype=float), jnp.asarray(dz_m, dtype=float)
    z_b, z_t = z - dz / 2, z + dz / 2
    return _shell_air_mass(self.mu0, z_b, z_t, earth_radius_m) + _shell_air_mass(
        self.mu, z_b, z_t, earth_radius_m
    )

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.

\[ m = \frac{1}{\cos\theta_0} + \frac{1}{\cos\theta} \]

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:

>>> air_mass_factor(0.0, 0.0)
np.float64(2.0)
>>> round(float(air_mass_factor(60.0, 0.0)), 12)
3.0
Source code in src/radtrax/atmosphere/geometry.py
def 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.

    $$
    m = \\frac{1}{\\cos\\theta_0} + \\frac{1}{\\cos\\theta}
    $$

    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.

    Args:
        sza_deg: Solar zenith angle [degrees].
        vza_deg: Viewing zenith angle [degrees].

    Returns:
        The two-way air-mass factor (dimensionless).

    Examples:
        >>> air_mass_factor(0.0, 0.0)
        np.float64(2.0)
        >>> round(float(air_mass_factor(60.0, 0.0)), 12)
        3.0
    """
    sza = np.deg2rad(sza_deg)
    vza = np.deg2rad(vza_deg)
    return 1.0 / np.cos(sza) + 1.0 / np.cos(vza)