Beispiel #1
0
class GrainFacet(CTSModel):
    """
    Model hillslope evolution with block uplift.
    """
    def __init__(self,
                 grid_size,
                 report_interval=1.0e8,
                 run_duration=1.0,
                 output_interval=1.0e99,
                 settling_rate=2.2e8,
                 disturbance_rate=1.0,
                 weathering_rate=1.0,
                 uplift_interval=1.0,
                 plot_interval=1.0e99,
                 friction_coef=0.3,
                 fault_x=1.0,
                 rock_state_for_uplift=7,
                 opt_rock_collapse=False,
                 show_plots=True,
                 initial_state_grid=None,
                 opt_track_grains=False,
                 prop_data=None,
                 prop_reset_value=None,
                 callback_fn=None,
                 **kwds):
        """Call the initialize() method."""
        self.initializer(grid_size, report_interval, run_duration,
                         output_interval, settling_rate, disturbance_rate,
                         weathering_rate, uplift_interval, plot_interval,
                         friction_coef, fault_x, rock_state_for_uplift,
                         opt_rock_collapse, show_plots, initial_state_grid,
                         opt_track_grains, prop_data, prop_reset_value,
                         callback_fn, **kwds)

    def initializer(self, grid_size, report_interval, run_duration,
                    output_interval, settling_rate, disturbance_rate,
                    weathering_rate, uplift_interval, plot_interval,
                    friction_coef, fault_x, rock_state_for_uplift,
                    opt_rock_collapse, show_plots, initial_state_grid,
                    opt_track_grains, prop_data, prop_reset_value, callback_fn,
                    **kwds):
        """Initialize the grain hill model."""
        self.settling_rate = settling_rate
        self.disturbance_rate = disturbance_rate
        self.weathering_rate = weathering_rate
        self.uplift_interval = uplift_interval
        self.plot_interval = plot_interval
        self.friction_coef = friction_coef
        self.rock_state = rock_state_for_uplift  # 7 (resting sed) or 8 (rock)
        self.opt_track_grains = opt_track_grains
        self.callback_fn = callback_fn
        if opt_rock_collapse:
            self.collapse_rate = self.settling_rate
        else:
            self.collapse_rate = 0.0

        # Call base class init
        super(GrainFacet,
              self).initialize(grid_size=grid_size,
                               report_interval=report_interval,
                               grid_orientation='vertical',
                               grid_shape='rect',
                               show_plots=show_plots,
                               cts_type='oriented_hex',
                               run_duration=run_duration,
                               output_interval=output_interval,
                               initial_state_grid=initial_state_grid,
                               prop_data=prop_data,
                               prop_reset_value=prop_reset_value,
                               **kwds)

        # Set some things related to property-swapping and/or callback fn
        # if the user wants to track grain motion.
        #if opt_track_grains:
        #    propid = self.ca.propid
        #else:
        #    propid = None

        self.uplifter = LatticeNormalFault(
            fault_x_intercept=fault_x,
            grid=self.grid,
            node_state=self.grid.at_node['node_state'],
            propid=self.ca.propid,
            prop_data=self.ca.prop_data,
            prop_reset_value=self.ca.prop_reset_value)

        self.initialize_timing(output_interval, plot_interval, uplift_interval,
                               report_interval)

    def initialize_timing(self, output_interval, plot_interval,
                          uplift_interval, report_interval):
        """Set up variables related to timing of uplift, output, reporting"""

        self.current_time = 0.0

        # Next time for output to file
        self.next_output = output_interval

        # Next time for a plot
        if self._show_plots:
            self.next_plot = plot_interval
        else:
            self.next_plot = self.run_duration + 1

        # Next time for a progress report to user
        self.next_report = report_interval

        # Next time to add baselevel adjustment
        self.next_uplift = uplift_interval

        # Iteration numbers, for output files
        self.output_iteration = 1

    def node_state_dictionary(self):
        """
        Create and return dict of node states.

        Overrides base-class method. Here, we simply call on a function in
        the lattice_grain module.
        """
        return lattice_grain_node_states()

    def transition_list(self):
        """
        Make and return list of Transition object.
        """
        xn_list = lattice_grain_transition_list(g=self.settling_rate,
                                                f=self.friction_coef,
                                                motion=self.settling_rate,
                                                swap=self.opt_track_grains,
                                                callback=self.callback_fn)
        xn_list = self.add_weathering_and_disturbance_transitions(
            xn_list,
            self.disturbance_rate,
            self.weathering_rate,
            collapse_rate=self.collapse_rate)
        return xn_list

    def add_weathering_and_disturbance_transitions(self,
                                                   xn_list,
                                                   d=0.0,
                                                   w=0.0,
                                                   collapse_rate=0.0,
                                                   swap=False,
                                                   callback=None):
        """
        Add transition rules representing weathering and/or grain disturbance
        to the list, and return the list.

        Parameters
        ----------
        xn_list : list of Transition objects
            List of objects that encode information about the link-state
            transitions. Normally should first be initialized with lattice-grain
            transition rules, then passed to this function to add rules for
            weathering and disturbance.
        d : float (optional)
            Rate of transition (1/time) from fluid / resting grain pair to
            mobile-grain / fluid pair, representing grain disturbance.
        w : float (optional)
            Rate of transition (1/time) from fluid / rock pair to
            fluid / resting-grain pair, representing weathering.

        Returns
        -------
        xn_list : list of Transition objects
            Modified transition list.
        """

        # Disturbance rule
        if d > 0.0:
            xn_list.append(
                Transition((7, 0, 0), (0, 1, 0), d, 'disturbance', swap,
                           callback))
            xn_list.append(
                Transition((7, 0, 1), (0, 2, 1), d, 'disturbance', swap,
                           callback))
            xn_list.append(
                Transition((7, 0, 2), (0, 3, 2), d, 'disturbance', swap,
                           callback))
            xn_list.append(
                Transition((0, 7, 0), (4, 0, 0), d, 'disturbance', swap,
                           callback))
            xn_list.append(
                Transition((0, 7, 1), (5, 0, 1), d, 'disturbance', swap,
                           callback))
            xn_list.append(
                Transition((0, 7, 2), (6, 0, 2), d, 'disturbance', swap,
                           callback))

        # Weathering rule
        if w > 0.0:
            xn_list.append(Transition((8, 0, 0), (7, 0, 0), w, 'weathering'))
            xn_list.append(Transition((8, 0, 1), (7, 0, 1), w, 'weathering'))
            xn_list.append(Transition((8, 0, 2), (7, 0, 2), w, 'weathering'))
            xn_list.append(Transition((0, 8, 0), (0, 7, 0), w, 'weathering'))
            xn_list.append(Transition((0, 8, 1), (0, 7, 1), w, 'weathering'))
            xn_list.append(Transition((0, 8, 2), (0, 7, 2), w, 'weathering'))

            # "Vertical rock collapse" rule: a rock particle overlying air
            # will collapse, transitioning to a downward-moving grain
            if collapse_rate > 0.0:
                xn_list.append(
                    Transition((0, 8, 0), (4, 0, 0), collapse_rate,
                               'rock collapse', swap, callback))

        if _DEBUG:
            print()
            print('setup_transition_list(): list has ' + str(len(xn_list)) +
                  ' transitions:')
            for t in xn_list:
                print('  From state ' + str(t.from_state) + ' to state ' +
                      str(t.to_state) + ' at rate ' + str(t.rate) +
                      ' called ' + str(t.name))

        return xn_list

    def initialize_node_state_grid(self):
        """Set up initial node states.

        Examples
        --------
        >>> gh = GrainFacet((5, 7))
        >>> gh.grid.at_node['node_state']
        array([8, 7, 7, 8, 7, 7, 7, 0, 7, 7, 0, 7, 7, 7, 0, 0, 0, 0, 0, 0, 0, 0,
               0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
        """

        # For shorthand, get a reference to the node-state grid
        nsg = self.grid.at_node['node_state']

        # Fill the bottom two rows with grains
        right_side_x = 0.866025403784 * (self.grid.number_of_node_columns - 1)
        for i in range(self.grid.number_of_nodes):
            if self.grid.node_y[i] < 2.0:
                if (self.grid.node_x[i] > 0.0
                        and self.grid.node_x[i] < right_side_x):
                    nsg[i] = 7

        # Place "wall" particles in the lower-left and lower-right corners
        if self.grid.number_of_node_columns % 2 == 0:
            bottom_right = self.grid.number_of_node_columns - 1
        else:
            bottom_right = self.grid.number_of_node_columns // 2
        nsg[0] = 8  # bottom left
        nsg[bottom_right] = 8

        return nsg

    def run(self, to=None):
        """Run the model."""
        if to is None:
            run_to = self.run_duration
        else:
            run_to = to

        while self.current_time < run_to:

            # Figure out what time to run to this iteration
            next_pause = min(self.next_output, self.next_plot)
            next_pause = min(next_pause, self.next_uplift)
            next_pause = min(next_pause, run_to)

            # Once in a while, print out simulation and real time to let the user
            # know that the sim is running ok
            current_real_time = time.time()
            if current_real_time >= self.next_report:
                print('Current sim time' + str(self.current_time) + '(' + \
                      str(100 * self.current_time / self.run_duration) + '%)')
                self.next_report = current_real_time + self.report_interval

            # Run until next pause
            self.ca.run(next_pause, self.ca.node_state)
            self.current_time = next_pause

            # Handle output to file
            if self.current_time >= self.next_output:
                self.write_output(self.grid, 'grain_hill_model',
                                  self.output_iteration)
                self.output_iteration += 1
                self.next_output += self.output_interval

            # Handle plotting on display
            if self._show_plots and self.current_time >= self.next_plot:
                self.ca_plotter.update_plot()
                axis('off')
                self.next_plot += self.plot_interval

            # Handle uplift
            if self.current_time >= self.next_uplift:
                self.uplifter.do_offset(ca=self.ca,
                                        current_time=self.current_time,
                                        rock_state=self.rock_state)
                self.next_uplift += self.uplift_interval

    def get_profile_and_soil_thickness(self, grid, data):
        """Calculate and return profiles of elevation and soil thickness.

        Examples
        --------
        >>> from landlab import HexModelGrid
        >>> hg = HexModelGrid(4, 5, shape='rect', orientation='vert')
        >>> ns = hg.add_zeros('node', 'node_state', dtype=int)
        >>> ns[[0, 3, 1, 6, 4, 9, 2]] = 8
        >>> ns[[8, 13, 11, 16, 14]] = 7
        >>> gh = GrainHill((3, 7))  # grid size arbitrary here
        >>> (elev, thickness) = gh.get_profile_and_soil_thickness(hg, ns)
        >>> elev
        array([0. , 2.5, 3. , 2.5, 0. ])
        >>> thickness
        array([0., 2., 2., 1., 0.])
        """
        nc = grid.number_of_node_columns
        elev = zeros(nc)
        soil = zeros(nc)
        for col in range(nc):
            states = data[grid.nodes[:, col]]
            (rows_with_rock_or_sed, ) = where(states > 0)
            if len(rows_with_rock_or_sed) == 0:
                elev[col] = 0.0
            else:
                elev[col] = amax(rows_with_rock_or_sed) + 0.5 * (col % 2)
            soil[col] = count_nonzero(logical_and(states > 0, states < 8))

        return elev, soil
