def _test_water_inputs(self, grid, runoff_rate): """Test inputs for runoff_rate and water__unit_flux_in.""" if "water__unit_flux_in" not in grid.at_node: if runoff_rate is None: # assume that if runoff rate is not supplied, that the value # should be set to one everywhere. grid.add_ones("node", "water__unit_flux_in", dtype=float) else: runoff_rate = return_array_at_node(grid, runoff_rate) grid.at_node["water__unit_flux_in"] = runoff_rate else: if runoff_rate is not None: print("FlowAccumulator found both the field " + "'water__unit_flux_in' and a provided float or " + "array for the runoff_rate argument. THE FIELD IS " + "BEING OVERWRITTEN WITH THE SUPPLIED RUNOFF_RATE!") runoff_rate = return_array_at_node(grid, runoff_rate) grid.at_node["water__unit_flux_in"] = runoff_rate # perform a test (for politeness!) that the old name for the water_in # field is not present: if "water__discharge_in" in grid.at_node: warnings.warn( "This component formerly took 'water__discharge" + "_in' as an input field. However, this field is " + "now named 'water__unit_flux_in'. You are still " + "using a field with the old name. Please update " + "your code if you intended to use that field.", DeprecationWarning, )
def _test_water_inputs(self, grid, runoff_rate): """Test inputs for runoff_rate and water__unit_flux_in.""" try: grid.at_node['water__unit_flux_in'] except FieldError: if runoff_rate is None: # assume that if runoff rate is not supplied, that the value # should be set to one everywhere. grid.add_ones('node', 'water__unit_flux_in', dtype=float) else: runoff_rate = return_array_at_node(grid, runoff_rate) grid.at_node['water__unit_flux_in'] = runoff_rate else: if runoff_rate is not None: print ("FlowAccumulator found both the field " + "'water__unit_flux_in' and a provided float or " + "array for the runoff_rate argument. THE FIELD IS " + "BEING OVERWRITTEN WITH THE SUPPLIED RUNOFF_RATE!") runoff_rate = return_array_at_node(grid, runoff_rate) grid.at_node['water__unit_flux_in'] = runoff_rate # perform a test (for politeness!) that the old name for the water_in # field is not present: if 'water__discharge_in' in grid.at_node: warnings.warn("This component formerly took 'water__discharge" + "_in' as an input field. However, this field is " + "now named 'water__unit_flux_in'. You are still " + "using a field with the old name. Please update " + "your code if you intended the FlowRouter to use " + "that field.", DeprecationWarning)
def __init__(self, grid, K_sed=None, K_br=None, F_f=None, phi=None, H_star=None, v_s=None, m_sp=None, n_sp=None, sp_crit_sed=None, sp_crit_br=None, discharge_field='surface_water__discharge', solver='basic', dt_min=DEFAULT_MINIMUM_TIME_STEP, **kwds): """Initialize the Space model. """ super(Space, self).__init__(grid, m_sp=m_sp, n_sp=n_sp, phi=phi, F_f=F_f, v_s=v_s, dt_min=dt_min, discharge_field=discharge_field) self._grid = grid #store grid # space specific inits self.H_star = H_star if 'soil__depth' in grid.at_node: self.soil__depth = grid.at_node['soil__depth'] else: self.soil__depth = grid.add_zeros( 'soil__depth', at='node', dtype=float) if 'bedrock__elevation' in grid.at_node: self.bedrock__elevation = grid.at_node['bedrock__elevation'] else: self.bedrock__elevation = grid.add_zeros( 'bedrock__elevation', at='node', dtype=float) self.bedrock__elevation[:] = self.topographic__elevation -\ self.soil__depth self.Es = np.zeros(grid.number_of_nodes) self.Er = np.zeros(grid.number_of_nodes) #K's and critical values can be floats, grid fields, or arrays self.K_sed = return_array_at_node(grid, K_sed) self.K_br = return_array_at_node(grid, K_br) self.sp_crit_sed = return_array_at_node(grid, sp_crit_sed) self.sp_crit_br = return_array_at_node(grid, sp_crit_br) # Handle option for solver if solver == 'basic': self.run_one_step = self.run_one_step_basic elif solver == 'adaptive': self.run_one_step = self.run_with_adaptive_time_step_solver self.time_to_flat = np.zeros(grid.number_of_nodes) self.porosity_factor = 1.0 / (1.0 - self.phi) else: raise ValueError("Parameter 'solver' must be one of: " + "'basic', 'adaptive'")
def _changed_surface(self): """Check if the surface values have changed. If the surface values are stored as a field, it is important to check if they have changed since the component was instantiated. """ if isinstance(self.surface, six.string_types): self.surface_values = return_array_at_node(self._grid, self.surface)
def _changed_surface(self): """Check if the surface values have changed. If the surface values are stored as a field, it is important to check if they have changed since the component was instantiated. """ if isinstance(self._surface, str): self._surface_values = return_array_at_node(self._grid, self._surface)
def _test_water_inputs(self, grid, runoff_rate): """Test inputs for runoff_rate and water__unit_flux_in.""" if "water__unit_flux_in" not in grid.at_node: if runoff_rate is None: # assume that if runoff rate is not supplied, that the value # should be set to one everywhere. grid.add_ones("water__unit_flux_in", at="node", dtype=float) else: runoff_rate = return_array_at_node(grid, runoff_rate) grid.at_node["water__unit_flux_in"] = runoff_rate else: if runoff_rate is not None: print("FlowAccumulator found both the field " + "'water__unit_flux_in' and a provided float or " + "array for the runoff_rate argument. THE FIELD IS " + "BEING OVERWRITTEN WITH THE SUPPLIED RUNOFF_RATE!") runoff_rate = return_array_at_node(grid, runoff_rate) grid.at_node["water__unit_flux_in"] = runoff_rate
def test_return_array(): mg = RasterModelGrid((10, 10)) node_vals = np.arange(mg.number_of_nodes) out = return_array_at_node(mg, node_vals) np.testing.assert_array_equal(np.arange(mg.number_of_nodes), out) link_vals = np.arange(mg.number_of_links) out = return_array_at_link(mg, link_vals) np.testing.assert_array_equal(np.arange(mg.number_of_links), out)
def plot_profiles( self, field="topographic__elevation", xlabel="Distance Along Profile", ylabel="Plotted Quantity", title="Extracted Profiles", color=None, ): """Plot distance-upstream vs at at-node or size (nnodes,) quantity. Parameters ---------- field : field name or nnode array Array of the at-node-field to plot against distance upstream. Default value is the at-node field 'topographic__elevation'. xlabel : str, optional X-axis label, default is "Distance Along Profile". ylabel : str, optional Y-axis label, default value is "Plotted Quantity". title : str, optional Plot title, default value is "Extracted Profiles". color : RGBA tuple or color string Color to use in order to plot all profiles the same color. Default is None, and the colors assigned to each profile are used. """ quantity = return_array_at_node(self._grid, field) # create segments the way that line collection likes them. segments = [] qmin = [] qmax = [] for idx, nodes in enumerate(self._nodes): segments.append( list(zip(self._distance_along_profile[idx], quantity[nodes]))) qmin.append(min(quantity[nodes])) qmax.append(max(quantity[nodes])) # We need to set the plot limits. ax = plt.gca() ax.set_xlim( _recursive_min(self._distance_along_profile), _recursive_max(self._distance_along_profile), ) ax.set_ylim(min(qmin), max(qmax)) line_segments = LineCollection(segments) colors = color or self._colors line_segments.set_color(colors) ax.add_collection(line_segments) ax.set_xlabel(xlabel) ax.set_ylabel(ylabel) ax.set_title(title)
def dz_advection(self): """Rate of vertical advection. Parameters ---------- dz_advection : float, `(n_nodes, )` shape array, or at-node field array optional Change in rock elevation due to advection by some external process. This can be changed using the property setter. Dimensions are in length, not length per time. Returns ------- current rate of vertical advection """ return return_array_at_node(self._grid, self._dz_advection)
def __init__(self, grid, surface): """Initialize the _FlowDirector class.""" # We keep a local reference to the grid super(_FlowDirector, self).__init__(grid) self._grid = grid self._bc_set_code = self.grid.bc_set_code # set up the grid type testing self._is_raster = isinstance(self._grid, RasterModelGrid) if not self._is_raster: self.method = None # save elevations as class properites. self.surface = surface self.surface_values = return_array_at_node(grid, surface)
def __init__(self, grid, surface): """Initialize the _FlowDirector class.""" # We keep a local reference to the grid super(_FlowDirector, self).__init__(grid) self._grid = grid self._bc_set_code = self.grid.bc_set_code # set up the grid type testing self._is_raster = isinstance(self._grid, RasterModelGrid) if not self._is_raster: self.method = None # save elevations as class properites. self.surface = surface self.surface_values = return_array_at_node(grid, surface)
def __init__(self, grid, surface): """Initialize the _FlowDirector class.""" # We keep a local reference to the grid super(_FlowDirector, self).__init__(grid) self._bc_set_code = self._grid.bc_set_code # set up the grid type testing self._is_raster = isinstance(self._grid, RasterModelGrid) if not self._is_raster: self._method = None # save elevations as class properites. self._surface = surface self._surface_values = return_array_at_node(grid, surface) grid.add_zeros("flow__sink_flag", at="node", dtype=bool, clobber=True)
def rock_id(self): """Rock type for deposition. Parameters ---------- rock_id : value or `(n_nodes, )` shape array, optional Rock type id for new material if deposited. This can be changed using the property setter. Returns ------- current type of rock being deposited (if deposition occurs) """ if self._rock_id is None: return None else: return return_array_at_node(self._grid, self._rock_id)
def __init__( self, grid, surface="topographic__elevation", method="D8", fill_flat=False, ignore_overfill=False, ): """ Initialise the component. """ if "flow__receiver_node" in grid.at_node: if grid.at_node["flow__receiver_node"].size != grid.size("node"): msg = ( "A route-to-multiple flow director has been " "run on this grid. The landlab development team has not " "verified that SinkFillerBarnes is compatible with " "route-to-multiple methods. Please open a GitHub Issue " "to start this process." ) raise NotImplementedError(msg) # Most of the functionality of this component is directly inherited # from SinkFillerBarnes, so super(SinkFillerBarnes, self).__init__( grid, surface=surface, method=method, fill_flat=fill_flat, fill_surface=surface, redirect_flow_steepest_descent=False, reaccumulate_flow=False, ignore_overfill=ignore_overfill, track_lakes=True, ) # note we will always track the fills, since we're only doing this # once... Likewise, no need for flow routing; this is not going to # get used dynamically. self._supplied_surface = return_array_at_node(grid, surface).copy() # create the only new output field: self._sed_fill_depth = self.grid.add_zeros( "node", "sediment_fill__depth", noclobber=False )
def __init__( self, grid, surface="topographic__elevation", method="D8", fill_flat=False, ignore_overfill=False, ): """ Initialise the component. """ if "flow__receiver_node" in grid.at_node: if grid.at_node["flow__receiver_node"].size != grid.size("node"): msg = ( "A route-to-multiple flow director has been " "run on this grid. The landlab development team has not " "verified that SinkFillerBarnes is compatible with " "route-to-multiple methods. Please open a GitHub Issue " "to start this process." ) raise NotImplementedError(msg) # Most of the functionality of this component is directly inherited # from SinkFillerBarnes, so super(SinkFillerBarnes, self).__init__( grid, surface=surface, method=method, fill_flat=fill_flat, fill_surface=surface, redirect_flow_steepest_descent=False, reaccumulate_flow=False, ignore_overfill=ignore_overfill, track_lakes=True, ) # note we will always track the fills, since we're only doing this # once... Likewise, no need for flow routing; this is not going to # get used dynamically. self._supplied_surface = return_array_at_node(grid, surface).copy() # create the only new output field: self._sed_fill_depth = self.grid.add_zeros( "node", "sediment_fill__depth", noclobber=False )
def __init__( self, grid, K_sed=0.02, K_br=0.02, F_f=0.0, phi=0.3, H_star=0.1, v_s=1.0, v_s_lake=None, m_sp=0.5, n_sp=1.0, sp_crit_sed=0.0, sp_crit_br=0.0, discharge_field="surface_water__discharge", erode_flooded_nodes=False, thickness_lim=100, ): """Initialize the SpaceLargeScaleEroder model. Parameters ---------- grid : ModelGrid Landlab ModelGrid object K_sed : float, array of float, or str, optional Erodibility for sediment (units vary) as either a number or a field name. K_br : float, array of float, or str, optional Erodibility for bedrock (units vary) as either a number or a field name. F_f : float, optional Fraction of permanently suspendable fines in bedrock [-]. phi : float, optional Sediment porosity [-]. H_star : float, optional Sediment thickness required for full entrainment [L]. v_s : float, optional Effective settling velocity for chosen grain size metric [L/T]. v_s_lake : float, optional Effective settling velocity in lakes for chosen grain size metric [L/T]. m_sp : float, optional Drainage area exponent (units vary). n_sp : float, optional Slope exponent (units vary). sp_crit_sed : float, array of float, or str, optional Critical stream power to erode sediment [E/(TL^2)]. sp_crit_br : float, array of float, or str, optional Critical stream power to erode rock [E/(TL^2)] discharge_field : float, array of float, or str, optional Discharge [L^2/T]. The default is to use the grid field 'surface_water__discharge', which is simply drainage area multiplied by the default rainfall rate (1 m/yr). To use custom spatially/temporally varying rainfall, use 'water__unit_flux_in' to specify water input to the FlowAccumulator. erode_flooded_nodes : bool, optional Whether erosion occurs in flooded nodes identified by a depression/lake mapper (e.g., DepressionFinderAndRouter). When set to false, the field *flood_status_code* must be present on the grid (this is created by the DepressionFinderAndRouter). Default True. """ if grid.at_node["flow__receiver_node"].size != grid.size("node"): msg = ( "A route-to-multiple flow director has been " "run on this grid. The landlab development team has not " "verified that SpaceLargeScaleEroder is compatible with " "route-to-multiple methods. Please open a GitHub Issue " "to start this process." ) raise NotImplementedError(msg) super(SpaceLargeScaleEroder, self).__init__(grid) self._soil__depth = grid.at_node["soil__depth"] self._topographic__elevation = grid.at_node["topographic__elevation"] if "bedrock__elevation" in grid.at_node: self._bedrock__elevation = grid.at_node["bedrock__elevation"] else: self._bedrock__elevation = grid.add_zeros( "bedrock__elevation", at="node", dtype=float ) self._bedrock__elevation[:] = ( self._topographic__elevation - self._soil__depth ) # Check consistency of bedrock, soil and topogarphic elevation fields err_msg = ( "The sum of bedrock elevation and topographic elevation should be equal" ) np.testing.assert_almost_equal( grid.at_node["bedrock__elevation"] + grid.at_node["soil__depth"], grid.at_node["topographic__elevation"], decimal=5, err_msg=err_msg, ) # specific inits self._thickness_lim = thickness_lim self._H_star = H_star self._sed_erosion_term = np.zeros(grid.number_of_nodes) self._br_erosion_term = np.zeros(grid.number_of_nodes) self._Es = np.zeros(grid.number_of_nodes) self._Er = np.zeros(grid.number_of_nodes) # K's and critical values can be floats, grid fields, or arrays # use setters defined below self._K_sed = K_sed self._K_br = K_br self._sp_crit_sed = return_array_at_node(grid, sp_crit_sed) self._sp_crit_br = return_array_at_node(grid, sp_crit_br) self._erode_flooded_nodes = erode_flooded_nodes self._flow_receivers = grid.at_node["flow__receiver_node"] self._stack = grid.at_node["flow__upstream_node_order"] self._slope = grid.at_node["topographic__steepest_slope"] self.initialize_output_fields() self._qs = grid.at_node["sediment__outflux"] self._q = return_array_at_node(grid, discharge_field) # for backward compatibility (remove in 3.0.0+) grid.at_node["sediment__flux"] = grid.at_node["sediment__outflux"] self._Q_to_the_m = np.zeros(grid.number_of_nodes) self._S_to_the_n = np.zeros(grid.number_of_nodes) # store other constants self._m_sp = np.float64(m_sp) self._n_sp = np.float64(n_sp) self._phi = np.float64(phi) self._v_s = np.float64(v_s) if isinstance(grid, RasterModelGrid): self._link_lengths = grid.length_of_d8 else: self._link_lengths = grid.length_of_link if v_s_lake is None: self._v_s_lake = np.float64(v_s) else: self._v_s_lake = np.float64(v_s_lake) self._F_f = np.float64(F_f) if phi >= 1.0: raise ValueError("Porosity must be < 1.0") if F_f > 1.0: raise ValueError("Fraction of fines must be <= 1.0") if phi < 0.0: raise ValueError("Porosity must be > 0.0") if F_f < 0.0: raise ValueError("Fraction of fines must be > 0.0")
def flow_directions_dinf(grid, elevs="topographic__elevation", baselevel_nodes=None): """ Find Dinfinity flow directions and proportions on a raster grid. Finds and returns flow directions and proportions for a given elevation grid by the D infinity method (Tarboton, 1997). Each node is assigned two flow directions, toward the two neighboring nodes that are on the steepest subtriangle. Partitioning of flow is done based on the aspect of the subtriangle. This method does not support irregular grids. Parameters ---------- grid : ModelGrid A grid of type Voroni. elevs : field name at node or array of length node The surface to direct flow across. baselevel_nodes : array_like, optional IDs of open boundary (baselevel) nodes. Returns ------- receivers : ndarray of size (num nodes, max neighbors at node) For each node, the IDs of the nodes that receive its flow. For nodes that do not direct flow to all neighbors, BAD_INDEX_VALUE is given as a placeholder. The ID of the node itself is given if no other receiver is assigned. proportions : ndarray of size (num nodes, max neighbors at node) For each receiver, the proportion of flow (between 0 and 1) is given. A proportion of zero indicates that the link does not have flow along it. slopes: ndarray of size (num nodes, max neighbors at node) For each node in the array ``recievers``, the slope value (positive downhill) in the direction of flow. If no flow occurs (value of ``recievers`` is -1), then this array is set to 0. steepest_slope : ndarray The slope value (positive downhill) in the direction of flow. steepest_receiver : ndarray For each node, the node ID of the node connected by the steepest link. BAD_INDEX_VALUE is given if no flow emmanates from the node. sink : ndarray IDs of nodes that are flow sinks (they are their own receivers) receiver_links : ndarray of size (num nodes, max neighbors at node) ID of links that leads from each node to its receiver, or UNDEFINED_INDEX if no flow occurs on this link. steepest_link : ndarray For each node, the link ID of the steepest link. BAD_INDEX_VALUE is given if no flow emmanates from the node. Examples -------- >>> from landlab import RasterModelGrid >>> from landlab.components.flow_director.flow_direction_dinf import( ... flow_directions_dinf) Dinfinity routes flow based on the relative proportion of flow along the triangular facets around a central raster node. >>> grid = RasterModelGrid((3,3), spacing=(1, 1)) >>> _ = grid.add_field('topographic__elevation', ... 2.*grid.node_x+grid.node_y, ... at = 'node') >>> (receivers, proportions, slopes, ... steepest_slope, steepest_receiver, ... sink, receiver_links, steepest_link) = flow_directions_dinf(grid) >>> receivers array([[ 0, -1], [ 0, 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]]) 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, 3], [ 1, 4], [ 0, 1], [ 3, 0], [ 4, 1], [ 3, 4], [ 6, 3], [ 7, 4]]) >>> slopes array([[-2. , -0. ], [ 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) ### 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.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) # # active link directions. # ald_at_node = grid.active_link_dirs_at_node # adld_at_node = grid.active_diagonal_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.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 ) # 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] = 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. # 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, slopes_to_receivers, steepest_slope, steepest_receiver, sink, receiver_links, steepest_link, )
def __init__(self, grid, K=0.002, v_s=1.0, m_sp=0.5, n_sp=1.0, sp_crit=0.0, F_f=0.0, discharge_field="surface_water__discharge", solver="basic", dt_min=DEFAULT_MINIMUM_TIME_STEP, **kwds): """Initialize the ErosionDeposition model. Parameters ---------- grid : ModelGrid Landlab ModelGrid object K : float, field name, or array Erodibility for substrate (units vary). v_s : float Effective settling velocity for chosen grain size metric [L/T]. m_sp : float Discharge exponent (units vary) n_sp : float Slope exponent (units vary) sp_crit : float, field name, or array Critical stream power to erode substrate [E/(TL^2)] F_f : float Fraction of eroded material that turns into "fines" that do not contribute to (coarse) sediment load. Defaults to zero. discharge_field : float, field name, or array Discharge [L^2/T]. The default is to use the grid field 'surface_water__discharge', which is simply drainage area multiplied by the default rainfall rate (1 m/yr). To use custom spatially/temporally varying rainfall, use 'water__unit_flux_in' to specify water input to the FlowAccumulator. solver : string Solver to use. Options at present include: (1) 'basic' (default): explicit forward-time extrapolation. Simple but will become unstable if time step is too large. (2) 'adaptive': adaptive time-step solver that estimates a stable step size based on the shortest time to "flattening" among all upstream-downstream node pairs. Examples --------- >>> import numpy as np >>> from landlab import RasterModelGrid >>> from landlab.components import FlowAccumulator >>> from landlab.components import DepressionFinderAndRouter >>> from landlab.components import ErosionDeposition >>> from landlab.components import FastscapeEroder >>> np.random.seed(seed = 5000) Define grid and initial topography: -5x5 grid with baselevel in the lower left corner -all other boundary nodes closed -Initial topography is plane tilted up to the upper right + noise >>> nr = 5 >>> nc = 5 >>> dx = 10 >>> mg = RasterModelGrid((nr, nc), xy_spacing=10.0) >>> _ = mg.add_zeros('node', 'topographic__elevation') >>> mg['node']['topographic__elevation'] += (mg.node_y/10 + ... mg.node_x/10 + np.random.rand(len(mg.node_y)) / 10) >>> mg.set_closed_boundaries_at_grid_edges(bottom_is_closed=True, ... left_is_closed=True, ... right_is_closed=True, ... top_is_closed=True) >>> mg.set_watershed_boundary_condition_outlet_id(0,\ mg['node']['topographic__elevation'], -9999.) >>> fsc_dt = 100. >>> ed_dt = 1. Check initial topography >>> mg.at_node['topographic__elevation'] # doctest: +NORMALIZE_WHITESPACE array([ 0.02290479, 1.03606698, 2.0727653 , 3.01126678, 4.06077707, 1.08157495, 2.09812694, 3.00637448, 4.07999597, 5.00969486, 2.04008677, 3.06621577, 4.09655859, 5.04809001, 6.02641123, 3.05874171, 4.00585786, 5.0595697 , 6.04425233, 7.05334077, 4.05922478, 5.0409473 , 6.07035008, 7.0038935 , 8.01034357]) Instantiate Fastscape eroder, flow router, and depression finder >>> fr = FlowAccumulator(mg, flow_director='D8') >>> df = DepressionFinderAndRouter(mg) >>> fsc = FastscapeEroder( ... mg, ... K_sp=.001, ... m_sp=.5, ... n_sp=1) Burn in an initial drainage network using the Fastscape eroder: >>> for x in range(100): ... fr.run_one_step() ... df.map_depressions() ... flooded = np.where(df.flood_status==3)[0] ... fsc.run_one_step(dt = fsc_dt) ... mg.at_node['topographic__elevation'][0] -= 0.001 #uplift Instantiate the E/D component: >>> ed = ErosionDeposition( ... mg, ... K=0.00001, ... v_s=0.001, ... m_sp=0.5, ... n_sp = 1.0, ... sp_crit=0) Now run the E/D component for 2000 short timesteps: >>> for x in range(2000): #E/D component loop ... fr.run_one_step() ... df.map_depressions() ... ed.run_one_step(dt = ed_dt) ... mg.at_node['topographic__elevation'][0] -= 2e-4 * ed_dt Now we test to see if topography is right: >>> np.around(mg.at_node['topographic__elevation'], decimals=3) # doctest: +NORMALIZE_WHITESPACE array([-0.477, 1.036, 2.073, 3.011, 4.061, 1.082, -0.08 , -0.065, -0.054, 5.01 , 2.04 , -0.065, -0.065, -0.053, 6.026, 3.059, -0.054, -0.053, -0.035, 7.053, 4.059, 5.041, 6.07 , 7.004, 8.01 ]) """ if grid.at_node["flow__receiver_node"].size != grid.size("node"): msg = ("A route-to-multiple flow director has been " "run on this grid. The landlab development team has not " "verified that ErosionDeposition is compatible with " "route-to-multiple methods. Please open a GitHub Issue " "to start this process.") raise NotImplementedError(msg) if "phi" in kwds: msg = "As of Landlab v2 ErosionDeposition no longer takes the keyword argument phi. The sediment flux is considered to represent bulk deposit volume rather than mineral volume, and therefore porosity does not impact the dynamics. The following pull request explains the math behind this: https://github.com/landlab/landlab/pull/1186." raise ValueError(msg) elif len(kwds) > 0: kwdstr = " ".join(list(kwds.keys())) raise ValueError( "Extra kwds passed to ErosionDeposition:{kwds}".format( kwds=kwdstr)) super().__init__( grid, m_sp=m_sp, n_sp=n_sp, F_f=F_f, v_s=v_s, dt_min=dt_min, discharge_field=discharge_field, ) # E/D specific inits. # K's and critical values can be floats, grid fields, or arrays # use setter for K defined below self.K = K self._sp_crit = return_array_at_node(grid, sp_crit) # Handle option for solver if solver == "basic": self.run_one_step = self.run_one_step_basic elif solver == "adaptive": self.run_one_step = self.run_with_adaptive_time_step_solver self._time_to_flat = np.zeros(grid.number_of_nodes) else: raise ValueError("Parameter 'solver' must be one of: " + "'basic', 'adaptive'")
def test_no_field(): mg = RasterModelGrid((10, 10)) with pytest.raises(FieldError): return_array_at_node(mg, "spam") with pytest.raises(FieldError): return_array_at_link(mg, "spam")
def __init__( self, grid, K=None, phi=None, v_s=None, m_sp=None, n_sp=None, sp_crit=0.0, F_f=0.0, discharge_field="surface_water__discharge", solver="basic", dt_min=DEFAULT_MINIMUM_TIME_STEP, **kwds ): """Initialize the ErosionDeposition model. Parameters ---------- grid : ModelGrid Landlab ModelGrid object K : float, field name, or array Erodibility for substrate (units vary). phi : float Sediment porosity [-]. v_s : float Effective settling velocity for chosen grain size metric [L/T]. m_sp : float Discharge exponent (units vary) n_sp : float Slope exponent (units vary) sp_crit : float, field name, or array Critical stream power to erode substrate [E/(TL^2)] F_f : float Fraction of eroded material that turns into "fines" that do not contribute to (coarse) sediment load. Defaults to zero. discharge_field : float, field name, or array Discharge [L^2/T]. solver : string Solver to use. Options at present include: (1) 'basic' (default): explicit forward-time extrapolation. Simple but will become unstable if time step is too large. (2) 'adaptive': adaptive time-step solver that estimates a stable step size based on the shortest time to "flattening" among all upstream-downstream node pairs. Examples --------- >>> import numpy as np >>> from landlab import RasterModelGrid >>> from landlab.components import FlowAccumulator >>> from landlab.components import DepressionFinderAndRouter >>> from landlab.components import ErosionDeposition >>> from landlab.components import FastscapeEroder >>> np.random.seed(seed = 5000) Define grid and initial topography: -5x5 grid with baselevel in the lower left corner -all other boundary nodes closed -Initial topography is plane tilted up to the upper right + noise >>> nr = 5 >>> nc = 5 >>> dx = 10 >>> mg = RasterModelGrid((nr, nc), xy_spacing=10.0) >>> _ = mg.add_zeros('node', 'topographic__elevation') >>> mg['node']['topographic__elevation'] += (mg.node_y/10 + ... mg.node_x/10 + np.random.rand(len(mg.node_y)) / 10) >>> mg.set_closed_boundaries_at_grid_edges(bottom_is_closed=True, ... left_is_closed=True, ... right_is_closed=True, ... top_is_closed=True) >>> mg.set_watershed_boundary_condition_outlet_id(0,\ mg['node']['topographic__elevation'], -9999.) >>> fsc_dt = 100. >>> ed_dt = 1. Check initial topography >>> mg.at_node['topographic__elevation'] # doctest: +NORMALIZE_WHITESPACE array([ 0.02290479, 1.03606698, 2.0727653 , 3.01126678, 4.06077707, 1.08157495, 2.09812694, 3.00637448, 4.07999597, 5.00969486, 2.04008677, 3.06621577, 4.09655859, 5.04809001, 6.02641123, 3.05874171, 4.00585786, 5.0595697 , 6.04425233, 7.05334077, 4.05922478, 5.0409473 , 6.07035008, 7.0038935 , 8.01034357]) Instantiate Fastscape eroder, flow router, and depression finder >>> fsc = FastscapeEroder(mg, K_sp=.001, m_sp=.5, n_sp=1) >>> fr = FlowAccumulator(mg, flow_director='D8') >>> df = DepressionFinderAndRouter(mg) Burn in an initial drainage network using the Fastscape eroder: >>> for x in range(100): ... fr.run_one_step() ... df.map_depressions() ... flooded = np.where(df.flood_status==3)[0] ... fsc.run_one_step(dt = fsc_dt, flooded_nodes=flooded) ... mg.at_node['topographic__elevation'][0] -= 0.001 #uplift Instantiate the E/D component: >>> ed = ErosionDeposition(mg, K=0.00001, phi=0.0, v_s=0.001, ... m_sp=0.5, n_sp = 1.0, sp_crit=0) Now run the E/D component for 2000 short timesteps: >>> for x in range(2000): #E/D component loop ... fr.run_one_step() ... df.map_depressions() ... flooded = np.where(df.flood_status==3)[0] ... ed.run_one_step(dt = ed_dt, flooded_nodes=flooded) ... mg.at_node['topographic__elevation'][0] -= 2e-4 * ed_dt Now we test to see if topography is right: >>> np.around(mg.at_node['topographic__elevation'], decimals=3) # doctest: +NORMALIZE_WHITESPACE array([-0.477, 1.036, 2.073, 3.011, 4.061, 1.082, -0.08 , -0.065, -0.054, 5.01 , 2.04 , -0.065, -0.065, -0.053, 6.026, 3.059, -0.054, -0.053, -0.035, 7.053, 4.059, 5.041, 6.07 , 7.004, 8.01 ]) """ if grid.at_node["flow__receiver_node"].size != grid.size("node"): msg = ( "A route-to-multiple flow director has been " "run on this grid. The landlab development team has not " "verified that ErosionDeposition is compatible with " "route-to-multiple methods. Please open a GitHub Issue " "to start this process." ) raise NotImplementedError(msg) super(ErosionDeposition, self).__init__( grid, m_sp=m_sp, n_sp=n_sp, phi=phi, F_f=F_f, v_s=v_s, dt_min=dt_min, discharge_field=discharge_field, ) self._grid = grid # store grid # E/D specific inits. # K's and critical values can be floats, grid fields, or arrays self.K = return_array_at_node(grid, K) self.sp_crit = return_array_at_node(grid, sp_crit) # Handle option for solver if solver == "basic": self.run_one_step = self.run_one_step_basic elif solver == "adaptive": self.run_one_step = self.run_with_adaptive_time_step_solver self.time_to_flat = np.zeros(grid.number_of_nodes) else: raise ValueError( "Parameter 'solver' must be one of: " + "'basic', 'adaptive'" )
def __init__(self, grid, surface="topographic__elevation", flow_director="FlowDirectorSteepest", runoff_rate=None, depression_finder=None, **kwargs): """ Initialize the FlowAccumulator component. Saves the grid, tests grid type, tests imput types and compatability for the flow_director and depression_finder keyword arguments, tests the argument of runoff_rate, and initializes new fields. """ super(FlowAccumulator, self).__init__(grid) # Keep a local reference to the grid self._grid = grid # Grid type testing self._is_raster = isinstance(self._grid, RasterModelGrid) self._is_Voroni = isinstance(self._grid, VoronoiDelaunayGrid) self.kwargs = kwargs # STEP 1: Testing of input values, supplied either in function call or # as part of the grid. self._test_water_inputs(grid, runoff_rate) # save elevations and node_cell_area to class properites. self.surface = surface self.surface_values = return_array_at_node(grid, surface) node_cell_area = self._grid.cell_area_at_node.copy() node_cell_area[self._grid.closed_boundary_nodes] = 0. self.node_cell_area = node_cell_area # STEP 2: # identify Flow Director method, save name, import and initialize the correct # flow director component if necessary self._add_director(flow_director) self._add_depression_finder(depression_finder) # This component will track of the following variables. # Attempt to create each, if they already exist, assign the existing # version to the local copy. # - drainage area at each node # - receiver of each node # - D array # - delta array # - missing nodes in stack. if "drainage_area" not in grid.at_node: self.drainage_area = grid.add_zeros("drainage_area", at="node", dtype=float) else: self.drainage_area = grid.at_node["drainage_area"] if "surface_water__discharge" not in grid.at_node: self.discharges = grid.add_zeros("surface_water__discharge", at="node", dtype=float) else: self.discharges = grid.at_node["surface_water__discharge"] if "flow__upstream_node_order" not in grid.at_node: self.upstream_ordered_nodes = grid.add_field( "flow__upstream_node_order", BAD_INDEX_VALUE * grid.ones(at="node", dtype=int), at="node", dtype=int, ) else: self.upstream_ordered_nodes = grid.at_node[ "flow__upstream_node_order"] if "flow__data_structure_delta" not in grid.at_node: self.delta_structure = grid.add_field( "flow__data_structure_delta", BAD_INDEX_VALUE * grid.ones(at="node", dtype=int), at="node", dtype=int, ) else: self.delta_structure = grid.at_node["flow__data_structure_delta"] if "flow__data_structure_D" not in grid.at_link: if self.flow_director.to_n_receivers == "many" and self._is_raster: # needs to be BAD_INDEX_VALUE self.D_structure = grid.add_field( "flow__data_structure_D", BAD_INDEX_VALUE * np.ones( (self._grid.number_of_links, 2), dtype=int), at="link", dtype=int, noclobber=False, ) else: # needs to be BAD_INDEX_VALUE self.D_structure = grid.add_field( "flow__data_structure_D", BAD_INDEX_VALUE * grid.ones(at="link"), at="link", dtype=int, ) else: self.D_structure = grid.at_link["flow__data_structure_D"] self.nodes_not_in_stack = True
def __init__( self, grid, surface="topographic__elevation", flow_director="FlowDirectorSteepest", runoff_rate=None, depression_finder=None, **kwargs ): """Initialize the FlowAccumulator component. Saves the grid, tests grid type, tests imput types and compatability for the flow_director and depression_finder keyword arguments, tests the argument of runoff_rate, and initializes new fields. """ super(FlowAccumulator, self).__init__(grid) # Keep a local reference to the grid # Grid type testing self._is_raster = isinstance(self._grid, RasterModelGrid) self._is_Voroni = isinstance(self._grid, VoronoiDelaunayGrid) self._is_Network = isinstance(self._grid, NetworkModelGrid) self._kwargs = kwargs # STEP 1: Testing of input values, supplied either in function call or # as part of the grid. self._test_water_inputs(grid, runoff_rate) # save elevations and node_cell_area to class properites. self._surface = surface self._surface_values = return_array_at_node(grid, surface) if self._is_Network: try: node_cell_area = self._grid.at_node["cell_area_at_node"] except FieldError: raise FieldError( "In order for the FlowAccumulator to work, the " "grid must have an at-node field called " "cell_area_at_node." ) else: node_cell_area = self._grid.cell_area_at_node.copy() node_cell_area[self._grid.closed_boundary_nodes] = 0.0 self._node_cell_area = node_cell_area # STEP 2: # identify Flow Director method, save name, import and initialize the correct # flow director component if necessary self._add_director(flow_director) self._add_depression_finder(depression_finder) # This component will track of the following variables. # Attempt to create each, if they already exist, assign the existing # version to the local copy. # - drainage area at each node # - receiver of each node # - delta array self.initialize_output_fields() self._drainage_area = grid.at_node["drainage_area"] self._discharges = grid.at_node["surface_water__discharge"] self._upstream_ordered_nodes = grid.at_node["flow__upstream_node_order"] if np.all(self._upstream_ordered_nodes == 0): self._upstream_ordered_nodes.fill(self._grid.BAD_INDEX) self._delta_structure = grid.at_node["flow__data_structure_delta"] if np.all(self._delta_structure == 0): self._delta_structure[:] = self._grid.BAD_INDEX self._D_structure = self._grid.BAD_INDEX * grid.ones(at="link", dtype=int) self._nodes_not_in_stack = True if len(self._kwargs) > 0: kwdstr = " ".join(list(self._kwargs.keys())) raise ValueError( "Extra kwargs passed to FlowAccumulator:{kwds}".format(kwds=kwdstr) )
def __init__(self, grid, K=None, phi=None, v_s=None, m_sp=None, n_sp=None, sp_crit=0.0, F_f=0.0, discharge_field='surface_water__discharge', solver='basic', dt_min=DEFAULT_MINIMUM_TIME_STEP, **kwds): """Initialize the ErosionDeposition model. Parameters ---------- grid : ModelGrid Landlab ModelGrid object K : float, field name, or array Erodibility for substrate (units vary). phi : float Sediment porosity [-]. v_s : float Effective settling velocity for chosen grain size metric [L/T]. m_sp : float Drainage area exponent (units vary) n_sp : float Slope exponent (units vary) sp_crit : float, field name, or array Critical stream power to erode substrate [E/(TL^2)] F_f : float Fraction of eroded material that turns into "fines" that do not contribute to (coarse) sediment load. Defaults to zero. discharge_field : float, field name, or array Discharge [L^2/T]. solver : string Solver to use. Options at present include: (1) 'basic' (default): explicit forward-time extrapolation. Simple but will become unstable if time step is too large. (2) 'adaptive': adaptive time-step solver that estimates a stable step size based on the shortest time to "flattening" among all upstream-downstream node pairs. Examples --------- >>> import numpy as np >>> from landlab import RasterModelGrid >>> from landlab.components.flow_routing import FlowRouter >>> from landlab.components import DepressionFinderAndRouter >>> from landlab.components import ErosionDeposition >>> from landlab.components import FastscapeEroder >>> np.random.seed(seed = 5000) Define grid and initial topography: -5x5 grid with baselevel in the lower left corner -all other boundary nodes closed -Initial topography is plane tilted up to the upper right + noise >>> nr = 5 >>> nc = 5 >>> dx = 10 >>> mg = RasterModelGrid((nr, nc), 10.0) >>> _ = mg.add_zeros('node', 'topographic__elevation') >>> mg['node']['topographic__elevation'] += (mg.node_y/10 + ... mg.node_x/10 + np.random.rand(len(mg.node_y)) / 10) >>> mg.set_closed_boundaries_at_grid_edges(bottom_is_closed=True, ... left_is_closed=True, ... right_is_closed=True, ... top_is_closed=True) >>> mg.set_watershed_boundary_condition_outlet_id(0,\ mg['node']['topographic__elevation'], -9999.) >>> fsc_dt = 100. >>> ed_dt = 1. Check initial topography >>> mg.at_node['topographic__elevation'] # doctest: +NORMALIZE_WHITESPACE array([ 0.02290479, 1.03606698, 2.0727653 , 3.01126678, 4.06077707, 1.08157495, 2.09812694, 3.00637448, 4.07999597, 5.00969486, 2.04008677, 3.06621577, 4.09655859, 5.04809001, 6.02641123, 3.05874171, 4.00585786, 5.0595697 , 6.04425233, 7.05334077, 4.05922478, 5.0409473 , 6.07035008, 7.0038935 , 8.01034357]) Instantiate Fastscape eroder, flow router, and depression finder >>> fsc = FastscapeEroder(mg, K_sp=.001, m_sp=.5, n_sp=1) >>> fr = FlowRouter(mg) #instantiate >>> df = DepressionFinderAndRouter(mg) Burn in an initial drainage network using the Fastscape eroder: >>> for x in range(100): ... fr.run_one_step() ... df.map_depressions() ... flooded = np.where(df.flood_status==3)[0] ... fsc.run_one_step(dt = fsc_dt, flooded_nodes=flooded) ... mg.at_node['topographic__elevation'][0] -= 0.001 #uplift Instantiate the E/D component: >>> ed = ErosionDeposition(mg, K=0.00001, phi=0.0, v_s=0.001, ... m_sp=0.5, n_sp = 1.0, sp_crit=0) Now run the E/D component for 2000 short timesteps: >>> for x in range(2000): #E/D component loop ... fr.run_one_step() ... df.map_depressions() ... flooded = np.where(df.flood_status==3)[0] ... ed.run_one_step(dt = ed_dt, flooded_nodes=flooded) ... mg.at_node['topographic__elevation'][0] -= 2e-4 * ed_dt Now we test to see if topography is right: >>> np.around(mg.at_node['topographic__elevation'], decimals=3) # doctest: +NORMALIZE_WHITESPACE array([-0.477, 1.036, 2.073, 3.011, 4.061, 1.082, -0.08 , -0.065, -0.054, 5.01 , 2.04 , -0.065, -0.065, -0.053, 6.026, 3.059, -0.054, -0.053, -0.035, 7.053, 4.059, 5.041, 6.07 , 7.004, 8.01 ]) """ if (grid.at_node['flow__receiver_node'].size != grid.size('node')): msg = ('A route-to-multiple flow director has been ' 'run on this grid. The landlab development team has not ' 'verified that ErosionDeposition is compatible with ' 'route-to-multiple methods. Please open a GitHub Issue ' 'to start this process.') raise NotImplementedError(msg) super(ErosionDeposition, self).__init__(grid, m_sp=m_sp, n_sp=n_sp, phi=phi, F_f=F_f, v_s=v_s, dt_min=dt_min, discharge_field=discharge_field) self._grid = grid #store grid # E/D specific inits. # K's and critical values can be floats, grid fields, or arrays self.K = return_array_at_node(grid, K) self.sp_crit = return_array_at_node(grid, sp_crit) # Handle option for solver if solver == 'basic': self.run_one_step = self.run_one_step_basic elif solver == 'adaptive': self.run_one_step = self.run_with_adaptive_time_step_solver self.time_to_flat = np.zeros(grid.number_of_nodes) else: raise ValueError("Parameter 'solver' must be one of: " + "'basic', 'adaptive'")
def K_br(self, new_val): self._K_br = return_array_at_node(self._grid, new_val)
def __init__( self, grid, soil_density=1330, water_density=997.9, C=12.0, N=0.85, gravity=constants.g, channel_bottom_sediment_grain__d50_diameter="channel_bottom_sediment_grain__d50_diameter", ): """Initialize the DimensionlessDischarge. Parameters ---------- soil_density : float, field name, or array, optional density of soil (kg/m^3) (defaults to 1330) water_density : float, optional density of water (kg/m^3) (defaults to 997.9) C : float, optional Numerator of the debris flow threshold equation; Empirically derived constant (See Tang et al. 2019). (defaults to 12.0) N : float, optional Exponent for slope in the denominator of the debris flow threshold equation; Empirically derived constant (See Tang et al. 2019). (defaults to 0.85) gravity : float, optional (defaults to scipy's standard acceleration of gravity) channel_bottom_sediment_grain__d50_diameter : float, field_name, or array defaults to the field name "channel_bottom_sediment_grain__d50_diameter" """ super().__init__(grid) # Store parameters # scalar parameters. self._c = C self._n = N self._water_density = water_density self._gravity = gravity # get topographic__elevation values and change into slope of a # stream segment self._stream_slopes = self._elevationToSlope() # both soil density and d50 can be float, array, or field name. self._soil_density = return_array_at_node(self.grid, soil_density) self._channel_bottom_sediment_grain__d50_diameter = return_array_at_node( self.grid, channel_bottom_sediment_grain__d50_diameter) # create output fields self.grid.add_zeros("dimensionless_discharge", at="node") self.grid.add_full("dimensionless_discharge_above_threshold", False, at="node", dtype=bool) self.grid.add_zeros("dimensionless_discharge_threshold", at="node", dtype=float) # calculate threshold values for each segment self._calc_threshold()
def __init__(self, grid, K_sed=None, K_br=None, F_f=None, phi=None, H_star=None, v_s=None, m_sp=None, n_sp=None, sp_crit_sed=None, sp_crit_br=None, discharge_field='surface_water__discharge', solver='basic', dt_min=DEFAULT_MINIMUM_TIME_STEP, **kwds): """Initialize the Space model. """ if (grid.at_node['flow__receiver_node'].size != grid.size('node')): msg = ('A route-to-multiple flow director has been ' 'run on this grid. The landlab development team has not ' 'verified that SPACE is compatible with ' 'route-to-multiple methods. Please open a GitHub Issue ' 'to start this process.') raise NotImplementedError(msg) super(Space, self).__init__(grid, m_sp=m_sp, n_sp=n_sp, phi=phi, F_f=F_f, v_s=v_s, dt_min=dt_min, discharge_field=discharge_field) self._grid = grid #store grid # space specific inits self.H_star = H_star if 'soil__depth' in grid.at_node: self.soil__depth = grid.at_node['soil__depth'] else: self.soil__depth = grid.add_zeros( 'soil__depth', at='node', dtype=float) if 'bedrock__elevation' in grid.at_node: self.bedrock__elevation = grid.at_node['bedrock__elevation'] else: self.bedrock__elevation = grid.add_zeros( 'bedrock__elevation', at='node', dtype=float) self.bedrock__elevation[:] = self.topographic__elevation -\ self.soil__depth self.Es = np.zeros(grid.number_of_nodes) self.Er = np.zeros(grid.number_of_nodes) #K's and critical values can be floats, grid fields, or arrays self.K_sed = return_array_at_node(grid, K_sed) self.K_br = return_array_at_node(grid, K_br) self.sp_crit_sed = return_array_at_node(grid, sp_crit_sed) self.sp_crit_br = return_array_at_node(grid, sp_crit_br) # Handle option for solver if solver == 'basic': self.run_one_step = self.run_one_step_basic elif solver == 'adaptive': self.run_one_step = self.run_with_adaptive_time_step_solver self.time_to_flat = np.zeros(grid.number_of_nodes) self.porosity_factor = 1.0 / (1.0 - self.phi) else: raise ValueError("Parameter 'solver' must be one of: " + "'basic', 'adaptive'")
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 __init__( self, grid, K_sp=0.001, threshold_sp=0.0, sp_type="set_mn", m_sp=0.5, n_sp=1.0, a_sp=None, b_sp=None, c_sp=None, channel_width_field=1.0, discharge_field="drainage_area", erode_flooded_nodes=True, ): """Initialize the StreamPowerEroder. Parameters ---------- grid : ModelGrid A grid. K_sp : float, array, or field name K in the stream power equation (units vary with other parameters). threshold_sp : positive float, array, or field name, optional The threshold stream power, below which no erosion occurs. This threshold is assumed to be in "stream power" units, i.e., if sp_type is 'Shear_stress', the value should be tau**a. sp_type : {'set_mn', 'Total', 'Unit', 'Shear_stress'} Controls how the law is implemented. If 'set_mn', use the supplied values of m_sp and n_sp. Else, component will derive values of m and n from supplied values of a_sp, b_sp, and c_sp, following Whipple and Tucker: * If ``'Total'``, ``m = a * c``, ``n = a``. * If ``'Unit'``, ``m = a * c *(1 - b)``, ``n = a``. * If ``'Shear_stress'``, ``m = 2 * a * c * (1 - b) / 3``, ``n = 2 * a / 3``. m_sp : float, optional m in the stream power equation (power on drainage area). Overridden if a_sp, b_sp, and c_sp are supplied. n_sp : float, optional, ~ 0.5<n_sp<4. n in the stream power equation (power on slope). Overridden if a_sp, b_sp, and c_sp are supplied. a_sp : float, optional The power on the SP/shear term to get the erosion rate; the "erosional process" term. Only used if sp_type is not 'set_mn'. b_sp : float, optional The power on discharge to get width; the "hydraulic geometry" term. Only used if sp_type in ('Unit', 'Shear_stress'). c_sp : float, optional The power on area to get discharge; the "basin hydology" term. Only used if sp_type is not 'set_mn'. channel_width_field : None, float, array, or field name, optional If not None, component will look for node-centered data describing channel width or if an array, will take the array as the channel widths. It will use the widths to implement incision ~ stream power per unit width. If sp_type is 'set_mn', follows the equation given above. If sp_type in ('Unit', 'Shear_stress'), the width value will be implemented directly. W has no effect if sp_type is 'Total'. discharge_field : float, field name, or array, optional Discharge [L^2/T]. The default is to use the grid field 'drainage_area'. To use custom spatially/temporally varying rainfall, use 'water__unit_flux_in' to specify water input to the FlowAccumulator and use "surface_water__discharge" for this keyword argument. erode_flooded_nodes : bool (optional) Whether erosion occurs in flooded nodes identified by a depression/lake mapper (e.g., DepressionFinderAndRouter). When set to false, the field *flood_status_code* must be present on the grid (this is created by the DepressionFinderAndRouter). Default True. """ super().__init__(grid) if "flow__receiver_node" in grid.at_node: if grid.at_node["flow__receiver_node"].size != grid.size("node"): msg = ( "A route-to-multiple flow director has been " "run on this grid. The landlab development team has not " "verified that StreamPowerEroder is compatible with " "route-to-multiple methods. Please open a GitHub Issue " "to start this process.") raise NotImplementedError(msg) if not erode_flooded_nodes: if "flood_status_code" not in self._grid.at_node: msg = ( "In order to not erode flooded nodes another component " "must create the field *flood_status_code*. You want to " "run a lake mapper/depression finder.") raise ValueError(msg) self._erode_flooded_nodes = erode_flooded_nodes self._A = return_array_at_node(grid, discharge_field) self._elevs = return_array_at_node(grid, "topographic__elevation") self._sp_crit = return_array_at_node(grid, threshold_sp) # use setter for K defined below self.K = K_sp assert np.all(self._sp_crit >= 0.0) if discharge_field == "drainage_area": self._use_Q = False else: self._use_Q = True if channel_width_field is None: self._use_W = False else: self._use_W = True self._W = return_array_at_node(grid, channel_width_field) if np.any(threshold_sp != 0.0): self._set_threshold = True # ^flag for sed_flux_dep_incision to see if the threshold was # manually set. else: self._set_threshold = False self._type = sp_type if sp_type == "set_mn": assert (float(m_sp) >= 0.0) and (float(n_sp) >= 0.0), "m and n must be positive" self._m = float(m_sp) self._n = float(n_sp) assert ( (a_sp is None) and (b_sp is None) and (c_sp is None) ), "If sp_type is 'set_mn', do not pass values for a, b, or c!" else: assert sp_type in ("Total", "Unit", "Shear_stress"), ( "sp_type not recognised. It must be 'set_mn', 'Total', " + "'Unit', or 'Shear_stress'.") assert (m_sp == 0.5 and n_sp == 1.0), "Do not set m and n if sp_type is not 'set_mn'!" assert float(a_sp) >= 0.0, "a must be positive" self._a = float(a_sp) if b_sp is not None: assert float(b_sp) >= 0.0, "b must be positive" self._b = float(b_sp) else: assert self._use_W, "b was not set" self._b = 0.0 if c_sp is not None: assert float(c_sp) >= 0.0, "c must be positive" self._c = float(c_sp) else: assert self._use_Q, "c was not set" self._c = 1.0 if self._type == "Total": self._n = self._a self._m = self._a * self._c # ==_a if use_Q elif self._type == "Unit": self._n = self._a self._m = self._a * self._c * (1.0 - self._b) # ^ ==_a iff use_Q&use_W etc elif self._type == "Shear_stress": self._m = 2.0 * self._a * self._c * (1.0 - self._b) / 3.0 self._n = 2.0 * self._a / 3.0 else: raise MissingKeyError( "Not enough information was provided on the exponents to use!" ) # m and n will always be set, but care needs to be taken to include Q # and W directly if appropriate self._stream_power_erosion = self._grid.zeros(centering="node") self._alpha = self._grid.zeros("node")
def __init__(self, grid, m_sp=None, n_sp=None, phi=None, F_f=None, v_s=None, discharge_field='surface_water__discharge', dt_min=DEFAULT_MINIMUM_TIME_STEP): """Initialize the ErosionDeposition model. Parameters ---------- grid : ModelGrid Landlab ModelGrid object m_sp : float Drainage area exponent (units vary) n_sp : float Slope exponent (units vary) phi : float Sediment porosity [-]. F_f : float Fraction of eroded material that turns into "fines" that do not contribute to (coarse) sediment load. Defaults to zero. v_s : float Effective settling velocity for chosen grain size metric [L/T]. discharge_field : float, field name, or array Discharge [L^2/T]. dt_min : float, optional Only applies when adaptive solver is used. Minimum timestep that adaptive solver will use when subdividing unstable timesteps. Default values is 0.001. [T]. """ super(_GeneralizedErosionDeposition, self).__init__(grid) self.flow_receivers = grid.at_node['flow__receiver_node'] self.stack = grid.at_node['flow__upstream_node_order'] self.topographic__elevation = grid.at_node['topographic__elevation'] self.slope = grid.at_node['topographic__steepest_slope'] self.link_to_reciever = grid.at_node['flow__link_to_receiver_node'] self.cell_area_at_node = grid.cell_area_at_node if isinstance(grid, RasterModelGrid): self.link_lengths = grid.length_of_d8 else: self.link_lengths = grid.length_of_link if 'sediment__flux' in grid.at_node: self.qs = grid.at_node['sediment__flux'] else: self.qs = grid.add_zeros('sediment__flux', at='node', dtype=float) self.q = return_array_at_node(grid, discharge_field) # Create arrays for sediment influx at each node, discharge to the # power "m", and deposition rate self.qs_in = np.zeros(grid.number_of_nodes) self.Q_to_the_m = np.zeros(grid.number_of_nodes) self.S_to_the_n = np.zeros(grid.number_of_nodes) self.depo_rate = np.zeros(grid.number_of_nodes) # store other constants self.m_sp = float(m_sp) self.n_sp = float(n_sp) self.phi = float(phi) self.v_s = float(v_s) self.dt_min = dt_min self.F_f = float(F_f) if phi >= 1.0: raise ValueError('Porosity must be < 1.0') if F_f > 1.0: raise ValueError('Fraction of fines must be <= 1.0') if phi < 0.0: raise ValueError('Porosity must be > 0.0') if F_f < 0.0: raise ValueError('Fraction of fines must be > 0.0')
def K(self, new_val): self._K = return_array_at_node(self._grid, new_val)
def __init__(self, grid, surface='topographic__elevation', flow_director='FlowDirectorSteepest', runoff_rate=None, depression_finder=None, **kwargs): """ Initialize the FlowAccumulator component. Saves the grid, tests grid type, tests imput types and compatability for the flow_director and depression_finder keyword arguments, tests the argument of runoff_rate, and initializes new fields. """ super(FlowAccumulator, self).__init__(grid) # Keep a local reference to the grid self._grid = grid # Grid type testing self._is_raster = isinstance(self._grid, RasterModelGrid) self._is_Voroni = isinstance(self._grid, VoronoiDelaunayGrid) self.kwargs = kwargs # STEP 1: Testing of input values, supplied either in function call or # as part of the grid. self._test_water_inputs(grid, runoff_rate) # save elevations and node_cell_area to class properites. self.surface = surface self.surface_values = return_array_at_node(grid, surface) node_cell_area = self._grid.cell_area_at_node.copy() node_cell_area[self._grid.closed_boundary_nodes] = 0. self.node_cell_area = node_cell_area # STEP 2: # identify Flow Director method, save name, import and initialize the correct # flow director component if necessary self._add_director(flow_director) self._add_depression_finder(depression_finder) # This component will track of the following variables. # Attempt to create each, if they already exist, assign the existing # version to the local copy. # - drainage area at each node # - receiver of each node # - D array # - delta array # - missing nodes in stack. try: self.drainage_area = grid.add_zeros('drainage_area', at='node', dtype=float) except FieldError: self.drainage_area = grid.at_node['drainage_area'] try: self.discharges = grid.add_zeros('surface_water__discharge', at='node', dtype=float) except FieldError: self.discharges = grid.at_node['surface_water__discharge'] try: self.upstream_ordered_nodes = grid.add_field('flow__upstream_node_order', BAD_INDEX_VALUE*grid.ones(at='node', dtype=int), at='node', dtype=int) except FieldError: self.upstream_ordered_nodes = grid.at_node[ 'flow__upstream_node_order'] try: self.delta_structure = grid.add_field('flow__data_structure_delta', BAD_INDEX_VALUE*grid.ones(at='node', dtype=int), at='node', dtype=int) except FieldError: self.delta_structure = grid.at_node['flow__data_structure_delta'] try: if self.flow_director.to_n_receivers == 'many' and self._is_raster: # needs to be BAD_INDEX_VALUE self.D_structure = grid.add_field('flow__data_structure_D', BAD_INDEX_VALUE*np.ones((self._grid.number_of_links, 2), dtype=int), at='link', dtype=int, noclobber=False) else: # needs to be BAD_INDEX_VALUE self.D_structure = grid.add_field('flow__data_structure_D', BAD_INDEX_VALUE*grid.ones(at='link'), at='link', dtype=int) except FieldError: self.D_structure = grid.at_link['flow__data_structure_D'] self.nodes_not_in_stack = True
def __init__( self, grid, m_sp, n_sp, F_f, v_s, discharge_field="surface_water__discharge", dt_min=DEFAULT_MINIMUM_TIME_STEP, ): """Initialize the GeneralizedErosionDeposition model. Parameters ---------- grid : ModelGrid Landlab ModelGrid object m_sp : float Discharge exponent (units vary) n_sp : float Slope exponent (units vary) F_f : float Fraction of eroded material that turns into "fines" that do not contribute to (coarse) sediment load. Defaults to zero. v_s : float Effective settling velocity for chosen grain size metric [L/T]. discharge_field : float, field name, or array Discharge [L^2/T]. dt_min : float, optional Only applies when adaptive solver is used. Minimum timestep that adaptive solver will use when subdividing unstable timesteps. Default values is 0.001. [T]. """ super().__init__(grid) self._flow_receivers = grid.at_node["flow__receiver_node"] self._stack = grid.at_node["flow__upstream_node_order"] self._topographic__elevation = grid.at_node["topographic__elevation"] self._slope = grid.at_node["topographic__steepest_slope"] self._link_to_reciever = grid.at_node["flow__link_to_receiver_node"] self._cell_area_at_node = grid.cell_area_at_node if isinstance(grid, RasterModelGrid): self._link_lengths = grid.length_of_d8 else: self._link_lengths = grid.length_of_link self.initialize_output_fields() self._qs = grid.at_node["sediment__flux"] self._q = return_array_at_node(grid, discharge_field) # Create arrays for sediment influx at each node, discharge to the # power "m", and deposition rate self._qs_in = np.zeros(grid.number_of_nodes) self._Q_to_the_m = np.zeros(grid.number_of_nodes) self._S_to_the_n = np.zeros(grid.number_of_nodes) self._depo_rate = np.zeros(grid.number_of_nodes) # store other constants self._m_sp = float(m_sp) self._n_sp = float(n_sp) self._v_s = float(v_s) self._dt_min = dt_min self._F_f = float(F_f) if F_f > 1.0: raise ValueError("Fraction of fines must be <= 1.0") if F_f < 0.0: raise ValueError("Fraction of fines must be > 0.0")
def __init__( self, grid, surface="topographic__elevation", flow_director="FlowDirectorSteepest", runoff_rate=None, depression_finder=None, **kwargs ): """Initialize the FlowAccumulator component. Saves the grid, tests grid type, tests imput types and compatability for the flow_director and depression_finder keyword arguments, tests the argument of runoff_rate, and initializes new fields. """ super(FlowAccumulator, self).__init__(grid) # Keep a local reference to the grid self._grid = grid # Grid type testing self._is_raster = isinstance(self._grid, RasterModelGrid) self._is_Voroni = isinstance(self._grid, VoronoiDelaunayGrid) self._is_Network = isinstance(self._grid, NetworkModelGrid) self.kwargs = kwargs # STEP 1: Testing of input values, supplied either in function call or # as part of the grid. self._test_water_inputs(grid, runoff_rate) # save elevations and node_cell_area to class properites. self.surface = surface self.surface_values = return_array_at_node(grid, surface) if self._is_Network: try: node_cell_area = self._grid.at_node["cell_area_at_node"] except FieldError: raise FieldError( "In order for the FlowAccumulator to work, the " "grid must have an at-node field called " "cell_area_at_node." ) else: node_cell_area = self._grid.cell_area_at_node.copy() node_cell_area[self._grid.closed_boundary_nodes] = 0.0 self.node_cell_area = node_cell_area # STEP 2: # identify Flow Director method, save name, import and initialize the correct # flow director component if necessary self._add_director(flow_director) self._add_depression_finder(depression_finder) # This component will track of the following variables. # Attempt to create each, if they already exist, assign the existing # version to the local copy. # - drainage area at each node # - receiver of each node # - D array # - delta array # - missing nodes in stack. if "drainage_area" not in grid.at_node: self.drainage_area = grid.add_zeros("drainage_area", at="node", dtype=float) else: self.drainage_area = grid.at_node["drainage_area"] if "surface_water__discharge" not in grid.at_node: self.discharges = grid.add_zeros( "surface_water__discharge", at="node", dtype=float ) else: self.discharges = grid.at_node["surface_water__discharge"] if "flow__upstream_node_order" not in grid.at_node: self.upstream_ordered_nodes = grid.add_field( "flow__upstream_node_order", BAD_INDEX_VALUE * grid.ones(at="node", dtype=int), at="node", dtype=int, ) else: self.upstream_ordered_nodes = grid.at_node["flow__upstream_node_order"] if "flow__data_structure_delta" not in grid.at_node: self.delta_structure = grid.add_field( "flow__data_structure_delta", BAD_INDEX_VALUE * grid.ones(at="node", dtype=int), at="node", dtype=int, ) else: self.delta_structure = grid.at_node["flow__data_structure_delta"] try: D = BAD_INDEX_VALUE * grid.ones(at="link", dtype=int) D_structure = np.array([D], dtype=object) self.D_structure = grid.add_field( "flow__data_structure_D", D_structure, at="grid", dtype=object, noclobber=False, ) except FieldError: self.D_structure = grid.at_grid["flow__data_structure_D"] self.nodes_not_in_stack = True
def __init__(self, grid, K_sed=None, K_br=None, F_f=None, phi=None, H_star=None, v_s=None, m_sp=None, n_sp=None, sp_crit_sed=None, sp_crit_br=None, discharge_field='surface_water__discharge', solver='basic', dt_min=DEFAULT_MINIMUM_TIME_STEP, **kwds): """Initialize the Space model. """ if (grid.at_node['flow__receiver_node'].size != grid.size('node')): msg = ('A route-to-multiple flow director has been ' 'run on this grid. The landlab development team has not ' 'verified that SPACE is compatible with ' 'route-to-multiple methods. Please open a GitHub Issue ' 'to start this process.') raise NotImplementedError(msg) super(Space, self).__init__(grid, m_sp=m_sp, n_sp=n_sp, phi=phi, F_f=F_f, v_s=v_s, dt_min=dt_min, discharge_field=discharge_field) self._grid = grid #store grid # space specific inits self.H_star = H_star if 'soil__depth' in grid.at_node: self.soil__depth = grid.at_node['soil__depth'] else: self.soil__depth = grid.add_zeros('soil__depth', at='node', dtype=float) if 'bedrock__elevation' in grid.at_node: self.bedrock__elevation = grid.at_node['bedrock__elevation'] else: self.bedrock__elevation = grid.add_zeros('bedrock__elevation', at='node', dtype=float) self.bedrock__elevation[:] = self.topographic__elevation -\ self.soil__depth self.Es = np.zeros(grid.number_of_nodes) self.Er = np.zeros(grid.number_of_nodes) #K's and critical values can be floats, grid fields, or arrays self.K_sed = return_array_at_node(grid, K_sed) self.K_br = return_array_at_node(grid, K_br) self.sp_crit_sed = return_array_at_node(grid, sp_crit_sed) self.sp_crit_br = return_array_at_node(grid, sp_crit_br) # Handle option for solver if solver == 'basic': self.run_one_step = self.run_one_step_basic elif solver == 'adaptive': self.run_one_step = self.run_with_adaptive_time_step_solver self.time_to_flat = np.zeros(grid.number_of_nodes) self.porosity_factor = 1.0 / (1.0 - self.phi) else: raise ValueError("Parameter 'solver' must be one of: " + "'basic', 'adaptive'")
def rock_id(self, rock_id): return_array_at_node(self._grid, rock_id) # verify that this will work. # verify that all rock types are valid self._rock_id = rock_id
def __init__( self, grid, surface="topographic__elevation", method="D8", fill_flat=False, ignore_overfill=False, ): """Initialise the component. Parameters ---------- grid : ModelGrid A grid. surface : field name at node or array of length node The surface to fill. method : {'Steepest', 'D8'} Whether or not to recognise diagonals as valid flow paths, if a raster. Otherwise, no effect. fill_flat : bool If True, pits will be filled to perfectly horizontal. If False, the new surface will be slightly inclined (at machine precision) to give steepest descent flow paths to the outlet, once they are calculated. ignore_overfill : bool If True, suppresses the Error that would normally be raised during creation of a gentle incline on a fill surface (i.e., if not fill_flat). Typically this would happen on a synthetic DEM where more than one outlet is possible at the same elevation. If True, the was_there_overfill property can still be used to see if this has occurred. """ if "flow__receiver_node" in grid.at_node: if grid.at_node["flow__receiver_node"].size != grid.size("node"): msg = ( "A route-to-multiple flow director has been " "run on this grid. The landlab development team has not " "verified that SinkFillerBarnes is compatible with " "route-to-multiple methods. Please open a GitHub Issue " "to start this process." ) raise NotImplementedError(msg) # Most of the functionality of this component is directly inherited # from SinkFillerBarnes, so super().__init__( grid, surface=surface, method=method, fill_flat=fill_flat, fill_surface=surface, redirect_flow_steepest_descent=False, reaccumulate_flow=False, ignore_overfill=ignore_overfill, track_lakes=True, ) # note we will always track the fills, since we're only doing this # once... Likewise, no need for flow routing; this is not going to # get used dynamically. self._supplied_surface = return_array_at_node(grid, surface).copy() # create the only new output field: self._sed_fill_depth = self._grid.add_zeros( "node", "sediment_fill__depth", clobber=True )
def test_no_field(): mg = RasterModelGrid((10, 10)) with pytest.raises(FieldError): return_array_at_node(mg, "spam")
def __init__( self, grid, K_sed=0.02, K_br=0.02, F_f=0.0, phi=0.3, H_star=0.1, v_s=1.0, m_sp=0.5, n_sp=1.0, sp_crit_sed=0.0, sp_crit_br=0.0, discharge_field="surface_water__discharge", solver="basic", erode_flooded_nodes=True, dt_min=DEFAULT_MINIMUM_TIME_STEP, ): """Initialize the Space model. Parameters ---------- grid : ModelGrid Landlab ModelGrid object K_sed : float, field name, or array Erodibility for sediment (units vary). K_br : float, field name, or array Erodibility for bedrock (units vary). F_f : float Fraction of permanently suspendable fines in bedrock [-]. phi : float Sediment porosity [-]. H_star : float Sediment thickness required for full entrainment [L]. v_s : float Effective settling velocity for chosen grain size metric [L/T]. m_sp : float Drainage area exponent (units vary) n_sp : float Slope exponent (units vary) sp_crit_sed : float, field name, or array Critical stream power to erode sediment [E/(TL^2)] sp_crit_br : float, field name, or array Critical stream power to erode rock [E/(TL^2)] discharge_field : float, field name, or array Discharge [L^2/T]. The default is to use the grid field 'surface_water__discharge', which is simply drainage area multiplied by the default rainfall rate (1 m/yr). To use custom spatially/temporally varying rainfall, use 'water__unit_flux_in' to specify water input to the FlowAccumulator. erode_flooded_nodes : bool (optional) Whether erosion occurs in flooded nodes identified by a depression/lake mapper (e.g., DepressionFinderAndRouter). When set to false, the field *flood_status_code* must be present on the grid (this is created by the DepressionFinderAndRouter). Default True. solver : string Solver to use. Options at present include: (1) 'basic' (default): explicit forward-time extrapolation. Simple but will become unstable if time step is too large. (2) 'adaptive': subdivides global time step as needed to prevent slopes from reversing and alluvium from going negative. """ if grid.at_node["flow__receiver_node"].size != grid.size("node"): msg = ("A route-to-multiple flow director has been " "run on this grid. The landlab development team has not " "verified that SPACE is compatible with " "route-to-multiple methods. Please open a GitHub Issue " "to start this process.") raise NotImplementedError(msg) super(Space, self).__init__( grid, m_sp=m_sp, n_sp=n_sp, phi=phi, F_f=F_f, v_s=v_s, dt_min=dt_min, discharge_field=discharge_field, erode_flooded_nodes=erode_flooded_nodes, ) # space specific inits self._H_star = H_star self._soil__depth = grid.at_node["soil__depth"] if "bedrock__elevation" in grid.at_node: self._bedrock__elevation = grid.at_node["bedrock__elevation"] else: self._bedrock__elevation = grid.add_zeros("bedrock__elevation", at="node", dtype=float) self._bedrock__elevation[:] = (self._topographic__elevation - self._soil__depth) self._Es = np.zeros(grid.number_of_nodes) self._Er = np.zeros(grid.number_of_nodes) # K's and critical values can be floats, grid fields, or arrays # use setters defined below self.K_sed = K_sed self.K_br = K_br self._sp_crit_sed = return_array_at_node(grid, sp_crit_sed) self._sp_crit_br = return_array_at_node(grid, sp_crit_br) # Handle option for solver if solver == "basic": self.run_one_step = self.run_one_step_basic elif solver == "adaptive": self.run_one_step = self.run_with_adaptive_time_step_solver self._time_to_flat = np.zeros(grid.number_of_nodes) self._porosity_factor = 1.0 / (1.0 - self._phi) else: raise ValueError("Parameter 'solver' must be one of: " + "'basic', 'adaptive'")
def __init__( self, grid, K_sp=0.001, m_sp=0.5, n_sp=1.0, threshold_sp=0.0, discharge_field="drainage_area", erode_flooded_nodes=True, ): """Initialize the Fastscape stream power component. Note: a timestep, dt, can no longer be supplied to this component through the input file. It must instead be passed directly to the run method. Parameters ---------- grid : ModelGrid A grid. K_sp : float, array, or field name K in the stream power equation (units vary with other parameters). m_sp : float, optional m in the stream power equation (power on drainage area). n_sp : float, optional n in the stream power equation (power on slope). threshold_sp : float, array, or field name Erosion threshold in the stream power equation. discharge_field : float, field name, or array, optional Discharge [L^2/T]. The default is to use the grid field 'drainage_area'. To use custom spatially/temporally varying rainfall, use 'water__unit_flux_in' to specify water input to the FlowAccumulator and use "surface_water__discharge" for this keyword argument. erode_flooded_nodes : bool (optional) Whether erosion occurs in flooded nodes identified by a depression/lake mapper (e.g., DepressionFinderAndRouter). When set to false, the field *flood_status_code* must be present on the grid (this is created by the DepressionFinderAndRouter). Default True. """ super().__init__(grid) if "flow__receiver_node" in grid.at_node: if grid.at_node["flow__receiver_node"].size != grid.size("node"): msg = ( "A route-to-multiple flow director has been " "run on this grid. The landlab development team has not " "verified that FastscapeEroder is compatible with " "route-to-multiple methods. Please open a GitHub Issue " "to start this process.") raise NotImplementedError(msg) if not erode_flooded_nodes: if "flood_status_code" not in self._grid.at_node: msg = ( "In order to not erode flooded nodes another component " "must create the field *flood_status_code*. You want to " "run a lake mapper/depression finder.") raise ValueError(msg) self._erode_flooded_nodes = erode_flooded_nodes # use setter for K defined below self.K = K_sp self._m = float(m_sp) self._n = float(n_sp) if isinstance(threshold_sp, (float, int)): self._thresholds = float(threshold_sp) else: self._thresholds = return_array_at_node(grid, threshold_sp) self._A = return_array_at_node(grid, discharge_field) # make storage variables self._A_to_the_m = grid.zeros(at="node") self._alpha = grid.empty(at="node")
def K_sed(self, new_val): self._K_sed = return_array_at_node(self._grid, new_val)
def __init__( self, grid, m_sp=None, n_sp=None, phi=None, F_f=None, v_s=None, discharge_field="surface_water__discharge", dt_min=DEFAULT_MINIMUM_TIME_STEP, ): """Initialize the ErosionDeposition model. Parameters ---------- grid : ModelGrid Landlab ModelGrid object m_sp : float Discharge exponent (units vary) n_sp : float Slope exponent (units vary) phi : float Sediment porosity [-]. F_f : float Fraction of eroded material that turns into "fines" that do not contribute to (coarse) sediment load. Defaults to zero. v_s : float Effective settling velocity for chosen grain size metric [L/T]. discharge_field : float, field name, or array Discharge [L^2/T]. dt_min : float, optional Only applies when adaptive solver is used. Minimum timestep that adaptive solver will use when subdividing unstable timesteps. Default values is 0.001. [T]. """ super(_GeneralizedErosionDeposition, self).__init__(grid) self.flow_receivers = grid.at_node["flow__receiver_node"] self.stack = grid.at_node["flow__upstream_node_order"] self.topographic__elevation = grid.at_node["topographic__elevation"] self.slope = grid.at_node["topographic__steepest_slope"] self.link_to_reciever = grid.at_node["flow__link_to_receiver_node"] self.cell_area_at_node = grid.cell_area_at_node if isinstance(grid, RasterModelGrid): self.link_lengths = grid.length_of_d8 else: self.link_lengths = grid.length_of_link if "sediment__flux" in grid.at_node: self.qs = grid.at_node["sediment__flux"] else: self.qs = grid.add_zeros("sediment__flux", at="node", dtype=float) self.q = return_array_at_node(grid, discharge_field) # Create arrays for sediment influx at each node, discharge to the # power "m", and deposition rate self.qs_in = np.zeros(grid.number_of_nodes) self.Q_to_the_m = np.zeros(grid.number_of_nodes) self.S_to_the_n = np.zeros(grid.number_of_nodes) self.depo_rate = np.zeros(grid.number_of_nodes) # store other constants self.m_sp = float(m_sp) self.n_sp = float(n_sp) self.phi = float(phi) self.v_s = float(v_s) self.dt_min = dt_min self.F_f = float(F_f) if phi >= 1.0: raise ValueError("Porosity must be < 1.0") if F_f > 1.0: raise ValueError("Fraction of fines must be <= 1.0") if phi < 0.0: raise ValueError("Porosity must be > 0.0") if F_f < 0.0: raise ValueError("Fraction of fines must be > 0.0")