Skip to content

Gridded (ISIMIP DDM30) routing: network, attributes, forcing, and training - #194

Merged
taddyb merged 35 commits into
DeepGroundwater:masterfrom
taddyb:feat/gridded-engine
Sep 9, 2026
Merged

taddyb merged 35 commits into
DeepGroundwater:masterfrom
taddyb:feat/gridded-engine

Conversation

@taddyb

@taddyb taddyb commented Sep 8, 2026 •

Copy link
Copy Markdown
Collaborator

Adds a gridded routing path to DDR: build a river network from the ISIMIP DDM30 0.5° drainage-direction grid, aggregate the attributes it needs from native rasters, regrid the forcing onto it, and train the KAN on it at Juniata and CONUS scale.

The routing engine, mmc.py, merit.py and the MERIT/Lynker paths are untouched. Everything here is additive.

What's in it

Engine (ddr_engine.gridded)

  • attributes.py — 0.5° cell polygons, extractrs coverage-weighted zonal means onto cells (reprojecting cells into each raster's CRS), in-memory extraction for derived rasters, cell areas, and a cell-table → lat/lon grid scatter
  • terrain.py, climate.py — slope from a DEM using its own pixel size, FAO-56 extraterrestrial radiation, Hargreaves PET, Walsh & Lawler seasonality, snowfall fraction, aridity with a precipitation floor
  • forcing.py — catchment→cell area weights and mass-conserving redistribution of per-catchment Q′
  • subdivide.py — split cells into sub-reaches so Muskingum-Cunge runs near Courant 1
  • validate.py — flow-direction, unit-range, coverage and consistency checks

Scripts — acquisition (acquire_merit_hydro.py, acquire_gee_attributes.py), builders (build_gridded_attributes.py, build_gridded_qprime.py, build_subdivided_adjacency.py), and validate_gridded.py

Examples — examples/juniata_gridded/, a self-contained sample mirroring examples/juniata. Ships a 1.6 MB bundle, so the whole thing runs with no external stores:

uv run python examples/juniata_gridded/train_gridded.py --epochs 30
uv run python examples/juniata_gridded/eval_plots.py --run examples/juniata_gridded/runs/latest

CONUS training and eval-plot scripts alongside it, mirroring the ddrs eval-plot families.

Docs — docs/gridded_data.md walks from nothing to a built store: Earth Engine setup, DDM30 download, the raster acquisition script, direct downloads (SoilGrids, WorldClim, GLHYMPS, MERIT catchments), building the attribute and forcing stores, gauge snapping, validation, and the data gotchas. This previously lived only in wiki/, which is gitignored and so never reached users.

Data products (on /mnt/ssd1, not in git)

Product Contents
ddm30_conus_attributes.{nc,ic} + _grid.nc 25 attributes × 5,198 CONUS cells; NetCDF keyed on COMID (the merit_global_attributes_v2 layout, so ddr and ddrs read it unchanged), icechunk keyed on divide_id, plus the 280×720 ISIMIP grid
ddm30_global_attributes.* the same for all 67,424 cells of the ISIMIP grid
ddm30_conus_uh_retrospective_regridded.ic dHBV2 UH Q′ area-weighted onto cells, 4,085 cells × 14,976 days
ddm30_subreach_adjacency.zarr 91,867 Courant-1 sub-reaches

Attributes come from native rasters only. Q′ is volumetric per catchment, so moving it to cells is mass-conserving aggregation, verified to 8e-8; ~40% of catchments straddle a cell edge, which the previous flowline-midpoint assignment mishandled.

Results

Fixed-parameter benchmark, 321 gauges, WY1996: routed median NSE 0.587 against 0.437 for the summed-Q′ baseline.

Trained, Juniata (27 sub-reaches, 30 epochs): routed NSE 0.795 / KGE 0.737 against summed 0.594 / 0.671, zero negative solves.

Trained, CONUS (620 gauges, 45 epochs): median NSE 0.542 against summed 0.390, beating the baseline at 81% of gauges, NSE > 0.5 at 336.

Validation suite: 67 checks passing on the CONUS store.

Findings worth review

Sub-reach splitting fixes conditioning but not daily skill. At 0.5° with dt = 3600 s the Cunge X saturates at its 0.5 cap, so c₁ is negative in ~88% of states. Splitting to Courant ≈ 1 drops negative solves 10× but leaves paired ΔNSE at −0.0002 across 321 gauges. The implicit solve was already absorbing them, and mass conservation is guaranteed by c₁+c₂+c₃ = 1 and c₄ = c₁+c₂. Courant 1 is nonetheless the default, on the argument that a well-conditioned operator should give cleaner gradients for training; that specific claim is not yet measured.

Three quarters of gages_3000 cannot be represented at 0.5°. 2,416 of 3,211 gauges drain less than one cell (~2,350 km²). Training uses the 620 above 2,000 km² that snap within drainage-area tolerance.

Parameter medians converge, but their spreads saturate against the bounds. An untrained KAN emits sigmoid ≈ 0.5, which denormalizes to the midpoint of n's [0.02, 0.2] bounds, i.e. 0.111 — a floodplain roughness, not a channel one. Over 45 epochs the median rises to 0.145 by epoch 17 then settles at 0.127, but the 10–90 spread grows the entire run to 0.05–0.19, pressing the ceiling; q reaches 0.93 against a maximum of 1.0. The extra 33 epochs bought 0.017 NSE for 3.2 hours. Worth deciding whether to narrow the bounds or bias the output layer so training starts somewhere physical.

Channel geometry is not sensible and longer training makes it worse. Downstream exponents are width b = −0.09 (Leopold & Maddock ≈ 0.5) and depth f = 0.75 (≈ 0.4) at 45 epochs, against b = 0.08 at 12 — so larger rivers are now made slightly narrower, which is backwards. Width-to-depth falls below 10 above roughly 4,000 km². Nothing in an L1-on-daily-flow objective rewards physical channel shape, so p and q drift wherever they help the hydrograph. p_spatial's log-space range of [1, 200] is the first suspect.

Notes

taddyb and others added 30 commits September 1, 2026 22:20
New ddr_engine.gridded sub-package for the ISIMIP DDM30 0.5-degree river
network. A D8 flow-direction raster maps to the existing pipeline after
the upstream dict, so merit.build_graph, subset_upstream, create_subset_coo
and the core zarr I/O are reused unmodified.

- graph.py: DDM30 direction codes -> downstream/upstream connectivity
  (lon wrap, codes 0 and -1 as terminals, off-grid/NaN diagnostics)
- build.py: toposort + lower-triangular COO, haversine cell lengths,
  build_ddm30_adjacency (COO + length_m/slope/lat/lon/basin aligned to
  order), gauge snapping and per-STAID upstream subsets
- core/converters.py: GridCellOrderConverter registered as "ddm30"
- CLI: python -m ddr_engine.gridded <nc_dir> [--path] [--gages csv]
- 47 tests on a 4x4 sandbox grid plus real-data smoke tests (67,424
  cells / 55,866 edges; 4-cell Juniata above USGS 01567000) that skip
  when data/ddm30/ is absent

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
ddr_benchmarks.gridded: pure helpers for benchmarking DDR on the DDM30
grid - point-to-cell assignment, downstream network closure, topological
accumulation (drainage areas / summed-Q' baseline), and 3x3
drainage-area-matched gauge snapping (median da_ratio 1.02 for 392/406
CONUS gauges >5000 km2; naive nearest-center snapping loses half).

ddr_benchmarks.gridded_runner.run_conus_benchmark: routes dHBV2 UH daily
Qr aggregated per cell (flowline midpoints, cached parquet) through
MuskingumCunge with fixed parameters (n=0.05, q_spatial=0.4, no KAN) and
scores daily discharge vs USGS observations with a summed-Q' baseline.

Measured (WY1996-1997, 7994 cells, 321 gauges, 28s CPU): routed median
NSE 0.629 vs 0.480 summed baseline; routing improves 296/321 gauges;
neg_solve rate 0.049% - the dt << K negative-coefficient concern did not
materialize with per-timestep Cunge X.

Tests: 14 unit tests on synthetic networks; integration-marked
acceptance test asserting >=200 gauges, neg_solve <0.5%, median routed
NSE >=0.55, and routing beating the baseline at >=75% of gauges
(5/5 passing locally in 33s).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
Standalone PEP 723 uv script: pulls MERIT/Hydro/v1_0_1 elv/dir/upa/wth
clipped to a bbox (CONUS default) as MERIT-convention GeoTIFF tiles into
/mnt/ssd1/data/merit_hydro/rasters/, resumable via skip-if-exists.
Pure tile-math helpers TDD'd (11 tests); GEE imports lazy so tests run
without earthengine-api/geedim installed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
geedim 1.9's Initialize reads the removed ee.data._credentials attr and
crashes with current earthengine-api; geedim 2.0 keeps the same
MaskedImage.download interface (crs/crs_transform/shape via
prepareForExport kwargs). Verified with a real n40w080 elv tile download.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
Nine CONUS layers for the gridded KAN: GMTED2010 mean elevation,
HiHydroSoil v2.0 x6 (0-5cm), GLWD v2 total area pct, MOD13A2 NDVI mean
(2000-2019), MOD10A1 snow-cover mean (2000-2020). Declarative Layer
registry (Image / ic_filter / ic_mean with valid-range masking), single
GeoTIFF per layer at native scale, resumable. 9 tests on the registry
and selection logic; asset IDs and band names probed live before coding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
cell_polygons builds the 0.5deg DDM30 cell GeoDataFrame from flat cell
ids; extract_cell_attributes runs coverage-weighted zonal means of any
raster stack onto cells (per-column scale factors, NaN outside coverage,
geedim FILL_MASK band skipped). Smoke-verified against downloaded GMTED
elevation and HiHydroSoil Ksat over the four Juniata cells.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
MOD10A1 21-yr mean tripped EE's interactive concurrency limit at 32
parallel tile requests x 500 m. Add per-layer max_requests (snow: 4) and
export snow at 1 km - ample for 0.5-degree cell aggregation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
Projected rasters (SoilGrids in Goode Homolosine) silently returned
all-NaN: the clip window was computed in degrees. Cells are now
transformed to the raster CRS for clip, intersection and zonal stats,
which also keeps coverage weighting in an equal-area frame. Verified:
SoilGrids clay+sand+silt sum to 100% per Juniata cell.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
Song et al. define FW as "fraction of open water" from GLWD v1, whose
water classes were lakes, reservoirs and rivers. Sum GLWD v2 classes 1-6
server-side (small streams excluded to match v1); keep the all-class
total for reference. New "ic_sum" registry kind.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
The 21-year daily mean at 1 km took ~8 min per tile on Earth Engine
(hours for CONUS). MOD10CM, the paper's snow source, is a 0.05-degree
CMG, so 5 km matches its native resolution and is 100x finer than the
0.5-degree cells it feeds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
catchsize per cell and a scatter of the cell-indexed attribute table onto
the (lat, lon) DDM30 grid for ISIMIP-style output.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
…rid)

Aggregates the acquired native rasters onto CONUS DDM30 cells via
extractrs and writes (1) a COMID-dimensioned NetCDF in the exact layout
of merit_global_attributes_v2.nc, loadable by ddr's AttributesReader and
ddrs without code changes, and (2) the same variables on the 280x720
(lat, lon) grid for ISIMIP-style tooling.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
At 4 MB tiles the largest CONUS snow tile timed out client-side (~30 min)
while the other three finished in <3 min; 0.5 MB tiles keep each request
well under geedim's timeout.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
HiHydroSoil WCsat has an unflagged int32-max overflow pixel (California
coast) that poisoned one cell mean (theta_sat = 7.6). extract_cell_attributes
gains an optional per-raster valid_range applied before zonal stats; the
builder bounds all HiHydroSoil layers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
…ngine

terrain.slope_degrees: lat/lon DEM slope with cos(lat) east-west pixel
size. climate: FAO-56 extraterrestrial radiation (verified against the
FAO-56 worked example) and annual Hargreaves PET from monthly tmin/tmax.
attributes.extract_from_dataarray: zonal stat of an in-memory DataArray
(mean/max/...) so derived rasters need not hit disk; extract_cell_attributes
now delegates to it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
meanslope (deg) and log10_uparea (log10 km2) per cell from the 3" MERIT
Hydro tiles (per-tile; tile edges align with cell edges); meanP, meanTa,
ETPOT_Hargr (Hargreaves from monthly tmin/tmax) and aridity = PET/P from
WorldClim 2.1 30" zips read in place via vsizip and clipped to CONUS.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
…ax-upa

MERIT max upa reports the largest river touching a cell (Juniata outlet
cell -> 60,414 km2 from the Susquehanna clipping its corner) while the
DDM30 network routes 9,419 km2 through it; provide both so the KAN input
choice is explicit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
Per discussion: upstream area is the cumulative area function over the
routing network (headwater cell = own area, each downstream cell adds all
upstream). The MERIT max-upa column stays as log10_uparea_merit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
… store

Walsh & Lawler seasonality index over the WorldClim monthly precipitation
and monthly Hargreaves PET stacks, and a precipitation-weighted snowfall
fraction with a -1..3 C rain/snow ramp. The builder now also writes an
icechunk repo (dim divide_id = cell id) alongside the two NetCDFs, so the
store loads through ddr's icechunk attribute path as well as the MERIT
NetCDF path used by ddrs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
…les)

