efast-phenocam-validation/gap_validation/whittaker_compare.py
Felix Delattre 374be6865d Foo
2026-05-16 18:01:46 +02:00

64 lines
2 KiB
Python

"""Whittaker S2 GCC (λ=400 d²) as a spatial constant vs withheld S2 GCC; crossover vs fusion nse_s2."""
from __future__ import annotations
from pathlib import Path
from metrics_stats import (
WHITTAKER_LAMBDA_DAYS_SQ,
_norm_date_key,
_s2_gcc_series_from_preselection,
_whittaker_smooth_dict,
)
def whittaker_gcc_on_gap_masked_series(
base: Path,
strategy: str,
prediction_iso: str,
withheld_iso: str,
lam: float = WHITTAKER_LAMBDA_DAYS_SQ,
) -> float | None:
"""Whittaker smooth on cloud-screened S2 GCC **excluding** the withheld acquisition day.
Comparator aligned with ``baseline.s2_whittaker_lambda400`` in ``metrics_stats`` (same λ,
same preselection GCC), but the withheld date is removed so the smoother does not see
the target acquisition. Value at ``prediction_iso`` (YYYY-MM-DD) is returned.
"""
pred_k = _norm_date_key(prediction_iso)
wh_k = _norm_date_key(withheld_iso)
if not pred_k or not wh_k:
return None
all_gcc, flags = _s2_gcc_series_from_preselection(base)
if not all_gcc:
return None
idx = 0 if strategy == "aggressive" else 1
kept = sorted(
(d, g)
for d, g in all_gcc.items()
if d in flags and not flags[d][idx] and _norm_date_key(d) != wh_k
)
if len(kept) < 2:
return None
obs_d, obs_v = zip(*kept)
smooth = _whittaker_smooth_dict(obs_d, obs_v, lam)
return smooth.get(pred_k)
def first_gap_where_fusion_below_whittaker(
rows: list[dict],
*,
fusion_key: str = "nse_s2",
whittaker_key: str = "nse_s2",
) -> int | None:
"""Smallest ``gap_days`` where fusion[metric] < whittaker[metric] (strict)."""
eligible = [
r
for r in rows
if r.get(fusion_key) is not None and r.get(whittaker_key) is not None
]
eligible.sort(key=lambda r: r["gap_days"])
for r in eligible:
if r[fusion_key] < r[whittaker_key]:
return int(r["gap_days"])
return None