Source code for pyradtran.channels

"""
Instrument-channel convolution and brightness temperature.

Convolve spectral pyRadtran results with instrument spectral response
functions (SRFs) and convert thermal radiances to brightness temperatures.

Examples
--------
>>> channel_ds = convolve_channels(result_ds, srf)
>>> tb = brightness_temperature(channel_ds["uu"], 10500.0)

See Also
--------
pyradtran.interface.PyRadtranAccessor.run : ``channels=`` integration.
"""

import logging

import numpy as np
import xarray as xr

logger = logging.getLogger(__name__)

# CODATA 2018
_H = 6.62607015e-34  # J s
_C = 2.99792458e8  # m s-1
_KB = 1.380649e-23  # J K-1

#: radiance unit -> factor converting to W m-2 m-1 sr-1 (SI, per metre)
_UNIT_TO_SI = {
    "mW m-2 nm-1 sr-1": 1e-3 * 1e9,
    "W m-2 nm-1 sr-1": 1e9,
}


[docs] def convolve_channels( result: xr.Dataset, srf: xr.DataArray, keep_spectral: bool = False, ) -> xr.Dataset: """SRF-average all spectral variables onto a ``channel`` dimension. Parameters ---------- result : xarray.Dataset Spectral simulation result with a ``wavelength`` dimension (nm). srf : xarray.DataArray Spectral response, dims ``(channel, wavelength)``. Interpolated onto the result's wavelength grid; values outside the SRF grid are treated as zero response. keep_spectral : bool, default ``False`` Keep the original spectral variables under ``<name>_spectral``. Returns ------- xarray.Dataset Channel-space dataset; non-spectral variables pass through. """ if "wavelength" not in result.dims: raise ValueError("result has no 'wavelength' dimension to convolve") wl = result["wavelength"] phi = _interp_srf(srf, wl) norm = phi.integrate("wavelength") norm_vals = np.asarray(norm.values, dtype=float) bad = ~np.isfinite(norm_vals) | (norm_vals <= 0) if np.any(bad): bad_channels = [str(c) for c in np.asarray(srf["channel"].values)[bad]] raise ValueError( f"SRF for channel(s) {bad_channels} has no overlap with the " f"result wavelength grid " f"({float(wl.min())}{float(wl.max())} nm); convolution would " f"divide by zero" ) out = xr.Dataset(attrs=dict(result.attrs)) for name, da in result.data_vars.items(): if "wavelength" in da.dims: conv = (da * phi).integrate("wavelength") / norm out[name] = conv if keep_spectral: out[f"{name}_spectral"] = da else: out[name] = da out["channel"] = srf["channel"] return out
def _interp_srf(srf: xr.DataArray, wl: xr.DataArray) -> xr.DataArray: """Interpolate an SRF onto the result wavelength grid (numpy only). Zero response outside the SRF's own wavelength grid. """ srf = srf.sortby("wavelength") srf_wl = srf["wavelength"].values.astype(float) target = wl.values.astype(float) phi_rows = [ np.interp( target, srf_wl, srf.isel(channel=i).values.astype(float), left=0.0, right=0.0, ) for i in range(srf.sizes["channel"]) ] return xr.DataArray( np.stack(phi_rows), dims=("channel", "wavelength"), coords={"channel": srf["channel"].values, "wavelength": target}, )
[docs] def brightness_temperature( radiance, wavelength_nm, radiance_units: str = "mW m-2 nm-1 sr-1", ): """Convert monochromatic radiance to brightness temperature (K). Inverse Planck: ``T = c2 / (lambda * ln(1 + c1 / (lambda^5 * L)))`` with ``c1 = 2 h c^2`` and ``c2 = h c / k``. Parameters ---------- radiance : array-like or xarray.DataArray Spectral radiance. wavelength_nm : float or array-like Wavelength in nanometres. radiance_units : str, default ``"mW m-2 nm-1 sr-1"`` Units of *radiance* (uvspec's default radiance unit). Returns ------- Brightness temperature in K, same shape as *radiance*. """ if radiance_units not in _UNIT_TO_SI: raise ValueError( f"Unsupported radiance_units '{radiance_units}'; " f"expected one of {sorted(_UNIT_TO_SI)}" ) L_si = np.asarray(radiance, dtype=float) * _UNIT_TO_SI[radiance_units] lam = np.asarray(wavelength_nm, dtype=float) * 1e-9 c1 = 2 * _H * _C**2 c2 = _H * _C / _KB return c2 / (lam * np.log1p(c1 / (lam**5 * L_si)))
__all__ = ["convolve_channels", "brightness_temperature"]