Repository navigation
Gridded (ISIMIP DDM30) routing: network, attributes, forcing, and training - #194
Merged
Merged
Conversation
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
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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.pyand 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 scatterterrain.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 floorforcing.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 1validate.py— flow-direction, unit-range, coverage and consistency checksScripts — acquisition (
acquire_merit_hydro.py,acquire_gee_attributes.py), builders (build_gridded_attributes.py,build_gridded_qprime.py,build_subdivided_adjacency.py), andvalidate_gridded.pyExamples —
examples/juniata_gridded/, a self-contained sample mirroringexamples/juniata. Ships a 1.6 MB bundle, so the whole thing runs with no external stores:CONUS training and eval-plot scripts alongside it, mirroring the ddrs eval-plot families.
Docs —
docs/gridded_data.mdwalks 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 inwiki/, which is gitignored and so never reached users.Data products (on
/mnt/ssd1, not in git)ddm30_conus_attributes.{nc,ic}+_grid.ncCOMID(themerit_global_attributes_v2layout, so ddr and ddrs read it unchanged), icechunk keyed ondivide_id, plus the 280×720 ISIMIP gridddm30_global_attributes.*ddm30_conus_uh_retrospective_regridded.icddm30_subreach_adjacency.zarrAttributes 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;qreaches 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
pandqdrift wherever they help the hydrograph.p_spatial's log-space range of [1, 200] is the first suspect.Notes
wiki/plan-gridded-juniata.md(wiki is gitignored). Its open question about thejuniata-samplebranch is resolved: Juniata single-catchment sample: self-contained bundle, train/test module, physics notebook #193 merged it, andexamples/juniata_gridded/now sits alongsideexamples/juniata/rather than duplicating its scaffolding