Source code for shachen.composite
"""Cloud-cleared composite clear-sky background (Miller et al. 2017, ยง3.2).
The emissivity-free alternative to :mod:`shachen.background`: stack the same
time-of-day scenes from the preceding ~14 days and, per pixel, keep the three
TIR brightness temperatures from the day with the warmest BT10.4 (clouds are
cold; the warmest day is taken as the clear-sky estimate). Because the
composite is built from real observations it carries the split-window
water-vapor depression the semianalytic (emissivity x Planck, no atmosphere)
background lacks: the ~1 K high bias that zeroes DT1/DT2 on transparent winter
plumes (see docs/deviations.md).
Spectral coherence: all three bands come from the same selected day, and a
day is a candidate at a pixel only where all three bands are finite there.
"""
from collections.abc import Sequence
import numpy as np
import xarray as xr
from shachen.constants import Band
#: The bands a composite scene must carry (the three DEBRA TIR windows).
COMPOSITE_BANDS: tuple[Band, ...] = (Band.TIR_86, Band.TIR_104, Band.TIR_123)
[docs]
def composite_background(scenes: Sequence[xr.Dataset]) -> xr.Dataset:
"""Clear-sky background signals from a stack of same-time-of-day scenes.
Each element of ``scenes`` must carry 2-D ``bt_tir_86``, ``bt_tir_104``,
``bt_tir_123`` (K) on one common grid; ValueError on an empty sequence, a
missing variable, or any shape mismatch. Per pixel, a scene is a
*candidate* only where all three bands are finite; the candidate with the
warmest ``bt_tir_104`` is selected (first wins on exact ties) and all
three bands are taken from it. Pixels with zero candidates are NaN.
Returns the :func:`shachen.background.background_signals` contract โ
``bt_bg_tir_86``, ``bt_bg_tir_104``, ``bt_bg_tir_123`` (units "K"),
``rsw_bg`` (= bt_bg_tir_123 - bt_bg_tir_104) and ``btd_bg``
(= bt_bg_tir_86 - bt_bg_tir_104) โ plus ``n_valid`` (per-pixel candidate
count, integer dtype) and ``attrs["n_scenes"] = len(scenes)``.
"""
if len(scenes) == 0:
raise ValueError("composite_background needs at least one scene")
# Gather each band as a (n_scenes, y, x) stack, validating each.
shape: tuple[int, ...] | None = None
stacks: dict[Band, np.ndarray] = {}
for band in COMPOSITE_BANDS:
name = f"bt_{band.value}"
layers = []
for i, scene in enumerate(scenes):
if name not in scene:
raise ValueError(f"scene {i} is missing variable {name!r}")
values = np.asarray(scene[name].values, dtype=float)
if shape is None:
shape = values.shape
elif values.shape != shape:
raise ValueError(
f"shape mismatch: scene {i} {name} {values.shape} vs expected {shape}"
)
layers.append(values)
stacks[band] = np.stack(layers, axis=0)
# A scene is a candidate at a pixel only where all three bands are finite.
candidate = np.logical_and.reduce([np.isfinite(stacks[band]) for band in COMPOSITE_BANDS])
n_valid = candidate.sum(axis=0)
any_candidate = n_valid > 0
# Warmest-BT10.4 candidate; argmax takes the first scene on exact ties.
# Non-candidates are masked to -inf so they can never win; all-masked
# columns fall back to index 0 and are NaN-ed via any_candidate below.
bt104_masked = np.where(candidate, stacks[Band.TIR_104], -np.inf)
selected = bt104_masked.argmax(axis=0)
out = xr.Dataset(attrs={"n_scenes": len(scenes)})
for band in COMPOSITE_BANDS:
picked = np.take_along_axis(stacks[band], selected[np.newaxis], axis=0)[0]
out[f"bt_bg_{band.value}"] = (
("y", "x"),
np.where(any_candidate, picked, np.nan),
)
out["rsw_bg"] = out["bt_bg_tir_123"] - out["bt_bg_tir_104"]
out["btd_bg"] = out["bt_bg_tir_86"] - out["bt_bg_tir_104"]
out["n_valid"] = (("y", "x"), n_valid)
for band in COMPOSITE_BANDS:
out[f"bt_bg_{band.value}"].attrs.update(
units="K",
long_name=f"clear-sky background brightness temperature ({band.value})",
)
out["rsw_bg"].attrs.update(
units="K", long_name="background split-window signal BT(12.3) - BT(10.4)"
)
out["btd_bg"].attrs.update(
units="K", long_name="background brightness temperature difference BT(8.6) - BT(10.4)"
)
out["n_valid"].attrs.update(long_name="number of candidate (all-bands-finite) scenes per pixel")
return out