Constants

Every tuning bound, offset and weight in one place.

All frozen numbers in one place: DEBRA’s tuning constants and the classic Dust RGB recipe.

Every (MIN, MAX) scaling bound, offset, and weight from Miller et al. (2017), JGR Atmospheres, doi:10.1002/2017JD027365, using the 26 Feb 2020 erratum versions of Eqs. 7, 21, 22, 24, 25. The paper notes “minor retuning” may be needed per sensor; retune here, nowhere else.

Sign/orientation conventions follow the printed equations exactly.

The sensor facts here (Band and the channel maps) are not from the paper; they are what each instrument calls the roles the algorithms read.

class shachen.constants.Bounds[source]

Bases: object

(MIN, MAX) pair for the Eq. 3 normalization primitive.

min: float
max: float
__init__(min: float, max: float) None
class shachen.constants.Band[source]

Bases: StrEnum

Spectral roles the algorithms read (nominal wavelengths in um).

Seven of these are DEBRA’s inputs (DEBRA_BANDS); TIR_112 exists only for the classic Dust RGB green gun.

VIS_064 = 'vis_064'

cloud mask + day baseline image

NIR_160 = 'nir_160'

daytime cloud test (reserved)

SWIR_39 = 'swir_39'

night thin-cirrus test CM4

WV_62 = 'wv_62'

deep-convection test CM2

TIR_86 = 'tir_86'

dust test DT2 (8.4-8.6 um)

TIR_104 = 'tir_104'

clean window reference (10.3-10.4 um)

TIR_112 = 'tir_112'

classic Dust RGB green gun only (11.2 um; not a DEBRA input)

TIR_123 = 'tir_123'

dirty window, RSW / DT1 (12.3 um)

__new__(value)
shachen.constants.ABI_BANDS: dict[Band, str] = {Band.NIR_160: 'C05', Band.SWIR_39: 'C07', Band.TIR_104: 'C13', Band.TIR_112: 'C14', Band.TIR_123: 'C15', Band.TIR_86: 'C11', Band.VIS_064: 'C02', Band.WV_62: 'C08'}

Spectral role -> GOES-R ABI channel name (satpy dataset names).

shachen.constants.AHI_BANDS: dict[Band, str] = {Band.NIR_160: 'B05', Band.SWIR_39: 'B07', Band.TIR_104: 'B13', Band.TIR_112: 'B14', Band.TIR_123: 'B15', Band.TIR_86: 'B11', Band.VIS_064: 'B03', Band.WV_62: 'B08'}

Spectral role -> Himawari AHI band name (satpy dataset names).

shachen.constants.DEBRA_BANDS: tuple[Band, ...] = (Band.VIS_064, Band.NIR_160, Band.SWIR_39, Band.WV_62, Band.TIR_86, Band.TIR_104, Band.TIR_123)

The seven DEBRA algorithm input roles (Miller et al. 2017 Table 1). Band.TIR_112 exists only for the classic Dust RGB comparison baseline (shachen.dustrgb); pipelines that feed DEBRA alone load these.

shachen.constants.BAND_CENTER_UM: dict[Band, float] = {Band.NIR_160: 1.61, Band.SWIR_39: 3.9, Band.TIR_104: 10.33, Band.TIR_112: 11.19, Band.TIR_123: 12.3, Band.TIR_86: 8.44, Band.VIS_064: 0.64, Band.WV_62: 6.19}

Nominal central wavelengths (um) used to interpolate the UWBF emissivity hinge points to sensor band centers (ABI values; AHI within tolerance).

class shachen.constants.CloudMaskConstants[source]

Bases: object

Eqs. 1-12.

cm1_cold_offset_k: float = 50.0

CM1 (Eq. 1): 1 - N(BT10.4; T_skin - cm1_cold_offset_k, T_skin)

cm2: Bounds

CM2 (Eq. 4): N(BT10.4 - BT6.2)

cm3: Bounds

