def __init__(self, modern_dem_name, outlet_id, chi_mask_dem_name=None, from_file=None): """Initialize MetricCalculator with names of postglacial and modern DEMs.""" if from_file is None: # Read and remember the modern DEM (whether data or model) (self.grid, self.z) = self.read_topography(modern_dem_name) #print self.grid.x_of_node self.grid.set_watershed_boundary_condition_outlet_id(outlet_id, self.z, nodata_value=-9999) # Instantiate and run a FlowRouter and lake filler, so we get # drainage area for cumulative-area statistic, and also fields for chi. fr = FlowRouter(self.grid) dfr = DepressionFinderAndRouter(self.grid) fr.route_flow() dfr.map_depressions() # Remember modern drainage area grid self.area = self.grid.at_node['drainage_area'] # Instantiate a ChiFinder for chi-index self.chi_finder = ChiFinder(self.grid, min_drainage_area=10000., reference_concavity=0.5) core_nodes = np.zeros(self.area.shape, dtype=bool) core_nodes[self.grid.core_nodes] = True # Read and remember the MASK, if provided if chi_mask_dem_name is None: self.mask = (self.area>1e5) self.till_mask = np.zeros(self.mask.shape, dtype=bool) self.till_mask[self.grid.core_nodes] = 1 else: (self.mask_grid, zmask) = self.read_topography(chi_mask_dem_name) mask = (zmask>0)*1 self.mask = (self.area>1e5)*(mask==1) mask_bool = (zmask>0) self.till_mask = np.zeros(self.mask.shape, dtype=bool) self.till_mask[mask_bool*core_nodes] = 1 # # Create dictionary to contain metrics self.metric = {} else: with open(from_file, 'r') as f: metrics = load(f) self.modern_dem_name = metrics.pop('Topo file') self.metric = metrics fn_split = from_file.split('.') fn_split[-1] = 'chi' fn_split.append('txt') chi_filename = '.'.join(fn_split) self.density_chi = np.loadtxt(chi_filename)
def __init__(self, input_file=None, params=None): """Initialize the StochasticDischargeHortonianModel.""" # Call ErosionModel's init super(StochasticDischargeHortonianModel, self).__init__(input_file=input_file, params=params) # Instantiate components self.flow_router = FlowRouter(self.grid, **self.params) self.lake_filler = DepressionFinderAndRouter(self.grid, **self.params) self.rain_generator = \ PrecipitationDistribution(delta_t=self.params['dt'], total_time=self.params['run_duration'], **self.params) # Add a field for discharge if 'surface_water__discharge' not in self.grid.at_node: self.grid.add_zeros('node', 'surface_water__discharge') self.discharge = self.grid.at_node['surface_water__discharge'] # Get the infiltration-capacity parameter self.infilt = self.params['infiltration_capacity'] # Run flow routing and lake filler (only once, because we are not # not changing topography) self.flow_router.run_one_step() self.lake_filler.map_depressions()
def initialize(self, input_stream=None): """ The BMI-style initialize method takes an optional input_stream parameter, which may be either a ModelParameterDictionary object or an input stream from which a ModelParameterDictionary can read values. """ # Create a ModelParameterDictionary for the inputs if input_stream is None: inputs = None elif type(input_stream) == ModelParameterDictionary: inputs = input_stream else: inputs = ModelParameterDictionary(input_stream) # Make sure the grid includes elevation data. This means either: # 1. The grid has a node field called 'topographic__elevation', or # 2. The input file has an item called 'ELEVATION_FIELD_NAME' *and* # a field by this name exists in the grid. try: self._elev = self._grid.at_node["topographic__elevation"] except FieldError: try: self.topo_field_name = inputs.read_string("ELEVATION_" + "FIELD_NAME") except AttributeError: print("Error: Because your grid does not have a node field") print('called "topographic__elevation", you need to pass the') print("name of a text input file or ModelParameterDictionary,") print("and this file or dictionary needs to include the name") print("of another field in your grid that contains your") print("elevation data.") raise AttributeError except MissingKeyError: print("Error: Because your grid does not have a node field") print('called "topographic__elevation", your input file (or') print("ModelParameterDictionary) must include an entry with") print('the key "ELEVATION_FIELD_NAME", which gives the name') print("of a field in your grid that contains your elevation") print("data.") raise MissingKeyError("ELEVATION_FIELD_NAME") try: self._elev = self._grid.at_node[self.topo_field_name] except AttributeError: print( "Your grid does not seem to have a node field called", self.topo_field_name, ) else: self.topo_field_name = "topographic__elevation" # create the only new output field: self.sed_fill_depth = self._grid.add_zeros("node", "sediment_fill__depth", noclobber=False) self._lf = DepressionFinderAndRouter(self._grid, routing=self._routing) self._fr = FlowAccumulator(self._grid, flow_director=self._routing)
def __init__(self, input_file=None, params=None): """Initialize the LinearDiffusionModel.""" # Call ErosionModel's init super(DrainageAreaModel, self).__init__(input_file=input_file, params=params) # Instantiate a FlowRouter and DepressionFinderAndRouter components self.flow_router = FlowRouter(self.grid, **self.params) self.lake_filler = DepressionFinderAndRouter(self.grid, **self.params)
def test_pits_as_IDs(dans_grid3): """ Smoke test for passing specific IDs, not an array, to the mapper. """ dans_grid3.fr.run_one_step() pits = np.nonzero(dans_grid3.mg.at_node["flow__sink_flag"])[0] lf = DepressionFinderAndRouter(dans_grid3.mg, pits=pits) lf.map_depressions() assert dans_grid3.mg.at_node["drainage_area"] == approx(dans_grid3.A_new)
def test_filling_supplied_pits(dans_grid3): """ Test the filler without rereouting, but confusingly, where there *is* aready routing information available! Also tests the supply of an array for 'pits' """ dans_grid3.fr.run_one_step() lf = DepressionFinderAndRouter( dans_grid3.mg, reroute_flow=False, pits="flow__sink_flag" ) lf.map_depressions() assert_array_equal(dans_grid3.mg.at_node["flow__receiver_node"], dans_grid3.r_old)
def test_D8_D4_fill(d4_grid): """ Tests the functionality of D4 filling. """ lfD8 = DepressionFinderAndRouter( d4_grid.mg1, pits=None, routing="D8", reroute_flow=False ) lfD4 = DepressionFinderAndRouter( d4_grid.mg2, pits=None, routing="D4", reroute_flow=False ) lfD8.map_depressions() lfD4.map_depressions() assert lfD8.number_of_lakes == 1 assert lfD4.number_of_lakes == 3 correct_D8_lake_map = np.empty(7 * 7, dtype=int) correct_D8_lake_map.fill(XX) correct_D8_lake_map[d4_grid.lake_nodes] = 10 correct_D4_lake_map = correct_D8_lake_map.copy() correct_D4_lake_map[d4_grid.lake_nodes[5:]] = 32 correct_D4_lake_map[d4_grid.lake_nodes[-2]] = 38 correct_D8_depths = np.zeros(7 * 7, dtype=float) correct_D8_depths[d4_grid.lake_nodes] = 2.0 correct_D4_depths = correct_D8_depths.copy() correct_D4_depths[d4_grid.lake_nodes[5:]] = 4.0 correct_D4_depths[d4_grid.lake_nodes[-2]] = 3.0 assert_array_equal(lfD8.lake_map, correct_D8_lake_map) assert_array_equal(lfD4.lake_map, correct_D4_lake_map) assert d4_grid.mg1.at_node["depression__depth"] == approx(correct_D8_depths) assert d4_grid.mg2.at_node["depression__depth"] == approx(correct_D4_depths)
def test_filling_alone(dans_grid3): """ Test the filler alone, w/o supplying information on the pits. Setting the the *pits* parameter to None causes the mapper to look for pits using its _find_pits method. """ lf = DepressionFinderAndRouter(dans_grid3.mg, reroute_flow=False, pits=None) assert lf._user_supplied_pits is None lf.map_depressions() assert_array_equal( dans_grid3.mg.at_node["flow__receiver_node"], XX * np.ones(49, dtype=int) ) assert_array_equal(lf.depression_outlet_map, dans_grid3.depr_outlet_target)
def __init__(self, grid, routing_method): self._grid = grid if routing_method == "D8": self.fd = FlowDirectorD8(self._grid) elif routing_method == "Steepest": self.fd = FlowDirectorSteepest(self._grid) else: raise ValueError("routing_method must be either D8 or Steepest.") self.fa = FlowAccumulator( self._grid, surface="topographic__elevation", flow_director=self.fd, runoff_rate="average_surface_water__specific_discharge", ) self.lmb = LakeMapperBarnes( self._grid, method=routing_method, fill_flat=False, surface="topographic__elevation", fill_surface="topographic__elevation", redirect_flow_steepest_descent=False, reaccumulate_flow=False, track_lakes=False, ignore_overfill=True, ) self.dfr = DepressionFinderAndRouter(self._grid)
def test_find_lowest_node_on_lake_perimeter_c(): """ Ensures the key functionality of the cfunc is working. """ mg = RasterModelGrid((7, 7), xy_spacing=0.5) z = mg.add_field("node", "topographic__elevation", mg.node_x.copy()) z += 0.01 * mg.node_y mg.at_node["topographic__elevation"].reshape(mg.shape)[2:5, 2:5] *= 0.1 fr = FlowAccumulator(mg, flow_director="D8") fr.run_one_step() # the flow "gets stuck" in the hole df = DepressionFinderAndRouter(mg) node_nbrs = df._node_nbrs flood_status = df.flood_status elev = df._elev BIG_ELEV = df._BIG_ELEV nodes_this_depression = mg.zeros("node", dtype=int) nodes_this_depression[0] = 16 pit_count = 1 assert find_lowest_node_on_lake_perimeter_c(node_nbrs, flood_status, elev, nodes_this_depression, pit_count, BIG_ELEV) == (23, 1) nodes_this_depression[1] = 8 pit_count = 2 assert find_lowest_node_on_lake_perimeter_c(node_nbrs, flood_status, elev, nodes_this_depression, pit_count, BIG_ELEV) == (0, 2)
def __init__(self, input_file=None, params=None): """Initialize the LinearDiffusionModel.""" # Call ErosionModel's init super(EffectiveDrainageAreaModel, self).__init__(input_file=input_file, params=params) # Instantiate a FlowRouter and DepressionFinderAndRouter components self.flow_router = FlowRouter(self.grid, **self.params) self.lake_filler = DepressionFinderAndRouter(self.grid, **self.params) # Add a field for effective drainage area self.eff_area = self.grid.add_zeros('node', 'effective_drainage_area') # Get the effective-area parameter self.sat_param = self.params['saturation_area_scale']
def __init__(self, input_file=None, params=None): """Initialize the StreamPowerThresholdModel.""" # Call ErosionModel's init super(StreamPowerThresholdModel, self).__init__(input_file=input_file, params=params) # Instantiate a FlowRouter and DepressionFinderAndRouter components self.flow_router = FlowRouter(self.grid, **self.params) self.lake_filler = DepressionFinderAndRouter(self.grid, **self.params) # Instantiate a FastscapeEroder component self.eroder = StreamPowerSmoothThresholdEroder( self.grid, K_sp=self.params['K_sp'], threshold_sp=self.params['threshold_sp'])
def d4_grid(): """Test functionality of routing when D4 is specified. The elevation field in this test looks like:: 1 2 3 4 5 6 7 1 2 3 0 5 0 7 1 2 3 4 0 0 7 1 2 3 0 5 6 7 1 2 0 0 0 6 7 1 2 3 0 5 6 7 1 2 3 4 5 6 7 """ mg1 = RasterModelGrid(7, 7, 1.) mg2 = RasterModelGrid(7, 7, 1.) z = mg1.node_x.copy() + 1. lake_nodes = np.array([10, 16, 17, 18, 24, 32, 33, 38, 40]) z[lake_nodes] = 0. mg1.add_field("node", "topographic__elevation", z, units="-") mg2.add_field("node", "topographic__elevation", z, units="-") frD8 = FlowAccumulator(mg1, flow_director='D8') frD4 = FlowAccumulator(mg2, flow_director='D4') lfD8 = DepressionFinderAndRouter(mg1, routing="D8") lfD4 = DepressionFinderAndRouter(mg2, routing="D4") class DansGrid(object): pass d4_grid = DansGrid() d4_grid.mg1 = mg1 d4_grid.mg2 = mg2 d4_grid.z = z d4_grid.lake_nodes = lake_nodes d4_grid.frD8 = frD8 d4_grid.frD4 = frD4 d4_grid.lfD8 = lfD8 d4_grid.lfD4 = lfD4 return d4_grid
def test_route_to_multiple_error_raised(): mg = RasterModelGrid((10, 10)) z = mg.add_zeros("topographic__elevation", at="node") z += mg.x_of_node + mg.y_of_node fa = FlowAccumulator(mg, flow_director="MFD") fa.run_one_step() with pytest.raises(NotImplementedError): DepressionFinderAndRouter(mg)
def initialize(self, input_stream=None): """ The BMI-style initialize method takes an optional input_stream parameter, which may be either a ModelParameterDictionary object or an input stream from which a ModelParameterDictionary can read values. """ # Create a ModelParameterDictionary for the inputs if input_stream is None: inputs = None elif type(input_stream) == ModelParameterDictionary: inputs = input_stream else: inputs = ModelParameterDictionary(input_stream) # Make sure the grid includes elevation data. This means either: # 1. The grid has a node field called 'topographic__elevation', or # 2. The input file has an item called 'ELEVATION_FIELD_NAME' *and* # a field by this name exists in the grid. try: self._elev = self._grid.at_node["topographic__elevation"] except FieldError: try: self.topo_field_name = inputs.read_string("ELEVATION_" + "FIELD_NAME") except AttributeError: print("Error: Because your grid does not have a node field") print('called "topographic__elevation", you need to pass the') print("name of a text input file or ModelParameterDictionary,") print("and this file or dictionary needs to include the name") print("of another field in your grid that contains your") print("elevation data.") raise AttributeError except MissingKeyError: print("Error: Because your grid does not have a node field") print('called "topographic__elevation", your input file (or') print("ModelParameterDictionary) must include an entry with") print('the key "ELEVATION_FIELD_NAME", which gives the name') print("of a field in your grid that contains your elevation") print("data.") raise MissingKeyError("ELEVATION_FIELD_NAME") try: self._elev = self._grid.at_node[self.topo_field_name] except AttributeError: print( "Your grid does not seem to have a node field called", self.topo_field_name, ) else: self.topo_field_name = "topographic__elevation" # create the only new output field: self.sed_fill_depth = self._grid.add_zeros( "node", "sediment_fill__depth", noclobber=False ) self._lf = DepressionFinderAndRouter(self._grid, routing=self._routing) self._fr = FlowAccumulator(self._grid, flow_director=self._routing)
def test_lake_mapper(): """ Create a test grid and run a series of tests. """ # Make a test grid rmg = create_test_grid() # Instantiate a lake mapper # (Note that we don't need to send it an input file name, because our grid # already has a topographic__elevation field) lm = DepressionFinderAndRouter(rmg) # Run it on our test grid lm.map_depressions() # Run tests check_fields1(rmg) check_array_values1(rmg, lm) check_fields2(rmg) check_array_values2(rmg, lm)
def test_lake_mapper(): """ Create a test grid and run a series of tests. """ # Make a test grid rmg = create_test_grid() # Instantiate a lake mapper # (Note that we don't need to send it an input file name, because our grid # already has a topographic__elevation field) lm = DepressionFinderAndRouter(rmg) # Run it on our test grid lm.map_depressions() # Run tests check_fields1(rmg) check_array_values1(rmg, lm) check_fields2(rmg) check_array_values2(rmg, lm)
def __init__(self, input_file=None, params=None): """Initialize the HybridAlluviumModel.""" # Call ErosionModel's init super(HybridAlluviumModel, self).__init__(input_file=input_file, params=params) # Instantiate a FlowRouter and DepressionFinderAndRouter components self.flow_router = FlowRouter(self.grid, **self.params) self.lake_filler = DepressionFinderAndRouter(self.grid, **self.params) #make area_field and/or discharge_field depending on discharge_method if self.params['discharge_method'] is not None: if self.params['discharge_method'] == 'area_field': area_field = self.grid.at_node['drainage_area'] discharge_field = None elif self.params['discharge_method'] == 'discharge_field': discharge_field = self.grid.at_node['surface_water__discharge'] area_field = None else: raise (KeyError) else: area_field = None discharge_field = None # Instantiate a HybridAlluvium component self.eroder = Space(self.grid, K_sed=self.params['K_sed'], K_br=self.params['K_br'], F_f=self.params['F_f'], phi=self.params['phi'], H_star=self.params['H_star'], v_s=self.params['v_s'], m_sp=self.params['m_sp'], n_sp=self.params['n_sp'], sp_crit_sed=self.params['sp_crit_sed'], sp_crit_br=self.params['sp_crit_br'], method=self.params['method'], discharge_method=self.params['discharge_method'], area_field=area_field, discharge_field=discharge_field)
class DrainageAreaModel(_ErosionModel): """ A DrainageAreaModel simply computes drainage area on a raster-grid DEM. """ def __init__(self, input_file=None, params=None): """Initialize the LinearDiffusionModel.""" # Call ErosionModel's init super(DrainageAreaModel, self).__init__(input_file=input_file, params=params) # Instantiate a FlowRouter and DepressionFinderAndRouter components self.flow_router = FlowRouter(self.grid, **self.params) self.lake_filler = DepressionFinderAndRouter(self.grid, **self.params) def run_one_step(self, dt): """ Advance model for one time-step of duration dt. """ self.flow_router.run_one_step() # for debug from landlab.io import write_esri_ascii import numpy write_esri_ascii('test_dr_area_before_temp.txt', self.grid, names='drainage_area', clobber=True) loga = self.grid.add_zeros('node', 'loga') loga[:] = numpy.log10(self.grid.at_node['drainage_area'] + 1) write_esri_ascii('test_logdr_area_before_temp.txt', self.grid, names='loga', clobber=True) write_esri_ascii('test_sink_flag_before_temp.txt', self.grid, names='flow__sink_flag', clobber=True) self.lake_filler.map_depressions()
class EffectiveDrainageAreaModel(_ErosionModel): """ A DrainageAreaModel simply computes drainage area on a raster-grid DEM. """ def __init__(self, input_file=None, params=None): """Initialize the LinearDiffusionModel.""" # Call ErosionModel's init super(EffectiveDrainageAreaModel, self).__init__(input_file=input_file, params=params) # Instantiate a FlowRouter and DepressionFinderAndRouter components self.flow_router = FlowRouter(self.grid, **self.params) self.lake_filler = DepressionFinderAndRouter(self.grid, **self.params) # Add a field for effective drainage area self.eff_area = self.grid.add_zeros('node', 'effective_drainage_area') # Get the effective-area parameter self.sat_param = self.params['saturation_area_scale'] def run_one_step(self, dt): """ Advance model for one time-step of duration dt. """ # Run flow routing and lake filler self.flow_router.run_one_step() self.lake_filler.map_depressions() # Calculate effective area area = self.grid.at_node['drainage_area'] slope = self.grid.at_node['topographic__steepest_slope'] cores = self.grid.core_nodes self.eff_area[cores] = ( area[cores] * np.exp(-self.sat_param * slope[cores] / area[cores])) # Lower outlet self.z[self.outlet_node] -= self.outlet_lowering_rate * dt print(('lowering outlet to ', self.z[self.outlet_node]))
def __init__(self, input_file=None, params=None): """Initialize the StreamPowerVarThresholdModel.""" # Call ErosionModel's init super(StreamPowerVarThresholdModel, self).__init__(input_file=input_file, params=params) # Instantiate a FlowRouter and DepressionFinderAndRouter components self.flow_router = FlowRouter(self.grid, **self.params) self.lake_filler = DepressionFinderAndRouter(self.grid, **self.params) # Create a field for the (initial) erosion threshold self.threshold = self.grid.add_zeros('node', 'erosion__threshold') self.threshold[:] = self.params['threshold_sp'] # Instantiate a FastscapeEroder component self.eroder = StreamPowerSmoothThresholdEroder( self.grid, K_sp=self.params['K_sp'], threshold_sp=self.threshold) # Get the parameter for rate of threshold increase with erosion depth self.thresh_change_per_depth = self.params['thresh_change_per_depth']
class StreamPowerThresholdModel(_ErosionModel): """ A StreamPowerThresholdModel computes erosion using a form of the unit stream power model that represents a threshold using an exponential term. """ def __init__(self, input_file=None, params=None): """Initialize the StreamPowerThresholdModel.""" # Call ErosionModel's init super(StreamPowerThresholdModel, self).__init__(input_file=input_file, params=params) # Instantiate a FlowRouter and DepressionFinderAndRouter components self.flow_router = FlowRouter(self.grid, **self.params) self.lake_filler = DepressionFinderAndRouter(self.grid, **self.params) # Instantiate a FastscapeEroder component self.eroder = StreamPowerSmoothThresholdEroder( self.grid, K_sp=self.params['K_sp'], threshold_sp=self.params['threshold_sp']) def run_one_step(self, dt): """ Advance model for one time-step of duration dt. """ # Route flow self.flow_router.run_one_step() self.lake_filler.map_depressions() # Get IDs of flooded nodes, if any flooded = np.where(self.lake_filler.flood_status == 3)[0] # Do some erosion (but not on the flooded nodes) self.eroder.run_one_step(dt, flooded_nodes=flooded)
def test_steady_state_with_basic_solver_option(): """ Test that model matches the transport-limited analytical solution for slope/area relationship at steady state: S=((U * v_s) / (K * A^m) + U / (K * A^m))^(1/n). Also test that model matches the analytical solution for steady-state sediment flux: Qs = U * A * (1 - phi). """ #set up a 5x5 grid with one open outlet node and low initial elevations. nr = 5 nc = 5 mg = RasterModelGrid((nr, nc), 10.0) z = mg.add_zeros('node', 'topographic__elevation') mg['node']['topographic__elevation'] += mg.node_y / 100000 \ + mg.node_x / 100000 \ + np.random.rand(len(mg.node_y)) / 10000 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.) #Instantiate DepressionFinderAndRouter df = DepressionFinderAndRouter(mg) # Create a D8 flow handler fa = FlowAccumulator(mg, flow_director='D8', depression_finder='DepressionFinderAndRouter') # Parameter values for detachment-limited test K = 0.01 U = 0.0001 dt = 1.0 F_f = 0.0 #all sediment is considered coarse bedload m_sp = 0.5 n_sp = 1.0 v_s = 0.5 phi=0.5 # Instantiate the ErosionDeposition component... ed = ErosionDeposition(mg, K=K, F_f=F_f, phi=phi, v_s=v_s, m_sp=m_sp, n_sp=n_sp, sp_crit=0, solver='basic') # ... and run it to steady state (5000x1-year timesteps). for i in range(5000): fa.run_one_step() flooded = np.where(df.flood_status==3)[0] ed.run_one_step(dt=dt, flooded_nodes=flooded) z[mg.core_nodes] += U * dt #m #compare numerical and analytical slope solutions num_slope = mg.at_node['topographic__steepest_slope'][mg.core_nodes] analytical_slope = (np.power(((U * v_s) / (K * np.power(mg.at_node['drainage_area'][mg.core_nodes], m_sp))) + (U / (K * np.power(mg.at_node['drainage_area'][mg.core_nodes], m_sp))), 1./n_sp)) #test for match with analytical slope-area relationship testing.assert_array_almost_equal(num_slope, analytical_slope, decimal=8, err_msg='E/D slope-area test failed', verbose=True) #compare numerical and analytical sediment flux solutions num_sedflux = mg.at_node['sediment__flux'][mg.core_nodes] analytical_sedflux = (U * mg.at_node['drainage_area'][mg.core_nodes] * (1 - phi)) #test for match with anakytical sediment flux testing.assert_array_almost_equal(num_sedflux, analytical_sedflux, decimal=8, err_msg='E/D sediment flux test failed', verbose=True)
def test_degenerate_drainage(): """ This "hourglass" configuration should be one of the hardest to correctly re-route. """ mg = RasterModelGrid(9, 5) z_init = mg.node_x.copy() * 0.0001 + 1. lake_pits = np.array([7, 11, 12, 13, 17, 27, 31, 32, 33, 37]) z_init[lake_pits] = -1. z_init[22] = 0. # the common spill pt for both lakes z_init[21] = 0.1 # an adverse bump in the spillway z_init[20] = -0.2 # the spillway z = mg.add_field("node", "topographic__elevation", z_init) fr = FlowAccumulator(mg, flow_director='D8') lf = DepressionFinderAndRouter(mg) fr.run_one_step() lf.map_depressions() # correct_A = np.array([ 0., 0., 0., 0., 0., # 0., 1., 3., 1., 0., # 0., 5., 1., 2., 0., # 0., 1., 10., 1., 0., # 21., 21., 1., 1., 0., # 0., 1., 9., 1., 0., # 0., 3., 1., 2., 0., # 0., 1., 1., 1., 0., # 0., 0., 0., 0., 0.]) correct_A = np.array( [ 0., 0., 0., 0., 0., 0., 1., 3., 1., 0., 0., 2., 4., 2., 0., 0., 1., 10., 1., 0., 21., 21., 1., 1., 0., 0., 1., 9., 1., 0., 0., 2., 2., 2., 0., 0., 1., 1., 1., 0., 0., 0., 0., 0., 0., ] ) thelake = np.concatenate((lake_pits, [22])).sort() assert mg.at_node["drainage_area"] == approx(correct_A)
fa = FlowAccumulator(grid, surface='topographic__elevation', flow_director='D8') lmb = LakeMapperBarnes( grid, method='D8', fill_flat=False, surface="topographic__elevation", fill_surface="topographic__elevation", redirect_flow_steepest_descent=False, reaccumulate_flow=False, track_lakes=False, ignore_overfill=True, ) dfr = DepressionFinderAndRouter(grid) ld = LinearDiffuser(grid, D) sp = FastscapeEroder(grid, K_sp=Ksp, m_sp=0.5, n_sp=1.0, threshold_sp=E0) for i in range(N): dfr._find_pits() if dfr._number_of_pits > 0: lmb.run_one_step() z[grid.core_nodes] += U * dt ld.run_one_step(dt) fa.run_one_step() sp.run_one_step(dt)
class SinkFiller(Component): """ This component identifies depressions in a topographic surface, then fills them in in the topography. No attempt is made to conserve sediment mass. User may specify whether the holes should be filled to flat, or with a gradient downwards towards the depression outlet. The gradient can be spatially variable, and is chosen to not reverse any drainage directions at the perimeter of each lake. The primary method of this class is 'run_one_step'. 'fill_pits' is a synonym. Constructor assigns a copy of the grid, and calls the initialize method. Examples -------- >>> from landlab import RasterModelGrid >>> from landlab import BAD_INDEX_VALUE as XX >>> from landlab.components import FlowAccumulator, SinkFiller >>> import numpy as np >>> lake1 = np.array([34, 35, 36, 44, 45, 46, 54, 55, 56, 65, 74]) >>> lake2 = np.array([78, 87, 88]) >>> guard_nodes = np.array([23, 33, 53, 63, 73, 83]) >>> lake = np.concatenate((lake1, lake2)) >>> mg = RasterModelGrid((10, 10), 1.) >>> z = np.ones(100, dtype=float) >>> z += mg.node_x # add a slope >>> z[guard_nodes] += 0.001 # forces the flow out of a particular node >>> z[lake] = 0. >>> field = mg.add_field('node', 'topographic__elevation', z, ... units='-', copy=True) >>> fr = FlowAccumulator(mg, flow_director='D8') >>> fr.run_one_step() >>> mg.at_node['flow__sink_flag'][mg.core_nodes].sum() 14 >>> hf = SinkFiller(mg, apply_slope=False) >>> hf.run_one_step() >>> np.allclose(mg.at_node['topographic__elevation'][lake1], 4.) True >>> np.allclose(mg.at_node['topographic__elevation'][lake2], 7.) True Now reset and demonstrate the adding of an inclined surface: >>> field[:] = z >>> hf = SinkFiller(mg, apply_slope=True) >>> hf.run_one_step() >>> hole1 = np.array([4.00007692, 4.00015385, 4.00023077, 4.00030769, ... 4.00038462, 4.00046154, 4.00053846, 4.00061538, ... 4.00069231, 4.00076923, 4.00084615]) >>> hole2 = np.array([7.4, 7.2, 7.6]) >>> np.allclose(mg.at_node['topographic__elevation'][lake1], hole1) True >>> np.allclose(mg.at_node['topographic__elevation'][lake2], hole2) True >>> fr.run_one_step() >>> mg.at_node['flow__sink_flag'][mg.core_nodes].sum() 0 """ _name = "SinkFiller" _input_var_names = ("topographic__elevation", ) _output_var_names = ("topographic__elevation", "sediment_fill__depth") _var_units = {"topographic__elevation": "m", "sediment_fill__depth": "m"} _var_mapping = { "topographic__elevation": "node", "sediment_fill__depth": "node" } _var_doc = { "topographic__elevation": "Surface topographic elevation", "sediment_fill__depth": "Depth of sediment added at each" + "node", } @use_file_name_or_kwds def __init__(self, grid, routing="D8", apply_slope=False, fill_slope=1.e-5, **kwds): """ Parameters ---------- grid : ModelGrid A landlab grid. routing : {'D8', 'D4'} (optional) If grid is a raster type, controls whether fill connectivity can occur on diagonals ('D8', default), or only orthogonally ('D4'). Has no effect if grid is not a raster. apply_slope : bool If False (default), leave the top of the filled sink flat. If True, apply the slope fill_slope to the top surface to allow subsequent flow routing. A test is performed to ensure applying this slope will not alter the drainage structure at the edge of the filled region (i.e., that we are not accidentally reversing the flow direction far from the outlet.) fill_slope : float (m/m) The slope added to the top surface of filled pits to allow flow routing across them, if apply_slope. """ 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 SinkFiller is compatible with " "route-to-multiple methods. Please open a GitHub Issue " "to start this process.") raise NotImplementedError(msg) self._grid = grid if routing is not "D8": assert routing is "D4" self._routing = routing if (type(self._grid) is landlab.grid.raster.RasterModelGrid) and (routing is "D8"): self._D8 = True self.num_nbrs = 8 else: self._D8 = False # useful shorthand for thia test we do a lot if type(self._grid) is landlab.grid.raster.RasterModelGrid: self.num_nbrs = 4 self._fill_slope = fill_slope self._apply_slope = apply_slope self.initialize() def initialize(self, input_stream=None): """ The BMI-style initialize method takes an optional input_stream parameter, which may be either a ModelParameterDictionary object or an input stream from which a ModelParameterDictionary can read values. """ # Create a ModelParameterDictionary for the inputs if input_stream is None: inputs = None elif type(input_stream) == ModelParameterDictionary: inputs = input_stream else: inputs = ModelParameterDictionary(input_stream) # Make sure the grid includes elevation data. This means either: # 1. The grid has a node field called 'topographic__elevation', or # 2. The input file has an item called 'ELEVATION_FIELD_NAME' *and* # a field by this name exists in the grid. try: self._elev = self._grid.at_node["topographic__elevation"] except FieldError: try: self.topo_field_name = inputs.read_string("ELEVATION_" + "FIELD_NAME") except AttributeError: print("Error: Because your grid does not have a node field") print('called "topographic__elevation", you need to pass the') print("name of a text input file or ModelParameterDictionary,") print("and this file or dictionary needs to include the name") print("of another field in your grid that contains your") print("elevation data.") raise AttributeError except MissingKeyError: print("Error: Because your grid does not have a node field") print('called "topographic__elevation", your input file (or') print("ModelParameterDictionary) must include an entry with") print('the key "ELEVATION_FIELD_NAME", which gives the name') print("of a field in your grid that contains your elevation") print("data.") raise MissingKeyError("ELEVATION_FIELD_NAME") try: self._elev = self._grid.at_node[self.topo_field_name] except AttributeError: print( "Your grid does not seem to have a node field called", self.topo_field_name, ) else: self.topo_field_name = "topographic__elevation" # create the only new output field: self.sed_fill_depth = self._grid.add_zeros("node", "sediment_fill__depth", noclobber=False) self._lf = DepressionFinderAndRouter(self._grid, routing=self._routing) self._fr = FlowAccumulator(self._grid, flow_director=self._routing) def fill_pits(self, **kwds): """ This is a synonym for the main method :func:`run_one_step`. """ self.run_one_step(**kwds) def run_one_step(self, **kwds): """ This is the main method. Call it to fill depressions in a starting topography. """ # added for back-compatibility with old formats try: self._apply_slope = kwds["apply_slope"] except KeyError: pass self.original_elev = self._elev.copy() # We need this, as we'll have to do ALL this again if we manage # to jack the elevs too high in one of the "subsidiary" lakes. # We're going to implement the lake_mapper component to do the heavy # lifting here, then delete its fields. This means we first need to # test if these fields already exist, in which case, we should *not* # delete them! existing_fields = {} spurious_fields = set() set_of_outputs = set(self._lf.output_var_names) | set( self._fr.output_var_names) try: set_of_outputs.remove(self.topo_field_name) except KeyError: pass for field in set_of_outputs: try: existing_fields[field] = self._grid.at_node[field].copy() except FieldError: # not there; good! spurious_fields.add(field) self._fr.run_one_step() self._lf.map_depressions(pits=self._grid.at_node["flow__sink_flag"], reroute_flow=True) # add the depression depths to get up to flat: self._elev += self._grid.at_node["depression__depth"] # if apply_slope is none, we're now done! But if not... if self._apply_slope: # new way of doing this - use the upstream structure! Should be # both more general and more efficient for (outlet_node, lake_code) in zip(self._lf.lake_outlets, self._lf.lake_codes): lake_nodes = np.where(self._lf.lake_map == lake_code)[0] lake_perim = self._get_lake_ext_margin(lake_nodes) perim_elevs = self._elev[lake_perim] out_elev = self._elev[outlet_node] lowest_elev_perim = perim_elevs[perim_elevs != out_elev].min() # note we exclude the outlet node elev_increment = (lowest_elev_perim - self._elev[outlet_node] ) / (lake_nodes.size + 2.) assert elev_increment > 0. all_ordering = self._grid.at_node["flow__upstream_node_order"] upstream_order_bool = np.in1d(all_ordering, lake_nodes, assume_unique=True) lake_upstream_order = all_ordering[upstream_order_bool] argsort_lake = np.argsort(lake_upstream_order) elevs_to_add = (np.arange(lake_nodes.size, dtype=float) + 1.) * elev_increment sorted_elevs_to_add = elevs_to_add[argsort_lake] self._elev[lake_nodes] += sorted_elevs_to_add # now put back any fields that were present initially, and wipe the # rest: for delete_me in spurious_fields: if delete_me in self._grid.at_node: self._grid.delete_field("node", delete_me) for update_me in existing_fields.keys(): self.grid.at_node[update_me][:] = existing_fields[update_me] # fill the output field self.sed_fill_depth[:] = self._elev - self.original_elev @deprecated(use="fill_pits", version=1.0) def _fill_pits_old(self, apply_slope=None): """ .. deprecated:: 0.1.38 Use :func:`fill_pits` instead. This is the main method. Call it to fill depressions in a starting topography. **Output fields** * `topographic__elevation` : the updated elevations * `sediment_fill__depth` : the depth of sediment added at each node Parameters ---------- apply_slope : None, bool, or float If a float is provided this is the slope of the surface down towards the lake outlet. Supply a small positive number, e.g., 1.e-5 (or True, to use this default value). A test is performed to ensure applying this slope will not alter the drainage structure at the edge of the filled region (i.e., that we are not accidentally reversing the flow direction far from the outlet.) The component will automatically decrease the (supplied or default) gradient a number of times to try to accommodate this, but will eventually raise an OverflowError if it can't deal with it. If you pass True, the method will use the default value of 1.e-5. """ self.original_elev = self._elev.copy() # We need this, as we'll have to do ALL this again if we manage # to jack the elevs too high in one of the "subsidiary" lakes. # We're going to implement the lake_mapper component to do the heavy # lifting here, then delete its fields. This means we first need to # test if these fields already exist, in which case, we should *not* # delete them! existing_fields = {} spurious_fields = set() set_of_outputs = self._lf.output_var_names | self._fr.output_var_names try: set_of_outputs.remove(self.topo_field_name) except KeyError: pass for field in set_of_outputs: try: existing_fields[field] = self._grid.at_node[field].copy() except FieldError: # not there; good! spurious_fields.add(field) self._fr.run_one_step() self._lf.map_depressions(pits=self._grid.at_node["flow__sink_flag"], reroute_flow=False) # add the depression depths to get up to flat: self._elev += self._grid.at_node["depression__depth"] # if apply_slope is none, we're now done! But if not... if apply_slope is True: apply_slope = self._fill_slope elif type(apply_slope) in (float, int): assert apply_slope >= 0. if apply_slope: # this isn't very efficient, but OK as we're only running this # code ONCE in almost all use cases sublake = False unstable = True stability_increment = 0 self.lake_nodes_treated = np.array([], dtype=int) while unstable: while 1: for (outlet_node, lake_code) in zip(self._lf.lake_outlets, self._lf.lake_codes): self._apply_slope_current_lake(apply_slope, outlet_node, lake_code, sublake) # Call the mapper again here. Bail out if no core pits are # found. # This is necessary as there are some configs where adding # the slope could create subsidiary pits in the topo self._lf.map_depressions(pits=None, reroute_flow=False) if len(self._lf.lake_outlets) == 0.: break self._elev += self._grid.at_node["depression__depth"] sublake = True self.lake_nodes_treated = np.array([], dtype=int) # final test that all lakes are not reversing flow dirs all_lakes = np.where( self._lf.flood_status < BAD_INDEX_VALUE)[0] unstable = self.drainage_directions_change( all_lakes, self.original_elev, self._elev) if unstable: apply_slope *= 0.1 sublake = False self.lake_nodes_treated = np.array([], dtype=int) self._elev[:] = self.original_elev # put back init conds stability_increment += 1 if stability_increment == 10: raise OverflowError("Filler could not find a stable " + "condition with a sloping " + "surface!") # now put back any fields that were present initially, and wipe the # rest: for delete_me in spurious_fields: if delete_me in self.grid.at_node: self._grid.delete_field("node", delete_me) for update_me in existing_fields.keys(): self.grid.at_node[update_me] = existing_fields[update_me] # fill the output field self.sed_fill_depth[:] = self._elev - self.original_elev def _add_slopes(self, slope, outlet_node, lake_code): """ Assuming you have already run the lake_mapper, adds an incline towards the outlet to the nodes in the lake. """ new_elevs = self._elev.copy() outlet_coord = (self._grid.node_x[outlet_node], self._grid.node_y[outlet_node]) lake_nodes = np.where(self._lf.lake_map == lake_code)[0] lake_nodes = np.setdiff1d(lake_nodes, self.lake_nodes_treated) # lake_ext_margin = self._get_lake_ext_margin(lake_nodes) d = self._grid.calc_distances_of_nodes_to_point(outlet_coord, node_subset=lake_nodes) add_vals = slope * d new_elevs[lake_nodes] += add_vals self.lake_nodes_treated = np.union1d(self.lake_nodes_treated, lake_nodes) return new_elevs, lake_nodes def _get_lake_ext_margin(self, lake_nodes): """ Returns the nodes forming the external margin of the lake, honoring the *routing* method (D4/D8) if applicable. """ if self._D8 is True: all_poss = np.union1d( self.grid.active_adjacent_nodes_at_node[lake_nodes], self.grid.diagonal_adjacent_nodes_at_node[lake_nodes], ) else: all_poss = np.unique( self.grid.active_adjacent_nodes_at_node[lake_nodes]) lake_ext_edge = np.setdiff1d(all_poss, lake_nodes) return lake_ext_edge[lake_ext_edge != BAD_INDEX_VALUE] def _get_lake_int_margin(self, lake_nodes, lake_ext_edge): """ Returns the nodes forming the internal margin of the lake, honoring the *routing* method (D4/D8) if applicable. """ lee = lake_ext_edge if self._D8 is True: all_poss_int = np.union1d( self._grid.active_adjacent_nodes_at_node[lee], self._grid.diagonal_adjacent_nodes_at_node[lee], ) else: all_poss_int = np.unique( self._grid.active_adjacent_nodes_at_node[lee]) lake_int_edge = np.intersect1d(all_poss_int, lake_nodes) return lake_int_edge[lake_int_edge != BAD_INDEX_VALUE] def _apply_slope_current_lake(self, apply_slope, outlet_node, lake_code, sublake): """ Wraps the _add_slopes method to allow handling of conditions where the drainage structure would be changed or we're dealing with a sublake. """ while 1: starting_elevs = self._elev.copy() self._elev[:], lake_nodes = self._add_slopes( apply_slope, outlet_node, lake_code) # ext_edge = self._get_lake_ext_margin(lake_nodes) if sublake: break else: if not self.drainage_directions_change( lake_nodes, starting_elevs, self._elev): break else: # put the elevs back... self._elev[lake_nodes] = starting_elevs[lake_nodes] # the slope was too big. Reduce it. apply_slope *= 0.1 # if we get here, either sublake, or drainage dirs are stable def drainage_directions_change(self, lake_nodes, old_elevs, new_elevs): """ True if the drainage structure at lake margin changes, False otherwise. """ ext_edge = self._get_lake_ext_margin(lake_nodes) if self._D8: edge_neighbors = np.hstack(( self.grid.active_adjacent_nodes_at_node[ext_edge], self.grid.diagonal_adjacent_nodes_at_node[ext_edge], )) else: edge_neighbors = self.grid.active_adjacent_nodes_at_node[ ext_edge].copy() edge_neighbors[edge_neighbors == BAD_INDEX_VALUE] = -1 # ^value irrelevant old_neighbor_elevs = old_elevs[edge_neighbors] new_neighbor_elevs = new_elevs[edge_neighbors] # enforce the "don't change drainage direction" condition: edge_elevs = old_elevs[ext_edge].reshape((ext_edge.size, 1)) cond = np.allclose((edge_elevs >= old_neighbor_elevs), (edge_elevs >= new_neighbor_elevs)) # if True, we're good, the tilting didn't mess with the fr return not cond
v_profile = profile * vmax accum_disp = profile * float(dxy) # This is an array for counting how many pixels need to be moved nshift = np.zeros(np.size(yLocation)) n_buff = 0 # optional extra buffer zone incase you only want to move a subset. ################################################################################ ## Last, we instantiate landlab components that will evolve the landscape ##### ################################################################################ fr = FlowRouter(rmg) # standard D8 flow routing algorithm sp = FastscapeEroder(rmg, K_sp='K_sp', m_sp=m, n_sp=n, threshold_sp=0) # river eroder lin_diffuse = LinearDiffuser(rmg, linear_diffusivity='D') #linear diffuser fill = DepressionFinderAndRouter(rmg) #lake filling algorithm nts = int(num_frames) ds = xr.Dataset( data_vars={ 'topographic__elevation': ( ('time', 'y', 'x'), # tuple of dimensions np.empty((nts, rmg.shape[0], rmg.shape[1])), # n-d array of data { 'units': 'meters' }) }, # dictionary with data attributes coords={ 'x': ( ('x'), # tuple of dimensions rmg.x_of_node.reshape(
def test_edge_draining(): """ This tests when the lake attempts to drain from an edge, where an issue is suspected. """ # Create a 7x7 test grid with a well defined hole in it, AT THE EDGE. mg = RasterModelGrid((7, 7)) z = mg.node_x.copy() guard_sides = np.concatenate((np.arange(7, 14), np.arange(35, 42))) edges = np.concatenate((np.arange(7), np.arange(42, 49))) hole_here = np.array(([15, 16, 22, 23, 29, 30])) z[guard_sides] = z[13] z[edges] = -2.0 # force flow outwards from the tops of the guards z[hole_here] = -1.0 A_new = np.array([[[ 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 15.0, 5.0, 4.0, 3.0, 2.0, 1.0, 0.0, 0.0, 10.0, 4.0, 3.0, 2.0, 1.0, 0.0, 0.0, 1.0, 4.0, 3.0, 2.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, ]]]).flatten() depr_outlet_target = np.array([ XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, 14, 14, XX, XX, XX, XX, XX, 14, 14, XX, XX, XX, XX, XX, 14, 14, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, ]).flatten() mg.add_field("topographic__elevation", z, at="node", units="-") fr = FlowAccumulator(mg, flow_director="D8") lf = DepressionFinderAndRouter(mg) fr.run_one_step() lf.map_depressions() assert mg.at_node["drainage_area"] == approx(A_new) assert lf.depression_outlet_map == approx(depr_outlet_target)
def test_composite_pits(): """ A test to ensure the component correctly handles cases where there are multiple pits, inset into each other. """ mg = RasterModelGrid(10, 10, 1.) z = mg.add_field("node", "topographic__elevation", mg.node_x.copy()) # a sloping plane # np.random.seed(seed=0) # z += np.random.rand(100)/10000. # punch one big hole z.reshape((10, 10))[3:8, 3:8] = 0. # dig a couple of inset holes z[57] = -1. z[44] = -2. z[54] = -10. # make an outlet z[71] = 0.9 fr = FlowAccumulator(mg, flow_director='D8') lf = DepressionFinderAndRouter(mg) fr.run_one_step() lf.map_depressions() flow_sinks_target = np.zeros(100, dtype=bool) flow_sinks_target[mg.boundary_nodes] = True # no internal sinks now: assert_array_equal(mg.at_node["flow__sink_flag"], flow_sinks_target) # test conservation of mass: assert mg.at_node["drainage_area"].reshape((10, 10))[1:-1, 1].sum() == approx( 8. ** 2 ) # ^all the core nodes # test the actual flow field: # nA = np.array([ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., # 8., 8., 7., 6., 5., 4., 3., 2., 1., 0., # 1., 1., 1., 1., 1., 1., 1., 1., 1., 0., # 1., 1., 1., 4., 2., 2., 8., 4., 1., 0., # 1., 1., 1., 8., 3., 15., 3., 2., 1., 0., # 1., 1., 1., 13., 25., 6., 3., 2., 1., 0., # 1., 1., 1., 45., 3., 3., 5., 2., 1., 0., # 50., 50., 49., 3., 2., 2., 2., 4., 1., 0., # 1., 1., 1., 1., 1., 1., 1., 1., 1., 0., # 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]) nA = np.array( [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 8., 8., 7., 6., 5., 4., 3., 2., 1., 0., 1., 1., 1., 1., 1., 1., 1., 1., 1., 0., 1., 1., 1., 4., 2., 2., 6., 4., 1., 0., 1., 1., 1., 6., 3., 12., 3., 2., 1., 0., 1., 1., 1., 8., 20., 4., 3., 2., 1., 0., 1., 1., 1., 35., 5., 4., 3., 2., 1., 0., 50., 50., 49., 13., 10., 8., 6., 4., 1., 0., 1., 1., 1., 1., 1., 1., 1., 1., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., ] ) assert_array_equal(mg.at_node["drainage_area"], nA) # the lake code map: lc = np.array( [ XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, 57, 57, 57, 57, 57, XX, XX, XX, XX, XX, 57, 57, 57, 57, 57, XX, XX, XX, XX, XX, 57, 57, 57, 57, 57, XX, XX, XX, XX, XX, 57, 57, 57, 57, 57, XX, XX, XX, XX, XX, 57, 57, 57, 57, 57, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, ] ) # test the remaining properties: assert lf.lake_outlets.size == 1 assert lf.lake_outlets[0] == 72 outlets_in_map = np.unique(lf.depression_outlet_map) assert outlets_in_map.size == 2 assert outlets_in_map[1] == 72 assert lf.number_of_lakes == 1 assert lf.lake_codes[0] == 57 assert_array_equal(lf.lake_map, lc) assert lf.lake_areas[0] == approx(25.) assert lf.lake_volumes[0] == approx(63.)
def test_matches_bedrock_alluvial_solution(): """ Test that model matches the bedrock-alluvial analytical solution for slope/area relationship at steady state: S=((U * v_s * (1 - F_f)) / (K_sed * A^m) + U / (K_br * A^m))^(1/n). Also test that the soil depth everywhere matches the bedrock-alluvial analytical solution at steady state: H = -H_star * ln(1 - (v_s / (K_sed / (K_br * (1 - F_f)) + v_s))). """ #set up a 5x5 grid with one open outlet node and low initial elevations. nr = 5 nc = 5 mg = RasterModelGrid((nr, nc), 10.0) z = mg.add_zeros('node', 'topographic__elevation') br = mg.add_zeros('node', 'bedrock__elevation') soil = mg.add_zeros('node', 'soil__depth') mg['node']['topographic__elevation'] += ( mg.node_y / 100000 + mg.node_x / 100000 + np.random.rand(len(mg.node_y)) / 10000) 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.) soil[:] += 0. #initial condition of no soil depth. br[:] = z[:] z[:] += soil[:] #Instantiate DepressionFinderAndRouter df = DepressionFinderAndRouter(mg) # Create a D8 flow handler fa = FlowAccumulator(mg, flow_director='D8', depression_finder='DepressionFinderAndRouter') # Parameter values for detachment-limited test K_br = 0.02 K_sed = 0.02 U = 0.0001 dt = 1.0 F_f = 0.2 #all detached rock disappears; detachment-ltd end-member m_sp = 0.5 n_sp = 1.0 v_s = 0.25 H_star = 0.1 # Instantiate the Space component... sp = Space(mg, K_sed=K_sed, K_br=K_br, F_f=F_f, phi=0.0, H_star=H_star, v_s=v_s, m_sp=m_sp, n_sp=n_sp, sp_crit_sed=0, sp_crit_br=0) # ... and run it to steady state (10000x1-year timesteps). for i in range(10000): fa.run_one_step() flooded = np.where(df.flood_status == 3)[0] sp.run_one_step(dt=dt, flooded_nodes=flooded) br[mg.core_nodes] += U * dt #m soil[0] = 0. #enforce 0 soil depth at boundary to keep lowering steady z[:] = br[:] + soil[:] #compare numerical and analytical slope solutions num_slope = mg.at_node['topographic__steepest_slope'][mg.core_nodes] analytical_slope = (np.power( ((U * v_s * (1 - F_f)) / (K_sed * np.power(mg.at_node['drainage_area'][mg.core_nodes], m_sp))) + (U / (K_br * np.power(mg.at_node['drainage_area'][mg.core_nodes], m_sp))), 1. / n_sp)) #test for match with analytical slope-area relationship testing.assert_array_almost_equal( num_slope, analytical_slope, decimal=8, err_msg='SPACE bedrock-alluvial slope-area test failed', verbose=True) #compare numerical and analytical soil depth solutions num_h = mg.at_node['soil__depth'][mg.core_nodes] analytical_h = -H_star * np.log(1 - (v_s / (K_sed / (K_br * (1 - F_f)) + v_s))) #test for match with analytical sediment depth testing.assert_array_almost_equal( num_h, analytical_h, decimal=5, err_msg='SPACE bedrock-alluvial soil thickness test failed', verbose=True)
print('No pre-existing topography. Creating own random noise topo.') #Create boundary conditions of the model grid (either closed or fixed-head) for edge in (mg.nodes_at_left_edge,mg.nodes_at_right_edge, mg.nodes_at_top_edge): mg.status_at_node[edge] = CLOSED_BOUNDARY for edge in (mg.nodes_at_bottom_edge): mg.status_at_node[edge] = FIXED_VALUE_BOUNDARY #Initialize Fastscape fc = FastscapeEroder(mg, K_sp = ksp , m_sp = msp, n_sp = nsp, rainfall_intensity = 1) fr = FlowRouter(mg) lm = DepressionFinderAndRouter(mg) for i in range(nSteps): fr.run_one_step(dt=1) lm.map_depressions() fc.run_one_step(dt=1) mg.at_node['topographic__elevation'][mg.core_nodes] += 0.0002 z = mg.at_node['topographic__elevation'] plt.figure() imshow_grid(mg,z) plt.savefig('test.png') plt.close() np.save('iniTopo',z)
def test_edge_draining(): """ This tests when the lake attempts to drain from an edge, where an issue is suspected. """ # Create a 7x7 test grid with a well defined hole in it, AT THE EDGE. mg = RasterModelGrid((7, 7), (1., 1.)) z = mg.node_x.copy() guard_sides = np.concatenate((np.arange(7, 14), np.arange(35, 42))) edges = np.concatenate((np.arange(7), np.arange(42, 49))) hole_here = np.array(([15, 16, 22, 23, 29, 30])) z[guard_sides] = z[13] z[edges] = -2. # force flow outwards from the tops of the guards z[hole_here] = -1. A_new = np.array( [ [ [ 0., 1., 1., 1., 1., 1., 0., 0., 1., 1., 1., 1., 1., 0., 15., 5., 4., 3., 2., 1., 0., 0., 10., 4., 3., 2., 1., 0., 0., 1., 4., 3., 2., 1., 0., 0., 1., 1., 1., 1., 1., 0., 0., 1., 1., 1., 1., 1., 0., ] ] ] ).flatten() depr_outlet_target = np.array( [ XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, 14, 14, XX, XX, XX, XX, XX, 14, 14, XX, XX, XX, XX, XX, 14, 14, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, ] ).flatten() mg.add_field("node", "topographic__elevation", z, units="-") fr = FlowAccumulator(mg, flow_director='D8') lf = DepressionFinderAndRouter(mg) fr.run_one_step() lf.map_depressions() assert mg.at_node["drainage_area"] == approx(A_new) assert lf.depression_outlet_map == approx(depr_outlet_target)
def test_degenerate_drainage(): """ This "hourglass" configuration should be one of the hardest to correctly re-route. """ mg = RasterModelGrid((9, 5)) z_init = mg.node_x.copy() * 0.0001 + 1.0 lake_pits = np.array([7, 11, 12, 13, 17, 27, 31, 32, 33, 37]) z_init[lake_pits] = -1.0 z_init[22] = 0.0 # the common spill pt for both lakes z_init[21] = 0.1 # an adverse bump in the spillway z_init[20] = -0.2 # the spillway mg.add_field("topographic__elevation", z_init, at="node") fr = FlowAccumulator(mg, flow_director="D8") lf = DepressionFinderAndRouter(mg) fr.run_one_step() lf.map_depressions() # correct_A = np.array([ 0., 0., 0., 0., 0., # 0., 1., 3., 1., 0., # 0., 5., 1., 2., 0., # 0., 1., 10., 1., 0., # 21., 21., 1., 1., 0., # 0., 1., 9., 1., 0., # 0., 3., 1., 2., 0., # 0., 1., 1., 1., 0., # 0., 0., 0., 0., 0.]) correct_A = np.array([ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 3.0, 1.0, 0.0, 0.0, 2.0, 4.0, 2.0, 0.0, 0.0, 1.0, 10.0, 1.0, 0.0, 21.0, 21.0, 1.0, 1.0, 0.0, 0.0, 1.0, 9.0, 1.0, 0.0, 0.0, 2.0, 2.0, 2.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ]) assert mg.at_node["drainage_area"] == approx(correct_A)
class SinkFiller(Component): """ This component identifies depressions in a topographic surface, then fills them in in the topography. No attempt is made to conserve sediment mass. User may specify whether the holes should be filled to flat, or with a gradient downwards towards the depression outlet. The gradient can be spatially variable, and is chosen to not reverse any drainage directions at the perimeter of each lake. The primary method of this class is 'run_one_step'. 'fill_pits' is a synonym. Constructor assigns a copy of the grid, and calls the initialize method. Examples -------- >>> from landlab import RasterModelGrid >>> from landlab import BAD_INDEX_VALUE as XX >>> from landlab.components import FlowAccumulator, SinkFiller >>> import numpy as np >>> lake1 = np.array([34, 35, 36, 44, 45, 46, 54, 55, 56, 65, 74]) >>> lake2 = np.array([78, 87, 88]) >>> guard_nodes = np.array([23, 33, 53, 63, 73, 83]) >>> lake = np.concatenate((lake1, lake2)) >>> mg = RasterModelGrid((10, 10), 1.) >>> z = np.ones(100, dtype=float) >>> z += mg.node_x # add a slope >>> z[guard_nodes] += 0.001 # forces the flow out of a particular node >>> z[lake] = 0. >>> field = mg.add_field('node', 'topographic__elevation', z, ... units='-', copy=True) >>> fr = FlowAccumulator(mg, flow_director='D8') >>> fr.run_one_step() >>> mg.at_node['flow__sink_flag'][mg.core_nodes].sum() 14 >>> hf = SinkFiller(mg, apply_slope=False) >>> hf.run_one_step() >>> np.allclose(mg.at_node['topographic__elevation'][lake1], 4.) True >>> np.allclose(mg.at_node['topographic__elevation'][lake2], 7.) True Now reset and demonstrate the adding of an inclined surface: >>> field[:] = z >>> hf = SinkFiller(mg, apply_slope=True) >>> hf.run_one_step() >>> hole1 = np.array([4.00007692, 4.00015385, 4.00023077, 4.00030769, ... 4.00038462, 4.00046154, 4.00053846, 4.00061538, ... 4.00069231, 4.00076923, 4.00084615]) >>> hole2 = np.array([7.4, 7.2, 7.6]) >>> np.allclose(mg.at_node['topographic__elevation'][lake1], hole1) True >>> np.allclose(mg.at_node['topographic__elevation'][lake2], hole2) True >>> fr.run_one_step() >>> mg.at_node['flow__sink_flag'][mg.core_nodes].sum() 0 """ _name = 'SinkFiller' _input_var_names = ('topographic__elevation', ) _output_var_names = ('topographic__elevation', 'sediment_fill__depth', ) _var_units = {'topographic__elevation': 'm', 'sediment_fill__depth': 'm', } _var_mapping = {'topographic__elevation': 'node', 'sediment_fill__depth': 'node', } _var_doc = {'topographic__elevation': 'Surface topographic elevation', 'sediment_fill__depth': 'Depth of sediment added at each' + 'node', } @use_file_name_or_kwds def __init__(self, grid, routing='D8', apply_slope=False, fill_slope=1.e-5, **kwds): """ Parameters ---------- grid : ModelGrid A landlab grid. routing : {'D8', 'D4'} (optional) If grid is a raster type, controls whether fill connectivity can occur on diagonals ('D8', default), or only orthogonally ('D4'). Has no effect if grid is not a raster. apply_slope : bool If False (default), leave the top of the filled sink flat. If True, apply the slope fill_slope to the top surface to allow subsequent flow routing. A test is performed to ensure applying this slope will not alter the drainage structure at the edge of the filled region (i.e., that we are not accidentally reversing the flow direction far from the outlet.) fill_slope : float (m/m) The slope added to the top surface of filled pits to allow flow routing across them, if apply_slope. """ 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 SinkFiller is compatible with ' 'route-to-multiple methods. Please open a GitHub Issue ' 'to start this process.') raise NotImplementedError(msg) self._grid = grid if routing is not 'D8': assert routing is 'D4' self._routing = routing if ((type(self._grid) is landlab.grid.raster.RasterModelGrid) and (routing is 'D8')): self._D8 = True self.num_nbrs = 8 else: self._D8 = False # useful shorthand for thia test we do a lot if type(self._grid) is landlab.grid.raster.RasterModelGrid: self.num_nbrs = 4 self._fill_slope = fill_slope self._apply_slope = apply_slope self.initialize() def initialize(self, input_stream=None): """ The BMI-style initialize method takes an optional input_stream parameter, which may be either a ModelParameterDictionary object or an input stream from which a ModelParameterDictionary can read values. """ # Create a ModelParameterDictionary for the inputs if input_stream is None: inputs = None elif type(input_stream) == ModelParameterDictionary: inputs = input_stream else: inputs = ModelParameterDictionary(input_stream) # Make sure the grid includes elevation data. This means either: # 1. The grid has a node field called 'topographic__elevation', or # 2. The input file has an item called 'ELEVATION_FIELD_NAME' *and* # a field by this name exists in the grid. try: self._elev = self._grid.at_node['topographic__elevation'] except FieldError: try: self.topo_field_name = inputs.read_string('ELEVATION_' + 'FIELD_NAME') except AttributeError: print('Error: Because your grid does not have a node field') print('called "topographic__elevation", you need to pass the') print('name of a text input file or ModelParameterDictionary,') print('and this file or dictionary needs to include the name') print('of another field in your grid that contains your') print('elevation data.') raise AttributeError except MissingKeyError: print('Error: Because your grid does not have a node field') print('called "topographic__elevation", your input file (or') print('ModelParameterDictionary) must include an entry with') print('the key "ELEVATION_FIELD_NAME", which gives the name') print('of a field in your grid that contains your elevation') print('data.') raise MissingKeyError('ELEVATION_FIELD_NAME') try: self._elev = self._grid.at_node[self.topo_field_name] except AttributeError: print('Your grid does not seem to have a node field called', self.topo_field_name) else: self.topo_field_name = 'topographic__elevation' # create the only new output field: self.sed_fill_depth = self._grid.add_zeros('node', 'sediment_fill__depth', noclobber=False) self._lf = DepressionFinderAndRouter(self._grid, routing=self._routing) self._fr = FlowAccumulator(self._grid, flow_director=self._routing) def fill_pits(self, **kwds): """ This is a synonym for the main method :func:`run_one_step`. """ self.run_one_step(**kwds) def run_one_step(self, **kwds): """ This is the main method. Call it to fill depressions in a starting topography. """ # added for back-compatibility with old formats try: self._apply_slope = kwds['apply_slope'] except KeyError: pass self.original_elev = self._elev.copy() # We need this, as we'll have to do ALL this again if we manage # to jack the elevs too high in one of the "subsidiary" lakes. # We're going to implement the lake_mapper component to do the heavy # lifting here, then delete its fields. This means we first need to # test if these fields already exist, in which case, we should *not* # delete them! existing_fields = {} spurious_fields = set() set_of_outputs = (set(self._lf.output_var_names) | set(self._fr.output_var_names)) try: set_of_outputs.remove(self.topo_field_name) except KeyError: pass for field in set_of_outputs: try: existing_fields[field] = self._grid.at_node[field].copy() except FieldError: # not there; good! spurious_fields.add(field) self._fr.run_one_step() self._lf.map_depressions(pits=self._grid.at_node['flow__sink_flag'], reroute_flow=True) # add the depression depths to get up to flat: self._elev += self._grid.at_node['depression__depth'] # if apply_slope is none, we're now done! But if not... if self._apply_slope: # new way of doing this - use the upstream structure! Should be # both more general and more efficient for (outlet_node, lake_code) in zip(self._lf.lake_outlets, self._lf.lake_codes): lake_nodes = np.where(self._lf.lake_map == lake_code)[0] lake_perim = self._get_lake_ext_margin(lake_nodes) perim_elevs = self._elev[lake_perim] out_elev = self._elev[outlet_node] lowest_elev_perim = perim_elevs[perim_elevs != out_elev].min() # note we exclude the outlet node elev_increment = ((lowest_elev_perim-self._elev[outlet_node]) / (lake_nodes.size + 2.)) assert elev_increment > 0. all_ordering = self._grid.at_node['flow__upstream_node_order'] upstream_order_bool = np.in1d(all_ordering, lake_nodes, assume_unique=True) lake_upstream_order = all_ordering[upstream_order_bool] argsort_lake = np.argsort(lake_upstream_order) elevs_to_add = (np.arange(lake_nodes.size, dtype=float) + 1.) * elev_increment sorted_elevs_to_add = elevs_to_add[argsort_lake] self._elev[lake_nodes] += sorted_elevs_to_add # now put back any fields that were present initially, and wipe the # rest: for delete_me in spurious_fields: if delete_me in self._grid.at_node: self._grid.delete_field('node', delete_me) for update_me in existing_fields.keys(): self.grid.at_node[update_me][:] = existing_fields[update_me] # fill the output field self.sed_fill_depth[:] = self._elev - self.original_elev @deprecated(use='fill_pits', version=1.0) def _fill_pits_old(self, apply_slope=None): """ .. deprecated:: 0.1.38 Use :func:`fill_pits` instead. This is the main method. Call it to fill depressions in a starting topography. **Output fields** * `topographic__elevation` : the updated elevations * `sediment_fill__depth` : the depth of sediment added at each node Parameters ---------- apply_slope : None, bool, or float If a float is provided this is the slope of the surface down towards the lake outlet. Supply a small positive number, e.g., 1.e-5 (or True, to use this default value). A test is performed to ensure applying this slope will not alter the drainage structure at the edge of the filled region (i.e., that we are not accidentally reversing the flow direction far from the outlet.) The component will automatically decrease the (supplied or default) gradient a number of times to try to accommodate this, but will eventually raise an OverflowError if it can't deal with it. If you pass True, the method will use the default value of 1.e-5. """ self.original_elev = self._elev.copy() # We need this, as we'll have to do ALL this again if we manage # to jack the elevs too high in one of the "subsidiary" lakes. # We're going to implement the lake_mapper component to do the heavy # lifting here, then delete its fields. This means we first need to # test if these fields already exist, in which case, we should *not* # delete them! existing_fields = {} spurious_fields = set() set_of_outputs = self._lf.output_var_names | self._fr.output_var_names try: set_of_outputs.remove(self.topo_field_name) except KeyError: pass for field in set_of_outputs: try: existing_fields[field] = self._grid.at_node[field].copy() except FieldError: # not there; good! spurious_fields.add(field) self._fr.run_one_step() self._lf.map_depressions(pits=self._grid.at_node['flow__sink_flag'], reroute_flow=False) # add the depression depths to get up to flat: self._elev += self._grid.at_node['depression__depth'] # if apply_slope is none, we're now done! But if not... if apply_slope is True: apply_slope = self._fill_slope elif type(apply_slope) in (float, int): assert apply_slope >= 0. if apply_slope: # this isn't very efficient, but OK as we're only running this # code ONCE in almost all use cases sublake = False unstable = True stability_increment = 0 self.lake_nodes_treated = np.array([], dtype=int) while unstable: while 1: for (outlet_node, lake_code) in zip(self._lf.lake_outlets, self._lf.lake_codes): self._apply_slope_current_lake(apply_slope, outlet_node, lake_code, sublake) # Call the mapper again here. Bail out if no core pits are # found. # This is necessary as there are some configs where adding # the slope could create subsidiary pits in the topo self._lf.map_depressions(pits=None, reroute_flow=False) if len(self._lf.lake_outlets) == 0.: break self._elev += self._grid.at_node['depression__depth'] sublake = True self.lake_nodes_treated = np.array([], dtype=int) # final test that all lakes are not reversing flow dirs all_lakes = np.where(self._lf.flood_status < BAD_INDEX_VALUE)[0] unstable = self.drainage_directions_change(all_lakes, self.original_elev, self._elev) if unstable: apply_slope *= 0.1 sublake = False self.lake_nodes_treated = np.array([], dtype=int) self._elev[:] = original_elev # put back init conds stability_increment += 1 if stability_increment == 10: raise OverflowError('Filler could not find a stable ' + 'condition with a sloping ' + 'surface!') # now put back any fields that were present initially, and wipe the # rest: for delete_me in spurious_fields: if delete_me in self.grid.at_node: self._grid.delete_field('node', delete_me) for update_me in existing_fields.keys(): self.grid.at_node[update_me] = existing_fields[update_me] # fill the output field self.sed_fill_depth[:] = self._elev - self.original_elev def _add_slopes(self, slope, outlet_node, lake_code): """ Assuming you have already run the lake_mapper, adds an incline towards the outlet to the nodes in the lake. """ new_elevs = self._elev.copy() outlet_coord = (self._grid.node_x[outlet_node], self._grid.node_y[outlet_node]) lake_nodes = np.where(self._lf.lake_map == lake_code)[0] lake_nodes = np.setdiff1d(lake_nodes, self.lake_nodes_treated) lake_ext_margin = self._get_lake_ext_margin(lake_nodes) d = self._grid.calc_distances_of_nodes_to_point(outlet_coord, node_subset=lake_nodes) add_vals = slope*d new_elevs[lake_nodes] += add_vals self.lake_nodes_treated = np.union1d(self.lake_nodes_treated, lake_nodes) return new_elevs, lake_nodes def _get_lake_ext_margin(self, lake_nodes): """ Returns the nodes forming the external margin of the lake, honoring the *routing* method (D4/D8) if applicable. """ if self._D8 is True: all_poss = np.union1d( self.grid.active_adjacent_nodes_at_node[lake_nodes], self.grid.diagonal_adjacent_nodes_at_node[lake_nodes]) else: all_poss = np.unique(self.grid.active_adjacent_nodes_at_node[ lake_nodes]) lake_ext_edge = np.setdiff1d(all_poss, lake_nodes) return lake_ext_edge[lake_ext_edge != BAD_INDEX_VALUE] def _get_lake_int_margin(self, lake_nodes, lake_ext_edge): """ Returns the nodes forming the internal margin of the lake, honoring the *routing* method (D4/D8) if applicable. """ lee = lake_ext_edge if self._D8 is True: all_poss_int = np.union1d( self._grid.active_adjacent_nodes_at_node[lee], self._grid.diagonal_adjacent_nodes_at_node[lee]) else: all_poss_int = np.unique(self._grid.active_adjacent_nodes_at_node[lee]) lake_int_edge = np.intersect1d(all_poss_int, lake_nodes) return lake_int_edge[lake_int_edge != BAD_INDEX_VALUE] def _apply_slope_current_lake(self, apply_slope, outlet_node, lake_code, sublake): """ Wraps the _add_slopes method to allow handling of conditions where the drainage structure would be changed or we're dealing with a sublake. """ while 1: starting_elevs = self._elev.copy() self._elev[:], lake_nodes = self._add_slopes(apply_slope, outlet_node, lake_code) ext_edge = self._get_lake_ext_margin(lake_nodes) if sublake: break else: if not self.drainage_directions_change(lake_nodes, starting_elevs, self._elev): break else: # put the elevs back... self._elev[lake_nodes] = starting_elevs[lake_nodes] # the slope was too big. Reduce it. apply_slope *= 0.1 # if we get here, either sublake, or drainage dirs are stable def drainage_directions_change(self, lake_nodes, old_elevs, new_elevs): """ True if the drainage structure at lake margin changes, False otherwise. """ ext_edge = self._get_lake_ext_margin(lake_nodes) if self._D8: edge_neighbors = np.hstack( (self.grid.active_adjacent_nodes_at_node[ext_edge], self.grid.diagonal_adjacent_nodes_at_node[ext_edge])) else: edge_neighbors = self.grid.active_adjacent_nodes_at_node[ ext_edge].copy() edge_neighbors[edge_neighbors == BAD_INDEX_VALUE] = -1 # ^value irrelevant old_neighbor_elevs = old_elevs[edge_neighbors] new_neighbor_elevs = new_elevs[edge_neighbors] # enforce the "don't change drainage direction" condition: edge_elevs = old_elevs[ext_edge].reshape((ext_edge.size, 1)) cond = np.allclose((edge_elevs >= old_neighbor_elevs), (edge_elevs >= new_neighbor_elevs)) # if True, we're good, the tilting didn't mess with the fr return not cond
def test_three_pits(): """ A test to ensure the component correctly handles cases where there are multiple pits. """ mg = RasterModelGrid((10, 10)) z = mg.add_field("topographic__elevation", mg.node_x.copy(), at="node") # a sloping plane # np.random.seed(seed=0) # z += np.random.rand(100)/10000. # punch some holes z[33] = 1.0 z[43] = 1.0 z[37] = 4.0 z[74:76] = 1.0 fr = FlowAccumulator(mg, flow_director="D8") lf = DepressionFinderAndRouter(mg) fr.run_one_step() lf.map_depressions() flow_sinks_target = np.zeros(100, dtype=bool) flow_sinks_target[mg.boundary_nodes] = True # no internal sinks now: assert_array_equal(mg.at_node["flow__sink_flag"], flow_sinks_target) # test conservation of mass: assert mg.at_node["drainage_area"].reshape( (10, 10))[1:-1, 1].sum() == approx(8.0**2) # ^all the core nodes # test the actual flow field: nA = np.array([ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 8.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0, 0.0, 2.0, 2.0, 1.0, 1.0, 2.0, 1.0, 1.0, 1.0, 1.0, 0.0, 26.0, 26.0, 25.0, 15.0, 11.0, 10.0, 9.0, 8.0, 1.0, 0.0, 2.0, 2.0, 1.0, 9.0, 2.0, 1.0, 1.0, 1.0, 1.0, 0.0, 2.0, 2.0, 1.0, 1.0, 5.0, 4.0, 3.0, 2.0, 1.0, 0.0, 2.0, 2.0, 1.0, 1.0, 1.0, 1.0, 3.0, 2.0, 1.0, 0.0, 20.0, 20.0, 19.0, 18.0, 17.0, 12.0, 3.0, 2.0, 1.0, 0.0, 2.0, 2.0, 1.0, 1.0, 1.0, 1.0, 3.0, 2.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ]) assert_array_equal(mg.at_node["drainage_area"], nA) # test a couple more properties: lc = np.empty(100, dtype=int) lc.fill(XX) lc[33] = 33 lc[43] = 33 lc[37] = 37 lc[74:76] = 74 assert_array_equal(lf.lake_map, lc) assert_array_equal(lf.lake_codes, [33, 37, 74]) assert lf.number_of_lakes == 3 assert lf.lake_areas == approx([2.0, 1.0, 2.0]) assert lf.lake_volumes == approx([2.0, 2.0, 4.0])
# add initial noise to produce convergent flow from the initial conditions np.random.seed(91) # so our figures are reproducible mg_noise = np.random.rand(mg.number_of_nodes)/1000. # set up the input fields zr = mg.add_zeros('node', 'topographic__elevation') zr += mg_noise # Landlab sets fixed elevation boundary conditions by default. This is # what we want, so we will not modify these here. # instantiate the components: frr = FlowRouter(mg) # water__unit_flux_in gets automatically ingested spr = StreamPowerEroder(mg, K_sp=K_sp, m_sp=m_sp, n_sp=n_sp, threshold_sp=0, use_Q=None) lake = DepressionFinderAndRouter(mg) # Hillslopes dfn = LinearDiffuser(mg, linear_diffusivity=K_hs) zr_last = -9999 keep_running = np.mean(np.abs(zr - zr_last)) >= end_thresh ti = 0 while keep_running: zr_last = zr.copy() zr[mg.core_nodes] += uplift_rate*dt dfn.run_one_step(dt) # hillslopes always diffusive, even when dry frr.run_one_step() lake.map_depressions() spr.run_one_step(dt, flooded_nodes=lake.lake_at_node) keep_running = np.mean(np.abs(zr - zr_last)) >= end_thresh
def test_composite_pits(): """ A test to ensure the component correctly handles cases where there are multiple pits, inset into each other. """ mg = RasterModelGrid((10, 10)) z = mg.add_field("topographic__elevation", mg.node_x.copy(), at="node") # a sloping plane # np.random.seed(seed=0) # z += np.random.rand(100)/10000. # punch one big hole z.reshape((10, 10))[3:8, 3:8] = 0.0 # dig a couple of inset holes z[57] = -1.0 z[44] = -2.0 z[54] = -10.0 # make an outlet z[71] = 0.9 fr = FlowAccumulator(mg, flow_director="D8") lf = DepressionFinderAndRouter(mg) fr.run_one_step() lf.map_depressions() flow_sinks_target = np.zeros(100, dtype=bool) flow_sinks_target[mg.boundary_nodes] = True # no internal sinks now: assert_array_equal(mg.at_node["flow__sink_flag"], flow_sinks_target) # test conservation of mass: assert mg.at_node["drainage_area"].reshape( (10, 10))[1:-1, 1].sum() == approx(8.0**2) # ^all the core nodes # test the actual flow field: # nA = np.array([ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., # 8., 8., 7., 6., 5., 4., 3., 2., 1., 0., # 1., 1., 1., 1., 1., 1., 1., 1., 1., 0., # 1., 1., 1., 4., 2., 2., 8., 4., 1., 0., # 1., 1., 1., 8., 3., 15., 3., 2., 1., 0., # 1., 1., 1., 13., 25., 6., 3., 2., 1., 0., # 1., 1., 1., 45., 3., 3., 5., 2., 1., 0., # 50., 50., 49., 3., 2., 2., 2., 4., 1., 0., # 1., 1., 1., 1., 1., 1., 1., 1., 1., 0., # 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]) nA = np.array([ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 8.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 1.0, 1.0, 1.0, 4.0, 2.0, 2.0, 6.0, 4.0, 1.0, 0.0, 1.0, 1.0, 1.0, 6.0, 3.0, 12.0, 3.0, 2.0, 1.0, 0.0, 1.0, 1.0, 1.0, 8.0, 20.0, 4.0, 3.0, 2.0, 1.0, 0.0, 1.0, 1.0, 1.0, 35.0, 5.0, 4.0, 3.0, 2.0, 1.0, 0.0, 50.0, 50.0, 49.0, 13.0, 10.0, 8.0, 6.0, 4.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, ]) assert_array_equal(mg.at_node["drainage_area"], nA) # the lake code map: lc = np.array([ XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, 57, 57, 57, 57, 57, XX, XX, XX, XX, XX, 57, 57, 57, 57, 57, XX, XX, XX, XX, XX, 57, 57, 57, 57, 57, XX, XX, XX, XX, XX, 57, 57, 57, 57, 57, XX, XX, XX, XX, XX, 57, 57, 57, 57, 57, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, XX, ]) # test the remaining properties: assert lf.lake_outlets.size == 1 assert lf.lake_outlets[0] == 72 outlets_in_map = np.unique(lf.depression_outlet_map) assert outlets_in_map.size == 2 assert outlets_in_map[1] == 72 assert lf.number_of_lakes == 1 assert lf.lake_codes[0] == 57 assert_array_equal(lf.lake_map, lc) assert lf.lake_areas[0] == approx(25.0) assert lf.lake_volumes[0] == approx(63.0)
def get_ordered_cells_for_soil_moisture(grid, outlet_id=None): """ Runs Landlab's FlowRouter and DepressionFinderAndRouter to route flow. Also orders the cells in the descending order of channel length (upstream cell order). Parameters: ========== grid: grid object RasterModelGrid outlet_id: int (Optional) Outlet id to be set Returns: ======= ordered_cells: np.array(dtype=int) cells ordered in descending order of channel length grid: grid object updated RasterModelGrid """ if outlet_id == None: outlet_id = np.argmin(grid.at_node['topographic__elevation']) outlet = grid.set_watershed_boundary_condition_outlet_id(outlet_id, grid.at_node['topographic__elevation'], nodata_value=-9999.,) grid.set_closed_boundaries_at_grid_edges(True, True, True, True) flw_r = FlowRouter(grid) flw_r.run_one_step() df = DepressionFinderAndRouter(grid) df.map_depressions() r = grid.at_node['flow__receiver_node'][grid.node_at_core_cell] R = np.zeros(grid.number_of_nodes, dtype=int) R[grid.node_at_core_cell] = r channel_length = np.zeros(grid.number_of_nodes, dtype=int) # Compute channel lengths for each node in the wtrshd (node_at_core_cell) for node in grid.node_at_core_cell: node_c = node.copy() while R[node_c] != node_c: channel_length[node] += 1 node_c = R[node_c] grid.at_node['channel_length'] = channel_length # Sorting nodes in the ascending order of channel length # NOTE: length of ordered_nodes = grid.number_of_core_cells ordered_nodes = grid.node_at_core_cell[ np.argsort(channel_length[grid.node_at_core_cell])] # Sorting nodes in the descending order of channel length ordered_nodes = ordered_nodes[::-1] dd = 1 # switch 2 for while loop count_loops = 0 # No. of loops while runs while dd: dd = 0 count_loops += 1 sorted_order = list(ordered_nodes) alr_counted_ = [] for node_ in sorted_order: donors = [] donors = list(grid.node_at_core_cell[np.where(r==node_)[0]]) if len(donors) != 0: for k in range(0, len(donors)): if donors[k] not in alr_counted_: sorted_order.insert(donors[k], sorted_order.pop(sorted_order.index(node_))) dd = 1 alr_counted_.append(node_) ordered_nodes = np.array(sorted_order) ordered_cells = grid.cell_at_node[ordered_nodes] return ordered_cells, grid
def test_three_pits(): """ A test to ensure the component correctly handles cases where there are multiple pits. """ mg = RasterModelGrid(10, 10, 1.) z = mg.add_field("node", "topographic__elevation", mg.node_x.copy()) # a sloping plane # np.random.seed(seed=0) # z += np.random.rand(100)/10000. # punch some holes z[33] = 1. z[43] = 1. z[37] = 4. z[74:76] = 1. fr = FlowAccumulator(mg, flow_director='D8') lf = DepressionFinderAndRouter(mg) fr.run_one_step() lf.map_depressions() flow_sinks_target = np.zeros(100, dtype=bool) flow_sinks_target[mg.boundary_nodes] = True # no internal sinks now: assert_array_equal(mg.at_node["flow__sink_flag"], flow_sinks_target) # test conservation of mass: assert mg.at_node["drainage_area"].reshape((10, 10))[1:-1, 1].sum() == approx( 8. ** 2 ) # ^all the core nodes # test the actual flow field: nA = np.array( [ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 8., 8., 7., 6., 5., 4., 3., 2., 1., 0., 2., 2., 1., 1., 2., 1., 1., 1., 1., 0., 26., 26., 25., 15., 11., 10., 9., 8., 1., 0., 2., 2., 1., 9., 2., 1., 1., 1., 1., 0., 2., 2., 1., 1., 5., 4., 3., 2., 1., 0., 2., 2., 1., 1., 1., 1., 3., 2., 1., 0., 20., 20., 19., 18., 17., 12., 3., 2., 1., 0., 2., 2., 1., 1., 1., 1., 3., 2., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., ] ) assert_array_equal(mg.at_node["drainage_area"], nA) # test a couple more properties: lc = np.empty(100, dtype=int) lc.fill(XX) lc[33] = 33 lc[43] = 33 lc[37] = 37 lc[74:76] = 74 assert_array_equal(lf.lake_map, lc) assert_array_equal(lf.lake_codes, [33, 37, 74]) assert lf.number_of_lakes == 3 assert lf.lake_areas == approx([2., 1., 2.]) assert lf.lake_volumes == approx([2., 2., 4.])