Spaces:
Running on Zero
Running on Zero
File size: 5,403 Bytes
4bb7968 | 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 110 111 112 113 114 115 116 117 | """
Builds genuine [n_nodes, T] dynamic tensors (discharge, groundwater,
climate) for a basin's reach graph and feeds them directly into
physics_losses.py's routing_consistency_loss -- the actual integration
point dynamic_features.py exists for. Without this script, that
function has real inputs it could consume but nothing actually
producing them.
Usage:
python -m scripts.build_dynamic_tensors --data-root datasets --basin risle
"""
import argparse
from pathlib import Path
import numpy as np
import pandas as pd
try:
from src.graph.dynamic_features import (
build_discharge_timeseries, build_groundwater_timeseries,
build_climate_timeseries, assemble_dynamic_tensor,
)
from src.graph.physics_losses import build_routing_index, routing_consistency_loss
except ImportError:
import sys
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from src.graph.dynamic_features import (
build_discharge_timeseries, build_groundwater_timeseries,
build_climate_timeseries, assemble_dynamic_tensor,
)
from src.graph.physics_losses import build_routing_index, routing_consistency_loss
BASIN_FILE_NAMES = {0: "eure", 1: "risle"}
def run_for_basin(data_root: Path, basin_id: int, file_key: str, date_range, skip_climate: bool) -> None:
graph_dir = data_root / "reach_graph"
nodes_path = graph_dir / f"{file_key}_nodes_enriched.csv"
edges_path = graph_dir / f"{file_key}_edges.csv"
if not nodes_path.exists() or not edges_path.exists():
print(f"{file_key}: missing enriched nodes/edges -- run build_reach_graphs.py + "
f"enrich_reach_graph.py first. Skipping.")
return
nodes_df = pd.read_csv(nodes_path)
edges_df = pd.read_csv(edges_path)
print(f"--- {file_key}: {len(nodes_df)} nodes, {len(edges_df)} edges ---")
discharge_wide = build_discharge_timeseries(nodes_df, data_root / "hydrometric", date_range)
Q, dates = assemble_dynamic_tensor(nodes_df, discharge_wide)
n_real = int((~np.isnan(Q)).sum())
print(f"discharge tensor: {Q.shape}, {n_real}/{Q.size} real (non-NaN) values "
f"({100*n_real/Q.size:.3f}% coverage -- expect this to be tiny, only real "
f"gauges with real observations ever have a value here)")
level_wide, depth_wide = build_groundwater_timeseries(nodes_df, data_root / "ades", date_range)
level_tensor, _ = assemble_dynamic_tensor(nodes_df, level_wide)
depth_tensor, _ = assemble_dynamic_tensor(nodes_df, depth_wide)
print(f"groundwater level tensor: {level_tensor.shape}, "
f"{int((~np.isnan(level_tensor)).sum())} real values")
climate_tensors = {}
if not skip_climate and (data_root / "safran").exists():
try:
climate_dict = build_climate_timeseries(nodes_df, data_root / "safran", date_range)
for var, wide in climate_dict.items():
tensor, _ = assemble_dynamic_tensor(nodes_df, wide)
climate_tensors[var] = tensor
print(f"climate variables: {list(climate_tensors.keys())}")
except Exception as e:
print(f"climate skipped (error: {e})")
out_dir = graph_dir / "dynamic"
out_dir.mkdir(exist_ok=True)
save_kwargs = {
"discharge": Q, "groundwater_level": level_tensor, "groundwater_depth": depth_tensor,
"dates": np.array([str(d) for d in dates]), "station_codes": nodes_df["station_code"].values,
}
save_kwargs.update({f"climate_{k}": v for k, v in climate_tensors.items()})
out_path = out_dir / f"{file_key}_dynamic.npz"
np.savez(out_path, **save_kwargs)
print(f"saved to {out_path}")
# The actual integration: routing_consistency_loss needs Q + routing_index together.
routing_index = build_routing_index(nodes_df, edges_df, timestep_hours=24.0)
print(f"routing_index: {len(routing_index)} edge(s) with a usable lag at this timestep")
loss = routing_consistency_loss(Q, routing_index)
print(f"routing_consistency_loss on the REAL discharge tensor: {loss}")
if np.isnan(loss):
print(" ^ NaN. This is exactly the thing worth checking before assuming this loss "
"is usable: real Q is almost entirely NaN (only real gauges with real "
"observations have values), and if routing_consistency_loss doesn't mask "
"NaN out of its residuals before averaging, one missing value anywhere "
"poisons the entire loss to NaN. See whether this fired.")
print()
def main() -> None:
parser = argparse.ArgumentParser(description="Build dynamic tensors and wire into physics_losses.py")
parser.add_argument("--data-root", type=Path, default=Path("datasets"))
parser.add_argument("--basin", choices=["eure", "risle", "both"], default="both")
parser.add_argument("--start-date", type=str, default="2013-01-01")
parser.add_argument("--end-date", type=str, default="2026-12-31")
parser.add_argument("--skip-climate", action="store_true")
args = parser.parse_args()
date_range = (args.start_date, args.end_date)
basins = BASIN_FILE_NAMES.items() if args.basin == "both" else \
[(k, v) for k, v in BASIN_FILE_NAMES.items() if v == args.basin]
for basin_id, file_key in basins:
run_for_basin(args.data_root, basin_id, file_key, date_range, args.skip_climate)
if __name__ == "__main__":
main() |