CM3 (Eq. 5): N(BT10.4 - BT12.3), day/night thin cirrus

cm4: Bounds

CM4 (Eq. 6): N(BT3.9 - BT10.4), night-only thin cirrus

r1: Bounds

R1 (Eq. 7, erratum): N(BT12.3 - BT10.4) * (1 - CM1)

r2: Bounds

R2 (Eqs. 8-9): N(BT8.6 - BT10.4) * restoral weights

cm_norm: Bounds

Eq. 12: CM_norm = N(CM)

__init__(cm1_cold_offset_k: float = 50.0, cm2: Bounds = <factory>, cm3: Bounds = <factory>, cm4: Bounds = <factory>, r1: Bounds = <factory>, r2: Bounds = <factory>, cm_norm: Bounds = <factory>) None
class shachen.constants.DustTestConstants[source]

Bases: object

Eqs. 13-15.

dt1_max_rsw_k: float = 3.5

DT1 (Eq. 13): (RSW_obs - RSW_bg) / (max_rsw_k - RSW_bg), RSW = BT12.3 - BT10.4

dt2_max_btd_k: float = 3.0

DT2 (Eq. 14): (BTD_obs - BTD_bg) / (max_btd_k - BTD_bg), BTD = BT8.6 - BT10.4

dt3_shift_land_k: float = -10.0

DT3 (Eq. 15): (BT10.4 - (T_merra - S - dt3_depth_k)) / dt3_depth_k

dt3_shift_ocean_k: float = 5.0
dt3_depth_k: float = 50.0
__init__(dt1_max_rsw_k: float = 3.5, dt2_max_btd_k: float = 3.0, dt3_shift_land_k: float = -10.0, dt3_shift_ocean_k: float = 5.0, dt3_depth_k: float = 50.0) None
class shachen.constants.ConfidenceConstants[source]

Bases: object

Eqs. 16-22 (Eqs. 21-22 per erratum).

dt3_weight_trm: float = 0.5

Eq. 17: CF_trm weights DT3 by this factor; Eq. 18: CF_ngt likewise.

dt3_weight_ngt: float = 0.5
cf_norm: Bounds

Eq. 19: each CF variant normalized with these bounds.

blend_exponent: float = 1.5

Eqs. 20-21: terminator blending on cos(theta_sun), exponent 1.5.

ngt_trm_zenith_deg: Bounds

night/terminator interface: N(cos theta; cos 105 deg, cos 90 deg)

trm_day_zenith_deg: Bounds

terminator/day interface: N(cos theta; cos 90 deg, cos 75 deg)

__init__(dt3_weight_trm: float = 0.5, dt3_weight_ngt: float = 0.5, cf_norm: Bounds = <factory>, blend_exponent: float = 1.5, ngt_trm_zenith_deg: Bounds = <factory>, trm_day_zenith_deg: Bounds = <factory>) None
class shachen.constants.ImageryConstants[source]

Bases: object

Eqs. 23-29 (Eqs. 24-25 per erratum).

bg_blend_zenith_deg: Bounds

Eq. 25 (erratum): B_bg = 1 - N(theta_sun; 79 deg, 89 deg)^1.5

bg_blend_exponent: float = 1.5
cf_cap: float = 0.5

Eqs. 27-29: RED = GRN = BI*(1 - min(CF, cf_cap)) + CF; BLU = BI*(1 - min(CF, cf_cap)) + blue_dimming * CF

blue_dimming: float = 0.1
gun_max: float = 1.2

Per-gun rescale [0, gun_max] -> [0, 255]

__init__(bg_blend_zenith_deg: Bounds = <factory>, bg_blend_exponent: float = 1.5, cf_cap: float = 0.5, blue_dimming: float = 0.1, gun_max: float = 1.2) None
shachen.constants.COLOR_DIMMING: dict[str, tuple[float, float, float]] = {'blue': (0.25, 0.25, 1.0), 'green': (0.1, 1.0, 0.1), 'pink': (1.0, 0.25, 0.25), 'yellow': (1.0, 1.0, 0.1)}

