Esempio n. 1
0
    def __init__(self, modern_dem_name, outlet_id, chi_mask_dem_name=None, from_file=None):
        """Initialize MetricCalculator with names of postglacial and modern
        DEMs."""


        if from_file is None:

            # Read and remember the modern DEM (whether data or model)
            (self.grid, self.z) = self.read_topography(modern_dem_name)
            #print self.grid.x_of_node
    
            self.grid.set_watershed_boundary_condition_outlet_id(outlet_id,
                                                                 self.z, nodata_value=-9999)
    
            # Instantiate and run a FlowRouter and lake filler, so we get
            # drainage area for cumulative-area statistic, and also fields for chi.
            fr = FlowRouter(self.grid)
            dfr = DepressionFinderAndRouter(self.grid)
            fr.route_flow()
            dfr.map_depressions()
    
            # Remember modern drainage area grid
            self.area = self.grid.at_node['drainage_area']
    
            # Instantiate a ChiFinder for chi-index
            self.chi_finder = ChiFinder(self.grid, min_drainage_area=10000.,
                                        reference_concavity=0.5)
            
            core_nodes = np.zeros(self.area.shape, dtype=bool)
            core_nodes[self.grid.core_nodes] = True
            # Read and remember the MASK, if provided
            if chi_mask_dem_name is None:
                 self.mask = (self.area>1e5)
                 self.till_mask = np.zeros(self.mask.shape, dtype=bool) 
                 self.till_mask[self.grid.core_nodes] = 1
            else:
                (self.mask_grid, zmask) = self.read_topography(chi_mask_dem_name)
                mask = (zmask>0)*1
                self.mask = (self.area>1e5)*(mask==1)
                
                mask_bool = (zmask>0)
                self.till_mask = np.zeros(self.mask.shape, dtype=bool) 
                self.till_mask[mask_bool*core_nodes] = 1
            # 
            
            # Create dictionary to contain metrics
            self.metric = {}
        
        else:
            with open(from_file, 'r') as f:
                metrics = load(f)
                self.modern_dem_name = metrics.pop('Topo file')

                self.metric = metrics
    
            fn_split = from_file.split('.')
            fn_split[-1] = 'chi'
            fn_split.append('txt')
            chi_filename = '.'.join(fn_split)
            self.density_chi = np.loadtxt(chi_filename)
Esempio n. 2
0
def test_D8_D4_fill(d4_grid):
    """
    Tests the functionality of D4 filling.
    """
    lfD8 = DepressionFinderAndRouter(
        d4_grid.mg1, pits=None, routing="D8", reroute_flow=False
    )
    lfD4 = DepressionFinderAndRouter(
        d4_grid.mg2, pits=None, routing="D4", reroute_flow=False
    )

    lfD8.map_depressions()
    lfD4.map_depressions()

    assert lfD8.number_of_lakes == 1
    assert lfD4.number_of_lakes == 3

    correct_D8_lake_map = np.empty(7 * 7, dtype=int)
    correct_D8_lake_map.fill(XX)
    correct_D8_lake_map[d4_grid.lake_nodes] = 10
    correct_D4_lake_map = correct_D8_lake_map.copy()
    correct_D4_lake_map[d4_grid.lake_nodes[5:]] = 32
    correct_D4_lake_map[d4_grid.lake_nodes[-2]] = 38
    correct_D8_depths = np.zeros(7 * 7, dtype=float)
    correct_D8_depths[d4_grid.lake_nodes] = 2.0
    correct_D4_depths = correct_D8_depths.copy()
    correct_D4_depths[d4_grid.lake_nodes[5:]] = 4.0
    correct_D4_depths[d4_grid.lake_nodes[-2]] = 3.0

    assert_array_equal(lfD8.lake_map, correct_D8_lake_map)
    assert_array_equal(lfD4.lake_map, correct_D4_lake_map)

    assert d4_grid.mg1.at_node["depression__depth"] == approx(correct_D8_depths)
    assert d4_grid.mg2.at_node["depression__depth"] == approx(correct_D4_depths)
Esempio n. 3
0
def test_pits_as_IDs(dans_grid3):
    """
    Smoke test for passing specific IDs, not an array, to the mapper.
    """
    dans_grid3.fr.run_one_step()

    pits = np.nonzero(dans_grid3.mg.at_node["flow__sink_flag"])[0]
    lf = DepressionFinderAndRouter(dans_grid3.mg, pits=pits)
    lf.map_depressions()

    assert dans_grid3.mg.at_node["drainage_area"] == approx(dans_grid3.A_new)
Esempio n. 4
0
def test_filling_supplied_pits(dans_grid3):
    """
    Test the filler without rereouting, but confusingly, where there *is*
    aready routing information available!
    Also tests the supply of an array for 'pits'
    """
    dans_grid3.fr.run_one_step()

    lf = DepressionFinderAndRouter(
        dans_grid3.mg, reroute_flow=False, pits="flow__sink_flag"
    )
    lf.map_depressions()
    assert_array_equal(dans_grid3.mg.at_node["flow__receiver_node"], dans_grid3.r_old)
Esempio n. 5
0
def test_filling_alone(dans_grid3):
    """
    Test the filler alone, w/o supplying information on the pits.

    Setting the the *pits* parameter to None causes the mapper to look for pits
    using its _find_pits method.
    """
    lf = DepressionFinderAndRouter(dans_grid3.mg, reroute_flow=False, pits=None)
    assert lf._user_supplied_pits is None

    lf.map_depressions()
    assert_array_equal(
        dans_grid3.mg.at_node["flow__receiver_node"], XX * np.ones(49, dtype=int)
    )
    assert_array_equal(lf.depression_outlet_map, dans_grid3.depr_outlet_target)
Esempio n. 6
0
def test_lake_mapper():
    """
    Create a test grid and run a series of tests.
    """
    # Make a test grid
    rmg = create_test_grid()

    # Instantiate a lake mapper
    # (Note that we don't need to send it an input file name, because our grid
    # already has a topographic__elevation field)
    lm = DepressionFinderAndRouter(rmg)

    # Run it on our test grid
    lm.map_depressions()

    # Run tests
    check_fields1(rmg)
    check_array_values1(rmg, lm)
    check_fields2(rmg)
    check_array_values2(rmg, lm)
Esempio n. 7
0
def test_lake_mapper():
    """
    Create a test grid and run a series of tests.
    """
    # Make a test grid
    rmg = create_test_grid()

    # Instantiate a lake mapper
    # (Note that we don't need to send it an input file name, because our grid
    # already has a topographic__elevation field)
    lm = DepressionFinderAndRouter(rmg)

    # Run it on our test grid
    lm.map_depressions()

    # Run tests
    check_fields1(rmg)
    check_array_values1(rmg, lm)
    check_fields2(rmg)
    check_array_values2(rmg, lm)
class DrainageAreaModel(_ErosionModel):
    """
    A DrainageAreaModel simply computes drainage area on a raster-grid DEM.
    """
    def __init__(self, input_file=None, params=None):
        """Initialize the LinearDiffusionModel."""

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

        # Instantiate a FlowRouter and DepressionFinderAndRouter components
        self.flow_router = FlowRouter(self.grid, **self.params)
        self.lake_filler = DepressionFinderAndRouter(self.grid, **self.params)

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

        # for debug
        from landlab.io import write_esri_ascii
        import numpy

        write_esri_ascii('test_dr_area_before_temp.txt',
                         self.grid,
                         names='drainage_area',
                         clobber=True)
        loga = self.grid.add_zeros('node', 'loga')
        loga[:] = numpy.log10(self.grid.at_node['drainage_area'] + 1)
        write_esri_ascii('test_logdr_area_before_temp.txt',
                         self.grid,
                         names='loga',
                         clobber=True)
        write_esri_ascii('test_sink_flag_before_temp.txt',
                         self.grid,
                         names='flow__sink_flag',
                         clobber=True)

        self.lake_filler.map_depressions()
Esempio n. 9
0
class EffectiveDrainageAreaModel(_ErosionModel):
    """
    A DrainageAreaModel simply computes drainage area on a raster-grid DEM.
    """
    def __init__(self, input_file=None, params=None):
        """Initialize the LinearDiffusionModel."""

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

        # Instantiate a FlowRouter and DepressionFinderAndRouter components
        self.flow_router = FlowRouter(self.grid, **self.params)
        self.lake_filler = DepressionFinderAndRouter(self.grid, **self.params)

        # Add a field for effective drainage area
        self.eff_area = self.grid.add_zeros('node', 'effective_drainage_area')

        # Get the effective-area parameter
        self.sat_param = self.params['saturation_area_scale']

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

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

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

        # Lower outlet
        self.z[self.outlet_node] -= self.outlet_lowering_rate * dt
        print(('lowering outlet to ', self.z[self.outlet_node]))
Esempio n. 10
0
class StreamPowerThresholdModel(_ErosionModel):
    """
    A StreamPowerThresholdModel computes erosion using a form of the unit
    stream power model that represents a threshold using an exponential term.
    """
    def __init__(self, input_file=None, params=None):
        """Initialize the StreamPowerThresholdModel."""

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

        # Instantiate a FlowRouter and DepressionFinderAndRouter components
        self.flow_router = FlowRouter(self.grid, **self.params)
        self.lake_filler = DepressionFinderAndRouter(self.grid, **self.params)

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

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

        # Route flow
        self.flow_router.run_one_step()
        self.lake_filler.map_depressions()

        # Get IDs of flooded nodes, if any
        flooded = np.where(self.lake_filler.flood_status == 3)[0]

        # Do some erosion (but not on the flooded nodes)
        self.eroder.run_one_step(dt, flooded_nodes=flooded)
Esempio n. 11
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)
Esempio n. 12
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)
Esempio n. 13
0
class SinkFiller(Component):
    """
    This component identifies depressions in a topographic surface, then fills
    them in in the topography.  No attempt is made to conserve sediment mass.
    User may specify whether the holes should be filled to flat, or with a
    gradient downwards towards the depression outlet. The gradient can be
    spatially variable, and is chosen to not reverse any drainage directions
    at the perimeter of each lake.

    The primary method of this class is 'run_one_step'. 'fill_pits' is a
    synonym.

    Constructor assigns a copy of the grid, and calls the initialize
    method.

    Examples
    --------
    >>> from landlab import RasterModelGrid
    >>> from landlab import BAD_INDEX_VALUE as XX
    >>> from landlab.components import FlowAccumulator, SinkFiller
    >>> import numpy as np
    >>> lake1 = np.array([34, 35, 36, 44, 45, 46, 54, 55, 56, 65, 74])
    >>> lake2 = np.array([78, 87, 88])
    >>> guard_nodes = np.array([23, 33, 53, 63, 73, 83])
    >>> lake = np.concatenate((lake1, lake2))
    >>> mg = RasterModelGrid((10, 10), 1.)
    >>> z = np.ones(100, dtype=float)
    >>> z += mg.node_x  # add a slope
    >>> z[guard_nodes] += 0.001  # forces the flow out of a particular node
    >>> z[lake] = 0.
    >>> field = mg.add_field('node', 'topographic__elevation', z,
    ...                      units='-', copy=True)
    >>> fr = FlowAccumulator(mg, flow_director='D8')
    >>> fr.run_one_step()
    >>> mg.at_node['flow__sink_flag'][mg.core_nodes].sum()
    14
    >>> hf = SinkFiller(mg, apply_slope=False)
    >>> hf.run_one_step()
    >>> np.allclose(mg.at_node['topographic__elevation'][lake1], 4.)
    True
    >>> np.allclose(mg.at_node['topographic__elevation'][lake2], 7.)
    True

    Now reset and demonstrate the adding of an inclined surface:

    >>> field[:] = z
    >>> hf = SinkFiller(mg, apply_slope=True)
    >>> hf.run_one_step()
    >>> hole1 = np.array([4.00007692, 4.00015385, 4.00023077, 4.00030769,
    ...                   4.00038462, 4.00046154, 4.00053846, 4.00061538,
    ...                   4.00069231, 4.00076923, 4.00084615])
    >>> hole2 = np.array([7.4, 7.2, 7.6])
    >>> np.allclose(mg.at_node['topographic__elevation'][lake1], hole1)
    True
    >>> np.allclose(mg.at_node['topographic__elevation'][lake2], hole2)
    True
    >>> fr.run_one_step()
    >>> mg.at_node['flow__sink_flag'][mg.core_nodes].sum()
    0
    """
    _name = 'SinkFiller'

    _input_var_names = ('topographic__elevation',
                        )

    _output_var_names = ('topographic__elevation',
                         'sediment_fill__depth',
                         )

    _var_units = {'topographic__elevation': 'm',
                  'sediment_fill__depth': 'm',
                  }

    _var_mapping = {'topographic__elevation': 'node',
                    'sediment_fill__depth': 'node',
                    }

    _var_doc = {'topographic__elevation': 'Surface topographic elevation',
                'sediment_fill__depth': 'Depth of sediment added at each' +
                                        'node',
                }

    @use_file_name_or_kwds
    def __init__(self, grid, routing='D8', apply_slope=False,
                 fill_slope=1.e-5, **kwds):
        """
        Parameters
        ----------
        grid : ModelGrid
            A landlab grid.
        routing : {'D8', 'D4'} (optional)
            If grid is a raster type, controls whether fill connectivity can
            occur on diagonals ('D8', default), or only orthogonally ('D4').
            Has no effect if grid is not a raster.
        apply_slope : bool
            If False (default), leave the top of the filled sink flat. If True,
            apply the slope fill_slope to the top surface to allow subsequent flow
            routing. A test is performed to ensure applying this slope will not
            alter the drainage structure at the edge of the filled region
            (i.e., that we are not accidentally reversing the flow direction
            far from the outlet.)
        fill_slope : float (m/m)
            The slope added to the top surface of filled pits to allow flow
            routing across them, if apply_slope.
        """
        if 'flow__receiver_node' in grid.at_node:
            if (grid.at_node['flow__receiver_node'].size != grid.size('node')):
                msg = ('A route-to-multiple flow director has been '
                       'run on this grid. The landlab development team has not '
                       'verified that SinkFiller is compatible with '
                       'route-to-multiple methods. Please open a GitHub Issue '
                       'to start this process.')
                raise NotImplementedError(msg)

        self._grid = grid
        if routing is not 'D8':
            assert routing is 'D4'
        self._routing = routing
        if ((type(self._grid) is landlab.grid.raster.RasterModelGrid) and
                (routing is 'D8')):
            self._D8 = True
            self.num_nbrs = 8
        else:
            self._D8 = False  # useful shorthand for thia test we do a lot
            if type(self._grid) is landlab.grid.raster.RasterModelGrid:
                self.num_nbrs = 4
        self._fill_slope = fill_slope
        self._apply_slope = apply_slope
        self.initialize()

    def initialize(self, input_stream=None):
        """
        The BMI-style initialize method takes an optional input_stream
        parameter, which may be either a ModelParameterDictionary object or
        an input stream from which a ModelParameterDictionary can read values.
        """
        # Create a ModelParameterDictionary for the inputs
        if input_stream is None:
            inputs = None
        elif type(input_stream) == ModelParameterDictionary:
            inputs = input_stream
        else:
            inputs = ModelParameterDictionary(input_stream)

        # Make sure the grid includes elevation data. This means either:
        #  1. The grid has a node field called 'topographic__elevation', or
        #  2. The input file has an item called 'ELEVATION_FIELD_NAME' *and*
        #     a field by this name exists in the grid.
        try:
            self._elev = self._grid.at_node['topographic__elevation']
        except FieldError:
            try:
                self.topo_field_name = inputs.read_string('ELEVATION_' +
                                                          'FIELD_NAME')
            except AttributeError:
                print('Error: Because your grid does not have a node field')
                print('called "topographic__elevation", you need to pass the')
                print('name of a text input file or ModelParameterDictionary,')
                print('and this file or dictionary needs to include the name')
                print('of another field in your grid that contains your')
                print('elevation data.')
                raise AttributeError
            except MissingKeyError:
                print('Error: Because your grid does not have a node field')
                print('called "topographic__elevation", your input file (or')
                print('ModelParameterDictionary) must include an entry with')
                print('the key "ELEVATION_FIELD_NAME", which gives the name')
                print('of a field in your grid that contains your elevation')
                print('data.')
                raise MissingKeyError('ELEVATION_FIELD_NAME')
            try:
                self._elev = self._grid.at_node[self.topo_field_name]
            except AttributeError:
                print('Your grid does not seem to have a node field called',
                      self.topo_field_name)
        else:
            self.topo_field_name = 'topographic__elevation'
        # create the only new output field:
        self.sed_fill_depth = self._grid.add_zeros('node',
                                                   'sediment_fill__depth',
                                                   noclobber=False)

        self._lf = DepressionFinderAndRouter(self._grid, routing=self._routing)
        self._fr = FlowAccumulator(self._grid, flow_director=self._routing)

    def fill_pits(self, **kwds):
        """
        This is a synonym for the main method :func:`run_one_step`.
        """
        self.run_one_step(**kwds)

    def run_one_step(self, **kwds):
        """
        This is the main method. Call it to fill depressions in a starting
        topography.
        """
        # added for back-compatibility with old formats
        try:
            self._apply_slope = kwds['apply_slope']
        except KeyError:
            pass
        self.original_elev = self._elev.copy()
        # We need this, as we'll have to do ALL this again if we manage
        # to jack the elevs too high in one of the "subsidiary" lakes.
        # We're going to implement the lake_mapper component to do the heavy
        # lifting here, then delete its fields. This means we first need to
        # test if these fields already exist, in which case, we should *not*
        # delete them!
        existing_fields = {}
        spurious_fields = set()
        set_of_outputs = (set(self._lf.output_var_names) |
                          set(self._fr.output_var_names))
        try:
            set_of_outputs.remove(self.topo_field_name)
        except KeyError:
            pass
        for field in set_of_outputs:
            try:
                existing_fields[field] = self._grid.at_node[field].copy()
            except FieldError:  # not there; good!
                spurious_fields.add(field)

        self._fr.run_one_step()
        self._lf.map_depressions(pits=self._grid.at_node['flow__sink_flag'],
                                 reroute_flow=True)
        # add the depression depths to get up to flat:
        self._elev += self._grid.at_node['depression__depth']
        # if apply_slope is none, we're now done! But if not...
        if self._apply_slope:
            # new way of doing this - use the upstream structure! Should be
            # both more general and more efficient
            for (outlet_node, lake_code) in zip(self._lf.lake_outlets,
                                                self._lf.lake_codes):
                lake_nodes = np.where(self._lf.lake_map == lake_code)[0]
                lake_perim = self._get_lake_ext_margin(lake_nodes)
                perim_elevs = self._elev[lake_perim]
                out_elev = self._elev[outlet_node]
                lowest_elev_perim = perim_elevs[perim_elevs != out_elev].min()
                # note we exclude the outlet node
                elev_increment = ((lowest_elev_perim-self._elev[outlet_node]) /
                                  (lake_nodes.size + 2.))
                assert elev_increment > 0.
                all_ordering = self._grid.at_node['flow__upstream_node_order']
                upstream_order_bool = np.in1d(all_ordering, lake_nodes,
                                              assume_unique=True)
                lake_upstream_order = all_ordering[upstream_order_bool]
                argsort_lake = np.argsort(lake_upstream_order)
                elevs_to_add = (np.arange(lake_nodes.size, dtype=float) +
                                1.) * elev_increment
                sorted_elevs_to_add = elevs_to_add[argsort_lake]
                self._elev[lake_nodes] += sorted_elevs_to_add
        # now put back any fields that were present initially, and wipe the
        # rest:
        for delete_me in spurious_fields:
            if delete_me in self._grid.at_node:
                self._grid.delete_field('node', delete_me)
        for update_me in existing_fields.keys():
            self.grid.at_node[update_me][:] = existing_fields[update_me]
        # fill the output field
        self.sed_fill_depth[:] = self._elev - self.original_elev

    @deprecated(use='fill_pits', version=1.0)
    def _fill_pits_old(self, apply_slope=None):
        """

        .. deprecated:: 0.1.38
            Use :func:`fill_pits` instead.

        This is the main method. Call it to fill depressions in a starting
        topography.

        **Output fields**

        *  `topographic__elevation` : the updated elevations
        *  `sediment_fill__depth` : the depth of sediment added at each node

        Parameters
        ----------
        apply_slope : None, bool, or float
            If a float is provided this is the slope of the surface down
            towards the lake outlet. Supply a small positive number, e.g.,
            1.e-5 (or True, to use this default value).
            A test is performed to ensure applying this slope will not alter
            the drainage structure at the edge of the filled region (i.e.,
            that we are not accidentally reversing the flow direction far
            from the outlet.) The component will automatically decrease the
            (supplied or default) gradient a number of times to try to
            accommodate this, but will eventually raise an OverflowError
            if it can't deal with it. If you pass True, the method will use
            the default value of 1.e-5.
        """
        self.original_elev = self._elev.copy()
        # We need this, as we'll have to do ALL this again if we manage
        # to jack the elevs too high in one of the "subsidiary" lakes.
        # We're going to implement the lake_mapper component to do the heavy
        # lifting here, then delete its fields. This means we first need to
        # test if these fields already exist, in which case, we should *not*
        # delete them!
        existing_fields = {}
        spurious_fields = set()
        set_of_outputs = self._lf.output_var_names | self._fr.output_var_names
        try:
            set_of_outputs.remove(self.topo_field_name)
        except KeyError:
            pass
        for field in set_of_outputs:
            try:
                existing_fields[field] = self._grid.at_node[field].copy()
            except FieldError:  # not there; good!
                spurious_fields.add(field)

        self._fr.run_one_step()
        self._lf.map_depressions(pits=self._grid.at_node['flow__sink_flag'],
                                 reroute_flow=False)
        # add the depression depths to get up to flat:
        self._elev += self._grid.at_node['depression__depth']
        # if apply_slope is none, we're now done! But if not...
        if apply_slope is True:
            apply_slope = self._fill_slope
        elif type(apply_slope) in (float, int):
            assert apply_slope >= 0.
        if apply_slope:
            # this isn't very efficient, but OK as we're only running this
            # code ONCE in almost all use cases
            sublake = False
            unstable = True
            stability_increment = 0
            self.lake_nodes_treated = np.array([], dtype=int)
            while unstable:
                while 1:
                    for (outlet_node, lake_code) in zip(self._lf.lake_outlets,
                                                        self._lf.lake_codes):
                        self._apply_slope_current_lake(apply_slope,
                                                       outlet_node,
                                                       lake_code, sublake)
                    # Call the mapper again here. Bail out if no core pits are
                    # found.
                    # This is necessary as there are some configs where adding
                    # the slope could create subsidiary pits in the topo
                    self._lf.map_depressions(pits=None, reroute_flow=False)
                    if len(self._lf.lake_outlets) == 0.:
                        break
                    self._elev += self._grid.at_node['depression__depth']
                    sublake = True
                    self.lake_nodes_treated = np.array([], dtype=int)
                # final test that all lakes are not reversing flow dirs
                all_lakes = np.where(self._lf.flood_status <
                                     BAD_INDEX_VALUE)[0]
                unstable = self.drainage_directions_change(all_lakes,
                                                           self.original_elev,
                                                           self._elev)
                if unstable:
                    apply_slope *= 0.1
                    sublake = False
                    self.lake_nodes_treated = np.array([], dtype=int)
                    self._elev[:] = original_elev  # put back init conds
                    stability_increment += 1
                    if stability_increment == 10:
                        raise OverflowError('Filler could not find a stable ' +
                                            'condition with a sloping ' +
                                            'surface!')
        # now put back any fields that were present initially, and wipe the
        # rest:
        for delete_me in spurious_fields:
            if delete_me in self.grid.at_node:
                self._grid.delete_field('node', delete_me)
        for update_me in existing_fields.keys():
            self.grid.at_node[update_me] = existing_fields[update_me]
        # fill the output field
        self.sed_fill_depth[:] = self._elev - self.original_elev

    def _add_slopes(self, slope, outlet_node, lake_code):
        """
        Assuming you have already run the lake_mapper, adds an incline towards
        the outlet to the nodes in the lake.
        """
        new_elevs = self._elev.copy()
        outlet_coord = (self._grid.node_x[outlet_node],
                        self._grid.node_y[outlet_node])
        lake_nodes = np.where(self._lf.lake_map == lake_code)[0]
        lake_nodes = np.setdiff1d(lake_nodes, self.lake_nodes_treated)
        lake_ext_margin = self._get_lake_ext_margin(lake_nodes)
        d = self._grid.calc_distances_of_nodes_to_point(outlet_coord,
                                                        node_subset=lake_nodes)
        add_vals = slope*d
        new_elevs[lake_nodes] += add_vals
        self.lake_nodes_treated = np.union1d(self.lake_nodes_treated,
                                             lake_nodes)
        return new_elevs, lake_nodes

    def _get_lake_ext_margin(self, lake_nodes):
        """
        Returns the nodes forming the external margin of the lake, honoring
        the *routing* method (D4/D8) if applicable.
        """
        if self._D8 is True:
            all_poss = np.union1d(
                self.grid.active_adjacent_nodes_at_node[lake_nodes],
                self.grid.diagonal_adjacent_nodes_at_node[lake_nodes])
        else:
            all_poss = np.unique(self.grid.active_adjacent_nodes_at_node[
                lake_nodes])
        lake_ext_edge = np.setdiff1d(all_poss, lake_nodes)
        return lake_ext_edge[lake_ext_edge != BAD_INDEX_VALUE]

    def _get_lake_int_margin(self, lake_nodes, lake_ext_edge):
        """
        Returns the nodes forming the internal margin of the lake, honoring
        the *routing* method (D4/D8) if applicable.
        """
        lee = lake_ext_edge
        if self._D8 is True:
            all_poss_int = np.union1d(
                self._grid.active_adjacent_nodes_at_node[lee],
                self._grid.diagonal_adjacent_nodes_at_node[lee])
        else:
            all_poss_int = np.unique(self._grid.active_adjacent_nodes_at_node[lee])
        lake_int_edge = np.intersect1d(all_poss_int, lake_nodes)
        return lake_int_edge[lake_int_edge != BAD_INDEX_VALUE]

    def _apply_slope_current_lake(self, apply_slope, outlet_node, lake_code,
                                 sublake):
        """
        Wraps the _add_slopes method to allow handling of conditions where the
        drainage structure would be changed or we're dealing with a sublake.
        """
        while 1:
            starting_elevs = self._elev.copy()
            self._elev[:], lake_nodes = self._add_slopes(apply_slope,
                                                         outlet_node,
                                                         lake_code)
            ext_edge = self._get_lake_ext_margin(lake_nodes)
            if sublake:
                break
            else:
                if not self.drainage_directions_change(lake_nodes,
                                                       starting_elevs,
                                                       self._elev):
                    break
                else:
                    # put the elevs back...
                    self._elev[lake_nodes] = starting_elevs[lake_nodes]
                    # the slope was too big. Reduce it.
                    apply_slope *= 0.1
        # if we get here, either sublake, or drainage dirs are stable

    def drainage_directions_change(self, lake_nodes, old_elevs, new_elevs):
        """
        True if the drainage structure at lake margin changes, False otherwise.
        """
        ext_edge = self._get_lake_ext_margin(lake_nodes)
        if self._D8:
            edge_neighbors = np.hstack(
                (self.grid.active_adjacent_nodes_at_node[ext_edge],
                 self.grid.diagonal_adjacent_nodes_at_node[ext_edge]))
        else:
            edge_neighbors = self.grid.active_adjacent_nodes_at_node[
                ext_edge].copy()
        edge_neighbors[edge_neighbors == BAD_INDEX_VALUE] = -1
        # ^value irrelevant
        old_neighbor_elevs = old_elevs[edge_neighbors]
        new_neighbor_elevs = new_elevs[edge_neighbors]
        # enforce the "don't change drainage direction" condition:
        edge_elevs = old_elevs[ext_edge].reshape((ext_edge.size, 1))
        cond = np.allclose((edge_elevs >= old_neighbor_elevs),
                           (edge_elevs >= new_neighbor_elevs))
        # if True, we're good, the tilting didn't mess with the fr
        return not cond
