added gap validation.

This commit is contained in:
Felix Delattre 2026-05-17 15:55:15 +02:00
parent 374be6865d
commit 740249115b
12 changed files with 997 additions and 116 deletions

View file

@ -1,14 +1,20 @@
"""EFAST with symlinked S2 dir (withhold one acquisition); outputs under validation/."""
"""EFAST with symlinked S2 dir (gap window omitted); outputs under validation/."""
from __future__ import annotations
from datetime import datetime
from pathlib import Path
from tempfile import TemporaryDirectory
from fusion import run_efast, run_efast_itb
from preparation import _get_base_dir, _get_itb_base_dir
from gap_validation.s2_mask_dir import build_masked_s2_dir_bti, build_masked_s2_dir_itb
from gap_validation.s2_mask_dir import (
acquisition_yyyymmdd_in_window,
assert_no_leakage,
build_masked_s2_dir_bti,
build_masked_s2_dir_itb,
)
def prepared_s3_dir(season: int, site_name: str, strategy: str) -> Path:
@ -19,20 +25,35 @@ def validation_fusion_dir(
site_name: str,
season: int,
gap_days: int,
transition: str,
strategy: str,
sigma: int | None,
mode: str,
) -> Path:
"""``data/.../validation/fusion/gap_{n}/{strategy}_sigma{20|30}_{bti|itb}/``."""
"""``data/.../validation/fusion/gap_{n}_{transition}/{strategy}_sigma{20|30}_{bti|itb}/``."""
sig = 30 if sigma == 30 else 20
return (
Path(f"data/{site_name}/{season}/validation")
/ "fusion"
/ f"gap_{gap_days}"
/ f"gap_{gap_days}_{transition}"
/ f"{strategy}_sigma{sig}_{mode}"
)
def excluded_acquisition_days(
prepared_s2: Path,
window_start_iso: str,
window_end_iso: str,
withheld_yyyymmdd: str,
) -> set[str]:
"""Union of gap-window S2 days and the withheld validation acquisition."""
w0 = datetime.strptime(window_start_iso[:10], "%Y-%m-%d").date()
w1 = datetime.strptime(window_end_iso[:10], "%Y-%m-%d").date()
excluded = acquisition_yyyymmdd_in_window(prepared_s2, w0, w1)
excluded.add(withheld_yyyymmdd)
return excluded
def run_masked_fusion_one_date(
season: int,
site_position: tuple[float, float],
@ -41,19 +62,24 @@ def run_masked_fusion_one_date(
sigma: int | None,
mode: str,
prediction_date_iso: str,
window_start_iso: str,
window_end_iso: str,
withheld_yyyymmdd: str,
fusion_output_dir: Path,
) -> Path:
"""Build temp masked S2 dir, run EFAST for ``prediction_date_iso`` only; return output dir."""
"""Build temp masked S2 dir, run EFAST for ``prediction_date_iso`` only."""
fusion_output_dir.mkdir(parents=True, exist_ok=True)
date_range = f"{prediction_date_iso[:10]}/{prediction_date_iso[:10]}"
s3_dir = prepared_s3_dir(season, site_name, strategy)
with TemporaryDirectory(prefix="gapval_s2_") as tmp:
tmp_s2 = Path(tmp) / "s2"
if mode == "bti":
prep_s2 = _get_base_dir(season, site_name, strategy) / "s2"
build_masked_s2_dir_bti(prep_s2, withheld_yyyymmdd, tmp_s2)
excl = excluded_acquisition_days(
prep_s2, window_start_iso, window_end_iso, withheld_yyyymmdd
)
build_masked_s2_dir_bti(prep_s2, excl, tmp_s2)
assert_no_leakage(withheld_yyyymmdd, tmp_s2)
run_efast(
season,
site_position,
@ -62,13 +88,16 @@ def run_masked_fusion_one_date(
sigma=sigma,
date_range=date_range,
s2_output_dir=tmp_s2,
s3_output_dir=s3_dir,
s3_output_dir=prepared_s3_dir(season, site_name, strategy),
fusion_output_dir=fusion_output_dir,
)
elif mode == "itb":
prep_s2 = _get_itb_base_dir(season, site_name, strategy) / "s2"
s3_itb = _get_itb_base_dir(season, site_name, strategy) / "s3"
build_masked_s2_dir_itb(prep_s2, withheld_yyyymmdd, tmp_s2)
excl = excluded_acquisition_days(
prep_s2, window_start_iso, window_end_iso, withheld_yyyymmdd
)
build_masked_s2_dir_itb(prep_s2, excl, tmp_s2)
assert_no_leakage(withheld_yyyymmdd, tmp_s2)
run_efast_itb(
season,
site_position,
@ -77,7 +106,7 @@ def run_masked_fusion_one_date(
sigma=sigma,
date_range=date_range,
s2_output_dir=tmp_s2,
s3_output_dir=s3_itb,
s3_output_dir=_get_itb_base_dir(season, site_name, strategy) / "s3",
fusion_output_dir=fusion_output_dir,
)
else:
@ -86,6 +115,64 @@ def run_masked_fusion_one_date(
return fusion_output_dir
def run_masked_fusion_season(
season: int,
site_position: tuple[float, float],
site_name: str,
strategy: str,
sigma: int | None,
mode: str,
window_start_iso: str,
window_end_iso: str,
withheld_yyyymmdd: str,
fusion_output_dir: Path,
) -> Path:
"""Full-season EFAST on gap-degraded S2 stack (temporal NSE_PC tier)."""
fusion_output_dir.mkdir(parents=True, exist_ok=True)
date_range = f"{season}-01-01/{season}-12-31"
with TemporaryDirectory(prefix="gapval_s2_") as tmp:
tmp_s2 = Path(tmp) / "s2"
if mode == "bti":
prep_s2 = _get_base_dir(season, site_name, strategy) / "s2"
excl = excluded_acquisition_days(
prep_s2, window_start_iso, window_end_iso, withheld_yyyymmdd
)
build_masked_s2_dir_bti(prep_s2, excl, tmp_s2)
assert_no_leakage(withheld_yyyymmdd, tmp_s2)
run_efast(
season,
site_position,
site_name,
cleaning_strategy=strategy,
sigma=sigma,
date_range=date_range,
s2_output_dir=tmp_s2,
s3_output_dir=prepared_s3_dir(season, site_name, strategy),
fusion_output_dir=fusion_output_dir,
)
else:
prep_s2 = _get_itb_base_dir(season, site_name, strategy) / "s2"
excl = excluded_acquisition_days(
prep_s2, window_start_iso, window_end_iso, withheld_yyyymmdd
)
build_masked_s2_dir_itb(prep_s2, excl, tmp_s2)
assert_no_leakage(withheld_yyyymmdd, tmp_s2)
run_efast_itb(
season,
site_position,
site_name,
cleaning_strategy=strategy,
sigma=sigma,
date_range=date_range,
s2_output_dir=tmp_s2,
s3_output_dir=_get_itb_base_dir(season, site_name, strategy) / "s3",
fusion_output_dir=fusion_output_dir,
)
return fusion_output_dir
def production_fusion_path(
season: int,
site_name: str,