Per-layer global_scale_m: GMTED at 30" (matches the paper's GTOPO30-era
slope lineage), HiHydroSoil/GLWD/NDVI at 1 km, snow at 5 km - all still
>=50x finer than the 0.5-degree cells they feed, and tractable as single
global GeoTIFFs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
The CONUS GMTED export is 7.5" while the global export is 30"; assuming
30" everywhere made CONUS meanslope 4x too small (median 0.43 deg vs the
2.5 deg the MERIT tiles give). slope_from_dataarray reads the resolution
from the DataArray's own coordinates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
ddr_engine.gridded.validate holds the checkable invariants (flow-direction
topology, documented unit ranges, cross-variable identities, area mass
balance) with 14 unit tests; scripts/validate_gridded.py runs them against
a built store and adds two Muskingum-Cunge sections: coefficient sanity at
area-scaled realistic flows, and a synthetic flood routed through the real
network with ddr's implicit solve.

At 0.5 deg with dt=3600 s the Cunge X saturates at its 0.5 cap, so dt << 2KX
and c1 is negative for most cells - the expected regime for this grid, which
is why the implicit-solve checks gate rather than coefficient positivity.

Also NaN the SoilGrids texture where all three fractions read 0: SoilGrids
fills 0 over permanent water (33 Great Lakes cells) while HiHydroSoil
correctly returns nodata there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
…ntities

