r"""
stream_power_smooth_threshold.py: Defines the StreamPowerSmoothThresholdEroder,
which is a version of the FastscapeEroder (derived from it).
StreamPowerSmoothThresholdEroder uses a mathematically smooth threshold
formulation, rather than one with a singularity. The erosion rate is defined as
follows:
$\omega = K A^m S$
$E = \omega - \omega_c \left[ 1 - \exp ( -\omega / \omega_c ) \right]$
Created on Sat Nov 26 08:36:49 2016
@author: gtucker
"""
import numpy as np
from ..depression_finder.lake_mapper import _FLOODED
from .cfuncs import smooth_stream_power_eroder_solver
from .fastscape_stream_power import FastscapeEroder
[docs]
class StreamPowerSmoothThresholdEroder(FastscapeEroder):
"""Stream erosion component with smooth threshold function.
Parameters
----------
grid : ModelGrid
A grid.
K_sp : float, array, or field name
K in the stream power equation (units vary with other parameters).
m_sp : float, optional
m in the stream power equation (power on drainage area).
n_sp : float, optional, ~ 0.5<n_sp<4.
n in the stream power equation (power on slope). NOTE: NOT PRESENTLY
HONORED BY StreamPowerSmoothThresholdEroder (TODO)
threshold_sp : float (TODO: array, or field name)
The threshold stream power.
discharge_field : float, field name, or array, optional
Discharge [L^2/T]. The default is to use the grid field
'drainage_area'. To use custom spatially/temporally varying
rainfall, use 'water__unit_flux_in' to specify water input to the
FlowAccumulator and use "surface_water__discharge" for this
keyword argument.
erode_flooded_nodes : bool (optional)
Whether erosion occurs in flooded nodes identified by a
depression/lake mapper (e.g., DepressionFinderAndRouter). When set
to false, the field *flood_status_code* must be present on the grid
(this is created by the DepressionFinderAndRouter). Default True.
Examples
--------
>>> from landlab import RasterModelGrid
>>> rg = RasterModelGrid((3, 4))
>>> rg.set_closed_boundaries_at_grid_edges(False, True, True, True)
>>> z = rg.add_zeros("node", "topographic__elevation")
>>> z[5] = 2.0
>>> z[6] = 1.0
>>> from landlab.components import FlowAccumulator
>>> fr = FlowAccumulator(rg, flow_director="D4")
>>> fr.run_one_step()
>>> from landlab.components import StreamPowerSmoothThresholdEroder
>>> sp = StreamPowerSmoothThresholdEroder(rg, K_sp=1.0)
>>> sp.thresholds
1.0
>>> sp.run_one_step(dt=1.0)
>>> import numpy as np
>>> np.round(z[5:7], 3)
array([1.646, 0.667])
>>> z[5] = 2.0
>>> z[6] = 1.0
>>> import numpy as np
>>> q = np.zeros(rg.number_of_nodes) + 0.25
>>> q[6] = 100.0
>>> sp = StreamPowerSmoothThresholdEroder(rg, K_sp=1.0, discharge_field=q)
>>> sp.run_one_step(dt=1.0)
>>> np.round(z[5:7], 3)
array([1.754, 0.164])
References
----------
**Required Software Citation(s) Specific to this Component**
Barnhart, K., Glade, R., Shobe, C., Tucker, G. (2019). Terrainbento 1.0: a
Python package for multi-model analysis in long-term drainage basin
evolution. Geoscientific Model Development 12(4), 1267--1297.
https://dx.doi.org/10.5194/gmd-12-1267-2019
**Additional References**
Braun, J., Willett, S. (2013). A very efficient O(n), implicit and parallel
method to solve the stream power equation governing fluvial incision and
landscape evolution. Geomorphology 180-181(C), 170-179.
https://dx.doi.org/10.1016/j.geomorph.2012.10.008
"""
_name = "StreamPowerSmoothThresholdEroder"
_unit_agnostic = True
_cite_as = """
@article{barnhart2019terrain,
author = {Barnhart, Katherine R and Glade, Rachel C and Shobe, Charles M
and Tucker, Gregory E},
title = {{Terrainbento 1.0: a Python package for multi-model analysis in
long-term drainage basin evolution}},
doi = {10.5194/gmd-12-1267-2019},
pages = {1267---1297},
number = {4},
volume = {12},
journal = {Geoscientific Model Development},
year = {2019},
}
"""
_info = {
"drainage_area": {
"dtype": float,
"intent": "in",
"optional": False,
"units": "m**2",
"mapping": "node",
"doc": "Upstream accumulated surface area contributing to the node's discharge",
},
"flow__link_to_receiver_node": {
"dtype": int,
"intent": "in",
"optional": False,
"units": "-",
"mapping": "node",
"doc": "ID of link downstream of each node, which carries the discharge",
},
"flow__receiver_node": {
"dtype": int,
"intent": "in",
"optional": False,
"units": "-",
"mapping": "node",
"doc": "Node array of receivers (node that receives flow from current node)",
},
"flow__upstream_node_order": {
"dtype": int,
"intent": "in",
"optional": False,
"units": "-",
"mapping": "node",
"doc": "Node array containing downstream-to-upstream ordered list of node IDs",
},
"topographic__elevation": {
"dtype": float,
"intent": "inout",
"optional": False,
"units": "m",
"mapping": "node",
"doc": "Land surface topographic elevation",
},
}
[docs]
def __init__(
self,
grid,
K_sp=None,
m_sp=0.5,
n_sp=1.0,
threshold_sp=1.0,
discharge_field="drainage_area",
erode_flooded_nodes=True,
):
"""Initialize StreamPowerSmoothThresholdEroder."""
if "flow__receiver_node" in grid.at_node and grid.at_node[
"flow__receiver_node"
].size != grid.size("node"):
raise NotImplementedError(
"A route-to-multiple flow director has been "
"run on this grid. The landlab development team has not "
"verified that StreamPowerSmoothThresholdEroder is compatible "
"with route-to-multiple methods. Please open a GitHub Issue "
"to start this process."
)
if not erode_flooded_nodes and "flood_status_code" not in grid.at_node:
raise ValueError(
"In order to not erode flooded nodes another component "
"must create the field *flood_status_code*. You want to "
"run a lake mapper/depression finder."
)
if n_sp != 1.0:
raise ValueError(
"StreamPowerSmoothThresholdEroder currently only " "supports n_sp = 1"
)
# Call base-class init
super().__init__(
grid,
K_sp=K_sp,
m_sp=m_sp,
n_sp=n_sp,
threshold_sp=threshold_sp,
discharge_field=discharge_field,
)
# Arrays with parameters for use in implicit solver
self._gamma = grid.empty(at="node")
self._delta = grid.empty(at="node")
@property
def alpha(self):
"""Erosion term divided by link length.
Alpha is given as::
alpha = K A^m dt / L
where K is the erodibility, A is the drainage area, m is the
drainage area exponent, dt is the timestep, and L is the link length.
"""
return self._alpha
@property
def gamma(self):
"""Erosion threshold times timestep."""
return self._gamma
@property
def thresholds(self):
"""Erosion thresholds."""
return self._thresholds
@property
def delta(self):
"""Erosion term divided by link length and erosion threshold.
delta is given as::
delta = K A^m dt / (L * omega_c)
where K is the erodibility, A is the drainage area, m is the
drainage area exponent, dt is the timestep, L is the link length, and
omega_c is the erosion threshold.
"""
return self._delta
[docs]
def run_one_step(self, dt, runoff_rate=None):
"""Run one forward iteration of duration dt.
Parameters
----------
dt : float
Time step size
runoff_rate : (not used yet)
(to be added later)
Examples
--------
>>> from landlab import RasterModelGrid
>>> rg = RasterModelGrid((3, 3))
>>> rg.set_closed_boundaries_at_grid_edges(False, True, True, True)
>>> z = rg.add_zeros("node", "topographic__elevation")
>>> z[4] = 1.0
>>> from landlab.components import FlowAccumulator
>>> fr = FlowAccumulator(rg, flow_director="D4")
>>> fr.run_one_step()
>>> from landlab.components import StreamPowerSmoothThresholdEroder
>>> sp = StreamPowerSmoothThresholdEroder(rg, K_sp=1.0)
>>> sp.run_one_step(dt=1.0)
>>> sp.alpha
array([0., 0., 0., 0., 1., 0., 0., 0., 0.])
>>> sp.gamma
array([0., 0., 0., 0., 1., 0., 0., 0., 0.])
>>> sp.delta
array([0., 0., 0., 0., 1., 0., 0., 0., 0.])
"""
if not self._erode_flooded_nodes:
flood_status = self._grid.at_node["flood_status_code"]
flooded_nodes = np.nonzero(flood_status == _FLOODED)[0]
else:
flooded_nodes = []
# Set up needed arrays
#
# Get shorthand for elevation field ("z"), and for up-to-downstream
# ordered array of node IDs ("upstream_order_IDs")
node_id = np.arange(self._grid.number_of_nodes)
upstream_order_IDs = self._grid["node"]["flow__upstream_node_order"]
z = self._grid["node"]["topographic__elevation"]
flow_receivers = self._grid["node"]["flow__receiver_node"]
# Get an array of flow-link length for each node that has a defined
# receiver (i.e., that drains to another node).
defined_flow_receivers = np.not_equal(
self._grid["node"]["flow__link_to_receiver_node"], self._grid.BAD_INDEX
)
defined_flow_receivers[flow_receivers == node_id] = False
if flooded_nodes is not None:
defined_flow_receivers[flooded_nodes] = False
flow_link_lengths = self._grid.length_of_d8[
self._grid.at_node["flow__link_to_receiver_node"][defined_flow_receivers]
]
# Set up alpha, beta, delta arrays
#
# First, compute drainage area or discharge raised to the power m.
np.power(self._A, self._m, out=self._A_to_the_m)
# Alpha
self._alpha[~defined_flow_receivers] = 0.0
self._alpha[defined_flow_receivers] = (
self._K[defined_flow_receivers]
* dt
* self._A_to_the_m[defined_flow_receivers]
/ flow_link_lengths
)
# Gamma
self._gamma[~defined_flow_receivers] = 0.0
# Delta
self._delta[~defined_flow_receivers] = 0.0
if isinstance(self._thresholds, np.ndarray):
self._gamma[defined_flow_receivers] = (
dt * self._thresholds[defined_flow_receivers]
)
self._delta[defined_flow_receivers] = (
self._K[defined_flow_receivers]
* self._A_to_the_m[defined_flow_receivers]
) / (self._thresholds[defined_flow_receivers] * flow_link_lengths)
self._delta[defined_flow_receivers][
self._thresholds[defined_flow_receivers] == 0.0
] = 0.0
else:
self._gamma[defined_flow_receivers] = dt * self._thresholds
if self._thresholds == 0:
self._delta[defined_flow_receivers] = 0.0
else:
self._delta[defined_flow_receivers] = (
self._K[defined_flow_receivers]
* self._A_to_the_m[defined_flow_receivers]
) / (self._thresholds * flow_link_lengths)
# Iterate over nodes from downstream to upstream, using scipy's
# 'newton' function to find new elevation at each node in turn.
smooth_stream_power_eroder_solver(
upstream_order_IDs, flow_receivers, z, self._alpha, self._gamma, self._delta
)