Beispiel #2
0
class GrainFacetSimulator(CTSModel):
    """
    Model facet-slope evolution with 60-degree normal-fault slip.
    """
    def __init__(self, grid_size, report_interval=1.0e8, run_duration=1.0,
                 output_interval=1.0e99, disturbance_rate=0.0,
                 weathering_rate=0.0, dissolution_rate=0.0,
                 uplift_interval=1.0, baselevel_rise_interval=0,
                 plot_interval=1.0e99, friction_coef=0.3,
                 fault_x=1.0, cell_width=1.0, grav_accel=9.8,
                 init_state_grid=None, save_plots=False, plot_filename=None,
                 plot_filetype='.png', seed=0, **kwds):
        """Call the initialize() method."""
        self.initialize(grid_size, report_interval, run_duration,
                        output_interval, disturbance_rate, weathering_rate,
                        dissolution_rate, uplift_interval,
                        baselevel_rise_interval, plot_interval, friction_coef,
                        fault_x,cell_width, grav_accel, init_state_grid,
                        save_plots, plot_filename, plot_filetype, seed, **kwds)

    def initialize(self, grid_size, report_interval, run_duration,
                   output_interval, disturbance_rate, weathering_rate,
                   dissolution_rate, uplift_interval, baselevel_rise_interval,
                   plot_interval, friction_coef, fault_x, cell_width,
                   grav_accel, init_state_grid=None, save_plots=False,
                   plot_filename=None, plot_filetype='.png', seed=0, **kwds):
        """Initialize the grain hill model."""
        self.disturbance_rate = disturbance_rate
        self.weathering_rate = weathering_rate
        self.dissolution_rate = dissolution_rate
        self.uplift_interval = uplift_interval
        self.baselevel_rise_interval = baselevel_rise_interval
        self.plot_interval = plot_interval
        self.friction_coef = friction_coef

        self.settling_rate = calculate_settling_rate(cell_width, grav_accel)

        # Call base class init
        super(GrainFacetSimulator, self).initialize(grid_size=grid_size,
                                          report_interval=report_interval,
                                          grid_orientation='vertical',
                                          grid_shape='rect',
                                          show_plots=False,
                                          cts_type='oriented_hex',
                                          run_duration=run_duration,
                                          output_interval=output_interval,
                                          plot_every_transition=False,
                                          initial_state_grid=init_state_grid,
                                          closed_boundaries=(True, True,
                                                             False, False),
                                          seed=seed)

        ns = self.grid.at_node['node_state']
        self.uplifter = LatticeNormalFault(fault_x_intercept=fault_x,
                                           grid=self.grid,
                                           node_state=ns)

        # initialize plotting
        if plot_interval <= run_duration:
            import matplotlib.pyplot as plt
            plt.ion()
            plt.figure(1)
            self.save_plots = save_plots
            if save_plots:
                self.plot_filename = plot_filename
                self.plot_filetype = plot_filetype
                nplots = (self.run_duration / self.plot_interval) + 1
                self.ndigits = int(np.floor(np.log10(nplots))) + 1
                this_filename = (plot_filename + '0'.zfill(self.ndigits)
                                 + plot_filetype)
                print(this_filename)
            else:
                this_filename = None
            plot_hill(self.grid, this_filename)

        # Work out the next times to plot and output
        self.next_output = self.output_interval
        self.next_plot = self.plot_interval
        self.plot_iteration = 1

        # Next time for a progress report to user
        self.next_report = self.report_interval

        # And baselevel adjustment
        self.next_uplift = self.uplift_interval
        if self.baselevel_rise_interval > 0:
            self.next_baselevel = self.baselevel_rise_interval
            self.baselevel_row = 1
        else:
            self.next_baselevel = self.run_duration + 1

        self.current_time = 0.0

    def node_state_dictionary(self):
        """
        Create and return dict of node states.

        Overrides base-class method. Here, we simply call on a function in
        the lattice_grain module.
        """
        return lattice_grain_node_states()

    def transition_list(self):
        """
        Make and return list of Transition object.
        """
        xn_list = lattice_grain_transition_list(g=self.settling_rate,
                                                f=self.friction_coef,
                                                motion=self.settling_rate)
        xn_list = self.add_weathering_and_disturbance_transitions(xn_list,
                    self.disturbance_rate, self.weathering_rate,
                    self.dissolution_rate)
        return xn_list

    def add_weathering_and_disturbance_transitions(self, xn_list, d=0.0, w=0.0,
                                                   diss=0.0):
        """
        Add transition rules representing weathering and/or grain disturbance
        to the list, and return the list.

        Parameters
        ----------
        xn_list : list of Transition objects
            List of objects that encode information about the link-state
            transitions. Normally should first be initialized with lattice-grain
            transition rules, then passed to this function to add rules for
            weathering and disturbance.
        d : float (optional, default=0.0)
            Rate of transition (1/time) from fluid / resting grain pair to
            mobile-grain / fluid pair, representing grain disturbance.
        w : float (optional, default=0.0)
            Rate of transition (1/time) from fluid / rock pair to
            fluid / resting-grain pair, representing weathering.
        diss : float (optional, default=0.0)
            Dissolution: rate of transition from fluid / rock pair to
            fluid / fluid pair.

        Returns
        -------
        xn_list : list of Transition objects
            Modified transition list.
        """

        # Disturbance rule
        if d > 0.0:
            xn_list.append( Transition((7,0,0), (0,1,0), d, 'disturbance') )
            xn_list.append( Transition((7,0,1), (0,2,1), d, 'disturbance') )
            xn_list.append( Transition((7,0,2), (0,3,2), d, 'disturbance') )
            xn_list.append( Transition((0,7,0), (4,0,0), d, 'disturbance') )
            xn_list.append( Transition((0,7,1), (5,0,1), d, 'disturbance') )
            xn_list.append( Transition((0,7,2), (6,0,2), d, 'disturbance') )

        # Weathering rule
        if w > 0.0:
            xn_list.append( Transition((8,0,0), (7,0,0), w, 'weathering') )
            xn_list.append( Transition((8,0,1), (7,0,1), w, 'weathering') )
            xn_list.append( Transition((8,0,2), (7,0,2), w, 'weathering') )
            xn_list.append( Transition((0,8,0), (0,7,0), w, 'weathering') )
            xn_list.append( Transition((0,8,1), (0,7,1), w, 'weathering') )
            xn_list.append( Transition((0,8,2), (0,7,2), w, 'weathering') )

        # Dissolution rule
        if diss > 0.0:
            xn_list.append( Transition((8,0,0), (0,0,0), diss, 'dissolution') )
            xn_list.append( Transition((8,0,1), (0,0,1), diss, 'dissolution') )
            xn_list.append( Transition((8,0,2), (0,0,2), diss, 'dissolution') )
            xn_list.append( Transition((0,8,0), (0,0,0), diss, 'dissolution') )
            xn_list.append( Transition((0,8,1), (0,0,1), diss, 'dissolution') )
            xn_list.append( Transition((0,8,2), (0,0,2), diss, 'dissolution') )

        if _DEBUG:
            print('')
            print('setup_transition_list(): list has ' + str(len(xn_list))
                  + ' transitions:')
            for t in xn_list:
                print('  From state ' + str(t.from_state) + ' to state '
                      + str(t.to_state) + ' at rate ' + str(t.rate) + 'called'
                      + str(t.name))

        return xn_list

    def initialize_node_state_grid(self):
        """Set up initial node states.

        Examples
        --------
        >>> from grainhill import GrainHill
        >>> gh = GrainHill((5, 7))
        >>> gh.grid.at_node['node_state'][:20]
        array([8, 7, 7, 8, 7, 7, 7, 0, 7, 7, 0, 7, 7, 7, 0, 0, 0, 0, 0, 0])
        """

        # For shorthand, get a reference to the node-state grid
        nsg = self.grid.at_node['node_state']

        # Fill the bottom two rows with grains
        right_side_x = 0.866025403784 * (self.grid.number_of_node_columns - 1)
        for i in range(self.grid.number_of_nodes):
            if self.grid.node_y[i] < 2.0:
                if (self.grid.node_x[i] > 0.0 and
                    self.grid.node_x[i] < right_side_x):
                    nsg[i] = 8

        # Place "wall" particles in the lower-left and lower-right corners
        if self.grid.number_of_node_columns % 2 == 0:
            bottom_right = self.grid.number_of_node_columns - 1
        else:
            bottom_right = self.grid.number_of_node_columns // 2
        nsg[0] = 8  # bottom left
        nsg[bottom_right] = 8

        return nsg

    def update_until(self, run_to_time):
        """Advance up to a specified time."""
        while self.current_time < run_to_time:

            # Figure out what time to run to this iteration
            next_pause = min(self.next_output, self.next_plot)
            next_pause = min(next_pause, self.next_uplift)
            next_pause = min(next_pause, self.next_baselevel)
            next_pause = min(next_pause, run_to_time)

            # Once in a while, print out simulation and real time to let the user
            # know that the sim is running ok
            current_real_time = time.time()
            if current_real_time >= self.next_report:
                print('Current sim time ' + str(self.current_time) + ' (' + \
                      str(100 * self.current_time / self.run_duration) + '%)')
                self.next_report = current_real_time + self.report_interval

            # Run the model forward in time until the next output step
            print('Running to...' + str(next_pause))
            self.ca.run(next_pause, self.ca.node_state)
            self.current_time = next_pause

            # Handle output to file
            if self.current_time >= self.next_output:
                self.next_output += self.output_interval

            # Handle plotting on display
            if self.current_time >= self.next_plot:
                if self.save_plots:
                    this_filename = (self.plot_filename
                                     + str(self.plot_iteration).zfill(self.ndigits)
                                     + self.plot_filetype)
                else:
                    this_filename = None
                plot_hill(self.grid, this_filename)
                self.plot_iteration += 1
                self.next_plot += self.plot_interval

            # Handle fault slip
            if self.current_time >= self.next_uplift:
                self.uplifter.do_offset(ca=self.ca,
                                        current_time=self.current_time,
                                        rock_state=8)
