Spaces:
Running
Running
Download scripts/prep_virtual_points.py from vincewin/CREST_runner: direct link, hf CLI and curl.
- Browser
- Download file 7.9 kB
-
https://huggingface.co/spaces/vincewin/CREST_runner/resolve/main/scripts/prep_virtual_points.py
- Command line
-
hf download hf://spaces/vincewin/CREST_runner/scripts/prep_virtual_points.py
-
curl -L -o prep_virtual_points.py https://huggingface.co/spaces/vincewin/CREST_runner/resolve/main/scripts/prep_virtual_points.py
7.9 kB
| """One-time prep: HydroBASINS pour points -> CONUS virtual-point catalog. | |
| Virtual points fill the gaps between USGS gauges so every part of CONUS has a | |
| simulatable hindcast outlet. Pfafstetter level 07 pour points (one per | |
| sub-basin outlet, ~1,800 km2 nominal sub-basins) give seamless coverage at | |
| ~1/3 the density of the gauge network; points that duplicate an existing | |
| gauge (same HydroRIVERS reach, or within GAUGE_DUP_KM) are dropped because | |
| the gauge is strictly better there (it has observations). | |
| Each point is snapped to its HydroRIVERS reach to inherit the modeled | |
| drainage area (UPLAND_SKM) — EF5 uses that as BASINAREA to lock the outlet | |
| onto the right flow-accumulation cell. Coastal aggregate sub-basins carry one | |
| pour point per outlet; points below MIN_UPLAND_KM2 (beach-scale watersheds | |
| the MRMS+HydroSHEDS setup can't resolve) are dropped, and only the nearest | |
| point per reach survives. | |
| Builds gauges/virtual_points.parquet in vincewin/CREST_data, consumed by | |
| hf_data/virtualpoints.py. Ids are "V" + HYRIV_ID (unique after the per-reach | |
| dedupe, and can never collide with 8-digit USGS ids). | |
| python scripts/prep_virtual_points.py [--level 07] [--dry-run] | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import io | |
| import os | |
| import sys | |
| import tempfile | |
| import zipfile | |
| import numpy as np | |
| import pyarrow as pa | |
| import pyarrow.parquet as pq | |
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) | |
| import truststore | |
| truststore.inject_into_ssl() | |
| from forcing_update_common import HF_REPO, hf_token # noqa: E402 | |
| POUR_DIR = r"E:\hydroZone\pour_points" | |
| OUT_PATH = "gauges/virtual_points.parquet" | |
| BBOX = (-125.0, 24.0, -66.5, 50.0) # CONUS | |
| MIN_UPLAND_KM2 = 100.0 | |
| GAUGE_DUP_KM = 2.0 | |
| SNAP_RADIUS_DEG = 0.05 | |
| KM_PER_DEG = 111.0 | |
| NATION_URL = ("https://www2.census.gov/geo/tiger/GENZ2023/shp/" | |
| "cb_2023_us_nation_5m.zip") | |
| def conus_polygon(cache_dir: str): | |
| """Census national boundary clipped to the CONUS bbox (drops AK/HI/PR).""" | |
| import requests | |
| import pyogrio | |
| from shapely.geometry import box | |
| shp = os.path.join(cache_dir, "cb_nation", "cb_2023_us_nation_5m.shp") | |
| if not os.path.exists(shp): | |
| os.makedirs(os.path.dirname(shp), exist_ok=True) | |
| r = requests.get(NATION_URL, timeout=120) | |
| r.raise_for_status() | |
| zipfile.ZipFile(io.BytesIO(r.content)).extractall(os.path.dirname(shp)) | |
| nat = pyogrio.read_dataframe(shp) | |
| return nat.geometry.iloc[0].intersection(box(*BBOX)) | |
| def main() -> int: | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--level", default="07") | |
| ap.add_argument("--dry-run", action="store_true") | |
| args = ap.parse_args() | |
| token = hf_token() | |
| import pyogrio | |
| from shapely.prepared import prep | |
| from scipy.spatial import cKDTree | |
| from huggingface_hub import HfApi, hf_hub_download, CommitOperationAdd | |
| cache = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), | |
| "_cache") | |
| conus = prep(conus_polygon(cache)) | |
| shp = os.path.join(POUR_DIR, f"hybas_pour_lev{args.level}_v1.shp") | |
| print("reading", shp, "…", flush=True) | |
| df = pyogrio.read_dataframe(shp, bbox=BBOX) | |
| inside = np.fromiter((conus.contains(g) for g in df.geometry), bool, len(df)) | |
| df = df[inside] | |
| px = df.geometry.x.to_numpy("float64") | |
| py = df.geometry.y.to_numpy("float64") | |
| hyb = df.HYBAS_ID.to_numpy("int64") | |
| print(f" {inside.sum():,} pour points inside CONUS (of {len(inside):,} in bbox)") | |
| # -- snap to HydroRIVERS reaches (drainage area + dedupe key) -------------- | |
| riv = pq.read_table(hf_hub_download(HF_REPO, "rivers/hydrorivers_na.parquet", | |
| repo_type="dataset", token=token)) | |
| rid = riv.column("id").to_numpy() | |
| rup = riv.column("upland").to_numpy() | |
| rord = riv.column("ord").to_numpy() | |
| lats_l = riv.column("lats").to_pylist() | |
| lons_l = riv.column("lons").to_pylist() | |
| flat_lon = np.concatenate([np.asarray(a, "float64") for a in lons_l]) | |
| flat_lat = np.concatenate([np.asarray(a, "float64") for a in lats_l]) | |
| owner = np.repeat(np.arange(len(rid)), [len(a) for a in lats_l]) | |
| tree = cKDTree(np.column_stack([flat_lon, flat_lat])) | |
| dists, vidx = tree.query(np.column_stack([px, py]), k=1, | |
| distance_upper_bound=SNAP_RADIUS_DEG, workers=-1) | |
| ok = np.isfinite(dists) | |
| reach_i = np.where(ok, owner[np.clip(vidx, 0, len(owner) - 1)], -1) | |
| up = np.where(ok, rup[np.clip(reach_i, 0, len(rup) - 1)], 0.0) | |
| keep = ok & (up >= MIN_UPLAND_KM2) | |
| print(f" {keep.sum():,} snapped to a reach with >= {MIN_UPLAND_KM2:.0f} km² upland") | |
| # -- drop duplicates: same reach (keep nearest), gauged reach, near-gauge -- | |
| cat = pq.read_table(hf_hub_download(HF_REPO, "gauges/gagesII_9322.parquet", | |
| repo_type="dataset", token=token)) | |
| glat = cat.column("LAT_GAGE").to_numpy().astype("float64") | |
| glon = cat.column("LNG_GAGE").to_numpy().astype("float64") | |
| gid = np.array([str(s).zfill(8) for s in cat.column("STAID").to_pylist()]) | |
| gtree = cKDTree(np.column_stack([glon, glat])) | |
| snap = pq.read_table(hf_hub_download(HF_REPO, "rivers/gauge_reach.parquet", | |
| repo_type="dataset", token=token)) | |
| gauged_reaches = set(snap.column("reach_id").to_pylist()) | |
| order = np.argsort(dists[keep]) # nearest snap wins its reach | |
| cand = np.where(keep)[0][order] | |
| seen: set[int] = set() | |
| sel: list[int] = [] | |
| n_dup_reach = n_gauged = 0 | |
| for i in cand: | |
| ri = int(reach_i[i]) | |
| if ri in seen: | |
| n_dup_reach += 1 | |
| continue | |
| seen.add(ri) | |
| if int(rid[ri]) in gauged_reaches: | |
| n_gauged += 1 # a USGS gauge owns this reach | |
| continue | |
| sel.append(i) | |
| sel = np.array(sel) | |
| gd, gi = gtree.query(np.column_stack([px[sel], py[sel]]), workers=-1) | |
| gd_km = gd * KM_PER_DEG | |
| far = gd_km >= GAUGE_DUP_KM | |
| print(f" dropped: {n_dup_reach:,} same-reach dups, {n_gauged:,} on gauged " | |
| f"reaches, {(~far).sum():,} within {GAUGE_DUP_KM:.0f} km of a gauge") | |
| sel, gd_km, gi = sel[far], gd_km[far], gi[far] | |
| print(f" -> {len(sel):,} virtual points") | |
| # -- gauge-local timezone for the hydrograph axis -------------------------- | |
| from timezonefinder import TimezoneFinder | |
| tf = TimezoneFinder(in_memory=True) | |
| tzs = [tf.timezone_at(lng=float(px[i]), lat=float(py[i])) or "America/Chicago" | |
| for i in sel] | |
| ri = reach_i[sel] | |
| tbl = pa.table({ | |
| "vp": [f"V{int(rid[r])}" for r in ri], | |
| "lat": py[sel], | |
| "lon": px[sel], | |
| "area_km2": rup[ri].astype("float32"), | |
| "reach_id": rid[ri].astype("int64"), | |
| "hybas_id": hyb[sel], | |
| "ord": rord[ri].astype("int8"), | |
| "near_gid": [gid[j] for j in gi], | |
| "near_km": gd_km.astype("float32"), | |
| "tz": tzs, | |
| "level": np.full(len(sel), int(args.level), "int8"), | |
| }) | |
| a = rup[ri] | |
| print(f" area km²: p10={np.percentile(a, 10):.0f} med={np.median(a):.0f} " | |
| f"p90={np.percentile(a, 90):.0f} max={a.max():.0f}") | |
| if args.dry_run: | |
| print("[dry-run: no upload]") | |
| return 0 | |
| tmp = os.path.join(tempfile.mkdtemp(), "virtual_points.parquet") | |
| pq.write_table(tbl, tmp, compression="zstd") | |
| print("parquet:", round(os.path.getsize(tmp) / 1e6, 2), "MB") | |
| api = HfApi(token=token) | |
| api.create_commit(repo_id=HF_REPO, repo_type="dataset", | |
| operations=[CommitOperationAdd(OUT_PATH, tmp)], | |
| commit_message=f"virtual points lev{args.level}: " | |
| f"{len(sel):,} CONUS pour points") | |
| print("uploaded", OUT_PATH) | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |