Exemplo n.º 1
0
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])
Exemplo n.º 2
0
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_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)
Exemplo n.º 4
0
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)
Exemplo n.º 5
0
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)
Exemplo n.º 6
0
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
Exemplo n.º 7
0
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)
Exemplo n.º 8
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)
Exemplo n.º 9
0
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
Exemplo n.º 10
0
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_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
Exemplo n.º 12
0
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))
Exemplo n.º 13
0
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)
Exemplo n.º 14
0
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)
Exemplo n.º 15
0
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)
Exemplo n.º 16
0
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
Exemplo n.º 17
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)
Exemplo n.º 18
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)
Exemplo n.º 19
0
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)
Exemplo n.º 20
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)
Exemplo n.º 21
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):
        DepressionFinderAndRouter(mg)
Exemplo n.º 22
0
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()
Exemplo n.º 23
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):
        ErosionDeposition(mg, K=0.01, phi=0.0, v_s=0.001, m_sp=0.5, n_sp=1.0, sp_crit=0)
Exemplo n.º 24
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):
        ChiFinder(mg, min_drainage_area=1., reference_concavity=1.)
Exemplo n.º 25
0
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)
Exemplo n.º 26
0
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)
Exemplo n.º 27
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):
        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)
Exemplo n.º 28
0
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)
Exemplo n.º 29
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)
Exemplo n.º 30
0
def test_bool_mask():
    mg = RasterModelGrid((10, 10))
    mg.add_zeros("node", "topographic__elevation")
    noise = np.random.rand(mg.size("node"))
    mg.at_node["topographic__elevation"] += noise
    fr = FlowAccumulator(mg, flow_director="D8")
    fr.run_one_step()
    mask = np.zeros(len(mg.at_node["topographic__elevation"]), dtype=bool)
    mask[np.where(mg.at_node["drainage_area"] > 5)] = 1
    with pytest.raises(ValueError):
        DrainageDensity(mg, channel__mask=mask)
Exemplo n.º 31
0
def test_composite_pits():
    """
    A test to ensure the component correctly handles cases where there are
    multiple pits, inset into each other.
    """
    mg = RasterModelGrid((10, 10))
    z = mg.add_field("topographic__elevation", mg.node_x.copy(), at="node")
    # a sloping plane
    # np.random.seed(seed=0)
    # z += np.random.rand(100)/10000.
    # punch one big hole
    z.reshape((10, 10))[3:8, 3:8] = 0.0
    # dig a couple of inset holes
    z[57] = -1.0
    z[44] = -2.0
    z[54] = -10.0

    # make an outlet
    z[71] = 0.9

    fr = FlowAccumulator(mg, flow_director="D8")
    lf = DepressionFinderAndRouter(mg)
    fr.run_one_step()
    lf.map_depressions()

    flow_sinks_target = np.zeros(100, dtype=bool)
    flow_sinks_target[mg.boundary_nodes] = True
    # no internal sinks now:
    assert_array_equal(mg.at_node["flow__sink_flag"], flow_sinks_target)

    # test conservation of mass:
    assert mg.at_node["drainage_area"].reshape(
        (10, 10))[1:-1, 1].sum() == approx(8.0**2)
    # ^all the core nodes

    # test the actual flow field:
    #    nA = np.array([  0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,
    #                     8.,   8.,   7.,   6.,   5.,   4.,   3.,   2.,   1.,   0.,
    #                     1.,   1.,   1.,   1.,   1.,   1.,   1.,   1.,   1.,   0.,
    #                     1.,   1.,   1.,   4.,   2.,   2.,   8.,   4.,   1.,   0.,
    #                     1.,   1.,   1.,   8.,   3.,  15.,   3.,   2.,   1.,   0.,
    #                     1.,   1.,   1.,  13.,  25.,   6.,   3.,   2.,   1.,   0.,
    #                     1.,   1.,   1.,  45.,   3.,   3.,   5.,   2.,   1.,   0.,
    #                    50.,  50.,  49.,   3.,   2.,   2.,   2.,   4.,   1.,   0.,
    #                     1.,   1.,   1.,   1.,   1.,   1.,   1.,   1.,   1.,   0.,
    #                     0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.])
    nA = np.array([
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
        8.0,
        8.0,
        7.0,
        6.0,
        5.0,
        4.0,
        3.0,
        2.0,
        1.0,
        0.0,
        1.0,
        1.0,
        1.0,
        1.0,
        1.0,
        1.0,
        1.0,
        1.0,
        1.0,
        0.0,
        1.0,
        1.0,
        1.0,
        4.0,
        2.0,
        2.0,
        6.0,
        4.0,
        1.0,
        0.0,
        1.0,
        1.0,
        1.0,
        6.0,
        3.0,
        12.0,
        3.0,
        2.0,
        1.0,
        0.0,
        1.0,
        1.0,
        1.0,
        8.0,
        20.0,
        4.0,
        3.0,
        2.0,
        1.0,
        0.0,
        1.0,
        1.0,
        1.0,
        35.0,
        5.0,
        4.0,
        3.0,
        2.0,
        1.0,
        0.0,
        50.0,
        50.0,
        49.0,
        13.0,
        10.0,
        8.0,
        6.0,
        4.0,
        1.0,
        0.0,
        1.0,
        1.0,
        1.0,
        1.0,
        1.0,
        1.0,
        1.0,
        1.0,
        1.0,
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
    ])
    assert_array_equal(mg.at_node["drainage_area"], nA)

    # the lake code map:
    lc = np.array([
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        57,
        57,
        57,
        57,
        57,
        XX,
        XX,
        XX,
        XX,
        XX,
        57,
        57,
        57,
        57,
        57,
        XX,
        XX,
        XX,
        XX,
        XX,
        57,
        57,
        57,
        57,
        57,
        XX,
        XX,
        XX,
        XX,
        XX,
        57,
        57,
        57,
        57,
        57,
        XX,
        XX,
        XX,
        XX,
        XX,
        57,
        57,
        57,
        57,
        57,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
    ])

    # test the remaining properties:
    assert lf.lake_outlets.size == 1
    assert lf.lake_outlets[0] == 72
    outlets_in_map = np.unique(lf.depression_outlet_map)
    assert outlets_in_map.size == 2
    assert outlets_in_map[1] == 72
    assert lf.number_of_lakes == 1
    assert lf.lake_codes[0] == 57
    assert_array_equal(lf.lake_map, lc)
    assert lf.lake_areas[0] == approx(25.0)
    assert lf.lake_volumes[0] == approx(63.0)
Exemplo n.º 32
0
class BasicDdHy(_ErosionModel):
    """
    A BasicDdHy computes erosion using 1) the hybrid alluvium component
    with a threshold that varies with cumulative incision depth, the linear
    diffusion component.
    """
    def __init__(self,
                 input_file=None,
                 params=None,
                 BaselevelHandlerClass=None):
        """
        Initialize the BasicDdHy
        """

        # Call ErosionModel's init
        super(BasicDdHy,
              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)  # L2/T
            * self.get_parameter_from_exponent('linear_diffusivity'))
        v_s = self.get_parameter_from_exponent('v_sc')  # unitless
        self.sp_crit = (
            self._length_factor  # L/T
            * self.get_parameter_from_exponent('erosion__threshold'))

        # 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.sp_crit  #starting value

        # Handle solver option
        try:
            solver = self.params['solver']
        except:
            solver = 'original'

        # Instantiate an ErosionDeposition component
        self.eroder = ErosionDeposition(self.grid,
                                        K=self.K_sp,
                                        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'],
                                        sp_crit='erosion__threshold',
                                        method='threshold_stream_power',
                                        discharge_method='drainage_area',
                                        area_field='drainage_area',
                                        solver=solver)

        # 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 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 cumulative erosion and update threshold
        cum_ero = self.grid.at_node['cumulative_erosion__depth']
        cum_ero[:] = (self.z -
                      self.grid.at_node['initial_topographic__elevation'])
        self.threshold[:] = (self.sp_crit -
                             (self.thresh_change_per_depth * cum_ero))
        self.threshold[self.threshold < self.sp_crit] = self.sp_crit

        # 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)

        # 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()
Exemplo n.º 33
0
def test_matches_detachment_solution():
    """
    Test that model matches the detachment-limited analytical solution
    for slope/area relationship at steady state: S=(U/K_br)^(1/n)*A^(-m/n).
    """

    #set up a 5x5 grid with one open outlet node and low initial elevations.
    nr = 5
    nc = 5
    mg = RasterModelGrid((nr, nc), 10.0)

    z = mg.add_zeros('node', 'topographic__elevation')
    br = mg.add_zeros('node', 'bedrock__elevation')
    soil = mg.add_zeros('node', 'soil__depth')

    mg['node']['topographic__elevation'] += mg.node_y / 10000 \
        + mg.node_x / 10000 \
        + np.random.rand(len(mg.node_y)) / 10000
    mg.set_closed_boundaries_at_grid_edges(bottom_is_closed=True,
                                           left_is_closed=True,
                                           right_is_closed=True,
                                           top_is_closed=True)
    mg.set_watershed_boundary_condition_outlet_id(
        0, mg['node']['topographic__elevation'], -9999.)
    br[:] = z[:] - soil[:]

    # Create a D8 flow handler
    fa = FlowAccumulator(mg, flow_director='D8')

    # Parameter values for detachment-limited test
    K_br = 0.01
    U = 0.0001
    dt = 1.0
    F_f = 1.0  #all detached rock disappears; detachment-ltd end-member
    m_sp = 0.5
    n_sp = 1.0

    # Instantiate the Space component...
    sp = Space(mg,
               K_sed=0.00001,
               K_br=K_br,
               F_f=F_f,
               phi=0.1,
               H_star=1.,
               v_s=0.001,
               m_sp=m_sp,
               n_sp=n_sp,
               sp_crit_sed=0,
               sp_crit_br=0)

    # ... and run it to steady state (2000x1-year timesteps).
    for i in range(2000):
        fa.run_one_step()
        sp.run_one_step(dt=dt)
        z[mg.core_nodes] += U * dt  #m
        br[mg.core_nodes] = z[mg.core_nodes] - soil[mg.core_nodes]

    #compare numerical and analytical slope solutions
    num_slope = mg.at_node['topographic__steepest_slope'][mg.core_nodes]
    analytical_slope = np.power(U / K_br, 1./n_sp) \
        * np.power(mg.at_node['drainage_area'][mg.core_nodes], -m_sp / n_sp)

    #test for match with analytical slope-area relationship
    testing.assert_array_almost_equal(
        num_slope,
        analytical_slope,
        decimal=8,
        err_msg='SPACE detachment-limited test failed',
        verbose=True)
Exemplo n.º 34
0
def test_steady_state_with_basic_solver_option():
    """
    Test that model matches the transport-limited analytical solution
    for slope/area relationship at steady state: S=((U * v_s) / (K * A^m)
    + U / (K * A^m))^(1/n).

    Also test that model matches the analytical solution for steady-state
    sediment flux: Qs = U * A * (1 - phi).
    """

    #set up a 5x5 grid with one open outlet node and low initial elevations.
    nr = 5
    nc = 5
    mg = RasterModelGrid((nr, nc), 10.0)

    z = mg.add_zeros('node', 'topographic__elevation')

    mg['node']['topographic__elevation'] += mg.node_y / 100000 \
        + mg.node_x / 100000 \
        + np.random.rand(len(mg.node_y)) / 10000
    mg.set_closed_boundaries_at_grid_edges(bottom_is_closed=True,
                                           left_is_closed=True,
                                           right_is_closed=True,
                                           top_is_closed=True)
    mg.set_watershed_boundary_condition_outlet_id(
        0, mg['node']['topographic__elevation'], -9999.)

    #Instantiate DepressionFinderAndRouter
    df = DepressionFinderAndRouter(mg)

    # Create a D8 flow handler
    fa = FlowAccumulator(mg,
                         flow_director='D8',
                         depression_finder='DepressionFinderAndRouter')

    # Parameter values for detachment-limited test
    K = 0.01
    U = 0.0001
    dt = 1.0
    F_f = 0.0  #all sediment is considered coarse bedload
    m_sp = 0.5
    n_sp = 1.0
    v_s = 0.5
    phi = 0.5

    # Instantiate the ErosionDeposition component...
    ed = ErosionDeposition(mg,
                           K=K,
                           F_f=F_f,
                           phi=phi,
                           v_s=v_s,
                           m_sp=m_sp,
                           n_sp=n_sp,
                           sp_crit=0,
                           solver='basic')

    # ... and run it to steady state (5000x1-year timesteps).
    for i in range(5000):
        fa.run_one_step()
        flooded = np.where(df.flood_status == 3)[0]
        ed.run_one_step(dt=dt, flooded_nodes=flooded)
        z[mg.core_nodes] += U * dt  #m

    #compare numerical and analytical slope solutions
    num_slope = mg.at_node['topographic__steepest_slope'][mg.core_nodes]
    analytical_slope = (np.power(
        ((U * v_s) /
         (K * np.power(mg.at_node['drainage_area'][mg.core_nodes], m_sp))) +
        (U / (K * np.power(mg.at_node['drainage_area'][mg.core_nodes], m_sp))),
        1. / n_sp))

    #test for match with analytical slope-area relationship
    testing.assert_array_almost_equal(num_slope,
                                      analytical_slope,
                                      decimal=8,
                                      err_msg='E/D slope-area test failed',
                                      verbose=True)

    #compare numerical and analytical sediment flux solutions
    num_sedflux = mg.at_node['sediment__flux'][mg.core_nodes]
    analytical_sedflux = (U * mg.at_node['drainage_area'][mg.core_nodes] *
                          (1 - phi))

    #test for match with anakytical sediment flux
    testing.assert_array_almost_equal(num_sedflux,
                                      analytical_sedflux,
                                      decimal=8,
                                      err_msg='E/D sediment flux test failed',
                                      verbose=True)
Exemplo n.º 35
0
class BasicCh(_ErosionModel):
    """
    A BasicCh computes erosion using cubic diffusion, basic stream
    power, and Q~A.
    """

    def __init__(self, input_file=None, params=None,
                 BaselevelHandlerClass=None):
        """Initialize the BasicCh."""

        # Call ErosionModel's init
        super(BasicCh, 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

        # 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 LinearDiffuser component
        self.diffuser = TaylorNonLinearDiffuser(self.grid,
                                               linear_diffusivity=linear_diffusivity,
                                               slope_crit=self.params['slope_crit'],
                                               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)

        # 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()
class BasicChRtTh(_ErosionModel):
    """
    A BasicChRt model computes erosion using cubic diffusion, basic stream
    power with two rock units, and Q~A.
    """
    def __init__(self,
                 input_file=None,
                 params=None,
                 BaselevelHandlerClass=None):
        """Initialize the BasicChRt model."""

        # Call ErosionModel's init
        super(BasicChRtTh,
              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')
        rock_erosion__threshold = self.get_parameter_from_exponent(
            'rock_erosion__threshold')
        till_erosion__threshold = self.get_parameter_from_exponent(
            'till_erosion__threshold')
        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,
                                 rock_erosion__threshold,
                                 till_erosion__threshold, 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 StreamPowerSmoothThresholdEroder component
        self.eroder = StreamPowerSmoothThresholdEroder(
            self.grid,
            K_sp=self.erody,
            threshold_sp=self.threshold,
            m_sp=self.params['m_sp'],
            n_sp=self.params['n_sp'])

        # Instantiate a LinearDiffuser component
        self.diffuser = TaylorNonLinearDiffuser(
            self.grid,
            linear_diffusivity=linear_diffusivity,
            slope_crit=self.params['slope_crit'],
            nterms=7)

    def setup_rock_and_till(self, file_name, rock_erody, till_erody,
                            rock_thresh, till_thresh, 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)
        rock_erody : float
            Water erosion coefficient for bedrock
        till_erody : float
            Water erosion coefficient for till
        rock_thresh : float
            Water erosion threshold for bedrock
        till_thresh : float
            Water erosion threshold for till
        contact_width : float [L]
            Characteristic width of the interface zone between rock and till

        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.
        """
        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 field for threshold values
        if 'erosion__threshold' in self.grid.at_node:
            self.threshold = self.grid.at_node['erosion__threshold']
        else:
            self.threshold = self.grid.add_zeros('node', 'erosion__threshold')

        # 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 the threshold values for rock and till
        self.rock_thresh = rock_thresh
        self.till_thresh = till_thresh

        # Read and remember the contact zone characteristic width
        self.contact_width = contact_width

    def update_erodibility_and_threshold_fields(self):
        """Update erodibility and threshold 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.
            4. Threshold values are set similarly.

        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")
        D_over_D_star = ((self.z[self.data_nodes] -
                          self.rock_till_contact[self.data_nodes]) /
                         self.contact_width)

        # truncate D_over_D star to remove potential for overflow in exponent
        D_over_D_star[D_over_D_star < -100.0] = -100.0
        D_over_D_star[D_over_D_star > 100.0] = 100.0

        self.erody_wt[self.data_nodes] = (1.0 / (1.0 + np.exp(-D_over_D_star)))

        # (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)

        # Calculate the effective thresholds using weighted averaging
        self.threshold[:] = (self.erody_wt * self.till_thresh +
                             (1.0 - self.erody_wt) * self.rock_thresh)

    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 and threshold field
        self.update_erodibility_and_threshold_fields()

        # Do some erosion (but not on the flooded nodes)
        self.eroder.run_one_step(dt, flooded_nodes=flooded)

        # 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()
Exemplo n.º 37
0
def test_get_watershed_outlet():
    grid = RasterModelGrid((7, 7))

    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)
Exemplo n.º 38
0
def test_phi_affects_transience():
    """Test that different porosity values affect the transient case."""

    # Set up one 5x5 grid with open boundaries and low initial elevations.
    mg1 = HexModelGrid((7, 7))
    z1 = mg1.add_zeros("topographic__elevation", at="node")
    z1[:] = 0.01 * mg1.x_of_node

    # Create a D8 flow handler
    fa1 = FlowAccumulator(mg1, flow_director="FlowDirectorSteepest")

    # Parameter values for test 1
    K1 = 0.001
    vs1 = 0.0001
    U1 = 0.001
    dt1 = 10.0
    phi1 = 0.1

    # Create the ErosionDeposition component...
    ed1 = ErosionDeposition(mg1,
                            K=K1,
                            phi=phi1,
                            v_s=vs1,
                            m_sp=0.5,
                            n_sp=1.0,
                            solver="basic")

    # ... and run it to steady state.
    for i in range(200):
        fa1.run_one_step()
        ed1.run_one_step(dt=dt1)
        z1[mg1.core_nodes] += U1 * dt1

    # Set up a second 5x5 grid with open boundaries and low initial elevations.
    mg2 = HexModelGrid((7, 7))
    z2 = mg2.add_zeros("topographic__elevation", at="node")
    z2[:] = 0.01 * mg2.x_of_node

    # Create a D8 flow handler
    fa2 = FlowAccumulator(mg2, flow_director="FlowDirectorSteepest")

    # Parameter values for test 1
    K2 = 0.001
    vs2 = 0.0001
    U2 = 0.001
    dt2 = 10.0
    phi2 = 0.9

    # Create the ErosionDeposition component...
    ed2 = ErosionDeposition(mg2,
                            K=K2,
                            phi=phi2,
                            v_s=vs2,
                            m_sp=0.5,
                            n_sp=1.0,
                            solver="basic")

    # ... and run it to steady state.
    for i in range(200):
        fa2.run_one_step()
        ed2.run_one_step(dt=dt2)
        z2[mg2.core_nodes] += U2 * dt2

    # Test the results: higher phi should be lower slope
    s1 = mg1.at_node["topographic__steepest_slope"][mg1.core_nodes]
    s2 = mg2.at_node["topographic__steepest_slope"][mg2.core_nodes]
    testing.assert_array_less(s2, s1)
class BasicHyVs(_ErosionModel):
    """
    A BasicHyVs computes erosion using linear diffusion,
    hybrid alluvium fluvial erosion, and Q ~ A exp( -b S / A).

    "VSA" stands for "variable source area".
    """
    def __init__(self,
                 input_file=None,
                 params=None,
                 BaselevelHandlerClass=None):
        """Initialize the BasicHyVs."""

        # Call ErosionModel's init
        super(BasicHyVs,
              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
        recharge_rate = (self._length_factor * self.params['recharge_rate']
                         )  # L/T
        soil_thickness = (self._length_factor *
                          self.params['initial_soil_thickness'])  # L
        K_hydraulic_conductivity = (self._length_factor *
                                    self.params['K_hydraulic_conductivity']
                                    )  # has units length per time

        v_sc = self.get_parameter_from_exponent(
            'v_sc')  # normalized settling velocity. Unitless.

        # Instantiate a FlowAccumulator with DepressionFinderAndRouter using D8 method
        self.flow_router = FlowAccumulator(
            self.grid,
            flow_director='D8',
            depression_finder=DepressionFinderAndRouter)

        # set methods and fields. K's and sp_crits need to be field names
        method = 'simple_stream_power'
        discharge_method = 'drainage_area'
        area_field = 'effective_drainage_area'
        discharge_field = None

        # Add a field for effective drainage area
        if 'effective_drainage_area' in self.grid.at_node:
            self.eff_area = self.grid.at_node['effective_drainage_area']
        else:
            self.eff_area = self.grid.add_zeros('node',
                                                'effective_drainage_area')

        # Get the effective-area parameter
        self.sat_param = (
            (K_hydraulic_conductivity * soil_thickness * self.grid.dx) /
            recharge_rate)

        # Handle solver option
        try:
            solver = self.params['solver']
        except KeyError:
            solver = 'original'

        # Instantiate a SPACE component
        self.eroder = ErosionDeposition(self.grid,
                                        K=self.K_sp,
                                        F_f=self.params['F_f'],
                                        phi=self.params['phi'],
                                        v_s=v_sc,
                                        m_sp=self.params['m_sp'],
                                        n_sp=self.params['n_sp'],
                                        method=method,
                                        discharge_method=discharge_method,
                                        area_field=area_field,
                                        discharge_field=discharge_field,
                                        solver=solver)

        # Instantiate a LinearDiffuser component
        self.diffuser = LinearDiffuser(self.grid,
                                       linear_diffusivity=linear_diffusivity)

    def calc_effective_drainage_area(self):
        """Calculate and store effective drainage area.

        Effective drainage area is defined as:

        $A_{eff} = A \exp ( \alpha S / A) = A R_r$

        where $S$ is downslope-positive steepest gradient, $A$ is drainage
        area, $R_r$ is the runoff ratio, and $\alpha$ is the saturation
        parameter.
        """

        area = self.grid.at_node['drainage_area']
        slope = self.grid.at_node['topographic__steepest_slope']
        cores = self.grid.core_nodes
        self.eff_area[cores] = (
            area[cores] *
            (np.exp(-self.sat_param * slope[cores] / area[cores])))

    def run_one_step(self, dt):
        """
        Advance model for one time-step of duration dt.
        """

        # Route flow
        self.flow_router.run_one_step()

        # Update effective runoff ratio
        self.calc_effective_drainage_area()

        # Zero out effective area in flooded nodes
        self.eff_area[self.flow_router.depression_finder.flood_status ==
                      3] = 0.0

        # Do some erosion
        # (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)

        # 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()
Exemplo n.º 40
0
class KinwaveImplicitOverlandFlow(Component):
    r"""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:

    .. math::

        (H^{t+1} - H^t)/\Delta t = Q_{in}/A - Q_{out}/A + R

    where :math:`H` is water depth, :math:`t` indicates time step
    number, :math:`\Delta t` is time step duration, :math:`Q_{in}` is
    total inflow discharge, :math:`Q_{out}` is total outflow
    discharge, :math:`A` is cell area, and :math:`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:

    .. math::

        q = (1/C_r) H^\alpha S^{1/2}

    where :math:`C_r` is a roughness coefficient (such as
    Manning's n), :math:`\alpha` is an exponent equal to :math:`5/3`
    for the Manning equation and :math:`3/2` for the Chezy family,
    and :math:`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

    .. math::

        Q_{out} = \sum_{i=1}^N (1/C_r) H^\alpha S_i^{1/2} W_i

    where :math:`N` is the number of outflow faces (i.e., faces
    where the ground slopes downhill away from the cell's node),
    and :math:`W_i` is the width of face :math:`i`.

    We use the depth at the cell's node, so this simplifies to:

    .. math::

        Q_{out} = (1/C_r) H'^\alpha \sum_{i=1}^N S_i^{1/2} W_i

    We define :math:`H` in the above as a weighted sum of
    the "old" (time step :math:`t`) and "new" (time step :math:`t+1`)
    depth values:

    .. math::

        H' = w H^{t+1} + (1-w) H^t

    If :math:`w=1`, the method is fully implicit. If :math:`w=0`,
    it is a simple forward explicit method.

    When we combine these equations, we have an equation that includes the
    unknown :math:`H^{t+1}` and a bunch of terms that are known.
    If :math:`w\ne 0`, it is a nonlinear equation in :math:`H^{t+1}`,
    and must be solved iteratively. We do this using a root-finding
    method in the scipy.optimize library.

    Examples
    --------
    >>> from landlab import RasterModelGrid
    >>> rg = RasterModelGrid((4, 5), xy_spacing=10.0)
    >>> z = rg.add_zeros("topographic__elevation", at="node")
    >>> 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.])

    References
    ----------
    **Required Software Citation(s) Specific to this Component**

    None Listed

    **Additional References**

    None Listed

    """

    _name = "KinwaveImplicitOverlandFlow"

    _info = {
        "surface_water__depth": {
            "dtype": float,
            "intent": "out",
            "optional": False,
            "units": "m",
            "mapping": "node",
            "doc": "Depth of water on the surface",
        },
        "surface_water_inflow__discharge": {
            "dtype": float,
            "intent": "out",
            "optional": False,
            "units": "m3/s",
            "mapping": "node",
            "doc": "water volume inflow rate to the cell around each node",
        },
        "topographic__elevation": {
            "dtype": float,
            "intent": "in",
            "optional": False,
            "units": "m",
            "mapping": "node",
            "doc": "Land surface topographic elevation",
        },
        "topographic__gradient": {
            "dtype": float,
            "intent": "out",
            "optional": False,
            "units": "m/m",
            "mapping": "link",
            "doc": "Gradient of the ground surface",
        },
    }

    def __init__(
        self,
        grid,
        runoff_rate=1.0,
        roughness=0.01,
        changing_topo=False,
        depth_exp=1.5,
        weight=1.0,
    ):
        """Initialize the KinwaveImplicitOverlandFlow.

        Parameters
        ----------
        grid : ModelGrid
            Landlab ModelGrid object
        runoff_rate : float, optional (defaults to 1 mm/hr)
            Precipitation rate, mm/hr. The value provide is divided by
            3600000.0.
        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)
        """
        super(KinwaveImplicitOverlandFlow, self).__init__(grid)
        # Store parameters and do unit conversion

        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
        self._elev = grid.at_node["topographic__elevation"]

        # Create fields...
        self.initialize_output_fields()

        self._depth = grid.at_node["surface_water__depth"]
        self._slope = grid.at_link["topographic__gradient"]
        self._disch_in = grid.at_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

    @property
    def runoff_rate(self):
        """Runoff rate.

        Parameters
        ----------
        runoff_rate : float, optional (defaults to 1 mm/hr)
            Precipitation rate, mm/hr. The value provide is divided by
            3600000.0.

        Returns
        -------
        The current value of the runoff rate.
        """
        return self._runoff_rate

    @runoff_rate.setter
    def runoff_rate(self, new_rate):
        assert new_rate > 0
        self._runoff_rate = new_rate / 3600000.0  # convert to m/s

    @property
    def vel_coef(self):
        """Velocity coefficient."""
        return self._vel_coef

    @property
    def depth(self):
        """The depth of water at each node."""
        return self._depth

    def run_one_step(self, dt):
        """Calculate water flow for a time period `dt`."""

        # 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__link_to_receiver_node"]

            # (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.length_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 * self._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])
Exemplo n.º 41
0
def test_degenerate_drainage():
    """
    This "hourglass" configuration should be one of the hardest to correctly
    re-route.
    """
    mg = RasterModelGrid((9, 5))
    z_init = mg.node_x.copy() * 0.0001 + 1.0
    lake_pits = np.array([7, 11, 12, 13, 17, 27, 31, 32, 33, 37])
    z_init[lake_pits] = -1.0
    z_init[22] = 0.0  # the common spill pt for both lakes
    z_init[21] = 0.1  # an adverse bump in the spillway
    z_init[20] = -0.2  # the spillway
    mg.add_field("topographic__elevation", z_init, at="node")

    fr = FlowAccumulator(mg, flow_director="D8")
    lf = DepressionFinderAndRouter(mg)
    fr.run_one_step()
    lf.map_depressions()

    #    correct_A = np.array([ 0.,   0.,   0.,   0.,   0.,
    #                           0.,   1.,   3.,   1.,   0.,
    #                           0.,   5.,   1.,   2.,   0.,
    #                           0.,   1.,  10.,   1.,   0.,
    #                          21.,  21.,   1.,   1.,   0.,
    #                           0.,   1.,   9.,   1.,   0.,
    #                           0.,   3.,   1.,   2.,   0.,
    #                           0.,   1.,   1.,   1.,   0.,
    #                           0.,   0.,   0.,   0.,   0.])

    correct_A = np.array([
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
        1.0,
        3.0,
        1.0,
        0.0,
        0.0,
        2.0,
        4.0,
        2.0,
        0.0,
        0.0,
        1.0,
        10.0,
        1.0,
        0.0,
        21.0,
        21.0,
        1.0,
        1.0,
        0.0,
        0.0,
        1.0,
        9.0,
        1.0,
        0.0,
        0.0,
        2.0,
        2.0,
        2.0,
        0.0,
        0.0,
        1.0,
        1.0,
        1.0,
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
    ])

    assert mg.at_node["drainage_area"] == approx(correct_A)
Exemplo n.º 42
0
def test_three_pits():
    """
    A test to ensure the component correctly handles cases where there are
    multiple pits.
    """
    mg = RasterModelGrid((10, 10))
    z = mg.add_field("topographic__elevation", mg.node_x.copy(), at="node")
    # a sloping plane
    # np.random.seed(seed=0)
    # z += np.random.rand(100)/10000.
    # punch some holes
    z[33] = 1.0
    z[43] = 1.0
    z[37] = 4.0
    z[74:76] = 1.0
    fr = FlowAccumulator(mg, flow_director="D8")
    lf = DepressionFinderAndRouter(mg)
    fr.run_one_step()
    lf.map_depressions()

    flow_sinks_target = np.zeros(100, dtype=bool)
    flow_sinks_target[mg.boundary_nodes] = True
    # no internal sinks now:
    assert_array_equal(mg.at_node["flow__sink_flag"], flow_sinks_target)

    # test conservation of mass:
    assert mg.at_node["drainage_area"].reshape(
        (10, 10))[1:-1, 1].sum() == approx(8.0**2)
    # ^all the core nodes

    # test the actual flow field:
    nA = np.array([
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
        8.0,
        8.0,
        7.0,
        6.0,
        5.0,
        4.0,
        3.0,
        2.0,
        1.0,
        0.0,
        2.0,
        2.0,
        1.0,
        1.0,
        2.0,
        1.0,
        1.0,
        1.0,
        1.0,
        0.0,
        26.0,
        26.0,
        25.0,
        15.0,
        11.0,
        10.0,
        9.0,
        8.0,
        1.0,
        0.0,
        2.0,
        2.0,
        1.0,
        9.0,
        2.0,
        1.0,
        1.0,
        1.0,
        1.0,
        0.0,
        2.0,
        2.0,
        1.0,
        1.0,
        5.0,
        4.0,
        3.0,
        2.0,
        1.0,
        0.0,
        2.0,
        2.0,
        1.0,
        1.0,
        1.0,
        1.0,
        3.0,
        2.0,
        1.0,
        0.0,
        20.0,
        20.0,
        19.0,
        18.0,
        17.0,
        12.0,
        3.0,
        2.0,
        1.0,
        0.0,
        2.0,
        2.0,
        1.0,
        1.0,
        1.0,
        1.0,
        3.0,
        2.0,
        1.0,
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
        0.0,
    ])
    assert_array_equal(mg.at_node["drainage_area"], nA)

    # test a couple more properties:
    lc = np.empty(100, dtype=int)
    lc.fill(XX)
    lc[33] = 33
    lc[43] = 33
    lc[37] = 37
    lc[74:76] = 74
    assert_array_equal(lf.lake_map, lc)
    assert_array_equal(lf.lake_codes, [33, 37, 74])
    assert lf.number_of_lakes == 3
    assert lf.lake_areas == approx([2.0, 1.0, 2.0])
    assert lf.lake_volumes == approx([2.0, 2.0, 4.0])
Exemplo n.º 43
0
def test_sp_discharges_new():
    dt = 1.0

    mg = RasterModelGrid((5, 5))
    mg.add_zeros("topographic__elevation", at="node")
    z = np.array([
        5.0,
        5.0,
        0.0,
        5.0,
        5.0,
        5.0,
        2.0,
        1.0,
        2.0,
        5.0,
        5.0,
        3.0,
        2.0,
        3.0,
        5.0,
        5.0,
        4.0,
        4.0,
        4.0,
        5.0,
        5.0,
        5.0,
        5.0,
        5.0,
        5.0,
    ])
    mg["node"]["topographic__elevation"] = z

    fr = FlowAccumulator(mg, flow_director="D8")
    sp = StreamPowerEroder(mg, K_sp=0.5, m_sp=0.5, n_sp=1.0, threshold_sp=0.0)

    # perform the loop (once!)
    for i in range(1):
        fr.run_one_step()
        sp.run_one_step(dt)

    z_tg = np.array([
        5.0,
        5.0,
        0.0,
        5.0,
        5.0,
        5.0,
        1.47759225,
        0.43050087,
        1.47759225,
        5.0,
        5.0,
        2.32883687,
        1.21525044,
        2.32883687,
        5.0,
        5.0,
        3.27261262,
        3.07175015,
        3.27261262,
        5.0,
        5.0,
        5.0,
        5.0,
        5.0,
        5.0,
    ])

    assert_array_almost_equal(mg.at_node["topographic__elevation"], z_tg)
Exemplo n.º 44
0
class BasicCv(ErosionModel):
    """
    A BasicCV computes erosion using linear diffusion, basic stream
    power, and Q~A.

    It also has basic climate change
    """
    def __init__(self,
                 input_file=None,
                 params=None,
                 BaselevelHandlerClass=None):
        """Initialize the BasicCv model."""
        # Call ErosionModel's init
        super(BasicCv,
              self).__init__(input_file=input_file,
                             params=params,
                             BaselevelHandlerClass=BaselevelHandlerClass)

        K_sp = self.get_parameter_from_exponent('K_sp')
        linear_diffusivity = (
            self._length_factor**
            2.) * self.get_parameter_from_exponent('linear_diffusivity')

        self.climate_factor = self.params['climate_factor']
        self.climate_constant_date = self.params['climate_constant_date']

        time = [0, self.climate_constant_date, self.params['run_duration']]
        K = [K_sp * self.climate_factor, K_sp, K_sp]
        self.K_through_time = interp1d(time, K)

        # 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=K[0],
                                      m_sp=self.params['m_sp'],
                                      n_sp=self.params['n_sp'])

        # Instantiate a LinearDiffuser component
        self.diffuser = LinearDiffuser(self.grid,
                                       linear_diffusivity=linear_diffusivity)

    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 erosion based on climate
        self.eroder.K = float(self.K_through_time(self.model_time))

        # Do some erosion (but not on the flooded nodes)
        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()
Exemplo n.º 45
0
def test_get_watershed_nodes():
    grid = RasterModelGrid((7, 7))

    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)
Exemplo n.º 46
0
class BasicDdSt(_StochasticErosionModel):
    """
    A BasicDdSt computes erosion using (1) unit
    stream power with a threshold, (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 BasicDdSt."""

        # Call ErosionModel's init
        super(BasicDdSt,
              self).__init__(input_file=input_file,
                             params=params,
                             BaselevelHandlerClass=BaselevelHandlerClass)

        # Get Parameters:
        K_sp = self.get_parameter_from_exponent('K_stochastic_sp')
        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

        # Get the parameter for rate of threshold increase with erosion depth
        self.thresh_change_per_depth = self.params['thresh_change_per_depth']

        # Instantiate a FlowAccumulator with DepressionFinderAndRouter using D8 method
        self.flow_router = FlowAccumulator(
            self.grid,
            flow_director='D8',
            depression_finder=DepressionFinderAndRouter)

        # 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']  # has units length per time
        self.infilt = infiltration_capacity

        # Keep a reference to drainage area
        self.area = self.grid.at_node['drainage_area']

        # Run flow routing and lake filler
        self.flow_router.run_one_step()

        # Create a field for the (initial) erosion threshold
        self.threshold = self.grid.add_zeros('node', 'erosion__threshold')
        self.threshold[:] = self.threshold_value

        # Get the parameter for rate of threshold increase with erosion depth
        self.thresh_change_per_depth = self.params['thresh_change_per_depth']

        # Instantiate a FastscapeEroder component
        self.eroder = StreamPowerSmoothThresholdEroder(
            self.grid,
            m_sp=self.params['m_sp'],
            n_sp=self.params['n_sp'],
            K_sp=K_sp,
            use_Q=self.discharge,
            threshold_sp=self.threshold)

        # 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 update_threshold_field(self):
        """Update the threshold based on cumulative erosion depth."""
        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]

        # Handle water erosion
        self.handle_water_erosion_with_threshold(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 handle_water_erosion_with_threshold(self, dt, flooded):
        """Handle water erosion.

        This function takes the place of the _BaseSt function of the name
        handle_water_erosion_with_threshold in order to handle water erosion
        correctly for model BasicDdSt.
        """
        # (if we're varying precipitation parameters through time, update them)
        if self.opt_var_precip:
            self.intermittency_factor, self.mean_storm__intensity = self.pc.get_current_precip_params(
                self.model_time)

        # If we're handling duration deterministically, as a set fraction of
        # time step duration, calculate a rainfall intensity. Otherwise,
        # assume it's already been calculated.
        if not self.opt_stochastic_duration:
            self.rain_rate = np.random.exponential(self.mean_storm__intensity)
            dt_water = dt * self.intermittency_factor
        else:
            dt_water = dt

        # Calculate discharge field
        area = self.grid.at_node['drainage_area']
        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)))
        else:
            runoff = self.rain_rate

        self.discharge[:] = runoff * area

        # Handle water erosion:
        #
        #   If we are running stochastic duration, then self.rain_rate will
        #   have been calculated already. It might be zero, in which case we
        #   are between storms, so we don't do water erosion.
        #
        #   If we're NOT doing stochastic duration, then we'll run water
        #   erosion for one or more sub-time steps, each with its own
        #   randomly drawn precipitation intensity.
        #
        if self.opt_stochastic_duration and self.rain_rate > 0.0:
            self.update_threshold_field()
            runoff = self.calc_runoff_and_discharge()
            self.eroder.run_one_step(dt, flooded_nodes=flooded)
        elif not self.opt_stochastic_duration:
            dt_water = ((dt * self.intermittency_factor) /
                        float(self.n_sub_steps))
            for i in range(self.n_sub_steps):
                self.rain_rate = \
                    self.rain_generator.generate_from_stretched_exponential(
                        self.scale_factor, self.shape_factor)
                self.update_threshold_field()
                runoff = self.calc_runoff_and_discharge()
                self.eroder.run_one_step(dt_water, flooded_nodes=flooded)
Exemplo n.º 47
0
def test_get_watershed_masks_with_area_threshold():
    rmg = RasterModelGrid((7, 7), xy_spacing=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)
Exemplo n.º 48
0
def test_complex_case():
    mg = RasterModelGrid(20, 5)
    z = mg.add_zeros("topographic__elevation", at="node")
    z += mg.y_of_node
    middle = mg.x_of_node == 2
    z[middle] *= 0.1

    mg.set_closed_boundaries_at_grid_edges(
        bottom_is_closed=False,
        left_is_closed=True,
        right_is_closed=True,
        top_is_closed=True,
    )

    fa = FlowAccumulator(mg, flow_director="D4")
    fa.run_one_step()

    long_path = calculate_distance_to_divide(mg, longest_path=True)
    short_path = calculate_distance_to_divide(mg, longest_path=False)

    long_correct = np.array(
        [
            [0.0, 1.0, 19.0, 1.0, 0.0],
            [0.0, 0.0, 18.0, 0.0, 0.0],
            [0.0, 0.0, 17.0, 0.0, 0.0],
            [0.0, 0.0, 16.0, 0.0, 0.0],
            [0.0, 0.0, 15.0, 0.0, 0.0],
            [0.0, 0.0, 14.0, 0.0, 0.0],
            [0.0, 0.0, 13.0, 0.0, 0.0],
            [0.0, 0.0, 12.0, 0.0, 0.0],
            [0.0, 0.0, 11.0, 0.0, 0.0],
            [0.0, 0.0, 10.0, 0.0, 0.0],
            [0.0, 0.0, 9.0, 0.0, 0.0],
            [0.0, 0.0, 8.0, 0.0, 0.0],
            [0.0, 0.0, 7.0, 0.0, 0.0],
            [0.0, 0.0, 6.0, 0.0, 0.0],
            [0.0, 0.0, 5.0, 0.0, 0.0],
            [0.0, 0.0, 4.0, 0.0, 0.0],
            [0.0, 0.0, 3.0, 0.0, 0.0],
            [0.0, 0.0, 2.0, 0.0, 0.0],
            [0.0, 0.0, 1.0, 0.0, 0.0],
            [0.0, 0.0, 0.0, 0.0, 0.0],
        ]
    )

    short_correct = np.array(
        [
            [0.0, 1.0, 3.0, 1.0, 0.0],
            [0.0, 0.0, 2.0, 0.0, 0.0],
            [0.0, 0.0, 1.0, 0.0, 0.0],
            [0.0, 0.0, 1.0, 0.0, 0.0],
            [0.0, 0.0, 1.0, 0.0, 0.0],
            [0.0, 0.0, 1.0, 0.0, 0.0],
            [0.0, 0.0, 1.0, 0.0, 0.0],
            [0.0, 0.0, 1.0, 0.0, 0.0],
            [0.0, 0.0, 1.0, 0.0, 0.0],
            [0.0, 0.0, 1.0, 0.0, 0.0],
            [0.0, 0.0, 1.0, 0.0, 0.0],
            [0.0, 0.0, 1.0, 0.0, 0.0],
            [0.0, 0.0, 1.0, 0.0, 0.0],
            [0.0, 0.0, 1.0, 0.0, 0.0],
            [0.0, 0.0, 1.0, 0.0, 0.0],
            [0.0, 0.0, 1.0, 0.0, 0.0],
            [0.0, 0.0, 1.0, 0.0, 0.0],
            [0.0, 0.0, 1.0, 0.0, 0.0],
            [0.0, 0.0, 1.0, 0.0, 0.0],
            [0.0, 0.0, 0.0, 0.0, 0.0],
        ]
    )

    np.testing.assert_array_equal(short_path.reshape(mg.shape), short_correct)
    np.testing.assert_array_equal(long_path.reshape(mg.shape), long_correct)
Exemplo n.º 49
0
time_on = time.time()

# 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, _, _ = sp.erode(
        mg,
        dt,
        node_drainage_areas="drainage_area",
        slopes_at_nodes="topographic__steepest_slope",
        K_if_used="K_values",
    )
    # add uplift
    mg.at_node["topographic__elevation"][mg.core_nodes] += uplift * dt
    elapsed_time += dt

time_off = time.time()
print("Elapsed time: ", time_off - time_on)
Exemplo n.º 50
0
def test_edge_draining():
    """
    This tests when the lake attempts to drain from an edge, where an issue
    is suspected.
    """
    # Create a 7x7 test grid with a well defined hole in it, AT THE EDGE.
    mg = RasterModelGrid((7, 7))

    z = mg.node_x.copy()
    guard_sides = np.concatenate((np.arange(7, 14), np.arange(35, 42)))
    edges = np.concatenate((np.arange(7), np.arange(42, 49)))
    hole_here = np.array(([15, 16, 22, 23, 29, 30]))
    z[guard_sides] = z[13]
    z[edges] = -2.0  # force flow outwards from the tops of the guards
    z[hole_here] = -1.0

    A_new = np.array([[[
        0.0,
        1.0,
        1.0,
        1.0,
        1.0,
        1.0,
        0.0,
        0.0,
        1.0,
        1.0,
        1.0,
        1.0,
        1.0,
        0.0,
        15.0,
        5.0,
        4.0,
        3.0,
        2.0,
        1.0,
        0.0,
        0.0,
        10.0,
        4.0,
        3.0,
        2.0,
        1.0,
        0.0,
        0.0,
        1.0,
        4.0,
        3.0,
        2.0,
        1.0,
        0.0,
        0.0,
        1.0,
        1.0,
        1.0,
        1.0,
        1.0,
        0.0,
        0.0,
        1.0,
        1.0,
        1.0,
        1.0,
        1.0,
        0.0,
    ]]]).flatten()

    depr_outlet_target = np.array([
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        14,
        14,
        XX,
        XX,
        XX,
        XX,
        XX,
        14,
        14,
        XX,
        XX,
        XX,
        XX,
        XX,
        14,
        14,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
        XX,
    ]).flatten()

    mg.add_field("topographic__elevation", z, at="node", units="-")

    fr = FlowAccumulator(mg, flow_director="D8")
    lf = DepressionFinderAndRouter(mg)

    fr.run_one_step()
    lf.map_depressions()
    assert mg.at_node["drainage_area"] == approx(A_new)
    assert lf.depression_outlet_map == approx(depr_outlet_target)
Exemplo n.º 51
0
# Initializing the elevation values
_ = mg.add_zeros('node', 'topographic__elevation')
z = np.zeros((Nr, Nc))
mg.at_node['topographic__elevation'] = z.reshape(Nr * Nc)

# Imposing fixed elevation boundary conditions
for edge in (mg.nodes_at_left_edge, mg.nodes_at_right_edge):
    mg.status_at_node[edge] = FIXED_VALUE_BOUNDARY
for edge in (mg.nodes_at_bottom_edge, mg.nodes_at_top_edge):
    mg.status_at_node[edge] = FIXED_VALUE_BOUNDARY

# Selecting D_infinity as the flow direction method
fc = FlowAccumulator(mg, flow_director='FlowDirectorDINF')
fd = LinearDiffuser(mg, linear_diffusivity=D)
fc.run_one_step()

# Minimum time-steps for simulation to run
min_try = 500

# Other important variables for the simulation
i = -1
t = 0
dt = 1.
diff_list = []
steady_state = False

while steady_state is False:

    # Selecting 'dt' for high accuracy (user dependent)
    i += 1
