def test_can_run_with_hex(): """Test that model can run with hex model grid.""" # Set up a 5x5 grid with open boundaries and low initial elevations. mg = HexModelGrid(7, 7) z = mg.add_zeros("node", "topographic__elevation") z[:] = 0.01 * mg.x_of_node # Create a D8 flow handler fa = FlowAccumulator(mg, flow_director="FlowDirectorSteepest") # Parameter values for test 1 U = 0.001 dt = 10.0 # Create the Space component... sp = Space( mg, K_sed=0.00001, K_br=0.00000000001, F_f=0.5, phi=0.1, H_star=1., v_s=0.001, m_sp=0.5, n_sp=1.0, sp_crit_sed=0, sp_crit_br=0, ) # ... and run it to steady state. for i in range(2000): fa.run_one_step() sp.run_one_step(dt=dt) z[mg.core_nodes] += U * dt
def test_mass_conserve_with_depression_finder_Space(grid2, H, solver, depression_finder, phi, v_s, H_star, dt): grid2.at_node["soil__depth"][:] = H assert grid2.status_at_node[1] == grid2.BC_NODE_IS_FIXED_VALUE z_init = grid2.at_node["topographic__elevation"].copy() if depression_finder is None: fa = FlowAccumulator(grid2) else: fa = FlowAccumulator(grid2, depression_finder=depression_finder, routing="D4") fa.run_one_step() ed = Space(grid2, solver=solver, phi=phi, v_s=v_s, H_star=H_star) ed.run_one_step(dt) # see above test for notes. dH = grid2.at_node["soil__depth"][:] - H dH *= 1 - phi dBr = grid2.at_node["bedrock__elevation"] - (z_init - H) mass_change = dH + dBr # assert that the mass loss over the surface is exported through the one # outlet. net_change = mass_change[grid2.core_nodes].sum() + ( ed.sediment_influx[1] * dt / grid2.cell_area_at_node[11]) assert_array_almost_equal(net_change, 0.0, decimal=10)
def test_can_run_with_hex(): """Test that model can run with hex model grid.""" # Set up a 5x5 grid with open boundaries and low initial elevations. mg = HexModelGrid(7, 7) z = mg.add_zeros('node', 'topographic__elevation') z[:] = 0.01 * mg.x_of_node # Create a D8 flow handler fa = FlowAccumulator(mg, flow_director='FlowDirectorSteepest') # Parameter values for test 1 U = 0.001 dt = 10.0 # Create the Space component... sp = Space(mg, K_sed=0.00001, K_br=0.00000000001, F_f=0.5, phi=0.1, H_star=1., v_s=0.001, m_sp=0.5, n_sp=1.0, sp_crit_sed=0, sp_crit_br=0) # ... and run it to steady state. for i in range(2000): fa.run_one_step() sp.run_one_step(dt=dt) z[mg.core_nodes] += U * dt
def test_mass_conserve_all_closed_Space(grid, H, solver, phi, v_s, H_star, dt): grid.at_node["soil__depth"][:] = H z_init = grid.at_node["topographic__elevation"].copy() fa = FlowAccumulator(grid) fa.run_one_step() ed = Space(grid, solver=solver, phi=phi, v_s=v_s, H_star=H_star) ed.run_one_step(dt) # in space, everything is either bedrock or sediment. check for # conservation. dH = grid.at_node["soil__depth"][:] - H # sediment is defined as having a porosity so all changes (up or down ) # must be adjusted to mass. dH *= 1 - phi dBr = grid.at_node["bedrock__elevation"] - (z_init - H) mass_change = dH + dBr assert_array_almost_equal(mass_change.sum(), 0.0, decimal=10)
class BasicHySa(ErosionModel): """ A BasicHySa computes erosion using linear diffusion, hybrid alluvium, and Q~A. It creates soil through weathering and fluvial bedrock erosion, and consideres soil thickness in calculating hillslope diffusion. """ def __init__(self, input_file=None, params=None, BaselevelHandlerClass=None): """Initialize the BasicSa.""" # Call ErosionModel's init super(BasicHySa, self).__init__(input_file=input_file, params=params, BaselevelHandlerClass=BaselevelHandlerClass) self.K_br = self.get_parameter_from_exponent('K_rock_sp') self.K_sed = self.get_parameter_from_exponent('K_sed_sp') linear_diffusivity = ( self._length_factor**2.) * self.get_parameter_from_exponent( 'linear_diffusivity') # has units length^2/time v_sc = self.get_parameter_from_exponent( 'v_sc') # normalized settling velocity. Unitless. linear_diffusivity = ( self._length_factor**2.) * self.get_parameter_from_exponent( 'linear_diffusivity') # has units length^2/time try: initial_soil_thickness = (self._length_factor) * self.params[ 'initial_soil_thickness'] # has units length except KeyError: initial_soil_thickness = 1.0 # default value soil_transport_decay_depth = (self._length_factor) * self.params[ 'soil_transport_decay_depth'] # has units length max_soil_production_rate = (self._length_factor) * self.params[ 'max_soil_production_rate'] # has units length per time soil_production_decay_depth = (self._length_factor) * self.params[ 'soil_production_decay_depth'] # has units length # Instantiate a FlowAccumulator with DepressionFinderAndRouter using D8 method self.flow_router = FlowAccumulator( self.grid, flow_director='D8', depression_finder=DepressionFinderAndRouter) #set methods and fields. K's and sp_crits need to be field names method = 'simple_stream_power' discharge_method = 'discharge_field' area_field = None discharge_field = 'surface_water__discharge' # Instantiate a SPACE component self.eroder = Space(self.grid, K_sed=self.K_sed, K_br=self.K_br, F_f=self.params['F_f'], phi=self.params['phi'], H_star=self.params['H_star'], v_s=v_sc, m_sp=self.params['m_sp'], n_sp=self.params['n_sp'], method=method, discharge_method=discharge_method, area_field=area_field, discharge_field=discharge_field, solver=self.params['solver']) # Create soil thickness (a.k.a. depth) field if 'soil__depth' in self.grid.at_node: soil_thickness = self.grid.at_node['soil__depth'] else: soil_thickness = self.grid.add_zeros('node', 'soil__depth') # Create bedrock elevation field if 'bedrock__elevation' in self.grid.at_node: bedrock_elev = self.grid.at_node['bedrock__elevation'] else: bedrock_elev = self.grid.add_zeros('node', 'bedrock__elevation') # Set soil thickness and bedrock elevation try: initial_soil_thickness = self.params['initial_soil_thickness'] except KeyError: initial_soil_thickness = 1.0 # default value soil_thickness[:] = initial_soil_thickness bedrock_elev[:] = self.z - initial_soil_thickness # Instantiate diffusion and weathering components self.diffuser = DepthDependentDiffuser( self.grid, linear_diffusivity=linear_diffusivity, soil_transport_decay_depth=soil_transport_decay_depth) self.weatherer = ExponentialWeatherer( self.grid, max_soil_production_rate=max_soil_production_rate, soil_production_decay_depth=soil_production_decay_depth) self.grid.at_node['soil__depth'][:] = \ self.grid.at_node['topographic__elevation'] - \ self.grid.at_node['bedrock__elevation'] def run_one_step(self, dt): """ Advance model for one time-step of duration dt. """ # Route flow self.flow_router.run_one_step() # Get IDs of flooded nodes, if any flooded = np.where( self.flow_router.depression_finder.flood_status == 3)[0] #print('There are ' + str(len(flooded)) + ' flooded nodes') # Do some erosion (but not on the flooded nodes) # (if we're varying K through time, update that first) if self.opt_var_precip: erode_factor = self.pc.get_erodibility_adjustment_factor( self.model_time) self.eroder.K_sed = self.K_sed * erode_factor self.eroder.K_br = self.K_br * erode_factor self.eroder.run_one_step(dt, flooded_nodes=flooded) # We must also now erode the bedrock where relevant. If water erosion # into bedrock has occurred, the bedrock elevation will be higher than # the actual elevation, so we simply re-set bedrock elevation to the # lower of itself or the current elevation. b = self.grid.at_node['bedrock__elevation'] b[:] = np.minimum(b, self.grid.at_node['topographic__elevation']) # Calculate regolith-production rate self.weatherer.calc_soil_prod_rate() # Generate and move soil around self.diffuser.run_one_step(dt) # calculate model time self.model_time += dt # Lower outlet self.update_outlet(dt) # Check walltime self.check_walltime() # Check stability self.check_stability() def check_stability(self): """Check stability and exit if unstable.""" fields = self.grid.at_node.keys() for f in fields: if (np.any(np.isnan(self.grid.at_node[f])) or np.any(np.isinf(self.grid.at_node[f]))): # model is unstable, write message and exit. with open('model_failed.txt', 'w') as f: f.write('This model run became unstable\n') exit
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), xy_spacing=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, )
def test_matches_detachment_solution(): """ Test that model matches the detachment-limited analytical solution for slope/area relationship at steady state: S=(U/K_br)^(1/n)*A^(-m/n). """ # set up a 5x5 grid with one open outlet node and low initial elevations. nr = 5 nc = 5 mg = RasterModelGrid((nr, nc), xy_spacing=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 / 10000 + mg.node_x / 10000 + 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. ) br[:] = z[:] - soil[:] # Create a D8 flow handler fa = FlowAccumulator(mg, flow_director="D8") # Parameter values for detachment-limited test K_br = 0.01 U = 0.0001 dt = 1.0 F_f = 1.0 # all detached rock disappears; detachment-ltd end-member m_sp = 0.5 n_sp = 1.0 # Instantiate the Space component... sp = Space( mg, K_sed=0.00001, K_br=K_br, F_f=F_f, phi=0.1, H_star=1., v_s=0.001, m_sp=m_sp, n_sp=n_sp, sp_crit_sed=0, sp_crit_br=0, ) # ... and run it to steady state (2000x1-year timesteps). for i in range(2000): fa.run_one_step() sp.run_one_step(dt=dt) z[mg.core_nodes] += U * dt # m br[mg.core_nodes] = z[mg.core_nodes] - soil[mg.core_nodes] # compare numerical and analytical slope solutions num_slope = mg.at_node["topographic__steepest_slope"][mg.core_nodes] analytical_slope = np.power(U / K_br, 1. / n_sp) * np.power( mg.at_node["drainage_area"][mg.core_nodes], -m_sp / n_sp ) # test for match with analytical slope-area relationship testing.assert_array_almost_equal( num_slope, analytical_slope, decimal=8, err_msg="SPACE detachment-limited test failed", verbose=True, )
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)
class HybridAlluviumModel(_ErosionModel): """ A HybridAlluviumModel computes erosion of sediment and bedrock using dual mass conservation on the bed and in the water column. It applies exponential entrainment rules to account for bed cover. """ 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) 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_matches_detachment_solution(): """ Test that model matches the detachment-limited analytical solution for slope/area relationship at steady state: S=(U/K_br)^(1/n)*A^(-m/n). """ #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 / 10000 \ + mg.node_x / 10000 \ + 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.) br[:] = z[:] - soil[:] # Create a D8 flow handler fa = FlowAccumulator(mg, flow_director='D8') # Parameter values for detachment-limited test K_br = 0.01 U = 0.0001 dt = 1.0 F_f = 1.0 #all detached rock disappears; detachment-ltd end-member m_sp = 0.5 n_sp = 1.0 # Instantiate the Space component... sp = Space(mg, K_sed=0.00001, K_br=K_br, F_f=F_f, phi=0.1, H_star=1., v_s=0.001, m_sp=m_sp, n_sp=n_sp, sp_crit_sed=0, sp_crit_br=0) # ... and run it to steady state (2000x1-year timesteps). for i in range(2000): fa.run_one_step() sp.run_one_step(dt=dt) z[mg.core_nodes] += U * dt #m br[mg.core_nodes] = z[mg.core_nodes] - soil[mg.core_nodes] #compare numerical and analytical slope solutions num_slope = mg.at_node['topographic__steepest_slope'][mg.core_nodes] analytical_slope = np.power(U / K_br, 1./n_sp) \ * np.power(mg.at_node['drainage_area'][mg.core_nodes], -m_sp / n_sp) #test for match with analytical slope-area relationship testing.assert_array_almost_equal( num_slope, analytical_slope, decimal=8, err_msg='SPACE detachment-limited test failed', verbose=True)
def test_matches_transport_solution(): """ Test that model matches the transport-limited analytical solution for slope/area relationship at steady state: S=((U * v_s) / (K_sed * A^m) + U / (K_sed * 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') 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[:] += 100. #initial soil depth of 100 m 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_sed = 0.01 U = 0.0001 dt = 1.0 F_f = 1.0 #all detached rock disappears; detachment-ltd end-member m_sp = 0.5 n_sp = 1.0 v_s = 0.5 phi=0.5 # Instantiate the Space component... sp = Space(mg, K_sed=K_sed, K_br=0.01, F_f=F_f, phi=phi, H_star=1., 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 (5000x1-year timesteps). for i in range(5000): 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] = 100. #enforce constant 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) / (K_sed * np.power(mg.at_node['drainage_area'][mg.core_nodes], m_sp))) + (U / (K_sed * 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 transport-limited 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='SPACE transport-limited sediment flux test failed', verbose=True)
def test_matches_transport_solution(): """ Test that model matches the transport-limited analytical solution for slope/area relationship at steady state: S=((U * v_s) / (K_sed * A^m) + U / (K_sed * 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), xy_spacing=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.0) soil[:] += 100.0 # initial soil depth of 100 m 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_sed = 0.01 U = 0.0001 dt = 1.0 F_f = 1.0 # all detached rock disappears; detachment-ltd end-member m_sp = 0.5 n_sp = 1.0 v_s = 0.5 phi = 0.5 # Instantiate the Space component... sp = Space( mg, K_sed=K_sed, K_br=0.01, F_f=F_f, phi=phi, H_star=1.0, 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 (5000x1-year timesteps). for i in range(5000): 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] = 100.0 # enforce constant 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 - phi)) / (K_sed * np.power(mg.at_node["drainage_area"][mg.core_nodes], m_sp))) + ((U * (1 - phi)) / (K_sed * np.power(mg.at_node["drainage_area"][mg.core_nodes], m_sp))), 1.0 / n_sp, ) # test for match with analytical slope-area relationship testing.assert_array_almost_equal( num_slope, analytical_slope, decimal=8, err_msg="SPACE transport-limited 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="SPACE transport-limited sediment flux test failed", verbose=True, )
#Set rock uplift rate rock_uplift_rate = 1e-4 #m/yr while elapsed_time < run_time: #Run the flow router fr.run_one_step() #Run the depression finder and router; optional df.map_depressions() #Get list of nodes in depressions; only #used if using DepressionFinderAndRouter flooded = np.where(df.flood_status == 3)[0] #Run the SPACE model for one timestep sp.run_one_step(dt=timestep, flooded_nodes=flooded) #Move bedrock elevation of core nodes upwards relative to baselevel #at the rock uplift rate mg.at_node['bedrock__elevation'][ mg.core_nodes] += rock_uplift_rate * timestep #Strip any soil from basin outlet so that all topographic change is due to #rock uplift mg.at_node['soil__depth'][0] = 0. #Recalculate topographic elevation to account for rock uplift mg.at_node['topographic__elevation'][:] = \ mg.at_node['bedrock__elevation'][:] + mg.at_node['soil__depth'][:] if elapsed_time % 1000 == 0:
class BasicHySa(ErosionModel): r"""**BasicHySa** program. This model program combines :py:class:`BasicHy` and :py:class:`BasicSa` to evolve a topographic surface described by :math:`\eta` with the following governing equation: .. math:: \eta = \eta_b + H \frac{\partial H}{\partial t} = P_0 \exp (-H/H_s) + \frac{V_s Q_s}{Q(A)\left(1 - \phi \right)} - K_s Q(A)^{m}S^{n} (1 - e^{-H/H_*}) -\nabla q_h \frac{\partial \eta_b}{\partial t} = -P_0 \exp (-H/H_s) - K_r Q(A)^{m}S^{n} e^{-H/H_*} Q_s = \int_0^A \left(K_s Q(A)^{m}S^{n} (1-e^{-H/H_*}) + K_r (1-F_f) Q(A)^{m}S^{n} e^{-H/H_*} - \frac{V_s Q_s}{Q(A)}\right) dA where :math:`\eta_b` is the bedrock elevation, :math:`H` is the soil depth, :math:`P_0` is the maximum soil production rate, :math:`H_s` is the soil production decay depth, :math:`V_s` is effective sediment settling velocity, :math:`Q_s` is volumetric fluvial sediment flux, :math:`A` is the local drainage area, :math:`Q`, is the local discharge, :math:`S` is the local slope, :math:`\phi` is sediment porosity, :math:`F_f` is the fraction of fine sediment, :math:`K_r` and :math:`K_s` are rock and sediment erodibility respectively, :math:`m` and :math:`n` are the discharge and slope exponent parameters, :math:`H_*` is the bedrock roughness length scale, and :math:`r` is a runoff rate. Hillslope sediment flux per unit width :math:`q_h` is given by: .. math:: q_h = -D H^* \left[1-\exp \left( -\frac{H}{H_0} \right) \right] \nabla \eta. where :math:`D` is soil diffusivity and :math:`H_0` is the soil transport depth scale. Refer to `Barnhart et al. (2019) <https://doi.org/10.5194/gmd-12-1267-2019>`_ Table 5 for full list of parameter symbols, names, and dimensions. The following at-node fields must be specified in the grid: - ``topographic__elevation`` - ``soil__depth`` """ _required_fields = ["topographic__elevation", "soil__depth"] def __init__( self, clock, grid, m_sp=0.5, n_sp=1.0, water_erodibility_sediment=0.001, water_erodibility_rock=0.0001, regolith_transport_parameter=0.1, settling_velocity=0.001, sediment_porosity=0.3, fraction_fines=0.5, roughness__length_scale=0.5, solver="basic", soil_production__maximum_rate=0.001, soil_production__decay_depth=0.5, soil_transport_decay_depth=0.5, sp_crit_br=0, sp_crit_sed=0, **kwargs ): """ Parameters ---------- clock : terrainbento Clock instance grid : landlab model grid instance The grid must have all required fields. m_sp : float, optional Drainage area exponent (:math:`m`). Default is 0.5. n_sp : float, optional Slope exponent (:math:`n`). Default is 1.0. water_erodibility : float, optional Water erodibility (:math:`K`). Default is 0.0001. regolith_transport_parameter : float, optional Regolith transport efficiency (:math:`D`). Default is 0.1. settling_velocity : float, optional Normalized settling velocity of entrained sediment (:math:`V_s`). Default is 0.001. sediment_porosity : float, optional Sediment porosity (:math:`\phi`). Default is 0.3. fraction_fines : float, optional Fraction of fine sediment that is permanently detached (:math:`F_f`). Default is 0.5. roughness__length_scale : float, optional Bedrock roughness length scale. Default is 0.5. solver : str, optional Solver option to pass to the Landlab `Space <https://landlab.readthedocs.io/en/master/reference/components/space.html>`_ component. Default is "basic". soil_production__maximum_rate : float, optional Maximum rate of soil production (:math:`P_{0}`). Default is 0.001. soil_production__decay_depth : float, optional Decay depth for soil production (:math:`H_{s}`). Default is 0.5. soil_transport_decay_depth : float, optional Decay depth for soil transport (:math:`H_{0}`). Default is 0.5. **kwargs : Keyword arguments to pass to :py:class:`ErosionModel`. Importantly these arguments specify the precipitator and the runoff generator that control the generation of surface water discharge (:math:`Q`). Returns ------- BasicHySa : model object Examples -------- This is a minimal example to demonstrate how to construct an instance of model **BasicHySa**. For more detailed examples, including steady-state test examples, see the terrainbento tutorials. To begin, import the model class. >>> from landlab import RasterModelGrid >>> from landlab.values import random >>> from terrainbento import Clock, BasicHySa >>> clock = Clock(start=0, stop=100, step=1) >>> grid = RasterModelGrid((5,5)) >>> _ = random(grid, "topographic__elevation") >>> _ = random(grid, "soil__depth") Construct the model. >>> model = BasicHySa(clock, grid) Running the model with ``model.run()`` would create output, so here we will just run it one step. >>> model.run_one_step(1.) >>> model.model_time 1.0 """ # Call ErosionModel"s init super().__init__(clock, grid, **kwargs) # verify correct fields are present. self._verify_fields(self._required_fields) soil_thickness = self.grid.at_node["soil__depth"] bedrock_elev = self.grid.add_zeros("node", "bedrock__elevation") bedrock_elev[:] = self.z - soil_thickness self.m = m_sp self.n = n_sp self.K_br = water_erodibility_rock self.K_sed = water_erodibility_sediment # Instantiate a SPACE component self.eroder = Space( self.grid, K_sed=self.K_sed, K_br=self.K_br, sp_crit_br=sp_crit_br, sp_crit_sed=sp_crit_sed, F_f=fraction_fines, phi=sediment_porosity, H_star=roughness__length_scale, v_s=settling_velocity, m_sp=self.m, n_sp=self.n, discharge_field="surface_water__discharge", solver=solver, ) # Instantiate diffusion and weathering components self.weatherer = ExponentialWeatherer( self.grid, soil_production__maximum_rate=soil_production__maximum_rate, soil_production__decay_depth=soil_production__decay_depth, ) self.diffuser = DepthDependentDiffuser( self.grid, linear_diffusivity=regolith_transport_parameter, soil_transport_decay_depth=soil_transport_decay_depth, ) self.grid.at_node["soil__depth"][:] = ( self.grid.at_node["topographic__elevation"] - self.grid.at_node["bedrock__elevation"] ) def run_one_step(self, step): """Advance model **BasicHySa** for one time-step of duration step. The **run_one_step** method does the following: 1. Creates rain and runoff, then directs and accumulates flow. 2. Assesses the location, if any, of flooded nodes where erosion should not occur. 3. Assesses if a :py:mod:`PrecipChanger` is an active boundary handler and if so, uses it to modify the erodibility by water. 4. Calculates erosion and deposition by water. 5. Calculates topographic change by linear diffusion. 6. Finalizes the step using the :py:mod:`ErosionModel` base class function **finalize__run_one_step**. This function updates all boundary handlers handlers by ``step`` and increments model time by ``step``. Parameters ---------- step : float Increment of time for which the model is run. """ # create and move water self.create_and_move_water(step) # Do some erosion (but not on the flooded nodes) # (if we're varying K through time, update that first) if "PrecipChanger" in self.boundary_handlers: erode_factor = self.boundary_handlers[ "PrecipChanger" ].get_erodibility_adjustment_factor() self.eroder.K_sed = self.K_sed * erode_factor self.eroder.K_br = self.K_br * erode_factor self.eroder.run_one_step(step) # We must also now erode the bedrock where relevant. If water erosion # into bedrock has occurred, the bedrock elevation will be higher than # the actual elevation, so we simply re-set bedrock elevation to the # lower of itself or the current elevation. b = self.grid.at_node["bedrock__elevation"] b[:] = np.minimum(b, self.grid.at_node["topographic__elevation"]) # Calculate regolith-production rate self.weatherer.calc_soil_prod_rate() # Generate and move soil around self.diffuser.run_one_step(step) # Finalize the run_one_step_method self.finalize__run_one_step(step) # Check stability self.check_stability() def check_stability(self): """Check model stability and exit if unstable.""" fields = self.grid.at_node.keys() for f in fields: if np.any(np.isnan(self.grid.at_node[f])) or np.any( np.isinf(self.grid.at_node[f]) ): raise SystemExit( "terrainbento ModelHySa: Model became unstable" )
LPJGUESS_BIN, LPJGUESS_CO2FILE, LPJGUESS_FORCINGS_STRING) print("finished with the initialization of the erosion components") print("---------------------") elapsed_time = 0 counter = 0 while elapsed_time < totalT: #create copy of "old" topography z0 = mg.at_node['topographic__elevation'].copy() #Call the erosion routines. fr.run_one_step() lm.map_depressions() floodedNodes = np.where(lm.flood_status == 3)[0] sp.run_one_step(dt=dt, flooded_nodes=floodedNodes) #fetch the nodes where space eroded the bedrock__elevation over topographic__elevation #after conversation with charlie shobe: b = mg.at_node['bedrock__elevation'] b[:] = np.minimum(b, mg.at_node['topographic__elevation']) #calculate regolith-production rate expWeath.calc_soil_prod_rate() #Generate and move the soil around. DDdiff.run_one_step(dt=dt) #run the landform classifier lc.run_one_step(elevationStepBin, 300, classtype=classificationType)