Files
2026-07-05 13:40:31 +03:00

4.6 KiB
Raw Permalink Blame History

swot river discharge pipeline

how it works (short version)

  • SWOT L2 HR RiverSP version D (SWOT_L2_HR_RiverSP_D), node + reach shapefiles from nasa earthdata
  • ALOS World 3D 30m DSM via GEE (or a local geotiff if you dont have GEE)
  • gauge WSE + discharge from india-WRIS
  • match each gauge to the nearest SWOT node (haversine), match times within ±3h
  • depth = WSE bed, A = width × depth, R = A/P, then Q = (1/n) · A · R^(2/3) · S^(1/2)
  • compare against gauge discharge, get R²/RMSE/NSE/bias + plots

setup

# Install UV

# Windows:
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

#linux/macOS:
curl -LsSf https://astral.sh/uv/install.sh | sh

nasa earthdata - first run will ask for username/password once and save them. also approve the "PO.DAAC" app on the earthdata site if downloads fail.

google earth engine - register at https://code.earthengine.google.com/register, then uv run earthengine authenticate and put your project id in config.yaml under gee.project.

gauge data - put the WRIS xlsx exports here:

data/gauge/wse/          River Water Level_<Station>.xlsx
data/gauge/discharge/    River Water Discharge_<Station>.xlsx

and add each station's lat/lon to stations.csv. station name has to match the filename part after the underscore ("(seasonal)" is ignored). get the exact coordinates from the station page on WRIS - if they're wrong the pipeline matches the wrong river and you get garbage (ask me how i know).

running it

uv run python run_pipeline.py                # all 8 steps
uv run python run_pipeline.py --steps 3-8    # skip the downloads
uv run python run_pipeline.py --steps 6,7,8  # just recompute discharge + export
# name what it does output
1 download SWOT granules from earthdata (~50gb for 2 yrs, skips already-downloaded files) data/swot/
2 process merge shapefiles, clip to basin, clean (slow, rerun only if AOI/dates change) data/processed/nodes_clean.csv etc
3 gauge parse the WRIS xlsx files (picks the MSL series, converts IST→UTC) gauge_wse.csv, gauge_discharge.csv
4 wse node matching + WSE validation matched_wse.csv, outputs/wse_metrics.csv
5 dem bed elevation (min DSM within 300m buffer) node_bed_elevation.csv
6 discharge manning's equation swot_discharge.csv
7 validate SWOT vs gauge discharge outputs/discharge_metrics.csv, plots
8 export everything into one excel workbook for analysis outputs/<project>_swot_analysis.xlsx

tuning knobs (config.yaml)

  • hydraulics.mannings_n - global roughness, default 0.035
  • hydraulics.mannings_n_per_station - override n per station, this is the main thing to play with. if a station is consistently overestimating by ~4x, try ~4x the n (it scales Q linearly). the n used shows up in the outputs so you always know which run used what
  • hydraulics.baseline_depth_min_m / baseline_depth_max_m - the DEM is unreliable at the channel (bank pixels, voids) so the implied low-water depth gets clamped into this band, default [0.5, 2.0]
  • quality.max_node_q, quality.swot_wse_mad_k, matching.wse_outlier_mad_k - outlier filtering. SWOT throws the occasional ±15m garbage observation, these catch it
  • matching.* - distance limit + time windows
  • hydraulics.scope: all - compute discharge at every node in the basin, not just gauge-matched ones (step 5 gets slower)

things to know before running the pipeline

  • the "bed elevation" from the DEM is really the water surface the satellite saw at acquisition time, so depths are relative, not true bathymetry. thats why I created a clamp
  • SWOT WSE is EGM2008, gauges are local MSL/datum - expect a constant offset (plots auto-shift when its > 1m, metrics report the raw bias)
  • dry season: SWOT sees pooled water behind weirs and manning's cant tell a standing pool from flowing water, so theres a discharge floor where the gauge says zero. like estimating traffic from how full the parking lot is - utter garbage data