Exemplo n.º 52
0
class ErosionModel(object):
    """Base class providing common functionality for terrainbento models.

    An **ErosionModel** is the skeleton for the models of terrain evolution in
    terrainbento.

    This is a base class that does not implement any processes, but rather
    simply handles I/O and setup. Derived classes are meant to include
    Landlab components to model actual erosion processes.

    It is expected that a derived model will define an **__init__** and a
    **run_one_step** method. If desired, the derived model can overwrite the
    existing **run_for**, **run**, and **finalize** methods.

    The following at-node fields must be specified in the grid:
        - ``topographic__elevation``
    """

    _required_fields = ["topographic__elevation"]

    # Setup
    @classmethod
    def from_file(cls, file_like):
        """Construct a terrainbento model from a file.

        Parameters
        ----------
        file_like : file_like or str
            Contents of a parameter file, a file-like object, or the path to
            a parameter file.

        Examples
        --------
        >>> from io import StringIO
        >>> filelike = StringIO('''
        ... grid:
        ...   RasterModelGrid:
        ...     - [4, 5]
        ...     - fields:
        ...         node:
        ...           topographic__elevation:
        ...             constant:
        ...               - value: 0.0
        ... clock:
        ...   step: 1
        ...   stop: 200
        ... ''')
        >>> model = ErosionModel.from_file(filelike)
        >>> model.clock.step
        1.0
        >>> model.clock.stop
        200.0
        >>> model.grid.shape
        (4, 5)
        """
        # first get contents.
        try:
            contents = file_like.read()
        except AttributeError:  # was a str
            if os.path.isfile(file_like):
                with open(file_like, "r") as fp:
                    contents = fp.read()
            else:
                contents = file_like  # not tested

        # then parse contents.
        params = yaml.safe_load(contents)

        # construct instance
        return cls.from_dict(params)

    @classmethod
    def from_dict(cls, params, output_writers=None):
        """Construct a terrainbento model from an input parameter dictionary.

        The input parameter dictionary portion associated with the "grid"
        keword will be passed directly to the Landlab
        `create_grid <https://landlab.readthedocs.io/en/master/reference/grid/create.html#landlab.grid.create.create_grid>`_.
        function.

        Parameters
        ----------
        params : dict
            Dictionary of input parameters.
        output_writers : dictionary of output writers.
            Classes or functions used to write incremental output (e.g. make a
            diagnostic plot). There are two formats for the dictionary entries:

            1) Items can have a key of "class" or "function" and a value of
               a list of simple output classes (uninstantiated) or
               functions, respectively. All output writers defined this way
               will use the `output_interval` provided to the ErosionModel
               constructor.
            2) Items can have a key with any unique string representing the
               output writer's name and a value containing a dict with the
               uninstantiated class and arguments. The value follows the
               format:

               .. code-block:: python

                   {
                    'class' : MyWriter,
                    'args' : [], # optional
                    'kwargs' : {}, # optional
                    }

               where `args` and `kwargs` are passed to the constructor for
               `MyWriter`. `MyWriter` must be a child class of
               GenericOutputWriter.

               The two formats can be present simultaneously. See the Jupyter
               notebook examples for more details.

        Examples
        --------
        >>> params = {
        ...     "grid": {
        ...         "RasterModelGrid": [
        ...             (4, 5),
        ...             {
        ...                 "fields": {
        ...                     "node": {
        ...                         "topographic__elevation": {
        ...                             "constant": [{"value": 0.0}]
        ...                         }
        ...                     }
        ...                 }
        ...             },
        ...         ]
        ...     },
        ...     "clock": {"step": 1, "stop": 200},
        ... }
        >>> model = ErosionModel.from_dict(params)
        >>> model.clock.step
        1.0
        >>> model.clock.stop
        200.0
        >>> model.grid.shape
        (4, 5)
        """
        cls._validate(params)

        # grid, clock
        grid = create_grid(params.pop("grid"))
        clock = Clock.from_dict(params.pop("clock"))

        # precipitator
        precip_params = params.pop("precipitator", _DEFAULT_PRECIPITATOR)
        precipitator = _setup_precipitator_or_runoff(grid, precip_params,
                                                     _SUPPORTED_PRECIPITATORS)

        # runoff_generator
        runoff_params = params.pop("runoff_generator",
                                   _DEFAULT_RUNOFF_GENERATOR)
        runoff_generator = _setup_precipitator_or_runoff(
            grid, runoff_params, _SUPPORTED_RUNOFF_GENERATORS)

        # boundary_handlers
        boundary_handlers = params.pop("boundary_handlers", {})
        bh_dict = {}
        for name in boundary_handlers:
            bh_params = boundary_handlers[name]
            bh_dict[name] = _setup_boundary_handlers(grid, name, bh_params)

        # create instance
        return cls(
            clock,
            grid,
            precipitator=precipitator,
            runoff_generator=runoff_generator,
            boundary_handlers=bh_dict,
            output_writers=output_writers,
            **params,
        )

    @classmethod
    def _validate(cls, params):
        """Make sure necessary things for a model grid and a clock are here."""
        if "grid" not in params:
            raise ValueError("No grid provided as part of input parameters")
        if "clock" not in params:
            raise ValueError("No clock provided as part of input parameters")

    def __init__(
        self,
        clock,
        grid,
        precipitator=None,
        runoff_generator=None,
        flow_director="FlowDirectorSteepest",
        depression_finder=None,
        flow_accumulator_kwargs=None,
        boundary_handlers=None,
        output_writers=None,
        output_default_netcdf=True,
        output_interval=None,
        save_first_timestep=True,
        save_last_timestep=True,
        output_prefix="terrainbento-output",
        output_dir=_DEFAULT_OUTPUT_DIR,
        fields=None,
    ):
        """
        Parameters
        ----------
        clock : terrainbento Clock instance
        grid : landlab model grid instance
            The grid must have all required fields.
        precipitator : terrainbento precipitator, optional
            An instantiated version of a valid precipitator. See the
            :py:mod:`precipitator <terrainbento.precipitator>` module for
            valid options. The precipitator creates rain. Default value is the
            :py:class:`UniformPrecipitator` with a rainfall flux of 1.0.
        runoff_generator : terrainbento runoff_generator, optional
            An instantiated version of a valid runoff generator. See the
            :py:mod:`runoff generator <terrainbento.runoff_generator>` module
            for valid options. The runoff generator converts rain into runoff.
            This runoff is then accumulated into surface water discharge
            (:math:`Q`) and used by channel erosion components. Default value
            is :py:class:`SimpleRunoff` in which all rainfall turns into
            runoff. For the drainage area version of the stream power law use
            the default precipitator and runoff_generator.

            If the default values of both the precipitator and
            runoff_generator are used, then :math:`Q` will be equal to drainage
            area.

        flow_director : str, optional
            String name of a
            `Landlab FlowDirector <https://landlab.readthedocs.io/en/master/reference/components/flow_director.html>`_.
            Default is "FlowDirectorSteepest".
        depression_finder : str, optional
            String name of a Landlab depression finder. Default is None.
        flow_accumulator_kwargs : dictionary, optional
            Dictionary of any additional keyword arguments to pass to the
            `Landlab FlowAccumulator <https://landlab.readthedocs.io/en/master/reference/components/flow_accum.html>`_.
            Default is an empty dictionary.
        boundary_handlers : dictionary, optional
            Dictionary with ``name: instance`` key-value pairs. Each entry
            must be a valid instance of a terrainbento boundary handler. See
            the :py:mod:`boundary handlers <terrainbento.boundary_handlers>`
            module for valid options.
        output_writers : dictionary of output writers.
            Classes or functions used to write incremental output (e.g. make a
            diagnostic plot). There are two formats for the dictionary entries:

            1. ("Old style") Items can have a key of "class" or "function"
               and a value that is a list of uninstantiated output classes
               or a list of output functions, respectively. All output
               writers defined this way will use the **output_interval**
               argument provided to the ErosionModel constructor.
            2. ("New style") Items can have a key of any unique string
               representing the output writer's name and a value that is a
               dictionary containing the uninstantiated class and any
               arguments. The dictionary follows the format:

               .. code-block:: python

                  {
                    'class' : MyWriter,
                    'args' : [], # optional
                    'kwargs' : {}, # optional
                    }

               where `args` and `kwargs` are passed to the constructor for
               `MyWriter`. All new style output writers must be a child
               class of GenericOutputWriter. The ErosionModel reference is
               automatically prepended to args.

            The two formats can be present simultaneously. See the Jupyter
            notebook examples for more details.
        output_default_netcdf : bool, optional
            Indicates whether the erosion model should automatically create a
            simple netcdf output writer which behaves identical to the built-in
            netcdf writer from older terrainbento versions. Uses the
            'output_interval' argument as the output interval. Defaults to True.
        output_interval : float, optional
            The time between output calls for old-style output writers and the
            default netcdf writer. Default is the Clock's stop time.
        save_first_timestep : bool, optional
            Indicates whether model output should be saved at time zero (the
            initial conditions). This affects old and new style output writers.
            Default is True.
        save_last_timestep : bool, optional
            Indicates that the last output time must be at the clock stop time.
            This affects old and new style output writers. Defaults to True.
        output_prefix : str, optional
            String prefix for names of all output files. Default is
            ``"terrainbento-output"``.
        output_dir : string, optional
            Directory that output should be saved to. Defaults to an "output"
            directory in the current directory.
        fields : list, optional
            List of field names to write as netCDF output. Default is to only
            write out "topographic__elevation".

        Returns
        -------
        ErosionModel: object

        Examples
        --------
        This model is a base class and is not designed to be run on its own. We
        recommend that you look at the terrainbento tutorials for examples of
        usage.
        """
        flow_accumulator_kwargs = flow_accumulator_kwargs or {}
        boundary_handlers = boundary_handlers or {}
        output_writers = output_writers or {}
        fields = fields or ["topographic__elevation"]
        # type checking
        if isinstance(clock, Clock) is False:
            raise ValueError("Provided Clock is not valid.")
        if isinstance(grid, ModelGrid) is False:
            raise ValueError("Provided Grid is not valid.")

        # save the grid, clock, and parameters.
        self.grid = grid
        self.clock = clock

        # first pass of verifying fields
        self._verify_fields(self._required_fields)

        # save reference to elevation
        self.z = grid.at_node["topographic__elevation"]

        self.grid.add_zeros("node", "cumulative_elevation_change")

        self.grid.add_field("node", "initial_topographic__elevation",
                            self.z.copy())

        # save output_information
        self.save_first_timestep = save_first_timestep
        self.save_last_timestep = save_last_timestep
        self._output_prefix = output_prefix
        self.output_dir = output_dir
        self.output_fields = fields
        self._output_files = []
        if output_interval is None:
            output_interval = clock.stop
        self.output_interval = output_interval

        # instantiate model time.
        self._model_time = 0.0

        # instantiate container for computational timestep:
        self._compute_time = [tm.time()]

        ###################################################################
        # address Precipitator and RUNOFF_GENERATOR
        ###################################################################

        # verify that precipitator is valid
        if precipitator is None:
            precipitator = UniformPrecipitator(self.grid)
        else:
            if isinstance(precipitator, _VALID_PRECIPITATORS) is False:
                raise ValueError("Provided value for precipitator not valid.")
        self.precipitator = precipitator

        # verify that runoff_generator is valid
        if runoff_generator is None:
            runoff_generator = SimpleRunoff(self.grid)
        else:
            if isinstance(runoff_generator, _VALID_RUNOFF_GENERATORS) is False:
                raise ValueError(
                    "Provide value for runoff_generator not valid.")
        self.runoff_generator = runoff_generator

        ###################################################################
        # instantiate flow direction and accumulation
        ###################################################################
        # Instantiate a FlowAccumulator, if DepressionFinder is provided
        # AND director = Steepest, then we need routing to be D4,
        # otherwise, just passing params should be sufficient.
        if (depression_finder is not None) and (flow_director
                                                == "FlowDirectorSteepest"):
            self.flow_accumulator = FlowAccumulator(
                self.grid,
                routing="D4",
                depression_finder=depression_finder,
                **flow_accumulator_kwargs,
            )
        else:
            self.flow_accumulator = FlowAccumulator(
                self.grid,
                flow_director=flow_director,
                depression_finder=depression_finder,
                **flow_accumulator_kwargs,
            )

        if self.flow_accumulator.depression_finder is None:
            self._erode_flooded_nodes = True
        else:
            self._erode_flooded_nodes = False

        ###################################################################
        # Boundary Conditions and Output Writers
        ###################################################################
        _verify_boundary_handler(boundary_handlers)
        self.boundary_handlers = boundary_handlers

        # Instantiate all the output writers and store in a list
        self.all_output_writers = self._setup_output_writers(
            output_writers,
            output_default_netcdf,
        )

        # Keep track of when each writer needs to write next
        self.active_output_times = {}  # {next time : [writers]}
        self.sorted_output_times = []  # sorted list of the next output times
        for ow_writer in self.all_output_writers:
            first_time = ow_writer.advance_iter()
            self._update_output_times(ow_writer, first_time, None)

    def _verify_fields(self, required_fields):
        """Verify all required fields are present."""
        for field in required_fields:
            if field not in self.grid.at_node:
                raise ValueError(
                    "Required field {field} not present.".format(field=field))

    def _setup_output_writers(self, output_writers, output_default_netcdf):
        """Convert all output writers to the new style and instantiate output
        writer classes.

        Parameters
        ----------
        output_writers : dictionary of output writers.
            Classes or functions used to write incremental output (e.g. make a
            diagnostic plot). There are two formats for the dictionary entries:

            1) ("Old style") Items can have a key of "class" or "function"
               and a value that is a list of uninstantiated output classes
               or a list of output functions, respectively. All output
               writers defined this way will use the **output_interval**
               argument provided to the ErosionModel constructor.
            2) ("New style") Items can have a key of any unique string
               representing the output writer's name and a value that is a
               dictionary containing the uninstantiated class and any
               arguments. The dictionary follows the format: {
                    'class' : MyWriter,
                    'args' : [], # optional
                    'kwargs' : {}, # optional
                    }
               where `args` and `kwargs` are passed to the constructor for
               `MyWriter`. All new style output writers must be a child
               class of GenericOutputWriter. The ErosionModel reference is
               automatically prepended to args.

            The two formats can be present simultaneously. See the Jupyter
            notebook examples for more details.

        output_default_netcdf : bool, optional
            Indicates whether the erosion model should automatically create a
            simple netcdf output writer which behaves identical to the built-in
            netcdf writer from older terrainbento versions. Uses the
            'output_interval' argument as the output interval. Defaults to True.

        Returns
        -------
        instantiated_output_writers : list of GenericOutputWriter objects
            A list of instantiated output writers all based on the
            GenericOutputWriter.

        Notes
        -----
        All classes and functions provided in the 'class' and 'function'
        entries in the output_writer dictionary will be given to an adapter
        class for StaticIntervalOutputWriter so that they can be used with the
        new framework. All of theses writers will use `output_interval`.

        """

        # Note: I can't guarantee that the names will stay unique. I need to
        # convert the 'class' and 'function' writers to the new style and there
        # is a non-zero chance the user happens to use a name for the new style
        # that has the same name that I give the converted writers. These names
        # are used for output filenames, so I don't want to use anything ugly.
        # Hence why I return a list instead of another dictionary.

        # Add a default netcdf writer if desired.
        assert isinstance(output_default_netcdf, bool)
        if output_default_netcdf:
            output_writers["simple-netcdf"] = {
                "class": OWSimpleNetCDF,
                "args": [self.output_fields],
                "kwargs": {
                    "intervals": self.output_interval,
                    "add_id": True,
                    "output_dir": self.output_dir,
                },
            }

        instantiated_writers = []
        for name in output_writers:
            if name == "class":
                # Old style class output writers. Give information to an
                # adapter for instantiating as a static interval writer.
                for ow_class in output_writers["class"]:
                    new_writer = StaticIntervalOutputClassAdapter(
                        model=self,
                        output_interval=self.output_interval,
                        ow_class=ow_class,
                        save_first_timestep=self.save_first_timestep,
                        save_last_timestep=self.save_last_timestep,
                        output_dir=self.output_dir,
                    )
                    # new_name = new_writer.name
                    # assert new_name not in instantiated_writers, \
                    #        f"Output writer '{name}' already exists"
                    instantiated_writers.append(new_writer)

            elif name == "function":
                # Old style function output writers. Give information to an
                # adapter for instantiating as a static interval writer.
                for ow_function in output_writers["function"]:
                    new_writer = StaticIntervalOutputFunctionAdapter(
                        model=self,
                        output_interval=self.output_interval,
                        ow_function=ow_function,
                        save_first_timestep=self.save_first_timestep,
                        save_last_timestep=self.save_last_timestep,
                        output_dir=self.output_dir,
                    )
                    instantiated_writers.append(new_writer)

            else:
                # New style output writer class
                writer_dict = output_writers[name]
                assert isinstance(
                    writer_dict, dict
                ), "The new style output writer entry must be a dictionary"
                assert "class" in writer_dict, "".join([
                    f"New style output writer {name} must have a 'class'",
                    "entry",
                ])
                ow_class = writer_dict["class"]
                ow_args = writer_dict.get("args", [self])
                ow_kwargs = writer_dict.get("kwargs", {})

                # Prepend a reference to the model to the args (if not there)
                if ow_args and ow_args[0] is not self:
                    ow_args = [self] + ow_args
                elif ow_args is None:  # pragma: no cover
                    ow_args = [self]

                # Add some kwargs if they were not already provided
                defaults = {
                    "name": name,
                    "save_first_timestep": self.save_first_timestep,
                    "save_last_timestep": self.save_last_timestep,
                    "output_dir": self.output_dir,
                }
                if issubclass(ow_class, StaticIntervalOutputWriter):
                    if "times" not in ow_kwargs:
                        # Using a static interval writer and no times provided,
                        # use the output_interval as a default interval.
                        defaults["intervals"] = self.output_interval
                defaults.update(ow_kwargs)
                ow_kwargs = defaults

                new_writer = ow_class(*ow_args, **ow_kwargs)
                instantiated_writers.append(new_writer)

        return instantiated_writers

    # Attributes
    @property
    def model_time(self):
        """Return current time of model integration in model time units."""
        return self._model_time

    @property
    def next_output_time(self):
        """Return the next output time in model time units. If there are no
        more active output writers, return np.inf instead."""
        if self.sorted_output_times:

            return self.sorted_output_times[0]
        else:
            return np.inf

    @property
    def output_prefix(self):
        """ Model prefix for output filenames. """
        return self._output_prefix

    @property
    def _out_file_name(self):
        """(Deprecated) Get the filename model prefix. Used to get the netcdf
        filename base."""
        warnings.warn(
            " ".join([
                "ErosionModel's _out_file_name is no longer available.",
                "Getting _output_prefix instead, but may not behave as expected.",
                "Please use the 'output_prefix' argument in the constructor.",
            ]),
            DeprecationWarning,
        )
        return self._output_prefix

    @_out_file_name.setter
    def _out_file_name(self, prefix):
        """(Deprecated) Set the filename model prefix. Used to set the netcdf
        filename base."""
        warnings.warn(
            " ".join([
                "ErosionModel's _out_file_name is no longer available.",
                "Setting _output_prefix instead, but may not behave as expected.",
                "Please use the 'output_prefix' argument in the constructor.",
            ]),
            DeprecationWarning,
        )
        self._output_prefix = prefix

    # Model run methods
    def calculate_cumulative_change(self):
        """Calculate cumulative node-by-node changes in elevation."""
        self.grid.at_node["cumulative_elevation_change"][:] = (
            self.grid.at_node["topographic__elevation"] -
            self.grid.at_node["initial_topographic__elevation"])

    def create_and_move_water(self, step):
        """Create and move water.

        Run the precipitator, the runoff generator, and the flow
        accumulator, in that order.
        """
        self.precipitator.run_one_step(step)
        self.runoff_generator.run_one_step(step)
        self.flow_accumulator.run_one_step()

    def finalize__run_one_step(self, step):
        """Finalize run_one_step method.

        This base-class method increments model time and updates
        boundary conditions.
        """
        # calculate model time
        self._model_time += step

        # Update boundary conditions
        self.update_boundary_conditions(step)

    def finalize(self):
        """Finalize model.

        This base-class method does nothing. Derived classes can
        override it to run any required finalization steps.
        """
        pass

    def run_for(self, step, runtime):
        """Run model without interruption for a specified time period.

        ``run_for`` runs the model for the duration ``runtime`` with model time
        steps of ``step``.

        Parameters
        ----------
        step : float
            Model run timestep.
        runtime : float
            Total duration for which to run model.
        """
        elapsed_time = 0.0
        keep_running = True
        while keep_running:
            if elapsed_time + step >= runtime:
                step = runtime - elapsed_time
                keep_running = False
            self.run_one_step(step)
            elapsed_time += step

    def run(self):
        """Run the model until complete.

        The model will run for the duration indicated by the input file
        or dictionary parameter ``"stop"``, at a time step specified by
        the parameter ``"step"``, and create ouput at intervals specified by
        the individual output writers.
        """
        self._itters = []

        if self.save_first_timestep:
            self.iteration = 0
            self._itters.append(0)
            self.calculate_cumulative_change()
            self.write_output()
        self.iteration = 1
        time_now = self._model_time
        while time_now < self.clock.stop:
            next_run_pause = min(
                # time_now + self.output_interval, self.clock.stop,
                self.next_output_time,
                self.clock.stop,
            )
            assert next_run_pause > time_now
            self.run_for(self.clock.step, next_run_pause - time_now)
            time_now = self._model_time
            self._itters.append(self.iteration)
            self.calculate_cumulative_change()
            self.write_output()
            self.iteration += 1

        # now that the model is finished running, execute finalize.
        self.finalize()

    def _ensure_precip_runoff_are_vanilla(self, vsa_precip=False):
        """Ensure only default versions of precipitator/runoff are used.

        Some models only work when the precipitator and runoff generator
        are the default versions.
        """
        if isinstance(self.precipitator, UniformPrecipitator) is False:
            raise ValueError(
                "This model must be run with a UniformPrecipitator.")

        if vsa_precip is False:
            if self.precipitator._rainfall_flux != 1:
                raise ValueError(
                    "This model must use a rainfall__flux value of 1.0.")

        # if isinstance(self.runoff_generator, SimpleRunoff) is False:
        #     raise ValueError("This model must be run with SimpleRunoff.")

        if self.runoff_generator.runoff_proportion != 1.0:
            raise ValueError("The model must use a runoff_proportion of 1.0.")

    def update_boundary_conditions(self, step):
        """Run all boundary handlers forward by step.

        Parameters
        ----------
        step : float
            Timestep in unit of model time.
        """
        # Run each of the baselevel handlers.
        for name in self.boundary_handlers:
            self.boundary_handlers[name].run_one_step(step)

    # Output methods
    def write_output(self):
        """Run output writers if it is the correct model time.  """

        # assert that the model has not passed the next output time.
        assert self._model_time <= self.next_output_time, "".join([
            f"Model time (t={self._model_time}) has passed the next ",
            f"output time (t={self.next_output_time})",
        ])

        if self._model_time == self.next_output_time:
            # The current model time matches the next output time
            current_time = self.sorted_output_times.pop(0)
            current_writers = self.active_output_times.pop(current_time)
            for ow_writer in current_writers:
                # Run all the output writers associated with this time.
                ow_writer.run_one_step()
                next_time = ow_writer.advance_iter()
                self._update_output_times(ow_writer, next_time, current_time)

    def _update_output_times(self, ow_writer, new_time, current_time):
        """Private method to update the dictionary of active output writers
        and the sorted list of next output times.

        Parameters
        ----------
        ow_writer : GenericOutputWriter object
            The output writer that has just finished writing output and advanced
            it's time iterator.
        new_time : float
            The next time that the output writer will need to write output.
        current_time : float
            The current model time.

        Notes
        -----
        This function enforces that output times align with model steps. If an
        output writer returns a next_time that is in between model steps, then
        the output time is delayed to the following step and a warning is
        generated. This function may generate skip warnings if the subsequent
        next times are less than the delayed step time.

        """
        if new_time is None:
            # The output writer has exhausted all of it's output times.
            # Do not add it back to the active dict/list
            return

        model_step = self.clock.step
        if current_time is not None:
            try:
                assert new_time > current_time
            except AssertionError:
                warnings.warn("".join([
                    f"The output writer {ow_writer.name} is providing a ",
                    "next time that is less than or equal to the current ",
                    "time. Possibly because the previous time was in ",
                    "between steps, delaying the output until now. ",
                    "Skipping ahead.",
                ]))
                for n_skips in range(10):
                    # Allow 10 attempts to skip
                    new_time = ow_writer.advance_iter()
                    if new_time is None:
                        # iterator exhausted. No more processing needed
                        return
                    elif new_time > current_time:
                        break
                else:
                    # Could not find a suitable next_time
                    raise AssertionError("".join([
                        "Output writer failed to return a next time greater ",
                        "than the current time after several attempts.",
                    ]))

        # See if the new output time aligns with the model step.
        if (new_time % model_step) != 0.0:
            warnings.warn("".join([
                f"Output writer {ow_writer.name} is requesting a ",
                "time that is not divisible by the model step. ",
                "Delaying output to the following step.\n",
                f"Output time = {new_time}\n",
                f"Model step = {model_step}\n",
                f"Remainder = {new_time % model_step}\n\n",
            ]))
            new_time = np.ceil(new_time / model_step) * model_step

        # Add the writer to the active_output_times dict
        if new_time in self.active_output_times:
            # New time is already in the active_output_times dictionary
            self.active_output_times[new_time].append(ow_writer)
        else:
            # New time is not in the active_output_times dictionary
            # Add it to the dict and resort the output times list
            self.active_output_times[new_time] = [ow_writer]
            self.sorted_output_times = sorted(self.active_output_times)

    def to_xarray_dataset(
        self,
        time_unit="time units",
        reference_time="model start",
        space_unit="space units",
    ):
        """Convert model output to an xarray dataset.

        If you would like to have CF compliant NetCDF make sure that your time
        and space units and reference times will work with standard decoding.

        The default time unit and reference time will give the time dimention a
        value of "time units since model start". The default space unit will
        give a value of "space unit".

        Parameters
        ----------
        time_unit: str, optional
            Name of time unit. Default is "time units".
        reference time: str, optional
            Reference tim. Default is "model start".
        space_unit: str, optional
            Name of space unit. Default is "space unit".
        """
        # open all files as a xarray dataset
        ds = xr.open_mfdataset(
            self.get_output(extension="nc"),
            concat_dim="nt",
            engine="netcdf4",
            combine="nested",
            data_vars=self.output_fields,
        )

        # add a time dimension
        time_array = np.asarray(self._itters) * self.output_interval
        time = xr.DataArray(
            time_array,
            dims=("nt"),
            attrs={
                "units": time_unit + " since " + reference_time,
                "standard_name": "time",
            },
        )

        ds["time"] = time

        # set x and y to coordinates
        ds = ds.set_coords(["x", "y", "time"])

        # rename dimensions
        ds = ds.rename(name_dict={"ni": "x", "nj": "y", "nt": "time"})

        # set x and y units
        ds["x"] = xr.DataArray(ds.x, dims=("x"), attrs={"units": space_unit})
        ds["y"] = xr.DataArray(ds.y, dims=("y"), attrs={"units": space_unit})

        return ds

    def save_to_xarray_dataset(
        self,
        filename="terrainbento.nc",
        time_unit="time units",
        reference_time="model start",
        space_unit="space units",
    ):
        """Save model output to xarray dataset.

        If you would like to have CF compliant NetCDF make sure that your time
        and space units and reference times will work with standard decoding.

        The default time unit and reference time will give the time dimention a
        value of "time units since model start". The default space unit will
        give a value of "space unit".

        Parameters
        ----------
        filename: str, optional
            The file path where the file should be saved. The default value is
            "terrainbento.nc".
        time_unit: str, optional
            Name of time unit. Default is "time units".
        reference time: str, optional
            Reference tim. Default is "model start".
        space_unit: str, optional
            Name of space unit. Default is "space unit".
        """
        ds = self.to_xarray_dataset(
            time_unit=time_unit,
            space_unit=space_unit,
            reference_time=reference_time,
        )
        ds.to_netcdf(filename, engine="netcdf4", format="NETCDF4")
        ds.close()

    def _format_extension_and_writer_args(self, extension, writer):
        """Private method to parse the extension and writer arguments for the
        **remove_output** and **get_output** functions.

        Parameters
        ----------
        extension : string or list of strings or None
            Specify the type(s) of files to look for.
        writer : GenericOutputWriter instance or list of instances or string or list of strings or None
            Specify which output writers to look at either by the writer's
            handle or by the writer's name.

        Returns
        -------
        extension_list : list of strings
            List of strings representing the types of files to look for. Can
            return [None] indicating all files.
        writer_list : list of GenericOutputWriters
            List of GenericOutputWriter instances to look at. Can
            return [None] indicating all output writers should be looked at.
        """

        extension_list = None
        writer_list = None

        if isinstance(writer, GenericOutputWriter):
            # Writer argument is an object, convert to a list
            writer_list = [writer]
        elif isinstance(writer, str):
            # Writer argument is the name of the writer, get object
            writer_list = self.get_output_writer(writer)
        elif isinstance(writer, list):
            # Writer argument is a list
            writer_list = []
            # Check what is in the list
            for i, w in enumerate(writer):
                if isinstance(w, GenericOutputWriter):
                    writer_list.append(w)
                elif isinstance(w, str):
                    # Item is a name, replace with the object
                    found_writers = self.get_output_writer(w)
                    writer_list += found_writers
                else:  # pragma: no cover
                    raise TypeError(f"Unrecognized writer argument. {w}")
        elif writer is None:
            # Default to all writers
            writer_list = self.all_output_writers
        else:  # pragma: no cover
            raise TypeError(f"Unrecognized writer argument. {writer}")

        if isinstance(extension, str):
            # Extension argument is a string
            extension_list = [extension]
        elif isinstance(extension, list):
            # Extension argument is a list of strings
            extension_list = extension
            assert all([isinstance(e, str) for e in extension_list])
        elif extension is None:
            # Default to all extensions
            extension_list = [None]
        else:  # pragma: no cover
            raise TypeError(f"Unrecognized extension argument. {extension}")

        return extension_list, writer_list

    def remove_output_netcdfs(self):
        """Remove netcdf output files written during a model run. Only works
        for new style writers including the default netcdf writer."""
        self.remove_output(extension="nc")

    def remove_output(self, extension=None, writer=None):
        """Remove files written by new style writers during a model
        run. Does not work for old style writers which have no way to report
        what they have written. Can specify types of files and/or writers.

        To do: allow 'writer' to be a string for the name of the writer?

        Parameters
        ----------
        extension : string or list of strings, optional
            Specify what type(s) of files should be deleted. Defaults to None
            which deletes all file types. Don't include a leading period.
        writer : GenericOutputWriter instance or list of instances or string or list of strings or None
            Specify if the files should come from certain output writers either
            by the writer's handle or by the writer's name. Defaults to
            deleting files from all writers.

        """

        lists = self._format_extension_and_writer_args(extension, writer)
        extension_list, writer_list = lists

        for ow in writer_list:
            assert ow is not None
            for ext in extension_list:
                assert ext is None or isinstance(ext, str)
                if ext and ext[0] == ".":
                    ext = ext[1:]  # ignore leading period if present
                ow.delete_output_files(ext)

    def get_output(self, extension=None, writer=None):
        """Get a list of filepaths for files written by new style writers
        during a model run. Does not work for old style writers which have no
        way to report what they have written.  Can specify types of files
        and/or writers.

        Parameters
        ----------
        extension : string or list of strings, optional
            Specify what type(s) of files should be returned. Defaults to None
            which returns all file types. Don't include a leading period.
        writer : GenericOutputWriter instance or list of instances or string or list of strings or None
            Specify if the files should come from certain output writers either
            by the writer's handle or by the writer's name. Defaults to
            returning files from all writers.

        Returns
        -------
        filepaths : list of strings
            A list of filepath strings that match the desired extensions and
            writers.
        """

        lists = self._format_extension_and_writer_args(extension, writer)
        extension_list, writer_list = lists

        output_list = []
        for ow in writer_list:
            assert ow is not None
            for ext in extension_list:
                assert ext is None or isinstance(ext, str)
                output_list += ow.get_output_filepaths(ext)

        return output_list

    def get_output_writer(self, name):
        """Get the references for object writer(s) from the writer's name.

        Parameters
        ----------
        name : string
            The name of the output writer to look for. Can match multiple
            writers.

        Returns
        -------
        matches : list of GenericOutputWriter objects
            The list of any GenericOutputWriter whose name contains the
            argument name string. Will return an empty list if there are no
            matches.

        """
        matches = []
        for ow in self.all_output_writers:
            if name in ow.name:
                matches.append(ow)
        return matches
