From 643e223db17f31e6c3ee2733ff805edd1fb2cb94 Mon Sep 17 00:00:00 2001 From: HenryGeorgist Date: Wed, 15 Jul 2026 00:43:53 +0000 Subject: [PATCH] fix(lmc): fill in-domain NaN precip gaps so corrupt AORC days do not empty the domain The AORC mirror has NaN patches on isolated storm days (e.g. 1985-11-07, 2003-10-19, 2024-06-18). 3.LMC.py sums raw precip over the whole basin polygon and requires every year's annual-max window to be finite, so one corrupt cell NaNs the L-moment of every anchor whose footprint covers it. Over the full ~45yr record this collapsed the L-moment field from 2675 cells to 47, so CLMPV could not grow a transposition domain (a clean 5yr window hid the problem). run_lmc now fills in-domain NaN precip cells with 0 before LMC. Off-domain cells (NaN in every file) are left untouched, so the valid-anchor set is unchanged; each cell's spatial L-moment averages over all years, so one 0-filled bad day is negligible. Verified in-container: LMCol chunk 0 finite cells 11 -> 424. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/plugin.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/plugin.py b/src/plugin.py index fb249f0..b62def4 100644 --- a/src/plugin.py +++ b/src/plugin.py @@ -702,6 +702,45 @@ def run_lmc(action): ) else: _get_folder(action, "raw_precipitation", lmc) + + # AORC mirror data quality: some storm days carry NaN patches over in-domain + # cells (e.g. 1985-11-07, 2003-10-19). 3.LMC.py sums raw precip over the whole + # basin polygon and requires every year's annual-max window to be finite, so a + # single corrupt cell NaNs the L-moment of every anchor whose footprint covers + # it -> over a multi-decade record the domain-wide L-moment field collapses to a + # handful of cells and the transposition domain cannot grow (a clean 5-year + # window hides this; the full record exposes it). Fill in-domain NaN precip + # cells with 0 before LMC: a cell's spatial L-moment averages over all years, so + # one 0-filled bad day is negligible, while genuine off-domain cells (NaN in + # every file) are left untouched so the valid-anchor set is unchanged. + precvar = str(attrs.get("precvar", "precrate")) + _pfiles = sorted(glob.glob(os.path.join(lmc, f"{precip_prefix}*{precip_suffix}"))) + if _pfiles: + _sample = _pfiles[:: max(1, len(_pfiles) // 40)] + _acc = None + for _pf in _sample: + with xr.open_dataset(_pf) as _pd: + _fin = np.isfinite(_pd[precvar].isel(time=0).values).astype(np.int32) + _acc = _fin if _acc is None else _acc + _fin + _domain = _acc >= (len(_sample) // 2 + 1) + _filled = 0 + for _pf in _pfiles: + _pd = xr.open_dataset(_pf) + _pd.load() + _pd.close() + _a = _pd[precvar].values + _bad = ~np.isfinite(_a) & _domain[None, :, :] + if _bad.any(): + _a[_bad] = 0.0 + _tmp = _pf + ".tmp" + _pd.to_netcdf(_tmp) + _pd.close() + os.replace(_tmp, _pf) + _filled += 1 + logger.info( + f"LMC: filled in-domain NaN gaps in {_filled} of {len(_pfiles)} precip file(s)" + ) + _get_shapefile(action, "watershed_shapefile", lmc, stem="WS") out_key = str(attrs.get("output_key", ""))