foo
This commit is contained in:
parent
e3af4bf2f4
commit
bfd5d73dff
6 changed files with 760 additions and 61 deletions
|
|
@ -7,7 +7,6 @@ import json
|
|||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from gap_validation.calendar import DEFAULT_GAP_LENGTHS, TRANSITIONS
|
||||
from gap_validation.run import run_validation
|
||||
|
||||
# Primary season per site (matches scripts/export_thesis_tables.py).
|
||||
|
|
@ -47,26 +46,49 @@ def _parse_scenario(key: str) -> tuple[str, int | None, str]:
|
|||
return strategy, sigma if sigma == 30 else (None if sigma == 20 else sigma), mode
|
||||
|
||||
|
||||
def _best_bti_from_metrics(metrics_path: Path) -> str | None:
|
||||
def _best_from_metrics(metrics_path: Path, workflow: str) -> str | None:
|
||||
"""Best scenario key (max no-gap NSE_PC) for ``workflow`` (``bti`` or ``itb``)."""
|
||||
if workflow not in ("bti", "itb"):
|
||||
raise ValueError(f"workflow must be bti or itb, got {workflow!r}")
|
||||
if not metrics_path.is_file():
|
||||
return None
|
||||
temporal = json.loads(metrics_path.read_text(encoding="utf-8")).get("temporal") or {}
|
||||
want_itb = workflow == "itb"
|
||||
best_key, best_nse = None, None
|
||||
for k, v in temporal.items():
|
||||
if not k.endswith("_itb") and isinstance(v, dict):
|
||||
n = v.get("nse_pc")
|
||||
if isinstance(n, (int, float)) and (best_nse is None or n > best_nse):
|
||||
best_nse = n
|
||||
best_key = k
|
||||
if k.endswith("_itb") != want_itb or not isinstance(v, dict):
|
||||
continue
|
||||
n = v.get("nse_pc")
|
||||
if isinstance(n, (int, float)) and (best_nse is None or n > best_nse):
|
||||
best_nse = n
|
||||
best_key = k
|
||||
return best_key
|
||||
|
||||
|
||||
def _best_bti_from_metrics(metrics_path: Path) -> str | None:
|
||||
return _best_from_metrics(metrics_path, "bti")
|
||||
|
||||
|
||||
def _best_itb_from_metrics(metrics_path: Path) -> str | None:
|
||||
return _best_from_metrics(metrics_path, "itb")
|
||||
|
||||
|
||||
def _resolve_workflows(workflow: str) -> tuple[str, ...]:
|
||||
return ("bti", "itb") if workflow == "both" else (workflow,)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description="Batch spatial gap validation (six sites).")
|
||||
ap.add_argument("--data-dir", type=Path, default=Path("data"))
|
||||
ap.add_argument("--sites-geojson", type=Path, default=Path("data/sites.geojson"))
|
||||
ap.add_argument("--skip-fusion", action="store_true")
|
||||
ap.add_argument("--write-manifest-only", action="store_true")
|
||||
ap.add_argument(
|
||||
"--workflow",
|
||||
choices=["bti", "itb", "both"],
|
||||
default="both",
|
||||
help="Fusion workflow(s) to validate (default: both best BtI and best ItB).",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--gap-days",
|
||||
type=int,
|
||||
|
|
@ -76,6 +98,7 @@ def main() -> None:
|
|||
args = ap.parse_args()
|
||||
positions = _site_positions(args.sites_geojson)
|
||||
gap_filter = args.gap_days
|
||||
workflows = _resolve_workflows(args.workflow)
|
||||
|
||||
for site, season in sorted(PRIMARY_SEASON.items()):
|
||||
pos = positions.get(site)
|
||||
|
|
@ -83,28 +106,29 @@ def main() -> None:
|
|||
print(f"[skip] No coordinates for {site}")
|
||||
continue
|
||||
metrics_path = args.data_dir / site / str(season) / "metrics.json"
|
||||
scenario_key = _best_bti_from_metrics(metrics_path)
|
||||
if not scenario_key:
|
||||
print(f"[skip] {site} {season}: no metrics.json / BtI scenarios")
|
||||
continue
|
||||
strategy, sigma, mode = _parse_scenario(scenario_key)
|
||||
sigma_kw = 30 if sigma == 30 else None
|
||||
print(f"=== {site} {season} {scenario_key} ===")
|
||||
out = run_validation(
|
||||
site,
|
||||
season,
|
||||
pos,
|
||||
strategy,
|
||||
sigma_kw,
|
||||
mode,
|
||||
skip_manifest=False,
|
||||
skip_fusion=args.skip_fusion,
|
||||
write_manifest_only=args.write_manifest_only,
|
||||
gap_days_filter=gap_filter,
|
||||
transition_filter=None,
|
||||
s2_calendar_strategy=strategy,
|
||||
)
|
||||
print(out)
|
||||
for workflow in workflows:
|
||||
scenario_key = _best_from_metrics(metrics_path, workflow)
|
||||
if not scenario_key:
|
||||
print(f"[skip] {site} {season}: no metrics.json / {workflow} scenarios")
|
||||
continue
|
||||
strategy, sigma, mode = _parse_scenario(scenario_key)
|
||||
sigma_kw = 30 if sigma == 30 else None
|
||||
print(f"=== {site} {season} {scenario_key} ===")
|
||||
out = run_validation(
|
||||
site,
|
||||
season,
|
||||
pos,
|
||||
strategy,
|
||||
sigma_kw,
|
||||
mode,
|
||||
skip_manifest=False,
|
||||
skip_fusion=args.skip_fusion,
|
||||
write_manifest_only=args.write_manifest_only,
|
||||
gap_days_filter=gap_filter,
|
||||
transition_filter=None,
|
||||
s2_calendar_strategy=strategy,
|
||||
)
|
||||
print(out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ from pathlib import Path
|
|||
|
||||
from gap_validation.batch_spatial import (
|
||||
PRIMARY_SEASON,
|
||||
_best_bti_from_metrics,
|
||||
_best_from_metrics,
|
||||
_parse_scenario,
|
||||
_resolve_workflows,
|
||||
_site_positions,
|
||||
)
|
||||
from gap_validation.temporal_pc import run_temporal_pc
|
||||
|
|
@ -19,9 +20,16 @@ def main() -> None:
|
|||
ap.add_argument("--data-dir", type=Path, default=Path("data"))
|
||||
ap.add_argument("--sites-geojson", type=Path, default=Path("data/sites.geojson"))
|
||||
ap.add_argument("--skip-fusion", action="store_true")
|
||||
ap.add_argument(
|
||||
"--workflow",
|
||||
choices=["bti", "itb", "both"],
|
||||
default="both",
|
||||
help="Fusion workflow(s) to validate (default: both best BtI and best ItB).",
|
||||
)
|
||||
ap.add_argument("--gap-days", type=int, action="append")
|
||||
args = ap.parse_args()
|
||||
positions = _site_positions(args.sites_geojson)
|
||||
workflows = _resolve_workflows(args.workflow)
|
||||
|
||||
for site, season in sorted(PRIMARY_SEASON.items()):
|
||||
pos = positions.get(site)
|
||||
|
|
@ -29,27 +37,28 @@ def main() -> None:
|
|||
print(f"[skip] No coordinates for {site}")
|
||||
continue
|
||||
metrics_path = args.data_dir / site / str(season) / "metrics.json"
|
||||
scenario_key = _best_bti_from_metrics(metrics_path)
|
||||
if not scenario_key:
|
||||
print(f"[skip] {site} {season}: no metrics.json")
|
||||
continue
|
||||
strategy, sigma, mode = _parse_scenario(scenario_key)
|
||||
sigma_kw = 30 if sigma == 30 else None
|
||||
print(f"=== {site} {season} temporal {scenario_key} ===")
|
||||
out = run_temporal_pc(
|
||||
site,
|
||||
season,
|
||||
pos,
|
||||
strategy,
|
||||
sigma_kw,
|
||||
mode,
|
||||
skip_manifest=False,
|
||||
skip_fusion=args.skip_fusion,
|
||||
gap_days_filter=args.gap_days,
|
||||
transition_filter=None,
|
||||
s2_calendar_strategy=strategy,
|
||||
)
|
||||
print(out)
|
||||
for workflow in workflows:
|
||||
scenario_key = _best_from_metrics(metrics_path, workflow)
|
||||
if not scenario_key:
|
||||
print(f"[skip] {site} {season}: no metrics.json / {workflow} scenarios")
|
||||
continue
|
||||
strategy, sigma, mode = _parse_scenario(scenario_key)
|
||||
sigma_kw = 30 if sigma == 30 else None
|
||||
print(f"=== {site} {season} temporal {scenario_key} ===")
|
||||
out = run_temporal_pc(
|
||||
site,
|
||||
season,
|
||||
pos,
|
||||
strategy,
|
||||
sigma_kw,
|
||||
mode,
|
||||
skip_manifest=False,
|
||||
skip_fusion=args.skip_fusion,
|
||||
gap_days_filter=args.gap_days,
|
||||
transition_filter=None,
|
||||
s2_calendar_strategy=strategy,
|
||||
)
|
||||
print(out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -12,8 +12,9 @@ from phenology_timesat import phenocam_phenology_path
|
|||
|
||||
from gap_validation.batch_spatial import (
|
||||
PRIMARY_SEASON,
|
||||
_best_bti_from_metrics,
|
||||
_best_from_metrics,
|
||||
_parse_scenario,
|
||||
_resolve_workflows,
|
||||
_site_positions,
|
||||
)
|
||||
from gap_validation.calendar import load_manifest, validation_dir
|
||||
|
|
@ -57,11 +58,12 @@ def compute_offsets_for_site(
|
|||
season: int,
|
||||
site_position: tuple[float, float],
|
||||
*,
|
||||
workflow: str = "bti",
|
||||
gap_days_list: tuple[int, ...] = (15, 30),
|
||||
) -> list[dict]:
|
||||
base = Path(f"data/{site}/{season}")
|
||||
metrics_path = base / "metrics.json"
|
||||
scenario_key = _best_bti_from_metrics(metrics_path)
|
||||
scenario_key = _best_from_metrics(metrics_path, workflow)
|
||||
if not scenario_key:
|
||||
return []
|
||||
ref_path = phenocam_phenology_path(site, season)
|
||||
|
|
@ -112,14 +114,26 @@ def write_phenology_offsets(
|
|||
season: int,
|
||||
site_position: tuple[float, float],
|
||||
*,
|
||||
workflow: str = "bti",
|
||||
gap_days_list: tuple[int, ...] = (15, 30),
|
||||
) -> Path:
|
||||
rows = compute_offsets_for_site(
|
||||
site, season, site_position, gap_days_list=gap_days_list
|
||||
site, season, site_position, workflow=workflow, gap_days_list=gap_days_list
|
||||
)
|
||||
out = validation_dir(site, season) / "gap_phenology_offsets.json"
|
||||
payload = {"site_name": site, "season": season, "records": rows}
|
||||
vdir = validation_dir(site, season)
|
||||
payload = {
|
||||
"site_name": site,
|
||||
"season": season,
|
||||
"workflow": workflow,
|
||||
"records": rows,
|
||||
}
|
||||
out = vdir / f"gap_phenology_offsets_{workflow}.json"
|
||||
out.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
||||
if workflow == "bti":
|
||||
# Legacy alias for backward-compatible readers.
|
||||
(vdir / "gap_phenology_offsets.json").write_text(
|
||||
json.dumps(payload, indent=2) + "\n", encoding="utf-8"
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
|
|
@ -127,14 +141,22 @@ def main() -> None:
|
|||
ap = argparse.ArgumentParser(description="Gap fusion TIMESAT offsets vs PhenoCam.")
|
||||
ap.add_argument("--data-dir", type=Path, default=Path("data"))
|
||||
ap.add_argument("--sites-geojson", type=Path, default=Path("data/sites.geojson"))
|
||||
ap.add_argument(
|
||||
"--workflow",
|
||||
choices=["bti", "itb", "both"],
|
||||
default="both",
|
||||
help="Fusion workflow(s) (default: both best BtI and best ItB).",
|
||||
)
|
||||
args = ap.parse_args()
|
||||
positions = _site_positions(args.sites_geojson)
|
||||
workflows = _resolve_workflows(args.workflow)
|
||||
for site, season in sorted(PRIMARY_SEASON.items()):
|
||||
pos = positions.get(site)
|
||||
if not pos:
|
||||
continue
|
||||
p = write_phenology_offsets(site, season, pos)
|
||||
print(p)
|
||||
for workflow in workflows:
|
||||
p = write_phenology_offsets(site, season, pos, workflow=workflow)
|
||||
print(p)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -273,8 +273,13 @@ def run_validation(
|
|||
}
|
||||
},
|
||||
}
|
||||
out_path = vdir / "gap_validation_summary.json"
|
||||
out_path = vdir / f"gap_validation_summary_{mode}.json"
|
||||
out_path.write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8")
|
||||
if mode == "bti":
|
||||
# Legacy alias for backward-compatible readers (webapp, older scripts).
|
||||
(vdir / "gap_validation_summary.json").write_text(
|
||||
json.dumps(summary, indent=2) + "\n", encoding="utf-8"
|
||||
)
|
||||
return out_path
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ from metrics_stats import (
|
|||
)
|
||||
|
||||
from gap_validation.calendar import TRANSITIONS, load_manifest, validation_dir, write_manifest
|
||||
from gap_validation.fusion_masked import run_masked_fusion_season, validation_fusion_dir
|
||||
from gap_validation.fusion_masked import run_masked_fusion_season
|
||||
from gap_validation.run import (
|
||||
_filter_entries,
|
||||
_scenario_key,
|
||||
|
|
@ -247,8 +247,13 @@ def run_temporal_pc(
|
|||
}
|
||||
},
|
||||
}
|
||||
out_path = vdir / "gap_metrics.json"
|
||||
out_path = vdir / f"gap_metrics_{mode}.json"
|
||||
out_path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
||||
if mode == "bti":
|
||||
# Legacy alias for backward-compatible readers.
|
||||
(vdir / "gap_metrics.json").write_text(
|
||||
json.dumps(payload, indent=2) + "\n", encoding="utf-8"
|
||||
)
|
||||
return out_path
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue