From c4d2a66529d43013e3480cf0e22a357fcedebd6e Mon Sep 17 00:00:00 2001 From: Hugh Runyan Date: Tue, 4 Aug 2026 09:12:34 -0700 Subject: [PATCH] Fix leap-year Oct/Dec notch in the monthly FOD decay mask MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit estimate_emissions_monthly masked deposits younger than the ~half-year emission lag with a strict `integral_adjusted > 0`. The cutoff is integral_adjusted == 0 (deposit exactly 0.5 yr old). In a LEAP year the monthly grid lands exactly on it for the deposit six months before a 31-day emission month (Jun->Dec, Apr->Oct: 183/366 == 0.5); in a common year it lands just past it (183/365 == 0.50137). Because integral_adjusted is a difference of two O(k) cumulative sums, at the cutoff it is 0 +/- ~1e-15 (catastrophic cancellation), so a bare `> 0` dropped that whole deposit-month unpredictably -> a spurious ~1-2% notch in the 31-day peak months (Oct, Dec) in leap years only. This surfaced as Climate TRACE QA "de-trended z-score > 3" flags on modeled countries at Oct-2020/Dec-2020 (e.g. BGD, CMR). Fix: mask with a tolerance (`>= -1e-9`) — well below any real k-time step (genuine just-past-cutoff values are ~1e-4..1e-3) but above FP noise — so the exact-half-year deposit is retained consistently in leap and common years. Impact (model-output-change): within-year redistribution only; the leap-year monthly profile is smoothed (Oct/Dec notch closes to the ~0.2% genuine 365-vs-366-day floor), and leap-year ANNUAL totals rise by the physically-real extra-day mass (~+0.25%; +0.36% on a BGD-like hot/wet growing site). Common years are bit-for-bit unchanged; only leap-year Oct/Dec cells move, all additive, no other-month leakage (validated across k regimes + BGD's real inputs). Reported/single-value sites are unaffected (this path is modeled-only). Paired with RMI_Climate_TRACE_Waste_Methane PR #78 (branch country-exceedance-source-trace), whose diagnostic surfaced and traced this. Co-Authored-By: Claude Opus 4.8 --- SWEET_python/model_v2.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/SWEET_python/model_v2.py b/SWEET_python/model_v2.py index 9a0c744..1dd6f90 100644 --- a/SWEET_python/model_v2.py +++ b/SWEET_python/model_v2.py @@ -264,10 +264,23 @@ def expand_to_months(annual_series_or_value): half_year_offset = 0.5 * ks_monthly[None, :] # k at deposit month integral_adjusted = integral - half_year_offset - # Mask out emissions where integral_adjusted <= 0 - # This is analogous to the annual model's mask for years_back <= 0 - # (no emissions until ~0.5 years after deposit) - mask_positive_integral = integral_adjusted > 0 + # Mask out deposits younger than the ~half-year emission lag (analogous + # to the annual model's years_back <= 0 mask: no emission until ~0.5 yr + # after deposit). The cutoff is integral_adjusted == 0 (deposit exactly + # half a year old). In a LEAP year the monthly grid lands EXACTLY on that + # cutoff for the deposit six months before a 31-day emission month + # (Jun->Dec, Apr->Oct: 183/366 == 0.5), whereas in a common year it lands + # just past it (183/365 == 0.50137). integral_adjusted is a difference of + # two O(k) cumulative sums, so at the cutoff it is 0 +/- ~1e-15 + # (catastrophic cancellation) and a bare `> 0` (or `>= 0`) includes/drops + # that whole deposit-month unpredictably -> a spurious ~1-2% Oct/Dec notch + # in leap years only. Use a tolerance well below any real k-time step + # (genuine "just past cutoff" values are ~1e-4..1e-3) but above FP noise, + # so the exact-half-year deposit is retained consistently in leap and + # common years. Within-year redistribution; leap-year annual rises by the + # physically-real extra-day mass (~+0.25%), common years unchanged. + _HALF_YEAR_TOL = 1e-9 + mask_positive_integral = integral_adjusted >= -_HALF_YEAR_TOL decay = np.exp(-integral_adjusted) decay[~mask_valid] = 0 decay[~mask_positive_integral] = 0