Ejemplo n.º 1
0
    def find_perimeter_nodes(self, pts):
        """
        Uses a convex hull to locate the perimeter nodes of the Voronoi grid,
        then sets them as fixed value boundary nodes.
        It then sets/updates the various relevant node lists held by the grid,
        and returns *node_status*, *core_nodes*, *boundary_nodes*.
        """

        # Calculate the convex hull for the set of points
        from scipy.spatial import ConvexHull
        hull = ConvexHull(pts, qhull_options='Qc')  # see below why we use 'Qt'

        # The ConvexHull object lists the edges that form the hull. We need to
        # get from this list of edges the unique set of nodes. To do this, we
        # first flatten the list of vertices that make up all the hull edges
        # ("simplices"), so it becomes a 1D array. With that, we can use the 
        # set() function to turn the array into a set, which removes duplicate 
        # vertices. Then we turn it back into an array, which now contains the 
        # set of IDs for the nodes that make up the convex hull.
        #   The next thing to worry about is the fact that the mesh perimeter
        # might contain nodes that are co-planar (that is, co-linear in our 2D
        # world). For example, if you make a set of staggered points for a
        # hexagonal lattice using make_hex_points(), there will be some
        # co-linear points along the perimeter. The ones of these that don't
        # form convex corners won't be included in convex_hull_nodes, but they
        # are nonetheless part of the perimeter and need to be included in
        # the list of boundary_nodes. To deal with this, we pass the 'Qt'
        # option to ConvexHull, which makes it generate a list of coplanar
        # points. We include these in our set of boundary nodes.
        convex_hull_nodes = numpy.array(list(set(hull.simplices.flatten())))
        coplanar_nodes = hull.coplanar[:, 0]
        boundary_nodes = as_id_array(numpy.concatenate(
            (convex_hull_nodes, coplanar_nodes)))

        # Now we'll create the "node_status" array, which contains the code
        # indicating whether the node is interior and active (=0) or a
        # boundary (=1). This means that all perimeter (convex hull) nodes are
        # initially flagged as boundary code 1. An application might wish to 
        # change this so that, for example, some boundaries are inactive.
        node_status = numpy.zeros(len(pts[:, 0]), dtype=numpy.int8)
        node_status[boundary_nodes] = 1

        # It's also useful to have a list of interior nodes
        core_nodes = as_id_array(numpy.where(node_status == 0)[0])

        # save the arrays and update the properties
        self._node_status = node_status
        self._num_active_nodes = node_status.size
        self._num_core_nodes = len(core_nodes)
        self._num_core_cells = len(core_nodes)
        self._core_cells = numpy.arange(len(core_nodes), dtype=numpy.int)
        self.active_cells = numpy.arange(node_status.size, dtype=numpy.int)
        self._node_at_cell = core_nodes
        self._boundary_nodes = boundary_nodes

        # Return the results
        return node_status, core_nodes, boundary_nodes
Ejemplo n.º 2
0
    def _find_perimeter_nodes_and_BC_set(self, pts):
        """
        Uses a convex hull to locate the perimeter nodes of the Voronoi grid,
        then sets them as fixed value boundary nodes.
        It then sets/updates the various relevant node lists held by the grid,
        and returns *node_status*, *core_nodes*, *boundary_nodes*.
        """

        # Calculate the convex hull for the set of points
        from scipy.spatial import ConvexHull

        hull = ConvexHull(pts, qhull_options="Qc")  # see below why we use 'Qt'

        # The ConvexHull object lists the edges that form the hull. We need to
        # get from this list of edges the unique set of nodes. To do this, we
        # first flatten the list of vertices that make up all the hull edges
        # ("simplices"), so it becomes a 1D array. With that, we can use the
        # set() function to turn the array into a set, which removes duplicate
        # vertices. Then we turn it back into an array, which now contains the
        # set of IDs for the nodes that make up the convex hull.
        #   The next thing to worry about is the fact that the mesh perimeter
        # might contain nodes that are co-planar (that is, co-linear in our 2D
        # world). For example, if you make a set of staggered points for a
        # hexagonal lattice using make_hex_points(), there will be some
        # co-linear points along the perimeter. The ones of these that don't
        # form convex corners won't be included in convex_hull_nodes, but they
        # are nonetheless part of the perimeter and need to be included in
        # the list of boundary_nodes. To deal with this, we pass the 'Qt'
        # option to ConvexHull, which makes it generate a list of coplanar
        # points. We include these in our set of boundary nodes.
        convex_hull_nodes = np.array(list(set(hull.simplices.flatten())))
        coplanar_nodes = hull.coplanar[:, 0]
        boundary_nodes = as_id_array(
            np.concatenate((convex_hull_nodes, coplanar_nodes)))

        # Now we'll create the "node_status" array, which contains the code
        # indicating whether the node is interior and active (=0) or a
        # boundary (=1). This means that all perimeter (convex hull) nodes are
        # initially flagged as boundary code 1. An application might wish to
        # change this so that, for example, some boundaries are inactive.
        node_status = np.zeros(len(pts[:, 0]), dtype=np.uint8)
        node_status[boundary_nodes] = 1

        # It's also useful to have a list of interior nodes
        core_nodes = as_id_array(np.where(node_status == 0)[0])

        # save the arrays and update the properties
        self._node_status = node_status
        self._node_at_cell = core_nodes
        self._boundary_nodes = boundary_nodes

        self.status_at_node = node_status

        # Return the results
        return node_status, core_nodes, boundary_nodes
Ejemplo n.º 3
0
def flow_accumulation(receiver_nodes,
                      node_cell_area=1.0,
                      runoff_rate=1.0,
                      boundary_nodes=None):
    """Calculate drainage area and (steady) discharge.

    Calculates and returns the drainage area and (steady) discharge at each
    node, along with a downstream-to-upstream ordered list (array) of node IDs.

    Examples
    --------
    >>> import numpy as np
    >>> from landlab.components.flow_accum import flow_accumulation
    >>> r = np.array([2, 5, 2, 7, 5, 5, 6, 5, 7, 8])-1
    >>> a, q, s = flow_accumulation(r)
    >>> a
    array([  1.,   3.,   1.,   1.,  10.,   4.,   3.,   2.,   1.,   1.])
    >>> q
    array([  1.,   3.,   1.,   1.,  10.,   4.,   3.,   2.,   1.,   1.])
    >>> s
    array([4, 1, 0, 2, 5, 6, 3, 8, 7, 9])
    """

    s = as_id_array(make_ordered_node_array(receiver_nodes))
    # Note that this ordering of s DOES INCLUDE closed nodes. It really shouldn't!
    # But as we don't have a copy of the grid accessible here, we'll solve this
    # problem as part of route_flow_dn.

    a, q = find_drainage_area_and_discharge(s, receiver_nodes, node_cell_area,
                                            runoff_rate, boundary_nodes)

    return a, q, s