Exemplo n.º 53
0
def test_matches_bedrock_alluvial_solution():
    """
    Test that model matches the bedrock-alluvial analytical solution
    for slope/area relationship at steady state:
    S=((U * v_s * (1 - F_f)) / (K_sed * A^m) + U / (K_br * A^m))^(1/n).

    Also test that the soil depth everywhere matches the bedrock-alluvial
    analytical solution at steady state:
    H = -H_star * ln(1 - (v_s / (K_sed / (K_br * (1 - F_f)) + v_s))).
    """

    #set up a 5x5 grid with one open outlet node and low initial elevations.
    nr = 5
    nc = 5
    mg = RasterModelGrid((nr, nc), 10.0)

    z = mg.add_zeros('node', 'topographic__elevation')
    br = mg.add_zeros('node', 'bedrock__elevation')
    soil = mg.add_zeros('node', 'soil__depth')

    mg['node']['topographic__elevation'] += (
        mg.node_y / 100000 + mg.node_x / 100000 +
        np.random.rand(len(mg.node_y)) / 10000)
    mg.set_closed_boundaries_at_grid_edges(bottom_is_closed=True,
                                           left_is_closed=True,
                                           right_is_closed=True,
                                           top_is_closed=True)
    mg.set_watershed_boundary_condition_outlet_id(
        0, mg['node']['topographic__elevation'], -9999.)
    soil[:] += 0.  #initial condition of no soil depth.
    br[:] = z[:]
    z[:] += soil[:]

    #Instantiate DepressionFinderAndRouter
    df = DepressionFinderAndRouter(mg)

    # Create a D8 flow handler
    fa = FlowAccumulator(mg,
                         flow_director='D8',
                         depression_finder='DepressionFinderAndRouter')

    # Parameter values for detachment-limited test
    K_br = 0.02
    K_sed = 0.02
    U = 0.0001
    dt = 1.0
    F_f = 0.2  #all detached rock disappears; detachment-ltd end-member
    m_sp = 0.5
    n_sp = 1.0
    v_s = 0.25
    H_star = 0.1

    # Instantiate the Space component...
    sp = Space(mg,
               K_sed=K_sed,
               K_br=K_br,
               F_f=F_f,
               phi=0.0,
               H_star=H_star,
               v_s=v_s,
               m_sp=m_sp,
               n_sp=n_sp,
               sp_crit_sed=0,
               sp_crit_br=0)

    # ... and run it to steady state (10000x1-year timesteps).
    for i in range(10000):
        fa.run_one_step()
        flooded = np.where(df.flood_status == 3)[0]
        sp.run_one_step(dt=dt, flooded_nodes=flooded)
        br[mg.core_nodes] += U * dt  #m
        soil[0] = 0.  #enforce 0 soil depth at boundary to keep lowering steady
        z[:] = br[:] + soil[:]

    #compare numerical and analytical slope solutions
    num_slope = mg.at_node['topographic__steepest_slope'][mg.core_nodes]
    analytical_slope = (np.power(
        ((U * v_s * (1 - F_f)) /
         (K_sed * np.power(mg.at_node['drainage_area'][mg.core_nodes], m_sp)))
        +
        (U /
         (K_br * np.power(mg.at_node['drainage_area'][mg.core_nodes], m_sp))),
        1. / n_sp))

    #test for match with analytical slope-area relationship
    testing.assert_array_almost_equal(
        num_slope,
        analytical_slope,
        decimal=8,
        err_msg='SPACE bedrock-alluvial slope-area test failed',
        verbose=True)

    #compare numerical and analytical soil depth solutions
    num_h = mg.at_node['soil__depth'][mg.core_nodes]
    analytical_h = -H_star * np.log(1 - (v_s / (K_sed / (K_br *
                                                         (1 - F_f)) + v_s)))

    #test for match with analytical sediment depth
    testing.assert_array_almost_equal(
        num_h,
        analytical_h,
        decimal=5,
        err_msg='SPACE bedrock-alluvial soil thickness test failed',
        verbose=True)
