def test_mask_is_stable(): mg = RasterModelGrid((10, 10)) mg.add_zeros("node", "topographic__elevation") np.random.seed(3542) noise = np.random.rand(mg.size("node")) mg.at_node["topographic__elevation"] += noise fr = FlowAccumulator(mg, flow_director="D8") fsc = FastscapeEroder(mg, K_sp=0.01, m_sp=0.5, n_sp=1) for x in range(2): fr.run_one_step() fsc.run_one_step(dt=10.0) mg.at_node["topographic__elevation"][mg.core_nodes] += 0.01 mask = np.zeros(len(mg.at_node["topographic__elevation"]), dtype=np.uint8) mask[np.where(mg.at_node["drainage_area"] > 0)] = 1 mask0 = mask.copy() dd = DrainageDensity(mg, channel__mask=mask) mask1 = mask.copy() dd.calc_drainage_density() mask2 = mask.copy() assert_array_equal(mask0, mask1) assert_array_equal(mask0[mg.core_nodes], mask2[mg.core_nodes])
def test_sp_discharges_new(): input_str = os.path.join(_THIS_DIR, 'test_sp_params_discharge_new.txt') inputs = ModelParameterDictionary(input_str, auto_type=True) nrows = 5 ncols = 5 dx = inputs.read_float('dx') dt = inputs.read_float('dt') mg = RasterModelGrid(nrows, ncols, dx) mg.add_zeros('topographic__elevation', at='node') z = np.array([5., 5., 0., 5., 5., 5., 2., 1., 2., 5., 5., 3., 2., 3., 5., 5., 4., 4., 4., 5., 5., 5., 5., 5., 5.]) mg['node']['topographic__elevation'] = z fr = FlowAccumulator(mg, flow_director='D8') sp = StreamPowerEroder(mg, **inputs) # perform the loop (once!) for i in range(1): fr.run_one_step() sp.run_one_step(dt) z_tg = np.array([5. , 5. , 0. , 5. , 5. , 5. , 1.47759225, 0.43050087, 1.47759225, 5. , 5. , 2.32883687, 1.21525044, 2.32883687, 5. , 5. , 3.27261262, 3.07175015, 3.27261262, 5. , 5. , 5. , 5. , 5. , 5. ]) assert_array_almost_equal(mg.at_node['topographic__elevation'], z_tg)
def test_get_watershed_outlet(): grid = RasterModelGrid((7, 7), 1) z = np.array([ -9999., -9999., -9999., -9999., -9999., -9999., -9999., -9999., 26., 0., 30., 32., 34., -9999., -9999., 28., 1., 25., 28., 32., -9999., -9999., 30., 3., 3., 11., 34., -9999., -9999., 32., 11., 25., 18., 38., -9999., -9999., 34., 32., 34., 36., 40., -9999., -9999., -9999., -9999., -9999., -9999., -9999., -9999.]) grid.at_node['topographic__elevation'] = z imposed_outlet = 2 grid.set_watershed_boundary_condition_outlet_id(imposed_outlet, z, nodata_value=-9999.) fr = FlowAccumulator(grid, flow_director='D8') fr.run_one_step() test_node = 32 determined_outlet = get_watershed_outlet(grid, test_node) np.testing.assert_equal(determined_outlet, imposed_outlet) # Create a pit. pit_node = 38 grid.at_node['topographic__elevation'][pit_node] -= 32 fr.run_one_step() pit_outlet = get_watershed_outlet(grid, test_node) np.testing.assert_equal(pit_outlet, pit_node)
def test_get_watershed_masks_with_area_threshold(): rmg = RasterModelGrid((7, 7), 200) z = np.array([ -9999., -9999., -9999., -9999., -9999., -9999., -9999., -9999., 26., 0., 26., 30., 34., -9999., -9999., 28., 1., 28., 5., 32., -9999., -9999., 30., 3., 30., 10., 34., -9999., -9999., 32., 11., 32., 15., 38., -9999., -9999., 34., 32., 34., 36., 40., -9999., -9999., -9999., -9999., -9999., -9999., -9999., -9999.]) rmg.at_node['topographic__elevation'] = z rmg.set_closed_boundaries_at_grid_edges(True, True, True, False) # Route flow. fr = FlowAccumulator(rmg, flow_director='D8') fr.run_one_step() # Get the masks of watersheds greater than or equal to 80,000 # square-meters. critical_area = 80000 mask = get_watershed_masks_with_area_threshold(rmg, critical_area) # Assert that mask null nodes have a drainage area below critical area. null_nodes = np.where(mask == -1)[0] A = rmg.at_node['drainage_area'][null_nodes] below_critical_area_nodes = A < critical_area trues = np.ones(len(A), dtype=bool) np.testing.assert_equal(below_critical_area_nodes, trues)
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_assertion_error(): """Test that the correct assertion error will be raised.""" mg = RasterModelGrid(10, 10) z = mg.add_zeros('topographic__elevation', at='node') z += 200 + mg.x_of_node + mg.y_of_node + np.random.randn(mg.size('node')) 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, z, -9999) fa = FlowAccumulator(mg, flow_director='D8', depression_finder=DepressionFinderAndRouter) sp = FastscapeEroder(mg, K_sp=.0001, m_sp=.5, n_sp=1) ld = LinearDiffuser(mg, linear_diffusivity=0.0001) dt = 100 for i in range(200): fa.run_one_step() flooded = np.where(fa.depression_finder.flood_status==3)[0] sp.run_one_step(dt=dt, flooded_nodes=flooded) ld.run_one_step(dt=dt) mg.at_node['topographic__elevation'][0] -= 0.001 # Uplift assert_raises(AssertionError, analyze_channel_network_and_plot, mg, threshold = 100, starting_nodes = [0], number_of_channels=2)
def test_sed_dep(): input_file = os.path.join(_THIS_DIR, "sed_dep_params.txt") inputs = ModelParameterDictionary(input_file, auto_type=True) nrows = inputs.read_int("nrows") ncols = inputs.read_int("ncols") dx = inputs.read_float("dx") uplift_rate = inputs.read_float("uplift_rate") runtime = inputs.read_float("total_time") dt = inputs.read_float("dt") nt = int(runtime // dt) uplift_per_step = uplift_rate * dt mg = RasterModelGrid((nrows, ncols), xy_spacing=(dx, dx)) mg.add_zeros("topographic__elevation", at="node") z = np.loadtxt(os.path.join(_THIS_DIR, "seddepinit.txt")) mg["node"]["topographic__elevation"] = z mg.set_closed_boundaries_at_grid_edges(True, False, True, False) fr = FlowAccumulator(mg, flow_director="D8") sde = SedDepEroder(mg, **inputs) for i in range(nt): mg.at_node["topographic__elevation"][mg.core_nodes] += uplift_per_step mg = fr.run_one_step() mg, _ = sde.erode(dt) z_tg = np.loadtxt(os.path.join(_THIS_DIR, "seddepz_tg.txt")) assert_array_almost_equal( mg.at_node["topographic__elevation"][mg.core_nodes], z_tg[mg.core_nodes] )
def test_stupid_shaped_hole(sink_grid4): """Tests inclined fill into a surface with a deliberately awkward shape.""" fr = FlowAccumulator(sink_grid4, flow_director="D8") hf = SinkFiller(sink_grid4, apply_slope=True) hf.fill_pits() 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]) assert_array_almost_equal( sink_grid4.at_node["topographic__elevation"][sink_grid4.lake1], hole1 ) assert_array_almost_equal( sink_grid4.at_node["topographic__elevation"][sink_grid4.lake2], hole2 ) fr.run_one_step() assert sink_grid4.at_node["flow__sink_flag"][sink_grid4.core_nodes].sum() == 0
def test_MFD_S_slope_w_diag(): mg = RasterModelGrid((10, 10)) mg.add_field("topographic__elevation", mg.node_y, at="node") fa = FlowAccumulator(mg, flow_director="FlowDirectorMFD", diagonals=True) fa.run_one_step() # this one should part to south west, part to south, and part to southeast sw_diags = mg.diagonal_adjacent_nodes_at_node[:, 2] se_diags = mg.diagonal_adjacent_nodes_at_node[:, 3] s_links = mg.adjacent_nodes_at_node[:, 3] node_ids = np.arange(mg.number_of_nodes) true_recievers = -1 * np.ones(fa.flow_director.receivers.shape) true_recievers[mg.core_nodes, 3] = s_links[mg.core_nodes] true_recievers[mg.core_nodes, 6] = sw_diags[mg.core_nodes] true_recievers[mg.core_nodes, 7] = se_diags[mg.core_nodes] true_recievers[mg.boundary_nodes, 0] = node_ids[mg.boundary_nodes] true_proportions = np.zeros(fa.flow_director.proportions.shape) true_proportions[mg.boundary_nodes, 0] = 1 total_sum_of_slopes = 1. + 2. * (1. / 2. ** 0.5) true_proportions[mg.core_nodes, 3] = 1.0 / total_sum_of_slopes true_proportions[mg.core_nodes, 6] = (1. / 2. ** 0.5) / total_sum_of_slopes true_proportions[mg.core_nodes, 7] = (1. / 2. ** 0.5) / total_sum_of_slopes assert_array_equal(true_recievers, fa.flow_director.receivers) assert_array_almost_equal(true_proportions, fa.flow_director.proportions)
def test_D4_routing(sink_grid5): """ Tests inclined fill into a surface with a deliberately awkward shape. This is testing D4 routing. """ fr = FlowAccumulator(sink_grid5, flow_director="D4") hf = SinkFiller(sink_grid5, routing="D4", apply_slope=True) hf.fill_pits() hole1 = np.array( [ 4.00016667, 4.00033333, 4.0005, 4.00008333, 4.00025, 4.00041667, 4.000833, 4.00066667, 4.00058333, 4.00075, 4.334, ] ) hole2 = np.array([7.6, 7.2, 7.4]) assert_array_almost_equal( sink_grid5.at_node["topographic__elevation"][sink_grid5.lake1], hole1 ) assert_array_almost_equal( sink_grid5.at_node["topographic__elevation"][sink_grid5.lake2], hole2 ) fr.run_one_step() assert sink_grid5.at_node["flow__sink_flag"][sink_grid5.core_nodes].sum() == 0
def test_track_source(): """Unit tests for track_source(). """ grid = RasterModelGrid((5, 5), spacing=(1., 1.)) grid.at_node['topographic__elevation'] = np.array([5., 5., 5., 5., 5., 5., 4., 5., 1., 5., 0., 3., 5., 3., 0., 5., 4., 5., 2., 5., 5., 5., 5., 5., 5.]) grid.status_at_node[10] = 0 grid.status_at_node[14] = 0 fr = FlowAccumulator(grid, flow_director='D8') fr.run_one_step() r = grid.at_node['flow__receiver_node'] assert r[6] == 10 assert r[7] == 8 assert r[18] == 14 hsd_ids = np.empty(grid.number_of_nodes, dtype=int) hsd_ids[:] = 1 hsd_ids[2:5] = 0 hsd_ids[7:10] = 0 (hsd_upstr, flow_accum) = track_source(grid, hsd_ids) assert hsd_upstr[8] == [1, 0, 0] assert hsd_upstr[14] == [1, 1, 1, 1, 0, 0, 1] assert flow_accum[14] == 7
def test_with_thresh(): K = 0.001 U = 0.01 m = 0.5 n = 1.0 threshold = 1.0 dt = 1000 mg = RasterModelGrid(30, 3, xy_spacing=100.) mg.set_closed_boundaries_at_grid_edges(True, False, True, False) z = mg.zeros(at="node") mg["node"]["topographic__elevation"] = z + np.random.rand(len(z)) / 1000. fa = FlowAccumulator(mg) sp = Spst(mg, K_sp=K, threshold_sp=threshold) for i in range(100): fa.run_one_step() sp.run_one_step(dt) mg["node"]["topographic__elevation"][mg.core_nodes] += U * dt actual_slopes = mg.at_node["topographic__steepest_slope"][mg.core_nodes[1:-1]] actual_areas = mg.at_node["drainage_area"][mg.core_nodes[1:-1]] predicted_slopes_upper = ((U + threshold) / (K * (actual_areas ** m))) ** (1. / n) predicted_slopes_lower = ((U + 0.0) / (K * (actual_areas ** m))) ** (1. / n) # assert actual and predicted slopes are in the correct range for the slopes. assert np.all(actual_slopes > predicted_slopes_lower) assert np.all(actual_slopes < predicted_slopes_upper)
def test_sed_dep_new(): """ This tests only the power_law version of the SDE. It uses a landscape run to 5000 100y iterations, then having experienced a 20-fold uplift acceleration for a further 30000 y. It tests the outcome of the next 1000 y of erosion. """ mg = RasterModelGrid((25, 50), 200.) for edge in (mg.nodes_at_left_edge, mg.nodes_at_top_edge, mg.nodes_at_right_edge): mg.status_at_node[edge] = CLOSED_BOUNDARY z = mg.add_zeros('node', 'topographic__elevation') fr = FlowAccumulator(mg, flow_director='D8') sde = SedDepEroder(mg, K_sp=1.e-4, sed_dependency_type='almost_parabolic', Qc='power_law', K_t=1.e-4) initconds = os.path.join(os.path.dirname(__file__), 'perturbedcondst300.txt') finalconds = os.path.join(os.path.dirname(__file__), 'tenmorestepsfrom300.txt') z[:] = np.loadtxt(initconds) dt = 100. up = 0.05 for i in range(10): fr.run_one_step() sde.run_one_step(dt) z[mg.core_nodes] += 20.*up assert_array_almost_equal(z, np.loadtxt(finalconds))
def test_no_thresh(): K = 0.001 U = 0.01 m = 0.5 n = 1.0 threshold = 0.0 dt = 1000 mg = RasterModelGrid(30, 3, xy_spacing=100.) mg.set_closed_boundaries_at_grid_edges(True, False, True, False) z = mg.zeros(at="node") mg["node"]["topographic__elevation"] = z + np.random.rand(len(z)) / 1000. fa = FlowAccumulator(mg) sp = Spst(mg, K_sp=K, threshold_sp=threshold) for i in range(100): fa.run_one_step() sp.run_one_step(dt) mg["node"]["topographic__elevation"][mg.core_nodes] += U * dt actual_slopes = mg.at_node["topographic__steepest_slope"][mg.core_nodes[1:-1]] actual_areas = mg.at_node["drainage_area"][mg.core_nodes[1:-1]] predicted_slopes = (U / (K * (actual_areas ** m))) ** (1. / n) assert_array_almost_equal(actual_slopes, predicted_slopes)
def test_get_watershed_nodes(): grid = RasterModelGrid((7, 7), 1) z = np.array([ -9999., -9999., -9999., -9999., -9999., -9999., -9999., -9999., 26., 0., 30., 32., 34., -9999., -9999., 28., 1., 25., 28., 32., -9999., -9999., 30., 3., 3., 11., 34., -9999., -9999., 32., 11., 25., 18., 38., -9999., -9999., 34., 32., 34., 36., 40., -9999., -9999., -9999., -9999., -9999., -9999., -9999., -9999.]) grid.at_node['topographic__elevation'] = z outlet_id = 2 grid.set_watershed_boundary_condition_outlet_id(outlet_id, z, nodata_value=-9999.) fr = FlowAccumulator(grid, flow_director='D8') fr.run_one_step() ws_nodes = get_watershed_nodes(grid, outlet_id) # Given the watershed boundary conditions, the number of watershed nodes # should be equal to the number of core nodes plus 1 for the outlet node. np.testing.assert_equal(len(ws_nodes), grid.number_of_core_nodes + 1)
def test_no_reroute(): mg = RasterModelGrid((5, 5), xy_spacing=2.) z = mg.add_zeros("node", "topographic__elevation", dtype=float) z[1] = -1. z[6] = -2. z[19] = -2. z[18] = -1. z[17] = -3. fd = FlowDirectorSteepest(mg) fa = FlowAccumulator(mg) lmb = LakeMapperBarnes( mg, method="Steepest", fill_flat=True, redirect_flow_steepest_descent=True, track_lakes=True, ) lake_dict = {1: deque([6]), 18: deque([17])} fd.run_one_step() # fill the director fields fa.run_one_step() # get a drainage_area orig_surf = lmb._track_original_surface() lmb._redirect_flowdirs(orig_surf, lake_dict) assert mg.at_node["flow__receiver_node"][6] == 1 assert mg.at_node["flow__receiver_node"][17] == 18 assert mg.at_node["flow__receiver_node"][18] == 19
def test_filler_inclined2(sink_grid3): """ Tests an inclined fill into an inclined surface, with two holes. """ fr = FlowAccumulator(sink_grid3, flow_director="D8") hf = SinkFiller(sink_grid3, apply_slope=True) hf.fill_pits() hole1 = np.array( [ 4.00009091, 4.00018182, 4.00027273, 4.00036364, 4.00045455, 4.00054545, 4.00063636, 4.00072727, 4.00081818, ] ) hole2 = np.array([7.16666667, 7.33333333, 7.5, 7.66666667]) assert_array_almost_equal( sink_grid3.at_node["topographic__elevation"][sink_grid3.lake1], hole1 ) assert_array_almost_equal( sink_grid3.at_node["topographic__elevation"][sink_grid3.lake2], hole2 ) fr.run_one_step() assert sink_grid3.at_node["flow__sink_flag"][sink_grid3.core_nodes].sum() == 0
def test_route_to_multiple_error_raised_init(): mg = RasterModelGrid((10, 10)) z = mg.add_zeros("node", "topographic__elevation") z += mg.x_of_node + mg.y_of_node fa = FlowAccumulator(mg, flow_director="MFD") fa.run_one_step() with pytest.raises(NotImplementedError): SinkFillerBarnes(mg)
def test_flow__distance_raster_MFD_diagonals_true(): """Test of flow__distance utility with a raster grid and MFD.""" # instantiate a model grid mg = RasterModelGrid((5, 4), xy_spacing=(1, 1)) # instantiate an elevation array z = np.array( [[0, 0, 0, 0], [0, 21, 10, 0], [0, 31, 20, 0], [0, 32, 30, 0], [0, 0, 0, 0]], dtype="float64", ) # add the elevation field to the grid mg.add_field("node", "topographic__elevation", z) # instantiate the expected flow__distance array # considering flow directions calculated with MFD algorithm flow__distance_expected = np.array( [ [0, 0, 0, 0], [0, 1, 0, 0], [0, math.sqrt(2), 1, 0], [0, 1 + math.sqrt(2), 2, 0], [0, 0, 0, 0], ], dtype="float64", ) flow__distance_expected = np.reshape( flow__distance_expected, mg.number_of_node_rows * mg.number_of_node_columns ) # setting boundary conditions mg.set_closed_boundaries_at_grid_edges( bottom_is_closed=True, left_is_closed=True, right_is_closed=True, top_is_closed=True, ) # calculating flow directions with FlowAccumulator component fa = FlowAccumulator( mg, "topographic__elevation", flow_director="MFD", diagonals=True ) fa.run_one_step() # calculating flow distance map flow__distance = calculate_flow__distance(mg, add_to_grid=True, noclobber=False) # test that the flow__distance utility works as expected assert_array_equal(flow__distance_expected, flow__distance)
def test_flow__distance_regular_grid_d8(): """Test to demonstrate that flow__distance utility works as expected with regular grids""" # instantiate a model grid mg = RasterModelGrid((5, 4), spacing=(1, 1)) # instantiate an elevation array z = np.array([[0, 0, 0, 0], [0, 21, 10, 0], [0, 31, 20, 0], [0, 32, 30, 0], [0, 0, 0, 0]], dtype='float64') # add the elevation field to the grid mg.add_field('node', 'topographic__elevation', z) # instantiate the expected flow__distance array # considering flow directions calculated with D8 algorithm flow__distance_expected = np.array([[0, 0, 0, 0], [0, 1, 0, 0], [0, math.sqrt(2), 1, 0], [0, 1+math.sqrt(2), 2, 0], [0, 0, 0, 0]], dtype='float64') flow__distance_expected = np.reshape(flow__distance_expected, mg.number_of_node_rows * mg.number_of_node_columns) #setting boundary conditions mg.set_closed_boundaries_at_grid_edges(bottom_is_closed=True, left_is_closed=True, right_is_closed=True, top_is_closed=True) # calculating flow directions with FlowAccumulator component fr = FlowAccumulator(mg, flow_director='D8') fr.run_one_step() # calculating flow distance map flow__distance = calculate_flow__distance(mg, add_to_grid=True, noclobber=False) flow__distance = np.reshape(flow__distance, mg.number_of_node_rows * mg.number_of_node_columns) # modifying the flow distance map because boundary and outlet nodes should # not have flow__distance value different from 0 flow__distance[mg.boundary_nodes] = 0 outlet_id = 6 flow__distance[outlet_id] = 0 # test that the flow distance utility works as expected assert_array_equal(flow__distance_expected, flow__distance)
def test_route_to_multiple_error_raised(): mg = RasterModelGrid((10, 10)) z = mg.add_zeros("node", "topographic__elevation") z += mg.x_of_node + mg.y_of_node fa = FlowAccumulator(mg, flow_director="MFD") fa.run_one_step() with pytest.raises(NotImplementedError): ChiFinder(mg, min_drainage_area=1., reference_concavity=1.)
def test_bad_reference_area(): mg = RasterModelGrid((10, 10)) z = mg.add_zeros("node", "topographic__elevation") z += mg.x_of_node + mg.y_of_node fa = FlowAccumulator(mg) fa.run_one_step() with pytest.raises(ValueError): ChiFinder(mg, min_drainage_area=1.0, reference_area=-1)
def test_route_to_multiple_error_raised(): mg = RasterModelGrid((10, 10)) z = mg.add_zeros('node', 'topographic__elevation') 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 test_route_to_multiple_error_raised(): mg = RasterModelGrid((10, 10)) z = mg.add_zeros("node", "topographic__elevation") z += mg.x_of_node + mg.y_of_node fa = FlowAccumulator(mg, flow_director="MFD") fa.run_one_step() with pytest.raises(NotImplementedError): ErosionDeposition(mg, K=0.01, phi=0.0, v_s=0.001, m_sp=0.5, n_sp=1.0, sp_crit=0)
def test_route_to_multiple_error_raised(): mg = RasterModelGrid((10, 10)) z = mg.add_zeros("node", "topographic__elevation") z += mg.x_of_node + mg.y_of_node fa = FlowAccumulator(mg, flow_director="MFD") fa.run_one_step() with pytest.raises(NotImplementedError): TransportLengthHillslopeDiffuser(mg, erodibility=1.0, slope_crit=0.5)
def test_functions_with_Hex(): mg = HexModelGrid(10, 10) z = mg.add_zeros("node", "topographic__elevation") z += mg.x_of_node + mg.y_of_node fa = FlowAccumulator(mg) fa.run_one_step() ch = ChiFinder(mg, min_drainage_area=1.0, reference_concavity=1.0) ch.calculate_chi()
def test_flow__distance_irregular_grid_d4(): """Test to demonstrate that flow__distance utility works as expected with irregular grids""" # instantiate a model grid dx = 1.0 hmg = HexModelGrid(5, 3, dx) # instantiate and add the elevation field hmg.add_field( "topographic__elevation", hmg.node_x + np.round(hmg.node_y), at="node" ) # instantiate the expected flow__distance array flow__distance_expected = np.array( [ 0.0, 0.0, 0.0, 0.0, 0.0, dx, 0.0, 0.0, dx, dx, 2.0 * dx, 0.0, 0.0, 2.0 * dx, 2.0 * dx, 0.0, 0.0, 0.0, 0.0, ] ) # setting boundary conditions hmg.set_closed_nodes(hmg.boundary_nodes) # calculating flow directions with FlowAccumulator component: D4 algorithm fr = FlowAccumulator(hmg, flow_director="D4") fr.run_one_step() # calculating flow distance map flow__distance = calculate_flow__distance(hmg, add_to_grid=True, noclobber=False) # test that the flow__distance utility works as expected assert_almost_equal(flow__distance_expected, flow__distance, decimal=10)
def test_route_to_multiple_error_raised(): mg = RasterModelGrid((10, 10)) z = mg.add_zeros('node', 'topographic__elevation') z += mg.x_of_node + mg.y_of_node fa = FlowAccumulator(mg, flow_director='MFD') fa.run_one_step() with pytest.raises(NotImplementedError): Space(mg, K_sed=0.1, K_br=0.1, F_f=0.5, phi=0.1, H_star=1., v_s=0.001, m_sp=1.0, n_sp=0.5, sp_crit_sed=0, sp_crit_br=0)
def test_route_to_multiple_error_raised(): mg = RasterModelGrid((10, 10)) z = mg.add_zeros("node", "topographic__elevation") z += mg.x_of_node + mg.y_of_node fa = FlowAccumulator(mg, flow_director="MFD") fa.run_one_step() channel__mask = mg.zeros(at="node") with pytest.raises(NotImplementedError): DrainageDensity(mg, channel__mask=channel__mask)
def test_route_to_multiple_error_raised_run_StreamPowerEroder(): mg = RasterModelGrid((10, 10)) z = mg.add_zeros("node", "topographic__elevation") z += mg.x_of_node + mg.y_of_node sp = StreamPowerEroder(mg) fa = FlowAccumulator(mg, flow_director="MFD") fa.run_one_step() with pytest.raises(NotImplementedError): sp.run_one_step(10)
def __init__(self, grid, runoff_rate=1.0, roughness=0.01, changing_topo=False, depth_exp=1.5, weight=1.0, **kwds): """Initialize the KinwaveOverlandFlowModel. Parameters ---------- grid : ModelGrid Landlab ModelGrid object runoff_rate : float, optional (defaults to 1 mm/hr) Precipitation rate, mm/hr roughnes : float, defaults to 0.01 Manning roughness coefficient, s/m^1/3 changing_topo : boolean, optional (defaults to False) Flag indicating whether topography changes between time steps depth_exp : float (defaults to 1.5) Exponent on water depth in velocity equation (3/2 for Darcy/Chezy, 5/3 for Manning) weight : float (defaults to 1.0) Weighting on depth at new time step versus old time step (1 = all implicit; 0 = explicit) """ # Store grid and parameters and do unit conversion self._grid = grid self.runoff_rate = runoff_rate / 3600000.0 # convert to m/s self.vel_coef = 1.0 / roughness # do division now to save time self.changing_topo = changing_topo self.depth_exp = depth_exp self.weight = weight # Get elevation field try: self.elev = grid.at_node['topographic__elevation'] except: raise # Create fields... # Water depth if 'surface_water__depth' in grid.at_node: self.depth = grid.at_node['surface_water__depth'] else: self.depth = grid.add_zeros('node', 'surface_water__depth') # Slope if 'topographic__gradient' in grid.at_link: self.slope = grid.at_link['topographic__gradient'] else: self.slope = grid.add_zeros('link', 'topographic__gradient') # Velocity # if 'water__velocity' in grid.at_link: # self.vel = grid.at_link['water__velocity'] # else: # self.vel = grid.add_zeros('link', 'water__velocity') # Discharge # if 'surface_water__specific_discharge' in grid.at_link: # self.disch = grid.at_link['surface_water__specific_discharge'] # else: # self.disch = grid.add_zeros('link', # 'surface_water__specific_discharge') # Inflow discharge at nodes if 'surface_water_inflow__discharge' in grid.at_node: self.disch_in = grid.at_node['surface_water_inflow__discharge'] else: self.disch_in = grid.add_zeros('node', 'surface_water_inflow__discharge') # This array holds, for each node, the sum of sqrt(slope) x face width # for each link/face. self.grad_width_sum = grid.zeros('node') # This array holds the prefactor in the algebraic equation that we # will find a solution for. self.alpha = grid.zeros('node') # Instantiate flow router self.flow_accum = FlowAccumulator(grid, 'topographic__elevation', flow_director='MFD', partition_method='square_root_of_slope') # Flag to let us know whether this is our first iteration self.first_iteration = True
def test_fastscape(): input_str = os.path.join(_THIS_DIR, "drive_sp_params.txt") inputs = ModelParameterDictionary(input_str) nrows = inputs.read_int("nrows") ncols = inputs.read_int("ncols") dx = inputs.read_float("dx") dt = inputs.read_float("dt") time_to_run = inputs.read_float("run_time") uplift = inputs.read_float("uplift_rate") init_elev = inputs.read_float("init_elev") mg = RasterModelGrid(nrows, ncols, dx) mg.set_closed_boundaries_at_grid_edges(False, False, True, True) mg.add_zeros("topographic__elevation", at="node") z = mg.zeros(at="node") + init_elev numpy.random.seed(0) mg["node"]["topographic__elevation"] = z + numpy.random.rand(len(z)) / 1000. fr = FlowAccumulator(mg, flow_director="D8") fsp = Fsc(mg, input_str, method="D8") elapsed_time = 0. while elapsed_time < time_to_run: if elapsed_time + dt > time_to_run: dt = time_to_run - elapsed_time mg = fr.run_one_step() mg = fsp.erode(mg, dt=dt) mg.at_node["topographic__elevation"][mg.core_nodes] += uplift * dt elapsed_time += dt z_trg = numpy.array( [ 5.48813504e-04, 7.15189366e-04, 6.02763376e-04, 5.44883183e-04, 4.23654799e-04, 6.45894113e-04, 1.01830760e-02, 9.58036770e-03, 6.55865452e-03, 3.83441519e-04, 7.91725038e-04, 1.00142749e-02, 8.80798884e-03, 5.78387585e-03, 7.10360582e-05, 8.71292997e-05, 9.81911417e-03, 9.52243406e-03, 7.55093226e-03, 8.70012148e-04, 9.78618342e-04, 1.00629755e-02, 8.49253798e-03, 5.33216680e-03, 1.18274426e-04, 6.39921021e-04, 9.88956320e-03, 9.47119567e-03, 6.43790696e-03, 4.14661940e-04, 2.64555612e-04, 1.00450743e-02, 8.37262908e-03, 5.21540904e-03, 1.87898004e-05, 6.17635497e-04, 9.21286940e-03, 9.34022513e-03, 7.51114450e-03, 6.81820299e-04, 3.59507901e-04, 6.19166921e-03, 7.10456176e-03, 6.62585507e-03, 6.66766715e-04, 6.70637870e-04, 2.10382561e-04, 1.28926298e-04, 3.15428351e-04, 3.63710771e-04, ] ) assert_array_almost_equal(mg.at_node["topographic__elevation"], z_trg)
def __init__(self, input_file=None, params=None, BaselevelHandlerClass=None): """Initialize the BasicDd.""" # Call ErosionModel's init super(BasicDd, self).__init__(input_file=input_file, params=params, BaselevelHandlerClass=BaselevelHandlerClass) # Get Parameters and convert units if necessary: K_sp = self.get_parameter_from_exponent('K_sp', raise_error=False) K_ss = self.get_parameter_from_exponent('K_ss', raise_error=False) linear_diffusivity = ( self._length_factor**2.) * self.get_parameter_from_exponent( 'linear_diffusivity') # has units length^2/time # threshold has units of Length per Time which is what # StreamPowerSmoothThresholdEroder expects self.threshold_value = self._length_factor * self.get_parameter_from_exponent( 'erosion__threshold') # has units length/time # check that a stream power and a shear stress parameter have not both been given if K_sp != None and K_ss != None: raise ValueError('A parameter for both K_sp and K_ss has been' 'provided. Only one of these may be provided') elif K_sp != None or K_ss != None: if K_sp != None: self.K = K_sp else: self.K = (self._length_factor**( 1. / 3.)) * K_ss # K_ss has units Lengtg^(1/3) per Time else: raise ValueError('A value for K_sp or K_ss must be provided.') # Instantiate a FlowAccumulator with DepressionFinderAndRouter using D8 method self.flow_router = FlowAccumulator( self.grid, flow_director='D8', depression_finder=DepressionFinderAndRouter) # Create a field for the (initial) erosion threshold self.threshold = self.grid.add_zeros('node', 'erosion__threshold') self.threshold[:] = self.threshold_value # Instantiate a FastscapeEroder component self.eroder = StreamPowerSmoothThresholdEroder( self.grid, m_sp=self.params['m_sp'], n_sp=self.params['n_sp'], K_sp=self.K, 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'] # Instantiate a LinearDiffuser component self.diffuser = LinearDiffuser(self.grid, linear_diffusivity=linear_diffusivity)
class BasicRtSa(ErosionModel): """ A BasicSaRt computes erosion using linear diffusion, basic stream power with rock and till layers, and Q~A. It creates soil through weathering and consideres soil thickness in calculating hillslope diffusion. """ def __init__(self, input_file=None, params=None, BaselevelHandlerClass=None): """Initialize the BasicSaRt.""" # Call ErosionModel's init super(BasicSaRt, self).__init__(input_file=input_file, params=params, BaselevelHandlerClass=BaselevelHandlerClass) contact_zone__width = (self._length_factor) * self.params[ 'contact_zone__width'] # has units length self.K_rock_sp = self.get_parameter_from_exponent('K_rock_sp') self.K_till_sp = self.get_parameter_from_exponent('K_till_sp') linear_diffusivity = ( self._length_factor** 2.) * self.get_parameter_from_exponent('linear_diffusivity') # Set up rock-till self.setup_rock_and_till(self.params['rock_till_file__name'], self.K_rock_sp, self.K_till_sp, contact_zone__width) # Instantiate a FlowAccumulator with DepressionFinderAndRouter using D8 method self.flow_router = FlowAccumulator( self.grid, flow_director='D8', depression_finder=DepressionFinderAndRouter) # Instantiate a FastscapeEroder component self.eroder = FastscapeEroder(self.grid, K_sp=self.erody, m_sp=self.params['m_sp'], n_sp=self.params['n_sp']) # 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._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 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) def setup_rock_and_till(self, file_name, rock_erody, till_erody, contact_width): """Set up lithology handling for two layers with different erodibility. Parameters ---------- file_name : string Name of arc-ascii format file containing elevation of contact position at each grid node (or NODATA) Read elevation of rock-till contact from an esri-ascii format file containing the basal elevation value at each node, create a field for erodibility. Some considerations here: 1. We could represent the contact between two layers either as a depth below present land surface, or as an altitude. Using a depth would allow for vertical motion, because for a fixed surface, the depth remains constant while the altitude changes. But the depth must be updated every time the surface is eroded or aggrades. Using an altitude avoids having to update the contact position every time the surface erodes or aggrades, but any tectonic motion would need to be applied to the contact position as well. Here we'll use the altitude approach because this model was originally written for an application with lots of erosion expected but no tectonics. """ from landlab.io import read_esri_ascii # Read input data on rock-till contact elevation read_esri_ascii(file_name, grid=self.grid, name='rock_till_contact__elevation', halo=1) # Get a reference to the rock-till field self.rock_till_contact = self.grid.at_node[ 'rock_till_contact__elevation'] # Create field for erodibility if 'substrate__erodibility' in self.grid.at_node: self.erody = self.grid.at_node['substrate__erodibility'] else: self.erody = self.grid.add_zeros('node', 'substrate__erodibility') # Create array for erodibility weighting function self.erody_wt = np.zeros(self.grid.number_of_nodes) # Read the erodibility value of rock and till self.rock_erody = rock_erody self.till_erody = till_erody # Read and remember the contact zone characteristic width self.contact_width = contact_width def update_erodibility_field(self): """Update erodibility at each node based on elevation relative to contact elevation. To promote smoothness in the solution, the erodibility at a given point (x,y) is set as follows: 1. Take the difference between elevation, z(x,y), and contact elevation, b(x,y): D(x,y) = z(x,y) - b(x,y). This number could be positive (if land surface is above the contact), negative (if we're well within the rock), or zero (meaning the rock-till contact is right at the surface). 2. Define a smoothing function as: $F(D) = 1 / (1 + exp(-D/D*))$ This sigmoidal function has the property that F(0) = 0.5, F(D >> D*) = 1, and F(-D << -D*) = 0. Here, D* describes the characteristic width of the "contact zone", where the effective erodibility is a mixture of the two. If the surface is well above this contact zone, then F = 1. If it's well below the contact zone, then F = 0. 3. Set the erodibility using F: $K = F K_till + (1-F) K_rock$ So, as F => 1, K => K_till, and as F => 0, K => K_rock. In between, we have a weighted average. Translating these symbols into variable names: z = self.elev b = self.rock_till_contact D* = self.contact_width F = self.erody_wt K_till = self.till_erody K_rock = self.rock_erody """ # Update the erodibility weighting function (this is "F") self.erody_wt[self.data_nodes] = (1.0 / (1.0 + np.exp( -(self.z[self.data_nodes] - self.rock_till_contact[self.data_nodes]) / self.contact_width))) # (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.till_erody = self.K_till_sp * erode_factor self.rock_erody = self.K_rock_sp * erode_factor # Calculate the effective erodibilities using weighted averaging self.erody[:] = (self.erody_wt * self.till_erody + (1.0 - self.erody_wt) * self.rock_erody) 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] # Update the erodibility field self.update_erodibility_field() # Do some erosion (but not on the flooded nodes) self.eroder.run_one_step(dt, flooded_nodes=flooded, K_if_used=self.erody) # 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()
class BasicHySt(_StochasticErosionModel): """ A BasicHySt computes erosion using (1) hybrid alluvium river erosion, (2) linear nhillslope diffusion, and (3) generation of a random sequence of runoff events across a topographic surface. Examples -------- >>> from erosion_model import StochasticRainDepthDepThresholdModel >>> my_pars = {} >>> my_pars['dt'] = 1.0 >>> my_pars['run_duration'] = 1.0 >>> my_pars['infiltration_capacity'] = 1.0 >>> my_pars['K_sp'] = 1.0 >>> my_pars['threshold_sp'] = 1.0 >>> my_pars['linear_diffusivity'] = 0.01 >>> my_pars['mean_storm_duration'] = 0.002 >>> my_pars['mean_interstorm_duration'] = 0.008 >>> my_pars['mean_storm_depth'] = 0.025 >>> srt = StochasticRainDepthDepThresholdModel(params=my_pars) Warning: no DEM specified; creating 4x5 raster grid """ def __init__(self, input_file=None, params=None, BaselevelHandlerClass=None): """Initialize the BasicHySt.""" # Call ErosionModel's init super(BasicHySt, self).__init__(input_file=input_file, params=params, BaselevelHandlerClass=BaselevelHandlerClass) # Get Parameters: K = ((self._length_factor**0.5) # K_stochastic [=] L^(1/2) T^-(1/2) * self.get_parameter_from_exponent('K_stochastic_sp')) linear_diffusivity = ( (self._length_factor**2) * self.get_parameter_from_exponent('linear_diffusivity')) # L^2/T v_s = (self._length_factor) * self.get_parameter_from_exponent( 'v_s') # has units length per time # Instantiate a FlowAccumulator with DepressionFinderAndRouter using D8 method self.flow_router = FlowAccumulator( self.grid, flow_director='D8', depression_finder=DepressionFinderAndRouter) #set methods and fields. method = 'simple_stream_power' discharge_method = 'discharge_field' area_field = None discharge_field = 'surface_water__discharge' # instantiate rain generator self.instantiate_rain_generator() # 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 infiltration_capacity = (self._length_factor * self.params['infiltration_capacity']) # L/T self.infilt = infiltration_capacity # Run flow routing and lake filler self.flow_router.run_one_step() # Keep a reference to drainage area self.area = self.grid.at_node['drainage_area'] # Handle solver option try: solver = self.params['solver'] except: solver = 'original' # Instantiate an ErosionDeposition component self.eroder = ErosionDeposition(self.grid, K=K, F_f=self.params['F_f'], phi=self.params['phi'], v_s=v_s, 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=solver) # Instantiate a LinearDiffuser component self.diffuser = LinearDiffuser(self.grid, linear_diffusivity=linear_diffusivity) def calc_runoff_and_discharge(self): """Calculate runoff rate and discharge; return runoff.""" if self.rain_rate > 0.0 and self.infilt > 0.0: runoff = self.rain_rate - ( self.infilt * (1.0 - np.exp(-self.rain_rate / self.infilt))) if runoff < 0: runoff = 0 else: runoff = self.rain_rate self.discharge[:] = runoff * self.area return runoff 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] # Handle water erosion self.handle_water_erosion(dt, flooded) # Do some soil creep self.diffuser.run_one_step(dt) # calculate model time self.model_time += dt # Lower outlet self.update_outlet(dt) # Check walltime self.check_walltime()
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("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.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])
def test_flow__distance_raster_MFD_diagonals_true(): """Test of flow__distance utility with a raster grid and MFD.""" # instantiate a model grid mg = RasterModelGrid((5, 4), xy_spacing=(1, 1)) # instantiate an elevation array z = np.array( [[0, 0, 0, 0], [0, 21, 10, 0], [0, 31, 20, 0], [0, 32, 30, 0], [0, 0, 0, 0]], dtype="float64", ) # add the elevation field to the grid mg.add_field("node", "topographic__elevation", z) # instantiate the expected flow__distance array # considering flow directions calculated with MFD algorithm flow__distance_expected = np.array( [ [0, 0, 0, 0], [0, 1, 0, 0], [0, math.sqrt(2), 1, 0], [0, 1 + math.sqrt(2), 2, 0], [0, 0, 0, 0], ], dtype="float64", ) flow__distance_expected = np.reshape( flow__distance_expected, mg.number_of_node_rows * mg.number_of_node_columns) # setting boundary conditions mg.set_closed_boundaries_at_grid_edges( bottom_is_closed=True, left_is_closed=True, right_is_closed=True, top_is_closed=True, ) # calculating flow directions with FlowAccumulator component fa = FlowAccumulator(mg, "topographic__elevation", flow_director="MFD", diagonals=True) fa.run_one_step() # calculating flow distance map flow__distance = calculate_flow__distance(mg, add_to_grid=True, noclobber=False) # test that the flow__distance utility works as expected assert_array_equal(flow__distance_expected, flow__distance)
catagory_weight_file = os.path.join(os.path.abspath(os.sep), *['work', 'WVDP_EWG_STUDY3', 'study3py','auxillary_inputs', 'weights', 'sew.chi_elev_weight.20.txt']) cat_wt = np.loadtxt(catagory_weight_file) #%% input_file = glob.glob(os.path.join(os.sep, *(work_dir+['model**', '**', 'inputs_template.txt'])))[0] with open(input_file, 'r') as f: inputs = yaml.load(f) outlet_id = inputs['outlet_id'] # observed_topography observed_topo_file_name = inputs['modern_dem_name'] (grid, z) = read_esri_ascii(observed_topo_file_name, name='topographic__elevation', halo=1) grid.set_watershed_boundary_condition_outlet_id(inputs['outlet_id'], z, nodata_value=-9999) fa = FlowAccumulator(grid, flow_director='D8', depression_finder = 'DepressionFinderAndRouter') fa.run_one_step() #hs = grid.calc_hillshade_at_node() ch = ChiFinder(grid, min_drainage_area=10*grid.dx**2) ch.calculate_chi() # initial condition topography initial_topo_file_name = inputs['DEM_filename'] (igrid, iz) = read_esri_ascii(initial_topo_file_name, name='topographic__elevation', halo=1) igrid.set_watershed_boundary_condition_outlet_id(inputs['outlet_id'], iz, nodata_value=-9999) ifa = FlowAccumulator(igrid, flow_director='D8', depression_finder = 'DepressionFinderAndRouter') ifa.run_one_step() #ihs = grid.calc_hillshade_at_node()
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)) >>> 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.0e-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 != "D8": assert routing == "D4" self._routing = routing if (type(self._grid) is landlab.grid.raster.RasterModelGrid) and (routing == "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.0) assert elev_increment > 0.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.0) * 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.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.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
# create the fields in the grid mg.add_zeros("topographic__elevation", at="node") z = mg.zeros(at="node") + init_elev mg["node"]["topographic__elevation"] = z + numpy.random.rand(len(z)) / 1000. mg.add_zeros("node", "water__unit_flux_in") # make some K values in a field to test # mg.at_node['K_values'] = 0.1+numpy.random.rand(nrows*ncols)/10. mg.at_node["K_values"] = numpy.empty(nrows * ncols, dtype=float) # mg.at_node['K_values'].fill(0.1+numpy.random.rand()/10.) mg.at_node["K_values"].fill(0.001) print("Running ...") # instantiate the components: fr = FlowAccumulator(mg, flow_director="D8") sp = StreamPowerEroder(mg, input_file_string) # fsp = FastscapeEroder(mg, input_file_string) precip = PrecipitationDistribution(input_file=input_file_string) # load the Fastscape module too, to allow direct comparison fsp = FastscapeEroder(mg, input_file_string) try: # raise NameError mg = copy.deepcopy(mg_mature) except NameError: print("building a new grid...") out_interval = 50000. last_trunc = time_to_run # we use this to trigger taking an output plot # run to a steady state:
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.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)) >>> 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( ... "topographic__elevation", ... z, ... at="node", ... 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 References ---------- **Required Software Citation(s) Specific to this Component** None Listed **Additional References** Tucker, G., Lancaster, S., Gasparini, N., Bras, R., Rybarczyk, S. (2001). An object-oriented framework for distributed hydrologic and geomorphic modeling using triangulated irregular networks. Computers & Geosciences 27(8), 959-973. https://dx.doi.org/10.1016/s0098-3004(00)00134-5 """ _name = "SinkFiller" _unit_agnostic = True _info = { "sediment_fill__depth": { "dtype": float, "intent": "out", "optional": False, "units": "m", "mapping": "node", "doc": "Depth of sediment added at eachnode", }, "topographic__elevation": { "dtype": float, "intent": "inout", "optional": False, "units": "m", "mapping": "node", "doc": "Land surface topographic elevation", }, } def __init__(self, grid, routing="D8", apply_slope=False, fill_slope=1.0e-5): """ 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. """ super().__init__(grid) if "flow__receiver_node" in grid.at_node: if grid.at_node["flow__receiver_node"].size != grid.size("node"): msg = ( "A route-to-multiple flow director has been " "run on this grid. The landlab development team has not " "verified that SinkFiller is compatible with " "route-to-multiple methods. Please open a GitHub Issue " "to start this process.") raise NotImplementedError(msg) if routing != "D8": assert routing == "D4" self._routing = routing if isinstance(self._grid, RasterModelGrid) and (routing == "D8"): self._D8 = True self._num_nbrs = 8 else: self._D8 = False # useful shorthand for thia test we do a lot if isinstance(self._grid, RasterModelGrid): self._num_nbrs = 4 self._fill_slope = fill_slope self._apply_slope = apply_slope self._elev = self._grid.at_node["topographic__elevation"] self._topo_field_name = "topographic__elevation" # create the only new output field: self._sed_fill_depth = self._grid.add_zeros("node", "sediment_fill__depth", clobber=True) self._lf = DepressionFinderAndRouter(self._grid, routing=self._routing, reroute_flow=True) self._fr = FlowAccumulator(self._grid, flow_director=self._routing) def fill_pits(self): """This is a synonym for the main method :func:`run_one_step`.""" self.run_one_step() def run_one_step(self): """This is the main method. Call it to fill depressions in a starting topography. """ 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() # 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.0) assert elev_increment > 0.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.0) * 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 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 != self._grid.BAD_INDEX] 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 != self._grid.BAD_INDEX] 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 == self._grid.BAD_INDEX] = -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 __init__(self, grid, routing="D8", apply_slope=False, fill_slope=1.0e-5): """ 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. """ super().__init__(grid) if "flow__receiver_node" in grid.at_node: if grid.at_node["flow__receiver_node"].size != grid.size("node"): msg = ( "A route-to-multiple flow director has been " "run on this grid. The landlab development team has not " "verified that SinkFiller is compatible with " "route-to-multiple methods. Please open a GitHub Issue " "to start this process.") raise NotImplementedError(msg) if routing != "D8": assert routing == "D4" self._routing = routing if isinstance(self._grid, RasterModelGrid) and (routing == "D8"): self._D8 = True self._num_nbrs = 8 else: self._D8 = False # useful shorthand for thia test we do a lot if isinstance(self._grid, RasterModelGrid): self._num_nbrs = 4 self._fill_slope = fill_slope self._apply_slope = apply_slope self._elev = self._grid.at_node["topographic__elevation"] self._topo_field_name = "topographic__elevation" # create the only new output field: self._sed_fill_depth = self._grid.add_zeros("node", "sediment_fill__depth", clobber=True) self._lf = DepressionFinderAndRouter(self._grid, routing=self._routing, reroute_flow=True) self._fr = FlowAccumulator(self._grid, flow_director=self._routing)
def __init__(self, input_file=None, params=None, BaselevelHandlerClass=None): """Initialize the BasicSa.""" # Call ErosionModel's init super(BasicSa, self).__init__(input_file=input_file, params=params, BaselevelHandlerClass=BaselevelHandlerClass) # Get Parameters and convert units if necessary: self.K_sp = self.get_parameter_from_exponent('K_sp') 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) # Instantiate a FastscapeEroder component self.eroder = FastscapeEroder(self.grid, K_sp=self.K_sp, m_sp=self.params['m_sp'], n_sp=self.params['n_sp']) # 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 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)
rmg.set_nodata_nodes_to_closed(rmg.at_node['topographic__elevation'], -9999) imshow_grid(rmg, 'topographic__elevation') sfb = SinkFillerBarnes(rmg, fill_flat=False) sfb.run_one_step() # Set watershed boundary condition - use lowest elevation if basic call fails rmg.set_watershed_boundary_condition(z) fa = FlowAccumulator( rmg, # surface='topographic__elevation', flow_director='FlowDirectorD8', # runoff_rate=None, # depression_finder='DepressionFinderAndRouter', # routing='D4' ) fa.run_one_step() (da, q) = fa.accumulate_flow() outlet_node_to_sample = np.argmax(rmg.at_node['drainage_area']) print('Outlet Node = ' + str(outlet_node_to_sample) + '; Drainage Area= ' + str(da[outlet_node_to_sample] / 1000000) + ' km^2; Elev = ' + str(round(z[outlet_node_to_sample], 1)) + ' m') # For each of the links, there is a tail and head and node. # Look at tail nodes of all links, and look at node area, find link that connects
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("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, 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)
def test_flow__distance_regular_grid_d8(): """Test to demonstrate that flow__distance utility works as expected with regular grids""" # instantiate a model grid mg = RasterModelGrid((5, 4), xy_spacing=(1, 1)) # instantiate an elevation array z = np.array( [[0, 0, 0, 0], [0, 21, 10, 0], [0, 31, 20, 0], [0, 32, 30, 0], [0, 0, 0, 0]], dtype="float64", ) # add the elevation field to the grid mg.add_field("node", "topographic__elevation", z) # instantiate the expected flow__distance array # considering flow directions calculated with D8 algorithm flow__distance_expected = np.array( [ [0, 0, 0, 0], [0, 1, 0, 0], [0, math.sqrt(2), 1, 0], [0, 1 + math.sqrt(2), 2, 0], [0, 0, 0, 0], ], dtype="float64", ) flow__distance_expected = np.reshape( flow__distance_expected, mg.number_of_node_rows * mg.number_of_node_columns) # setting boundary conditions mg.set_closed_boundaries_at_grid_edges( bottom_is_closed=True, left_is_closed=True, right_is_closed=True, top_is_closed=True, ) # calculating flow directions with FlowAccumulator component fr = FlowAccumulator(mg, flow_director="D8") fr.run_one_step() # calculating flow distance map flow__distance = calculate_flow__distance(mg, add_to_grid=True, noclobber=False) flow__distance = np.reshape( flow__distance, mg.number_of_node_rows * mg.number_of_node_columns) # modifying the flow distance map because boundary and outlet nodes should # not have flow__distance value different from 0 flow__distance[mg.boundary_nodes] = 0 outlet_id = 6 flow__distance[outlet_id] = 0 # test that the flow distance utility works as expected assert_array_equal(flow__distance_expected, flow__distance)
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("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.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)
init_elev = inputs.read_float('init_elev') mg = RasterModelGrid(nrows, ncols, dx) #create the fields in the grid mg.add_zeros('topographic__elevation', at='node') z = mg.zeros(at='node') + init_elev mg['node']['topographic__elevation'] = z + numpy.random.rand(len(z)) / 1000. #make some K values in a field to test mg.at_node['K_values'] = 0.1 + numpy.random.rand(nrows * ncols) / 10. print('Running ...') #instantiate the components: fr = FlowAccumulator(mg, flow_director='D8') sp = StreamPowerEroder(mg, './drive_sp_params.txt') #load the Fastscape module too, to allow direct comparison fsp = FastscapeEroder(mg, './drive_sp_params.txt') #perform the loop: elapsed_time = 0. #total time in simulation while elapsed_time < time_to_run: print(elapsed_time) if elapsed_time + dt > time_to_run: print("Short step!") dt = time_to_run - elapsed_time mg = fr.run_one_step() #print 'Area: ', numpy.max(mg.at_node['drainage_area']) #mg = fsp.erode(mg) mg = fsp.erode(mg, K_if_used='K_values')
import numpy as np from matplotlib.pyplot import figure, show from six.moves import range from landlab import VoronoiDelaunayGrid from landlab.components import FlowAccumulator, StreamPowerEroder from landlab.plot.imshow import imshow_node_grid nnodes = 10000 x, y = np.random.rand(nnodes), np.random.rand(nnodes) mg = VoronoiDelaunayGrid(x, y) z = mg.add_field( "node", "topographic__elevation", np.random.rand(nnodes) / 10000., copy=False ) fr = FlowAccumulator(mg) spe = StreamPowerEroder(mg, "drive_sp_params_voronoi.txt") for i in range(100): z[mg.core_nodes] += 0.01 fr.run_one_step() spe.erode(mg, 1.) imshow_node_grid(mg, "topographic__elevation") show()
class BasicChSa(ErosionModel): """ A BasicChSa model computes erosion using depth-dependent cubic diffusion with a soil layer, basic stream power, and Q~A. """ def __init__(self, input_file=None, params=None, BaselevelHandlerClass=None): """Initialize the BasicChSa model.""" # Call ErosionModel's init super(BasicChSa, self).__init__(input_file=input_file, params=params, BaselevelHandlerClass=BaselevelHandlerClass) self.K_sp = self.get_parameter_from_exponent('K_sp') 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 # 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') soil_thickness[:] = initial_soil_thickness bedrock_elev[:] = self.z - initial_soil_thickness # Instantiate a FlowAccumulator with DepressionFinderAndRouter using D8 method self.flow_router = FlowAccumulator(self.grid, flow_director='D8', depression_finder = DepressionFinderAndRouter) # Instantiate a FastscapeEroder component self.eroder = FastscapeEroder(self.grid, K_sp=self.K_sp, m_sp=self.params['m_sp'], n_sp=self.params['n_sp']) # Instantiate a weathering component self.weatherer = ExponentialWeatherer(self.grid, max_soil_production_rate=max_soil_production_rate, soil_production_decay_depth=soil_production_decay_depth) # Instantiate a soil-transport component self.diffuser = DepthDependentTaylorDiffuser(self.grid, linear_diffusivity=linear_diffusivity, slope_crit=self.params['slope_crit'], soil_transport_decay_depth=soil_transport_decay_depth, nterms=11) 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] # Do some erosion (but not on the flooded nodes) # (if we're varying K through time, update that first) if self.opt_var_precip: self.eroder.K = (self.K_sp * self.pc.get_erodibility_adjustment_factor(self.model_time)) 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() # Do some soil creep self.diffuser.run_one_step(dt, dynamic_dt=True, if_unstable='raise', courant_factor=0.1) # calculate model time self.model_time += dt # Lower outlet self.update_outlet(dt) # Check walltime self.check_walltime()
from landlab.components import FlowAccumulator, SedDepEroder from landlab import RasterModelGrid import landlab import numpy as np from pylab import show, figure from landlab.io import read_esri_ascii (mg, z) = read_esri_ascii("bacia_piratini_90m.asc", name="topographic__elevation") fda = FlowAccumulator(mg, 'topographic__elevation') sde1 = SedDepEroder(mg, Qc="MPM") fda.run_one_step() sde1.run_one_step(dt=30) #dt em anos print(sde1.characteristic_grainsize)
def test_MassBalance(): # %% # set up a 15x15 grid with one open outlet node and low initial elevations. nr = 15 nc = 15 mg = RasterModelGrid((nr, nc), xy_spacing=10.0) z = mg.add_zeros("topographic__elevation", at="node") br = mg.add_zeros("bedrock__elevation", at="node") soil = mg.add_zeros("soil__depth", at="node") 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[:] += 0.0 # initial condition of no soil depth. br[:] = z[:] z[:] += soil[:] # Create a D8 flow handler # fa = PriorityFloodFlowRouter(mg, surface="topographic__elevation", flow_metric = 'D8',suppress_out=True) # fa.run_one_step() # Create a D8 flow handler fa = FlowAccumulator(mg, flow_director="D8", depression_finder="DepressionFinderAndRouter") # Parameter values for detachment-limited test K_br = 0.002 K_sed = 0.002 U = 0.0001 dt = 10.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 = SpaceLargeScaleEroder( 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, ) # Get values before run z = mg.at_node["topographic__elevation"] br = mg.at_node["bedrock__elevation"] H = mg.at_node["soil__depth"] cores = mg.core_nodes area = mg.cell_area_at_node # ... and run it to steady state (10000x1-year timesteps). for _ in range(10000): fa.run_one_step() soil_B = cp.deepcopy(H) bed_B = cp.deepcopy(br) vol_SSY_riv, V_leaving_riv = sp.run_one_step(dt=dt) diff_MB = (np.sum((bed_B[cores] - br[cores]) * area[cores]) + np.sum( (soil_B[cores] - H[cores]) * area[cores]) * (1 - sp._phi) - vol_SSY_riv * dt - V_leaving_riv) br[mg.core_nodes] += U * dt # m soil[ 0] = 0.0 # enforce 0 soil depth at boundary to keep lowering steady z[:] = br[:] + soil[:] # Test Every iteration testing.assert_array_almost_equal( z[cores], br[cores] + H[cores], decimal=5, err_msg= "Topography does not equal sum of bedrock and soil! Decrease timestep", verbose=True, ) testing.assert_array_less( abs(diff_MB), 1e-8 * mg.number_of_nodes, err_msg= "Mass balance error SpaceLargeScaleEroder! Try to resolve by becreasing timestep", verbose=True, )
class BasicDd(ErosionModel): """ A BasicDd computes erosion using linear diffusion, stream power with a smoothed threshold that is proportional to depth, and Q~A. """ def __init__(self, input_file=None, params=None, BaselevelHandlerClass=None): """Initialize the BasicDd.""" # Call ErosionModel's init super(BasicDd, self).__init__(input_file=input_file, params=params, BaselevelHandlerClass=BaselevelHandlerClass) # Get Parameters and convert units if necessary: K_sp = self.get_parameter_from_exponent('K_sp', raise_error=False) K_ss = self.get_parameter_from_exponent('K_ss', raise_error=False) linear_diffusivity = ( self._length_factor**2.) * self.get_parameter_from_exponent( 'linear_diffusivity') # has units length^2/time # threshold has units of Length per Time which is what # StreamPowerSmoothThresholdEroder expects self.threshold_value = self._length_factor * self.get_parameter_from_exponent( 'erosion__threshold') # has units length/time # check that a stream power and a shear stress parameter have not both been given if K_sp != None and K_ss != None: raise ValueError('A parameter for both K_sp and K_ss has been' 'provided. Only one of these may be provided') elif K_sp != None or K_ss != None: if K_sp != None: self.K = K_sp else: self.K = (self._length_factor**( 1. / 3.)) * K_ss # K_ss has units Lengtg^(1/3) per Time else: raise ValueError('A value for K_sp or K_ss must be provided.') # Instantiate a FlowAccumulator with DepressionFinderAndRouter using D8 method self.flow_router = FlowAccumulator( self.grid, flow_director='D8', depression_finder=DepressionFinderAndRouter) # Create a field for the (initial) erosion threshold self.threshold = self.grid.add_zeros('node', 'erosion__threshold') self.threshold[:] = self.threshold_value # Instantiate a FastscapeEroder component self.eroder = StreamPowerSmoothThresholdEroder( self.grid, m_sp=self.params['m_sp'], n_sp=self.params['n_sp'], K_sp=self.K, 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'] # Instantiate a LinearDiffuser component self.diffuser = LinearDiffuser(self.grid, linear_diffusivity=linear_diffusivity) def update_erosion_threshold_values(self): """Updates the erosion threshold at each node based on cumulative erosion so far.""" # Set the erosion threshold. # # Note that a minus sign is used because cum ero depth is negative for # erosion, positive for deposition. # The second line handles the case where there is growth, in which case # we want the threshold to stay at its initial value rather than # getting smaller. cum_ero = self.grid.at_node['cumulative_erosion__depth'] cum_ero[:] = (self.z - self.grid.at_node['initial_topographic__elevation']) self.threshold[:] = (self.threshold_value - (self.thresh_change_per_depth * cum_ero)) self.threshold[self.threshold < self.threshold_value] = \ self.threshold_value 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] # Calculate the new threshold values given cumulative erosion self.update_erosion_threshold_values() # Do some erosion (but not on the flooded nodes) # (if we're varying K through time, update that first) if self.opt_var_precip: self.eroder.K = ( self.K * self.pc.get_erodibility_adjustment_factor(self.model_time)) self.eroder.run_one_step(dt, flooded_nodes=flooded) # Do some soil creep self.diffuser.run_one_step(dt) # calculate model time self.model_time += dt # Lower outlet self.update_outlet(dt) # Check walltime self.check_walltime()
def test_soil_field_already_on_grid(): # %% """ Test that an existing soil grid field is not changed by instantiating SpaceLargeScaleEroder. """ # 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("topographic__elevation", at="node") br = mg.add_zeros("bedrock__elevation", at="node") soil = mg.add_zeros("soil__depth", at="node") soil += 1.0 # add 1m of soil everywehre 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.0) br[:] = z - soil # Create a D8 flow handler FlowAccumulator(mg, flow_director="D8") # Instantiate SpaceLargeScaleEroder sp = SpaceLargeScaleEroder( mg, K_sed=0.01, K_br=0.01, F_f=0.0, phi=0.0, v_s=0.001, m_sp=0.5, n_sp=1.0, sp_crit_sed=0, sp_crit_br=0, ) # ensure that 'soil__depth' field is everywhere equal to 1.0 m. testing.assert_array_equal( np.ones(mg.number_of_nodes), sp._soil__depth, err_msg="SpaceLargeScaleEroder soil depth field test failed", verbose=True, ) # %% Check getters testing.assert_array_equal( 0.01, sp.K_br, err_msg="Parameter value issue", verbose=True, ) testing.assert_array_equal( 0.01, sp.K_sed, err_msg="Parameter value issue", verbose=True, ) # sediment erosion is zero before running the component testing.assert_array_equal( np.zeros(mg.number_of_nodes), sp.Es, err_msg="Parameter value issue", verbose=True, ) # rock erosion is zero before running the component testing.assert_array_equal( np.zeros(mg.number_of_nodes), sp.Er, err_msg="Parameter value issue", verbose=True, ) # %% Check setters sp.K_br = 0.02 testing.assert_array_equal( 0.02, sp.K_br, err_msg="Parameter value issue", verbose=True, ) sp.K_sed = 0.02 testing.assert_array_equal( 0.02, sp.K_sed, err_msg="Parameter value issue", verbose=True, ) with pytest.raises(AttributeError): sp.Es = np.zeros(mg.number_of_nodes) with pytest.raises(AttributeError): sp.Er = np.zeros(mg.number_of_nodes)
class KinwaveImplicitOverlandFlow(Component): """ Calculate shallow water flow over topography. Landlab component that implements a two-dimensional kinematic wave model. This is a form of the 2D shallow-water equations in which energy slope is assumed to equal bed slope. The solution method is locally implicit, and works as follows. At each time step, we iterate from upstream to downstream over the topography. Because we are working downstream, we can assume that we know the total water inflow to a given cell. We solve the following mass conservation equation at each cell: $(H^{t+1} - H^t)/\Delta t = Q_{in}/A - Q_{out}/A + R$ where $H$ is water depth, $t$ indicates time step number, $\Delta t$ is time step duration, $Q_{in}$ is total inflow discharge, $Q_{out}$ is total outflow discharge, $A$ is cell area, and $R$ is local runoff rate (precipitation minus infiltration; could be negative if runon infiltration is occurring). The specific outflow discharge leaving a cell along one of its faces is: $q = (1/C_r) H^\alpha S^{1/2}$ where $C_r$ is a roughness coefficient (such as Manning's n), $\alpha$ is an exponent equal to 5/3 for the Manning equation and 3/2 for the Chezy family, and $S$ is the downhill-positive gradient of the link that crosses this particular face. Outflow discharge is zero for links that are flat or "uphill" from the given node. Total discharge out of a cell is then the sum of (specific discharge x face width) over all outflow faces $Q_{out} = \sum_{i=1}^N (1/C_r) H^\alpha S_i^{1/2} W_i$ where $N$ is the number of outflow faces (i.e., faces where the ground slopes downhill away from the cell's node), and $W_i$ is the width of face $i$. We use the depth at the cell's node, so this simplifies to: $Q_{out} = (1/C_r) H'^\alpha \sum_{i=1}^N S_i^{1/2} W_i$ We define $H$ in the above as a weighted sum of the "old" (time step $t$) and "new" (time step $t+1$) depth values: $H' = w H^{t+1} + (1-w) H^t$ If $w=1$, the method is fully implicit. If $w=0$, it is a simple forward explicit method. When we combine these equations, we have an equation that includes the unknown $H^{t+1}$ and a bunch of terms that are known. If $w\ne 0$, it is a nonlinear equation in $H^{t+1}$, and must be solved iteratively. We do this using a root-finding method in the scipy.optimize library. Construction: KinwaveImplicitOverlandFlow(grid, precip_rate=1.0, precip_duration=1.0, infilt_rate=0.0, roughness=0.01, **kwds) Parameters ---------- grid : ModelGrid A Landlab grid object. precip_rate : float, optional (defaults to 1 mm/hr) Precipitation rate, mm/hr precip_duration : float, optional (defaults to 1 hour) Duration of precipitation, hours infilt_rate : float, optional (defaults to 0) Maximum rate of infiltration, mm/hr roughnes : float, defaults to 0.01 Manning roughness coefficient, s/m^1/3 Examples -------- >>> from landlab import RasterModelGrid >>> rg = RasterModelGrid((4, 5), 10.0) >>> z = rg.add_zeros('node', 'topographic__elevation') >>> kw = KinwaveImplicitOverlandFlow(rg) >>> round(kw.runoff_rate * 1.0e7, 2) 2.78 >>> kw.vel_coef # default value 100.0 >>> rg.at_node['surface_water__depth'][6:9] array([ 0., 0., 0.]) """ _name = 'KinwaveImplicitOverlandFlow' _input_var_names = ( 'topographic__elevation', ) _output_var_names = ( 'topographic__gradient', 'surface_water__depth', #'water__velocity', #'water__specific_discharge', 'surface_water_inflow__discharge', ) _var_units = { 'topographic__elevation': 'm', 'topographic__slope': 'm/m', 'surface_water__depth': 'm', 'water__velocity': 'm/s', 'water__specific_discharge': 'm2/s', } _var_mapping = { 'topographic__elevation': 'node', 'topographic__gradient': 'link', 'surface_water__depth': 'node', #'water__velocity': 'link', #'water__specific_discharge': 'link', 'surface_water_inflow__discharge' : 'node', } _var_doc = { 'topographic__elevation': 'elevation of the ground surface relative to some datum', 'topographic__gradient': 'gradient of the ground surface', 'surface_water__depth': 'depth of water', # 'water__velocity': # 'flow velocity component in the direction of the link', # 'water__specific_discharge': # 'flow discharge component in the direction of the link', 'surface_water_inflow__discharge': 'water volume inflow rate to the cell around each node' } def __init__(self, grid, runoff_rate=1.0, roughness=0.01, changing_topo=False, depth_exp=1.5, weight=1.0, **kwds): """Initialize the KinwaveOverlandFlowModel. Parameters ---------- grid : ModelGrid Landlab ModelGrid object runoff_rate : float, optional (defaults to 1 mm/hr) Precipitation rate, mm/hr roughnes : float, defaults to 0.01 Manning roughness coefficient, s/m^1/3 changing_topo : boolean, optional (defaults to False) Flag indicating whether topography changes between time steps depth_exp : float (defaults to 1.5) Exponent on water depth in velocity equation (3/2 for Darcy/Chezy, 5/3 for Manning) weight : float (defaults to 1.0) Weighting on depth at new time step versus old time step (1 = all implicit; 0 = explicit) """ # Store grid and parameters and do unit conversion self._grid = grid self.runoff_rate = runoff_rate / 3600000.0 # convert to m/s self.vel_coef = 1.0 / roughness # do division now to save time self.changing_topo = changing_topo self.depth_exp = depth_exp self.weight = weight # Get elevation field try: self.elev = grid.at_node['topographic__elevation'] except: raise # Create fields... # Water depth if 'surface_water__depth' in grid.at_node: self.depth = grid.at_node['surface_water__depth'] else: self.depth = grid.add_zeros('node', 'surface_water__depth') # Slope if 'topographic__gradient' in grid.at_link: self.slope = grid.at_link['topographic__gradient'] else: self.slope = grid.add_zeros('link', 'topographic__gradient') # Velocity # if 'water__velocity' in grid.at_link: # self.vel = grid.at_link['water__velocity'] # else: # self.vel = grid.add_zeros('link', 'water__velocity') # Discharge # if 'surface_water__specific_discharge' in grid.at_link: # self.disch = grid.at_link['surface_water__specific_discharge'] # else: # self.disch = grid.add_zeros('link', # 'surface_water__specific_discharge') # Inflow discharge at nodes if 'surface_water_inflow__discharge' in grid.at_node: self.disch_in = grid.at_node['surface_water_inflow__discharge'] else: self.disch_in = grid.add_zeros('node', 'surface_water_inflow__discharge') # This array holds, for each node, the sum of sqrt(slope) x face width # for each link/face. self.grad_width_sum = grid.zeros('node') # This array holds the prefactor in the algebraic equation that we # will find a solution for. self.alpha = grid.zeros('node') # Instantiate flow router self.flow_accum = FlowAccumulator(grid, 'topographic__elevation', flow_director='MFD', partition_method='square_root_of_slope') # Flag to let us know whether this is our first iteration self.first_iteration = True def run_one_step(self, dt, current_time=0.0, runoff_rate=None, **kwds): """Calculate water flow for a time period `dt`. """ # Handle runoff rate if runoff_rate is None: runoff_rate = self.runoff_rate # If it's our first iteration, or if the topography may be changing, # do flow routing and calculate square root of slopes at links if self.changing_topo or self.first_iteration: # Calculate the ground-surface slope self.slope[self.grid.active_links] = \ self._grid.calc_grad_at_link(self.elev)[self._grid.active_links] # Take square root of slope magnitude for use in velocity eqn self.sqrt_slope = np.sqrt(np.abs(self.slope)) # Re-route flow, which gives us the downstream-to-upstream # ordering self.flow_accum.run_one_step() self.nodes_ordered = self.grid.at_node['flow__upstream_node_order'] self.flow_lnks = self.grid.at_node['flow__links_to_receiver_nodes'] # (Re)calculate, for each node, sum of sqrt(gradient) x width self.grad_width_sum[:] = 0.0 for i in range(self.flow_lnks.shape[1]): self.grad_width_sum[:] += (self.sqrt_slope[self.flow_lnks[:,i]] * self._grid.width_of_face[ self.grid.face_at_link[self.flow_lnks[:,i]]]) # Calculate values of alpha, which is defined as # # $\alpha = \frac{\Sigma W S^{1/2} \Delta t}{A C_r}$ cores = self.grid.core_nodes self.alpha[cores] = ( self.vel_coef * self.grad_width_sum[cores] * dt / (self.grid.area_of_cell[self.grid.cell_at_node[cores]])) # Zero out inflow discharge self.disch_in[:] = 0.0 # Upstream-to-downstream loop for i in range(len(self.nodes_ordered) - 1, -1, -1): n = self.nodes_ordered[i] if self.grid.status_at_node[n] == 0: # Solve for new water depth aa = self.alpha[n] cc = self.depth[n] ee = ((dt * runoff_rate) + (dt * self.disch_in[n] / self.grid.area_of_cell[self.grid.cell_at_node[n]])) self.depth[n] = newton(water_fn, self.depth[n], args=(aa, self.weight, cc, self.depth_exp, ee)) # Calc outflow Heff = (self.weight * self.depth[n] + (1.0 - self.weight) * cc) outflow = (self.vel_coef * (Heff ** self.depth_exp) * self.grad_width_sum[n]) # this is manning/chezy/darcy # Send flow downstream. Here we take total inflow discharge # and partition it among the node's neighbors. For this, we use # the flow director's "proportions" array, which contains, for # each node, the proportion of flow that heads out toward each # of its N neighbors. The proportion is zero if the neighbor is # uphill; otherwise, it is S^1/2 / sum(S^1/2). If for example # we have a raster grid, there will be four neighbors and four # proportions, some of which may be zero and some between 0 and # 1. self.disch_in[self.grid.adjacent_nodes_at_node[n]] += (outflow * self.flow_accum.flow_director.proportions[n])
def test_matches_detachment_solution_n_gr_1(): # %% """ 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("topographic__elevation", at="node") br = mg.add_zeros("bedrock__elevation", at="node") soil = mg.add_zeros("soil__depth", at="node") 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.0) 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.1 # Instantiate the SpaceLargeScaleEroder component... sp = SpaceLargeScaleEroder( mg, K_sed=0.00001, K_br=K_br, F_f=F_f, phi=0.1, H_star=1.0, 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 _ in range(4000): 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.0 / 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="SpaceLargeScaleEroder 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), xy_spacing=10.0) z = mg.add_zeros("topographic__elevation", at="node") br = mg.add_zeros("bedrock__elevation", at="node") soil = mg.add_zeros("soil__depth", at="node") 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[:] += 0.0 # initial condition of no soil depth. br[:] = z[:] z[:] += soil[:] # Create a D8 flow handler fa = FlowAccumulator(mg, flow_director="D8", depression_finder="DepressionFinderAndRouter") # Parameter values for detachment-limited test K_br = 0.002 K_sed = 0.002 U = 0.0001 dt = 10.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 SpaceLargeScaleEroder component... sp = SpaceLargeScaleEroder( 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 _ in range(10000): fa.run_one_step() sp.run_one_step(dt=dt) br[mg.core_nodes] += U * dt # m soil[ 0] = 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.0 / n_sp, ) # test for match with analytical slope-area relationship testing.assert_array_almost_equal( num_slope, analytical_slope, decimal=8, err_msg="SpaceLargeScaleEroder 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= "SpaceLargeScaleEroder bedrock-alluvial soil thickness test failed", verbose=True, )
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("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)
rmg, rmg.status_at_node, color_for_closed="blue", plot_name= 'Carbon Glacier Raster Model Grid\nClosed boundaries in blue, Outlet in white', var_name='elevation', var_units='m', grid_units=('m', 'm')) # ### Flow routing # #### Without depression finder/filler flow_accum = FlowAccumulator(rmg, surface='topographic__elevation', flow_director='FlowDirectorD8', depression_finder=None, runoff_rate=None) da, q = flow_accum.accumulate_flow() imshow_grid(rmg, 'drainage_area', plot_name='Contributing Area', var_name='Contributing Area', var_units='m^2', grid_units=('m', 'm')) fig = plt.gcf().set_size_inches(18.5, 10.5) # #### With depression finder/filler
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), xy_spacing=10.0) z = mg.add_zeros("topographic__elevation", at="node") 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) # 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 # Instantiate the ErosionDeposition component... ed = ErosionDeposition( mg, K=K, F_f=F_f, 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 _ in range(5000): fa.run_one_step() ed.run_one_step(dt=dt) 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.0 / 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] # 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, )