Ejemplo n.º 4
0
    def _setup_links_to_update_after_uplift(self):
        """Create and store array with IDs of links for which to update
        transitions after uplift.

        These are: all active boundary links, plus the lowest non-boundary
        links, including the next-to-lowest vertical links and those angling
        that are below them.

        Examples
        --------
        >>> from landlab import HexModelGrid
        >>> hg = HexModelGrid((6, 6), orientation="vertical", node_layout="rect")
        >>> lu = LatticeUplifter(grid=hg)
        >>> lu.links_to_update
        array([ 6,  7,  9, 10, 11, 12, 13, 14, 16, 17, 18, 19, 20, 22, 23, 24, 28,
               32, 36, 40, 44, 48, 52, 56, 60, 64, 68, 70, 71, 72, 73, 74, 75, 77,
               78])
        >>> hg = HexModelGrid((5, 5), orientation="vertical", node_layout="rect")
        >>> lu = LatticeUplifter(grid=hg)
        >>> lu.links_to_update
        array([ 5,  8,  9, 11, 12, 13, 14, 15, 16, 18, 20, 23, 26, 29, 33, 36, 39,
               42, 44, 46, 47, 48, 49, 50, 51])
        """
        g = self.grid
        nc = g.number_of_node_columns
        max_link_id = 3 * (nc - 1) + 2 * ((nc + 1) // 2) + nc // 2
        lower_active = logical_and(
            arange(g.number_of_links) < max_link_id, g.status_at_link == 0)
        boundary = logical_or(
            g.status_at_node[g.node_at_link_tail] != 0,
            g.status_at_node[g.node_at_link_head] != 0,
        )
        active_bnd = logical_and(boundary, g.status_at_link == 0)
        self.links_to_update = as_id_array(
            where(logical_or(lower_active, active_bnd))[0])
Ejemplo n.º 5
0
    def _create_profile_structure(self):
        """Create the profile_IDs data structure for channel network.

        The bound attribute self._profile structure is the channel segment
        datastructure. Profile structure is a list of length
        starting_nodes. Each element of profile_structure is itself a
        list of length number of stream segments that drain to each of the
        starting nodes. Each stream segment list contains the node ids of a
        stream segment from downstream to upstream.
        """
        self._data_struct = OrderedDict()

        for i in self._starting_nodes:
            channel_segment = []
            current_node = i

            # march downstream
            while self._flow_receiver[current_node] != current_node:
                channel_segment.append(current_node)
                current_node = self._flow_receiver[current_node]
            channel_segment.append(current_node)

            channel_segment.reverse()
            segment_tuple = (current_node, i)
            self._data_struct[i] = {
                segment_tuple: {
                    "ids": as_id_array(channel_segment)
                }
            }

        self._calculate_distances()
        self.assign_colors()
        self._create_flat_structures()
Ejemplo n.º 6
0
    def _setup_links_to_update_after_uplift(self):
        """Create and store array with IDs of links for which to update
        transitions after uplift.

        These are: all active boundary links, plus the lowest non-boundary
        links, including the next-to-lowest vertical links and those angling
        that are below them.

        Examples
        --------
        >>> from landlab import HexModelGrid
        >>> hg = HexModelGrid(6, 6, orientation='vert', shape='rect')
        >>> lu = LatticeUplifter(grid=hg)
        >>> lu.links_to_update
        array([ 8,  9, 11, 12, 13, 14, 15, 16, 18, 19, 20, 21, 22, 24, 25, 26, 30,
               34, 38, 42, 46, 50, 54, 58, 62, 66, 70, 72, 73, 74, 75, 76, 77, 79,
               80])
        >>> hg = HexModelGrid(5, 5, orientation='vert', shape='rect')
        >>> lu = LatticeUplifter(grid=hg)
        >>> lu.links_to_update
        array([ 7, 10, 11, 13, 14, 15, 16, 17, 18, 20, 22, 25, 28, 31, 35, 38, 41,
               44, 46, 48, 49, 50, 51, 52, 53])
        """
        g = self.grid
        nc = g.number_of_node_columns
        max_link_id = (3 * (nc - 1) + 2 * ((nc + 1) // 2) + nc // 2
                       + (nc - 1) // 2)
        lower_active = logical_and(arange(g.number_of_links) < max_link_id,
                                   g.status_at_link == 0)
        boundary = logical_or(g.status_at_node[g.node_at_link_tail] != 0,
                              g.status_at_node[g.node_at_link_head] != 0)
        active_bnd = logical_and(boundary, g.status_at_link == 0)
        self.links_to_update = as_id_array(where(logical_or(lower_active,
                                                            active_bnd))[0])
    def _setup_links_to_update_after_uplift(self):
        """Create and store array with IDs of links for which to update
        transitions after uplift.

        These are: all active boundary links, plus the lowest non-boundary
        links, including the next-to-lowest vertical links and those angling
        that are below them.

        Examples
        --------
        >>> from landlab import HexModelGrid
        >>> hg = HexModelGrid(6, 6, orientation='vert', shape='rect')
        >>> lu = LatticeUplifter(grid=hg)
        >>> lu.links_to_update
        array([ 8,  9, 11, 12, 13, 14, 15, 16, 18, 19, 20, 21, 22, 24, 25, 26, 30,
               34, 38, 42, 46, 50, 54, 58, 62, 66, 70, 72, 73, 74, 75, 76, 77, 79,
               80])
        >>> hg = HexModelGrid(5, 5, orientation='vert', shape='rect')
        >>> lu = LatticeUplifter(grid=hg)
        >>> lu.links_to_update
        array([ 7, 10, 11, 13, 14, 15, 16, 17, 18, 20, 22, 25, 28, 31, 35, 38, 41,
               44, 46, 48, 49, 50, 51, 52, 53])
        """
        g = self.grid
        nc = g.number_of_node_columns
        max_link_id = (3 * (nc - 1) + 2 * ((nc + 1) // 2) + nc // 2
                       + (nc - 1) // 2)
        lower_active = logical_and(arange(g.number_of_links) < max_link_id,
                                   g.status_at_link == 0)
        boundary = logical_or(g.status_at_node[g.node_at_link_tail] != 0,
                              g.status_at_node[g.node_at_link_head] != 0)
        active_bnd = logical_and(boundary, g.status_at_link == 0)
        self.links_to_update = as_id_array(where(logical_or(lower_active,
                                                            active_bnd))[0])
Ejemplo n.º 8
0
def flow_accumulation(
    receiver_nodes, node_cell_area=1.0, runoff_rate=1.0, boundary_nodes=None
):

    """Calculate drainage area and (steady) discharge.

    Calculates and returns the drainage area and (steady) discharge at each
    node, along with a downstream-to-upstream ordered list (array) of node IDs.

    Examples
    --------
    >>> import numpy as np
    >>> from landlab.components.flow_accum import flow_accumulation
    >>> r = np.array([2, 5, 2, 7, 5, 5, 6, 5, 7, 8])-1
    >>> a, q, s = flow_accumulation(r)
    >>> a
    array([  1.,   3.,   1.,   1.,  10.,   4.,   3.,   2.,   1.,   1.])
    >>> q
    array([  1.,   3.,   1.,   1.,  10.,   4.,   3.,   2.,   1.,   1.])
    >>> s
    array([4, 1, 0, 2, 5, 6, 3, 8, 7, 9])
    """

    s = as_id_array(make_ordered_node_array(receiver_nodes))
    # Note that this ordering of s DOES INCLUDE closed nodes. It really shouldn't!
    # But as we don't have a copy of the grid accessible here, we'll solve this
    # problem as part of route_flow_dn.

    a, q = find_drainage_area_and_discharge(
        s, receiver_nodes, node_cell_area, runoff_rate, boundary_nodes
    )

    return a, q, s
Ejemplo n.º 9
0
    def _create_profile_structure(self):
        """Create the profile_IDs data structure for channel network.

        The bound attribute self._profile structure is the channel segment
        datastructure. profile structure is a list of length
        number_of_watersheds. Each element of profile_structure is itself a
        list of length number of stream segments that drain to each of the
        starting nodes. Each stream segment list contains the node ids of a
        stream segment from downstream to upstream.
        """
        self._data_struct = OrderedDict()

        if self._main_channel_only:
            for i in self._outlet_nodes:
                (channel_segment,
                 nodes_to_process) = self._get_channel_segment(i)
                segment_tuple = (channel_segment[0], channel_segment[-1])
                self._data_struct[i] = {
                    segment_tuple: {
                        "ids": as_id_array(channel_segment)
                    }
                }

        else:
            for i in self._outlet_nodes:
                channel_network = OrderedDict()
                queue = [i]
                while len(queue) > 0:
                    node_to_process = queue.pop(0)
                    (channel_segment, nodes_to_process
                     ) = self._get_channel_segment(node_to_process)
                    segment_tuple = (channel_segment[0], channel_segment[-1])
                    channel_network[segment_tuple] = {
                        "ids": as_id_array(channel_segment)
                    }
                    queue.extend(nodes_to_process)
                self._data_struct[i] = channel_network

        self._calculate_distances()
        self.assign_colors()
        self._create_flat_structures()
Ejemplo n.º 10
0
    def _setup_links_to_update_after_offset(self, in_footwall):
        """Create and store array with IDs of links for which to update
        transitions after fault offset.

        These are: all active boundary links with at least one node in the
        footwall, plus the lowest non-boundary links, including the
        next-to-lowest vertical links and those angling that are below them,
        plus the fault-crossing links.

        Examples
        --------
        >>> from landlab import HexModelGrid
        >>> hg = HexModelGrid(5, 5, orientation='vert', shape='rect')
        >>> lu = LatticeNormalFault(fault_x_intercept=-0.01, grid=hg)
        >>> lu.first_link_shifted_to
        37
        >>> lu.links_to_update
        array([ 7, 10, 11, 13, 14, 15, 16, 17, 18, 20, 22, 23, 24, 25, 26, 27, 29,
               30, 31, 33, 36, 38, 42, 44, 46, 50, 51, 53])
        """
        g = self.grid
        lower_active = logical_and(
            arange(g.number_of_links) < self.first_link_shifted_to,
            g.status_at_link == ACTIVE_LINK,
        )
        link_in_fw = logical_or(
            in_footwall[g.node_at_link_tail], in_footwall[g.node_at_link_head]
        )
        lower_active_fw = logical_and(lower_active, link_in_fw)
        active_bnd = logical_and(
            g.status_at_link == ACTIVE_LINK,
            logical_or(
                g.status_at_node[g.node_at_link_tail] != 0,
                g.status_at_node[g.node_at_link_head] != 0,
            ),
        )
        active_bnd_fw = logical_and(active_bnd, link_in_fw)
        crosses_fw = logical_and(
            g.status_at_link == ACTIVE_LINK,
            logical_xor(
                in_footwall[g.node_at_link_tail], in_footwall[g.node_at_link_head]
            ),
        )
        update = logical_or(logical_or(lower_active_fw, active_bnd_fw), crosses_fw)
        self.links_to_update = as_id_array(where(update)[0])
Ejemplo n.º 11
0
def flow_directions(
    elev,
    active_links,
    tail_node,
    head_node,
    link_slope,
    grid=None,
    baselevel_nodes=None,
):
    """Find flow directions on a grid.

    Finds and returns flow directions for a given elevation grid. Each node is
    assigned a single direction, toward one of its N neighboring nodes (or
    itself, if none of its neighbors are lower).

    Parameters
    ----------
    elev : array_like
        Elevations at nodes.
    active_links : array_like
        IDs of active links.
    tail_node : array_like
        IDs of the tail node for each link.
    head_node : array_like
        IDs of the head node for each link.
    link_slope : array_like
        slope of each link, defined POSITIVE DOWNHILL (i.e., a negative value
        means the link runs uphill from the fromnode to the tonode).
    baselevel_nodes : array_like, optional
        IDs of open boundary (baselevel) nodes.

    Returns
    -------
    receiver : ndarray
        For each node, the ID of the node that receives its flow. Defaults to
        the node itself if no other receiver is assigned.
    steepest_slope : ndarray
        The slope value (positive downhill) in the direction of flow
    sink : ndarray
        IDs of nodes that are flow sinks (they are their own receivers)
    receiver_link : ndarray
        ID of link that leads from each node to its receiver, or
        BAD_INDEX_VALUE if none.

    Examples
    --------
    The example below assigns elevations to the 10-node example network in
    Braun and Willett (2012), so that their original flow pattern should be
    re-created.

    >>> import numpy as np
    >>> from landlab.components.flow_director import flow_directions
    >>> z = np.array([2.4, 1.0, 2.2, 3.0, 0.0, 1.1, 2.0, 2.3, 3.1, 3.2])
    >>> fn = np.array([1,4,4,0,1,2,5,1,5,6,7,7,8,6,3,3,2,0])
    >>> tn = np.array([4,5,7,1,2,5,6,5,7,7,8,9,9,8,8,6,3,3])
    >>> s = z[fn] - z[tn]  # slope with unit link length, positive downhill
    >>> active_links = np.arange(len(fn))
    >>> r, ss, snk, rl = flow_directions(z, active_links, fn, tn, s)
    >>> r
    array([1, 4, 1, 6, 4, 4, 5, 4, 6, 7])
    >>> ss
    array([ 1.4,  1. ,  1.2,  1. ,  0. ,  1.1,  0.9,  2.3,  1.1,  0.9])
    >>> snk
    array([4])
    >>> rl[3:8]
    array([15, -1,  1,  6,  2])
    """
    # OK, the following are rough notes on design: we want to work with just
    # the active links. Ways to do this:
    # *  Pass active_links in as argument
    # *  In calling code, only refer to receiver_links for active nodes

    # Setup
    num_nodes = len(elev)
    steepest_slope = np.zeros(num_nodes)
    receiver = np.arange(num_nodes)
    receiver_link = BAD_INDEX_VALUE + np.zeros(num_nodes, dtype=np.int)

    # For each link, find the higher of the two nodes. The higher is the
    # potential donor, and the lower is the potential receiver. If the slope
    # from donor to receiver is steeper than the steepest one found so far for
    # the donor, then assign the receiver to the donor and record the new slope.
    # (Note the minus sign when looking at slope from "t" to "f").
    #
    # NOTE: MAKE SURE WE ARE ONLY LOOKING AT ACTIVE LINKS
    # THIS REMAINS A PROBLEM AS OF DEJH'S EFFORTS, MID MARCH 14.
    # overridden as part of fastscape_stream_power

    adjust_flow_receivers(
        tail_node,
        head_node,
        elev,
        link_slope,
        active_links,
        receiver,
        receiver_link,
        steepest_slope,
    )

    node_id = np.arange(num_nodes)

    # Optionally, handle baselevel nodes: they are their own receivers
    if baselevel_nodes is not None:
        receiver[baselevel_nodes] = node_id[baselevel_nodes]
        receiver_link[baselevel_nodes] = BAD_INDEX_VALUE
        steepest_slope[baselevel_nodes] = 0.0

    # The sink nodes are those that are their own receivers (this will normally
    # include boundary nodes as well as interior ones; "pits" would be sink
    # nodes that are also interior nodes).
    (sink, ) = np.where(node_id == receiver)
    sink = as_id_array(sink)

    return receiver, steepest_slope, sink, receiver_link
Ejemplo n.º 12
0
    def _create_patches_from_delaunay_diagram(self, pts, vor):
        """
        Uses a delaunay diagram drawn from the provided points to
        generate an array of patches and patch-node-link connectivity.
        Returns ...
        DEJH, 10/3/14, modified May 16.
        """
        from scipy.spatial import Delaunay
        from landlab.core.utils import anticlockwise_argsort_points_multiline
        from .cfuncs import find_rows_containing_ID, \
            create_patches_at_element, create_links_at_patch
        tri = Delaunay(pts)
        assert np.array_equal(tri.points, vor.points)
        nodata = -1
        self._nodes_at_patch = as_id_array(tri.simplices)
        # self._nodes_at_patch = np.empty_like(_nodes_at_patch)
        self._number_of_patches = tri.simplices.shape[0]
        # get the patches in order:
        patches_xy = np.empty((self._number_of_patches, 2), dtype=float)
        patches_xy[:, 0] = np.mean(self.node_x[self._nodes_at_patch], axis=1)
        patches_xy[:, 1] = np.mean(self.node_y[self._nodes_at_patch], axis=1)
        orderforsort = argsort_points_by_x_then_y(patches_xy)
        self._nodes_at_patch = self._nodes_at_patch[orderforsort, :]
        patches_xy = patches_xy[orderforsort, :]
        # get the nodes around the patch in order:
        nodes_xy = np.empty((3, 2), dtype=float)

        # perform a CCW sort without a line-by-line loop:
        patch_nodes_x = self.node_x[self._nodes_at_patch]
        patch_nodes_y = self.node_y[self._nodes_at_patch]
        anticlockwise_argsort_points_multiline(patch_nodes_x,
                                               patch_nodes_y,
                                               out=self._nodes_at_patch)

        # need to build a squared off, masked array of the patches_at_node
        # the max number of patches for a node in the grid is the max sides of
        # the side-iest voronoi region.
        max_dimension = len(max(vor.regions, key=len))

        self._patches_at_node = np.full((self.number_of_nodes, max_dimension),
                                        nodata,
                                        dtype=int)

        self._nodes_at_patch = as_id_array(self._nodes_at_patch)
        self._patches_at_node = as_id_array(self._patches_at_node)

        create_patches_at_element(self._nodes_at_patch, self.number_of_nodes,
                                  self._patches_at_node)

        # build the patch-link connectivity:
        self._links_at_patch = np.empty((self._number_of_patches, 3),
                                        dtype=int)
        create_links_at_patch(self._nodes_at_patch, self._links_at_node,
                              self._number_of_patches, self._links_at_patch)
        patch_links_x = self.x_of_link[self._links_at_patch]
        patch_links_y = self.y_of_link[self._links_at_patch]
        anticlockwise_argsort_points_multiline(patch_links_x,
                                               patch_links_y,
                                               out=self._links_at_patch)

        self._patches_at_link = np.empty((self.number_of_links, 2), dtype=int)
        self._patches_at_link.fill(-1)
        create_patches_at_element(self._links_at_patch, self.number_of_links,
                                  self._patches_at_link)
        # a sort of the links will be performed here once we have corners

        self._patches_created = True
Ejemplo n.º 13
0
def flow_directions_mfd(
    elev,
    neighbors_at_node,
    links_at_node,
    active_link_dir_at_node,
    link_slope,
    baselevel_nodes=None,
    partition_method="slope",
):
    """Find multiple-flow-direction flow directions on a grid.

    Finds and returns flow directions and proportions for a given elevation
    grid. Each node is assigned multiple flow directions, toward all of the N
    neighboring nodes that are lower than it. If none of the neighboring nodes
    are lower, it is assigned to itself. Flow proportions can be calculated as
    proportional to slope (default) or proportional to the square root of
    slope, which is the solution to a steady kinematic wave.

    Parameters
    ----------
    elev : array_like
        Elevations at nodes.
    neighbors_at_node : array_like (num nodes, max neighbors at node)
        For each node, the link IDs of active links.
    links_at_node : array_like (num nodes, max neighbors at node)

    link_dir_at_node: array_like (num nodes, max neighbors at node)

        IDs of the head node for each link.
    link_slope : array_like
        slope of each link, defined POSITIVE DOWNHILL (i.e., a negative value
        means the link runs uphill from the fromnode to the tonode).
    baselevel_nodes : array_like, optional
        IDs of open boundary (baselevel) nodes.
    partition_method: string, optional
        Method for partitioning flow. Options include 'slope' (default) and
        'square_root_of_slope'.

    Returns
    -------
    receivers : ndarray of size (num nodes, max neighbors at node)
        For each node, the IDs of the nodes that receive its flow. For nodes
        that do not direct flow to all neighbors, BAD_INDEX_VALUE is given as
        a placeholder. The ID of the node itself is given if no other receiver
        is assigned.
    proportions : ndarray of size (num nodes, max neighbors at node)
        For each receiver, the proportion of flow (between 0 and 1) is given.
        A proportion of zero indicates that the link does not have flow along
        it.
    slopes: ndarray of size (num nodes, max neighbors at node)
        For each node in the array ``recievers``, the slope value (positive
        downhill) in the direction of flow. If no flow occurs (value of
        ``recievers`` is -1), then this array is set to 0.
    steepest_slope : ndarray
        The slope value (positive downhill) in the direction of flow.
    steepest_receiver : ndarray
        For each node, the node ID of the node connected by the steepest link.
        BAD_INDEX_VALUE is given if no flow emmanates from the node.
    sink : ndarray
        IDs of nodes that are flow sinks (they are their own receivers)
    receiver_links : ndarray of size (num nodes, max neighbors at node)
        ID of links that leads from each node to its receiver, or
        UNDEFINED_INDEX if no flow occurs on this link.
    steepest_link : ndarray
        For each node, the link ID of the steepest link.
        BAD_INDEX_VALUE is given if no flow emmanates from the node.

    Examples
    --------
    >>> from landlab import RasterModelGrid
    >>> import numpy as np
    >>> from landlab.components.flow_director.flow_direction_mfd import(
    ...                                           flow_directions_mfd)
    >>> grid = RasterModelGrid((3,3), xy_spacing=(1, 1))
    >>> elev = grid.add_field('topographic__elevation', grid.node_x+grid.node_y, at = 'node')

    For the first example, we will not pass any diagonal elements to the flow
    direction algorithm.

    >>> neighbors_at_node = grid.adjacent_nodes_at_node
    >>> links_at_node = grid.links_at_node
    >>> active_link_dir_at_node = grid.active_link_dirs_at_node
    >>> link_slope = np.arctan(grid.calc_grad_at_link(elev))
    >>> slopes_to_neighbors_at_node = link_slope[links_at_node]*active_link_dir_at_node
    >>> (receivers,
    ... proportions,
    ... slopes,
    ... steepest_slope,
    ... steepest_receiver,
    ... sink,
    ... receiver_links,
    ... steepest_link)= flow_directions_mfd(elev,
    ...                                     neighbors_at_node,
    ...                                     links_at_node,
    ...                                     active_link_dir_at_node,
    ...                                     link_slope,
    ...                                     baselevel_nodes=None,
    ...                                     partition_method='slope')
    >>> receivers
    array([[ 0, -1, -1, -1],
           [ 1, -1, -1, -1],
           [ 2, -1, -1, -1],
           [ 3, -1, -1, -1],
           [-1, -1,  3,  1],
           [-1, -1,  4, -1],
           [ 6, -1, -1, -1],
           [-1, -1, -1,  4],
           [ 8, -1, -1, -1]])
    >>> proportions
    array([[ 1. ,  0. ,  0. ,  0. ],
           [ 1. ,  0. ,  0. ,  0. ],
           [ 1. ,  0. ,  0. ,  0. ],
           [ 1. ,  0. ,  0. ,  0. ],
           [ 0. ,  0. ,  0.5,  0.5],
           [ 0. ,  0. ,  1. ,  0. ],
           [ 1. ,  0. ,  0. ,  0. ],
           [ 0. ,  0. ,  0. ,  1. ],
           [ 1. ,  0. ,  0. ,  0. ]])
    >>> proportions.sum(axis=-1)
    array([ 1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.])

    In the second example, we will pass diagonal elements to the flow direction
    algorithm.

    >>> dal = grid.active_d8
    >>> neighbors_at_node = np.hstack((grid.adjacent_nodes_at_node,
    ...                                grid.diagonal_adjacent_nodes_at_node))
    >>> links_at_node = grid.d8s_at_node
    >>> active_link_dir_at_node = grid.active_d8_dirs_at_node

    We need to create a list of diagonal links since it doesn't exist.

    >>> diag_links = np.sort(np.unique(grid.d8s_at_node[:, 4:]))
    >>> diag_links = diag_links[diag_links>0]
    >>> diag_grads = np.zeros(diag_links.shape)
    >>> where_active_diag = dal>=diag_links.min()
    >>> active_diags_inds = dal[where_active_diag]-diag_links.min()
    >>> active_diag_grads = grid._calculate_gradients_at_d8_active_links(elev)
    >>> diag_grads[active_diags_inds] = active_diag_grads[where_active_diag]
    >>> ortho_grads = grid.calc_grad_at_link(elev)
    >>> link_slope = np.hstack((np.arctan(ortho_grads),
    ...                         np.arctan(diag_grads)))
    >>> (receivers,
    ... proportions,
    ... slopes,
    ... steepest_slope,
    ... steepest_receiver,
    ... sink,
    ... receiver_links,
    ... steepest_link)= flow_directions_mfd(elev,
    ...                                     neighbors_at_node,
    ...                                     links_at_node,
    ...                                     active_link_dir_at_node,
    ...                                     link_slope,
    ...                                     baselevel_nodes=None,
    ...                                     partition_method='slope')
    >>> receivers
    array([[ 0, -1, -1, -1, -1, -1, -1, -1],
           [ 1, -1, -1, -1, -1, -1, -1, -1],
           [ 2, -1, -1, -1, -1, -1, -1, -1],
           [ 3, -1, -1, -1, -1, -1, -1, -1],
           [-1, -1,  3,  1, -1, -1,  0, -1],
           [-1, -1,  4, -1, -1, -1, -1, -1],
           [ 6, -1, -1, -1, -1, -1, -1, -1],
           [-1, -1, -1,  4, -1, -1, -1, -1],
           [-1, -1, -1, -1, -1, -1,  4, -1]])
    >>> proportions
    array([[ 1.        ,  0.        ,  0.        ,  0.        ,  0.        ,
             0.        ,  0.        ,  0.        ],
           [ 1.        ,  0.        ,  0.        ,  0.        ,  0.        ,
             0.        ,  0.        ,  0.        ],
           [ 1.        ,  0.        ,  0.        ,  0.        ,  0.        ,
             0.        ,  0.        ,  0.        ],
           [ 1.        ,  0.        ,  0.        ,  0.        ,  0.        ,
             0.        ,  0.        ,  0.        ],
           [ 0.        ,  0.        ,  0.31091174,  0.31091174,  0.        ,
             0.        ,  0.37817653,  0.        ],
           [ 0.        ,  0.        ,  1.        ,  0.        ,  0.        ,
             0.        ,  0.        ,  0.        ],
           [ 1.        ,  0.        ,  0.        ,  0.        ,  0.        ,
             0.        ,  0.        ,  0.        ],
           [ 0.        ,  0.        ,  0.        ,  1.        ,  0.        ,
             0.        ,  0.        ,  0.        ],
           [ 0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
             0.        ,  1.        ,  0.        ]])
    >>> slopes
    array([[ 0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
             0.        ,  0.        ,  0.        ],
           [ 0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
             0.        ,  0.        ,  0.        ],
           [ 0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
             0.        ,  0.        ,  0.        ],
           [ 0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
             0.        ,  0.        ,  0.        ],
           [ 0.        ,  0.        ,  0.78539816,  0.78539816,  0.        ,
             0.        ,  0.95531662,  0.        ],
           [ 0.        ,  0.        ,  0.78539816,  0.        ,  0.        ,
             0.        ,  0.        ,  0.        ],
           [ 0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
             0.        ,  0.        ,  0.        ],
           [ 0.        ,  0.        ,  0.        ,  0.78539816,  0.        ,
             0.        ,  0.        ,  0.        ],
           [ 0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
             0.        ,  0.95531662,  0.        ]])
    >>> proportions.sum(axis=-1)
    array([ 1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.])
    """
    # Calculate the number of nodes.
    num_nodes = len(elev)

    # Create a node array
    node_id = np.arange(num_nodes)

    # Calculate the maximum number of neighbors at node.
    max_number_of_neighbors = neighbors_at_node.shape[1]

    # Make a copy of neighbors_at_node so we can change it into the receiver
    # array.
    receivers = neighbors_at_node.copy()

    # Construct the array of slope to neighbors at node. This also will adjust
    # for the slope convention based on the direction of the link.
    slopes_to_neighbors_at_node = link_slope[
        links_at_node] * active_link_dir_at_node

    # Make a copy so this can be changed based on where no flow occurs.
    receiver_links = links_at_node.copy()

    # some of these potential recievers may have already been assigned as
    # UNDEFINED_INDEX because the link was inactive. Make a mask of these for
    # future use. Also find the close nodes.
    inactive_link_to_neighbor = active_link_dir_at_node == 0
    closed_nodes = np.sum(np.abs(active_link_dir_at_node), 1) == 0
    # Now calculate where flow occurs.
    # First, make an elevation array of potential receivers.
    potential_receiver_elev = elev[neighbors_at_node]

    # now make an array of the same shape (for direct comparison) of the source
    # node elevation.
    source_node_elev = elev[np.tile(node_id, (max_number_of_neighbors, 1)).T]

    # find where flow does not occur (source is lower that receiver)
    flow_does_not_occur = source_node_elev <= potential_receiver_elev

    # Where the source is lower, set receivers to UNDEFINED_INDEX
    receivers[flow_does_not_occur] = UNDEFINED_INDEX

    # Where the link is not active, set receivers to UNDEFINED_INDEX
    receivers[inactive_link_to_neighbor] = UNDEFINED_INDEX

    # Next, find where a node drains to itself
    drains_to_self = receivers.sum(1) == -1 * max_number_of_neighbors

    # Where this occurs, set the receiver ID in the first column of receivers
    # to the node ID.
    receivers[drains_to_self, 0] = node_id[drains_to_self]

    # Finally, set the first element of the closed nodes to themselves.
    receivers[closed_nodes, 0] = node_id[closed_nodes]

    # Next, calculate flow proportions.
    # Copy slope array and mask by where flow is not occuring and where the
    # link is inactive.
    flow_slopes = slopes_to_neighbors_at_node.copy()
    flow_slopes[flow_does_not_occur] = 0.0
    flow_slopes[inactive_link_to_neighbor] = 0.0

    if partition_method == "square_root_of_slope":
        values_for_partitioning = flow_slopes**0.5
    elif partition_method == "slope":
        values_for_partitioning = flow_slopes
    else:
        raise ValueError("Keyword argument to partition_method invalid.")

    # Calculate proportions by normalizing by rowsums.
    denom = np.tile(values_for_partitioning.sum(1),
                    (max_number_of_neighbors, 1)).T
    denom[denom <= 0] = 1  # to prevent runtime errors
    proportions = values_for_partitioning / denom
    proportions[drains_to_self, 0] = 1
    proportions[drains_to_self, 1:] = 0

    # Might need to sort by proportions and rearrange to follow expectations
    # of no UNDEFINED_INDEX value in first column. KRB NOT SURE

    # mask the receiver_links by where flow doesn't occur to return
    receiver_links[flow_does_not_occur] = UNDEFINED_INDEX
    receiver_links[inactive_link_to_neighbor] = UNDEFINED_INDEX

    # identify the steepest link so that the steepest receiver, link, and slope
    # can be returned.
    slope_sort = np.argsort(np.argsort(flow_slopes, axis=1),
                            axis=1) == (max_number_of_neighbors - 1)
    steepest_slope = flow_slopes[slope_sort]

    # identify the steepest link and steepest receiever.
    steepest_link = receiver_links[slope_sort]
    steepest_receiver = receivers[slope_sort]
    steepest_receiver[drains_to_self] = node_id[drains_to_self]

    # Optionally, handle baselevel nodes: they are their own receivers
    if baselevel_nodes is not None:
        receivers[baselevel_nodes, 0] = node_id[baselevel_nodes]
        receivers[baselevel_nodes, 1:] = -1
        proportions[baselevel_nodes, 0] = 1
        proportions[baselevel_nodes, 1:] = 0
        receiver_links[baselevel_nodes, :] = UNDEFINED_INDEX
        steepest_slope[baselevel_nodes] = 0.0

    # The sink nodes are those that are their own receivers (this will normally
    # include boundary nodes as well as interior ones; "pits" would be sink
    # nodes that are also interior nodes).
    (sink, ) = np.where(node_id == receivers[:, 0])
    sink = as_id_array(sink)

    slopes_to_neighbors_at_node[flow_does_not_occur] = 0
    slopes_to_neighbors_at_node[inactive_link_to_neighbor] = 0

    return (
        receivers,
        proportions,
        slopes_to_neighbors_at_node,
        steepest_slope,
        steepest_receiver,
        sink,
        receiver_links,
        steepest_link,
    )
Ejemplo n.º 14
0
def flow_directions_dinf(grid,
                         elevs="topographic__elevation",
                         baselevel_nodes=None):
    """
    Find Dinfinity flow directions and proportions on a raster grid.

    Finds and returns flow directions and proportions for a given elevation
    grid 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.

    This method does not support irregular grids.

    Parameters
    ----------
    grid : ModelGrid
        A grid of type Voroni.
    elevs : field name at node or array of length node
        The surface to direct flow across.
    baselevel_nodes : array_like, optional
        IDs of open boundary (baselevel) nodes.

    Returns
    -------
    receivers : ndarray of size (num nodes, max neighbors at node)
        For each node, the IDs of the nodes that receive its flow. For nodes
        that do not direct flow to all neighbors, BAD_INDEX_VALUE is given as
        a placeholder. The ID of the node itself is given if no other receiver
        is assigned.
    proportions : ndarray of size (num nodes, max neighbors at node)
        For each receiver, the proportion of flow (between 0 and 1) is given.
        A proportion of zero indicates that the link does not have flow along
        it.
    slopes: ndarray of size (num nodes, max neighbors at node)
        For each node in the array ``recievers``, the slope value (positive
        downhill) in the direction of flow. If no flow occurs (value of
        ``recievers`` is -1), then this array is set to 0.
    steepest_slope : ndarray
        The slope value (positive downhill) in the direction of flow.
    steepest_receiver : ndarray
        For each node, the node ID of the node connected by the steepest link.
        BAD_INDEX_VALUE is given if no flow emmanates from the node.
    sink : ndarray
        IDs of nodes that are flow sinks (they are their own receivers)
    receiver_links : ndarray of size (num nodes, max neighbors at node)
        ID of links that leads from each node to its receiver, or
        UNDEFINED_INDEX if no flow occurs on this link.
    steepest_link : ndarray
        For each node, the link ID of the steepest link.
        BAD_INDEX_VALUE is given if no flow emmanates from the node.

    Examples
    --------
    >>> from landlab import RasterModelGrid
    >>> from landlab.components.flow_director.flow_direction_dinf import(
    ...                                                  flow_directions_dinf)

    Dinfinity routes flow based on the relative proportion of flow along the
    triangular facets around a central raster node.

    >>> grid = RasterModelGrid((3,3), spacing=(1, 1))
    >>> _ = grid.add_field('topographic__elevation',
    ...                     2.*grid.node_x+grid.node_y,
    ...                     at = 'node')
    >>> (receivers, proportions, slopes,
    ... steepest_slope, steepest_receiver,
    ... sink, receiver_links, steepest_link) = flow_directions_dinf(grid)
    >>> receivers
    array([[ 0, -1],
           [ 0, -1],
           [ 1, -1],
           [ 0, -1],
           [ 3,  0],
           [ 4,  1],
           [ 3, -1],
           [ 6,  3],
           [ 7,  4]])
    >>> proportions
    array([[ 1.        ,  0.        ],
           [ 1.        , -0.        ],
           [ 1.        , -0.        ],
           [ 1.        ,  0.        ],
           [ 0.40966553,  0.59033447],
           [ 0.40966553,  0.59033447],
           [ 1.        ,  0.        ],
           [ 0.40966553,  0.59033447],
           [ 0.40966553,  0.59033447]])

    This method also works if the elevations are passed as an array instead of
    the (implied) field name 'topographic__elevation'.

    >>> z = grid['node']['topographic__elevation']
    >>> (receivers, proportions, slopes,
    ... steepest_slope, steepest_receiver,
    ... sink, receiver_links, steepest_link) = flow_directions_dinf(grid, z)
    >>> receivers
    array([[ 0, -1],
           [ 0, -1],
           [ 1, -1],
           [ 0, -1],
           [ 3,  0],
           [ 4,  1],
           [ 3, -1],
           [ 6,  3],
           [ 7,  4]])
    >>> slopes
    array([[-1.        , -2.12132034],
           [ 2.        ,  0.70710678],
           [ 2.        ,  0.70710678],
           [ 1.        , -0.70710678],
           [ 2.        ,  2.12132034],
           [ 2.        ,  2.12132034],
           [ 1.        , -0.70710678],
           [ 2.        ,  2.12132034],
           [ 2.        ,  2.12132034]])
    >>> proportions
    array([[ 1.        ,  0.        ],
           [ 1.        , -0.        ],
           [ 1.        , -0.        ],
           [ 1.        ,  0.        ],
           [ 0.40966553,  0.59033447],
           [ 0.40966553,  0.59033447],
           [ 1.        ,  0.        ],
           [ 0.40966553,  0.59033447],
           [ 0.40966553,  0.59033447]])
    """
    # grid type testing
    if isinstance(grid, VoronoiDelaunayGrid):
        raise NotImplementedError("Dinfinity is currently implemented for"
                                  " Raster grids only")
    # get elevs
    elevs = return_array_at_node(grid, elevs)

    # find where there are closed nodes.
    closed_nodes = grid.status_at_node == CLOSED_BOUNDARY

    closed_elevation = np.max(elevs[closed_nodes == False]) + 1000

    elevs[closed_nodes] = closed_elevation

    ### Step 1, some basic set-up, gathering information about the grid.

    # Calculate the number of nodes.
    num_nodes = len(elevs)

    # Set the number of receivers and facets.
    num_receivers = 2
    num_facets = 8

    # Create a node array
    node_id = np.arange(num_nodes)

    # create an array of the triangle numbers
    tri_numbers = np.arange(num_facets)

    ### Step 3, create some triangle datastructures because landlab (smartly)
    # makes it hard to deal with diagonals.

    # create list of triangle neighbors at node. Use orientation associated
    # with tarboton's 1997 algorithm, orthogonal link first, then diagonal.
    # has shape, (nnodes, 8 triangles, 2 neighbors)
    n_at_node = grid.adjacent_nodes_at_node
    dn_at_node = grid.diagonal_adjacent_nodes_at_node

    triangle_neighbors_at_node = np.stack(
        [
            np.vstack((n_at_node[:, 0], dn_at_node[:, 0])),
            np.vstack((n_at_node[:, 1], dn_at_node[:, 0])),
            np.vstack((n_at_node[:, 1], dn_at_node[:, 1])),
            np.vstack((n_at_node[:, 2], dn_at_node[:, 1])),
            np.vstack((n_at_node[:, 2], dn_at_node[:, 2])),
            np.vstack((n_at_node[:, 3], dn_at_node[:, 2])),
            np.vstack((n_at_node[:, 3], dn_at_node[:, 3])),
            np.vstack((n_at_node[:, 0], dn_at_node[:, 3])),
        ],
        axis=-1,
    )
    triangle_neighbors_at_node = triangle_neighbors_at_node.swapaxes(0, 1)

    # next create, triangle links at node
    l_at_node = grid.d8s_at_node[:, :4]
    dl_at_node = grid.d8s_at_node[:, 4:]
    triangle_links_at_node = np.stack(
        [
            np.vstack((l_at_node[:, 0], dl_at_node[:, 0])),
            np.vstack((l_at_node[:, 1], dl_at_node[:, 0])),
            np.vstack((l_at_node[:, 1], dl_at_node[:, 1])),
            np.vstack((l_at_node[:, 2], dl_at_node[:, 1])),
            np.vstack((l_at_node[:, 2], dl_at_node[:, 2])),
            np.vstack((l_at_node[:, 3], dl_at_node[:, 2])),
            np.vstack((l_at_node[:, 3], dl_at_node[:, 3])),
            np.vstack((l_at_node[:, 0], dl_at_node[:, 3])),
        ],
        axis=-1,
    )
    triangle_links_at_node = triangle_links_at_node.swapaxes(0, 1)

    # next create link directions and active link directions at node
    # link directions
    ld_at_node = grid.link_dirs_at_node
    dld_at_node = grid.diagonal_dirs_at_node
    triangle_link_dirs_at_node = np.stack(
        [
            np.vstack((ld_at_node[:, 0], dld_at_node[:, 0])),
            np.vstack((ld_at_node[:, 1], dld_at_node[:, 0])),
            np.vstack((ld_at_node[:, 1], dld_at_node[:, 1])),
            np.vstack((ld_at_node[:, 2], dld_at_node[:, 1])),
            np.vstack((ld_at_node[:, 2], dld_at_node[:, 2])),
            np.vstack((ld_at_node[:, 3], dld_at_node[:, 2])),
            np.vstack((ld_at_node[:, 3], dld_at_node[:, 3])),
            np.vstack((ld_at_node[:, 0], dld_at_node[:, 3])),
        ],
        axis=-1,
    )
    triangle_link_dirs_at_node = triangle_link_dirs_at_node.swapaxes(0, 1)

    # need to create a list of diagonal links since it doesn't exist.
    diag_links = np.sort(np.unique(grid.d8s_at_node[:, 4:]))
    diag_links = diag_links[diag_links > 0]

    # calculate graidents across diagonals and orthogonals
    diag_grads = grid._calculate_gradients_at_d8_links(elevs)
    ortho_grads = grid.calc_grad_at_link(elevs)

    # finally compile link slopes
    link_slope = np.hstack((ortho_grads, diag_grads))

    # Construct the array of slope to triangles at node. This also will adjust
    # for the slope convention based on the direction of the links.
    # this is a (nnodes, 2, 8) array
    slopes_to_triangles_at_node = (link_slope[triangle_links_at_node] *
                                   triangle_link_dirs_at_node)

    #### Step 3: make arrays necessary for the specific tarboton algorithm.
    # create a arrays
    ac = np.array([0., 1., 1., 2., 2., 3., 3., 4.])
    af = np.array([1., -1., 1., -1., 1., -1., 1., -1.])

    # construct d1 and d2, we know these because we know where the orthogonal
    # links are
    diag_length = ((grid.dx)**2 + (grid.dy)**2)**0.5

    # for irregular grids, d1 and d2 will need to be matricies
    d1 = np.array([
        grid.dx, grid.dy, grid.dy, grid.dx, grid.dx, grid.dy, grid.dy, grid.dy
    ])
    d2 = np.array([
        grid.dx, grid.dx, grid.dy, grid.dy, grid.dx, grid.dx, grid.dy, grid.dy
    ])

    thresh = np.arctan(d2 / d1)

    ##### Step 4, Initialize receiver and proportion arrays
    receivers = UNDEFINED_INDEX * np.ones(
        (num_nodes, num_receivers), dtype=int)
    receiver_closed = UNDEFINED_INDEX * np.ones(
        (num_nodes, num_receivers), dtype=int)
    proportions = np.zeros((num_nodes, num_receivers), dtype=float)
    receiver_links = UNDEFINED_INDEX * np.ones(
        (num_nodes, num_receivers), dtype=int)
    slopes_to_receivers = np.zeros((num_nodes, num_receivers), dtype=float)

    #### Step  5  begin the algorithm in earnest

    # construct e0, e1, e2 for all triangles at all nodes.
    # will be (nnodes, nfacets=8 for raster or nfacets = max number of patches
    # for irregular grids.

    # e0 is origin point of the facet
    e0 = elevs[node_id]

    # e1 is the point on the orthogoanal edges
    e1 = elevs[triangle_neighbors_at_node[:, 0, :]]
    # e2 is the point on the diagonal edges
    e2 = elevs[triangle_neighbors_at_node[:, 1, :]]

    # modification of original algorithm to address Landlab boundary conditions.
    # first,
    # for e1 and e2, mask out where nodes do not exits (e.g.
    # triangle_neighbors_at_node == -1)
    e1[triangle_neighbors_at_node[:, 0, :] == -1] = np.nan
    e2[triangle_neighbors_at_node[:, 1, :] == -1] = np.nan

    # loop through and calculate s1 and s2
    # this will only loop nfacets times.
    s1 = np.empty_like(e1)
    s2 = np.empty_like(e2)

    for i in range(num_facets):
        s1[:, i] = (e0 - e1[:, i]) / d1[i]
        s2[:, i] = (e1[:, i] - e2[:, i]) / d2[i]

    # calculate r and s, the direction and magnitude
    r = np.arctan2(s2, s1)
    s = ((s1**2) + (s2**2))**0.5

    r[np.isnan(r)] = 0
    # adjust r if it doesn't sit in the realm of (0, arctan(d2,d1))
    too_small = r < 0
    radj = r.copy()
    radj[too_small] = 0
    s[too_small] = s1[too_small]

    # to consider two big, we need to look by triangle.
    for i in range(num_facets):
        too_big = r[:, i] > thresh[i]
        radj[too_big, i] = thresh[i]
        s[too_big, i] = (e0[too_big] - e2[too_big, i]) / diag_length

    # calculate the geospatial version of r based on radj
    rg = np.empty_like(r)
    for i in range(num_facets):
        rg[:, i] = (af[i] * radj[:, i]) + (ac[i] * np.pi / 2.)

    # set slopes that are nan to below zero
    # if there is a flat slope, it should be chosen over the closed or non-existant
    # triangles that are represented by the nan values.
    s[np.isnan(s)] = -999.0

    # sort slopes
    # we've set slopes going to closed or non-existant triangles to -999.0, so
    # we shouldn't ever choose these.
    steepest_sort = np.argsort(s)

    # determine the steepest triangle
    steepest_triangle = tri_numbers[steepest_sort[:, -1]]

    # initialize arrays for the steepest rg and steepest s
    steepest_rg = np.empty_like(node_id, dtype=float)
    steepest_s = np.empty_like(node_id, dtype=float)

    closed_triangle_neighbors = closed_nodes[triangle_neighbors_at_node]
    for n in node_id:
        steepest_rg[n] = rg[n, steepest_sort[n, -1]]
        receiver_closed[n] = closed_triangle_neighbors[n, :, steepest_sort[n,
                                                                           -1]]
        steepest_s[n] = s[n, steepest_sort[n, -1]]
        receivers[n, :] = triangle_neighbors_at_node[n, :, steepest_sort[n,
                                                                         -1]]
        receiver_links[n, :] = triangle_links_at_node[n, :, steepest_sort[n,
                                                                          -1]]
        slopes_to_receivers[n, :] = slopes_to_triangles_at_node[
            n, :, steepest_sort[n, -1]]

    # construct the baseline for proportions
    rg_baseline = np.array([0., 1., 1., 2., 2., 3., 3., 4]) * np.pi / 2.

    # calculate alpha1 and alpha 2
    alpha2 = (steepest_rg -
              rg_baseline[steepest_triangle]) * af[steepest_triangle]
    alpha1 = thresh[steepest_triangle] - alpha2

    # calculate proportions from alpha
    proportions[:, 0] = (alpha1) / (alpha1 + alpha2)
    proportions[:, 1] = (alpha2) / (alpha1 + alpha2)

    # where proportions == 0, set reciever  to -1
    receivers[proportions == 0] = -1

    ### END OF THE Tarboton algorithm, start of work to make this code mesh
    # with other landlab flow directing algorithms.

    # identify what drains to itself, and set proportion and id values based on
    # that.

    # if proportions is nan, drain to self
    drains_to_self = np.isnan(proportions[:, 0])

    # if all slopes are leading out or flat, drain to self
    drains_to_self[steepest_s <= 0] = True

    # if both receiver nodes are closed, drain to self
    drains_to_two_closed = receiver_closed.sum(axis=1) == num_receivers
    drains_to_self[drains_to_two_closed] = True

    # if drains to one closed receiver, check that the open receiver actually
    # gets flow. If so, route all to the open receiver. If the receiver getting
    # all the flow is closed, then drain to self.
    all_flow_to_closed = np.sum(receiver_closed * proportions, axis=1) == 1
    drains_to_self[all_flow_to_closed] = True

    drains_to_one_closed = receiver_closed.sum(axis=1) == 1
    fix_flow = drains_to_one_closed * (all_flow_to_closed == False)
    first_column_has_closed = np.array(receiver_closed[:, 0] * fix_flow,
                                       dtype=bool)
    second_column_has_closed = np.array(receiver_closed[:, 1] * fix_flow,
                                        dtype=bool)

    # remove the link to the closed node
    receivers[first_column_has_closed, 0] = -1
    receivers[second_column_has_closed, 1] = -1

    # change the proportions
    proportions[first_column_has_closed, 0] = 0.
    proportions[first_column_has_closed, 1] = 1.

    proportions[second_column_has_closed, 0] = 1.
    proportions[second_column_has_closed, 1] = 0.

    # set properties of drains to self.
    receivers[drains_to_self, 0] = node_id[drains_to_self]
    receivers[drains_to_self, 1] = -1

    proportions[drains_to_self, 0] = 1.
    proportions[drains_to_self, 1] = 0.

    # set properties of closed
    receivers[closed_nodes, 0] = node_id[closed_nodes]
    receivers[closed_nodes, 1] = -1
    proportions[closed_nodes, 0] = 1.
    proportions[closed_nodes, 1] = 0.

    # mask the receiver_links by where flow doesn't occur to return
    receiver_links[drains_to_self, :] = UNDEFINED_INDEX

    # identify the steepest link so that the steepest receiver, link, and slope
    # can be returned.
    slope_sort = np.argsort(np.argsort(slopes_to_receivers, axis=1),
                            axis=1) == (num_receivers - 1)
    steepest_slope = slopes_to_receivers[slope_sort]
    steepest_slope[drains_to_self] = 0.

    ## identify the steepest link and steepest receiever.
    steepest_link = receiver_links[slope_sort]
    steepest_link[drains_to_self] = UNDEFINED_INDEX

    steepest_receiver = receivers[slope_sort]
    steepest_receiver[drains_to_self] = node_id[drains_to_self]

    # Optionally, handle baselevel nodes: they are their own receivers
    if baselevel_nodes is not None:
        receivers[baselevel_nodes, 0] = node_id[baselevel_nodes]
        receivers[baselevel_nodes, 1:] = -1
        proportions[baselevel_nodes, 0] = 1
        proportions[baselevel_nodes, 1:] = 0
        receiver_links[baselevel_nodes, :] = UNDEFINED_INDEX
        steepest_slope[baselevel_nodes] = 0.

    # ensure that if there is a -1, it is in the second column.
    order_reversed = receivers[:, 0] == -1

    receivers_out = receivers.copy()
    receivers_out[order_reversed, 1] = receivers[order_reversed, 0]
    receivers_out[order_reversed, 0] = receivers[order_reversed, 1]

    proportions_out = proportions.copy()
    proportions_out[order_reversed, 1] = proportions[order_reversed, 0]
    proportions_out[order_reversed, 0] = proportions[order_reversed, 1]

    slopes_to_receivers_out = slopes_to_receivers.copy()
    slopes_to_receivers_out[order_reversed,
                            1] = slopes_to_receivers[order_reversed, 0]
    slopes_to_receivers_out[order_reversed,
                            0] = slopes_to_receivers[order_reversed, 1]

    receiver_links_out = receiver_links.copy()
    receiver_links_out[order_reversed, 1] = receiver_links[order_reversed, 0]
    receiver_links_out[order_reversed, 0] = receiver_links[order_reversed, 1]

    # The sink nodes are those that are their own receivers (this will normally
    # include boundary nodes as well as interior ones; "pits" would be sink
    # nodes that are also interior nodes).
    (sink, ) = np.where(node_id == receivers[:, 0])
    sink = as_id_array(sink)

    return (
        receivers_out,
        proportions_out,
        slopes_to_receivers_out,
        steepest_slope,
        steepest_receiver,
        sink,
        receiver_links_out,
        steepest_link,
    )
Ejemplo n.º 15
0
def flow_directions(elev,
                    active_links,
                    tail_node,
                    head_node,
                    link_slope,
                    grid=None,
                    baselevel_nodes=None):
    """
    Find flow directions on a grid.

    Finds and returns flow directions for a given elevation grid. Each node is
    assigned a single direction, toward one of its N neighboring nodes (or
    itself, if none of its neighbors are lower).

    Parameters
    ----------
    elev : array_like
        Elevations at nodes.
    active_links : array_like
        IDs of active links.
    tail_node : array_like
        IDs of the tail node for each link.
    head_node : array_like
        IDs of the head node for each link.
    link_slope : array_like
        slope of each link, defined POSITIVE DOWNHILL (i.e., a negative value
        means the link runs uphill from the fromnode to the tonode).
    baselevel_nodes : array_like, optional
        IDs of open boundary (baselevel) nodes.

    Returns
    -------
    receiver : ndarray
        For each node, the ID of the node that receives its flow. Defaults to
        the node itself if no other receiver is assigned.
    steepest_slope : ndarray
        The slope value (positive downhill) in the direction of flow
    sink : ndarray
        IDs of nodes that are flow sinks (they are their own receivers)
    receiver_link : ndarray
        ID of link that leads from each node to its receiver, or
        UNDEFINED_INDEX if none.

    Examples
    --------
    The example below assigns elevations to the 10-node example network in
    Braun and Willett (2012), so that their original flow pattern should be
    re-created.

    >>> import numpy as np
    >>> from landlab.components.flow_routing import flow_directions
    >>> z = np.array([2.4, 1.0, 2.2, 3.0, 0.0, 1.1, 2.0, 2.3, 3.1, 3.2])
    >>> fn = np.array([1,4,4,0,1,2,5,1,5,6,7,7,8,6,3,3,2,0])
    >>> tn = np.array([4,5,7,1,2,5,6,5,7,7,8,9,9,8,8,6,3,3])
    >>> s = z[fn] - z[tn]  # slope with unit link length, positive downhill
    >>> active_links = np.arange(len(fn))
    >>> r, ss, snk, rl = flow_directions(z, active_links, fn, tn, s)
    >>> r
    array([1, 4, 1, 6, 4, 4, 5, 4, 6, 7])
    >>> ss
    array([ 1.4,  1. ,  1.2,  1. ,  0. ,  1.1,  0.9,  2.3,  1.1,  0.9])
    >>> snk
    array([4])
    >>> rl[3:8]
    array([15, -1,  1,  6,  2])

    OK, the following are rough notes on design: we want to work with just the
    active links. Ways to do this:
    *  Pass active_links in as argument
    *  In calling code, only refer to receiver_links for active nodes
    """
    # Setup
    num_nodes = len(elev)
    steepest_slope = np.zeros(num_nodes)
    receiver = np.arange(num_nodes)
    receiver_link = UNDEFINED_INDEX + np.zeros(num_nodes, dtype=np.int)

    # For each link, find the higher of the two nodes. The higher is the
    # potential donor, and the lower is the potential receiver. If the slope
    # from donor to receiver is steeper than the steepest one found so far for
    # the donor, then assign the receiver to the donor and record the new slope.
    # (Note the minus sign when looking at slope from "t" to "f").
    #
    # NOTE: MAKE SURE WE ARE ONLY LOOKING AT ACTIVE LINKS
    #THIS REMAINS A PROBLEM AS OF DEJH'S EFFORTS, MID MARCH 14.
    #overridden as part of fastscape_stream_power

    #DEJH attempting to replace the node-by-node loop, 5/28/14:
    #This is actually about the same speed on a 100*100 grid!
    #as of Dec 2014, we prioritise the weave if a weave is viable, and only do
    #the numpy methods if it's not (~10% speed gain on 100x100 grid;
    #presumably better if the grid is bigger)
    method = 'cython'
    if method == 'cython':
        from .cfuncs import adjust_flow_receivers

        adjust_flow_receivers(tail_node, head_node, elev, link_slope,
                              active_links, receiver, receiver_link,
                              steepest_slope)
    else:
        if grid == None or not RasterModelGrid in inspect.getmro(
                grid.__class__):
            for i in range(len(tail_node)):
                t = tail_node[i]
                h = head_node[i]
                if elev[t] > elev[h] and link_slope[i] > steepest_slope[t]:
                    receiver[t] = h
                    steepest_slope[t] = link_slope[i]
                    receiver_link[t] = active_links[i]
                elif elev[h] > elev[t] and -link_slope[i] > steepest_slope[h]:
                    receiver[h] = t
                    steepest_slope[h] = -link_slope[i]
                    receiver_link[h] = active_links[i]
        else:
            #alternative, assuming grid structure doesn't change between steps
            #global neighbor_nodes
            #global links_list #this is ugly. We need another way of saving that doesn't make these permanent (can't change grid size...)
            (non_boundary_nodes, ) = np.where(
                grid.node_status != CLOSED_BOUNDARY)
            try:
                elevs_array = np.where(neighbor_nodes != -1,
                                       elev[neighbor_nodes],
                                       np.finfo(float).max)
            except NameError:
                neighbor_nodes = np.empty((non_boundary_nodes.size, 8),
                                          dtype=int)
                #the target shape is (nnodes,4) & S,W,N,E,SW,NW,NE,SE
                neighbor_nodes[:, :4] = grid.get_neighbor_list(
                    bad_index=-1
                )[non_boundary_nodes, :][:, ::
                                         -1]  # comes as (nnodes, 4), and E,N,W,S
                neighbor_nodes[:, 4:] = grid._get_diagonal_list(
                    bad_index=-1)[non_boundary_nodes, :][:, [2, 1, 0,
                                                             3]]  #NE,NW,SW,SE
                links_list = np.empty_like(neighbor_nodes)
                links_list[:, :4] = grid.links_at_node[
                    non_boundary_nodes]  # Reorder as SWNE
                links_list[:, 4:6] = grid._diagonal_links_at_node[
                    non_boundary_nodes, 2:0:-1]
                links_list[:, 6] = grid._diagonal_links_at_node[
                    non_boundary_nodes, 0]
                links_list[:, 7] = grid._diagonal_links_at_node[
                    non_boundary_nodes, 3]  # final order SW,NW,NE,SE
                elevs_array = np.where(neighbor_nodes != -1,
                                       elev[neighbor_nodes],
                                       np.finfo(float).max / 1000.)
            slope_array = (elev[non_boundary_nodes].reshape(
                (non_boundary_nodes.size, 1)) - elevs_array
                           ) / grid._length_of_link_with_diagonals[links_list]
            axis_indices = np.argmax(slope_array, axis=1)
            steepest_slope[non_boundary_nodes] = slope_array[
                np.indices(axis_indices.shape), axis_indices]
            downslope = np.greater(steepest_slope, 0.)
            downslope_active = downslope[non_boundary_nodes]
            receiver[downslope] = neighbor_nodes[
                np.indices(axis_indices.shape), axis_indices][0,
                                                              downslope_active]
            receiver_link[downslope] = links_list[
                np.indices(axis_indices.shape), axis_indices][0,
                                                              downslope_active]

    node_id = np.arange(num_nodes)

    # Optionally, handle baselevel nodes: they are their own receivers
    if baselevel_nodes is not None:
        receiver[baselevel_nodes] = node_id[baselevel_nodes]
        receiver_link[baselevel_nodes] = UNDEFINED_INDEX
        steepest_slope[baselevel_nodes] = 0.

    # The sink nodes are those that are their own receivers (this will normally
    # include boundary nodes as well as interior ones; "pits" would be sink
    # nodes that are also interior nodes).
    (sink, ) = np.where(node_id == receiver)
    sink = as_id_array(sink)

    return receiver, steepest_slope, sink, receiver_link
Ejemplo n.º 16
0
    def accumulate_flow(self, update_flow_director=True):
        """Function to make FlowAccumulator calculate drainage area and
        discharge.

        Running run_one_step() results in the following to occur:
            1. Flow directions are updated (unless update_flow_director is set
            as False).
            2. Intermediate steps that analyse the drainage network topology
            and create datastructures for efficient drainage area and discharge
            calculations.
            3. Calculation of drainage area and discharge.
            4. Depression finding and mapping, which updates drainage area and
            discharge.
        """
        # set a couple of aliases
        a = self._grid["node"]["drainage_area"]
        q = self._grid["node"]["surface_water__discharge"]

        # step 1. Find flow directions by specified method
        if update_flow_director:
            self._flow_director.run_one_step()

        # further steps vary depending on how many recievers are present
        # one set of steps is for route to one (D8, Steepest/D4)

        # step 2. Get r
        r = as_id_array(self._grid["node"]["flow__receiver_node"])

        if self._flow_director._to_n_receivers == "one":

            # step 2b. Run depression finder if passed
            # Depression finder reaccumulates flow at the end of its routine.
            # At the moment, no depression finders work with to-many, so it
            # lives here
            if self._depression_finder_provided is not None:
                self._depression_finder.map_depressions()

                # if FlowDirectorSteepest is used, update the link directions
                if self._flow_director._name == "FlowDirectorSteepest":
                    self._flow_director._determine_link_directions()

            # step 3. Stack, D, delta construction
            nd = as_id_array(flow_accum_bw._make_number_of_donors_array(r))
            delta = as_id_array(flow_accum_bw._make_delta_array(nd))
            D = as_id_array(flow_accum_bw._make_array_of_donors(r, delta))
            s = as_id_array(flow_accum_bw.make_ordered_node_array(r))

            # put these in grid so that depression finder can use it.
            # store the generated data in the grid
            self._grid.at_node["flow__data_structure_delta"][:] = delta[1:]
            self._D_structure = D
            self._grid.at_node["flow__upstream_node_order"][:] = s

            # step 4. Accumulate (to one or to N depending on direction method)
            a[:], q[:] = self._accumulate_A_Q_to_one(s, r)

        else:
            # Get p
            p = self._grid["node"]["flow__receiver_proportions"]

            # step 3. Stack, D, delta construction
            nd = as_id_array(flow_accum_to_n._make_number_of_donors_array_to_n(r, p))
            delta = as_id_array(flow_accum_to_n._make_delta_array_to_n(nd))
            D = as_id_array(flow_accum_to_n._make_array_of_donors_to_n(r, p, delta))
            s = as_id_array(flow_accum_to_n.make_ordered_node_array_to_n(r, p))

            # put theese in grid so that depression finder can use it.
            # store the generated data in the grid
            self._grid["node"]["flow__data_structure_delta"][:] = delta[1:]
            self._D_structure = D

            self._grid["node"]["flow__upstream_node_order"][:] = s
            self._grid["node"]["flow__upstream_node_order"][:] = s

            # step 4. Accumulate (to one or to N depending on direction method)
            a[:], q[:] = self._accumulate_A_Q_to_n(s, r, p)

        return (a, q)
Ejemplo n.º 17
0
def flow_directions(elev, active_links, tail_node, head_node, link_slope,
                    grid=None, baselevel_nodes=None):
    """Find flow directions on a grid.

    Finds and returns flow directions for a given elevation grid. Each node is
    assigned a single direction, toward one of its N neighboring nodes (or
    itself, if none of its neighbors are lower).

    Parameters
    ----------
    elev : array_like
        Elevations at nodes.
    active_links : array_like
        IDs of active links.
    tail_node : array_like
        IDs of the tail node for each link.
    head_node : array_like
        IDs of the head node for each link.
    link_slope : array_like
        slope of each link, defined POSITIVE DOWNHILL (i.e., a negative value
        means the link runs uphill from the fromnode to the tonode).
    baselevel_nodes : array_like, optional
        IDs of open boundary (baselevel) nodes.

    Returns
    -------
    receiver : ndarray
        For each node, the ID of the node that receives its flow. Defaults to
        the node itself if no other receiver is assigned.
    steepest_slope : ndarray
        The slope value (positive downhill) in the direction of flow
    sink : ndarray
        IDs of nodes that are flow sinks (they are their own receivers)
    receiver_link : ndarray
        ID of link that leads from each node to its receiver, or
        UNDEFINED_INDEX if none.

    Examples
    --------
    The example below assigns elevations to the 10-node example network in
    Braun and Willett (2012), so that their original flow pattern should be
    re-created.

    >>> import numpy as np
    >>> from landlab.components.flow_routing import flow_directions
    >>> z = np.array([2.4, 1.0, 2.2, 3.0, 0.0, 1.1, 2.0, 2.3, 3.1, 3.2])
    >>> fn = np.array([1,4,4,0,1,2,5,1,5,6,7,7,8,6,3,3,2,0])
    >>> tn = np.array([4,5,7,1,2,5,6,5,7,7,8,9,9,8,8,6,3,3])
    >>> s = z[fn] - z[tn]  # slope with unit link length, positive downhill
    >>> active_links = np.arange(len(fn))
    >>> r, ss, snk, rl = flow_directions(z, active_links, fn, tn, s)
    >>> r
    array([1, 4, 1, 6, 4, 4, 5, 4, 6, 7])
    >>> ss
    array([ 1.4,  1. ,  1.2,  1. ,  0. ,  1.1,  0.9,  2.3,  1.1,  0.9])
    >>> snk
    array([4])
    >>> rl[3:8]
    array([15, -1,  1,  6,  2])
    """
    # OK, the following are rough notes on design: we want to work with just
    # the active links. Ways to do this:
    # *  Pass active_links in as argument
    # *  In calling code, only refer to receiver_links for active nodes

    # Setup
    num_nodes = len(elev)
    steepest_slope = np.zeros(num_nodes)
    receiver = np.arange(num_nodes)
    receiver_link = UNDEFINED_INDEX + np.zeros(num_nodes, dtype=np.int)

    # For each link, find the higher of the two nodes. The higher is the
    # potential donor, and the lower is the potential receiver. If the slope
    # from donor to receiver is steeper than the steepest one found so far for
    # the donor, then assign the receiver to the donor and record the new slope.
    # (Note the minus sign when looking at slope from "t" to "f").
    #
    # NOTE: MAKE SURE WE ARE ONLY LOOKING AT ACTIVE LINKS
    #THIS REMAINS A PROBLEM AS OF DEJH'S EFFORTS, MID MARCH 14.
    #overridden as part of fastscape_stream_power

    adjust_flow_receivers(tail_node, head_node, elev, link_slope,
                          active_links, receiver, receiver_link,
                          steepest_slope)

    node_id = np.arange(num_nodes)

    # Optionally, handle baselevel nodes: they are their own receivers
    if baselevel_nodes is not None:
        receiver[baselevel_nodes] = node_id[baselevel_nodes]
        receiver_link[baselevel_nodes] = UNDEFINED_INDEX
        steepest_slope[baselevel_nodes] = 0.

    # The sink nodes are those that are their own receivers (this will normally
    # include boundary nodes as well as interior ones; "pits" would be sink
    # nodes that are also interior nodes).
    (sink, ) = np.where(node_id==receiver)
    sink = as_id_array(sink)

    return receiver, steepest_slope, sink, receiver_link
Ejemplo n.º 18
0
    def map_depressions(self, pits='flow__sink_flag', reroute_flow=True):
        """Map depressions/lakes in a topographic surface.

        Parameters
        ----------
        pits : array or str or None, optional
            If a field name, the boolean field containing True where pits.
            If an array, either a boolean array of nodes of the pits, or an
            array of pit node IDs. It does not matter whether or not open
            boundary nodes are flagged as pits; they are never treated as such.
            Default is 'flow__sink_flag', the pit field output from
            'route_flow_dn'
        reroute_flow : bool, optional
            If True (default), and the component detects the output fields in
            the grid produced by the route_flow_dn component, this component
            will modify the existing flow fields to route the flow across the
            lake surface(s).
            Ensure you call this method *after* you have already routed flow
            in each loop of your model.

        Examples
        --------
        Test #1: 5x5 raster grid with a diagonal lake.

        >>> import numpy as np
        >>> from landlab import RasterModelGrid
        >>> from landlab.components.flow_routing import (
        ...     DepressionFinderAndRouter)

        >>> rg = RasterModelGrid(5, 5)
        >>> z = rg.add_zeros('node', 'topographic__elevation')
        >>> z[:] = np.array([100., 100.,  95., 100., 100.,
        ...                  100., 101.,  92.,   1., 100.,
        ...                  100., 101.,   2., 101., 100.,
        ...                  100.,   3., 101., 101., 100.,
        ...                   90.,  95., 100., 100., 100.])
        >>> df = DepressionFinderAndRouter(rg)
        >>> df.map_depressions(pits=None, reroute_flow=False)
        >>> df.display_depression_map()
        . . . . .
        . . . ~ .
        . . ~ . .
        . ~ . . .
        o . . . .
        """
        self._lake_map.fill(LOCAL_BAD_INDEX_VALUE)
        self.depression_outlets = []  # reset these
        # Locate nodes with pits
        if type(pits) == str:
            try:
                pits = self._grid.at_node[pits]
                supplied_pits = np.where(pits)[0]
                self.pit_node_ids = as_id_array(
                    np.setdiff1d(supplied_pits, self._grid.boundary_nodes))
                self.number_of_pits = self.pit_node_ids.size
                self.is_pit.fill(False)
                self.is_pit[self.pit_node_ids] = True
            except FieldError:
                self._find_pits()
        elif pits is None:
            self._find_pits()
        else:  # hopefully an array or other sensible iterable
            if len(pits) == self._grid.number_of_nodes:
                supplied_pits = np.where(pits)[0]
            else:  # it's an array of node ids
                supplied_pits = pits
            # remove any boundary nodes from the supplied pit list
            self.pit_node_ids = as_id_array(
                np.setdiff1d(supplied_pits, self._grid.boundary_nodes))

            self.number_of_pits = self.pit_node_ids.size
            self.is_pit.fill(False)
            self.is_pit[self.pit_node_ids] = True
        # Set up "lake code" array
        self.flood_status.fill(_UNFLOODED)
        self.flood_status[self.pit_node_ids] = _PIT

        self._identify_depressions_and_outlets()

        if reroute_flow and ('flow__receiver_node'
                             in self._grid.at_node.keys()):
            self.receivers = self._grid.at_node['flow__receiver_node']
            self.sinks = self._grid.at_node['flow__sink_flag']
            self.grads = self._grid.at_node['topographic__steepest_slope']
            self._route_flow()
            self._reaccumulate_flow()
Ejemplo n.º 19
0
    def _find_pits(self):
        """Locate local depressions ("pits") in a gridded elevation field.

        Notes
        -----
        **Uses**:

        * ``self._elev``
        * ``self._grid``

        **Creates**:

        * ``self.is_pit`` (node array of booleans): Flag indicating whether
          the node is a pit.
        * ``self.number_of_pits`` (int): Number of pits found.
        * ``self.pit_node_ids`` (node array of ints): IDs of the nodes that
          are pits

        A node is defined as being a pit if and only if:

        1. All neighboring core nodes have equal or greater elevation, and
        2. Any neighboring open boundary nodes have a greater elevation.

        The algorithm starts off assuming that all core nodes are pits. We then
        loop through all active links. For each link, if one node is higher
        than the other, the higher one cannot be a pit, so we flag it False.
        We also look at cases in which an active link's nodes have equal
        elevations. If one is an open boundary, then the other must be a core
        node, and we declare the latter not to be a pit (via rule 2 above).
        """
        # Create the is_pit array, with all core nodes initialized to True and
        # all boundary nodes initialized to False.
        self.is_pit.fill(True)
        self.is_pit[self._grid.boundary_nodes] = False

        # Loop over all active links: if one of a link's two nodes is higher
        # than the other, the higher one is not a pit. Also, if they have
        # equal elevations and one is an open boundary, the other is not a pit.
        act_links = self._grid.active_links
        h_orth = self._grid.node_at_link_head[act_links]
        t_orth = self._grid.node_at_link_tail[act_links]

        if type(self._grid) is landlab.grid.raster.RasterModelGrid:
            if not self._grid._diagonal_links_created:
                self._grid._setup_diagonal_links()

        h_diag = self._grid._diag_activelink_tonode
        t_diag = self._grid._diag_activelink_fromnode

        # These two lines assign the False flag to any node that is higher
        # than its partner on the other end of its link
        self.is_pit[h_orth[np.where(
            self._elev[h_orth] > self._elev[t_orth])[0]]] = False
        self.is_pit[t_orth[np.where(
            self._elev[t_orth] > self._elev[h_orth])[0]]] = False

        # If we have a raster grid, handle the diagonal active links too
        # (At the moment, their data structure is a bit different)
        # TODO: update the diagonal link data structures
        # DEJH doesn't understand why this can't be vectorized as above...
        if self._D8:
            for i in range(len(self._grid._diag_active_links)):
                h = self._grid._diag_activelink_tonode[i]
                t = self._grid._diag_activelink_fromnode[i]
                if self._elev[h] > self._elev[t]:
                    self.is_pit[h] = False
                elif self._elev[t] > self._elev[h]:
                    self.is_pit[t] = False
                elif self._elev[h] == self._elev[t]:
                    if self._grid.status_at_node[h] == FIXED_VALUE_BOUNDARY:
                        self.is_pit[t] = False
                    elif self._grid.status_at_node[t] == FIXED_VALUE_BOUNDARY:
                        self.is_pit[h] = False

        # Record the number of pits and the IDs of pit nodes.
        self.number_of_pits = np.count_nonzero(self.is_pit)
        self.pit_node_ids = as_id_array(np.where(self.is_pit)[0])
Ejemplo n.º 20
0
def flow_accumulation_to_n(
    receiver_nodes,
    receiver_proportions,
    node_cell_area=1.0,
    runoff_rate=1.0,
    boundary_nodes=None,
):

    """Calculate drainage area and (steady) discharge.

    Calculates and returns the drainage area and (steady) discharge at each
    node, along with a downstream-to-upstream ordered list (array) of node IDs.

    Examples
    --------
    >>> import numpy as np
    >>> from landlab.components.flow_accum.flow_accum_to_n import(
    ... flow_accumulation_to_n)
    >>> r = np.array([[ 1,  2],
    ...               [ 4,  5],
    ...               [ 1,  5],
    ...               [ 6,  2],
    ...               [ 4, -1],
    ...               [ 4, -1],
    ...               [ 5,  7],
    ...               [ 4,  5],
    ...               [ 6,  7],
    ...               [ 7,  8]])
    >>> p = np.array([[ 0.6,   0.4 ],
    ...               [ 0.85,  0.15],
    ...               [ 0.65,  0.35],
    ...               [ 0.9,   0.1 ],
    ...               [ 1.,    0.  ],
    ...               [ 1.,    0.  ],
    ...               [ 0.75,  0.25],
    ...               [ 0.55,  0.45],
    ...               [ 0.8,   0.2 ],
    ...               [ 0.95,  0.05]])
    >>> a, q, s = flow_accumulation_to_n(r, p)
    >>> a.round(4)
    array([  1.    ,   2.575 ,   1.5   ,   1.    ,  10.    ,   5.2465,
             2.74  ,   2.845 ,   1.05  ,   1.    ])
    >>> q.round(4)
    array([  1.    ,   2.575 ,   1.5   ,   1.    ,  10.    ,   5.2465,
             2.74  ,   2.845 ,   1.05  ,   1.    ])
    >>> s[0] == 4
    True
    >>> s[1] == 5
    True
    >>> s[9] == 9
    True
    >>> len(set([1, 7])-set(s[2:4]))
    0
    >>> len(set([2, 6])-set(s[4:6]))
    0
    >>> len(set([0, 3, 8])-set(s[6:9]))
    0
    """

    assert (
        receiver_nodes.shape == receiver_proportions.shape
    ), "r and p arrays are not the same shape"

    s = as_id_array(make_ordered_node_array_to_n(receiver_nodes, receiver_proportions))
    # Note that this ordering of s DOES INCLUDE closed nodes. It really
    # shouldn't!
    # But as we don't have a copy of the grid accessible here, we'll solve this
    # problem as part of route_flow_dn.

    a, q = find_drainage_area_and_discharge_to_n(
        s,
        receiver_nodes,
        receiver_proportions,
        node_cell_area,
        runoff_rate,
        boundary_nodes,
    )

    return a, q, s
Ejemplo n.º 21
0
    def _find_pits(self):
        """Locate local depressions ("pits") in a gridded elevation field.

        Notes
        -----
        **Uses**:

        * ``self._elev``
        * ``self._grid``

        **Creates**:

        * ``self.is_pit`` (node array of booleans): Flag indicating whether
          the node is a pit.
        * ``self.number_of_pits`` (int): Number of pits found.
        * ``self.pit_node_ids`` (node array of ints): IDs of the nodes that
          are pits

        A node is defined as being a pit if and only if:

        1. All neighboring core nodes have equal or greater elevation, and
        2. Any neighboring open boundary nodes have a greater elevation.

        The algorithm starts off assuming that all core nodes are pits. We then
        loop through all active links. For each link, if one node is higher
        than the other, the higher one cannot be a pit, so we flag it False.
        We also look at cases in which an active link's nodes have equal
        elevations. If one is an open boundary, then the other must be a core
        node, and we declare the latter not to be a pit (via rule 2 above).
        """
        # Create the is_pit array, with all core nodes initialized to True and
        # all boundary nodes initialized to False.
        self.is_pit.fill(True)
        self.is_pit[self._grid.boundary_nodes] = False

        # Loop over all active links: if one of a link's two nodes is higher
        # than the other, the higher one is not a pit. Also, if they have
        # equal elevations and one is an open boundary, the other is not a pit.
        act_links = self._grid.active_links
        h_orth = self._grid.node_at_link_head[act_links]
        t_orth = self._grid.node_at_link_tail[act_links]

        if type(self._grid) is landlab.grid.raster.RasterModelGrid:
            if not self._grid._diagonal_links_created:
                self._grid._create_diag_links_at_node()

        h_diag = self._grid._diag_activelink_tonode
        t_diag = self._grid._diag_activelink_fromnode

        # These two lines assign the False flag to any node that is higher
        # than its partner on the other end of its link
        self.is_pit[h_orth[np.where(
            self._elev[h_orth] > self._elev[t_orth])[0]]] = False
        self.is_pit[t_orth[np.where(
            self._elev[t_orth] > self._elev[h_orth])[0]]] = False

        # If we have a raster grid, handle the diagonal active links too
        # (At the moment, their data structure is a bit different)
        # TODO: update the diagonal link data structures
        # DEJH doesn't understand why this can't be vectorized as above...
        if self._D8:
            for i in range(len(self._grid._diag_active_links)):
                h = self._grid._diag_activelink_tonode[i]
                t = self._grid._diag_activelink_fromnode[i]
                if self._elev[h] > self._elev[t]:
                    self.is_pit[h] = False
                elif self._elev[t] > self._elev[h]:
                    self.is_pit[t] = False
                elif self._elev[h] == self._elev[t]:
                    if self._grid.status_at_node[h] == FIXED_VALUE_BOUNDARY:
                        self.is_pit[t] = False
                    elif self._grid.status_at_node[t] == FIXED_VALUE_BOUNDARY:
                        self.is_pit[h] = False

        # Record the number of pits and the IDs of pit nodes.
        self.number_of_pits = np.count_nonzero(self.is_pit)
        self.pit_node_ids = as_id_array(np.where(self.is_pit)[0])
Ejemplo n.º 22
0
    def accumulate_flow(self, update_flow_director=True):
        """Function to make FlowAccumulator calculate drainage area and
        discharge.

        Running run_one_step() results in the following to occur:
            1. Flow directions are updated (unless update_flow_director is set
            as False).
            2. Intermediate steps that analyse the drainage network topology
            and create datastructures for efficient drainage area and discharge
            calculations.
            3. Calculation of drainage area and discharge.
            4. Depression finding and mapping, which updates drainage area and
            discharge.
        """
        # set a couple of aliases
        a = self._grid["node"]["drainage_area"]
        q = self._grid["node"]["surface_water__discharge"]

        # step 1. Find flow directions by specified method
        if update_flow_director:
            self.flow_director.run_one_step()

        # further steps vary depending on how many recievers are present
        # one set of steps is for route to one (D8, Steepest/D4)

        # step 2. Get r
        r = as_id_array(self._grid["node"]["flow__receiver_node"])

        if self.flow_director.to_n_receivers == "one":

            # step 2b. Run depression finder if passed
            # Depression finder reaccumulates flow at the end of its routine.
            # At the moment, no depression finders work with to-many, so it
            # lives here
            if self.depression_finder_provided is not None:
                self.depression_finder.map_depressions()

                # if FlowDirectorSteepest is used, update the link directions
                if self.flow_director._name == "FlowDirectorSteepest":
                    self.flow_director._determine_link_directions()

            # step 3. Stack, D, delta construction
            nd = as_id_array(flow_accum_bw._make_number_of_donors_array(r))
            delta = as_id_array(flow_accum_bw._make_delta_array(nd))
            D = as_id_array(flow_accum_bw._make_array_of_donors(r, delta))
            s = as_id_array(flow_accum_bw.make_ordered_node_array(r))

            # put these in grid so that depression finder can use it.
            # store the generated data in the grid
            self._grid["node"]["flow__data_structure_delta"][:] = delta[1:]
            self._grid["grid"]["flow__data_structure_D"] = np.array([D], dtype=object)
            self._grid["node"]["flow__upstream_node_order"][:] = s

            # step 4. Accumulate (to one or to N depending on direction method)
            a[:], q[:] = self._accumulate_A_Q_to_one(s, r)

        else:
            # Get p
            p = self._grid["node"]["flow__receiver_proportions"]

            # step 3. Stack, D, delta construction
            nd = as_id_array(flow_accum_to_n._make_number_of_donors_array_to_n(r, p))
            delta = as_id_array(flow_accum_to_n._make_delta_array_to_n(nd))
            D = as_id_array(flow_accum_to_n._make_array_of_donors_to_n(r, p, delta))
            s = as_id_array(flow_accum_to_n.make_ordered_node_array_to_n(r, p))

            # put theese in grid so that depression finder can use it.
            # store the generated data in the grid
            self._grid["node"]["flow__data_structure_delta"][:] = delta[1:]
            self._grid["grid"]["flow__data_structure_D"][0] = np.array(
                [D], dtype=object
            )
            self._grid["node"]["flow__upstream_node_order"][:] = s
            self._grid["node"]["flow__upstream_node_order"][:] = s

            # step 4. Accumulate (to one or to N depending on direction method)
            a[:], q[:] = self._accumulate_A_Q_to_n(s, r, p)

        return (a, q)
Ejemplo n.º 23
0
def flow_accumulation_to_n(
    receiver_nodes,
    receiver_proportions,
    node_cell_area=1.0,
    runoff_rate=1.0,
    boundary_nodes=None,
):

    """Calculate drainage area and (steady) discharge.

    Calculates and returns the drainage area and (steady) discharge at each
    node, along with a downstream-to-upstream ordered list (array) of node IDs.

    Examples
    --------
    >>> import numpy as np
    >>> from landlab.components.flow_accum.flow_accum_to_n import(
    ... flow_accumulation_to_n)
    >>> r = np.array([[ 1,  2],
    ...               [ 4,  5],
    ...               [ 1,  5],
    ...               [ 6,  2],
    ...               [ 4, -1],
    ...               [ 4, -1],
    ...               [ 5,  7],
    ...               [ 4,  5],
    ...               [ 6,  7],
    ...               [ 7,  8]])
    >>> p = np.array([[ 0.6,   0.4 ],
    ...               [ 0.85,  0.15],
    ...               [ 0.65,  0.35],
    ...               [ 0.9,   0.1 ],
    ...               [ 1.,    0.  ],
    ...               [ 1.,    0.  ],
    ...               [ 0.75,  0.25],
    ...               [ 0.55,  0.45],
    ...               [ 0.8,   0.2 ],
    ...               [ 0.95,  0.05]])
    >>> a, q, s = flow_accumulation_to_n(r, p)
    >>> a.round(4)
    array([  1.    ,   2.575 ,   1.5   ,   1.    ,  10.    ,   5.2465,
             2.74  ,   2.845 ,   1.05  ,   1.    ])
    >>> q.round(4)
    array([  1.    ,   2.575 ,   1.5   ,   1.    ,  10.    ,   5.2465,
             2.74  ,   2.845 ,   1.05  ,   1.    ])
    >>> s[0] == 4
    True
    >>> s[1] == 5
    True
    >>> s[9] == 9
    True
    >>> len(set([1, 7])-set(s[2:4]))
    0
    >>> len(set([2, 6])-set(s[4:6]))
    0
    >>> len(set([0, 3, 8])-set(s[6:9]))
    0
    """

    assert (
        receiver_nodes.shape == receiver_proportions.shape
    ), "r and p arrays are not the same shape"

    s = as_id_array(make_ordered_node_array_to_n(receiver_nodes, receiver_proportions))
    # Note that this ordering of s DOES INCLUDE closed nodes. It really
    # shouldn't!
    # But as we don't have a copy of the grid accessible here, we'll solve this
    # problem as part of route_flow_dn.

    a, q = find_drainage_area_and_discharge_to_n(
        s,
        receiver_nodes,
        receiver_proportions,
        node_cell_area,
        runoff_rate,
        boundary_nodes,
    )

    return a, q, s
Ejemplo n.º 24
0
    def map_depressions(self, pits='flow__sink_flag', reroute_flow=True):
        """Map depressions/lakes in a topographic surface.

        Parameters
        ----------
        pits : array or str or None, optional
            If a field name, the boolean field containing True where pits.
            If an array, either a boolean array of nodes of the pits, or an
            array of pit node IDs. It does not matter whether or not open
            boundary nodes are flagged as pits; they are never treated as such.
            Default is 'flow__sink_flag', the pit field output from
            'route_flow_dn'
        reroute_flow : bool, optional
            If True (default), and the component detects the output fields in
            the grid produced by the route_flow_dn component, this component
            will modify the existing flow fields to route the flow across the
            lake surface(s).
            Ensure you call this method *after* you have already routed flow
            in each loop of your model.

        Examples
        --------
        Test #1: 5x5 raster grid with a diagonal lake.

        >>> import numpy as np
        >>> from landlab import RasterModelGrid
        >>> from landlab.components.flow_routing import (
        ...     DepressionFinderAndRouter)

        >>> rg = RasterModelGrid(5, 5)
        >>> z = rg.add_zeros('node', 'topographic__elevation')
        >>> z[:] = np.array([100., 100.,  95., 100., 100.,
        ...                  100., 101.,  92.,   1., 100.,
        ...                  100., 101.,   2., 101., 100.,
        ...                  100.,   3., 101., 101., 100.,
        ...                   90.,  95., 100., 100., 100.])
        >>> df = DepressionFinderAndRouter(rg)
        >>> df.map_depressions(pits=None, reroute_flow=False)
        >>> df.display_depression_map()
        . . . . .
        . . . ~ .
        . . ~ . .
        . ~ . . .
        o . . . .
        """
        self._lake_map.fill(LOCAL_BAD_INDEX_VALUE)
        self.depression_outlets = []  # reset these
        # Locate nodes with pits
        if type(pits) == str:
            try:
                pits = self._grid.at_node[pits]
                supplied_pits = np.where(pits)[0]
                self.pit_node_ids = as_id_array(
                    np.setdiff1d(supplied_pits, self._grid.boundary_nodes))
                self.number_of_pits = self.pit_node_ids.size
                self.is_pit.fill(False)
                self.is_pit[self.pit_node_ids] = True
            except FieldError:
                self._find_pits()
        elif pits is None:
            self._find_pits()
        else:  # hopefully an array or other sensible iterable
            if len(pits) == self._grid.number_of_nodes:
                supplied_pits = np.where(pits)[0]
            else:  # it's an array of node ids
                supplied_pits = pits
            # remove any boundary nodes from the supplied pit list
            self.pit_node_ids = as_id_array(
                np.setdiff1d(supplied_pits, self._grid.boundary_nodes))

            self.number_of_pits = self.pit_node_ids.size
            self.is_pit.fill(False)
            self.is_pit[self.pit_node_ids] = True
        # Set up "lake code" array
        self.flood_status.fill(_UNFLOODED)
        self.flood_status[self.pit_node_ids] = _PIT

        self._identify_depressions_and_outlets()

        if reroute_flow and ('flow__receiver_node' in
                             self._grid.at_node.keys()):
            self.receivers = self._grid.at_node['flow__receiver_node']
            self.sinks = self._grid.at_node['flow__sink_flag']
            self.grads = self._grid.at_node['topographic__steepest_slope']
            self._route_flow()
            self._reaccumulate_flow()
Ejemplo n.º 25
0
def flow_directions(elev, active_links, fromnode, tonode, link_slope,
                    grid=None, baselevel_nodes=None):
    """Find flow directions on a grid.

    Finds and returns flow directions for a given elevation grid. Each node is
    assigned a single direction, toward one of its N neighboring nodes (or
    itself, if none of its neighbors are lower).

    Parameters
    ----------
    elev : array_like
        Elevations at nodes.
    active_links : array_like
        IDs of active links.
    fromnode : array_like
        IDs of the "from" node for each link.
    tonode : array_like
        IDs of the "to" node for each link.
    link_slope : array_like
        slope of each link, defined POSITIVE DOWNHILL (i.e., a negative value
        means the link runs uphill from the fromnode to the tonode).
    baselevel_nodes : array_like, optional
        IDs of open boundary (baselevel) nodes.

    Returns
    -------
    receiver : ndarray
        For each node, the ID of the node that receives its flow. Defaults to
        the node itself if no other receiver is assigned.
    steepest_slope : ndarray
        The slope value (positive downhill) in the direction of flow
    sink : ndarray
        IDs of nodes that are flow sinks (they are their own receivers)
    receiver_link : ndarray
        ID of link that leads from each node to its receiver, or
        UNDEFINED_INDEX if none.

    Examples
    --------
    The example below assigns elevations to the 10-node example network in
    Braun and Willett (2012), so that their original flow pattern should be
    re-created.

    >>> import numpy as np
    >>> from landlab.components.flow_routing.flow_direction_DN import flow_directions
    >>> z = np.array([2.4, 1.0, 2.2, 3.0, 0.0, 1.1, 2.0, 2.3, 3.1, 3.2])
    >>> fn = np.array([1,4,4,0,1,2,5,1,5,6,7,7,8,6,3,3,2,0])
    >>> tn = np.array([4,5,7,1,2,5,6,5,7,7,8,9,9,8,8,6,3,3])
    >>> s = z[fn] - z[tn]  # slope with unit link length, positive downhill
    >>> active_links = np.arange(len(fn))
    >>> r, ss, snk, rl = flow_directions(z, active_links, fn, tn, s)
    >>> r
    array([1, 4, 1, 6, 4, 4, 5, 4, 6, 7])
    >>> ss
    array([ 1.4,  1. ,  1.2,  1. ,  0. ,  1.1,  0.9,  2.3,  1.1,  0.9])
    >>> snk
    array([4])
    >>> rl[3:8]
    array([        15, 2147483647,          1,          6,          2])

    OK, the following are rough notes on design: we want to work with just the
    active links. Ways to do this:
        - Pass active_links in as argument
        - In calling code, only refer to receiver_links for active nodes

    """
    # Setup
    num_nodes = len(elev)
    steepest_slope = np.zeros(num_nodes)
    receiver = np.arange(num_nodes)
    receiver_link = UNDEFINED_INDEX + np.zeros(num_nodes, dtype=np.int)

    # For each link, find the higher of the two nodes. The higher is the
    # potential donor, and the lower is the potential receiver. If the slope
    # from donor to receiver is steeper than the steepest one found so far for
    # the donor, then assign the receiver to the donor and record the new slope.
    # (Note the minus sign when looking at slope from "t" to "f").
    #
    # NOTE: MAKE SURE WE ARE ONLY LOOKING AT ACTIVE LINKS
    #THIS REMAINS A PROBLEM AS OF DEJH'S EFFORTS, MID MARCH 14.
    #overridden as part of fastscape_stream_power

    #DEJH attempting to replace the node-by-node loop, 5/28/14:
    #This is actually about the same speed on a 100*100 grid!
    #as of Dec 2014, we prioritise the weave if a weave is viable, and only do
    #the numpy methods if it's not (~10% speed gain on 100x100 grid;
    #presumably better if the grid is bigger)
    method = 'cython'
    if method == 'cython':
        from .cfuncs import adjust_flow_receivers

        adjust_flow_receivers(fromnode, tonode, elev, link_slope,
                              active_links, receiver, receiver_link,
                              steepest_slope)
    else:
        if grid==None or not RasterModelGrid in inspect.getmro(grid.__class__):
            for i in xrange(len(fromnode)):
                f = fromnode[i]
                t = tonode[i]
                if elev[f]>elev[t] and link_slope[i]>steepest_slope[f]:
                    receiver[f] = t
                    steepest_slope[f] = link_slope[i]
                    receiver_link[f] = active_links[i]
                elif elev[t]>elev[f] and -link_slope[i]>steepest_slope[t]:
                    receiver[t] = f
                    steepest_slope[t] = -link_slope[i]
                    receiver_link[t] = active_links[i]
        else:
            #alternative, assuming grid structure doesn't change between steps
            #global neighbor_nodes
            #global links_list #this is ugly. We need another way of saving that doesn't make these permanent (can't change grid size...)
            try:
                elevs_array = np.where(neighbor_nodes!=-1, elev[neighbor_nodes], np.finfo(float).max)
            except NameError:
                neighbor_nodes = np.empty((grid.active_nodes.size, 8), dtype=int)
                #the target shape is (nnodes,4) & S,W,N,E,SW,NW,NE,SE
                neighbor_nodes[:,:4] = grid.get_active_neighbors_at_node(bad_index=-1)[grid.active_nodes,:][:,::-1] # comes as (nnodes, 4), and E,N,W,S
                neighbor_nodes[:,4:] = grid.get_diagonal_list(bad_index=-1)[grid.active_nodes,:][:,[2,1,0,3]] #NE,NW,SW,SE
                links_list = np.empty_like(neighbor_nodes)
                links_list[:,:4] = grid.node_links().T[grid.active_nodes,:] #(n_active_nodes, SWNE)
                links_list[:,4:] = grid.diagonal_links_at_node().T[grid.active_nodes,:] #SW,NW,NE,NE
                elevs_array = np.where(neighbor_nodes!=-1, elev[neighbor_nodes], np.finfo(float).max/1000.)
            slope_array = (elev[grid.active_nodes].reshape((grid.active_nodes.size,1)) - elevs_array)/grid.link_length[links_list]
            axis_indices = np.argmax(slope_array, axis=1)
            steepest_slope[grid.active_nodes] = slope_array[np.indices(axis_indices.shape),axis_indices]
            downslope = np.greater(steepest_slope, 0.)
            downslope_active = downslope[grid.active_nodes]
            receiver[downslope] = neighbor_nodes[np.indices(axis_indices.shape),axis_indices][0,downslope_active]
            receiver_link[downslope] = links_list[np.indices(axis_indices.shape),axis_indices][0,downslope_active]

    node_id = np.arange(num_nodes)

    # Optionally, handle baselevel nodes: they are their own receivers
    if baselevel_nodes is not None:
        receiver[baselevel_nodes] = node_id[baselevel_nodes]
        receiver_link[baselevel_nodes] = UNDEFINED_INDEX
        steepest_slope[baselevel_nodes] = 0.

    # The sink nodes are those that are their own receivers (this will normally
    # include boundary nodes as well as interior ones; "pits" would be sink
    # nodes that are also interior nodes).
    (sink, ) = np.where(node_id==receiver)
    sink = as_id_array(sink)

    return receiver, steepest_slope, sink, receiver_link
Ejemplo n.º 26
0
    def _create_patches_from_delaunay_diagram(self, pts, vor):
        """
        Uses a delaunay diagram drawn from the provided points to
        generate an array of patches and patch-node-link connectivity.
        Returns ...
        DEJH, 10/3/14, modified May 16.
        """
        from scipy.spatial import Delaunay
        from landlab.core.utils import anticlockwise_argsort_points_multiline
        from .cfuncs import create_patches_at_element, create_links_at_patch

        tri = Delaunay(pts)
        assert np.array_equal(tri.points, vor.points)
        nodata = -1
        self._nodes_at_patch = as_id_array(tri.simplices)
        # self._nodes_at_patch = np.empty_like(_nodes_at_patch)
        self._number_of_patches = tri.simplices.shape[0]
        # get the patches in order:
        patches_xy = np.empty((self._number_of_patches, 2), dtype=float)
        patches_xy[:, 0] = np.mean(self.node_x[self._nodes_at_patch], axis=1)
        patches_xy[:, 1] = np.mean(self.node_y[self._nodes_at_patch], axis=1)
        orderforsort = argsort_points_by_x_then_y(patches_xy)
        self._nodes_at_patch = self._nodes_at_patch[orderforsort, :]
        patches_xy = patches_xy[orderforsort, :]

        # perform a CCW sort without a line-by-line loop:
        patch_nodes_x = self.node_x[self._nodes_at_patch]
        patch_nodes_y = self.node_y[self._nodes_at_patch]
        anticlockwise_argsort_points_multiline(
            patch_nodes_x, patch_nodes_y, out=self._nodes_at_patch
        )

        # need to build a squared off, masked array of the patches_at_node
        # the max number of patches for a node in the grid is the max sides of
        # the side-iest voronoi region.
        max_dimension = len(max(vor.regions, key=len))

        self._patches_at_node = np.full(
            (self.number_of_nodes, max_dimension), nodata, dtype=int
        )

        self._nodes_at_patch = as_id_array(self._nodes_at_patch)
        self._patches_at_node = as_id_array(self._patches_at_node)

        create_patches_at_element(
            self._nodes_at_patch, self.number_of_nodes, self._patches_at_node
        )

        # build the patch-link connectivity:
        self._links_at_patch = np.empty((self._number_of_patches, 3), dtype=int)
        create_links_at_patch(
            self._nodes_at_patch,
            self._links_at_node,
            self._number_of_patches,
            self._links_at_patch,
        )
        patch_links_x = self.x_of_link[self._links_at_patch]
        patch_links_y = self.y_of_link[self._links_at_patch]
        anticlockwise_argsort_points_multiline(
            patch_links_x, patch_links_y, out=self._links_at_patch
        )

        self._patches_at_link = np.empty((self.number_of_links, 2), dtype=int)
        self._patches_at_link.fill(-1)
        create_patches_at_element(
            self._links_at_patch, self.number_of_links, self._patches_at_link
        )
        # a sort of the links will be performed here once we have corners

        self._patches_created = True
Ejemplo n.º 27
0
def flow_directions_dinf(grid,
                         elevs='topographic__elevation',
                         baselevel_nodes=None):
    """
    Find Dinfinity flow directions and proportions on a raster grid.

    Finds and returns flow directions and proportions for a given elevation
    grid 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.

    This method does not support irregular grids.

    Parameters
    ----------
    grid : ModelGrid
        A grid of type Voroni.
    elevs : field name at node or array of length node
        The surface to direct flow across.
    baselevel_nodes : array_like, optional
        IDs of open boundary (baselevel) nodes.

    Returns
    -------
    receivers : ndarray of size (num nodes, max neighbors at node)
        For each node, the IDs of the nodes that receive its flow. For nodes
        that do not direct flow to all neighbors, BAD_INDEX_VALUE is given as
        a placeholder. The ID of the node itself is given if no other receiver
        is assigned.
    proportions : ndarray of size (num nodes, max neighbors at node)
        For each receiver, the proportion of flow (between 0 and 1) is given.
        A proportion of zero indicates that the link does not have flow along
        it.
    steepest_slope : ndarray
        The slope value (positive downhill) in the direction of flow.
    steepest_receiver : ndarray
        For each node, the node ID of the node connected by the steepest link.
        BAD_INDEX_VALUE is given if no flow emmanates from the node.
    sink : ndarray
        IDs of nodes that are flow sinks (they are their own receivers)
    receiver_links : ndarray of size (num nodes, max neighbors at node)
        ID of links that leads from each node to its receiver, or
        UNDEFINED_INDEX if no flow occurs on this link.
    steepest_link : ndarray
        For each node, the link ID of the steepest link.
        BAD_INDEX_VALUE is given if no flow emmanates from the node.

    Examples
    --------
    >>> from landlab import RasterModelGrid
    >>> from landlab.components.flow_director.flow_direction_dinf import(
    ...                                                  flow_directions_dinf)

    Dinfinity routes flow based on the relative proportion of flow along the
    triangular facets around a central raster node.

    >>> grid = RasterModelGrid((3,3), spacing=(1, 1))
    >>> _ = grid.add_field('topographic__elevation',
    ...                     2.*grid.node_x+grid.node_y,
    ...                     at = 'node')
    >>> (receivers, proportions,
    ... steepest_slope, steepest_receiver,
    ... sink, receiver_links, steepest_link) = flow_directions_dinf(grid)
    >>> receivers
    array([[ 0, -1],
           [ 0,  3],
           [ 1,  4],
           [ 0,  1],
           [ 3,  0],
           [ 4,  1],
           [ 3,  4],
           [ 6,  3],
           [ 7,  4]])
    >>> proportions
    array([[ 1.        ,  0.        ],
           [ 1.        , -0.        ],
           [ 1.        , -0.        ],
           [ 1.        ,  0.        ],
           [ 0.40966553,  0.59033447],
           [ 0.40966553,  0.59033447],
           [ 1.        ,  0.        ],
           [ 0.40966553,  0.59033447],
           [ 0.40966553,  0.59033447]])
    """
    # grid type testing
    if isinstance(grid, VoronoiDelaunayGrid):
        raise NotImplementedError('Dinfinity is currently implemented for'
                                  ' Raster grids only')
    # get elevs
    elevs = _return_surface(grid, elevs)

    ### Step 1, some basic set-up, gathering information about the grid.

    # Calculate the number of nodes.
    num_nodes = len(elevs)

    # Set the number of receivers and facets.
    num_receivers = 2
    num_facets = 8

    # Create a node array
    node_id = np.arange(num_nodes)

    # find where there are closed nodes.
    closed_nodes = grid.status_at_node == CLOSED_BOUNDARY

    # create an array of the triangle numbers
    tri_numbers = np.arange(num_facets)

    ### Step 3, create some triangle datastructures because landlab (smartly)
    # makes it hard to deal with diagonals.

    # create list of triangle neighbors at node. Use orientation associated
    # with tarboton's 1997 algorithm, orthogonal link first, then diagonal.
    # has shape, (nnodes, 8 triangles, 2 neighbors)
    n_at_node = grid.neighbors_at_node
    dn_at_node = grid._diagonal_neighbors_at_node

    triangle_neighbors_at_node = np.stack([np.vstack((n_at_node[:,0], dn_at_node[:,0])),
                                           np.vstack((n_at_node[:,1], dn_at_node[:,0])),
                                           np.vstack((n_at_node[:,1], dn_at_node[:,1])),
                                           np.vstack((n_at_node[:,2], dn_at_node[:,1])),
                                           np.vstack((n_at_node[:,2], dn_at_node[:,2])),
                                           np.vstack((n_at_node[:,3], dn_at_node[:,2])),
                                           np.vstack((n_at_node[:,3], dn_at_node[:,3])),
                                           np.vstack((n_at_node[:,0], dn_at_node[:,3]))],
                                          axis=-1)
    triangle_neighbors_at_node = triangle_neighbors_at_node.swapaxes(0,1)

    # next create, triangle links at node
    l_at_node = grid.links_at_node
    dl_at_node = grid._diagonal_links_at_node
    triangle_links_at_node = np.stack([np.vstack((l_at_node[:,0], dl_at_node[:,0])),
                                       np.vstack((l_at_node[:,1], dl_at_node[:,0])),
                                       np.vstack((l_at_node[:,1], dl_at_node[:,1])),
                                       np.vstack((l_at_node[:,2], dl_at_node[:,1])),
                                       np.vstack((l_at_node[:,2], dl_at_node[:,2])),
                                       np.vstack((l_at_node[:,3], dl_at_node[:,2])),
                                       np.vstack((l_at_node[:,3], dl_at_node[:,3])),
                                       np.vstack((l_at_node[:,0], dl_at_node[:,3]))],
                                      axis=-1)
    triangle_links_at_node = triangle_links_at_node.swapaxes(0,1)

    # next create link directions and active link directions at node
    # link directions
    ld_at_node = grid._link_dirs_at_node
    dld_at_node = grid._diag__link_dirs_at_node
    triangle_link_dirs_at_node = np.stack([np.vstack((ld_at_node[:,0], dld_at_node[:,0])),
                                           np.vstack((ld_at_node[:,1], dld_at_node[:,0])),
                                           np.vstack((ld_at_node[:,1], dld_at_node[:,1])),
                                           np.vstack((ld_at_node[:,2], dld_at_node[:,1])),
                                           np.vstack((ld_at_node[:,2], dld_at_node[:,2])),
                                           np.vstack((ld_at_node[:,3], dld_at_node[:,2])),
                                           np.vstack((ld_at_node[:,3], dld_at_node[:,3])),
                                           np.vstack((ld_at_node[:,0], dld_at_node[:,3]))],
                                          axis=-1)
    triangle_link_dirs_at_node = triangle_link_dirs_at_node.swapaxes(0,1)

#    # active link directions.
#    ald_at_node = grid.active_link_dirs_at_node
#    adld_at_node = grid._diag__active_link_dirs_at_node
#
#    triangle_active_link_dirs_at_node = np.stack([np.vstack((ald_at_node[:,0], adld_at_node[:,0])),
#                                                  np.vstack((ald_at_node[:,1], adld_at_node[:,0])),
#                                                  np.vstack((ald_at_node[:,1], adld_at_node[:,1])),
#                                                  np.vstack((ald_at_node[:,2], adld_at_node[:,1])),
#                                                  np.vstack((ald_at_node[:,2], adld_at_node[:,2])),
#                                                  np.vstack((ald_at_node[:,3], adld_at_node[:,2])),
#                                                  np.vstack((ald_at_node[:,3], adld_at_node[:,3])),
#                                                  np.vstack((ald_at_node[:,0], adld_at_node[:,3]))],
#                                                 axis=-1)
#    triangle_active_link_dirs_at_node = triangle_active_link_dirs_at_node.swapaxes(0,1)
#
    # need to create a list of diagonal links since it doesn't exist.
    diag_links = np.sort(np.unique(grid._diag_links_at_node))
    diag_links = diag_links[diag_links>0]

    # calculate graidents across diagonals and orthogonals
    diag_grads = grid._calculate_gradients_at_d8_links(elevs)
    ortho_grads = grid.calc_grad_at_link(elevs)

    # finally compile link slopes
    link_slope = np.hstack((np.arctan(ortho_grads),
                            np.arctan(diag_grads)))

    # Construct the array of slope to triangles at node. This also will adjust
    # for the slope convention based on the direction of the links.
    # this is a (nnodes, 2, 8) array
    slopes_to_triangles_at_node = link_slope[triangle_links_at_node]*triangle_link_dirs_at_node

    # identify where nodes are closed.
    closed_triangle_neighbors = closed_nodes[triangle_neighbors_at_node]
    # construct some arrays that deal with the distances between points on the
    # grid.

    #### Step 3: make arrays necessary for the specific tarboton algorithm.
    # create a arrays
    ac = np.array([0., 1., 1., 2., 2., 3., 3., 4.])
    af = np.array([1., -1., 1., -1., 1., -1., 1., -1.])

    # construct d1 and d2, we know these because we know where the orthogonal
    # links are
    diag_length = ((grid.dx)**2+(grid.dy)**2)**0.5

    # for irregular grids, d1 and d2 will need to be matricies
    d1 = np.array([grid.dx, grid.dy, grid.dy, grid.dx, grid.dx, grid.dy, grid.dy, grid.dy])
    d2 = np.array([grid.dx, grid.dx, grid.dy, grid.dy, grid.dx, grid.dx, grid.dy, grid.dy])

    thresh = np.arctan(d2/d1)

    ##### Step 4, Initialize receiver and proportion arrays
    receivers = UNDEFINED_INDEX * np.ones((num_nodes, num_receivers), dtype=int)
    receiver_closed = UNDEFINED_INDEX * np.ones((num_nodes, num_receivers), dtype=int)
    proportions = np.zeros((num_nodes, num_receivers), dtype=float)
    receiver_links = UNDEFINED_INDEX * np.ones((num_nodes, num_receivers), dtype=int)
    slopes_to_receivers = np.zeros((num_nodes, num_receivers), dtype=float)

    #### Step  5  begin the algorithm in earnest

    # construct e0, e1, e2 for all triangles at all nodes.
    # will be (nnodes, nfacets=8 for raster or nfacets = max number of patches
    # for irregular grids.

    # e0 is origin point of the facet
    e0 = elevs[node_id]

    # e1 is the point on the orthogoanal edges
    e1 = elevs[triangle_neighbors_at_node[:,0,:]]
    # e2 is the point on the diagonal edges
    e2 = elevs[triangle_neighbors_at_node[:,1,:]]

    # mask out where nodes do not exits (e.g. triangle_neighbors_at_node == -1)
    e2[triangle_neighbors_at_node[:,1,:] == -1] = np.nan
    e1[triangle_neighbors_at_node[:,0,:] == -1] = np.nan

    # loop through and calculate s1 and s2
    # this will only loop nfacets times.
    s1 = np.empty_like(e1)
    s2 = np.empty_like(e2)

    for i in range(num_facets):
        s1[:,i] = (e0 - e1[:, i])/d1[i]
        s2[:,i] = (e1[:, i]- e2[:, i])/d2[i]

    # calculate r and s, the direction and magnitude
    r = np.arctan2(s2, s1)
    s = ((s1**2)+(s2**2))**0.5

    r[np.isnan(r)] = 0
    # adjust r if it doesn't sit in the realm of (0, arctan(d2,d1))
    too_small = r < 0
    radj = r.copy()
    radj[too_small] = 0
    s[too_small] = s1[too_small]

    # to consider two big, we need to look by trangle.
    for i in range(num_facets):
        too_big = r[:, i] > thresh[i]
        radj[too_big, i] = thresh[i]
        s[too_big, i] = (e0[too_big] - e2[too_big, i])/diag_length

    # calculate the geospatial version of r based on radj
    rg = np.empty_like(r)
    for i in range(num_facets):
        rg[:, i] = (af[i]*radj[:, i]) + (ac[i]*np.pi/2.)

    # set slopes that are nan to zero
    s[np.isnan(s)] = 0

    # sort slopes based on
    steepest_sort = np.argsort(s)

    # determine the steepest triangle
    steepest_triangle = tri_numbers[steepest_sort[:, -1]]

    # initialize arrays for the steepest rg and steepest s
    steepest_rg = np.empty_like(node_id, dtype = float)
    steepest_s = np.empty_like(node_id, dtype = float)

    for n in node_id:
        steepest_rg[n] = rg[n, steepest_sort[n, -1]]
        receiver_closed[n] = closed_triangle_neighbors[n, :, steepest_sort[n, -1]]
        steepest_s[n] = s[n, steepest_sort[n, -1]]
        receivers[n, :] = triangle_neighbors_at_node[n, :, steepest_sort[n, -1]]
        receiver_links[n, :] = triangle_links_at_node[n, :, steepest_sort[n, -1]]
        slopes_to_receivers[n, :] = slopes_to_triangles_at_node[n, :, steepest_sort[n, -1]]

    # construct the baseline for proportions
    rg_baseline = np.array([0., 1., 1., 2., 2., 3., 3., 4])*np.pi/2.
    #rg_baseline = np.array([0., 0.5, 1., 1.5, 2., 2.5, 3., 3.5])*np.pi/4.

    # calculate alpha1 and alpha 2
    alpha2 = (steepest_rg-rg_baseline[steepest_triangle])*af[steepest_triangle]
    alpha1 = thresh[steepest_triangle] - alpha2

    # calculate proportions from alpha
    proportions[:, 0] = (alpha1)/(alpha1+alpha2)
    proportions[:, 1] = (alpha2)/(alpha1+alpha2)

    ### END OF THE Tarboton algorithm, start of work to make this code mesh
    # with other landlab flow directing algorithms.

    # identify what drains to itself, and set proportion and id values based on
    # that.

    # if proportions is nan, drain to self
    drains_to_self = np.isnan(proportions[:, 0])

    # if all slopes are leading out, drain to self
    drains_to_self[steepest_s <= 0] = True

    # if both receiver nodes are closed, drain to self
    drains_to_two_closed = receiver_closed.sum(axis=1) == num_receivers
    drains_to_self[drains_to_two_closed] = True

    # if drains to one closed receiver, check that the open receiver actually
    # gets flow. If so, route all to the open receiver. If the receiver getting
    # all the flow is closed, then drain to self.
    all_flow_to_closed = np.sum(receiver_closed*proportions, axis=1) == 1
    drains_to_self[all_flow_to_closed] = True

    drains_to_one_closed = receiver_closed.sum(axis=1)==1
    fix_flow = drains_to_one_closed * (all_flow_to_closed == False)
    first_column_has_closed = np.array(receiver_closed[:, 0]*fix_flow,
                                       dtype=bool)
    second_column_has_closed = np.array(receiver_closed[:, 1]*fix_flow,
                                        dtype=bool)

    # remove the link to the closed node
    receivers[first_column_has_closed, 0] = -1
    receivers[second_column_has_closed, 1] = -1

    # change the proportions
    proportions[first_column_has_closed,0] = 0.
    proportions[first_column_has_closed,1] = 1.

    proportions[second_column_has_closed, 0] = 1.
    proportions[second_column_has_closed, 1] = 0.

    # set properties of drains to self.
    receivers[drains_to_self, 0] = node_id[drains_to_self]
    receivers[drains_to_self, 1] = -1

    proportions[drains_to_self, 0] = 1.
    proportions[drains_to_self, 1] = 0.

    # mask the receiver_links by where flow doesn't occur to return
    receiver_links[drains_to_self, :] = UNDEFINED_INDEX

    # identify the steepest link so that the steepest receiver, link, and slope
    # can be returned.
    slope_sort = np.argsort(np.argsort(slopes_to_receivers,
                                       axis=1),
                            axis=1) == (num_receivers-1)
    steepest_slope = slopes_to_receivers[slope_sort]
    steepest_slope[drains_to_self] = 0.

    ## identify the steepest link and steepest receiever.
    steepest_link = receiver_links[slope_sort]
    steepest_link[drains_to_self] = UNDEFINED_INDEX

    steepest_receiver = receivers[slope_sort]
    steepest_receiver[drains_to_self] = UNDEFINED_INDEX


    # Optionally, handle baselevel nodes: they are their own receivers
    if baselevel_nodes is not None:
        receivers[baselevel_nodes,0] = node_id[baselevel_nodes]
        receivers[baselevel_nodes,1:] = -1
        proportions[baselevel_nodes, 0] = 1
        proportions[baselevel_nodes, 1:] = 0
        receiver_links[baselevel_nodes,:] = UNDEFINED_INDEX
        steepest_slope[baselevel_nodes] = 0.

    # The sink nodes are those that are their own receivers (this will normally
    # include boundary nodes as well as interior ones; "pits" would be sink
    # nodes that are also interior nodes).
    (sink, ) = np.where(node_id == receivers[:, 0])
    sink = as_id_array(sink)

    return (receivers, proportions, steepest_slope, steepest_receiver, sink,
            receiver_links, steepest_link)
Ejemplo n.º 28
0
def flow_directions_mfd(elev,
                        neighbors_at_node,
                        links_at_node,
                        active_link_dir_at_node,
                        link_slope,
                        baselevel_nodes=None,
                        partition_method='slope'):

    """
    Find multiple-flow-direction flow directions on a grid.

    Finds and returns flow directions and proportions for a given elevation
    grid. Each node is assigned multiple flow directions, toward all of the N
    neighboring nodes that are lower than it. If none of the neighboring nodes
    are lower, it is assigned to itself. Flow proportions can be calculated as
    proportional to slope (default) or proportional to the square root of
    slope, which is the solution to a steady kinematic wave.

    Parameters
    ----------
    elev : array_like
        Elevations at nodes.
    neighbors_at_node : array_like (num nodes, max neighbors at node)
        For each node, the link IDs of active links.
    links_at_node : array_like (num nodes, max neighbors at node)

    link_dir_at_node: array_like (num nodes, max neighbors at node)

        IDs of the head node for each link.
    link_slope : array_like
        slope of each link, defined POSITIVE DOWNHILL (i.e., a negative value
        means the link runs uphill from the fromnode to the tonode).
    baselevel_nodes : array_like, optional
        IDs of open boundary (baselevel) nodes.
    partition_method: string, optional
        Method for partitioning flow. Options include 'slope' (default) and
        'square_root_of_slope'.

    Returns
    -------
    receivers : ndarray of size (num nodes, max neighbors at node)
        For each node, the IDs of the nodes that receive its flow. For nodes
        that do not direct flow to all neighbors, BAD_INDEX_VALUE is given as
        a placeholder. The ID of the node itself is given if no other receiver
        is assigned.
    proportions : ndarray of size (num nodes, max neighbors at node)
        For each receiver, the proportion of flow (between 0 and 1) is given.
        A proportion of zero indicates that the link does not have flow along
        it.
    steepest_slope : ndarray
        The slope value (positive downhill) in the direction of flow.
    steepest_receiver : ndarray
        For each node, the node ID of the node connected by the steepest link.
        BAD_INDEX_VALUE is given if no flow emmanates from the node.
    sink : ndarray
        IDs of nodes that are flow sinks (they are their own receivers)
    receiver_links : ndarray of size (num nodes, max neighbors at node)
        ID of links that leads from each node to its receiver, or
        UNDEFINED_INDEX if no flow occurs on this link.
    steepest_link : ndarray
        For each node, the link ID of the steepest link.
        BAD_INDEX_VALUE is given if no flow emmanates from the node.

    Examples
    --------
    >>> from landlab import RasterModelGrid
    >>> import numpy as np
    >>> from landlab.components.flow_director.flow_direction_mfd import(
    ...                                           flow_directions_mfd)
    >>> grid = RasterModelGrid((3,3), spacing=(1, 1))
    >>> elev = grid.add_field('topographic__elevation', grid.node_x+grid.node_y, at = 'node')

    For the first example, we will not pass any diagonal elements to the flow
    direction algorithm.

    >>> neighbors_at_node = grid.neighbors_at_node
    >>> links_at_node = grid.links_at_node
    >>> active_link_dir_at_node = grid.active_link_dirs_at_node
    >>> link_slope = np.arctan(grid.calc_grad_at_link(elev))
    >>> slopes_to_neighbors_at_node = link_slope[links_at_node]*active_link_dir_at_node
    >>> (receivers,
    ... proportions,
    ... steepest_slope,
    ... steepest_receiver,
    ... sink,
    ... receiver_links,
    ... steepest_link)= flow_directions_mfd(elev,
    ...                                     neighbors_at_node,
    ...                                     links_at_node,
    ...                                     active_link_dir_at_node,
    ...                                     link_slope,
    ...                                     baselevel_nodes=None,
    ...                                     partition_method='slope')
    >>> receivers
    array([[ 0, -1, -1, -1],
           [ 1, -1, -1, -1],
           [ 2, -1, -1, -1],
           [ 3, -1, -1, -1],
           [-1, -1,  3,  1],
           [-1, -1,  4, -1],
           [ 6, -1, -1, -1],
           [-1, -1, -1,  4],
           [ 8, -1, -1, -1]])
    >>> proportions
    array([[ 1. ,  0. ,  0. ,  0. ],
           [ 1. ,  0. ,  0. ,  0. ],
           [ 1. ,  0. ,  0. ,  0. ],
           [ 1. ,  0. ,  0. ,  0. ],
           [ 0. ,  0. ,  0.5,  0.5],
           [ 0. ,  0. ,  1. ,  0. ],
           [ 1. ,  0. ,  0. ,  0. ],
           [ 0. ,  0. ,  0. ,  1. ],
           [ 1. ,  0. ,  0. ,  0. ]])
    >>> proportions.sum(axis=-1)
    array([ 1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.])

    In the second example, we will pass diagonal elements to the flow direction
    algorithm.

    >>> grid._create_diag_links_at_node()
    >>> dal, d8t, d8h = grid._d8_active_links()
    >>> neighbors_at_node = np.hstack((grid.neighbors_at_node,
    ...                                grid._diagonal_neighbors_at_node))
    >>> links_at_node = np.hstack((grid.links_at_node,
    ...                            grid._diagonal_links_at_node))
    >>> active_link_dir_at_node = np.hstack((grid.active_link_dirs_at_node,
    ...                                      grid._diag__active_link_dirs_at_node))

    We need to create a list of diagonal links since it doesn't exist.

    >>> diag_links = np.sort(np.unique(grid._diag_links_at_node))
    >>> diag_links = diag_links[diag_links>0]
    >>> diag_grads = np.zeros(diag_links.shape)
    >>> where_active_diag = dal>=diag_links.min()
    >>> active_diags_inds = dal[where_active_diag]-diag_links.min()
    >>> active_diag_grads = grid._calculate_gradients_at_d8_active_links(elev)
    >>> diag_grads[active_diags_inds] = active_diag_grads[where_active_diag]
    >>> ortho_grads = grid.calc_grad_at_link(elev)
    >>> link_slope = np.hstack((np.arctan(ortho_grads),
    ...                         np.arctan(diag_grads)))
    >>> (receivers,
    ... proportions,
    ... steepest_slope,
    ... steepest_receiver,
    ... sink,
    ... receiver_links,
    ... steepest_link)= flow_directions_mfd(elev,
    ...                                     neighbors_at_node,
    ...                                     links_at_node,
    ...                                     active_link_dir_at_node,
    ...                                     link_slope,
    ...                                     baselevel_nodes=None,
    ...                                     partition_method='slope')
    >>> receivers
    array([[ 0, -1, -1, -1, -1, -1, -1, -1],
           [ 1, -1, -1, -1, -1, -1, -1, -1],
           [ 2, -1, -1, -1, -1, -1, -1, -1],
           [ 3, -1, -1, -1, -1, -1, -1, -1],
           [-1, -1,  3,  1, -1, -1,  0, -1],
           [-1, -1,  4, -1, -1, -1, -1, -1],
           [ 6, -1, -1, -1, -1, -1, -1, -1],
           [-1, -1, -1,  4, -1, -1, -1, -1],
           [-1, -1, -1, -1, -1, -1,  4, -1]])
    >>> proportions
    array([[ 1.        ,  0.        ,  0.        ,  0.        ,  0.        ,
             0.        ,  0.        ,  0.        ],
           [ 1.        ,  0.        ,  0.        ,  0.        ,  0.        ,
             0.        ,  0.        ,  0.        ],
           [ 1.        ,  0.        ,  0.        ,  0.        ,  0.        ,
             0.        ,  0.        ,  0.        ],
           [ 1.        ,  0.        ,  0.        ,  0.        ,  0.        ,
             0.        ,  0.        ,  0.        ],
           [ 0.        ,  0.        ,  0.31091174,  0.31091174,  0.        ,
             0.        ,  0.37817653,  0.        ],
           [ 0.        ,  0.        ,  1.        ,  0.        ,  0.        ,
             0.        ,  0.        ,  0.        ],
           [ 1.        ,  0.        ,  0.        ,  0.        ,  0.        ,
             0.        ,  0.        ,  0.        ],
           [ 0.        ,  0.        ,  0.        ,  1.        ,  0.        ,
             0.        ,  0.        ,  0.        ],
           [ 0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
             0.        ,  1.        ,  0.        ]])
    >>> proportions.sum(axis=-1)
    array([ 1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.,  1.])
    """
    # Calculate the number of nodes.
    num_nodes = len(elev)

    # Create a node array
    node_id = np.arange(num_nodes)

    # Calculate the maximum number of neighbors at node.
    max_number_of_neighbors = neighbors_at_node.shape[1]

    # Make a copy of neighbors_at_node so we can change it into the receiver
    # array.
    receivers = neighbors_at_node.copy()

    # Construct the array of slope to neighbors at node. This also will adjust
    # for the slope convention based on the direction of the link.
    slopes_to_neighbors_at_node = link_slope[links_at_node]*active_link_dir_at_node

    # Make a copy so this can be changed based on where no flow occurs.
    receiver_links = links_at_node.copy()

    # some of these potential recievers may have already been assigned as
    # UNDEFINED_INDEX because the link was inactive. Make a mask of these for
    # future use. Also find the close nodes.
    inactive_link_to_neighbor = active_link_dir_at_node == 0
    closed_nodes = np.sum(np.abs(active_link_dir_at_node), 1) == 0
    # Now calculate where flow occurs.
    # First, make an elevation array of potential receivers.
    potential_receiver_elev = elev[neighbors_at_node]

    # now make an array of the same shape (for direct comparison) of the source
    # node elevation.
    source_node_elev = elev[np.tile(node_id, (max_number_of_neighbors,1)).T]

    # find where flow does not occur (source is lower that receiver)
    flow_does_not_occur = source_node_elev<=potential_receiver_elev

    # Where the source is lower, set receivers to UNDEFINED_INDEX
    receivers[flow_does_not_occur] = UNDEFINED_INDEX

    # Where the link is not active, set receivers to UNDEFINED_INDEX
    receivers[inactive_link_to_neighbor] = UNDEFINED_INDEX

    # Next, find where a node drains to itself
    drains_to_self = receivers.sum(1) == -1*max_number_of_neighbors

    # Where this occurs, set the receiver ID in the first column of receivers
    # to the node ID.
    receivers[drains_to_self, 0] = node_id[drains_to_self]

    # Finally, set the first element of the closed nodes to themselves.
    receivers[closed_nodes, 0] = node_id[closed_nodes]

    # Next, calculate flow proportions.
    # Copy slope array and mask by where flow is not occuring and where the
    # link is inactive.
    flow_slopes = slopes_to_neighbors_at_node.copy()
    flow_slopes[flow_does_not_occur] = 0.
    flow_slopes[inactive_link_to_neighbor] = 0.

    if partition_method == 'square_root_of_slope':
        values_for_partitioning = flow_slopes**0.5
    elif partition_method == 'slope':
        values_for_partitioning = flow_slopes
    else:
        raise ValueError ('Keyword argument to partition_method invalid.')

    # Calculate proportions by normalizing by rowsums.
    denom = np.tile(values_for_partitioning.sum(1), (max_number_of_neighbors,1)).T
    denom[denom<=0] = 1  # to prevent runtime errors
    proportions = values_for_partitioning/denom
    proportions[drains_to_self, 0] = 1
    proportions[drains_to_self, 1:] = 0

    # Might need to sort by proportions and rearrange to follow expectations
    # of no UNDEFINED_INDEX value in first column. KRB NOT SURE

    # mask the receiver_links by where flow doesn't occur to return
    receiver_links[flow_does_not_occur] = UNDEFINED_INDEX
    receiver_links[inactive_link_to_neighbor] = UNDEFINED_INDEX

    # identify the steepest link so that the steepest receiver, link, and slope
    # can be returned.
    slope_sort = np.argsort(np.argsort(flow_slopes,
                                   axis=1),
                        axis=1) == (max_number_of_neighbors-1)
    steepest_slope = flow_slopes[slope_sort]

    ## identify the steepest link and steepest receiever.
    steepest_link = receiver_links[slope_sort]
    steepest_receiver = receivers[slope_sort]

    # Optionally, handle baselevel nodes: they are their own receivers
    if baselevel_nodes is not None:
        receivers[baselevel_nodes,0] = node_id[baselevel_nodes]
        receivers[baselevel_nodes,1:] = -1
        proportions[baselevel_nodes, 0] = 1
        proportions[baselevel_nodes, 1:] = 0
        receiver_links[baselevel_nodes,:] = UNDEFINED_INDEX
        steepest_slope[baselevel_nodes] = 0.

    # The sink nodes are those that are their own receivers (this will normally
    # include boundary nodes as well as interior ones; "pits" would be sink
    # nodes that are also interior nodes).
    (sink, ) = np.where(node_id==receivers[:,0])
    sink = as_id_array(sink)

    return (receivers, proportions, steepest_slope, steepest_receiver, sink,
            receiver_links, steepest_link)