# Define AOI bounding box
# Snake River, Grand Teton National Park
= [-110.583466, 43.790715, -110.509222, 43.848585] # WSG84 coordinates, WSEN bbox
A wonderful aspect of geospatial data is that it’s both beautiful and informative. In this post I look at the migration of the Snake River in Grand Teton National Park, USA over time by creating a relative elevation model (REM). This takes a digital elevation model (DEM) and de-trends the baseline elevation so it follows the water’s surface. By doing so, we can clearly see the migration of the river’s channels and the associated features such as oxbow lakes, meander scars and terraces.
This post was inspired by this blog post and this notebook, and makes use of the HyRiver suite of package for data access: PyNHD for river flowlines and Py3DEP for a high resolution DEM.
For the first step we will retrieve the river’s path. We can retrieve the river’s flowline (geometry) from USGS data which is easily accessible using the PyNHD package
Code
import pynhd
import py3dep
import geopandas as gpd
import shapely
import matplotlib.pyplot as plt
import matplotlib as mpl
import pyproj
import numpy as np
import pygeoutils
import xarray as xr
from scipy.spatial import KDTree
%config InlineBackend.figure_formats = ['png']
# Connect to service
= pynhd.WaterData("nhdflowline_network")
water_data
# Get flowlines within bounding box
= water_data.bybox(bbox) flowlines
# This returns a GeoDataFrame
2) flowlines.head(
geometry | comid | fdate | resolution | gnis_id | gnis_name | lengthkm | reachcode | flowdir | wbareacomi | ... | qc_12 | vc_12 | qe_12 | ve_12 | lakefract | surfarea | rareahload | rpuid | vpuid | enabled | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | MULTILINESTRING Z ((-110.54139 43.79185 0, -11... | 23123173 | 2005-07-21T04:00:00Z | Medium | 3.651 | 17040101000094 | With Digitized | 0 | ... | 0.132 | 0.53390 | 0.132 | 0.53390 | NaN | NaN | NaN | 17a | 17 | 1 | ||
1 | MULTILINESTRING Z ((-110.54022 43.79151 0, -11... | 23123175 | 2001-02-07T05:00:00Z | Medium | 1603229 | Spread Creek | 0.102 | 17040101000095 | With Digitized | 0 | ... | 68.385 | 1.23836 | 68.385 | 1.23836 | NaN | NaN | NaN | 17a | 17 | 1 |
2 rows × 138 columns
We want to keep the main stream only, which in this case we can easily do by filtering on the gnis_name
= flowlines.loc[flowlines['gnis_name'].eq('Snake River')] flowline
# Plot flowlines and bounding box
# Create GeoDataframe with bounding box
= gpd.GeoDataFrame(data={'geometry': shapely.box(*bbox)}, index=[0], crs='EPSG:4326')
gdf_bbox = flowlines.explore(color='blue')
m ='red', m=m)
flowline.explore(color="black", style_kwds={"fillColor": "None"}, m=m)
gdf_bbox.explore(color m
For the next step, we’ll retrieve a DEM for the region. For this we can use the Py3DEP package which gives us easy access to the USDA 3DEP database
# Check which resolutions are available in our AOI
= py3dep.check_3dep_availability(bbox)
dem_resolutions dem_resolutions
{'1m': True,
'3m': True,
'5m': False,
'10m': True,
'30m': True,
'60m': False,
'topobathy': False}
DEM data is available at up to 1m
resolution, however we’ll use 3m
to save time and computation. We can simply use py3dep.get_dem
to download data for our AOI and chosen resolution and return an xarray.DataArray
= py3dep.get_dem(bbox, resolution=3, crs=flowline.crs) dem
# Plot DEM with river overlayed
= plt.subplots(figsize=(6, 4))
fig, ax =ax, robust=True);
dem.plot(ax=ax, color="r"); flowline.plot(ax
To de-trend the DEM, we need to find the elevation profile along the river’s flowline. While py3dep.elevation_profile
provides this functionality, it uses the 10m
DEM, and as we already have 3m
data we can use it instead with a little extra work.
# First combine all geometries to a single MultiLineString
= shapely.union_all(flowline.line_merge())
line
# Smooth the line to 3m spacing and extract height from the DEM
# Reproject to meters
= pyproj.Transformer.from_crs(flowline.crs, 5070, always_xy=True).transform
project = shapely.ops.transform(project, line)
line_5070
# Smooth line
= 3 # 3m spacing
spacing = int(np.ceil(line_5070.length / spacing))
npts = pygeoutils.smooth_linestring(line_5070, 0.1, npts)
line_5070_smooth
# Reproject back to original crs
= pyproj.Transformer.from_crs(5070, flowline.crs, always_xy=True).transform
project = shapely.ops.transform(project, line_5070_smooth)
line_smooth
# Extract elevation from DEM
= line_smooth.xy
xs, ys = xr.DataArray(xs, dims='distance'), xr.DataArray(ys, dims='distance')
xs, ys = dem.interp(x=xs, y=ys, method='nearest').dropna(dim='distance') river_dem
To mitigate artifacts arising from the river geometry and DEM not matching (due to natural change over time), we can ensure that the river elevation decreases or stays constant between pixels
# Ensure the elevation is non-increasing
= np.minimum.accumulate(river_dem.values) river_dem.values
=(4, 2)); river_dem.plot(figsize
Now we need to interpolate the height of the river over the entire AOI. We can do this using the inverse distance weighting (IDW) method.
Code
def idw(da_in: xr.DataArray, da_out: xr.DataArray, k: int = 10, n: float = 1) -> xr.DataArray:
"""
Inverse distance weighted interpolation
Args:
da_in: Input data to interpolate
da_out: Output grid
k: Number of nearest points to include
n: Exponent of the weighting
Returns:
Interpolated data
"""
= np.column_stack((da_in.x, da_in.y))
coords = KDTree(coords)
kdt
= np.dstack(np.meshgrid(da_out.x, da_out.y)).reshape(-1, 2)
grid = kdt.query(grid, k=k)
distances, indices
= np.power(np.reciprocal(distances), n)
weights = weights / weights.sum(axis=1, keepdims=True)
weights = weights * da_in.to_numpy()[indices]
interp = interp.sum(axis=1).reshape((da_out.sizes["y"], da_out.sizes["x"]))
interp = xr.DataArray(interp, dims=("y", "x"), coords={"x": da_out.x, "y": da_out.y})
interp return interp
= idw(river_dem, dem, k=200, n=0.5) river_dem_interp
=(6, 4)); river_dem_interp.plot(figsize
Finally, we can compute the REM from the river’s elevation profile and the DEM
= dem - river_dem_interp rem
# To simplify plotting, set minimum to zero
= rem - rem.min() rem
= rem.plot.imshow(figsize=(12,8), cmap="YlGnBu", norm=mpl.colors.LogNorm(vmin=1, vmax=10), add_colorbar=False, add_labels=False);
ax 'equal');
ax.axes.set_aspect('off'); ax.axes.axis(
We can clearly see the complex pattern of different paths the river has taken over the years, and the extent of the floodplains.