#! /usr/env/python
"""
flow_director_dinf.py: provides the component FlowDirectorDINF.
Directs flow on raster grids only using the Dinfinity algorithm of
Tarboton 1997.
"""
import numpy
from landlab import NodeStatus
from landlab.components.flow_director import flow_direction_dinf
from landlab.components.flow_director.flow_director_to_many import _FlowDirectorToMany
[docs]
class FlowDirectorDINF(_FlowDirectorToMany):
"""Flow direction on a raster grid by the D infinity method.
Directs flow by the D infinity method (Tarboton, 1997). Each node is
assigned two flow directions, toward the two neighboring nodes that are on
the steepest subtriangle. Partitioning of flow is done based on the aspect
of the subtriangle.
Specifically, it stores as ModelGrid fields:
- Node array of receivers (nodes that receive flow), or ITS OWN ID if
there is no receiver: *'flow__receiver_node'*. This array is 2D, and is
of dimension (number of nodes x max number of receivers).
- Node array of flow proportions: *'flow__receiver_proportions'*. This
array is 2D, and is of dimension (number of nodes x max number of
receivers).
- Node array of links carrying flow: *'flow__link_to_receiver_node'*.
This array is 2D, and is of dimension (number of nodes x max number of
receivers).
- Node array of downhill slopes corresponding to each receiver.:
*'topographic__steepest_slope'* This array is 2D, and is
of dimension (number of nodes x max number of receivers). If the slope is
uphill or flat, the value is assigned zero.
- Boolean node array of all local lows: *'flow__sink_flag'*
- Link array identifing if flow goes with (1) or against (-1) the link
direction: *'flow__link_direction'*
The primary method of this class is :func:`run_one_step`.
Examples
--------
This method works for both raster and irregular grids. First we will look
at a raster example, and then an irregular example.
>>> import numpy as numpy
>>> from landlab import RasterModelGrid
>>> from landlab.components import FlowDirectorDINF
>>> mg = RasterModelGrid((4, 4), xy_spacing=(1, 1))
>>> mg.set_closed_boundaries_at_grid_edges(True, True, True, False)
>>> _ = mg.add_field(
... "topographic__elevation",
... mg.node_x**2 + mg.node_y**2,
... at="node",
... )
The DINF flow director can be uses for raster grids only.
>>> fd = FlowDirectorDINF(mg, "topographic__elevation")
>>> fd.surface_values.reshape(mg.shape)
array([[ 0., 1., 4., 9.],
[ 1., 2., 5., 10.],
[ 4., 5., 8., 13.],
[ 9., 10., 13., 18.]])
>>> fd.run_one_step()
Unlike flow directors that only direct flow to one node or to all
downstream nodes, FlowDirectorDINF directs flow two nodes only. It stores
the receiver information is a (number of nodes x 2) shape field at nodes.
>>> mg.at_node["flow__receiver_node"]
array([[ 0, -1],
[ 1, -1],
[ 2, -1],
[ 3, -1],
[ 4, -1],
[ 0, -1],
[ 5, 1],
[ 7, -1],
[ 8, -1],
[ 5, -1],
[ 5, -1],
[11, -1],
[12, -1],
[13, -1],
[14, -1],
[15, -1]])
It also stores the proportions of flow going to each receiver, the link on
which the flow moves in at node arrays, and the slope of each link.
>>> mg.at_node["flow__receiver_proportions"]
array([[1. , 0. ],
[1. , 0. ],
[1. , 0. ],
[1. , 0. ],
[1. , 0. ],
[1. , 0. ],
[0.59033447, 0.40966553],
[1. , 0. ],
[1. , 0. ],
[1. , 0. ],
[1. , 0. ],
[1. , 0. ],
[1. , 0. ],
[1. , 0. ],
[1. , 0. ],
[1. , 0. ]])
>>> mg.at_node["flow__link_to_receiver_node"]
array([[-1, -1],
[-1, -1],
[-1, -1],
[-1, -1],
[ 3, 25],
[24, 4],
[ 8, 26],
[ 9, 28],
[14, 31],
[11, 33],
[32, 12],
[16, 34],
[21, 37],
[18, 39],
[19, 38],
[20, 40]])
>>> mg.at_node["topographic__steepest_slope"]
array([[-1.00000000e+00, -1.41421356e+00],
[ 1.00000000e+00, -7.12763635e+02],
[ 3.00000000e+00, 1.41421356e+00],
[ 5.00000000e+00, 2.82842712e+00],
[ 1.00900000e+03, 7.12763635e+02],
[ 1.41421356e+00, 1.00000000e+00],
[ 3.00000000e+00, 2.82842712e+00],
[ 1.00400000e+03, 7.10642315e+02],
[ 1.00400000e+03, 7.12056529e+02],
[ 3.00000000e+00, 0.00000000e+00],
[ 4.24264069e+00, 3.00000000e+00],
[ 1.00100000e+03, 7.09935208e+02],
[-0.00000000e+00, 7.09935208e+02],
[ 1.00400000e+03, 7.07813888e+02],
[ 1.00100000e+03, 7.09935208e+02],
[ 0.00000000e+00, 7.07813888e+02]])
Finally, FlowDirectorDINF identifies sinks, or local lows.
>>> mg.at_node["flow__sink_flag"].astype(int)
array([1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1])
The flow directors also have the ability to return the flow receiver nodes
through a function called direct_flow()
>>> fd = FlowDirectorDINF(mg, "topographic__elevation")
>>> fd.run_one_step()
>>> receivers, proportions = fd.direct_flow()
>>> receivers
array([[ 0, -1],
[ 1, -1],
[ 2, -1],
[ 3, -1],
[ 4, -1],
[ 0, -1],
[ 5, 1],
[ 7, -1],
[ 8, -1],
[ 5, -1],
[ 5, -1],
[11, -1],
[12, -1],
[13, -1],
[14, -1],
[15, -1]])
>>> proportions
array([[1. , 0. ],
[1. , 0. ],
[1. , 0. ],
[1. , 0. ],
[1. , 0. ],
[1. , 0. ],
[0.59033447, 0.40966553],
[1. , 0. ],
[1. , 0. ],
[1. , 0. ],
[1. , 0. ],
[1. , 0. ],
[1. , 0. ],
[1. , 0. ],
[1. , 0. ],
[1. , 0. ]])
For each donor node (represented by each row) the proportions should sum to
one.
>>> proportions.sum(axis=1)
array([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.])
References
----------
**Required Software Citation(s) Specific to this Component**
None Listed
**Additional References**
Tarboton, D. (1997). A new method for the determination of flow directions
and upslope areas in grid digital elevation models. Water Resources
Research 33(2), 309-319. https://dx.doi.org/10.1029/96wr03137
"""
_name = "FlowDirectorDINF"
_info = {
"flow__link_to_receiver_node": {
"dtype": int,
"intent": "out",
"optional": False,
"units": "-",
"mapping": "node",
"doc": "ID of link downstream of each node, which carries the discharge",
},
"flow__receiver_node": {
"dtype": int,
"intent": "out",
"optional": False,
"units": "-",
"mapping": "node",
"doc": "Node array of receivers (node that receives flow from current node)",
},
"flow__receiver_proportions": {
"dtype": float,
"intent": "out",
"optional": False,
"units": "-",
"mapping": "node",
"doc": "Node array of proportion of flow sent to each receiver.",
},
"flow__sink_flag": {
"dtype": bool,
"intent": "out",
"optional": False,
"units": "-",
"mapping": "node",
"doc": "Boolean array, True at local lows",
},
"topographic__elevation": {
"dtype": float,
"intent": "in",
"optional": True,
"units": "m",
"mapping": "node",
"doc": "Land surface topographic elevation",
},
"topographic__steepest_slope": {
"dtype": float,
"intent": "out",
"optional": False,
"units": "-",
"mapping": "node",
"doc": "The steepest *downhill* slope",
},
}
[docs]
def __init__(self, grid, surface="topographic__elevation"):
"""
Parameters
----------
grid : ModelGrid
A grid.
surface : field name at node or array of length node, optional
The surface to direct flow across, default is field at node:
topographic__elevation.
partition_method: string, optional
Method for partitioning flow. Options include 'slope' (default) and
'square_root_of_slope'.
"""
self._method = "DINF"
self._max_receivers = 2
super().__init__(grid, surface)
try:
self._grid.nodes_at_d8
except AttributeError:
self._is_Voroni = True
else:
self._is_Voroni = False
if self._is_Voroni:
raise NotImplementedError(
"FlowDirectorDINF is not implemented" " for irregular grids."
)
self.updated_boundary_conditions()
[docs]
def updated_boundary_conditions(self):
"""Method to update FlowDirectorDINF when boundary conditions change.
Call this if boundary conditions on the grid are updated after
the component is instantiated.
"""
self._active_links = self._grid.active_links
self._activelink_tail = self._grid.node_at_link_tail[self._grid.active_links]
self._activelink_head = self._grid.node_at_link_head[self._grid.active_links]
[docs]
def run_one_step(self):
"""Find flow directions and save to the model grid.
run_one_step() checks for updated boundary conditions, calculates
slopes on links, finds basself._surface_valuesel nodes based on the status at node,
calculates flow directions, and saves results to the grid.
An alternative to direct_flow() is direct_flow() which does the same
things but also returns the receiver nodes not return values.
"""
self.direct_flow()
[docs]
def direct_flow(self):
"""Find flow directions, save to the model grid, and return receivers.
direct_flow() checks for updated boundary conditions, calculates
slopes on links, finds basself._surface_valuesel nodes based on the status at node,
calculates flow directions, saves results to the grid, and returns a
at-node array of receiver nodes. This array is stored in the grid at:
grid['node']['flow__receiver_nodes']
An alternative to direct_flow() is run_one_step() which does the same
things but also returns a at-node array of receiver nodes. This array
is stored in the grid at:
grid['node']['flow__receiver_nodes']
"""
self._check_updated_bc()
# Step 1. Find and save base level nodes.
(baselevel_nodes,) = numpy.where(
numpy.logical_or(
self._grid.status_at_node == NodeStatus.FIXED_VALUE,
self._grid.status_at_node == NodeStatus.FIXED_GRADIENT,
)
)
# Calculate flow directions
(
self._receivers,
self._proportions,
slopes_to_receivers,
steepest_slope,
steepest_receiver,
sink,
receiver_links,
steepest_link,
) = flow_direction_dinf.flow_directions_dinf(
self._grid, self._surface_values, baselevel_nodes=baselevel_nodes
)
# Save the four ouputs of this component.
self._grid["node"]["flow__receiver_node"][:] = self._receivers
self._grid["node"]["flow__receiver_proportions"][:] = self._proportions
self._grid["node"]["topographic__steepest_slope"][:] = slopes_to_receivers
self._grid["node"]["flow__link_to_receiver_node"][:] = receiver_links
self._grid["node"]["flow__sink_flag"][:] = False
self._grid["node"]["flow__sink_flag"][sink] = True
return (self._receivers, self._proportions)
if __name__ == "__main__": # pragma: no cover
import doctest
doctest.testmod()