All 67 checks now pass on the CONUS store. Additions:
- per-attribute coverage (missing-data fraction), which surfaced one cell
  outside WorldClim's land mask whose NaN was poisoning the routing solve
- texture sum uses min_count so all-missing water cells are skipped
- c1+c2+c3=1 and c4=c1+c2 asserted directly: together they prove the MC
  steady state is exactly topological accumulation, i.e. mass conserving
- routing spin-up iterates to convergence instead of a fixed horizon
  (CONUS needs 2909 h; 72 h only fills 22% of the network)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
Q' is a volumetric flux per MERIT unit catchment, so moving it to cells is
mass-conserving aggregation: ddr_engine.gridded.forcing splits each
catchment's discharge across the cells it overlaps in proportion to
overlapping area (exact for straddling catchments, unlike the benchmark's
flowline-midpoint assignment) and sums per cell. Weights are cached to
parquet; scripts/build_gridded_qprime.py streams the retrospective in
5-year chunks into an icechunk store with dim divide_id = cell id.

Verified on 2000 catchments: regridded daily total matches the source to
8e-8 relative.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
The global GLWD open-water layer sums its six class percentages to 101 in
places (per-class rounding), which would put FW at 1.01 and fail the unit
check. Clip FW and snow_fraction to [0, 1].

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
polygon_area_mean does an area-weighted mean of polygon attributes over
cells, overlaying in the polygons' own CRS so GLHYMPS's equal-area
projection gives true weights. The builder reads GLHYMPS per block with a
bbox filter, so the 1.1 GB global vector never loads whole; the arithmetic
mean of log10 k is the geometric mean of k, the standard averaging for
permeability. Juniata: porosity 0.10-0.17, permeability -14.5 to -15.4
log10 m2, both inside merit_global_attributes_v2's ranges.

This removes the plan's Porosity -> WCsat substitution: the KAN input set
can now match the MERIT one attribute for attribute.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
Windowed reads through zip:// decompress from the start of each deflate
block, which dominated the global build (~2 min per 30-degree block even
for blocks with 3 cells). Plain GeoTIFFs are seekable; extract once with
unzip -j into worldclim/tif and the zips stay the fallback.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
Hyper-arid cells round to 0 mm/yr in WorldClim (14 cells globally) and
cell means over mostly-zero pixels reach ~1e-4 mm/yr, so a raw PET/P went
infinite and up to 2.4e6, failing the unit check. aridity_index floors P
at 1 mm/yr, WorldClim's reporting resolution: finite, monotone, and the
driest cells still rank highest (Sahara core ~2300).

Also point the scripts at ddm30_conus_uh_retrospective_regridded.ic,
renamed by the user from ddm30_conus_qprime.ic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
…ility

At 0.5 deg (33-79 km) with dt=3600 s the Cunge X saturates at 0.5 and c1 is
negative in 88.8% of cell-flow states. Splitting each cell into a chain of
sub-reaches raises the Courant number toward 1, where all Muskingum
coefficients are non-negative, without touching dt or any shared routing
code: ddr just sees a bigger lower-triangular network.

Measured on CONUS:
  cells (55.6 km)          5,197 nodes  median C 0.09  any coeff<0 88.8%
  fixed 6 km              45,153 nodes  median C 0.81  any coeff<0 61.3%
  Courant-matched (~5 km) 94,491 nodes  median C 1.00  any coeff<0 22.7%