Exemplo n.º 54
0
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:
    # We're going to cheat by running Fastscape SP for the first part of the solution
    for (
            interval_duration,
            rainfall_rate,
    ) in precip.yield_storm_interstorm_duration_intensity():
        if rainfall_rate != 0.:
            mg.at_node["water__unit_flux_in"].fill(rainfall_rate)
            mg = fr.run_one_step()
            # print 'Area: ', numpy.max(mg.at_node['drainage_area'])
            mg, _, _ = sp.erode(
                mg,
                interval_duration,
                Q_if_used="surface_water__discharge",
                K_if_used="K_values",
            )
        # add uplift
        mg.at_node["topographic__elevation"][mg.core_nodes] += (
            uplift * interval_duration)
        this_trunc = precip.elapsed_time // out_interval
        if this_trunc != last_trunc:  # a new loop round
            print("made it to loop ", out_interval * this_trunc)
            last_trunc = this_trunc
Exemplo n.º 55
0
class BasicRtVs(ErosionModel):
    """
    A BasicVsRt computes erosion using linear diffusion, basic stream
    power with 2 lithologies, and Q ~ A exp( -b S / A).
    """
    def __init__(self,
                 input_file=None,
                 params=None,
                 BaselevelHandlerClass=None):
        """Initialize the BasicVsRt."""

        # Call ErosionModel's init
        super(BasicVsRt,
              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')

        recharge_rate = (self._length_factor) * self.params[
            'recharge_rate']  # has units length per time
        soil_thickness = (self._length_factor) * self.params[
            'initial_soil_thickness']  # has units length
        K_hydraulic_conductivity = (self._length_factor) * self.params[
            'K_hydraulic_conductivity']  # has units length per time

        # 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)

        # Add a field for effective drainage area
        if 'effective_drainage_area' in self.grid.at_node:
            self.eff_area = self.grid.at_node['effective_drainage_area']
        else:
            self.eff_area = self.grid.add_zeros('node',
                                                'effective_drainage_area')

        # Get the effective-area parameter
        self.sat_param = (K_hydraulic_conductivity * soil_thickness *
                          self.grid.dx) / (recharge_rate)

        # Instantiate a FastscapeEroder component
        self.eroder = StreamPowerEroder(self.grid,
                                        K_sp=self.erody,
                                        m_sp=self.params['m_sp'],
                                        n_sp=self.params['n_sp'],
                                        use_Q=self.eff_area)

        # Instantiate a LinearDiffuser component
        self.diffuser = LinearDiffuser(self.grid,
                                       linear_diffusivity=linear_diffusivity)

    def calc_effective_drainage_area(self):
        """Calculate and store effective drainage area.

        Effective drainage area is defined as:

        $A_{eff} = A \exp ( \alpha S / A) = A R_r$

        where $S$ is downslope-positive steepest gradient, $A$ is drainage
        area, $R_r$ is the runoff ratio, and $\alpha$ is the saturation
        parameter.
        """

        area = self.grid.at_node['drainage_area']
        slope = self.grid.at_node['topographic__steepest_slope']
        cores = self.grid.core_nodes
        self.eff_area[cores] = (
            area[cores] *
            (np.exp(-self.sat_param * slope[cores] / area[cores])))

    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")
        core = self.grid.core_nodes
        if self.contact_width > 0.0:
            self.erody_wt[core] = (
                1.0 /
                (1.0 + np.exp(-(self.z[core] - self.rock_till_contact[core]) /
                              self.contact_width)))
        else:
            self.erody_wt[core] = 0.0
            self.erody_wt[np.where(self.z > self.rock_till_contact)[0]] = 1.0

        # (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()

        # Update effective runoff ratio
        self.calc_effective_drainage_area()

        # Zero out effective area in flooded nodes
        self.eff_area[self.flow_router.depression_finder.flood_status ==
                      3] = 0.0

        # Update the erodibility field
        self.update_erodibility_field()

        # Do some erosion (but not on the flooded nodes)
        self.eroder.run_one_step(dt)

        # 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()
class BasicSt(_StochasticErosionModel):
    """
    A StochasticHortonianSPModel generates a random sequency of
    runoff events across a topographic surface, calculating the resulting
    water discharge at each node.
    """
    def __init__(self,
                 input_file=None,
                 params=None,
                 BaselevelHandlerClass=None):
        """Initialize the StochasticDischargeHortonianModel."""

        # Call ErosionModel's init
        super(BasicSt,
              self).__init__(input_file=input_file,
                             params=params,
                             BaselevelHandlerClass=BaselevelHandlerClass)

        # Get Parameters:
        K_sp = self.get_parameter_from_exponent('K_stochastic_sp',
                                                raise_error=False)
        K_ss = self.get_parameter_from_exponent('K_stochastic_ss',
                                                raise_error=False)
        linear_diffusivity = (
            self._length_factor**2.) * self.get_parameter_from_exponent(
                'linear_diffusivity')  # has units length^2/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:
                K = K_sp
            else:
                K = (
                    self._length_factor**(1. / 2.)
                ) * K_ss  # K_stochastic has units Lengtg^(1/2) per Time^(1/2_
        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)
        # 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']  # has units length per time
        self.infilt = infiltration_capacity

        # Keep a reference to drainage area
        self.area = self.grid.at_node['drainage_area']

        # Run flow routing and lake filler
        self.flow_router.run_one_step()

        # Instantiate a FastscapeEroder component
        self.eroder = FastscapeEroder(self.grid,
                                      K_sp=K,
                                      m_sp=self.params['m_sp'],
                                      n_sp=self.params['n_sp'])

        # 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)

        # update model time
        self.model_time += dt

        # Lower outlet
        self.update_outlet(dt)

        # Check walltime
        self.check_walltime()