Esempio n. 14
0
#Create boundary conditions of the model grid (either closed or fixed-head)
for edge in (mg.nodes_at_left_edge,mg.nodes_at_right_edge, mg.nodes_at_top_edge):
    mg.status_at_node[edge] = CLOSED_BOUNDARY
for edge in (mg.nodes_at_bottom_edge):
    mg.status_at_node[edge] = FIXED_VALUE_BOUNDARY

#Initialize Fastscape
fc = FastscapeEroder(mg,
                    K_sp = ksp ,
                    m_sp = msp,
                    n_sp = nsp,
                    rainfall_intensity = 1)
fr = FlowRouter(mg)
lm = DepressionFinderAndRouter(mg)

for i in range(nSteps):
    fr.run_one_step(dt=1)
    lm.map_depressions()
    fc.run_one_step(dt=1)
    mg.at_node['topographic__elevation'][mg.core_nodes] += 0.0002

z = mg.at_node['topographic__elevation']

plt.figure()
imshow_grid(mg,z)
plt.savefig('test.png')
plt.close()

np.save('iniTopo',z)
Esempio n. 15
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), (1., 1.))

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

    A_new = np.array(
        [
            [
                [
                    0.,
                    1.,
                    1.,
                    1.,
                    1.,
                    1.,
                    0.,
                    0.,
                    1.,
                    1.,
                    1.,
                    1.,
                    1.,
                    0.,
                    15.,
                    5.,
                    4.,
                    3.,
                    2.,
                    1.,
                    0.,
                    0.,
                    10.,
                    4.,
                    3.,
                    2.,
                    1.,
                    0.,
                    0.,
                    1.,
                    4.,
                    3.,
                    2.,
                    1.,
                    0.,
                    0.,
                    1.,
                    1.,
                    1.,
                    1.,
                    1.,
                    0.,
                    0.,
                    1.,
                    1.,
                    1.,
                    1.,
                    1.,
                    0.,
                ]
            ]
        ]
    ).flatten()

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

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

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

    fr.run_one_step()
    lf.map_depressions()
    assert mg.at_node["drainage_area"] == approx(A_new)
    assert lf.depression_outlet_map == approx(depr_outlet_target)
