Is the river through my town running high, and is a flood building upstream?
Draw a rectangle, then check which collections cover it. Most NASA data is global, so a big box matches a lot.
-91.3, 30.2 → -90.9, 30.6 (Lower Mississippi near Baton Rouge, USA)Water-surface elevation is the height of the top of a river, measured from orbit. A satellite breaks a river into short stretches (called reaches) and, each time it flies over, reports two numbers per reach: how high the water is, and how wide the channel is. Watch those numbers climb on the reaches above your town and you can spot a flood wave while it is still upstream.
Is the river through my town running high, and is a flood building upstream?
Water-surface elevation is the height of the top of a river, measured from orbit. A satellite breaks a river into short stretches (called reaches) and, each time it flies over, reports two numbers per reach: how high the water is, and how wide the channel is. Watch those numbers climb on the reaches above your town and you can spot a flood wave while it is still upstream.
Which data to use, and how
Use this dataset: SWOT river reaches
(short name SWOT_L2_HR_RiverSP_reach_D). SWOT is a NASA + CNES (the French space agency) satellite,
flying since 2023, that measures the height and width of any river wider than about 100 m. Each row
in the data is one reach, tagged with a reach_id, a wse (water-surface elevation, in metres), and a
width (in metres).
Verified locally: for the Lower Mississippi near Baton Rouge, this product read a real water height of 1.88 m (channel width about 999 m) on a reach in the box from the 1 Jan 2024 overpass. That was one of 11 valid reaches the satellite measured there on that pass. The single number matters less than how it changes between overpasses. A steady rise pass after pass is the signature of a flood building.
Then do four steps (the runnable code further down does all of this; this is just the idea):
- Pick your river stretch. A small latitude/longitude box around your town, plus the months you care about.
- Pull each overpass. For every time the satellite flew over, get the reaches inside your box and
throw away the fill value (
-999999999999, a placeholder meaning “no reading here”). - Read height and width per reach.
wsetells you how high the water sits; a growingwidthmeans water is spilling toward the banks. - Watch one reach over time. Keep a single
reach_idand line up itswseacross successive overpasses. Rising means water climbing. Check the reaches upstream first, since they rise hours to days before your town does.
That’s the whole method. Below, “How a scientist answers this” names the exact tools, and the Run it cell does it live on synthetic data so you can see each step work.
What you can find out
- How high a river reach is sitting on a given overpass. The
wsefield, in metres, for every reach SWOT measured over your town. - How wide the channel is, and whether it is widening. The
widthfield, a direct sign of water spreading toward the banks. - Whether the river is rising. Line up
wsefor the samereach_idacross overpasses and read the trend. - Whether a flood is building upstream. Check the reaches above your town first; a rise there comes before a rise at home by hours to days.
- How far above normal today is. Compare this overpass to the spread of past overpasses for the same reach.
- Where water historically sits. Overlay JRC Global Surface Water to see the floodplain and the permanent-water baseline your reading should be judged against.
What it can’t tell you
- The actual flow (discharge, in m³/s). SWOT gives height and width, not volume. Turning height into flow needs a calibration curve or a hydraulic model. The height alone won’t do it.
- How today compares to your town’s official flood line.
wseis measured against a satellite reference surface (a geoid datum, a standardized “sea-level” model of the Earth’s shape), not the local benchmark your town’s flood stage is defined against. You have to line up the two reference levels before comparing to a flood threshold. - Small streams. SWOT only sees rivers wider than about 100 m; a creek through your neighbourhood is too narrow.
- Continuous, real-time tracking. Each reach is seen only on overpasses (every few days), so a fast flash flood can rise and fall between visits.
- Why the river is rising. Pair with GPM IMERG rainfall upstream to tell a storm apart from a dam release or snowmelt.
- Whether your specific street will flood. Reach-scale height is not a street-level flood map; that needs a detailed terrain model and local hydraulics.
Gotchas to watch for
- It’s a zipped shapefile, not the usual NetCDF. SWOT river reaches ship as a zipped shapefile
(a GIS map-data format), not the array format most NASA products use, so the normal reader won’t touch
it. Open it with
geopandasusing azip://path, as shown in the code below. - Drop the fill value before you do any maths. Reaches with no valid reading are stored as
-999999999999(a fill value — a stand-in number meaning “missing”). Average that in and it will wreck your numbers, so filter out any row wherewseequals that value first. - Heights are relative, not absolute.
wseis referenced to a geoid datum, so the raw number won’t match a local gauge and comparing it straight to a flood-stage number is meaningless. What you can trust is the change between overpasses, and that change is what flags a rising flood. - A few days between looks. You only get one reading per overpass, so a quick flood can peak unseen between visits. Watch upstream reaches for early warning, and don’t expect a smooth hour-by-hour curve.
The real-data code
The Run it cell above runs the method on synthetic data (no login). Below is the whole recipe
against the real archive: search every overpass, open the zipped shapefiles, read wse/width per
reach, track one reach across time, print whether it’s rising, and plot it. It needs a free Earthdata Login.
import os, re, warnings, numpy as np, pandas as pd, earthaccess, geopandas as gpd
import matplotlib.pyplot as plt
warnings.filterwarnings("ignore")
# load Earthdata creds from .env without `source` (passwords can break the shell)
for line in open(".env"):
m = re.match(r'\s*(?:export\s+)?([A-Z0-9_]+)\s*=\s*(.*)\s*$', line)
if m: os.environ.setdefault(m.group(1), m.group(2).strip().strip('"').strip("'"))
earthaccess.login(strategy="environment") # free Earthdata Login
W, S, E, N = -91.3, 30.2, -90.9, 30.6 # Lower Mississippi near Baton Rouge
FILL = -999999999999 # SWOT "no reading" placeholder
# 1. Every overpass that touched the box over the window we care about.
grans = earthaccess.search_data(short_name="SWOT_L2_HR_RiverSP_reach_D",
temporal=("2024-01-01", "2024-06-30"),
bounding_box=(W, S, E, N))
# 2. The product is a ZIPPED SHAPEFILE — download all overpasses, then read each
# with a zip:// path, clip to the box, and drop the fill value.
files = earthaccess.download(grans, local_path="swot")
frames = []
for f in files:
gdf = gpd.read_file(f"zip://{f}").cx[W:E, S:N]
gdf = gdf[(gdf["wse"] != FILL) & (gdf["width"] != FILL)]
if len(gdf):
gdf["t"] = pd.to_datetime(gdf["time_str"], errors="coerce") # overpass time
frames.append(gdf[["reach_id", "wse", "width", "t"]])
obs = pd.concat(frames, ignore_index=True).dropna(subset=["t"]).sort_values("t")
print(f"{len(obs)} reach-readings across {obs['t'].dt.date.nunique()} overpasses")
# 3. Track ONE reach over time — pick the reach seen on the most overpasses so the
# trend is well-sampled. wse rising pass after pass is the flood signature.
target = obs["reach_id"].value_counts().idxmax()
ts = obs[obs["reach_id"] == target].sort_values("t")
print(f"reach {target}: {len(ts)} overpasses, "
f"wse {ts['wse'].iloc[0]:.2f} m -> {ts['wse'].iloc[-1]:.2f} m, "
f"width {ts['width'].iloc[0]:.0f} m -> {ts['width'].iloc[-1]:.0f} m")
# 4. Slope of wse vs time (m per day) decides rising / falling / flat.
days = (ts["t"] - ts["t"].iloc[0]).dt.total_seconds().to_numpy() / 86400
slope = np.polyfit(days, ts["wse"].to_numpy(), 1)[0] if len(ts) > 1 else 0.0
verdict = "RISING — possible flood building" if slope > 0.01 else \
"falling" if slope < -0.01 else "roughly steady"
print(f"VERDICT: reach {target} is {verdict} ({slope:+.3f} m/day)")
# 5. Plot this reach's water-surface elevation across overpasses.
plt.plot(ts["t"], ts["wse"], "o-")
plt.ylabel("water-surface elevation (m)"); plt.xlabel("overpass date")
plt.title(f"SWOT reach {target} — {verdict}")
plt.tight_layout(); plt.show()
# Trust check: wse is on a geoid datum, not a local gauge, so judge the *change*,
# not the absolute number — and read the reaches UPSTREAM first, they rise before you do.
Where the data comes from
SWOT river reaches come from NASA’s PO.DAAC archive (the ocean/hydrology data center) through one free login (Earthdata Login). The optional add-ons are separate: GPM IMERG rainfall (also NASA, same login) tells you whether a storm caused the rise, and JRC Global Surface Water (from the European Commission’s Joint Research Centre) gives you the floodplain baseline to judge a reading against.
Sources
- SWOT river reach product (
SWOT_L2_HR_RiverSP_reach_D) is delivered as a zipped shapefile; each row is a river reach withreach_id,wse(metres), andwidth(metres), and the fill value is-999999999999.
Make it yours → Pick the town's reach and the upstream reaches, the overpass date range, and the rise threshold in the notebook.
A safe place to practise the method on SWOT_L2_HR_RiverSP_reach_D. The code is the real earthaccess workflow, but we feed it stand-in data shaped like the real product — so you can run it, change the numbers, and break it without an account. When you want the real data, use the recipe under “The real-data code” above, or download the notebook below.