Per-gun dimming triples (D_red, D_grn, D_blu) generalizing Eqs. 27-29: each gun is BI*(1 - min(CF, cf_cap)) + D_gun*CF, a full gun has D = 1.0. Presets from the paper’s section 4.2 text (“dust can be made pink by applying D = 0.25 to equations (28) and (29), green by applying D = 0.10 to equations (27) and (29), or blue by applying D = 0.25 to (27) and (28)”).

shachen.constants.COMPOSITE_WINDOW_DAYS: int = 14

Cloud-cleared composite background (paper section 3.2, Option B): rolling window length (“multiday (e.g., 14 day) composites”) and the fewest composite days accepted at load time before the background is refused.

class shachen.constants.DustRGBConstants[source]

Bases: object

One classic Dust RGB stretch set. Not part of DEBRA (Eqs. 1-29).

Each gun is N(x; MIN, MAX) ** (1/gamma) over the GOES-R/Himawari band mix of the CIRA Dust RGB Quick Guide (satpy’s dust composite): 12.3-10.3, 11.2-8.4, 10.3 — note the green minuend is 11.2 um, not the 10.8 um of SEVIRI’s IR10.8 - IR8.7.

The stretch values are per sensor: DUST_RGB (SEVIRI) and DUST_RGB_ABI are the two published sets, dispatched by DUST_RGB_BY_READER. See shachen.dustrgb for the references.

red: Bounds

RED: BT12.3 - BT10.4 (split window), [-4, +2] K, gamma 1.

green: Bounds

GRN: BT11.2 - BT8.6, [0, 15] K, gamma 2.5 (note: 11.2 um, not 10.4).

green_gamma: float = 2.5
blue: Bounds

BLU: BT10.4, [261, 289] K, gamma 1.

__init__(red: Bounds = <factory>, green: Bounds = <factory>, green_gamma: float = 2.5, blue: Bounds = <factory>) None
shachen.constants.DUST_RGB = DustRGBConstants(red=Bounds(min=-4.0, max=2.0), green=Bounds(min=0.0, max=15.0), green_gamma=2.5, blue=Bounds(min=261.0, max=289.0))

The original SEVIRI stretches (Lensky and Rosenfeld 2008, as formalised in EUMeTrain’s recipe compilation; satpy’s generic dust_default enhancement). Applied to Himawari here, which is what satpy does too: it ships no dust_ahi override. Also the fallback for an unknown sensor.

shachen.constants.DUST_RGB_ABI = DustRGBConstants(red=Bounds(min=-6.7, max=2.6), green=Bounds(min=-0.5, max=20.0), green_gamma=2.5, blue=Bounds(min=261.2, max=288.7))

The ABI-adjusted stretches from the CIRA GOES-R Dust RGB Quick Guide (satpy’s sensor-specific dust_abi enhancement), converted from the Quick Guide’s degrees Celsius. Berndt et al. (2018) is the method behind the adjustment: ABI’s channels do not sit where SEVIRI’s do, so the SEVIRI numbers put the clear-sky land background at the wrong end of the red stretch.

shachen.constants.DUST_RGB_BY_READER: dict[str, DustRGBConstants] = {'abi_l1b': DustRGBConstants(red=Bounds(min=-6.7, max=2.6), green=Bounds(min=-0.5, max=20.0), green_gamma=2.5, blue=Bounds(min=261.2, max=288.7)), 'ahi_hsd': DustRGBConstants(red=Bounds(min=-4.0, max=2.0), green=Bounds(min=0.0, max=15.0), green_gamma=2.5, blue=Bounds(min=261.0, max=289.0))}

satpy reader -> the stretch set that sensor’s operational product uses. shachen.pipeline.run_dust_rgb() reads this off scene.attrs["reader"] so a baseline rendered here matches the image forecasters see. The same rule is why there is no single canonical recipe to freeze: the scheme is retuned per imager (satpy carries ash_abi, convection_abi and the night-microphysics variants for the same reason).