The non-negativity condition reduces to c*dt - beta <= L <= c*dt + beta with
beta = Q/(T*S*c), so a fixed target only suits cells whose celerity is near
target/dt; celerity spans 0.6-2.4 m/s across CONUS. The residual 22.7% are
cells whose window is narrower than integer subdivision can hit (verified:
choosing the nearest achievable L rather than rounding k changes nothing).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
taddyb and others added 5 commits September 7, 2026 14:02
Celerity needs a discharge, and the summed-Q' baseline (topological
accumulation of the regridded forcing) is the honest estimate: 148.7 m3/s
at the Juniata outlet against 139.1 observed, where a 0.3*P*area proxy
gives 94.6.

Sizing flow is configurable. Measured time-weighted over 5 years, mean
flow is the right default - sizing for floods is worse overall because
most days are near mean flow and long reaches push c1 negative there:
  sized at mean  127k nodes  53.6% negative cell-days
  sized at p99    78k nodes  69.8%
  sized at max    62k nodes  74.0%

Also: cells with no forcing now fall back to the fixed 6 km target instead
of staying whole at 55 km, and reaches are floored at 1 km (a slow cell was
asking for 52 m). Reach lengths now span 1.0-27.8 km.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
run_subreach_benchmark routes a subdivided network: each cell's Q' from the
area-weighted regridded store is split evenly across its sub-reaches (mass
conserving) and a gauge reads the cell's most downstream sub-reach, which
carries the full cell accumulation. Routing and scoring are shared with the
cell benchmark via _route_and_score.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
train_gridded.py trains the KAN on the Juniata's 27 DDM30 sub-reaches;
train_conus.py scales to the usable subset of gages_3000 (620 gauges above
2,000 km2 - 75% of the 3,211 drain less than one 0.5 degree cell and cannot
be represented). Both learn n, q_spatial and p_spatial from the same ten
attributes as the MERIT config and dump parameters, predictions and
per-epoch history.

eval_plots.py and eval_plots_conus.py mirror the ddrs eval-plot families:
hydrographs, parameter maps and relations, convergence, metric
distributions against the summed-Q' baseline, and the channel-geometry
check against Leopold & Maddock exponents.

Training applies the same drainage-area correction as evaluation, so the
KAN is not asked to absorb a per-gauge area mismatch (0.82-1.21) into
roughness.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
The example now ships a 1.6 MB bundle (sub-reach subgraph, CONUS attribute
table for normalisation, Q' and observations for the four cells, gauge
metadata) so it runs with no external stores, matching examples/juniata.
extract_bundle.py regenerates it; train_gridded.py reads it by default and
reproduces the reference run (NSE 0.795 / KGE 0.737 vs summed 0.594 / 0.671).

docs/gridded_data.md is the missing piece: how to obtain every input from
nothing. The DDM30 network, the Earth Engine rasters, the direct downloads
(SoilGrids, WorldClim, GLHYMPS, MERIT catchments), building the attribute
and forcing stores, gauge snapping, validation, and the accumulated
gotchas. This lived only in wiki/, which is gitignored and so never
reached users.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZBLnrygRbcnyQvu2KL4vz
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant