File size: 5,265 Bytes
d6b0b7a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
"""
Enriches the reach graph's node tables (from scripts/build_reach_graphs.py)
with every available feature loader, via node_features.py's base_nodes_df
path -- closing the gap where the reach graph and the feature pipeline
existed separately but nothing actually connected them.

Usage:
    python -m scripts.enrich_reach_graph --data-root datasets
"""
import argparse
import sys
from pathlib import Path

import pandas as pd

try:
    from src.graph.node_features import build_node_features
except ImportError:
    sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
    from src.graph.node_features import build_node_features

BASIN_FILE_NAMES = {0: "eure", 1: "risle"}


def enrich_basin(
    data_root: Path, basin_id: int, skip_climate: bool = False,
    date_range: "tuple[str, str] | None" = None,
) -> None:
    file_key = BASIN_FILE_NAMES[basin_id]
    graph_dir = data_root / "reach_graph"
    nodes_path = graph_dir / f"{file_key}_nodes.csv"

    if not nodes_path.exists():
        print(f"{file_key}: no {nodes_path} found -- run scripts/build_reach_graphs.py first. Skipping.")
        return

    base_nodes = pd.read_csv(nodes_path)
    print(f"{file_key}: {len(base_nodes)} nodes loaded from {nodes_path}")

    enriched, report = build_node_features(
        base_nodes_df=base_nodes,
        idpr_path=data_root / "idpr.csv",
        ades_path=data_root / "ades",
        safran_path=None if skip_climate else data_root / "safran",
        # --skip-climate exists specifically because add_safran_features has
        # not been verified at reach-graph scale (~2,900 nodes) and is the
        # prime suspect for a real hang there, unlike everything else in
        # this pipeline which has been checked at this scale already (see
        # add_groundwater_features's rewrite, which hit exactly this class
        # of bug once before). Use this flag to get unblocked on everything
        # else while that gets diagnosed with the real source.
        catchment_path=data_root / "catchment_area.csv",
        hydrometric_path=data_root / "hydrometric",
        date_range=date_range,
        # Safe to include here, unlike an earlier version of this script
        # assumed: target_* attaches via a left-merge on station_code,
        # and real gauge codes ("H...") can never collide with BD TOPO
        # hydrographic node IDs ("NOEUDHYD...") or virtual infill node
        # IDs ("VIRTUAL::..."), confirmed against the real station code
        # namespace -- so only real gauge rows ever get a real target
        # value; every confluence/virtual node correctly gets NaN, not
        # a mislabeled one.
    )
    print(report)

    n_climate = enriched["climate_precip_mm"].notna().sum() if "climate_precip_mm" in enriched.columns else 0
    n_catchment = enriched["catchment_area_km2"].notna().sum() if "catchment_area_km2" in enriched.columns else 0
    n_targets = enriched["target_discharge_m3s_mean"].notna().sum() if "target_discharge_m3s_mean" in enriched.columns else 0
    print(f"  climate coverage: {n_climate}/{len(enriched)} nodes")
    print(f"  target (discharge) coverage: {n_targets}/{len(enriched)} nodes "
          f"(expected: only real gauges -- {int(base_nodes['is_gauged'].sum())} in this basin)")
    print(f"  catchment_area coverage: {n_catchment}/{len(enriched)} nodes "
          f"(expected: only real gauges have one -- see node_features.py's "
          f"add_catchment_features docstring; this is what water_balance_loss "
          f"can actually use right now, everything else needs a cumulative "
          f"catchment estimate this script doesn't build)")

    out_path = graph_dir / f"{file_key}_nodes_enriched.csv"
    enriched.to_csv(out_path, index=False)
    print(f"  saved to {out_path}")
    print()


def main() -> None:
    parser = argparse.ArgumentParser(description="Enrich reach graph nodes with feature loaders")
    parser.add_argument("--data-root", type=Path, default=Path("datasets"))
    parser.add_argument("--skip-climate", action="store_true",
                         help="Skip SAFRAN/ERA5 climate features -- use if that step hangs at reach-graph scale")
    parser.add_argument("--start-date", type=str, default="2013-01-01",
                         help="Start of the training period (inclusive). Default 2013-01-01: the window "
                              "maximizing discharge-gauge coverage (6/8 stations, 13,084 observations) -- "
                              "see the date-range analysis this default came from before changing it.")
    parser.add_argument("--end-date", type=str, default="2026-12-31",
                         help="End of the training period (inclusive).")
    parser.add_argument("--no-date-filter", action="store_true",
                         help="Disable date filtering entirely -- each source uses its own full history")
    args = parser.parse_args()

    date_range = None if args.no_date_filter else (args.start_date, args.end_date)
    if date_range:
        print(f"Using date range: {date_range[0]} to {date_range[1]}")
        print()

    for basin_id in BASIN_FILE_NAMES:
        enrich_basin(args.data_root, basin_id, skip_climate=args.skip_climate, date_range=date_range)


if __name__ == "__main__":
    main()