class StochasticDischargeHortonianModel(_ErosionModel):
    """
    A StochasticDischargeHortonianModel 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):
        """Initialize the StochasticDischargeHortonianModel."""

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

        # Instantiate components
        self.flow_router = FlowRouter(self.grid, **self.params)

        self.lake_filler = DepressionFinderAndRouter(self.grid, **self.params)

        self.rain_generator = \
            PrecipitationDistribution(delta_t=self.params['dt'],
                                      total_time=self.params['run_duration'],
                                      **self.params)

        # Add a field for discharge
        if 'surface_water__discharge' not in self.grid.at_node:
            self.grid.add_zeros('node', 'surface_water__discharge')
        self.discharge = self.grid.at_node['surface_water__discharge']                                    

        # Get the infiltration-capacity parameter
        self.infilt = self.params['infiltration_capacity']

        # Run flow routing and lake filler (only once, because we are not
        # not changing topography)
        self.flow_router.run_one_step()
        self.lake_filler.map_depressions()


    def reset_random_seed(self):
        """Re-set the random number generation sequence."""
        try:
            seed = self.params['random_seed']
        except KeyError:
            seed = 0
        self.rain_generator.seed_generator(seedval=seed)


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

        # Calculate discharge field
        area = self.grid.at_node['drainage_area']
        if 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


    def run_for(self, dt, runtime):
        """
        Run model without interruption for a specified time period.
        """
        self.rain_generator.delta_t = dt
        self.rain_generator.run_time = runtime
        for (tr, p) in self.rain_generator.yield_storm_interstorm_duration_intensity(): 
            self.rain_rate = p
            self.run_one_step(tr)


    def write_storm_sequence_to_file(self, filename=None):
        """
        Write event duration and intensity to a formatted text file.
        """

        # Re-seed the random number generator, so we get the same sequence.
        self.reset_random_seed()
        
        # Generate a set of event parameters. This is needed because the
        # PrecipitationDistribution component automatically generates a
        # parameter set on initialization. Therefore, to get to the same
        # starting point that we used in the sequence-through-time, we need
        # to regenerate these.
        self.rain_generator.get_precipitation_event_duration()
        self.rain_generator.get_interstorm_event_duration()
        self.rain_generator.get_storm_depth()
        self.rain_generator.get_storm_intensity()
        
        # Open a file for writing
        if filename is None:
            filename = 'event_sequence.txt'
        stormfile = open(filename, 'w')

        # Set the generator's time step and run time to the full duration of
        # the run. This ensures that we get a sequence that is as long as the
        # model run, and does not split events by time step (unlike the actual
        # model run)
        self.rain_generator.delta_t = self.params['run_duration']
        self.rain_generator.run_time = self.params['run_duration']
        tt = 0.0
        for (tr, p) in self.rain_generator.yield_storm_interstorm_duration_intensity():        
            runoff = p - self.infilt * (1.0 - np.exp(-p / self.infilt))
            stormfile.write(str(tt) + ',' + str(p) + ',' + str(runoff) + '\n')
            tt += tr
        stormfile.write(str(tt) + ',' + str(p) + ',' + str(runoff) + '\n')

        # Close the file
        stormfile.close()
Esempio n. 17
0
class StreamPowerVarThresholdModel(_ErosionModel):
    """
    A StreamPowerVarThresholdModel computes erosion using a form of the unit
    stream power model that represents a threshold using an exponential term.
    The threshold value itself depends on incision depth below an initial
    reference surface. This is meant to mimic coarsening of sediment in the
    channel with progressive incision, similar to the model of 
    Gran et al. (2013)
    """
    
    def __init__(self, input_file=None, params=None):
        """Initialize the StreamPowerVarThresholdModel."""

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

        # Instantiate a FlowRouter and DepressionFinderAndRouter components
        self.flow_router = FlowRouter(self.grid, **self.params)
        self.lake_filler = DepressionFinderAndRouter(self.grid, **self.params)
        
        # Create a field for the (initial) erosion threshold
        self.threshold = self.grid.add_zeros('node', 'erosion__threshold')
        self.threshold[:] = self.params['threshold_sp']
        
        # Instantiate a FastscapeEroder component
        self.eroder = StreamPowerSmoothThresholdEroder(
            self.grid,
            K_sp=self.params['K_sp'],
            threshold_sp=self.threshold)

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


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

        # Route flow
        self.flow_router.run_one_step()
        self.lake_filler.map_depressions()

        # Get IDs of flooded nodes, if any
        flooded = np.where(self.lake_filler.flood_status==3)[0]

        # Set the erosion threshold.
        #
        # Note that a minus sign is used because cum ero depth is negative for
        # erosion, positive for deposition.
        # The second line handles the case where there is growth, in which case
        # we want the threshold to stay at its initial value rather than 
        # getting smaller.
        cum_ero = self.grid.at_node['cumulative_erosion__depth']
        cum_ero[:] = (self.z
                      - self.grid.at_node['initial_topographic__elevation'])
        self.threshold[:] = (self.params['threshold_sp']
                             - (self.thresh_change_per_depth
                                * cum_ero))
        self.threshold[self.threshold < self.params['threshold_sp']] = \
            self.params['threshold_sp']

        # Do some erosion (but not on the flooded nodes)
        self.eroder.run_one_step(dt, flooded_nodes=flooded)
spr = StreamPowerEroder(mg, K_sp=K_sp, m_sp=m_sp, n_sp=n_sp, threshold_sp=0,
                        use_Q=None)
lake = DepressionFinderAndRouter(mg)
    
# Hillslopes
dfn = LinearDiffuser(mg, linear_diffusivity=K_hs)

zr_last = -9999
keep_running = np.mean(np.abs(zr - zr_last)) >= end_thresh
ti = 0
while keep_running:
    zr_last = zr.copy()
    zr[mg.core_nodes] += uplift_rate*dt
    dfn.run_one_step(dt) # hillslopes always diffusive, even when dry
    frr.run_one_step()
    lake.map_depressions()
    spr.run_one_step(dt, flooded_nodes=lake.lake_at_node)
    keep_running = np.mean(np.abs(zr - zr_last)) >= end_thresh
    ti += dt
    print ti/1000., 'kyr elapsed; ', np.mean(zr-zr_last) / dt * 1E6, \
          'um/yr surface uplift'
print "Convergence reached! Landscape is at steady state."

A = mg.at_node['drainage_area']#[not_edge]
A = A.reshape(ncells_side, ncells_side)
S = mg.at_node['topographic__steepest_slope']
S = S.reshape(ncells_side, ncells_side)

np.savetxt('Synthetic_data/z.txt', zr, fmt='%.2f')
np.savetxt('Synthetic_data/A.txt', A, fmt='%d')
np.savetxt('Synthetic_data/S.txt', S, fmt='%.5f')
Esempio n. 19
0
    # Now that we have performed the tectonic deformation, lets apply our
    # landscape evolution and watch the landscape change as a result.

    # Uplift the landscape
    rmg['node']['topographic__elevation'][rmg.core_nodes] += uplift_rate * dt

    # set the lower boundary as fixed elevation
    rmg['node']['topographic__elevation'][rmg.node_y == 0] = 0

    # Diffuse the landscape simulating hillslope sediment transport
    lin_diffuse.run_one_step(dt)

    # Accumulate and route flow, fill any lakes, and erode under the rivers
    fr.run_one_step()  # route flow
    DepressionFinderAndRouter.map_depressions(fill)  # fill lakes
    sp.run_one_step(dt)  # fastscape stream power eroder

    ## Calculate the geomorphic metric ##

    # In the paper, we use a geomorphic metric, BR, to quantify the
    # reorientation of the channels as time goes on. The code to calculate this
    # value is below but turned off as it can slow the model. Set the
    # 'calculate_BR' variable to 'True' if you want to calculate it.

    if calculate_BR:

        aspects = rmg.calc_aspect_at_node()  # measure pixel aspect

        # classify and count the number of pixels with certain directions
        asp_0_45 = float(np.logical_and(aspects >= 0, aspects <= 45).sum())
Esempio n. 20
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, 1.)
    z = mg.add_field("node", "topographic__elevation", mg.node_x.copy())
    # a sloping plane
    # np.random.seed(seed=0)
    # z += np.random.rand(100)/10000.
    # punch one big hole
    z.reshape((10, 10))[3:8, 3:8] = 0.
    # dig a couple of inset holes
    z[57] = -1.
    z[44] = -2.
    z[54] = -10.

    # make an outlet
    z[71] = 0.9

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

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

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

    # test the actual flow field:
    #    nA = np.array([  0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,
    #                     8.,   8.,   7.,   6.,   5.,   4.,   3.,   2.,   1.,   0.,
    #                     1.,   1.,   1.,   1.,   1.,   1.,   1.,   1.,   1.,   0.,
    #                     1.,   1.,   1.,   4.,   2.,   2.,   8.,   4.,   1.,   0.,
    #                     1.,   1.,   1.,   8.,   3.,  15.,   3.,   2.,   1.,   0.,
    #                     1.,   1.,   1.,  13.,  25.,   6.,   3.,   2.,   1.,   0.,
    #                     1.,   1.,   1.,  45.,   3.,   3.,   5.,   2.,   1.,   0.,
    #                    50.,  50.,  49.,   3.,   2.,   2.,   2.,   4.,   1.,   0.,
    #                     1.,   1.,   1.,   1.,   1.,   1.,   1.,   1.,   1.,   0.,
    #                     0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.,   0.])
    nA = np.array(
        [
            0.,
            0.,
            0.,
            0.,
            0.,
            0.,
            0.,
            0.,
            0.,
            0.,
            8.,
            8.,
            7.,
            6.,
            5.,
            4.,
            3.,
            2.,
            1.,
            0.,
            1.,
            1.,
            1.,
            1.,
            1.,
            1.,
            1.,
            1.,
            1.,
            0.,
            1.,
            1.,
            1.,
            4.,
            2.,
            2.,
            6.,
            4.,
            1.,
            0.,
            1.,
            1.,
            1.,
            6.,
            3.,
            12.,
            3.,
            2.,
            1.,
            0.,
            1.,
            1.,
            1.,
            8.,
            20.,
            4.,
            3.,
            2.,
            1.,
            0.,
            1.,
            1.,
            1.,
            35.,
            5.,
            4.,
            3.,
            2.,
            1.,
            0.,
            50.,
            50.,
            49.,
            13.,
            10.,
            8.,
            6.,
            4.,
            1.,
            0.,
            1.,
            1.,
            1.,
            1.,
            1.,
            1.,
            1.,
            1.,
            1.,
            0.,
            0.,
            0.,
            0.,
            0.,
            0.,
            0.,
            0.,
            0.,
            0.,
            0.,
        ]
    )
    assert_array_equal(mg.at_node["drainage_area"], nA)

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

    # test the remaining properties:
    assert lf.lake_outlets.size == 1
    assert lf.lake_outlets[0] == 72
    outlets_in_map = np.unique(lf.depression_outlet_map)
    assert outlets_in_map.size == 2
    assert outlets_in_map[1] == 72
    assert lf.number_of_lakes == 1
    assert lf.lake_codes[0] == 57
    assert_array_equal(lf.lake_map, lc)
    assert lf.lake_areas[0] == approx(25.)
    assert lf.lake_volumes[0] == approx(63.)
Esempio n. 21
0
def test_three_pits():
    """
    A test to ensure the component correctly handles cases where there are
    multiple pits.
    """
    mg = RasterModelGrid(10, 10, 1.)
    z = mg.add_field("node", "topographic__elevation", mg.node_x.copy())
    # a sloping plane
    # np.random.seed(seed=0)
    # z += np.random.rand(100)/10000.
    # punch some holes
    z[33] = 1.
    z[43] = 1.
    z[37] = 4.
    z[74:76] = 1.
    fr = FlowAccumulator(mg, flow_director='D8')
    lf = DepressionFinderAndRouter(mg)
    fr.run_one_step()
    lf.map_depressions()

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

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

    # test the actual flow field:
    nA = np.array(
        [
            0.,
            0.,
            0.,
            0.,
            0.,
            0.,
            0.,
            0.,
            0.,
            0.,
            8.,
            8.,
            7.,
            6.,
            5.,
            4.,
            3.,
            2.,
            1.,
            0.,
            2.,
            2.,
            1.,
            1.,
            2.,
            1.,
            1.,
            1.,
            1.,
            0.,
            26.,
            26.,
            25.,
            15.,
            11.,
            10.,
            9.,
            8.,
            1.,
            0.,
            2.,
            2.,
            1.,
            9.,
            2.,
            1.,
            1.,
            1.,
            1.,
            0.,
            2.,
            2.,
            1.,
            1.,
            5.,
            4.,
            3.,
            2.,
            1.,
            0.,
            2.,
            2.,
            1.,
            1.,
            1.,
            1.,
            3.,
            2.,
            1.,
            0.,
            20.,
            20.,
            19.,
            18.,
            17.,
            12.,
            3.,
            2.,
            1.,
            0.,
            2.,
            2.,
            1.,
            1.,
            1.,
            1.,
            3.,
            2.,
            1.,
            0.,
            0.,
            0.,
            0.,
            0.,
            0.,
            0.,
            0.,
            0.,
            0.,
            0.,
        ]
    )
    assert_array_equal(mg.at_node["drainage_area"], nA)

    # test a couple more properties:
    lc = np.empty(100, dtype=int)
    lc.fill(XX)
    lc[33] = 33
    lc[43] = 33
    lc[37] = 37
    lc[74:76] = 74
    assert_array_equal(lf.lake_map, lc)
    assert_array_equal(lf.lake_codes, [33, 37, 74])
    assert lf.number_of_lakes == 3
    assert lf.lake_areas == approx([2., 1., 2.])
    assert lf.lake_volumes == approx([2., 2., 4.])
Esempio n. 22
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.
    lake_pits = np.array([7, 11, 12, 13, 17, 27, 31, 32, 33, 37])
    z_init[lake_pits] = -1.
    z_init[22] = 0.  # the common spill pt for both lakes
    z_init[21] = 0.1  # an adverse bump in the spillway
    z_init[20] = -0.2  # the spillway
    z = mg.add_field("node", "topographic__elevation", z_init)

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

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

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

    thelake = np.concatenate((lake_pits, [22])).sort()

    assert mg.at_node["drainage_area"] == approx(correct_A)
Esempio n. 23
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])
Esempio n. 24
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)
Esempio n. 25
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.
    lake_pits = np.array([7, 11, 12, 13, 17, 27, 31, 32, 33, 37])
    z_init[lake_pits] = -1.
    z_init[22] = 0.  # the common spill pt for both lakes
    z_init[21] = 0.1  # an adverse bump in the spillway
    z_init[20] = -0.2  # the spillway
    z = mg.add_field("node", "topographic__elevation", z_init)

    fr = FlowRouter(mg)
    lf = DepressionFinderAndRouter(mg)
    fr.route_flow()
    lf.map_depressions()

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

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

    thelake = np.concatenate((lake_pits, [22])).sort()

    assert mg.at_node["drainage_area"] == approx(correct_A)
class HybridAlluviumModel(_ErosionModel):
    """
    A HybridAlluviumModel computes erosion of sediment and bedrock
    using dual mass conservation on the bed and in the water column. It
    applies exponential entrainment rules to account for bed cover.
    """
    def __init__(self, input_file=None, params=None):
        """Initialize the HybridAlluviumModel."""

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

        # Instantiate a FlowRouter and DepressionFinderAndRouter components
        self.flow_router = FlowRouter(self.grid, **self.params)
        self.lake_filler = DepressionFinderAndRouter(self.grid, **self.params)

        #make area_field and/or discharge_field depending on discharge_method
        if self.params['discharge_method'] is not None:
            if self.params['discharge_method'] == 'area_field':
                area_field = self.grid.at_node['drainage_area']
                discharge_field = None
            elif self.params['discharge_method'] == 'discharge_field':
                discharge_field = self.grid.at_node['surface_water__discharge']
                area_field = None
            else:
                raise (KeyError)
        else:
            area_field = None
            discharge_field = None

        # Instantiate a HybridAlluvium component
        self.eroder = Space(self.grid,
                            K_sed=self.params['K_sed'],
                            K_br=self.params['K_br'],
                            F_f=self.params['F_f'],
                            phi=self.params['phi'],
                            H_star=self.params['H_star'],
                            v_s=self.params['v_s'],
                            m_sp=self.params['m_sp'],
                            n_sp=self.params['n_sp'],
                            sp_crit_sed=self.params['sp_crit_sed'],
                            sp_crit_br=self.params['sp_crit_br'],
                            method=self.params['method'],
                            discharge_method=self.params['discharge_method'],
                            area_field=area_field,
                            discharge_field=discharge_field)

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

        # Route flow
        self.flow_router.run_one_step()
        self.lake_filler.map_depressions()

        # Get IDs of flooded nodes, if any
        flooded = np.where(self.lake_filler.flood_status == 3)[0]

        # Do some erosion (but not on the flooded nodes)
        self.eroder.run_one_step(dt, flooded_nodes=flooded)
Esempio n. 27
0
class SinkFiller(Component):
    """This component identifies depressions in a topographic surface, then
    fills them in in the topography.  No attempt is made to conserve sediment
    mass. User may specify whether the holes should be filled to flat, or with
    a gradient downwards towards the depression outlet. The gradient can be
    spatially variable, and is chosen to not reverse any drainage directions at
    the perimeter of each lake.

    The primary method of this class is 'run_one_step'. 'fill_pits' is a
    synonym.

    Constructor assigns a copy of the grid, and calls the initialize
    method.

    Examples
    --------
    >>> from landlab import RasterModelGrid
    >>> from landlab.components import FlowAccumulator, SinkFiller
    >>> import numpy as np
    >>> lake1 = np.array([34, 35, 36, 44, 45, 46, 54, 55, 56, 65, 74])
    >>> lake2 = np.array([78, 87, 88])
    >>> guard_nodes = np.array([23, 33, 53, 63, 73, 83])
    >>> lake = np.concatenate((lake1, lake2))
    >>> mg = RasterModelGrid((10, 10))
    >>> z = np.ones(100, dtype=float)
    >>> z += mg.node_x  # add a slope
    >>> z[guard_nodes] += 0.001  # forces the flow out of a particular node
    >>> z[lake] = 0.
    >>> field = mg.add_field(
    ...     "topographic__elevation",
    ...     z,
    ...     at="node",
    ...     units="-",
    ...     copy=True,
    ... )
    >>> fr = FlowAccumulator(mg, flow_director='D8')
    >>> fr.run_one_step()
    >>> mg.at_node['flow__sink_flag'][mg.core_nodes].sum()
    14
    >>> hf = SinkFiller(mg, apply_slope=False)
    >>> hf.run_one_step()
    >>> np.allclose(mg.at_node['topographic__elevation'][lake1], 4.)
    True
    >>> np.allclose(mg.at_node['topographic__elevation'][lake2], 7.)
    True

    Now reset and demonstrate the adding of an inclined surface:

    >>> field[:] = z
    >>> hf = SinkFiller(mg, apply_slope=True)
    >>> hf.run_one_step()
    >>> hole1 = np.array([4.00007692, 4.00015385, 4.00023077, 4.00030769,
    ...                   4.00038462, 4.00046154, 4.00053846, 4.00061538,
    ...                   4.00069231, 4.00076923, 4.00084615])
    >>> hole2 = np.array([7.4, 7.2, 7.6])
    >>> np.allclose(mg.at_node['topographic__elevation'][lake1], hole1)
    True
    >>> np.allclose(mg.at_node['topographic__elevation'][lake2], hole2)
    True
    >>> fr.run_one_step()
    >>> mg.at_node['flow__sink_flag'][mg.core_nodes].sum()
    0

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

    None Listed

    **Additional References**

    Tucker, G., Lancaster, S., Gasparini, N., Bras, R., Rybarczyk, S. (2001).
    An object-oriented framework for distributed hydrologic and geomorphic
    modeling using triangulated irregular networks. Computers & Geosciences
    27(8), 959-973. https://dx.doi.org/10.1016/s0098-3004(00)00134-5

    """

    _name = "SinkFiller"

    _unit_agnostic = True

    _info = {
        "sediment_fill__depth": {
            "dtype": float,
            "intent": "out",
            "optional": False,
            "units": "m",
            "mapping": "node",
            "doc": "Depth of sediment added at eachnode",
        },
        "topographic__elevation": {
            "dtype": float,
            "intent": "inout",
            "optional": False,
            "units": "m",
            "mapping": "node",
            "doc": "Land surface topographic elevation",
        },
    }

    def __init__(self,
                 grid,
                 routing="D8",
                 apply_slope=False,
                 fill_slope=1.0e-5):
        """
        Parameters
        ----------
        grid : ModelGrid
            A landlab grid.
        routing : {'D8', 'D4'} (optional)
            If grid is a raster type, controls whether fill connectivity can
            occur on diagonals ('D8', default), or only orthogonally ('D4').
            Has no effect if grid is not a raster.
        apply_slope : bool
            If False (default), leave the top of the filled sink flat. If True,
            apply the slope fill_slope to the top surface to allow subsequent flow
            routing. A test is performed to ensure applying this slope will not
            alter the drainage structure at the edge of the filled region
            (i.e., that we are not accidentally reversing the flow direction
            far from the outlet.)
        fill_slope : float (m/m)
            The slope added to the top surface of filled pits to allow flow
            routing across them, if apply_slope.
        """
        super().__init__(grid)

        if "flow__receiver_node" in grid.at_node:
            if grid.at_node["flow__receiver_node"].size != grid.size("node"):
                msg = (
                    "A route-to-multiple flow director has been "
                    "run on this grid. The landlab development team has not "
                    "verified that SinkFiller is compatible with "
                    "route-to-multiple methods. Please open a GitHub Issue "
                    "to start this process.")
                raise NotImplementedError(msg)

        if routing != "D8":
            assert routing == "D4"
        self._routing = routing
        if isinstance(self._grid, RasterModelGrid) and (routing == "D8"):
            self._D8 = True
            self._num_nbrs = 8
        else:
            self._D8 = False  # useful shorthand for thia test we do a lot
            if isinstance(self._grid, RasterModelGrid):
                self._num_nbrs = 4
        self._fill_slope = fill_slope
        self._apply_slope = apply_slope

        self._elev = self._grid.at_node["topographic__elevation"]
        self._topo_field_name = "topographic__elevation"

        # create the only new output field:
        self._sed_fill_depth = self._grid.add_zeros("node",
                                                    "sediment_fill__depth",
                                                    clobber=True)

        self._lf = DepressionFinderAndRouter(self._grid,
                                             routing=self._routing,
                                             reroute_flow=True)
        self._fr = FlowAccumulator(self._grid, flow_director=self._routing)

    def fill_pits(self):
        """This is a synonym for the main method :func:`run_one_step`."""
        self.run_one_step()

    def run_one_step(self):
        """This is the main method.

        Call it to fill depressions in a starting topography.
        """
        self._original_elev = self._elev.copy()
        # We need this, as we'll have to do ALL this again if we manage
        # to jack the elevs too high in one of the "subsidiary" lakes.
        # We're going to implement the lake_mapper component to do the heavy
        # lifting here, then delete its fields. This means we first need to
        # test if these fields already exist, in which case, we should *not*
        # delete them!
        existing_fields = {}
        spurious_fields = set()
        set_of_outputs = set(self._lf.output_var_names) | set(
            self._fr.output_var_names)
        try:
            set_of_outputs.remove(self._topo_field_name)
        except KeyError:
            pass
        for field in set_of_outputs:
            try:
                existing_fields[field] = self._grid.at_node[field].copy()
            except FieldError:  # not there; good!
                spurious_fields.add(field)

        self._fr.run_one_step()
        self._lf.map_depressions()
        # add the depression depths to get up to flat:
        self._elev += self._grid.at_node["depression__depth"]
        # if apply_slope is none, we're now done! But if not...
        if self._apply_slope:
            # new way of doing this - use the upstream structure! Should be
            # both more general and more efficient
            for (outlet_node, lake_code) in zip(self._lf.lake_outlets,
                                                self._lf.lake_codes):
                lake_nodes = np.where(self._lf.lake_map == lake_code)[0]
                lake_perim = self._get_lake_ext_margin(lake_nodes)
                perim_elevs = self._elev[lake_perim]
                out_elev = self._elev[outlet_node]
                lowest_elev_perim = perim_elevs[perim_elevs != out_elev].min()
                # note we exclude the outlet node
                elev_increment = (lowest_elev_perim - self._elev[outlet_node]
                                  ) / (lake_nodes.size + 2.0)
                assert elev_increment > 0.0
                all_ordering = self._grid.at_node["flow__upstream_node_order"]
                upstream_order_bool = np.in1d(all_ordering,
                                              lake_nodes,
                                              assume_unique=True)
                lake_upstream_order = all_ordering[upstream_order_bool]
                argsort_lake = np.argsort(lake_upstream_order)
                elevs_to_add = (np.arange(lake_nodes.size, dtype=float) +
                                1.0) * elev_increment
                sorted_elevs_to_add = elevs_to_add[argsort_lake]
                self._elev[lake_nodes] += sorted_elevs_to_add
        # now put back any fields that were present initially, and wipe the
        # rest:
        for delete_me in spurious_fields:
            if delete_me in self._grid.at_node:
                self._grid.delete_field("node", delete_me)
        for update_me in existing_fields.keys():
            self._grid.at_node[update_me][:] = existing_fields[update_me]
        # fill the output field
        self._sed_fill_depth[:] = self._elev - self._original_elev

    def _add_slopes(self, slope, outlet_node, lake_code):
        """Assuming you have already run the lake_mapper, adds an incline
        towards the outlet to the nodes in the lake."""
        new_elevs = self._elev.copy()
        outlet_coord = (self._grid.node_x[outlet_node],
                        self._grid.node_y[outlet_node])
        lake_nodes = np.where(self._lf.lake_map == lake_code)[0]
        lake_nodes = np.setdiff1d(lake_nodes, self._lake_nodes_treated)
        # lake_ext_margin = self._get_lake_ext_margin(lake_nodes)
        d = self._grid.calc_distances_of_nodes_to_point(outlet_coord,
                                                        node_subset=lake_nodes)
        add_vals = slope * d
        new_elevs[lake_nodes] += add_vals
        self._lake_nodes_treated = np.union1d(self._lake_nodes_treated,
                                              lake_nodes)
        return new_elevs, lake_nodes

    def _get_lake_ext_margin(self, lake_nodes):
        """Returns the nodes forming the external margin of the lake, honoring
        the *routing* method (D4/D8) if applicable."""
        if self._D8 is True:
            all_poss = np.union1d(
                self._grid.active_adjacent_nodes_at_node[lake_nodes],
                self._grid.diagonal_adjacent_nodes_at_node[lake_nodes],
            )
        else:
            all_poss = np.unique(
                self._grid.active_adjacent_nodes_at_node[lake_nodes])
        lake_ext_edge = np.setdiff1d(all_poss, lake_nodes)
        return lake_ext_edge[lake_ext_edge != self._grid.BAD_INDEX]

    def _get_lake_int_margin(self, lake_nodes, lake_ext_edge):
        """Returns the nodes forming the internal margin of the lake, honoring
        the *routing* method (D4/D8) if applicable."""
        lee = lake_ext_edge
        if self._D8 is True:
            all_poss_int = np.union1d(
                self._grid.active_adjacent_nodes_at_node[lee],
                self._grid.diagonal_adjacent_nodes_at_node[lee],
            )
        else:
            all_poss_int = np.unique(
                self._grid.active_adjacent_nodes_at_node[lee])
        lake_int_edge = np.intersect1d(all_poss_int, lake_nodes)
        return lake_int_edge[lake_int_edge != self._grid.BAD_INDEX]

    def _apply_slope_current_lake(self, apply_slope, outlet_node, lake_code,
                                  sublake):
        """Wraps the _add_slopes method to allow handling of conditions where
        the drainage structure would be changed or we're dealing with a
        sublake."""
        while 1:
            starting_elevs = self._elev.copy()
            self._elev[:], lake_nodes = self._add_slopes(
                apply_slope, outlet_node, lake_code)
            # ext_edge = self._get_lake_ext_margin(lake_nodes)
            if sublake:
                break
            else:
                if not self.drainage_directions_change(
                        lake_nodes, starting_elevs, self._elev):
                    break
                else:
                    # put the elevs back...
                    self._elev[lake_nodes] = starting_elevs[lake_nodes]
                    # the slope was too big. Reduce it.
                    apply_slope *= 0.1
        # if we get here, either sublake, or drainage dirs are stable

    def drainage_directions_change(self, lake_nodes, old_elevs, new_elevs):
        """True if the drainage structure at lake margin changes, False
        otherwise."""
        ext_edge = self._get_lake_ext_margin(lake_nodes)
        if self._D8:
            edge_neighbors = np.hstack((
                self._grid.active_adjacent_nodes_at_node[ext_edge],
                self._grid.diagonal_adjacent_nodes_at_node[ext_edge],
            ))
        else:
            edge_neighbors = self._grid.active_adjacent_nodes_at_node[
                ext_edge].copy()
        edge_neighbors[edge_neighbors == self._grid.BAD_INDEX] = -1
        # ^value irrelevant
        old_neighbor_elevs = old_elevs[edge_neighbors]
        new_neighbor_elevs = new_elevs[edge_neighbors]
        # enforce the "don't change drainage direction" condition:
        edge_elevs = old_elevs[ext_edge].reshape((ext_edge.size, 1))
        cond = np.allclose((edge_elevs >= old_neighbor_elevs),
                           (edge_elevs >= new_neighbor_elevs))
        # if True, we're good, the tilting didn't mess with the fr
        return not cond
Esempio n. 28
0
class SinkFiller(Component):
    """
    This component identifies depressions in a topographic surface, then fills
    them in in the topography.  No attempt is made to conserve sediment mass.
    User may specify whether the holes should be filled to flat, or with a
    gradient downwards towards the depression outlet. The gradient can be
    spatially variable, and is chosen to not reverse any drainage directions
    at the perimeter of each lake.

    The primary method of this class is 'run_one_step'. 'fill_pits' is a
    synonym.

    Constructor assigns a copy of the grid, and calls the initialize
    method.

    Examples
    --------
    >>> from landlab import RasterModelGrid
    >>> from landlab import BAD_INDEX_VALUE as XX
    >>> from landlab.components import FlowAccumulator, SinkFiller
    >>> import numpy as np
    >>> lake1 = np.array([34, 35, 36, 44, 45, 46, 54, 55, 56, 65, 74])
    >>> lake2 = np.array([78, 87, 88])
    >>> guard_nodes = np.array([23, 33, 53, 63, 73, 83])
    >>> lake = np.concatenate((lake1, lake2))
    >>> mg = RasterModelGrid((10, 10), 1.)
    >>> z = np.ones(100, dtype=float)
    >>> z += mg.node_x  # add a slope
    >>> z[guard_nodes] += 0.001  # forces the flow out of a particular node
    >>> z[lake] = 0.
    >>> field = mg.add_field('node', 'topographic__elevation', z,
    ...                      units='-', copy=True)
    >>> fr = FlowAccumulator(mg, flow_director='D8')
    >>> fr.run_one_step()
    >>> mg.at_node['flow__sink_flag'][mg.core_nodes].sum()
    14
    >>> hf = SinkFiller(mg, apply_slope=False)
    >>> hf.run_one_step()
    >>> np.allclose(mg.at_node['topographic__elevation'][lake1], 4.)
    True
    >>> np.allclose(mg.at_node['topographic__elevation'][lake2], 7.)
    True

    Now reset and demonstrate the adding of an inclined surface:

    >>> field[:] = z
    >>> hf = SinkFiller(mg, apply_slope=True)
    >>> hf.run_one_step()
    >>> hole1 = np.array([4.00007692, 4.00015385, 4.00023077, 4.00030769,
    ...                   4.00038462, 4.00046154, 4.00053846, 4.00061538,
    ...                   4.00069231, 4.00076923, 4.00084615])
    >>> hole2 = np.array([7.4, 7.2, 7.6])
    >>> np.allclose(mg.at_node['topographic__elevation'][lake1], hole1)
    True
    >>> np.allclose(mg.at_node['topographic__elevation'][lake2], hole2)
    True
    >>> fr.run_one_step()
    >>> mg.at_node['flow__sink_flag'][mg.core_nodes].sum()
    0
    """

    _name = "SinkFiller"

    _input_var_names = ("topographic__elevation", )

    _output_var_names = ("topographic__elevation", "sediment_fill__depth")

    _var_units = {"topographic__elevation": "m", "sediment_fill__depth": "m"}

    _var_mapping = {
        "topographic__elevation": "node",
        "sediment_fill__depth": "node"
    }

    _var_doc = {
        "topographic__elevation": "Surface topographic elevation",
        "sediment_fill__depth": "Depth of sediment added at each" + "node",
    }

    @use_file_name_or_kwds
    def __init__(self,
                 grid,
                 routing="D8",
                 apply_slope=False,
                 fill_slope=1.e-5,
                 **kwds):
        """
        Parameters
        ----------
        grid : ModelGrid
            A landlab grid.
        routing : {'D8', 'D4'} (optional)
            If grid is a raster type, controls whether fill connectivity can
            occur on diagonals ('D8', default), or only orthogonally ('D4').
            Has no effect if grid is not a raster.
        apply_slope : bool
            If False (default), leave the top of the filled sink flat. If True,
            apply the slope fill_slope to the top surface to allow subsequent flow
            routing. A test is performed to ensure applying this slope will not
            alter the drainage structure at the edge of the filled region
            (i.e., that we are not accidentally reversing the flow direction
            far from the outlet.)
        fill_slope : float (m/m)
            The slope added to the top surface of filled pits to allow flow
            routing across them, if apply_slope.
        """
        if "flow__receiver_node" in grid.at_node:
            if grid.at_node["flow__receiver_node"].size != grid.size("node"):
                msg = (
                    "A route-to-multiple flow director has been "
                    "run on this grid. The landlab development team has not "
                    "verified that SinkFiller is compatible with "
                    "route-to-multiple methods. Please open a GitHub Issue "
                    "to start this process.")
                raise NotImplementedError(msg)

        self._grid = grid
        if routing is not "D8":
            assert routing is "D4"
        self._routing = routing
        if (type(self._grid) is
                landlab.grid.raster.RasterModelGrid) and (routing is "D8"):
            self._D8 = True
            self.num_nbrs = 8
        else:
            self._D8 = False  # useful shorthand for thia test we do a lot
            if type(self._grid) is landlab.grid.raster.RasterModelGrid:
                self.num_nbrs = 4
        self._fill_slope = fill_slope
        self._apply_slope = apply_slope
        self.initialize()

    def initialize(self, input_stream=None):
        """
        The BMI-style initialize method takes an optional input_stream
        parameter, which may be either a ModelParameterDictionary object or
        an input stream from which a ModelParameterDictionary can read values.
        """
        # Create a ModelParameterDictionary for the inputs
        if input_stream is None:
            inputs = None
        elif type(input_stream) == ModelParameterDictionary:
            inputs = input_stream
        else:
            inputs = ModelParameterDictionary(input_stream)

        # Make sure the grid includes elevation data. This means either:
        #  1. The grid has a node field called 'topographic__elevation', or
        #  2. The input file has an item called 'ELEVATION_FIELD_NAME' *and*
        #     a field by this name exists in the grid.
        try:
            self._elev = self._grid.at_node["topographic__elevation"]
        except FieldError:
            try:
                self.topo_field_name = inputs.read_string("ELEVATION_" +
                                                          "FIELD_NAME")
            except AttributeError:
                print("Error: Because your grid does not have a node field")
                print('called "topographic__elevation", you need to pass the')
                print("name of a text input file or ModelParameterDictionary,")
                print("and this file or dictionary needs to include the name")
                print("of another field in your grid that contains your")
                print("elevation data.")
                raise AttributeError
            except MissingKeyError:
                print("Error: Because your grid does not have a node field")
                print('called "topographic__elevation", your input file (or')
                print("ModelParameterDictionary) must include an entry with")
                print('the key "ELEVATION_FIELD_NAME", which gives the name')
                print("of a field in your grid that contains your elevation")
                print("data.")
                raise MissingKeyError("ELEVATION_FIELD_NAME")
            try:
                self._elev = self._grid.at_node[self.topo_field_name]
            except AttributeError:
                print(
                    "Your grid does not seem to have a node field called",
                    self.topo_field_name,
                )
        else:
            self.topo_field_name = "topographic__elevation"
        # create the only new output field:
        self.sed_fill_depth = self._grid.add_zeros("node",
                                                   "sediment_fill__depth",
                                                   noclobber=False)

        self._lf = DepressionFinderAndRouter(self._grid, routing=self._routing)
        self._fr = FlowAccumulator(self._grid, flow_director=self._routing)

    def fill_pits(self, **kwds):
        """
        This is a synonym for the main method :func:`run_one_step`.
        """
        self.run_one_step(**kwds)

    def run_one_step(self, **kwds):
        """
        This is the main method. Call it to fill depressions in a starting
        topography.
        """
        # added for back-compatibility with old formats
        try:
            self._apply_slope = kwds["apply_slope"]
        except KeyError:
            pass
        self.original_elev = self._elev.copy()
        # We need this, as we'll have to do ALL this again if we manage
        # to jack the elevs too high in one of the "subsidiary" lakes.
        # We're going to implement the lake_mapper component to do the heavy
        # lifting here, then delete its fields. This means we first need to
        # test if these fields already exist, in which case, we should *not*
        # delete them!
        existing_fields = {}
        spurious_fields = set()
        set_of_outputs = set(self._lf.output_var_names) | set(
            self._fr.output_var_names)
        try:
            set_of_outputs.remove(self.topo_field_name)
        except KeyError:
            pass
        for field in set_of_outputs:
            try:
                existing_fields[field] = self._grid.at_node[field].copy()
            except FieldError:  # not there; good!
                spurious_fields.add(field)

        self._fr.run_one_step()
        self._lf.map_depressions(pits=self._grid.at_node["flow__sink_flag"],
                                 reroute_flow=True)
        # add the depression depths to get up to flat:
        self._elev += self._grid.at_node["depression__depth"]
        # if apply_slope is none, we're now done! But if not...
        if self._apply_slope:
            # new way of doing this - use the upstream structure! Should be
            # both more general and more efficient
            for (outlet_node, lake_code) in zip(self._lf.lake_outlets,
                                                self._lf.lake_codes):
                lake_nodes = np.where(self._lf.lake_map == lake_code)[0]
                lake_perim = self._get_lake_ext_margin(lake_nodes)
                perim_elevs = self._elev[lake_perim]
                out_elev = self._elev[outlet_node]
                lowest_elev_perim = perim_elevs[perim_elevs != out_elev].min()
                # note we exclude the outlet node
                elev_increment = (lowest_elev_perim - self._elev[outlet_node]
                                  ) / (lake_nodes.size + 2.)
                assert elev_increment > 0.
                all_ordering = self._grid.at_node["flow__upstream_node_order"]
                upstream_order_bool = np.in1d(all_ordering,
                                              lake_nodes,
                                              assume_unique=True)
                lake_upstream_order = all_ordering[upstream_order_bool]
                argsort_lake = np.argsort(lake_upstream_order)
                elevs_to_add = (np.arange(lake_nodes.size, dtype=float) +
                                1.) * elev_increment
                sorted_elevs_to_add = elevs_to_add[argsort_lake]
                self._elev[lake_nodes] += sorted_elevs_to_add
        # now put back any fields that were present initially, and wipe the
        # rest:
        for delete_me in spurious_fields:
            if delete_me in self._grid.at_node:
                self._grid.delete_field("node", delete_me)
        for update_me in existing_fields.keys():
            self.grid.at_node[update_me][:] = existing_fields[update_me]
        # fill the output field
        self.sed_fill_depth[:] = self._elev - self.original_elev

    @deprecated(use="fill_pits", version=1.0)
    def _fill_pits_old(self, apply_slope=None):
        """

        .. deprecated:: 0.1.38
            Use :func:`fill_pits` instead.

        This is the main method. Call it to fill depressions in a starting
        topography.

        **Output fields**

        *  `topographic__elevation` : the updated elevations
        *  `sediment_fill__depth` : the depth of sediment added at each node

        Parameters
        ----------
        apply_slope : None, bool, or float
            If a float is provided this is the slope of the surface down
            towards the lake outlet. Supply a small positive number, e.g.,
            1.e-5 (or True, to use this default value).
            A test is performed to ensure applying this slope will not alter
            the drainage structure at the edge of the filled region (i.e.,
            that we are not accidentally reversing the flow direction far
            from the outlet.) The component will automatically decrease the
            (supplied or default) gradient a number of times to try to
            accommodate this, but will eventually raise an OverflowError
            if it can't deal with it. If you pass True, the method will use
            the default value of 1.e-5.
        """
        self.original_elev = self._elev.copy()
        # We need this, as we'll have to do ALL this again if we manage
        # to jack the elevs too high in one of the "subsidiary" lakes.
        # We're going to implement the lake_mapper component to do the heavy
        # lifting here, then delete its fields. This means we first need to
        # test if these fields already exist, in which case, we should *not*
        # delete them!
        existing_fields = {}
        spurious_fields = set()
        set_of_outputs = self._lf.output_var_names | self._fr.output_var_names
        try:
            set_of_outputs.remove(self.topo_field_name)
        except KeyError:
            pass
        for field in set_of_outputs:
            try:
                existing_fields[field] = self._grid.at_node[field].copy()
            except FieldError:  # not there; good!
                spurious_fields.add(field)

        self._fr.run_one_step()
        self._lf.map_depressions(pits=self._grid.at_node["flow__sink_flag"],
                                 reroute_flow=False)
        # add the depression depths to get up to flat:
        self._elev += self._grid.at_node["depression__depth"]
        # if apply_slope is none, we're now done! But if not...
        if apply_slope is True:
            apply_slope = self._fill_slope
        elif type(apply_slope) in (float, int):
            assert apply_slope >= 0.
        if apply_slope:
            # this isn't very efficient, but OK as we're only running this
            # code ONCE in almost all use cases
            sublake = False
            unstable = True
            stability_increment = 0
            self.lake_nodes_treated = np.array([], dtype=int)
            while unstable:
                while 1:
                    for (outlet_node,
                         lake_code) in zip(self._lf.lake_outlets,
                                           self._lf.lake_codes):
                        self._apply_slope_current_lake(apply_slope,
                                                       outlet_node, lake_code,
                                                       sublake)
                    # Call the mapper again here. Bail out if no core pits are
                    # found.
                    # This is necessary as there are some configs where adding
                    # the slope could create subsidiary pits in the topo
                    self._lf.map_depressions(pits=None, reroute_flow=False)
                    if len(self._lf.lake_outlets) == 0.:
                        break
                    self._elev += self._grid.at_node["depression__depth"]
                    sublake = True
                    self.lake_nodes_treated = np.array([], dtype=int)
                # final test that all lakes are not reversing flow dirs
                all_lakes = np.where(
                    self._lf.flood_status < BAD_INDEX_VALUE)[0]
                unstable = self.drainage_directions_change(
                    all_lakes, self.original_elev, self._elev)
                if unstable:
                    apply_slope *= 0.1
                    sublake = False
                    self.lake_nodes_treated = np.array([], dtype=int)
                    self._elev[:] = self.original_elev  # put back init conds
                    stability_increment += 1
                    if stability_increment == 10:
                        raise OverflowError("Filler could not find a stable " +
                                            "condition with a sloping " +
                                            "surface!")
        # now put back any fields that were present initially, and wipe the
        # rest:
        for delete_me in spurious_fields:
            if delete_me in self.grid.at_node:
                self._grid.delete_field("node", delete_me)
        for update_me in existing_fields.keys():
            self.grid.at_node[update_me] = existing_fields[update_me]
        # fill the output field
        self.sed_fill_depth[:] = self._elev - self.original_elev

    def _add_slopes(self, slope, outlet_node, lake_code):
        """
        Assuming you have already run the lake_mapper, adds an incline towards
        the outlet to the nodes in the lake.
        """
        new_elevs = self._elev.copy()
        outlet_coord = (self._grid.node_x[outlet_node],
                        self._grid.node_y[outlet_node])
        lake_nodes = np.where(self._lf.lake_map == lake_code)[0]
        lake_nodes = np.setdiff1d(lake_nodes, self.lake_nodes_treated)
        # lake_ext_margin = self._get_lake_ext_margin(lake_nodes)
        d = self._grid.calc_distances_of_nodes_to_point(outlet_coord,
                                                        node_subset=lake_nodes)
        add_vals = slope * d
        new_elevs[lake_nodes] += add_vals
        self.lake_nodes_treated = np.union1d(self.lake_nodes_treated,
                                             lake_nodes)
        return new_elevs, lake_nodes

    def _get_lake_ext_margin(self, lake_nodes):
        """
        Returns the nodes forming the external margin of the lake, honoring
        the *routing* method (D4/D8) if applicable.
        """
        if self._D8 is True:
            all_poss = np.union1d(
                self.grid.active_adjacent_nodes_at_node[lake_nodes],
                self.grid.diagonal_adjacent_nodes_at_node[lake_nodes],
            )
        else:
            all_poss = np.unique(
                self.grid.active_adjacent_nodes_at_node[lake_nodes])
        lake_ext_edge = np.setdiff1d(all_poss, lake_nodes)
        return lake_ext_edge[lake_ext_edge != BAD_INDEX_VALUE]

    def _get_lake_int_margin(self, lake_nodes, lake_ext_edge):
        """
        Returns the nodes forming the internal margin of the lake, honoring
        the *routing* method (D4/D8) if applicable.
        """
        lee = lake_ext_edge
        if self._D8 is True:
            all_poss_int = np.union1d(
                self._grid.active_adjacent_nodes_at_node[lee],
                self._grid.diagonal_adjacent_nodes_at_node[lee],
            )
        else:
            all_poss_int = np.unique(
                self._grid.active_adjacent_nodes_at_node[lee])
        lake_int_edge = np.intersect1d(all_poss_int, lake_nodes)
        return lake_int_edge[lake_int_edge != BAD_INDEX_VALUE]

    def _apply_slope_current_lake(self, apply_slope, outlet_node, lake_code,
                                  sublake):
        """
        Wraps the _add_slopes method to allow handling of conditions where the
        drainage structure would be changed or we're dealing with a sublake.
        """
        while 1:
            starting_elevs = self._elev.copy()
            self._elev[:], lake_nodes = self._add_slopes(
                apply_slope, outlet_node, lake_code)
            # ext_edge = self._get_lake_ext_margin(lake_nodes)
            if sublake:
                break
            else:
                if not self.drainage_directions_change(
                        lake_nodes, starting_elevs, self._elev):
                    break
                else:
                    # put the elevs back...
                    self._elev[lake_nodes] = starting_elevs[lake_nodes]
                    # the slope was too big. Reduce it.
                    apply_slope *= 0.1
        # if we get here, either sublake, or drainage dirs are stable

    def drainage_directions_change(self, lake_nodes, old_elevs, new_elevs):
        """
        True if the drainage structure at lake margin changes, False otherwise.
        """
        ext_edge = self._get_lake_ext_margin(lake_nodes)
        if self._D8:
            edge_neighbors = np.hstack((
                self.grid.active_adjacent_nodes_at_node[ext_edge],
                self.grid.diagonal_adjacent_nodes_at_node[ext_edge],
            ))
        else:
            edge_neighbors = self.grid.active_adjacent_nodes_at_node[
                ext_edge].copy()
        edge_neighbors[edge_neighbors == BAD_INDEX_VALUE] = -1
        # ^value irrelevant
        old_neighbor_elevs = old_elevs[edge_neighbors]
        new_neighbor_elevs = new_elevs[edge_neighbors]
        # enforce the "don't change drainage direction" condition:
        edge_elevs = old_elevs[ext_edge].reshape((ext_edge.size, 1))
        cond = np.allclose((edge_elevs >= old_neighbor_elevs),
                           (edge_elevs >= new_neighbor_elevs))
        # if True, we're good, the tilting didn't mess with the fr
        return not cond
#Set elapsed time to zero
elapsed_time = 0

#Set model run time
run_time = 100000  #years

#Set rock uplift rate
rock_uplift_rate = 1e-4  #m/yr

while elapsed_time < run_time:
    #Run the flow router
    fr.run_one_step()

    #Run the depression finder and router; optional
    df.map_depressions()

    #Get list of nodes in depressions; only
    #used if using DepressionFinderAndRouter
    flooded = np.where(df.flood_status == 3)[0]

    #Run the SPACE model for one timestep
    sp.run_one_step(dt=timestep, flooded_nodes=flooded)

    #Move bedrock elevation of core nodes upwards relative to baselevel
    #at the rock uplift rate
    mg.at_node['bedrock__elevation'][
        mg.core_nodes] += rock_uplift_rate * timestep

    #Strip any soil from basin outlet so that all topographic change is due to
    #rock uplift
def get_ordered_cells_for_soil_moisture(grid, outlet_id=None):
    """
    Runs Landlab's FlowRouter and DepressionFinderAndRouter to
    route flow. Also orders the cells in the descending order of
    channel length (upstream cell order).
    
    Parameters:
    ==========    
    grid: grid object
        RasterModelGrid
    outlet_id: int (Optional)
        Outlet id to be set

    Returns:
    =======
    ordered_cells: np.array(dtype=int)
        cells ordered in descending order of channel length
    grid: grid object
        updated RasterModelGrid
    """

    if outlet_id == None:
        outlet_id = np.argmin(grid.at_node['topographic__elevation'])    
    outlet = grid.set_watershed_boundary_condition_outlet_id(outlet_id,
        grid.at_node['topographic__elevation'], nodata_value=-9999.,)
    grid.set_closed_boundaries_at_grid_edges(True, True, True, True)
    flw_r = FlowRouter(grid)
    flw_r.run_one_step()
    df = DepressionFinderAndRouter(grid)
    df.map_depressions()
    r = grid.at_node['flow__receiver_node'][grid.node_at_core_cell]
    R = np.zeros(grid.number_of_nodes, dtype=int)
    R[grid.node_at_core_cell] = r
    channel_length = np.zeros(grid.number_of_nodes, dtype=int)
    # Compute channel lengths for each node in the wtrshd (node_at_core_cell)
    for node in grid.node_at_core_cell:
        node_c = node.copy()
        while R[node_c] != node_c:
            channel_length[node] += 1
            node_c = R[node_c]
    grid.at_node['channel_length'] = channel_length
    # Sorting nodes in the ascending order of channel length
    # NOTE: length of ordered_nodes = grid.number_of_core_cells
    ordered_nodes = grid.node_at_core_cell[
        np.argsort(channel_length[grid.node_at_core_cell])]
    # Sorting nodes in the descending order of channel length
    ordered_nodes = ordered_nodes[::-1]
    dd = 1    # switch 2 for while loop
    count_loops = 0 # No. of loops while runs
    while dd:
        dd = 0
        count_loops += 1
        sorted_order = list(ordered_nodes)
        alr_counted_ = []
        for node_ in sorted_order:
            donors = []
            donors = list(grid.node_at_core_cell[np.where(r==node_)[0]])
            if len(donors) != 0:
                for k in range(0, len(donors)):
                    if donors[k] not in alr_counted_:
                        sorted_order.insert(donors[k], sorted_order.pop(sorted_order.index(node_)))
                        dd = 1    
            alr_counted_.append(node_)
        ordered_nodes = np.array(sorted_order)
    ordered_cells = grid.cell_at_node[ordered_nodes]
    return ordered_cells, grid
def run_model(mdl_input=None):
    if mdl_input == None:
        mdl_input = default_landscape_parameters()

    # for ease, seperate the input into its main comonents
    domain = mdl_input['model_domain']
    landscape = mdl_input['landscape']
    tectonics = mdl_input['tectonics']
    fault_opt = mdl_input['fault_opt']
    duration = mdl_input['duration']
    save_opt = mdl_input['save_opt']

    # set the pointspacing
    dxy = domain['ymax'] / domain['Nxy']

    # set the location of the fault
    if fault_opt['fault_pos'] == 'midway':
        fault_pos = domain['ymax'] / 2

    # time bookeeping
    current_time = 0  # time tracking variable [kyr]
    i = 0  # iteration tracker [integer]
    plot_num = 2  # number of iterations to run before each new plot
    #calculate_BR= False     # calculate the metric BR used in the main paper
    num_frames = ((duration['total_time'] - duration['shear_start']) /
                  duration['dt_during_shear']) / plot_num + 1

    # informative output file: e.g. arctan_exp_5m_fault
    if save_opt['save_format'] == 'Default':
        d = datetime.datetime.today()
        fn = '{slip}{localization:.2e}_{damage}{intensity:.2e}_fault'.format(
            slip=fault_opt['slip_profile'],
            localization=fault_opt['localization'],
            damage=fault_opt['damage_prof'],
            intensity=landscape['km'])

        save_opt['save_format'] = {
            'file_name': fn,
            'dirname': fn + d.strftime('%d-%m-%Y')
        }

    print('Starting model run: ' + save_opt['save_format']['file_name'])

    ###########################################################################
    ###########################################################################
    ## Define domain (e.g. number of columns and rows)
    ncols = int(domain['xmax'] * (1. + 2. * domain['loopBuff']) /
                dxy)  # number of columns, tripled for looped boundaries kdc
    nrows = int(domain['ymax'] / dxy)  # number of rows
    e1 = ncols * dxy * domain['loopBuff'] / (2. * domain['loopBuff'] + 1.
                                             )  # edge1
    e2 = ncols * dxy - e1  # edge2

    ## Build the landlab grid #
    rmg = RasterModelGrid((nrows, ncols),
                          dxy)  # build a landlab grid of nrows x ncols

    #set boundary conditions to have top closed and all others open
    rmg.set_closed_boundaries_at_grid_edges(tectonics['boundaries'][0],
                                            tectonics['boundaries'][1],
                                            tectonics['boundaries'][2],
                                            tectonics['boundaries'][3])

    # Add an elevation field + slope and noise to get the flow router started
    rmg.add_zeros('node', 'topographic__elevation')
    rmg['node']['topographic__elevation'] += (rmg.node_y * 0.1 +
                                              np.random.rand(nrows * ncols))

    ################################################################################
    ## Fourth, the fun part, set up the off-fault deformation profile #############
    ################################################################################

    # define how a fault will work
    class fault:
        def __init__(self, rmg, fault_pos=None, fault_width=None):
            self.rmg = rmg
            self.nrows = rmg.shape[0]
            self.ncols = rmg.shape[1]
            self.fault_pos = fault_pos
            self.fault_width = fault_width

        def slip_profile(self, fault_prof, width=None):
            '''
            Define a slip profile relative to the dimensions of the model.
            '''

            yLocation = np.arange(nrows) * self.rmg.dx

            if width == None:
                width = self.fault_width

            if fault_prof == 'arctan':
                profile = 0.5 + 1 / np.pi * np.arctan(
                    (yLocation - self.fault_pos) * np.pi /
                    width)  # deal with magic number
            elif fault_prof == 'heaviside':
                profile = np.heaviside(yLocation - self.fault_pos, 1)
            elif fault_prof == 'exp':
                profile = np.exp(-yLocation / (fault_opt['v_star'] / dxy))
            else:
                raise ('fault_opt must be one of: arctan, heavyside, exp')

            v_profile = profile * fault_opt['vmax']
            accum_disp = profile * self.rmg.dx

            return v_profile, accum_disp

        def lithology(self,
                      damage_opt=None,
                      width=None,
                      ko=None,
                      km=None,
                      Do=None,
                      Dm=None):

            if width == None:
                width = self.fault_width  # set to the instantiated width

            if damage_opt == None:
                damage_func = lambda x: np.zeros(x.size)
                km = 0
                Dm = 0

            # 2D profiles
            if damage_opt == 'boxcar':
                damage_func = lambda x: np.heaviside(x-fault_pos-width,1) - \
                                        np.heaviside(x-fault_pos+width,1)

            if damage_opt == 'heaviside_up':
                damage_func = lambda x: np.abs(
                    np.heaviside(x - fault_pos, 1) - 1)

            if damage_opt == 'heaviside_down':
                damage_func = lambda x: np.heaviside(x - fault_pos, 1)

            if damage_opt == 'exp':
                damage_func = lambda x: (
                    width / (abs(x - fault_pos) + width)
                )  # NEED TO DEFINE BUFF -> similar to c value in aftershocks

            def mk_dict(rmg, damage_func, ao, am):
                lith_ary = ao + (am - ao) * damage_func(rmg.node_y)
                damage_dict = dict(zip(rmg.node_y, lith_ary))
                return damage_dict

            attrs = {
                'K_sp': mk_dict(self.rmg, damage_func, ko, km),
                'D': mk_dict(self.rmg, damage_func, Do, Dm)
            }

            lith = Lithology(
                self.rmg,
                self.rmg.ones().ravel().reshape(1, len(
                    self.rmg.node_y)),  # note that this is a dummy thickness
                self.rmg.node_y.reshape(1, len(self.rmg.node_y)),
                attrs,
                rock_id=self.rmg.node_y)

            self.rmg['node']['topographic__elevation'] += 1.
            lith.run_one_step()
            #imshow_grid(self.rmg, 'K_sp', cmap='viridis', vmin=ko, vmax=km)
            return lith

    # define the position of the fault
    this_fault = fault(rmg, fault_pos, fault_opt['width'])
    this_fault.lithology(damage_opt=fault_opt['damage_prof'],
                         ko=landscape['ko'],
                         km=landscape['km'],
                         Do=landscape['Do'],
                         Dm=landscape['Dm'])
    v_profile, accum_disp = this_fault.slip_profile(
        fault_opt['slip_profile'],
        fault_opt['localization'] * fault_opt['width'])
    # This is an array for counting how many pixels need to be moved
    nshift = np.zeros(nrows)
    n_buff = 0  # optional extra buffer zone incase you only want to move a subset.

    ################################################################################
    ## Next, we instantiate landlab components that will evolve the landscape #####
    ################################################################################

    fr = FlowAccumulator(
        rmg, 'topographic__elevation',
        flow_director='FlowDirectorD8')  # standard D8 flow routing algorithm
    sp = FastscapeEroder(rmg,
                         K_sp='K_sp',
                         m_sp=landscape['m'],
                         n_sp=landscape['n'],
                         threshold_sp=0)  # river eroder
    lin_diffuse = LinearDiffuser(rmg, linear_diffusivity='D')  #linear diffuser
    fill = DepressionFinderAndRouter(rmg)  #lake filling algorithm

    ################################################################################
    ## Next, we instantiate an object to store the model output #####
    ################################################################################
    nts = int(num_frames)
    if not save_opt['save_out'] == False:
        # define the base coordinates of the grid
        ds = xr.Dataset(
            coords={
                'x': (
                    ('x'),  # tuple of dimensions
                    rmg.x_of_node.reshape(rmg.shape)[
                        0, :],  # 1-d array of coordinate data
                    {
                        'units': 'meters'
                    }),  # dictionary with data attributes
                'y': (('y'), rmg.y_of_node.reshape(rmg.shape)[:, 1], {
                    'units': 'meters'
                }),
                'time': (('time'), duration['dt'] * np.arange(nts) / 1e6, {
                    'units': 'millions of years since model start',
                    'standard_name': 'time'
                })
            })

        # introduce the fields according the the fields specified in "save_opt"
        for of in save_opt['out_fields']:
            ds[of] = (('time', 'y', 'x'),
                      np.empty((nts, rmg.shape[0], rmg.shape[1])), {
                          'units': 'meters'
                      })

    plotCounter = 0

    ################################################################################
    # Now that all parameters and landlab components are set, run the loop #######
    ################################################################################

    dt = duration['dt']  # define the timestep
    mean_elev = []
    time_array = []

    with tqdm(total=duration['total_time'], position=0, leave=True) as pbar:
        while (current_time <= duration['total_time']):
            pbar.n = np.round(current_time)
            pbar.refresh()

            ## Looped boundary conditions ##

            # Because the landlab flow router isn't currently set up to use looped
            # boundary conditions, I simulated them here by duplicating the landscape
            # to the left and right such that the flow accumulator would 'see' the
            # the appropriate amount of upstream drainage area.

            rmg['node']['topographic__elevation'][(rmg.node_x < e1)] = (
                rmg['node']['topographic__elevation'][(rmg.node_x >= (e2 - e1))
                                                      & (rmg.node_x < e2)])

            rmg['node']['topographic__elevation'][(rmg.node_x >= e2)] = (
                rmg['node']['topographic__elevation'][(rmg.node_x >= e1) &
                                                      (rmg.node_x < (2 * e1))])

            ## Tectonic off-fault deformation ##

            # To simulate off fault lateral displacement/deformation, we apply a
            # lateral velocity profile. This is done by taking the landlab elevations
            # and shifting their coordinates.
            if (current_time > duration['shear_start']):

                dt = duration[
                    'dt_during_shear']  # First set a lower timestep to keep it stable

                # Take the landlab grid elevations and reshape into a box nrows x ncols
                elev = rmg['node']['topographic__elevation']
                elev_box = np.reshape(elev, [nrows, ncols])

                # Calculate the offset that has accumulated over a timestep
                accum_disp += v_profile * dt

                # now scan up the landscape row by row looking for offset
                for r in range(nrows):

                    # check if the accumulated offset for a row is larger than a pixel
                    if accum_disp[r] >= dxy:

                        # if so, count the number of in the row pixels to be moved
                        nshift[r] = int(np.floor(accum_disp[r] / dxy))

                        # copy which pixels will be moved off the grid by displacement
                        temp = elev_box[r, n_buff:int(nshift[r]) + n_buff]

                        # move the row over by the number of pixels of accumulated offset
                        elev_box[r, n_buff:(
                            (ncols - n_buff) -
                            int(nshift[r]))] = elev_box[r,
                                                        int(nshift[r]) +
                                                        n_buff:ncols - n_buff]

                        # replace the values on the right side by the ones from the left
                        elev_box[r, ((ncols) - int(nshift[r])):ncols] = temp

                        # last, subtract the offset pixels from the accumulated displacement
                        accum_disp[r] -= dxy

                #This section is if you select a middle section to be moved independently
                elev_box[:, (ncols -
                             n_buff):ncols] = elev_box[:, n_buff:(2 * n_buff)]
                elev_box[:, 0:n_buff] = elev_box[:, ncols - 2 * n_buff:ncols -
                                                 n_buff]

                # Finally, reshape the elevation box into an array and feed to landlab
                elev_new = np.reshape(elev_box, nrows * ncols)
                rmg['node']['topographic__elevation'] = elev_new

            # record the evolution of the mean elevation
            mean_elev.append(np.mean(rmg['node']['topographic__elevation']))
            time_array.append(current_time)

            ## Landscape Evolution ##

            # Now that we have performed the tectonic deformation, lets apply our
            # landscape evolution and watch the landscape change as a result.

            # Uplift the landscape
            rmg['node']['topographic__elevation'][
                rmg.core_nodes] += tectonics['uplift_rate'] * dt

            # set the lower boundary as fixed elevation
            #rmg['node']['topographic__elevation'][rmg.node_y==0] = 0
            #rmg['node']['topographic__elevation'][rmg.node_y==max(rmg.node_y)] = 0

            # Diffuse the landscape simulating hillslope sediment transport
            lin_diffuse.run_one_step(dt)

            # Accumulate and route flow, fill any lakes, and erode under the rivers
            fr.run_one_step()  # route flow
            DepressionFinderAndRouter.map_depressions(fill)  # fill lakes
            sp.run_one_step(dt)  # fastscape stream power eroder

            ## Plotting ##
            # plot output at each [plot_num] iteration once we start lateral advection
            if i % plot_num == 0 and current_time > duration['shear_start']:

                if not save_opt['save_out'] == False:
                    for of in save_opt['out_fields']:
                        ds[of][plotCounter, :, :] = rmg['node'][of].reshape(
                            rmg.shape)
                plotCounter += 1
            current_time += dt  # update time tracker
            i += 1  # update iteration variable
    print('number of frames:', plotCounter)
    print('For loop complete')

    ##########################################################################
    ### We do some bookkeeping with the result ###############################
    ##########################################################################
    #    plt.figure()
    #    imshow_grid(rmg,'topographic__elevation',show_elements=False)
    #    # tweek edges of domain so that they match the buffer kdc
    #    plt.xlim((e1,e2))
    #    plt.show()
    #    plt.clf

    ##########################################################################
    ## plot the evolution of the mean elevation of the model:
    if tectonics['track_uplift'] == True:
        plt.figure()
        plt.plot(time_array, mean_elev, label='Mean elevation')
        plt.axvline(duration['shear_start'],
                    color='r',
                    label='Start lateral advection')
        plt.xlabel('Iteration number')
        plt.ylabel('Mean elevation')
        plt.legend()
        plt.show()

    # small function to save a specific field as a gif
    ## plot the output into a gif
    get_ipython().run_line_magic(
        'opts',
        "Image style(interpolation='bilinear', cmap='viridis') plot[colorbar=True]"
    )
    get_ipython().run_line_magic('output', 'size=100')

    fn = save_opt['save_format']['file_name']
    dn = save_opt['save_format']['dirname']
    if save_opt['save_out'] == True:
        os.mkdir(dn)

        for of in save_opt['out_fields']:
            hvds_topo = hv.Dataset(ds[of])
            topo = hvds_topo.to(hv.Image, ['x', 'y'])
            topo.opts(colorbar=True, fig_size=200, xlim=(e1, e2))
            hv.save(topo, os.path.join(dn, fn + of + '.gif'))
        with open(os.path.join(dn, fn + '.pkl'), 'wb') as f:
            pickle.dump([rmg, ds], f)
        # and to load the session again:
        # dill.load_session(filename)
    if save_opt['save_ascii'] == True:
        write_esri_ascii(os.path.join(
            dn, save_opt['save_format']['file_name'] + '.asc'),
                         rmg,
                         names='topographic__elevation')

    if save_opt['save_out'] == 'temp':
        hv.save(topo, 'temp_output.gif')

    if save_opt['append_to_log'] == True:

        def NestedDictValues(d):
            for v in d.values():
                if isinstance(v, dict):
                    yield from NestedDictValues(v)
                else:
                    yield v

        with open('Model_log.csv', mode='a') as fd:
            writer = csv.writer(fd)
            writer.writerow(NestedDictValues(mdl_input))
    return rmg