import warnings
import numpy as np
import scipy.constants
from landlab import Component
from landlab import MissingKeyError
from landlab.utils.decorators import make_return_array_immutable
[docs]
class SedDepEroder(Component):
"""
This module implements sediment flux dependent channel incision
following::
E = f(Qs, Qc) * ([a stream power-like term] - [an optional threshold]),
where E is the bed erosion rate, Qs is the volumetric sediment flux
into a node, and Qc is the volumetric sediment transport capacity at
that node.
This component is under active research and development; proceed with its
use at your own risk.
The details of the implementation are a function of the two key
arguments, *sed_dependency_type* and *Qc*. The former controls the
shape of the sediment dependent response function f(Qs, Qc), the
latter controls the way in which sediment transport capacities are
calculated (primarily, whether a full Meyer-Peter Muller approach is
used, or whether simpler stream-power-like equations can be assumed).
For Qc, 'power_law' broadly follows the assumptions in Gasparini et
al. 2006, 2007; 'MPM' broadly follows those in Hobley et al., 2011.
Note that a convex-up channel can result in many cases assuming MPM,
unless parameters b and c are carefully tuned.
If ``Qc == 'power_law'``::
E = K_sp * f(Qs, Qc) * A ** m_sp * S ** n_sp;
Qc = K_t * A ** m_t * S ** n_t
If ``Qc == 'MPM'``::
shear_stress = fluid_density * g * depth * S
= fluid_density * g * (mannings_n/k_w) ** 0.6 * (
k_Q* A ** c_sp) ** (0.6 * (1. - b_sp)) * S ** 0.7,
for consistency with MPM
E = K_sp * f(Qs, Qc) * (shear_stress ** a_sp - [threshold_sp])
Qc = 8 * C_MPM * sqrt((sed_density-fluid_density)/fluid_density *
g * D_char**3) * (shields_stress - threshold_shields)**1.5
shields_stress = shear_stress / (g * (sed_density-fluid_density) *
D_char)
If you choose Qc='MPM', you may provide thresholds for both channel
incision and shields number, or alternatively set either or both of
these threshold dynamically. The minimum shear stress can be made
equivalent to the Shields number using *set_threshold_from_Dchar*,
for full consistency with the MPM approach (i.e., the threshold
becomes a function of the characteristic grain size on the bed). The
Shields threshold itself can also be a weak function of slope if
*slope_sensitive_threshold*, following Lamb et al. 2008,
taustar_c = 0.15 * S ** 0.25.
The component is able to handle flooded nodes, if created by a lake
filler. It assumes the flow paths found in the fields already reflect
any lake routing operations, and then requires the optional argument
*flooded_depths* be passed to the run method. A flooded depression
acts as a perfect sediment trap, and will be filled sequentially
from the inflow points towards the outflow points.
References
----------
**Required Software Citation(s) Specific to this Component**
None Listed
**Additional References**
Hobley, D. E. J., Sinclair, H. D., Mudd, S. M., and Cowie, P. A.: Field
calibration of sediment flux dependent river incision, J. Geophys. Res.,
116, F04017, doi:10.1029/2010JF001935, 2011.
"""
_name = "SedDepEroder"
_unit_agnostic = False
_info = {
"channel__bed_shear_stress": {
"dtype": float,
"intent": "out",
"optional": False,
"units": "Pa",
"mapping": "node",
"doc": (
"Shear exerted on the bed of the channel, assuming all "
"discharge travels along a single, self-formed channel"
),
},
"channel__depth": {
"dtype": float,
"intent": "out",
"optional": True,
"units": "m",
"mapping": "node",
"doc": "Depth of the a single channel carrying all runoff through the node",
},
"channel__discharge": {
"dtype": float,
"intent": "out",
"optional": False,
"units": "m**3/s",
"mapping": "node",
"doc": (
"Volumetric water flux of the a single channel carrying all "
"runoff through the node"
),
},
"channel__width": {
"dtype": float,
"intent": "out",
"optional": True,
"units": "m",
"mapping": "node",
"doc": "Width of the a single channel carrying all runoff through the node",
},
"channel_sediment__relative_flux": {
"dtype": float,
"intent": "out",
"optional": False,
"units": "-",
"mapping": "node",
"doc": (
"The fluvial_sediment_flux_into_node divided by the "
"fluvial_sediment_transport_capacity"
),
},
"channel_sediment__volumetric_flux": {
"dtype": float,
"intent": "out",
"optional": False,
"units": "m**3/s",
"mapping": "node",
"doc": "Total volumetric fluvial sediment flux brought into the node from upstream",
},
"channel_sediment__volumetric_transport_capacity": {
"dtype": float,
"intent": "out",
"optional": False,
"units": "m**3/s",
"mapping": "node",
"doc": (
"Volumetric transport capacity of a channel carrying all runoff "
"through the node, assuming the Meyer-Peter Muller transport equation"
),
},
"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",
},
"topographic__steepest_slope": {
"dtype": float,
"intent": "in",
"optional": False,
"units": "-",
"mapping": "node",
"doc": "The steepest *downhill* slope",
},
}
[docs]
def __init__(
self,
grid,
K_sp=1.0e-6,
g=scipy.constants.g,
rock_density=2700,
sediment_density=2700,
fluid_density=1000,
runoff_rate=1.0,
sed_dependency_type="generalized_humped",
kappa_hump=13.683,
nu_hump=1.13,
phi_hump=4.24,
c_hump=0.00181,
Qc="power_law",
m_sp=0.5,
n_sp=1.0,
K_t=1.0e-4,
m_t=1.5,
n_t=1.0,
# these params for Qc='MPM':
C_MPM=1.0,
a_sp=1.0,
b_sp=0.5,
c_sp=1.0,
k_w=2.5,
k_Q=2.5e-7,
mannings_n=0.05,
threshold_shear_stress=None,
Dchar=0.05,
set_threshold_from_Dchar=True,
set_Dchar_from_threshold=False,
threshold_Shields=0.05,
slope_sensitive_threshold=False,
# params for model numeric behavior:
pseudoimplicit_repeats=5,
return_stream_properties=False,
# flooded node info
flooded_depths=None,
):
"""Constructor for the class.
Parameters
----------
grid : a ModelGrid
A grid.
K_sp : float (time unit must be *years*)
K in the stream power equation; the prefactor on the erosion
equation (units vary with other parameters).
g : float (m/s**2)
Acceleration due to gravity.
rock_density : float (Kg m**-3)
Bulk intact rock density.
sediment_density : float (Kg m**-3)
Typical density of loose sediment on the bed.
fluid_density : float (Kg m**-3)
Density of the fluid.
runoff_rate : float, array or field name (m/s)
The rate of excess overland flow production at each node (i.e.,
rainfall rate less infiltration).
pseudoimplicit_repeats : int
Number of loops to perform with the pseudoimplicit iterator,
seeking a stable solution. Convergence is typically rapid.
return_stream_properties : bool
Whether to perform a few additional calculations in order to set
the additional optional output fields, 'channel__width',
'channel__depth', and 'channel__discharge' (default False).
sed_dependency_type : {'generalized_humped', 'None', 'linear_decline',
'almost_parabolic'}
The shape of the sediment flux function. For definitions, see
Hobley et al., 2011. 'None' gives a constant value of 1.
NB: 'parabolic' is currently not supported, due to numerical
stability issues at channel heads.
Qc : {'power_law', 'MPM'}
Whether to use simple stream-power-like equations for both
sediment transport capacity and erosion rate, or more complex
forms based directly on the Meyer-Peter Muller equation and a
shear stress based erosion model consistent with MPM (per
Hobley et al., 2011).
If ``sed_dependency_type == 'generalized_humped'``...
kappa_hump : float
Shape parameter for sediment flux function. Primarily controls
function amplitude (i.e., scales the function to a maximum of 1).
Default follows Leh valley values from Hobley et al., 2011.
nu_hump : float
Shape parameter for sediment flux function. Primarily controls
rate of rise of the "tools" limb. Default follows Leh valley
values from Hobley et al., 2011.
phi_hump : float
Shape parameter for sediment flux function. Primarily controls
rate of fall of the "cover" limb. Default follows Leh valley
values from Hobley et al., 2011.
c_hump : float
Shape parameter for sediment flux function. Primarily controls
degree of function asymmetry. Default follows Leh valley values
from Hobley et al., 2011.
If ``Qc == 'power_law'``...
m_sp : float
Power on drainage area in the erosion equation.
n_sp : float
Power on slope in the erosion equation.
K_t : float (time unit must be in *years*)
Prefactor in the transport capacity equation.
m_t : float
Power on drainage area in the transport capacity equation.
n_t : float
Power on slope in the transport capacity equation.
if ``Qc == 'MPM'``...
C_MPM : float
A prefactor on the MPM relation, allowing tuning to known sediment
saturation conditions (leave as 1. in most cases).
a_sp : float
Power on shear stress to give erosion rate.
b_sp : float
Power on drainage area to give channel width.
c_sp : float
Power on drainage area to give discharge.
k_w : float (unit variable with b_sp)
Prefactor on A**b_sp to give channel width.
k_Q : float (unit variable with c_sp, but time unit in *seconds*)
Prefactor on A**c_sp to give discharge.
mannings_n : float
Manning's n for the channel.
threshold_shear_stress : None or float (Pa)
The threshold shear stress in the equation for erosion rate. If
None, implies that *set_threshold_from_Dchar* is True, and this
parameter will get set from the Dchar value and critical Shields
number.
Dchar :None, float, array, or field name (m)
The characteristic grain size on the bed, that controls the
relationship between critical Shields number and critical shear
stress. If None, implies that *set_Dchar_from_threshold* is True,
and this parameter will get set from the threshold_shear_stress
value and critical Shields number.
set_threshold_from_Dchar : bool
If True (default), threshold_shear_stress will be set based on
Dchar and threshold_Shields.
set_Dchar_from_threshold : bool
If True, Dchar will be set based on threshold_shear_stress and
threshold_Shields. Default is False.
threshold_Shields : None or float
The threshold Shields number. If None, implies that
*slope_sensitive_threshold* is True.
slope_sensitive_threshold : bool
If True, the threshold_Shields will be set according to
0.15 * S ** 0.25, per Lamb et al., 2008 & Hobley et al., 2011.
flooded_depths : array or field name (m)
Depths of flooding at each node, zero where no lake. Note that the
component will dynamically update this array as it fills nodes
with sediment (...but does NOT update any other related lake
fields).
"""
super().__init__(grid)
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 SedDepEroder is compatible with "
"route-to-multiple methods. Please open a GitHub Issue "
"to start this process."
)
self._flooded_depths = flooded_depths
self._pseudoimplicit_repeats = pseudoimplicit_repeats
self._link_S_with_trailing_blank = np.zeros(grid.number_of_links + 1)
# ^needs to be filled with values in execution
self._countactive_links = np.zeros_like(
self._link_S_with_trailing_blank, dtype=int
)
self._countactive_links[:-1] = 1
self._K_unit_time = K_sp / 31557600.0
# ^...because we work with dt in seconds
# set gravity
self._g = g
self._rock_density = rock_density
self._sed_density = sediment_density
self._fluid_density = fluid_density
self._relative_weight = (
(self._sed_density - self._fluid_density) / self._fluid_density * self._g
)
# ^to accelerate MPM calcs
self._rho_g = self._fluid_density * self._g
self._type = sed_dependency_type
assert self._type in (
"generalized_humped",
"None",
"linear_decline",
"almost_parabolic",
)
self._Qc = Qc
assert self._Qc in ("MPM", "power_law")
self._return_ch_props = return_stream_properties
if return_stream_properties:
assert self._Qc == "MPM", (
"Qc must be 'MPM' to return stream " + "properties"
)
if isinstance(runoff_rate, (float, int)):
self._runoff_rate = float(runoff_rate)
elif isinstance(runoff_rate, str):
self._runoff_rate = self._grid.at_node[runoff_rate]
else:
self._runoff_rate = np.array(runoff_rate)
assert runoff_rate.size == self._grid.number_of_nodes
if self._Qc == "MPM":
if threshold_shear_stress is not None:
self._thresh = threshold_shear_stress
self._set_threshold = True
# ^flag for sed_flux_dep_incision to see if the threshold was
# manually set.
# print("Found a shear stress threshold to use: ", self._thresh)
else:
warnings.warn("Found no incision threshold to use.", stacklevel=2)
self._thresh = 0.0
self._set_threshold = False
self._a = a_sp
self._b = b_sp
self._c = c_sp
self._k_Q = k_Q
self._k_w = k_w
self._mannings_n = mannings_n
if mannings_n < 0.0 or mannings_n > 0.2:
warnings.warn("Manning's n outside it's typical range", stacklevel=2)
self._diffusivity_power_on_A = 0.9 * self._c * (1.0 - self._b)
# ^i.e., q/D**(1/6)
self._override_threshold = set_threshold_from_Dchar
self._override_Dchar = set_Dchar_from_threshold
if self._override_threshold:
assert self._set_threshold is False, (
"If set_threshold_from_Dchar, threshold_Shields must be "
+ "set to None"
)
assert self._override_Dchar is False
if self._override_Dchar:
assert self._override_threshold is False
self._shields_crit = threshold_Shields
self._lamb_flag = slope_sensitive_threshold
if self._lamb_flag:
assert self._shields_crit is None, (
"If slope_sensitive_threshold, threshold_Shields must "
+ "be set to None"
)
elif self._Qc == "power_law":
self._m = m_sp
self._n = n_sp
self._Kt = K_t / 31557600.0 # in sec
self._mt = m_t
self._nt = n_t
# now conditional inputs
if self._type == "generalized_humped":
self._kappa = kappa_hump
self._nu = nu_hump
self._phi = phi_hump
self._c = c_hump
if self._Qc == "MPM":
if Dchar is not None:
if isinstance(Dchar, (int, float)):
self._Dchar_in = float(Dchar)
elif isinstance(Dchar, str):
self._Dchar_in = self._grid.at_node[Dchar]
else:
self._Dchar_in = np.array(Dchar)
assert self._Dchar_in.size == self._grid.number_of_nodes
assert (
not self._override_Dchar
), "If set_Dchar_from_threshold, Dchar must be set to None"
else:
assert self._override_Dchar
# remember the threshold getting set is already tau**a
if not self._lamb_flag:
self._Dchar_in = (
self._thresh
/ self._g
/ (self._sed_density - self._fluid_density)
/ self._shields_crit
)
else:
self._Dchar_in = None
self._C_MPM = C_MPM
if self._override_threshold:
# print("Overriding any supplied threshold...")
try:
self._thresh = (
self._shields_crit
* self._g
* (self._sed_density - self._fluid_density)
* self._Dchar_in
)
except AttributeError:
self._thresh = (
self._shields_crit
* self._g
* (self._sed_density - self._fluid_density)
* Dchar
)
# new 11/12/14
self._point6onelessb = 0.6 * (1.0 - self._b)
self._shear_stress_prefactor = (
self._fluid_density * self._g * (self._mannings_n / self._k_w) ** 0.6
)
if self._set_threshold is False or self._override_threshold:
try:
self._shields_prefactor_to_shear = (
(self._sed_density - self._fluid_density)
* self._g
* self._Dchar_in
)
except AttributeError: # no Dchar
self._shields_prefactor_to_shear_noDchar = (
self._sed_density - self._fluid_density
) * self._g
twothirds = 2.0 / 3.0
self._Qs_prefactor = (
4.0
* self._C_MPM**twothirds
* self._fluid_density**twothirds
/ (self._sed_density - self._fluid_density) ** twothirds
* self._g ** (twothirds / 2.0)
* mannings_n**0.6
* self._k_w ** (1.0 / 15.0)
* self._k_Q ** (0.6 + self._b / 15.0)
/ self._sed_density**twothirds
)
self._Qs_thresh_prefactor = (
4.0
* (
self._C_MPM
* self._k_w
* self._k_Q**self._b
/ self._fluid_density**0.5
/ (self._sed_density - self._fluid_density)
/ self._g
/ self._sed_density
)
** twothirds
)
# both these are divided by sed density to give a vol flux
self._Qs_power_onA = self._c * (0.6 + self._b / 15.0)
self._Qs_power_onAthresh = twothirds * self._b * self._c
self._cell_areas = np.empty(grid.number_of_nodes)
self._cell_areas.fill(np.mean(grid.area_of_cell))
self._cell_areas[grid.node_at_cell] = grid.area_of_cell
# set up the necessary fields:
self.initialize_output_fields()
if self._return_ch_props:
self.initialize_optional_output_fields()
[docs]
def get_sed_flux_function(self, rel_sed_flux):
"""Get the sediment flux function.
Parameters
----------
rel_sed_flux
"""
if self._type == "generalized_humped":
"""Returns K*f(qs,qc)"""
sed_flux_fn = (
self._kappa
* (rel_sed_flux**self._nu + self._c)
* np.exp(-self._phi * rel_sed_flux)
)
elif self._type == "linear_decline":
sed_flux_fn = 1.0 - rel_sed_flux
elif self._type == "parabolic":
raise MissingKeyError(
"Pure parabolic (where intersect at zero flux is exactly "
+ "zero) is currently not supported, sorry. Try "
+ "almost_parabolic instead?"
)
sed_flux_fn = 1.0 - 4.0 * (rel_sed_flux - 0.5) ** 2.0
elif self._type == "almost_parabolic":
sed_flux_fn = np.where(
rel_sed_flux > 0.1,
1.0 - 4.0 * (rel_sed_flux - 0.5) ** 2.0,
2.6 * rel_sed_flux + 0.1,
)
elif self._type == "None":
sed_flux_fn = 1.0
else:
raise MissingKeyError(
"Provided sed flux sensitivity type in input file was not "
+ "recognised!"
)
return sed_flux_fn
[docs]
def get_sed_flux_function_pseudoimplicit(
self, sed_in, trans_cap_vol_out, prefactor_for_volume, prefactor_for_dz
):
"""Get the pseudoimplicit sediment flux function.
Parameters
----------
sed_in
trans_cap_vol_out
prefactor_for_volume
prefactor_for_dz
"""
rel_sed_flux_in = sed_in / trans_cap_vol_out
rel_sed_flux = rel_sed_flux_in
if self._type == "generalized_humped":
"""Returns K*f(qs,qc)"""
def sed_flux_fn_gen(rel_sed_flux_in):
return (
self._kappa
* (rel_sed_flux_in**self._nu + self._c)
* np.exp(-self._phi * rel_sed_flux_in)
)
elif self._type == "linear_decline":
def sed_flux_fn_gen(rel_sed_flux_in):
return 1.0 - rel_sed_flux_in
elif self._type == "parabolic":
raise MissingKeyError(
"Pure parabolic (where intersect at zero flux is exactly "
+ "zero) is currently not supported, sorry. Try "
+ "almost_parabolic instead?"
)
def sed_flux_fn_gen(rel_sed_flux_in):
return 1.0 - 4.0 * (rel_sed_flux_in - 0.5) ** 2.0
elif self._type == "almost_parabolic":
def sed_flux_fn_gen(rel_sed_flux_in):
return np.where(
rel_sed_flux_in > 0.1,
1.0 - 4.0 * (rel_sed_flux_in - 0.5) ** 2.0,
2.6 * rel_sed_flux_in + 0.1,
)
elif self._type == "None":
def sed_flux_fn_gen(rel_sed_flux_in):
return 1.0
else:
raise MissingKeyError(
"Provided sed flux sensitivity type in input file was not "
+ "recognised!"
)
for _ in range(self._pseudoimplicit_repeats):
sed_flux_fn = sed_flux_fn_gen(rel_sed_flux)
sed_vol_added = prefactor_for_volume * sed_flux_fn
rel_sed_flux = rel_sed_flux_in + sed_vol_added / trans_cap_vol_out
# print rel_sed_flux
if rel_sed_flux >= 1.0:
rel_sed_flux = 1.0
break
if rel_sed_flux < 0.0:
rel_sed_flux = 0.0
break
last_sed_flux_fn = sed_flux_fn
sed_flux_fn = sed_flux_fn_gen(rel_sed_flux)
# this error could alternatively be used to break the loop
error_in_sed_flux_fn = sed_flux_fn - last_sed_flux_fn
dz = prefactor_for_dz * sed_flux_fn
sed_flux_out = rel_sed_flux * trans_cap_vol_out
return dz, sed_flux_out, rel_sed_flux, error_in_sed_flux_fn
[docs]
def run_one_step(self, dt):
"""Run the component across one timestep increment, dt.
Erosion occurs according to the sediment dependent rules specified
during initialization. Method is fully equivalent to the :func:`erode`
method.
Parameters
----------
dt : float (years, only!)
Timestep for which to run the component.
"""
grid = self._grid
node_z = grid.at_node["topographic__elevation"]
node_A = grid.at_node["drainage_area"]
flow_receiver = grid.at_node["flow__receiver_node"]
s_in = grid.at_node["flow__upstream_node_order"]
node_S = grid.at_node["topographic__steepest_slope"]
if isinstance(self._flooded_depths, str):
flooded_depths = grid.at_node[self._flooded_depths]
# also need a map of initial flooded conds:
flooded_nodes = flooded_depths > 0.0
elif isinstance(self._flooded_depths, np.ndarray):
assert self._flooded_depths.size == self._grid.number_of_nodes
flooded_nodes = self._flooded_depths > 0.0
# need an *updateable* record of the pit depths
else:
# if None, handle in loop
flooded_nodes = None
steepest_link = "flow__link_to_receiver_node"
link_length = np.empty(grid.number_of_nodes, dtype=float)
link_length.fill(np.nan)
draining_nodes = np.not_equal(grid.at_node[steepest_link], self._grid.BAD_INDEX)
core_draining_nodes = np.intersect1d(
np.where(draining_nodes)[0], grid.core_nodes, assume_unique=True
)
link_length[core_draining_nodes] = grid.length_of_d8[
grid.at_node[steepest_link][core_draining_nodes]
]
if self._Qc == "MPM":
if self._Dchar_in is not None:
self._Dchar = self._Dchar_in
else:
assert not self._set_threshold, (
"Something is seriously wrong with your model " + "initialization."
)
assert self._override_threshold, (
"You need to confirm to the module you intend it to "
+ "internally calculate a shear stress threshold, "
+ "with set_threshold_from_Dchar in the input file."
)
# we need to adjust the thresholds for the Shields number
# & gs dynamically:
variable_thresh = (
self._shields_crit
* self._g
* (self._sed_density - self._fluid_density)
* self._Dchar
)
if self._lamb_flag:
variable_shields_crit = 0.15 * node_S**0.25
try:
variable_thresh = (
variable_shields_crit * self._shields_prefactor_to_shear
)
except AttributeError:
variable_thresh = (
variable_shields_crit
* self._shields_prefactor_to_shear_noDchar
* self._Dchar
)
node_Q = self._k_Q * self._runoff_rate * node_A**self._c
shear_stress_prefactor_timesAparts = (
self._shear_stress_prefactor * node_Q**self._point6onelessb
)
try:
transport_capacities_thresh = (
self._thresh
* self._Qs_thresh_prefactor
* self._runoff_rate ** (0.66667 * self._b)
* node_A**self._Qs_power_onAthresh
)
except AttributeError:
transport_capacities_thresh = (
variable_thresh
* self._Qs_thresh_prefactor
* self._runoff_rate ** (0.66667 * self._b)
* node_A**self._Qs_power_onAthresh
)
transport_capacity_prefactor_withA = (
self._Qs_prefactor
* self._runoff_rate ** (0.6 + self._b / 15.0)
* node_A**self._Qs_power_onA
)
internal_t = 0.0
break_flag = False
dt_secs = dt * 31557600.0
counter = 0
rel_sed_flux = np.empty_like(node_Q)
# excess_vol_overhead = 0.
while 1:
# ^use the break flag, to improve computational efficiency for
# runs which are very stable
# we assume the drainage structure is forbidden to change
# during the whole dt
# note slopes will be *negative* at pits
# track how many loops we perform:
counter += 1
downward_slopes = node_S.clip(0.0)
# this removes the tendency to transfer material against
# gradient, including in any lake depressions
# we DON'T immediately zero trp capacity in the lake.
# positive_slopes = np.greater(downward_slopes, 0.)
slopes_tothe07 = downward_slopes**0.7
transport_capacities_S = (
transport_capacity_prefactor_withA * slopes_tothe07
)
trp_diff = (transport_capacities_S - transport_capacities_thresh).clip(
0.0
)
transport_capacities = np.sqrt(trp_diff * trp_diff * trp_diff)
shear_stress = shear_stress_prefactor_timesAparts * slopes_tothe07
shear_tothe_a = shear_stress**self._a
dt_this_step = dt_secs - internal_t
# ^timestep adjustment is made AFTER the dz calc
node_vol_capacities = transport_capacities * dt_this_step
sed_into_node = np.zeros(grid.number_of_nodes, dtype=float)
dz = np.zeros(grid.number_of_nodes, dtype=float)
cell_areas = self._cell_areas
try:
raise NameError
# ^tripped out deliberately for now; doesn't appear to
# accelerate much
weave.inline(
self._routing_code,
[
"len_s_in",
"sed_into_node",
"transport_capacities",
"dz",
"cell_areas",
"dt_this_step",
"flow__receiver_node",
],
)
except NameError:
for i in s_in[::-1]: # work downstream
cell_area = cell_areas[i]
if flooded_nodes is not None:
flood_depth = flooded_depths[i]
else:
flood_depth = 0.0
sed_flux_into_this_node = sed_into_node[i]
node_capacity = transport_capacities[i]
# ^we work in volume flux, not volume per se here
node_vol_capacity = node_vol_capacities[i]
if flood_depth > 0.0:
node_vol_capacity = 0.0
# requires special case handling - as much sed as
# possible is dumped here, then the remainder
# passed on
if sed_flux_into_this_node < node_vol_capacity:
# ^note incision is forbidden at capacity
# flooded nodes never enter this branch
# #implementing the pseudoimplicit method:
try:
thresh = variable_thresh
except NameError: # it doesn't exist
thresh = self._thresh
dz_prefactor = (
self._K_unit_time
* dt_this_step
* (shear_tothe_a[i] - thresh).clip(0.0)
)
vol_prefactor = dz_prefactor * cell_area
(
dz_here,
sed_flux_out,
rel_sed_flux_here,
error_in_sed_flux,
) = self.get_sed_flux_function_pseudoimplicit(
sed_flux_into_this_node,
node_vol_capacity,
vol_prefactor,
dz_prefactor,
)
# note now dz_here may never create more sed than
# the out can transport...
assert sed_flux_out <= node_vol_capacity, (
"failed at node "
+ str(s_in.size - i)
+ " with rel sed flux "
+ str(sed_flux_out / node_capacity)
)
rel_sed_flux[i] = rel_sed_flux_here
vol_pass = sed_flux_out
else:
rel_sed_flux[i] = 1.0
vol_dropped = sed_flux_into_this_node - node_vol_capacity
dz_here = -vol_dropped / cell_area
# with the pits, we aim to inhibit incision, but
# depo is OK. We have already zero'd any adverse
# grads, so sed can make it to the bottom of the
# pit but no further in a single step, which seems
# raeasonable. Pit should fill.
if flood_depth <= 0.0:
vol_pass = node_vol_capacity
else:
height_excess = -dz_here - flood_depth
# ...above water level
if height_excess <= 0.0:
vol_pass = 0.0
# dz_here is already correct
flooded_depths[i] += dz_here
else:
dz_here = -flood_depth
vol_pass = height_excess * cell_area
# ^bit cheeky?
flooded_depths[i] = 0.0
# note we must update flooded depths
# transiently to conserve mass
# do we need to retain a small downhill slope?
# ...don't think so. Will resolve itself on next
# timestep.
dz[i] -= dz_here
sed_into_node[flow_receiver[i]] += vol_pass
break_flag = True
node_z[grid.core_nodes] += dz[grid.core_nodes]
if break_flag:
break
# do we need to reroute the flow/recalc the slopes here?
# -> NO, slope is such a minor component of Diff we'll be OK
# BUT could be important not for the stability, but for the
# actual calc. So YES.
node_S = np.zeros_like(node_S)
node_S[core_draining_nodes] = (node_z - node_z[flow_receiver])[
core_draining_nodes
] / link_length[core_draining_nodes]
internal_t += dt_this_step # still in seconds, remember
elif self._Qc == "power_law":
transport_capacity_prefactor_withA = self._Kt * node_A**self._mt
erosion_prefactor_withA = self._K_unit_time * node_A**self._m
# ^doesn't include S**n*f(Qc/Qc)
internal_t = 0.0
break_flag = False
dt_secs = dt * 31557600.0
counter = 0
rel_sed_flux = np.empty_like(node_A)
while 1:
counter += 1
# print counter
downward_slopes = node_S.clip(0.0)
# positive_slopes = np.greater(downward_slopes, 0.)
slopes_tothen = downward_slopes**self._n
slopes_tothent = downward_slopes**self._nt
transport_capacities = (
transport_capacity_prefactor_withA * slopes_tothent
)
erosion_prefactor_withS = (
erosion_prefactor_withA * slopes_tothen
) # no time, no fqs
# shear_tothe_a = shear_stress**self._a
dt_this_step = dt_secs - internal_t
# ^timestep adjustment is made AFTER the dz calc
node_vol_capacities = transport_capacities * dt_this_step
sed_into_node = np.zeros(grid.number_of_nodes, dtype=float)
dz = np.zeros(grid.number_of_nodes, dtype=float)
cell_areas = self._cell_areas
for i in s_in[::-1]: # work downstream
cell_area = cell_areas[i]
if flooded_nodes is not None:
flood_depth = flooded_depths[i]
else:
flood_depth = 0.0
sed_flux_into_this_node = sed_into_node[i]
node_capacity = transport_capacities[i]
# ^we work in volume flux, not volume per se here
node_vol_capacity = node_vol_capacities[i]
if flood_depth > 0.0:
node_vol_capacity = 0.0
if sed_flux_into_this_node < node_vol_capacity:
# ^note incision is forbidden at capacity
dz_prefactor = dt_this_step * erosion_prefactor_withS[i]
vol_prefactor = dz_prefactor * cell_area
(
dz_here,
sed_flux_out,
rel_sed_flux_here,
error_in_sed_flux,
) = self.get_sed_flux_function_pseudoimplicit(
sed_flux_into_this_node,
node_vol_capacity,
vol_prefactor,
dz_prefactor,
)
# note now dz_here may never create more sed than the
# out can transport...
assert sed_flux_out <= node_vol_capacity, (
"failed at node "
+ str(s_in.size - i)
+ " with rel sed flux "
+ str(sed_flux_out / node_capacity)
)
rel_sed_flux[i] = rel_sed_flux_here
vol_pass = sed_flux_out
else:
rel_sed_flux[i] = 1.0
vol_dropped = sed_flux_into_this_node - node_vol_capacity
dz_here = -vol_dropped / cell_area
try:
isflooded = flooded_nodes[i]
except TypeError: # was None
isflooded = False
if flood_depth <= 0.0 and not isflooded:
vol_pass = node_vol_capacity
# we want flooded nodes which have already been
# filled to enter the else statement
else:
height_excess = -dz_here - flood_depth
# ...above water level
if height_excess <= 0.0:
vol_pass = 0.0
# dz_here is already correct
flooded_depths[i] += dz_here
else:
dz_here = -flood_depth
vol_pass = height_excess * cell_area
# ^bit cheeky?
flooded_depths[i] = 0.0
dz[i] -= dz_here
sed_into_node[flow_receiver[i]] += vol_pass
break_flag = True
node_z[grid.core_nodes] += dz[grid.core_nodes]
if break_flag:
break
# do we need to reroute the flow/recalc the slopes here?
# -> NO, slope is such a minor component of Diff we'll be OK
# BUT could be important not for the stability, but for the
# actual calc. So YES.
node_S = np.zeros_like(node_S)
# print link_length[core_draining_nodes]
node_S[core_draining_nodes] = (node_z - node_z[flow_receiver])[
core_draining_nodes
] / link_length[core_draining_nodes]
internal_t += dt_this_step # still in seconds, remember
if self._return_ch_props:
# add the channel property field entries,
# 'channel__width', 'channel__depth', and 'channel__discharge'
W = self._k_w * node_Q**self._b
H = shear_stress / self._rho_g / node_S # ...sneaky!
grid.at_node["channel__width"][:] = W
grid.at_node["channel__depth"][:] = H
grid.at_node["channel__discharge"][:] = node_Q
grid.at_node["channel__bed_shear_stress"][:] = shear_stress
grid.at_node["channel_sediment__volumetric_transport_capacity"][
:
] = transport_capacities
grid.at_node["channel_sediment__volumetric_flux"][:] = sed_into_node
grid.at_node["channel_sediment__relative_flux"][:] = rel_sed_flux
# elevs set automatically to the name used in the function call.
self._iterations_in_dt = counter
return grid, grid.at_node["topographic__elevation"]
@property
@make_return_array_immutable
def characteristic_grainsize(self):
"""Return the characteristic grain size used by the component.
Particularly useful if the set_Dchar_from_threshold flag was True
at initialization.
Returns
-------
Dchar : float or array
Examples
--------
>>> from landlab import RasterModelGrid
>>> from landlab.components import FlowAccumulator, SedDepEroder
>>> mg1 = RasterModelGrid((3, 4))
>>> z1 = mg1.add_zeros("node", "topographic__elevation")
>>> fa1 = FlowAccumulator(mg1)
>>> thresh_shields = np.arange(1, mg1.number_of_nodes + 1, dtype=float)
>>> thresh_shields /= 100.0
>>> sde1 = SedDepEroder(
... mg1,
... threshold_shear_stress=100.0,
... Qc="MPM",
... Dchar=None,
... set_threshold_from_Dchar=False,
... set_Dchar_from_threshold=True,
... threshold_Shields=thresh_shields,
... g=9.81,
... )
>>> sde1.characteristic_grainsize.reshape(mg1.shape)
array([[0.59962823, 0.29981412, 0.19987608, 0.14990706],
[0.11992565, 0.09993804, 0.08566118, 0.07495353],
[0.06662536, 0.05996282, 0.05451166, 0.04996902]])
>>> mg2 = RasterModelGrid((3, 4))
>>> z2 = mg2.add_zeros("node", "topographic__elevation")
>>> fa2 = FlowAccumulator(mg2)
>>> sde2 = SedDepEroder(
... mg2,
... threshold_shear_stress=100.0,
... Qc="MPM",
... Dchar=None,
... set_threshold_from_Dchar=False,
... set_Dchar_from_threshold=True,
... threshold_Shields=None,
... slope_sensitive_threshold=True,
... g=9.81,
... )
>>> S = mg2.at_node["topographic__steepest_slope"]
>>> S[:] = 0.05 # thresh = 100 Pa @ 5pc slope
>>> sde2.characteristic_grainsize.reshape(mg2.shape)
array([[0.08453729, 0.08453729, 0.08453729, 0.08453729],
[0.08453729, 0.08453729, 0.08453729, 0.08453729],
[0.08453729, 0.08453729, 0.08453729, 0.08453729]])
"""
# Dchar is None means self._lamb_flag, Dchar is spatially variable,
# and not calculated until the main loop
assert (
self._Qc == "MPM"
), "Characteristic grainsize is only calculated if Qc == 'MPM'"
if self._Dchar_in is not None:
return self._Dchar_in
else:
taustarcrit = (
0.15 * self._grid.at_node["topographic__steepest_slope"] ** 0.25
)
Dchar = self._thresh / (
self._g * (self._sed_density - self._fluid_density) * taustarcrit
)
return Dchar