#                for i in range(self.grid.number_of_links):
#                    if self.grid.status_at_link[i] == 4 and self.ca.next_trn_id[i] != -1:
#                        print((i, self.ca.next_trn_id[i]))
#                        print((self.grid.x_of_node[self.grid.node_at_link_tail[i]]))
#                        print((self.grid.y_of_node[self.grid.node_at_link_tail[i]]))
#                        print((self.grid.x_of_node[self.grid.node_at_link_head[i]]))
#                        print((self.grid.y_of_node[self.grid.node_at_link_head[i]]))
                self.next_uplift += self.uplift_interval

            # Handle baselevel rise
            if self.current_time >= self.next_baselevel:
                self.raise_baselevel(self.baselevel_row)
                self.baselevel_row += 1
                self.next_baselevel += self.baselevel_rise_interval

    def run(self, to=None):
        """Run the model."""
        if to is None:
            to = self.run_duration
        self.update_until(to)

    def raise_baselevel(self, baselevel_row):
        """Raise baselevel on left by closing a node on the left boundary.

        Parameters
        ----------
        baselevel_row : int
            Row number of the next left-boundary node to be closed

        Examples
        --------
        >>> params = { 'grid_size' : (10,5), 'baselevel_rise_interval' : 1.0 }
        >>> params['run_duration'] = 10.0
        >>> params['uplift_interval'] = 11.0
        >>> gfs = GrainFacetSimulator(**params)
        >>> gfs.run()
        Current sim time 0.0 (0.0%)
        Running to...1.0
        Running to...2.0
        Running to...3.0
        Running to...4.0
        Running to...5.0
        Running to...6.0
        Running to...7.0
        Running to...8.0
        Running to...9.0
        Running to...10.0
        >>> gfs.ca.node_state[:50:5]
        array([8, 8, 8, 8, 8, 8, 8, 8, 8, 8])
        """
        baselevel_node = self.grid.number_of_node_columns * baselevel_row
        if baselevel_node < self.grid.number_of_nodes:
            self.ca.node_state[baselevel_node] = 8
            self.ca.bnd_lnk[self.grid.links_at_node[baselevel_node]] = True
            self.grid.status_at_node[baselevel_node] = self.grid.BC_NODE_IS_CLOSED

    def nodes_in_column(self, col, num_rows, num_cols):
        """Return array of node IDs in given column.

        Examples
        --------
        >>> gfs = GrainFacetSimulator((3, 5))
        >>> gfs.nodes_in_column(1, 3, 5)
        array([ 3,  8, 13])
        >>> gfs.nodes_in_column(4, 3, 5)
        array([ 2,  7, 12])
        >>> gfs = GrainFacetSimulator((3, 6))
        >>> gfs.nodes_in_column(3, 3, 6)
        array([ 4, 10, 16])
        >>> gfs.nodes_in_column(4, 3, 6)
        array([ 2,  8, 14])
        """
        base_node = (col // 2) + (col % 2) * ((num_cols + 1) // 2)
        num_nodes = num_rows * num_cols
        return np.arange(base_node, num_nodes, num_cols)

    def get_profile_and_soil_thickness(self):
        """Calculate and return the topographic profile and the regolith
        thickness."""
        nr = self.ca.grid.number_of_node_rows
        nc = self.ca.grid.number_of_node_columns
        data = self.ca.node_state

        elev = np.zeros(nc)
        soil = np.zeros(nc)
        for c in range(nc):
            e = (c%2)/2.0
            s = 0
            r = 0
            while r<nr and data[c*nr+r]!=0:
                e+=1
                if data[c*nr+r]==7:
                    s+=1
                r+=1
            elev[c] = e
            soil[c] = s
        return elev, soil

    def report_info_for_debug(self, current_time):
        """Print out various bits of data, for testing and debugging."""
        print('\n Current time: ' + str(current_time))
        print('Node state:')
        print(self.ca.node_state)
        for lnk in range(self.grid.number_of_links):
            if self.grid.status_at_link[lnk] == 0:
                print((lnk, self.grid.node_at_link_tail[lnk],
                       self.grid.node_at_link_head[lnk],
                       self.ca.node_state[self.grid.node_at_link_tail[lnk]],
                       self.ca.node_state[self.grid.node_at_link_head[lnk]],
                       self.ca.link_state[lnk],self.ca.next_update[lnk],
                       self.ca.next_trn_id[lnk]))
        print('PQ:')
        print(self.ca.priority_queue._queue)

    def plot_to_file(self):
        """Plot profile of hill to file."""
        fname = self.plot_file_name + str(self.plot_number).zfill(4) + '.png'
        plot_hill(self.ca.grid, filename=fname)
        self.plot_number += 1
Beispiel #3
0
class GrainFacet(CTSModel):
    """
    Model hillslope evolution with block uplift.
    """
    def __init__(self, grid_size, report_interval=1.0e8, run_duration=1.0,
                 output_interval=1.0e99, settling_rate=2.2e8,
                 disturbance_rate=1.0, weathering_rate=1.0,
                 uplift_interval=1.0, plot_interval=1.0e99, friction_coef=0.3,
                 fault_x=1.0,
                 rock_state_for_uplift=7, opt_rock_collapse=False,
                 show_plots=True, initial_state_grid=None,
                 opt_track_grains=False, prop_data=None,
                 prop_reset_value=None, callback_fn=None, **kwds):
        """Call the initialize() method."""
        self.initializer(grid_size, report_interval, run_duration,
                        output_interval, settling_rate, disturbance_rate,
                        weathering_rate, uplift_interval, plot_interval,
                        friction_coef, fault_x, rock_state_for_uplift,
                        opt_rock_collapse, show_plots, initial_state_grid,
                        opt_track_grains, prop_data, prop_reset_value,
                        callback_fn, **kwds)

    def initializer(self, grid_size, report_interval, run_duration,
                   output_interval, settling_rate, disturbance_rate,
                   weathering_rate, uplift_interval, plot_interval,
                   friction_coef, fault_x, rock_state_for_uplift, opt_rock_collapse,
                   show_plots, initial_state_grid, opt_track_grains, prop_data,
                   prop_reset_value, callback_fn, **kwds):
        """Initialize the grain hill model."""
        self.settling_rate = settling_rate
        self.disturbance_rate = disturbance_rate
        self.weathering_rate = weathering_rate
        self.uplift_interval = uplift_interval
        self.plot_interval = plot_interval
        self.friction_coef = friction_coef
        self.rock_state = rock_state_for_uplift  # 7 (resting sed) or 8 (rock)
        self.opt_track_grains = opt_track_grains
        self.callback_fn = callback_fn
        if opt_rock_collapse:
            self.collapse_rate = self.settling_rate
        else:
            self.collapse_rate = 0.0

        # Call base class init
        super(GrainFacet, self).initialize(grid_size=grid_size,
                                           report_interval=report_interval,
                                           grid_orientation='vertical',
                                           grid_shape='rect',
                                           show_plots=show_plots,
                                           cts_type='oriented_hex',
                                           run_duration=run_duration,
                                           output_interval=output_interval,
                                           initial_state_grid=initial_state_grid,
                                           prop_data=prop_data,
                                           prop_reset_value=prop_reset_value,
                                           **kwds)

        # Close top and right edges so as to avoid boundary bug issue
        for edge in (self.grid.nodes_at_right_edge,
                     self.grid.nodes_at_top_edge):
            self.grid.status_at_node[edge] = CLOSED_BOUNDARY

        # Set some things related to property-swapping and/or callback fn
        # if the user wants to track grain motion.
        #if opt_track_grains:
        #    propid = self.ca.propid
        #else:
        #    propid = None

        self.uplifter = LatticeNormalFault(fault_x_intercept=fault_x,
                                           grid=self.grid,
                                           node_state=self.grid.at_node['node_state'],
                                           propid=self.ca.propid,
                                           prop_data=self.ca.prop_data,
                                           prop_reset_value=self.ca.prop_reset_value)

        self.initialize_timing(output_interval, plot_interval, uplift_interval,
                               report_interval)

    def initialize_timing(self, output_interval, plot_interval,
                          uplift_interval, report_interval):
        """Set up variables related to timing of uplift, output, reporting"""

        self.current_time = 0.0

        # Next time for output to file
        self.next_output = output_interval

        # Next time for a plot
        if self._show_plots:
            self.next_plot = plot_interval
        else:
            self.next_plot = self.run_duration + 1

        # Next time for a progress report to user
        self.next_report = report_interval

        # Next time to add baselevel adjustment
        self.next_uplift = uplift_interval

        # Iteration numbers, for output files
        self.output_iteration = 1

    def node_state_dictionary(self):
        """
        Create and return dict of node states.

        Overrides base-class method. Here, we simply call on a function in
        the lattice_grain module.
        """
        return lattice_grain_node_states()

    def transition_list(self):
        """
        Make and return list of Transition object.
        """
        xn_list = lattice_grain_transition_list(g=self.settling_rate,
                                                f=self.friction_coef,
                                                motion=self.settling_rate,
                                                swap=self.opt_track_grains,
                                                callback=self.callback_fn)
        xn_list = self.add_weathering_and_disturbance_transitions(xn_list,
                    self.disturbance_rate, self.weathering_rate,
                    collapse_rate=self.collapse_rate)
        return xn_list

    def add_weathering_and_disturbance_transitions(self, xn_list, d=0.0, w=0.0,
                                                   collapse_rate=0.0,
                                                   swap=False, callback=None):
        """
        Add transition rules representing weathering and/or grain disturbance
        to the list, and return the list.

        Parameters
        ----------
        xn_list : list of Transition objects
            List of objects that encode information about the link-state
            transitions. Normally should first be initialized with lattice-grain
            transition rules, then passed to this function to add rules for
            weathering and disturbance.
        d : float (optional)
            Rate of transition (1/time) from fluid / resting grain pair to
            mobile-grain / fluid pair, representing grain disturbance.
        w : float (optional)
            Rate of transition (1/time) from fluid / rock pair to
            fluid / resting-grain pair, representing weathering.

        Returns
        -------
        xn_list : list of Transition objects
            Modified transition list.
        """

        # Disturbance rule
        if d > 0.0:
            xn_list.append( Transition((7,0,0), (0,1,0), d, 'disturbance', swap, callback) )
            xn_list.append( Transition((7,0,1), (0,2,1), d, 'disturbance', swap, callback) )
            xn_list.append( Transition((7,0,2), (0,3,2), d, 'disturbance', swap, callback) )
            xn_list.append( Transition((0,7,0), (4,0,0), d, 'disturbance', swap, callback) )
            xn_list.append( Transition((0,7,1), (5,0,1), d, 'disturbance', swap, callback) )
            xn_list.append( Transition((0,7,2), (6,0,2), d, 'disturbance', swap, callback) )

        # Weathering rule
        if w > 0.0:
            xn_list.append( Transition((8,0,0), (7,0,0), w, 'weathering') )
            xn_list.append( Transition((8,0,1), (7,0,1), w, 'weathering') )
            xn_list.append( Transition((8,0,2), (7,0,2), w, 'weathering') )
            xn_list.append( Transition((0,8,0), (0,7,0), w, 'weathering') )
            xn_list.append( Transition((0,8,1), (0,7,1), w, 'weathering') )
            xn_list.append( Transition((0,8,2), (0,7,2), w, 'weathering') )

            # "Vertical rock collapse" rule: a rock particle overlying air
            # will collapse, transitioning to a downward-moving grain
            if collapse_rate > 0.0:
                xn_list.append( Transition((0,8,0), (4,0,0), collapse_rate,
                                           'rock collapse', swap, callback))

        if _DEBUG:
            print()
            print('setup_transition_list(): list has ' + str(len(xn_list))
                  + ' transitions:')
            for t in xn_list:
                print('  From state ' + str(t.from_state) + ' to state '
                      + str(t.to_state) + ' at rate ' + str(t.rate)
                      + ' called ' + str(t.name))

        return xn_list

    def initialize_node_state_grid(self):
        """Set up initial node states.

        Examples
        --------
        >>> gh = GrainFacet((5, 7))
        >>> gh.grid.at_node['node_state']
        array([8, 7, 7, 8, 7, 7, 7, 0, 7, 7, 0, 7, 7, 7, 0, 0, 0, 0, 0, 0, 0, 0,
               0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
        """

        # For shorthand, get a reference to the node-state grid
        nsg = self.grid.at_node['node_state']

        # Fill the bottom two rows with grains
        right_side_x = 0.866025403784 * (self.grid.number_of_node_columns - 1)
        for i in range(self.grid.number_of_nodes):
            if self.grid.node_y[i] < 2.0:
                if (self.grid.node_x[i] > 0.0 and
                    self.grid.node_x[i] < right_side_x):
                    nsg[i] = 7

        # Place "wall" particles in the lower-left and lower-right corners
        if self.grid.number_of_node_columns % 2 == 0:
            bottom_right = self.grid.number_of_node_columns - 1
        else:
            bottom_right = self.grid.number_of_node_columns // 2
        nsg[0] = 8  # bottom left
        nsg[bottom_right] = 8

        return nsg

    def run(self, to=None):
        """Run the model."""
        if to is None:
            run_to = self.run_duration
        else:
            run_to = to

        while self.current_time < run_to:

            # Figure out what time to run to this iteration
            next_pause = min(self.next_output, self.next_plot)
            next_pause = min(next_pause, self.next_uplift)
            next_pause = min(next_pause, run_to)

            # Once in a while, print out simulation and real time to let the user
            # know that the sim is running ok
            current_real_time = time.time()
            if current_real_time >= self.next_report:
                print('Current sim time' + str(self.current_time) + '(' + \
                      str(100 * self.current_time / self.run_duration) + '%)')
                self.next_report = current_real_time + self.report_interval

            # Run until next pause
            self.ca.run(next_pause, self.ca.node_state)
            self.current_time = next_pause

            # Handle output to file
            if self.current_time >= self.next_output:
                self.write_output(self.grid, 'grain_hill_model',
                                  self.output_iteration)
                self.output_iteration += 1
                self.next_output += self.output_interval

            # Handle plotting on display
            if self._show_plots and self.current_time >= self.next_plot:
                self.ca_plotter.update_plot()
                axis('off')
                self.next_plot += self.plot_interval

            # Handle uplift
            if self.current_time >= self.next_uplift:
                self.uplifter.do_offset(
                    ca=self.ca, current_time=self.current_time,
                    rock_state=self.rock_state)
                self.next_uplift += self.uplift_interval

    def get_profile_and_soil_thickness(self, grid, data):
        """Calculate and return profiles of elevation and soil thickness.

        Examples
        --------
        >>> from landlab import HexModelGrid
        >>> hg = HexModelGrid(4, 5, shape='rect', orientation='vert')
        >>> ns = hg.add_zeros('node', 'node_state', dtype=int)
        >>> ns[[0, 3, 1, 6, 4, 9, 2]] = 8
        >>> ns[[8, 13, 11, 16, 14]] = 7
        >>> gh = GrainHill((3, 7))  # grid size arbitrary here
        >>> (elev, thickness) = gh.get_profile_and_soil_thickness(hg, ns)
        >>> elev
        array([0. , 2.5, 3. , 2.5, 0. ])
        >>> thickness
        array([0., 2., 2., 1., 0.])
        """
        nc = grid.number_of_node_columns
        elev = zeros(nc)
        soil = zeros(nc)
        for col in range(nc):
            states = data[grid.nodes[:, col]]
            (rows_with_rock_or_sed, ) = where(states > 0)
            if len(rows_with_rock_or_sed) == 0:
                elev[col] = 0.0
            else:
                elev[col] = amax(rows_with_rock_or_sed) + 0.5 * (col % 2)
            soil[col] = count_nonzero(logical_and(states > 0, states < 8))

        return elev, soil
Beispiel #4
0
class GrainFacetSimulator(CTSModel):
    """
    Model facet-slope evolution with 60-degree normal-fault slip.
    """
    def __init__(self,
                 grid_size,
                 report_interval=1.0e8,
                 run_duration=1.0,
                 output_interval=1.0e99,
                 disturbance_rate=1.0e-6,
                 weathering_rate=1.0e-6,
                 uplift_interval=1.0,
                 plot_interval=1.0e99,
                 friction_coef=0.3,
                 fault_x=1.0,
                 **kwds):
        """Call the initialize() method."""
        self.initialize(grid_size, report_interval, run_duration,
                        output_interval, disturbance_rate, weathering_rate,
                        uplift_interval, plot_interval, friction_coef, fault_x,
                        **kwds)

    def initialize(self, grid_size, report_interval, run_duration,
                   output_interval, disturbance_rate, weathering_rate,
                   uplift_interval, plot_interval, friction_coef, fault_x,
                   **kwds):
        """Initialize the grain hill model."""
        self.disturbance_rate = disturbance_rate
        self.weathering_rate = weathering_rate
        self.uplift_interval = uplift_interval
        self.plot_interval = plot_interval
        self.friction_coef = friction_coef

        # Call base class init
        super(GrainFacetSimulator,
              self).initialize(grid_size=grid_size,
                               report_interval=report_interval,
                               grid_orientation='vertical',
                               grid_shape='rect',
                               show_plots=True,
                               cts_type='oriented_hex',
                               run_duration=run_duration,
                               output_interval=output_interval,
                               plot_every_transition=False)

        ns = self.grid.at_node['node_state']
        self.uplifter = LatticeNormalFault(fault_x_intercept=fault_x,
                                           grid=self.grid,
                                           node_state=ns)

    def node_state_dictionary(self):
        """
        Create and return dict of node states.
        
        Overrides base-class method. Here, we simply call on a function in
        the lattice_grain module.
        """
        return lattice_grain_node_states()

    def transition_list(self):
        """
        Make and return list of Transition object.
        """
        xn_list = lattice_grain_transition_list(g=1.0, f=self.friction_coef)
        xn_list = self.add_weathering_and_disturbance_transitions(
            xn_list, self.disturbance_rate, self.weathering_rate)
        return xn_list

    def add_weathering_and_disturbance_transitions(self,
                                                   xn_list,
                                                   d=0.0,
                                                   w=0.0):
        """
        Add transition rules representing weathering and/or grain disturbance
        to the list, and return the list.
        
        Parameters
        ----------
        xn_list : list of Transition objects
            List of objects that encode information about the link-state 
            transitions. Normally should first be initialized with lattice-grain
            transition rules, then passed to this function to add rules for
            weathering and disturbance.
        d : float (optional)
            Rate of transition (1/time) from fluid / resting grain pair to
            mobile-grain / fluid pair, representing grain disturbance.
        w : float (optional)
            Rate of transition (1/time) from fluid / rock pair to
            fluid / resting-grain pair, representing weathering.
        
        
        Returns
        -------
        xn_list : list of Transition objects
            Modified transition list.
        """

        # Disturbance rule
        xn_list.append(Transition((7, 0, 0), (0, 1, 0), d, 'disturbance'))
        xn_list.append(Transition((7, 0, 1), (0, 2, 1), d, 'disturbance'))
        xn_list.append(Transition((7, 0, 2), (0, 3, 2), d, 'disturbance'))
        xn_list.append(Transition((0, 7, 0), (4, 0, 0), d, 'disturbance'))
        xn_list.append(Transition((0, 7, 1), (5, 0, 1), d, 'disturbance'))
        xn_list.append(Transition((0, 7, 2), (6, 0, 2), d, 'disturbance'))

        # Weathering rule
        xn_list.append(Transition((8, 0, 0), (7, 0, 0), w, 'weathering'))
        xn_list.append(Transition((8, 0, 1), (7, 0, 1), w, 'weathering'))
        xn_list.append(Transition((8, 0, 2), (7, 0, 2), w, 'weathering'))
        xn_list.append(Transition((0, 8, 0), (0, 7, 0), w, 'weathering'))
        xn_list.append(Transition((0, 8, 1), (0, 7, 1), w, 'weathering'))
        xn_list.append(Transition((0, 8, 2), (0, 7, 2), w, 'weathering'))

        if _DEBUG:
            print
            print 'setup_transition_list(): list has', len(
                xn_list), 'transitions:'
            for t in xn_list:
                print '  From state', t.from_state, 'to state', t.to_state, 'at rate', t.rate, 'called', t.name

        return xn_list

    def initialize_node_state_grid(self):
        """Set up initial node states.
        
        Examples
        --------
        >>> gh = GrainHill((5, 7))
        >>> gh.grid.at_node['node_state']        
        array([8, 7, 7, 8, 7, 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
               0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
        """

        # For shorthand, get a reference to the node-state grid
        nsg = self.grid.at_node['node_state']

        # Fill the bottom two rows with grains
        right_side_x = 0.866025403784 * (self.grid.number_of_node_columns - 1)
        for i in range(self.grid.number_of_nodes):
            if self.grid.node_y[i] < 1.0:
                if (self.grid.node_x[i] > 0.0
                        and self.grid.node_x[i] < right_side_x):
                    nsg[i] = 7

        # Place "wall" particles in the lower-left and lower-right corners
        if self.grid.number_of_node_columns % 2 == 0:
            bottom_right = self.grid.number_of_node_columns - 1
        else:
            bottom_right = self.grid.number_of_node_columns // 2
        nsg[0] = 8  # bottom left
        nsg[bottom_right] = 8

        return nsg

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

        # Work out the next times to plot and output
        next_output = self.output_interval
        next_plot = self.plot_interval

        # Next time for a progress report to user
        next_report = self.report_interval

        # And baselevel adjustment
        next_uplift = self.uplift_interval

        current_time = 0.0
        while current_time < self.run_duration:

            # Figure out what time to run to this iteration
            next_pause = min(next_output, next_plot)
            next_pause = min(next_pause, next_uplift)
            next_pause = min(next_pause, self.run_duration)

            # Once in a while, print out simulation and real time to let the user
            # know that the sim is running ok
            current_real_time = time.time()
            if current_real_time >= next_report:
                print('Current sim time' + str(current_time) + '(' + \
                      str(100 * current_time / self.run_duration) + '%)')
                next_report = current_real_time + self.report_interval

            # Run the model forward in time until the next output step
            print('Running to...' + str(next_pause))
            self.ca.run(next_pause, self.ca.node_state)  #,
            #plot_each_transition=plot_every_transition, plotter=ca_plotter)
            current_time = next_pause

            # Handle output to file
            if current_time >= next_output:
                #write_output(hmg, filenm, output_iteration)
                #output_iteration += 1
                next_output += self.output_interval

            # Handle plotting on display
            if current_time >= next_plot:
                #node_state_grid[hmg.number_of_node_rows-1] = 8
                self.ca_plotter.update_plot()
                axis('off')
                next_plot += self.plot_interval

            # Handle fault slip
            if current_time >= next_uplift:
                self.uplifter.do_offset(rock_state=8)
                self.ca.update_link_states_and_transitions(current_time)
                next_uplift += self.uplift_interval

    def nodes_in_column(self, col, num_rows, num_cols):
        """Return array of node IDs in given column.
        
        Examples
        --------
        >>> gfs = GrainFacetSimulator((3, 5))
        >>> gfs.nodes_in_column(1, 3, 5)
        array([ 3,  8, 13])
        >>> gfs.nodes_in_column(4, 3, 5)
        array([ 2,  7, 12])
        >>> gfs = GrainFacetSimulator((3, 6))
        >>> gfs.nodes_in_column(3, 3, 6)
        array([ 4, 10, 16])
        >>> gfs.nodes_in_column(4, 3, 6)
        array([ 2,  8, 14])
        """
        base_node = (col // 2) + (col % 2) * ((num_cols + 1) // 2)
        num_nodes = num_rows * num_cols
        return np.arange(base_node, num_nodes, num_cols)

    def get_profile_and_soil_thickness(self):
        """Calculate and return the topographic profile and the regolith
        thickness."""
        nr = self.ca.grid.number_of_node_rows
        nc = self.ca.grid.number_of_node_columns
        data = self.ca.node_state

        elev = np.zeros(nc)
        soil = np.zeros(nc)
        for c in range(nc):
            e = (c % 2) / 2.0
            s = 0
            r = 0
            while r < nr and data[c * nr + r] != 0:
                e += 1
                if data[c * nr + r] == 7:
                    s += 1
                r += 1
            elev[c] = e
            soil[c] = s
        return elev, soil
class GrainFacetSimulator(CTSModel):
    """
    Model facet-slope evolution with 60-degree normal-fault slip.
    """
    def __init__(self, grid_size, report_interval=1.0e8, run_duration=1.0, 
                 output_interval=1.0e99, disturbance_rate=0.0,
                 weathering_rate=0.0, dissolution_rate=0.0,
                 uplift_interval=1.0, baselevel_rise_interval=0,
                 plot_interval=1.0e99, friction_coef=0.3,
                 fault_x=1.0, cell_width=1.0, grav_accel=9.8,
                 plot_file_name=None, **kwds):
        """Call the initialize() method."""
        self.initialize(grid_size, report_interval, run_duration,
                        output_interval, disturbance_rate, weathering_rate,
                        dissolution_rate, uplift_interval, 
                        baselevel_rise_interval, plot_interval, friction_coef,
                        fault_x,cell_width, grav_accel, plot_file_name, **kwds)

    def initialize(self, grid_size, report_interval, run_duration,
                   output_interval, disturbance_rate, weathering_rate, 
                   dissolution_rate, uplift_interval, baselevel_rise_interval,
                   plot_interval, friction_coef, fault_x, cell_width,
                   grav_accel, plot_file_name=None, **kwds):
        """Initialize the grain hill model."""
        self.disturbance_rate = disturbance_rate
        self.weathering_rate = weathering_rate
        self.dissolution_rate = dissolution_rate
        self.uplift_interval = uplift_interval
        self.baselevel_rise_interval = baselevel_rise_interval
        self.plot_interval = plot_interval
        self.friction_coef = friction_coef

        self.settling_rate = calculate_settling_rate(cell_width, grav_accel)

        # Call base class init
        super(GrainFacetSimulator, self).initialize(grid_size=grid_size, 
                                          report_interval=report_interval, 
                                          grid_orientation='vertical',
                                          grid_shape='rect',
                                          show_plots=True,
                                          cts_type='oriented_hex',
                                          run_duration=run_duration,
                                          output_interval=output_interval,
                                          plot_every_transition=False,
                                          closed_boundaries=(True, True,
                                                             False, False))

        ns = self.grid.at_node['node_state']
        self.uplifter = LatticeNormalFault(fault_x_intercept=fault_x,
                                           grid=self.grid, 
                                           node_state=ns)

        self.plot_file_name = plot_file_name
        if plot_file_name is not None:
            self.plot_number = 0
            self.plot_to_file()

    def node_state_dictionary(self):
        """
        Create and return dict of node states.

        Overrides base-class method. Here, we simply call on a function in
        the lattice_grain module.
        """
        return lattice_grain_node_states()

    def transition_list(self):
        """
        Make and return list of Transition object.
        """
        xn_list = lattice_grain_transition_list(g=self.settling_rate,
                                                f=self.friction_coef,
                                                motion=self.settling_rate)
        xn_list = self.add_weathering_and_disturbance_transitions(xn_list,
                    self.disturbance_rate, self.weathering_rate,
                    self.dissolution_rate)
        return xn_list

    def add_weathering_and_disturbance_transitions(self, xn_list, d=0.0, w=0.0,
                                                   diss=0.0):
        """
        Add transition rules representing weathering and/or grain disturbance
        to the list, and return the list.

        Parameters
        ----------
        xn_list : list of Transition objects
            List of objects that encode information about the link-state 
            transitions. Normally should first be initialized with lattice-grain
            transition rules, then passed to this function to add rules for
            weathering and disturbance.
        d : float (optional, default=0.0)
            Rate of transition (1/time) from fluid / resting grain pair to
            mobile-grain / fluid pair, representing grain disturbance.
        w : float (optional, default=0.0)
            Rate of transition (1/time) from fluid / rock pair to
            fluid / resting-grain pair, representing weathering.
        diss : float (optional, default=0.0)
            Dissolution: rate of transition from fluid / rock pair to 
            fluid / fluid pair.

        Returns
        -------
        xn_list : list of Transition objects
            Modified transition list.
        """

        # Disturbance rule
        if d > 0.0:
            xn_list.append( Transition((7,0,0), (0,1,0), d, 'disturbance') )
            xn_list.append( Transition((7,0,1), (0,2,1), d, 'disturbance') )
            xn_list.append( Transition((7,0,2), (0,3,2), d, 'disturbance') )
            xn_list.append( Transition((0,7,0), (4,0,0), d, 'disturbance') )
            xn_list.append( Transition((0,7,1), (5,0,1), d, 'disturbance') )
            xn_list.append( Transition((0,7,2), (6,0,2), d, 'disturbance') )

        # Weathering rule
        if w > 0.0:
            xn_list.append( Transition((8,0,0), (7,0,0), w, 'weathering') )
            xn_list.append( Transition((8,0,1), (7,0,1), w, 'weathering') )
            xn_list.append( Transition((8,0,2), (7,0,2), w, 'weathering') )
            xn_list.append( Transition((0,8,0), (0,7,0), w, 'weathering') )
            xn_list.append( Transition((0,8,1), (0,7,1), w, 'weathering') )
            xn_list.append( Transition((0,8,2), (0,7,2), w, 'weathering') )

        # Dissolution rule
        if diss > 0.0:
            xn_list.append( Transition((8,0,0), (0,0,0), diss, 'dissolution') )
            xn_list.append( Transition((8,0,1), (0,0,1), diss, 'dissolution') )
            xn_list.append( Transition((8,0,2), (0,0,2), diss, 'dissolution') )
            xn_list.append( Transition((0,8,0), (0,0,0), diss, 'dissolution') )
            xn_list.append( Transition((0,8,1), (0,0,1), diss, 'dissolution') )
            xn_list.append( Transition((0,8,2), (0,0,2), diss, 'dissolution') )

        if _DEBUG:
            print('')
            print('setup_transition_list(): list has ' + str(len(xn_list))
                  + ' transitions:')
            for t in xn_list:
                print('  From state ' + str(t.from_state) + ' to state '
                      + str(t.to_state) + ' at rate ' + str(t.rate) + 'called'
                      + str(t.name))

        return xn_list

    def initialize_node_state_grid(self):
        """Set up initial node states.

        Examples
        --------
        >>> from grainhill import GrainHill
        >>> gh = GrainHill((5, 7))
        >>> gh.grid.at_node['node_state'][:20] 
        array([8, 7, 7, 8, 7, 7, 7, 0, 7, 7, 0, 7, 7, 7, 0, 0, 0, 0, 0, 0])
        """

        # For shorthand, get a reference to the node-state grid
        nsg = self.grid.at_node['node_state']

        # Fill the bottom two rows with grains
        right_side_x = 0.866025403784 * (self.grid.number_of_node_columns - 1)
        for i in range(self.grid.number_of_nodes):
            if self.grid.node_y[i] < 2.0:
                if (self.grid.node_x[i] > 0.0 and
                    self.grid.node_x[i] < right_side_x):
                    nsg[i] = 8

        # Place "wall" particles in the lower-left and lower-right corners
        if self.grid.number_of_node_columns % 2 == 0:
            bottom_right = self.grid.number_of_node_columns - 1
        else:
            bottom_right = self.grid.number_of_node_columns // 2
        nsg[0] = 8  # bottom left
        nsg[bottom_right] = 8

        return nsg

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

        # Work out the next times to plot and output
        next_output = self.output_interval
        next_plot = self.plot_interval

        # Next time for a progress report to user
        next_report = self.report_interval

        # And baselevel adjustment
        next_uplift = self.uplift_interval
        if self.baselevel_rise_interval > 0:
            next_baselevel = self.baselevel_rise_interval
            baselevel_row = 1
        else:
            next_baselevel = self.run_duration + 1

        current_time = 0.0
        while current_time < self.run_duration:

            # Figure out what time to run to this iteration
            next_pause = min(next_output, next_plot)
            next_pause = min(next_pause, next_uplift)
            next_pause = min(next_pause, next_baselevel)
            next_pause = min(next_pause, self.run_duration)

            # Once in a while, print out simulation and real time to let the user
            # know that the sim is running ok
            current_real_time = time.time()
            if current_real_time >= next_report:
                print('Current sim time' + str(current_time) + '(' + \
                      str(100 * current_time / self.run_duration) + '%)')
                next_report = current_real_time + self.report_interval

            # Run the model forward in time until the next output step
            print('Running to...' + str(next_pause))
            self.ca.run(next_pause, self.ca.node_state)
            current_time = next_pause

            # Handle output to file
            if current_time >= next_output:
                next_output += self.output_interval

            # Handle plotting on display
            if current_time >= next_plot:
                self.ca_plotter.update_plot()
                if self.plot_file_name is not None:
                    self.plot_to_file()
                next_plot += self.plot_interval

            # Handle fault slip
            if current_time >= next_uplift:
                self.uplifter.do_offset(ca=self.ca, current_time=current_time,
                                        rock_state=8)
                for i in range(self.grid.number_of_links):
                    if self.grid.status_at_link[i] == 4 and self.ca.next_trn_id[i] != -1:
                        print i
                next_uplift += self.uplift_interval

            # Handle baselevel rise
            if current_time >= next_baselevel:
                self.raise_baselevel(baselevel_row)
                baselevel_row += 1
                next_baselevel += self.baselevel_rise_interval

    def raise_baselevel(self, baselevel_row):
        """Raise baselevel on left by closing a node on the left boundary.
        
        Parameters
        ----------
        baselevel_row : int
            Row number of the next left-boundary node to be closed
        
        Examples
        --------
        >>> params = { 'grid_size' : (10,5), 'baselevel_rise_interval' : 1.0 }
        >>> params['run_duration'] = 10.0
        >>> params['uplift_interval'] = 11.0
        >>> gfs = GrainFacetSimulator(**params)
        >>> gfs.run()
        Current sim time0.0(0.0%)
        Running to...1.0
        Running to...2.0
        Running to...3.0
        Running to...4.0
        Running to...5.0
        Running to...6.0
        Running to...7.0
        Running to...8.0
        Running to...9.0
        Running to...10.0
        >>> gfs.ca.node_state[:50:5]
        array([8, 8, 8, 8, 8, 8, 8, 8, 8, 8])
        """
        baselevel_node = self.grid.number_of_node_columns * baselevel_row
        if baselevel_node < self.grid.number_of_nodes:
            self.ca.node_state[baselevel_node] = 8
            self.ca.bnd_lnk[self.grid.links_at_node[baselevel_node]] = True
            self.grid.status_at_node[baselevel_node] = CLOSED_BOUNDARY

    def nodes_in_column(self, col, num_rows, num_cols):
        """Return array of node IDs in given column.
        
        Examples
        --------
        >>> gfs = GrainFacetSimulator((3, 5))
        >>> gfs.nodes_in_column(1, 3, 5)
        array([ 3,  8, 13])
        >>> gfs.nodes_in_column(4, 3, 5)
        array([ 2,  7, 12])
        >>> gfs = GrainFacetSimulator((3, 6))
        >>> gfs.nodes_in_column(3, 3, 6)
        array([ 4, 10, 16])
        >>> gfs.nodes_in_column(4, 3, 6)
        array([ 2,  8, 14])
        """
        base_node = (col // 2) + (col % 2) * ((num_cols + 1) // 2)
        num_nodes = num_rows * num_cols
        return np.arange(base_node, num_nodes, num_cols)

    def get_profile_and_soil_thickness(self):
        """Calculate and return the topographic profile and the regolith
        thickness."""
        nr = self.ca.grid.number_of_node_rows
        nc = self.ca.grid.number_of_node_columns
        data = self.ca.node_state

        elev = np.zeros(nc)
        soil = np.zeros(nc)
        for c in range(nc):
            e = (c%2)/2.0
            s = 0
            r = 0 
            while r<nr and data[c*nr+r]!=0:
                e+=1
                if data[c*nr+r]==7:
                    s+=1
                r+=1
            elev[c] = e
            soil[c] = s
        return elev, soil

    def report_info_for_debug(self, current_time):
        """Print out various bits of data, for testing and debugging."""
        print('\n Current time: ' + str(current_time))
        print('Node state:')
        print(self.ca.node_state)
        for lnk in range(self.grid.number_of_links):
            if self.grid.status_at_link[lnk] == 0:
                print((lnk, self.grid.node_at_link_tail[lnk], 
                       self.grid.node_at_link_head[lnk],
                       self.ca.node_state[self.grid.node_at_link_tail[lnk]], 
                       self.ca.node_state[self.grid.node_at_link_head[lnk]],
                       self.ca.link_state[lnk],self.ca.next_update[lnk],
                       self.ca.next_trn_id[lnk]))
        print('PQ:')
        print(self.ca.priority_queue._queue)
        
    def plot_to_file(self):
        """Plot profile of hill to file."""
        fname = self.plot_file_name + str(self.plot_number).zfill(4) + '.png'
        plot_hill(self.ca.grid, filename=fname)
        self.plot_number += 1
class GrainFacetSimulator(CTSModel):
    """
    Model facet-slope evolution with 60-degree normal-fault slip.
    """
    def __init__(self,
                 grid_size,
                 report_interval=1.0e8,
                 run_duration=1.0,
                 output_interval=1.0e99,
                 disturbance_rate=0.0,
                 weathering_rate=0.0,
                 dissolution_rate=0.0,
                 uplift_interval=1.0,
                 plot_interval=1.0e99,
                 friction_coef=0.3,
                 fault_x=1.0,
                 cell_width=1.0,
                 grav_accel=9.8,
                 plot_file_name=None,
                 **kwds):
        """Call the initialize() method."""
        self.initialize(grid_size, report_interval, run_duration,
                        output_interval, disturbance_rate, weathering_rate,
                        dissolution_rate, uplift_interval, plot_interval,
                        friction_coef, fault_x, cell_width, grav_accel,
                        plot_file_name, **kwds)

    def initialize(self,
                   grid_size,
                   report_interval,
                   run_duration,
                   output_interval,
                   disturbance_rate,
                   weathering_rate,
                   dissolution_rate,
                   uplift_interval,
                   plot_interval,
                   friction_coef,
                   fault_x,
                   cell_width,
                   grav_accel,
                   plot_file_name=None,
                   **kwds):
        """Initialize the grain hill model."""
        self.disturbance_rate = disturbance_rate
        self.weathering_rate = weathering_rate
        self.dissolution_rate = dissolution_rate
        self.uplift_interval = uplift_interval
        self.plot_interval = plot_interval
        self.friction_coef = friction_coef

        self.settling_rate = calculate_settling_rate(cell_width, grav_accel)

        # Call base class init
        super(GrainFacetSimulator,
              self).initialize(grid_size=grid_size,
                               report_interval=report_interval,
                               grid_orientation='vertical',
                               grid_shape='rect',
                               show_plots=True,
                               cts_type='oriented_hex',
                               run_duration=run_duration,
                               output_interval=output_interval,
                               plot_every_transition=False)

        # Close top and right edges so as to avoid boundary bug issue
        for edge in (self.grid.nodes_at_right_edge,
                     self.grid.nodes_at_top_edge):
            self.grid.status_at_node[edge] = CLOSED_BOUNDARY

        ns = self.grid.at_node['node_state']
        self.uplifter = LatticeNormalFault(fault_x_intercept=fault_x,
                                           grid=self.grid,
                                           node_state=ns)

        self.plot_file_name = plot_file_name
        if plot_file_name is not None:
            self.plot_number = 0
            self.plot_to_file()

    def node_state_dictionary(self):
        """
        Create and return dict of node states.

        Overrides base-class method. Here, we simply call on a function in
        the lattice_grain module.
        """
        return lattice_grain_node_states()

    def transition_list(self):
        """
        Make and return list of Transition object.
        """
        xn_list = lattice_grain_transition_list(g=self.settling_rate,
                                                f=self.friction_coef,
                                                motion=self.settling_rate)
        xn_list = self.add_weathering_and_disturbance_transitions(
            xn_list, self.disturbance_rate, self.weathering_rate,
            self.dissolution_rate)
        return xn_list

    def add_weathering_and_disturbance_transitions(self,
                                                   xn_list,
                                                   d=0.0,
                                                   w=0.0,
                                                   diss=0.0):
        """
        Add transition rules representing weathering and/or grain disturbance
        to the list, and return the list.

        Parameters
        ----------
        xn_list : list of Transition objects
            List of objects that encode information about the link-state 
            transitions. Normally should first be initialized with lattice-grain
            transition rules, then passed to this function to add rules for
            weathering and disturbance.
        d : float (optional, default=0.0)
            Rate of transition (1/time) from fluid / resting grain pair to
            mobile-grain / fluid pair, representing grain disturbance.
        w : float (optional, default=0.0)
            Rate of transition (1/time) from fluid / rock pair to
            fluid / resting-grain pair, representing weathering.
        diss : float (optional, default=0.0)
            Dissolution: rate of transition from fluid / rock pair to 
            fluid / fluid pair.

        Returns
        -------
        xn_list : list of Transition objects
            Modified transition list.
        """

        # Disturbance rule
        if d > 0.0:
            xn_list.append(Transition((7, 0, 0), (0, 1, 0), d, 'disturbance'))
            xn_list.append(Transition((7, 0, 1), (0, 2, 1), d, 'disturbance'))
            xn_list.append(Transition((7, 0, 2), (0, 3, 2), d, 'disturbance'))
            xn_list.append(Transition((0, 7, 0), (4, 0, 0), d, 'disturbance'))
            xn_list.append(Transition((0, 7, 1), (5, 0, 1), d, 'disturbance'))
            xn_list.append(Transition((0, 7, 2), (6, 0, 2), d, 'disturbance'))

        # Weathering rule
        if w > 0.0:
            xn_list.append(Transition((8, 0, 0), (7, 0, 0), w, 'weathering'))
            xn_list.append(Transition((8, 0, 1), (7, 0, 1), w, 'weathering'))
            xn_list.append(Transition((8, 0, 2), (7, 0, 2), w, 'weathering'))
            xn_list.append(Transition((0, 8, 0), (0, 7, 0), w, 'weathering'))
            xn_list.append(Transition((0, 8, 1), (0, 7, 1), w, 'weathering'))
            xn_list.append(Transition((0, 8, 2), (0, 7, 2), w, 'weathering'))

        # Dissolution rule
        if diss > 0.0:
            xn_list.append(
                Transition((8, 0, 0), (0, 0, 0), diss, 'dissolution'))
            xn_list.append(
                Transition((8, 0, 1), (0, 0, 1), diss, 'dissolution'))
            xn_list.append(
                Transition((8, 0, 2), (0, 0, 2), diss, 'dissolution'))
            xn_list.append(
                Transition((0, 8, 0), (0, 0, 0), diss, 'dissolution'))
            xn_list.append(
                Transition((0, 8, 1), (0, 0, 1), diss, 'dissolution'))
            xn_list.append(
                Transition((0, 8, 2), (0, 0, 2), diss, 'dissolution'))

        if _DEBUG:
            print('')
            print('setup_transition_list(): list has ' + str(len(xn_list)) +
                  ' transitions:')
            for t in xn_list:
                print('  From state ' + str(t.from_state) + ' to state ' +
                      str(t.to_state) + ' at rate ' + str(t.rate) + 'called' +
                      str(t.name))

        return xn_list

    def initialize_node_state_grid(self):
        """Set up initial node states.

        Examples
        --------
        >>> from grainhill import GrainHill
        >>> gh = GrainHill((5, 7))
        >>> gh.grid.at_node['node_state']        
        array([8, 7, 7, 8, 7, 7, 7, 0, 7, 7, 0, 7, 7, 7, 0, 0, 0, 0, 0, 0, 0, 0,
               0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
        """

        # For shorthand, get a reference to the node-state grid
        nsg = self.grid.at_node['node_state']

        # Fill the bottom two rows with grains
        right_side_x = 0.866025403784 * (self.grid.number_of_node_columns - 1)
        for i in range(self.grid.number_of_nodes):
            if self.grid.node_y[i] < 2.0:
                if (self.grid.node_x[i] > 0.0
                        and self.grid.node_x[i] < right_side_x):
                    nsg[i] = 8

        # Place "wall" particles in the lower-left and lower-right corners
        if self.grid.number_of_node_columns % 2 == 0:
            bottom_right = self.grid.number_of_node_columns - 1
        else:
            bottom_right = self.grid.number_of_node_columns // 2
        nsg[0] = 8  # bottom left
        nsg[bottom_right] = 8

        return nsg

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

        # Work out the next times to plot and output
        next_output = self.output_interval
        next_plot = self.plot_interval

        # Next time for a progress report to user
        next_report = self.report_interval

        # And baselevel adjustment
        next_uplift = self.uplift_interval

        current_time = 0.0
        while current_time < self.run_duration:

            # Figure out what time to run to this iteration
            next_pause = min(next_output, next_plot)
            next_pause = min(next_pause, next_uplift)
            next_pause = min(next_pause, self.run_duration)

            # Once in a while, print out simulation and real time to let the user
            # know that the sim is running ok
            current_real_time = time.time()
            if current_real_time >= next_report:
                print('Current sim time' + str(current_time) + '(' + \
                      str(100 * current_time / self.run_duration) + '%)')
                next_report = current_real_time + self.report_interval

            # Run the model forward in time until the next output step
            print('Running to...' + str(next_pause))
            self.ca.run(next_pause, self.ca.node_state)
            current_time = next_pause

            # Handle output to file
            if current_time >= next_output:
                next_output += self.output_interval

            # Handle plotting on display
            if current_time >= next_plot:
                self.ca_plotter.update_plot()
                if self.plot_file_name is not None:
                    self.plot_to_file()
                next_plot += self.plot_interval

            # Handle fault slip
            if current_time >= next_uplift:
                self.uplifter.do_offset(ca=self.ca,
                                        current_time=current_time,
                                        rock_state=8)
                next_uplift += self.uplift_interval

    def nodes_in_column(self, col, num_rows, num_cols):
        """Return array of node IDs in given column.
        
        Examples
        --------
        >>> gfs = GrainFacetSimulator((3, 5))
        >>> gfs.nodes_in_column(1, 3, 5)
        array([ 3,  8, 13])
        >>> gfs.nodes_in_column(4, 3, 5)
        array([ 2,  7, 12])
        >>> gfs = GrainFacetSimulator((3, 6))
        >>> gfs.nodes_in_column(3, 3, 6)
        array([ 4, 10, 16])
        >>> gfs.nodes_in_column(4, 3, 6)
        array([ 2,  8, 14])
        """
        base_node = (col // 2) + (col % 2) * ((num_cols + 1) // 2)
        num_nodes = num_rows * num_cols
        return np.arange(base_node, num_nodes, num_cols)

    def get_profile_and_soil_thickness(self):
        """Calculate and return the topographic profile and the regolith
        thickness."""
        nr = self.ca.grid.number_of_node_rows
        nc = self.ca.grid.number_of_node_columns
        data = self.ca.node_state

        elev = np.zeros(nc)
        soil = np.zeros(nc)
        for c in range(nc):
            e = (c % 2) / 2.0
            s = 0
            r = 0
            while r < nr and data[c * nr + r] != 0:
                e += 1
                if data[c * nr + r] == 7:
                    s += 1
                r += 1
            elev[c] = e
            soil[c] = s
        return elev, soil

    def report_info_for_debug(self, current_time):
        """Print out various bits of data, for testing and debugging."""
        print('\n Current time: ' + str(current_time))
        print('Node state:')
        print(self.ca.node_state)
        for lnk in range(self.grid.number_of_links):
            if self.grid.status_at_link[lnk] == 0:
                print((lnk, self.grid.node_at_link_tail[lnk],
                       self.grid.node_at_link_head[lnk],
                       self.ca.node_state[self.grid.node_at_link_tail[lnk]],
                       self.ca.node_state[self.grid.node_at_link_head[lnk]],
                       self.ca.link_state[lnk], self.ca.next_update[lnk],
                       self.ca.next_trn_id[lnk]))
        print('PQ:')
        print(self.ca.priority_queue._queue)

    def plot_to_file(self):
        """Plot profile of hill to file."""
        fname = self.plot_file_name + str(self.plot_number).zfill(4) + '.png'
        plot_hill(self.ca.grid, filename=fname)
        self.plot_number += 1