Exemplo n.º 57
0
grid.set_status_at_node_on_edges(
    right=grid.BC_NODE_IS_CLOSED,
    top=grid.BC_NODE_IS_CLOSED,
    left=grid.BC_NODE_IS_FIXED_VALUE,
    bottom=grid.BC_NODE_IS_CLOSED,
)
z = grid.add_zeros('node', 'topographic__elevation')
z[:] = 0.1 * hg * np.random.rand(len(z))

fa = FlowAccumulator(grid,
                     surface='topographic__elevation',
                     flow_director='D8',
                     depression_finder='LakeMapperBarnes')
ld = LinearDiffuser(grid, D)
sp = FastscapeEroder(grid, K_sp=Ksp, m_sp=0.5, n_sp=1.0)

for i in range(N):

    z[grid.core_nodes] += U * dt

    ld.run_one_step(dt)
    fa.run_one_step()
    sp.run_one_step(dt)

    # print('completed loop %d'%i)

    if i % output_interval == 0:
        print('finished iteration %d' % i)
        filename = base_path + '%d_grid_%d.nc' % (ID, i)
        to_netcdf(grid, filename, include="at_node:topographic__elevation")
Exemplo n.º 58
0
# Set up the incision field
incise = mg.add_zeros('node', 'incision')

# set model variables. dt is set by courant condition.

dt = 0.1  # Timestep in Ma
cell_area = mg.dx * mg.dy

######################################
### run erosion model for one step ###
######################################

print("Running flow router")
print(datetime.datetime.now().time())
frr.run_one_step()  # flow routing
zrold = zr[mg.nodes]  # pre-incision elevations

print("Running fastscaper")
print(datetime.datetime.now().time())
spr.run_one_step(dt)  # erosion: stream power
zrnew = zr[mg.nodes]  # post-incision elevations
incise = zrold - zrnew  # incision per cell
qs = incise * cell_area  # Volume sediment produced per cell
qsflat = qs.ravel()  # flatten qs for flow routing calculation

# extract cumulative flux (q) as function of flow length.
a, q = find_drainage_area_and_discharge(
    mg.at_node['flow__upstream_node_order'],
    mg.at_node['flow__receiver_node'],
    runoff=qsflat)  # a is number of nodes
Exemplo n.º 59
0
def test_matches_transport_solution():
    """
    Test that model matches the transport-limited analytical solution
    for slope/area relationship at steady state: S=((U * v_s) / (K_sed * A^m)
    + U / (K_sed * A^m))^(1/n).

    Also test that model matches the analytical solution for steady-state
    sediment flux: Qs = U * A * (1 - phi).
    """

    # set up a 5x5 grid with one open outlet node and low initial elevations.
    nr = 5
    nc = 5
    mg = RasterModelGrid((nr, nc), xy_spacing=10.0)

    z = mg.add_zeros("node", "topographic__elevation")
    br = mg.add_zeros("node", "bedrock__elevation")
    soil = mg.add_zeros("node", "soil__depth")

    mg["node"]["topographic__elevation"] += (
        mg.node_y / 100000 + mg.node_x / 100000 +
        np.random.rand(len(mg.node_y)) / 10000)
    mg.set_closed_boundaries_at_grid_edges(
        bottom_is_closed=True,
        left_is_closed=True,
        right_is_closed=True,
        top_is_closed=True,
    )
    mg.set_watershed_boundary_condition_outlet_id(
        0, mg["node"]["topographic__elevation"], -9999.0)
    soil[:] += 100.0  # initial soil depth of 100 m
    br[:] = z[:]
    z[:] += soil[:]

    # Instantiate DepressionFinderAndRouter
    df = DepressionFinderAndRouter(mg)

    # Create a D8 flow handler
    fa = FlowAccumulator(mg,
                         flow_director="D8",
                         depression_finder="DepressionFinderAndRouter")

    # Parameter values for detachment-limited test
    K_sed = 0.01
    U = 0.0001
    dt = 1.0
    F_f = 1.0  # all detached rock disappears; detachment-ltd end-member
    m_sp = 0.5
    n_sp = 1.0
    v_s = 0.5
    phi = 0.5

    # Instantiate the Space component...
    sp = Space(
        mg,
        K_sed=K_sed,
        K_br=0.01,
        F_f=F_f,
        phi=phi,
        H_star=1.0,
        v_s=v_s,
        m_sp=m_sp,
        n_sp=n_sp,
        sp_crit_sed=0,
        sp_crit_br=0,
    )

    # ... and run it to steady state (5000x1-year timesteps).
    for i in range(5000):
        fa.run_one_step()
        flooded = np.where(df.flood_status == 3)[0]
        sp.run_one_step(dt=dt, flooded_nodes=flooded)
        br[mg.core_nodes] += U * dt  # m
        soil[
            0] = 100.0  # enforce constant soil depth at boundary to keep lowering steady
        z[:] = br[:] + soil[:]

    # compare numerical and analytical slope solutions
    num_slope = mg.at_node["topographic__steepest_slope"][mg.core_nodes]
    analytical_slope = np.power(
        ((U * v_s * (1 - phi)) /
         (K_sed * np.power(mg.at_node["drainage_area"][mg.core_nodes], m_sp)))
        +
        ((U * (1 - phi)) /
         (K_sed * np.power(mg.at_node["drainage_area"][mg.core_nodes], m_sp))),
        1.0 / n_sp,
    )

    # test for match with analytical slope-area relationship
    testing.assert_array_almost_equal(
        num_slope,
        analytical_slope,
        decimal=8,
        err_msg="SPACE transport-limited slope-area test failed",
        verbose=True,
    )

    # compare numerical and analytical sediment flux solutions
    num_sedflux = mg.at_node["sediment__flux"][mg.core_nodes]
    analytical_sedflux = U * mg.at_node["drainage_area"][mg.core_nodes] * (1 -
                                                                           phi)

    # test for match with anakytical sediment flux
    testing.assert_array_almost_equal(
        num_sedflux,
        analytical_sedflux,
        decimal=8,
        err_msg="SPACE transport-limited sediment flux test failed",
        verbose=True,
    )
Exemplo n.º 60
0
def test_sp_voronoi():
    nnodes = 100

    np.random.seed(0)
    x = np.random.rand(nnodes)
    np.random.seed(1)
    y = np.random.rand(nnodes)
    mg = VoronoiDelaunayGrid(x, y)

    np.random.seed(2)
    z = mg.add_field("node",
                     "topographic__elevation",
                     np.random.rand(nnodes) / 10000.,
                     copy=False)

    fr = FlowAccumulator(mg)
    spe = StreamPowerEroder(
        mg, os.path.join(_THIS_DIR, "drive_sp_params_voronoi.txt"))

    for i in range(10):
        z[mg.core_nodes] += 0.01
        fr.run_one_step()
        spe.erode(mg, 1.)

    z_tg = np.array([
        4.35994902e-05,
        2.59262318e-06,
        5.49662478e-05,
        6.56738615e-03,
        4.20367802e-05,
        1.21371424e-02,
        2.16596169e-02,
        4.73320898e-02,
        6.00389761e-02,
        5.22007356e-02,
        5.37507115e-02,
        5.95794752e-02,
        5.29862904e-02,
        6.76465914e-02,
        7.31720024e-02,
        6.18730861e-02,
        8.53975293e-05,
        5.32189275e-02,
        7.34302556e-02,
        8.07385044e-02,
        5.05246090e-05,
        4.08940657e-02,
        7.39971005e-02,
        3.31915602e-02,
        6.72650419e-02,
        5.96745309e-05,
        4.72752445e-02,
        3.60359567e-02,
        7.59432065e-02,
        7.24461985e-02,
        7.80305760e-02,
        4.93866869e-02,
        8.69642467e-02,
        7.21627626e-02,
        8.96368291e-02,
        4.65142080e-02,
        6.07720217e-02,
        8.83372939e-02,
        2.35887558e-02,
        7.97616193e-02,
        8.35615355e-02,
        4.61809032e-02,
        6.34634214e-02,
        9.25711770e-02,
        4.11717225e-03,
        7.24493623e-02,
        7.97908053e-02,
        9.10375623e-02,
        9.13155023e-02,
        7.10567915e-02,
        7.35271752e-02,
        6.13091341e-02,
        9.45498463e-02,
        8.48532386e-02,
        8.82702021e-02,
        7.14969941e-02,
        2.22640943e-02,
        8.53311932e-02,
        7.49161159e-02,
        3.48837223e-02,
        9.30132692e-02,
        6.01817121e-05,
        3.87455443e-02,
        8.44673586e-02,
        9.35213577e-02,
        6.76075824e-02,
        1.58614508e-02,
        8.51346837e-02,
        8.83645680e-02,
        8.69944117e-02,
        5.04000439e-05,
        5.02319084e-02,
        8.63882765e-02,
        5.00991880e-02,
        7.65156630e-02,
        5.07591983e-02,
        6.54909962e-02,
        6.91505342e-02,
        7.33358371e-02,
        5.30109890e-02,
        2.99074601e-02,
        2.55509418e-06,
        8.21523907e-02,
        8.09368483e-02,
        4.35073025e-02,
        3.04096109e-02,
        3.26298627e-02,
        4.92259177e-02,
        5.48690358e-02,
        6.44732130e-02,
        6.28133567e-02,
        4.17977098e-06,
        5.37149677e-02,
        4.32828136e-02,
        1.30559903e-02,
        2.62405261e-02,
        2.86079272e-02,
        6.61481327e-05,
        1.70477133e-05,
        8.81652236e-05,
    ])

    assert_array_almost_equal(mg.at_node["topographic__elevation"], z_tg)