Note
This page was generated from a jupyter notebook.
A coupled rainfall-runoff model in Landlab¶
This tutorial demonstrates a very simple synthetic rainfall-runoff model in Landlab, using the SpatialPrecipitationDistribution
and OverlandFlow
components. This assumes no infiltration, but it could be added by modifying the rainfall__flux
field appropriately.
First, import the modules we’ll need.
[ ]:
import os
import matplotlib.pyplot as plt
import numpy as np
from landlab.components import OverlandFlow, SpatialPrecipitationDistribution
from landlab.io import esri_ascii
Set up a grid and load some arbitrary existing catchment elevation data. A functional version of this might use a real gauged catchment for comparison to reality.
[ ]:
# here we use an arbitrary, very small, "real" catchment
with open("hugo_site.asc") as fp:
mg = esri_ascii.load(fp, name="topographic__elevation", at="node")
z = mg.at_node["topographic__elevation"]
mg.status_at_node[mg.nodes_at_right_edge] = mg.BC_NODE_IS_FIXED_VALUE
mg.status_at_node[np.isclose(z, -9999.0)] = mg.BC_NODE_IS_CLOSED
mg.imshow(z, colorbar_label="Elevation (m)")
Build a mocked-up rainfall distribution using the SpatialPrecipitationDistribution
component.
It would be trivial to replace this with an imported real rainfall field - and we save and reload the pattern to highlight how this might work.
[ ]:
rain = SpatialPrecipitationDistribution(mg)
np.random.seed(26) # arbitrary to get a cool-looking storm out every time
# get the storm simulator to provide a storm
# There's only one storm generated here in the time series, so easy enough to do.
# first, check the directory we need for saving exists, and make it if not:
if not os.path.exists("./rainfall"):
os.makedirs("./rainfall")
# storm lengths in hrs
for storm_t, interstorm_t in rain.yield_storms(style="monsoonal"):
mg.at_node["rainfall__flux"] *= 0.001 # because the rainfall comes out in mm/h
# to make the storm heavier and more interesting!
mg.at_node["rainfall__flux"] *= 10.0
plt.figure()
# plot up this storm
mg.imshow("rainfall__flux", cmap="gist_ncar", colorbar_label="Rainfall flux (m/h)")
plt.show()
with open("./rainfall/rainfall.asc", "w") as fp:
esri_ascii.dump(mg, fp, name="rainfall__flux", at="node")
Now, load the rainfall files and set up the model, telling the flood router to accept the rainfalls in the file(s) as inputs.
In the first instance, this is set up as an instantaneous storm, with all the water dropped over the catchment in one go. Below, we modify this assumption to allow time distributed rainfall.
[ ]:
for filename in os.listdir("./rainfall"): # for each file in the folder
if filename.endswith(".asc"): # ...that ends with .asc...
if "rainfall__flux" in mg.at_node:
mg.at_node.pop("rainfall__flux")
with open(os.path.join("./rainfall", filename)) as fp:
esri_ascii.load(fp, name="rainfall__flux", at="node", out=mg)
else:
continue
mg.add_zeros("surface_water__depth", at="node")
# a veneer of water stabilises the model
mg.at_node["surface_water__depth"].fill(1.0e-12)
mg.at_node["surface_water__depth"] += mg.at_node["rainfall__flux"] * storm_t
of = OverlandFlow(mg, steep_slopes=True)
# storm_t here is the duration of the rainfall, from the rainfall component
# We're going to assume the rainfall arrives effectively instantaneously, but
# adding discharge during the run is completely viable
node_of_max_q = 2126 # established by examining the output of a previous run
outlet_depth = []
outlet_times = []
post_storm_elapsed_time = 0.0
last_storm_loop_tracker = 0.0
while post_storm_elapsed_time < 0.5 * 3600.0: # plot 30 mins-worth of runoff
dt = of.calc_time_step()
of.run_one_step(dt=dt)
post_storm_elapsed_time += dt
storm_loop_tracker = post_storm_elapsed_time % 180.0 # show every 3 min
# NB: Do NOT allow this plotting if there are multiple files in the folder
if storm_loop_tracker < last_storm_loop_tracker:
plt.figure()
mg.imshow("surface_water__depth", var_name="Stage (m)")
plt.title("Stage at t=" + str(post_storm_elapsed_time // 1) + "s")
last_storm_loop_tracker = storm_loop_tracker
outlet_depth.append(mg.at_node["surface_water__depth"][node_of_max_q])
outlet_times.append(post_storm_elapsed_time)
Now, plot the time series at the outlet (defined as the node that experiences peak stage):
[ ]:
plt.plot(outlet_times, outlet_depth, "-")
plt.xlabel("Time elapsed (s)")
plt.ylabel("Flood stage (m)")
We can relax the assumption that all this discharge is delivered instantaneously at the start of the run with some tweaking of the driver:
[ ]:
for filename in os.listdir("./rainfall"): # for each file in the folder
if filename.endswith(".asc"): # ...that ends with .asc...
if "rainfall__flux" in mg.at_node:
mg.at_node.pop("rainfall__flux")
with open(os.path.join("./rainfall", filename)) as fp:
esri_ascii.load(fp, name="rainfall__flux", at="node", out=mg)
else:
continue
mg.at_node["surface_water__depth"].fill(1.0e-12)
of = OverlandFlow(mg, steep_slopes=True)
node_of_max_q = 2126
total_mins_to_plot = 60.0 # plot 60 mins-worth of runoff
plot_interval_mins = 10.0 # show every 10 min
min_tstep_val = 1.0 # necessary to get the model going cleanly
outlet_depth = []
outlet_times = []
storm_elapsed_time = 0.0
total_elapsed_time = 0.0
last_storm_loop_tracker = 0.0
while total_elapsed_time < total_mins_to_plot * 60.0:
dt = of.calc_time_step()
remaining_total_time = total_mins_to_plot * 60.0 - total_elapsed_time
if storm_elapsed_time < storm_t * 3600.0:
remaining_storm_time = storm_t * 3600.0 - storm_elapsed_time
dt = min((dt, remaining_total_time, remaining_storm_time, min_tstep_val))
else:
dt = min((dt, remaining_total_time, min_tstep_val))
of.run_one_step(dt=dt)
total_elapsed_time += dt
storm_elapsed_time += dt
storm_loop_tracker = total_elapsed_time % (plot_interval_mins * 60.0)
# NB: Do NOT allow this plotting if there are multiple files in the folder
if storm_loop_tracker < last_storm_loop_tracker:
plt.figure()
mg.imshow("surface_water__depth", var_name="Stage (m)")
plt.title("Stage at t=" + str(total_elapsed_time // 1) + "s")
last_storm_loop_tracker = storm_loop_tracker
outlet_depth.append(mg.at_node["surface_water__depth"][node_of_max_q])
outlet_times.append(total_elapsed_time)
if storm_elapsed_time < storm_t * 3600.0:
mg.at_node["surface_water__depth"] += (
mg.at_node["rainfall__flux"] * dt / 3600.0
)
[ ]:
plt.plot(outlet_times, outlet_depth, "-")
plt.xlabel("Time elapsed (s)")
plt.ylabel("Flood stage (m)")
As expected, a more realistic spread of the rainfall across the storm gives a longer and more subdued flood pulse.
(An aside: the levelling off of the tail at h~0.125m is likely due to the permanent filling of a depression in the topography - the same thing is probably causing the deep pixels in the flow maps - or are these numerical instabilities? Resolving this is left as an exercise for the reader…)