class shachen.constants.DebraConstants[source]

Bases: object

The whole tuning surface, one group per stage of the algorithm.

cloud_mask: CloudMaskConstants
dust_tests: DustTestConstants
confidence: ConfidenceConstants
imagery: ImageryConstants
__init__(cloud_mask: CloudMaskConstants = <factory>, dust_tests: DustTestConstants = <factory>, confidence: ConfidenceConstants = <factory>, imagery: ImageryConstants = <factory>) None
shachen.constants.DEFAULTS = DebraConstants(cloud_mask=CloudMaskConstants(cm1_cold_offset_k=50.0, cm2=Bounds(min=0.0, max=25.0), cm3=Bounds(min=2.0, max=4.5), cm4=Bounds(min=5.0, max=8.0), r1=Bounds(min=0.0, max=3.5), r2=Bounds(min=-1.0, max=3.0), cm_norm=Bounds(min=0.45, max=0.8)), dust_tests=DustTestConstants(dt1_max_rsw_k=3.5, dt2_max_btd_k=3.0, dt3_shift_land_k=-10.0, dt3_shift_ocean_k=5.0, dt3_depth_k=50.0), confidence=ConfidenceConstants(dt3_weight_trm=0.5, dt3_weight_ngt=0.5, cf_norm=Bounds(min=0.25, max=2.5), blend_exponent=1.5, ngt_trm_zenith_deg=Bounds(min=105.0, max=90.0), trm_day_zenith_deg=Bounds(min=90.0, max=75.0)), imagery=ImageryConstants(bg_blend_zenith_deg=Bounds(min=79.0, max=89.0), bg_blend_exponent=1.5, cf_cap=0.5, blue_dimming=0.1, gun_max=1.2))

The paper-tuned default constant set.

shachen.constants.ABI_TUNED = DebraConstants(cloud_mask=CloudMaskConstants(cm1_cold_offset_k=50.0, cm2=Bounds(min=0.0, max=25.0), cm3=Bounds(min=2.0, max=4.5), cm4=Bounds(min=5.0, max=8.0), r1=Bounds(min=0.0, max=3.5), r2=Bounds(min=-1.0, max=3.0), cm_norm=Bounds(min=0.45, max=0.8)), dust_tests=DustTestConstants(dt1_max_rsw_k=3.5, dt2_max_btd_k=3.0, dt3_shift_land_k=-10.0, dt3_shift_ocean_k=5.0, dt3_depth_k=50.0), confidence=ConfidenceConstants(dt3_weight_trm=0.5, dt3_weight_ngt=0.5, cf_norm=Bounds(min=0.4, max=2.5), blend_exponent=1.5, ngt_trm_zenith_deg=Bounds(min=105.0, max=90.0), trm_day_zenith_deg=Bounds(min=90.0, max=75.0)), imagery=ImageryConstants(bg_blend_zenith_deg=Bounds(min=79.0, max=89.0), bg_blend_exponent=1.5, cf_cap=0.5, blue_dimming=0.1, gun_max=1.2))

Optional retune for the ABI + MERRA-2 + CAMEL stack. Single deviation from the paper: the Eq. 19 lower bound is raised 0.25 -> 0.40. Rationale: on this ancillary stack DT3 carries a 0.2-0.5 clear-sky floor over land (MERRA-2 skin T runs warmer than BT10.4, plus the -10 K land shift), which leaks through low-cloud-mask pixels as a faint yellow tint over vegetated and low-cloud areas. Sweeping the bound over the three reference cases: 0.40 removes 92% of the tinted area (SE-US box, fraction CF > 0.05: 0.37 -> 0.028) while the 2017-03-23 plume mean CF drops only 5% (0.476 -> 0.452) and the 2020-12-23 plume core stays distinct (p90 0.40). The paper itself anticipates “minor retuning” per sensor.