Skip to content

Add routing module: steady-state runoff accumulation + peak discharge (1/2)#15

Merged
rehsani merged 1 commit into
mainfrom
step-8-routing-accumulation
May 4, 2026
Merged

Add routing module: steady-state runoff accumulation + peak discharge (1/2)#15
rehsani merged 1 commit into
mainfrom
step-8-routing-accumulation

Conversation

@rehsani
Copy link
Copy Markdown
Owner

@rehsani rehsani commented May 4, 2026

Summary

PR 1 of 2 for the routing milestone. Closes the hydrologic half of routing — turns SCS-CN per-cell runoff (mm) into accumulated upstream volume (m³) and peak discharge (m³/s) along the flow direction grid. The follow-up PR adds Manning normal-depth at stream cells to close the hydraulic half (discharge → water level → HAND-driven inundation).

Module surface

Item What
`accumulate_runoff(runoff, flow_grid)` Reproject Q (10 m → 30 m via area-weighted average) → mm × cell-area → m³ → `pyflwdir.accuflux` downstream
`peak_discharge(accumulated, duration_s)` Q_peak = V_acc / T per cell
`cell_areas_m2(transform, shape, crs)` Handles WGS84 cos(lat) shrinkage; works for projected CRSes too
`AccumulatedRunoffGrid`, `DischargeGrid` Carry method/precip-source attribution + helper stats

Why steady-state, why two PRs

  • Steady-state (V/T) is appropriate for v0.2 — matches the project's "single event snapshot" model and gives a deterministic answer without time stepping. Time-resolved hydrographs (unit hydrograph or kinematic wave) are a v0.3 ambition.
  • Two PRs keeps each diff reviewable. PR1 (this one) gives a discharge field you can plot and validate; PR2 adds the hydraulic closure (Manning normal-depth → water level at stream cells → integration into HAND inundation).

Resolution alignment

`accumulate_runoff` reprojects the runoff raster (WorldCover 10 m grid) onto the DEM 30 m grid using `Resampling.average` — the area-weighted mean is the correct aggregation for a depth quantity. Each coarse cell's volume is then `mean_depth × cell_area`, where cell area handles WGS84 longitude convergence.

Test plan

  • `pytest -m "not integration"` — 294 passed (264 prior + 30 new routing tests; 16 deselected integration)
  • `cell_areas_m2` sanity: equator vs 60° N (cos(60°)=0.5 → half area), southern vs northern hemisphere parity
  • Peak-discharge arithmetic: V/T pinned at 1.0 m³/s for 21,600 m³ over 6 hr; halving T doubles Q; negative T raises
  • Robit Bata pinned values: outlet V_acc = 1.43 Mm³ (matches independent water-balance estimate), outlet peak Q = 66.2 m³/s for 100 mm × 6 hr event
  • Outlet of accumulated runoff matches the cell with maximum flow accumulation (same flow direction grid drives both)
  • End-to-end smoke test on Robit Bata: 17 stages all succeed, including new stage 15 (two-panel V_acc + Q_peak with stream overlay)

Notes

  • The bbox is square-cropped, not a closed basin — most cells drain off the edges. Outlet accumulation captures only the basin upstream of the bbox's true outlet (~31% of the patch in Robit Bata's case). This is correct.
  • Steady-state assumption breaks down for large basins where peak attenuation matters. Document this as a known limitation; the kinematic-wave path is left for v0.3.

floodpath.routing closes the rainfall-runoff loop's hydrologic-routing
half. Per-cell SCS-CN runoff (mm) is reprojected onto the DEM-derived
flow direction grid, converted to volume (m^3) using lat-aware cell
areas, and accumulated downstream via pyflwdir's accuflux. Dividing the
accumulated volume by an event duration yields per-cell peak discharge
(m^3/s). PR1 of a planned two-PR routing sequence; the follow-up adds
Manning normal-depth at stream cells to close the hydraulic side.

- floodpath/routing/utils.py: cell_areas_m2(transform, shape, crs)
  handles WGS84 cos(lat) longitude convergence for geographic CRSes
  and falls back to abs(a*e) for projected CRSes.
- floodpath/routing/accumulate.py: accumulate_runoff(runoff, flow_grid)
  -> AccumulatedRunoffGrid. Reprojects Q via Resampling.average (correct
  for area-weighted depth aggregation), zero-fills NaN before accuflux
  so the SCS-CN nodata patch doesn't poison downstream cells.
- floodpath/routing/discharge.py: peak_discharge(accumulated, duration_s)
  -> DischargeGrid with Q_peak = V_acc / T per cell.
- AccumulatedRunoffGrid carries total_volume_m3() (=outlet, NOT sum of
  cells which would double-count); DischargeGrid carries outlet_peak().
- Tests: 30 new — cell_areas_m2 sanity at equator vs 60 deg N (verifies
  cos(lat) shrinkage), peak-discharge V/T arithmetic with NRCS-default
  6-hour duration, robust pinned values against the existing Robit Bata
  fixtures: outlet V_acc = 1.43 Mm^3, peak Q at outlet = 66.2 m^3/s for
  100 mm uniform rain over 6-hour event.

Smoke test gains stage 15 (two-panel: log-scale V_acc + Q_peak with
stream overlay) and a new --event-duration-h CLI flag (default 6.0).
@rehsani rehsani merged commit ae179f4 into main May 4, 2026
1 check passed
@rehsani rehsani deleted the step-8-routing-accumulation branch May 4, 2026 18:58
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