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
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
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
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])
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()
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 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
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()
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])
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
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
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, )
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, )
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
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)
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
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()
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])
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
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])
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)
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
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
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)
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)