Module logic

This module contains all the logic required to execute sbelt simulations/runs. All functions/classes are designed for internal use and may change without note.

A primary purpose of this module is to manipulate n-7 NumPy arrays which represent multiple stream particles. In these n-7 arrays, a single particle is represented by a NumPy array with 7 attributes::

[x_location, diam, y_location, UID, active state, age, loop age]

In this general example, x_location and y_location define the centre point of spherical particle whose diameter is defined by diam. See the project docs for more information on the other attributes.

Expand source code
""" 
This module contains all the logic required to execute
sbelt simulations/runs. All functions/classes are designed 
for internal use and may change without note.

A primary purpose of this module is to manipulate n-7 NumPy 
arrays which represent multiple stream particles. In these
n-7 arrays, a _single_ particle is represented by a NumPy array 
with 7 attributes::

    [x_location, diam, y_location, UID, active state, age, loop age]

In this general example, x_location and y_location define the centre point of 
spherical particle whose diameter is defined by diam. See the project docs for
more information on the other attributes. 
"""

import math
import random
import numpy as np


import logging
logging.getLogger(__name__)


class Subregion():
    """ A subregion in the stream.
    
    A Subregion is defined by left (upstream)
    and right (downstream) boundaries. Each
    subregion maintains a NumPy list which is 
    used to record the number of model particles that 
    pass the downstream boundary in a given iteration. 
    For example::

        flux_list = [0,3,2,1]

    Means that 0 crossings happened in the first iteration, 
    3 happened in the 2nd, and so on. The list has 
    length equal to the number of iterations for a model run.
    
    Attributes:
        name: Name of the subregion.
        left_boundary: Location of the left boundary (float).
        right_boundary: Location of the right boundary (float).
        iterations: The number of iterations for the model run.
    """
    def __init__(self, name, left_boundary, right_boundary, iterations):
        self.name = name
        self.left_boundary = left_boundary
        self.right_boundary = right_boundary
        self.flux_list = np.zeros(iterations, dtype=np.int64)
        
    def leftBoundary(self):
        """Returns subregion's left boundary"""
        return self.left_boundary
    
    def rightBoundary(self):
        """Returns subregion's right boundary"""
        return self.right_boundary
    
    def getName(self):
        """Returns subregion's name"""
        return self.name

    def incrementFlux(self, iteration):
        """Increments flux list by 1.

        Args:
            iteration: The iteration/index to increment by 1
        """
        self.flux_list[iteration] += 1
    
    def getFluxList(self):
        """Returns subregion's flux list"""
        return self.flux_list

def get_event_particles(e_events, subregions, model_particles, level_limit, height_dependant=False):
    """ Find and return list of particles to be entrained

    Will loop through each subregion and select n = e_events
    model particles (within a subregion boundaries) at random 
    to be entrained. No particle will be selected twice.

    Args:
        e_events: The number of events requested per subregion (int).
        subregions: Python array of initialized Subregion objects. 
        model_particles: An n-7 NumPy array representing the stream's n 
            model particles.

    Returns:
        event_particles: A NumPy array of k uids representing the model particles
            that have been selected for entrainment. For example::

                [2.0, 5.0, 25.0]

            Will represent that model particles with uids 2.0, 5.0 and
            25.0 have been selected for entrainment. 
    """
    if e_events == 0:
        e_events = 1 #???
    
    event_particles = []
    for subregion in subregions:
        # Take only particles in the boundaries of the current subregion
        subregion_particles = model_particles[
                (model_particles[:,0] >= subregion.leftBoundary())
              & (model_particles[:,0] <= subregion.rightBoundary())]
        # Take only particles that are in-stream (not ghost)
        in_stream_particles = subregion_particles[
                                                subregion_particles[:,0] != -1]
        # Take only particles that are 'active' 
        active_particles =  in_stream_particles[
                                                in_stream_particles[:,4] != 0]

        # Do not take any particles that have been selected for entrainment (i.e do not double select)
        # This only happens when particles rest on the boundary. 
        active_event, active_idx, event_idx = np.intersect1d(active_particles[:,3], event_particles, return_indices=True)
        active_particles = np.delete(active_particles, active_idx, axis=0)

        subregion_event_ids = []  
        if height_dependant: # any particle at the level limit must be entrained
            levels = elevation_list(subregion_particles[:,2], desc=False)
            tip_particles = []
            # find the tip particles -- these are the particles being entrained
            if len(levels) == level_limit: 
                tip_particles = active_particles[active_particles[:,2] == levels[level_limit-1]]
            for particle in tip_particles:
                subregion_event_ids.append(particle[3])
                active_particles = active_particles[active_particles[:,2] != particle[2]]
        # If there are not enough particles in the subregion to sample from, alter the sample size
        if e_events > len(active_particles):
            random_sample = random.sample(range(len(active_particles)), 
                                        len(active_particles))
        else: 
            random_sample = random.sample(range(len(active_particles)), 
                                        e_events)
        # TODO: change so that we don't rely on loop index to grab particle
        for index in random_sample:
            subregion_event_ids.append(int(active_particles[index][3])  )
        
        ghost_particles = np.where(model_particles[:,0] == -1)[0]
        for index in ghost_particles:
            model_particles[index][0] = 0 
            subregion_event_ids.append(index)
        
        if e_events != len(subregion_event_ids):
            msg = (
                     f'Requested {e_events} events in {subregion.getName()} ' 
                     f'but {len(subregion_event_ids)} are occuring'
            )
            logging.info(msg)
        event_particles = event_particles + subregion_event_ids
    event_particles = np.array(event_particles, dtype=np.intp)

    return event_particles

def define_subregions(bed_length, num_subregions, iterations):
    """ Define subregion list for model stream.
    
    Args:
        bed_length: The length of the stream (int). 
        num_subregions: The number of subregions (int).
        iterations: The number of iterations for the model run (int).

    Returns:
        subregions_arr: Python array of initialized Subregion objects. 
    """
    subregion_length = bed_length/num_subregions
    left_boundary = 0.0
    subregions_arr = []
    for region in range(num_subregions):  
        right_boundary = left_boundary + subregion_length   
        subregion = Subregion(f'subregion-{region}', left_boundary, right_boundary, iterations)
        left_boundary = right_boundary
        
        subregions_arr.append(subregion)
    
    return subregions_arr
    
def build_streambed(bed_length, particle_diam):
    """ Builds the array of bed particles.
    
    Args:
        bed_length: The length of the stream (int).
        particle_diam: The diameter of all particles (float).
    
    Returns:
        bed_particles: An m-7 NumPy array representing the stream's m 
            bed particles. For example::

                [[x1, diam, y, uid1, active, age, loops], ... 
                            ,[xM, diam, y, uidM, active, age, loops]]
            
            Where all bed particles share the same diam (diam1=...=diamM) and 
            y (y1=...=yM), all uids are negative, and to represent 'static-ness'
            active = 0, age = 0, and loops = 0. 

    """
    max_particles = int(math.ceil( bed_length / particle_diam ))
    bed_particles = np.zeros([max_particles, 7],dtype=float)
    
    particle_id = -1
    centre = (particle_diam/2)  
    state = 0
    age = 0
    loop_age = 0
    elevation = 0
    while not bed_complete(centre, bed_length):  
        # index with negative indices... bed particles are built from the final element to the first
        bed_particles[particle_id] = [centre, particle_diam, elevation, particle_id, state, age, loop_age]
        centre += particle_diam
        particle_id += -1 # Bed particles get negative IDs
    
    return bed_particles

def bed_complete(centre, bed_length):
    if centre >= bed_length:
        return 1
    else: return 0


def determine_num_particles(pack_frac, num_vertices):
    """Return the number of model particles to be created
    based on the packing fraction"""
    
    num_particles = num_vertices * pack_frac
    num_particles = int(math.ceil(num_particles))
    
    return num_particles

# Trig from: https://math.stackexchange.com/questions/2293201/
def place_particle(particle, model_particles, bed_particles, h):
    """ Calculate new y (elevation) of particle based on it's 
        x (horizontal) location in stream.
    
    Provided a particle p's location x in the stream, 
    search for 2 supporting particles (s1, s2) that p 
    will rest on when placed at x.
    
    Calculate the y position of p based on the (x,y) of both 
    s1 and s2. The computed x for p might be different up to some 
    decimal point, so both x and y are rounded to 2 decimal places.

    
    Args:
        particle: NumPy array representing the model particle being placed.
        model_particles: An n-7 NumPy array representing the stream's n model particles.
        bed_particles: An m-7 NumPy array representing the stream's m bed particles.

    Returns:
        rounded_x: Rounded float of particle's new x location.
        rounded_y: Rounded float of particle's new y location.
        left_support: UID of the left support for the placed particle.
        right_support: UID of the right support for the placed particle.
    
    """
    left_support, right_support = find_supports(particle, model_particles, bed_particles)
    rounded_x = round(particle[0], 2)
    rounded_y = round(np.add(h, left_support[2]), 2)
    return rounded_x, rounded_y, left_support[3], right_support[3]


def update_particle_states(model_particles, model_supports):
    """ Set/update each model particle's state. 
    
    If any model particle p has a particle 
    resting on it in the stream then p must 
    be set to inactive indicated by a boolean 0.
    
    If p does not have any particles resting
    on top of it then it is considered active 
    indicated by a boolean 1.
    
    Args:
        model_particles: An n-7 NumPy array representing the stream's 
            n model particles.
        model_supports: An n-2 NumPy array with the uids of the two 
            particles supporting each model particle.
        bed_particles: An m-7 NumPy array representing the stream's m bed
            particles.

    Return values:
        model_particles: The provided model_particles array (Args) 
            but with updated active (attribute 4) values.
    """
    # Start by setting all model particles to active then 
    # only set to inactive if there is a particle sitting on top
    model_particles[:,4] = 1
    in_stream_particles = model_particles[model_particles[:,0] != -1]
    inactive_left = np.intersect1d(in_stream_particles[:,3], model_supports[:,0])
    inactive_right = np.intersect1d(in_stream_particles[:,3], model_supports[:,1])

    if inactive_left.size != 0:
        model_particles[inactive_left.astype(int), 4] = 0
    if inactive_right.size != 0:
        model_particles[inactive_right.astype(int), 4] = 0

    return model_particles


def find_supports(particle, model_particles, bed_particles):
    """ Find the 2 supporting particles for a given particle.

    Provided a particle p at location x, this function 
    will search the stream for particles that p would
    rest on, if being dropped at x. More generally, supporting particles 
    are those particles which directly hold up a particle. Supporting
    particles will always have a centre location that is 
    exactly a radius length away from p's centre.

    Args:
        particle: 1-7 NumPy array representing a model particle.
        model_particles: An n-7 NumPy array representing the stream's 
            n model particles.
        bed_particles: An m-7 NumPy array representing the stream's m 
            bed particles.

    Returns:
        left_support: NumPy array representing the left supporting particle
        right_support: NumPy array representing the right supporting particle
    """  
    all_particles = np.concatenate((model_particles, bed_particles), axis=0)
    # Define location where left and right supporting particles must sit
    # in order to be considered a supporting particle.
    # Note: This limits the model to using same-sized grains.
    left_center = particle[0] - (particle[1] / 2)
    right_center = particle[0] + (particle[1] / 2)
     
    l_candidates = all_particles[all_particles[:,0] == left_center]
    try:
        left_support = l_candidates[l_candidates[:,2] 
                                    == np.max(l_candidates[:,2])]
    except ValueError:
        error_msg = (
                     f'No left supporting particle at {left_center}' 
                     f'for particle {particle[3]}'
        )
        logging.error(error_msg)
        raise   
        
    r_candidates = all_particles[all_particles[:,0] == right_center] 
    try:
        right_support = r_candidates[r_candidates[:,2] == np.max(r_candidates[:,2])]
    except ValueError:
        error_msg = (
                     f'No right supporting particle at {right_center}' 
                     f'for particle {particle[3]}'
        )
        logging.error(error_msg)
        raise
    return left_support[0], right_support[0]


def set_model_particles(bed_particles, available_vertices, particle_diam, pack_fraction, h):
    """ Create array of n model particles and set each particle in-stream.
    
    Model particles are randomly placed at available vertex
    locations (x,y) across the bed. Location and initial attribute
    values are stored in the returned NumPy array. 
    
    Args:
        bed_particles: An m-7 NumPy array representing the stream's m bed particles.
        available_vertices: A NumPy array with all available vertices in the stream. 
        particle_diam: The diameter of all particles (float).
        pack_fraction: Packing density value (float). See THEORY.md in project
            repo for more information.
        h: Geometric value used in calculations of particle placement (float). See
            in-line and project documentation for further explanation.
    
    Returns:
        model_particles: An n-7 NumPy array representing the stream's n model particles and their 
            initial placement in the stream. For example::
            
                [[x1, diam, y1, uid1, active, age, loops], ... ,[xN, diam, yN, uidN, active, age, loops]]

            Where (xi, yi) pairs will define the centre location of each particle and no two particles
            will have the same (xi, yi) pair values. All uids are unique and are positive whole numbers.
            All particles will start with active = 1, age = 0, and loops = 0.

        model_supp: An n-2 NumPy array with the uids of the two particles supporting each 
            model particle. For example::
        
                [[[-1,-2]], ... ,[-3,-4]]

            The above example states that model particle with uid 0 (model_supp[0]) is supported
            by bed particles with uids -1 and -2. Similarly, the model particle with uid n 
            (model_supp[n]) is supported by bed particles with uids -3 and -4. 
     """ 
    num_placement_loc = np.size(available_vertices)
    # determine the number of model particles that should be introduced into the stream bed
    num_particles = determine_num_particles(pack_fraction, num_placement_loc)
    # create an empty n-6 array to store model particle information
    model_particles = np.zeros([num_particles, 7], dtype='float')
    model_supp = np.zeros([num_particles, 2], dtype='float')
  
    for particle in range(num_particles):  
        # the following lines select a vertex to place the current particle at, 
        # and ensure that it is not already occupied by another particle
        random_idx = random.randint(0, np.size(available_vertices)-1)
        vertex = available_vertices[random_idx]
        available_vertices = available_vertices[available_vertices != vertex]

        # intialize the particle information
        model_particles[particle][0] = vertex 
        model_particles[particle][1] = particle_diam
        
        model_particles[particle][3] = particle # id number for each particle
        model_particles[particle][4] = 1 # each particle begins as active
        
        # place particle at the chosen vertex
        p_x, p_y, left_supp, right_supp  = place_particle(model_particles[particle], 
                                                            model_particles, 
                                                            bed_particles, 
                                                            h)
        model_particles[particle][0] = p_x
        model_particles[particle][2] = p_y
        model_particles[particle][5] = 0
        model_particles[particle][6] = 0

        model_supp[particle][0] = left_supp
        model_supp[particle][1] = right_supp
    
    return model_particles, model_supp


def compute_available_vertices(model_particles, bed_particles, particle_diam, level_limit,
                               lifted_particles=None):
    """ Compute the avaliable vertices in the model stream.

    An available vertex is an x location that a 
    model particle is able to be entrained to.
    This function identifies the distinct elevations 
    present in the stream then looks at subsets of 
    particles in decesnding order of their elevation,
    in order to compute available vertices.
    
    For each elevation group, if a particle is sitting on a vertex
    x, then x cannot be available (it is occupied) and it
    is considered nulled. Then vertices v created by two 
    particles touching are considered. If v is not already 
    nulled by a particle occupying the location at a higher
    level, then it is considered an available vertex. This ends once the bed 
    particles (the lowest elevation) have been considered.
    

    Args:
        model_particles: An n-7 NumPy array representing the stream's 
            n model particles.
        bed_particles: An m-7 NumPy array representing the stream's m 
            bed particles. 
        lifted_particles: UID of particles that are 'lifted'. Lifted 
            particles will not be considered as present in the stream
            when the available vertices are being calculated; their
            (x,y) location will not be considered as occupied.
    
    Returns:
        available_vertices: A NumPy array with all available vertices in the stream. 
    """
    nulled_vertices = []
    avail_vertices = []
    
    # If we are lifting particles, we need to consider the subset of particles
    # that includes every particles _except_ the particles being 
    if lifted_particles is not None:  
        model_particles_lifted = np.delete(model_particles, 
                                           lifted_particles, 0)
        all_particles = np.concatenate((model_particles_lifted, 
                                        bed_particles), axis=0)
    else:
        all_particles = np.concatenate((model_particles, 
                                        bed_particles), axis=0)
    # Get unique model particle elevations in stream (descending)
    elevations = elevation_list(all_particles[:,2])
    
    for idx, elevation in enumerate(elevations):
        tmp_particles = all_particles[all_particles[:,2] == elevation]
        
        for particle in tmp_particles:    
            nulled_vertices.append(particle[0])
        
        right_vertices = tmp_particles[:,0] + (particle_diam / 2)
        left_vertices = tmp_particles[:,0] - (particle_diam / 2)
        tmp_shared_vertices = np.intersect1d(left_vertices, right_vertices)
        
        # Enforce level limit by nulling any vertex above limit:
        if len(elevations) == level_limit+1 and idx==0: 
            for vertex in tmp_shared_vertices:
                nulled_vertices.append(vertex)
        
        for vertex in tmp_shared_vertices:
            if vertex not in nulled_vertices:
                avail_vertices.append(vertex)
                
        del(tmp_shared_vertices)
        del(tmp_particles)
        
    available_vertices = np.array(avail_vertices)
    
    return available_vertices


def elevation_list(elevations, desc=True):
    """ Returns a sorted list of unique elevation values """
    ue = np.unique(elevations)
    if desc:
           ue = ue[::-1]
    return ue
 
def compute_hops(event_particle_ids, model_particles, mu, sigma, normal=False):
    """ Given a list of event paritcles, this function will 
    add a hop distance to current x locations of all event particles. 
    
    Current + Hop = desired hop distance. Hop values are randomly 
    selected from a log-normal or normal distribution. For more
    information on the use of these distributions see THEORY.md 
    in the project repository.
    
    Args:
        event_particle_ids: A NumPy array of k uids representing the model particles
            that have been selected for entrainment.
        model_particles: An n-7 NumPy array representing the stream's 
            n model particles.
        normal (default = False): Boolean flag for which distribution to sample
            Hop values from. True = sample from Normal, False = sample from log-Normal. 
    
    Returns:
        event_particles: A k-7 Numpy array representing each event particle
            with updated x locations (x=deried hop location).
    """
    event_particles = model_particles[event_particle_ids]
    if normal:
        s = np.random.normal(mu, sigma, len(event_particle_ids))
    else:
        s = np.random.lognormal(mu, sigma, len(event_particle_ids))
    s_hop = np.round(s, 1)
    s_hop = list(s_hop)
    event_particles[:,0] = event_particles[:,0] + s_hop
    
    return event_particles
 
def move_model_particles(event_particles, model_particles, model_supp, bed_particles, available_vertices, h):
    """ Move model particles in the stream.
    
    Given an array of event particles and their desired hops, move each
    event particle in-stream based on the event particle's desired hop. Sometimes
    the desired hop will equal an available vertex and sometimes it will not.
    When it does not, move the particle to the closest vertex to the 
    desired hop in the downstream direction.

    Args:
        event_particles: A k-7 Numpy array representing event particle.
        model_particles: An n-7 NumPy array representing the stream's 
            n model particles.
        model_supp: An n-2 NumPy array with the uids of the two particles supporting each 
            model particle (e.g model_supp[j] = supports for model particle j).  
        bed_particles: An m-7 NumPy array representing the stream's m 
            bed particles. 
        available_vertices: A NumPy array with all available vertices in the stream. 
        h: Geometric value used in calculations of particle placement (float). See
            in-line and project documentation for further explanation.
    
    Returns:
        model_particles: The provided model_particles array (Args) 
            but with updated (x,y) attributes based on placements. 
        model_supports: An updated model_supports (Args) based on 
            placements. Note that only event particles will ever 
            have their model supports updated.
    """
    # Randomly iterate over event particles
    for particle in np.random.permutation(event_particles):
        orig_x = model_particles[model_particles[:,3] == particle[3]][0][0]
        verified_hop = find_closest_vertex(particle[0], available_vertices)
        
        if verified_hop == -1:
            exceed_msg = (
                f'Particle {int(particle[3])} exceeded stream...'
                f'sending to -1 axis'
            )
            logging.info(exceed_msg) 
            particle[6] = particle[6] + 1
            particle[0] = verified_hop

            model_supp[int(particle[3])][0] = np.nan
            model_supp[int(particle[3])][1] = np.nan
        else:
            hop_msg = (
                f'Particle {int(particle[3])} entrained from {orig_x} '
                f'to {verified_hop}. Desired hop was: {particle[0]}'
            )
            logging.info(hop_msg)
            particle[0] = verified_hop
            available_vertices = available_vertices[available_vertices != verified_hop]

            placed_x, placed_y, left_supp, right_supp = place_particle(particle, model_particles, bed_particles, h)
            particle[0] = placed_x
            particle[2] = placed_y

            model_supp[int(particle[3])][0] = left_supp
            model_supp[int(particle[3])][1] = right_supp

        model_particles[model_particles[:,3] == particle[3]] = particle
    return model_particles, model_supp


def update_flux(initial_positions, final_positions, iteration, subregions):
    """ Update flux lists in each subregion.

    Given arrays of initial and final positions, this function 
    will update each subregion's flux list to indicate how many 
    paticles crossed the subregions's downstream boundary in 
    a given iteration.

    Args:
        initial_positions: NumPy array of initial x locations.
        final_positions: NumPy array of final (verified) x locations.
        iteration: The iteration for the flux to be updated for (int).
        subregions: Python array of Subregion objects.

    Returns:
        subregions: Python array of Subregion objects with updated flux lists.
    """
    # This can _most definitely_ be made quicker but for now, it works
    if len(initial_positions) != len(final_positions):
        raise ValueError(f'Initial_positions and final_positions do not contain the same # of elements')
    
    for position in range(0, len(initial_positions)):

        initial_pos = initial_positions[position]
        final_pos = final_positions[position]

        for idx, subregion in enumerate(subregions):
            if (initial_pos >= subregion.leftBoundary()) and (subregion.rightBoundary() > initial_pos):
                start_idx = idx

        for subregion_idx in range(start_idx, len(subregions)):
            if final_pos >= subregions[subregion_idx].rightBoundary():
                subregions[subregion_idx].incrementFlux(iteration)
            elif final_pos == -1 and subregion_idx == len(subregions)-1:
                subregions[subregion_idx].incrementFlux(iteration)

    return subregions

    
def find_closest_vertex(desired_hop, available_vertices):
    """ Find the closest downstream (greater than or equal) vertex
    in availbale vertices. If nothing exists, return -1.
    
    Args:
        desired_hop: Float representing the desired hop location
        available_location: A NumPy array with all available vertices in the stream. 
    
    Returns:
        vertex: The location of the closest available vertex that 
            is >= desired_hop (float).
    """    
    if available_vertices.size == 0:
        raise ValueError('Available vertices array is empty, cannot find closest vertex')
    if desired_hop < 0:
        raise ValueError('Desired hop is negative (invalid)')

    available_vertices = np.sort(available_vertices)
    forward_vertices = available_vertices[available_vertices >= desired_hop]
    
    if forward_vertices.size < 1:
        vertex = -1
    else:
        vertex = forward_vertices[0]
    return vertex    


def increment_age(model_particles, e_event_ids):
    """Increment model particles' age, set event particles to age 0"""
    
    model_particles[:,5] = model_particles[:,5] + 1 
    model_particles[e_event_ids, 5] = 0
    
    return model_particles

Functions

def bed_complete(centre, bed_length)
Expand source code
def bed_complete(centre, bed_length):
    if centre >= bed_length:
        return 1
    else: return 0
def build_streambed(bed_length, particle_diam)

Builds the array of bed particles.

Args

bed_length
The length of the stream (int).
particle_diam
The diameter of all particles (float).

Returns

bed_particles

An m-7 NumPy array representing the stream's m bed particles. For example::

[[x1, diam, y, uid1, active, age, loops], ... 
            ,[xM, diam, y, uidM, active, age, loops]]

Where all bed particles share the same diam (diam1=…=diamM) and y (y1=…=yM), all uids are negative, and to represent 'static-ness' active = 0, age = 0, and loops = 0.

Expand source code
def build_streambed(bed_length, particle_diam):
    """ Builds the array of bed particles.
    
    Args:
        bed_length: The length of the stream (int).
        particle_diam: The diameter of all particles (float).
    
    Returns:
        bed_particles: An m-7 NumPy array representing the stream's m 
            bed particles. For example::

                [[x1, diam, y, uid1, active, age, loops], ... 
                            ,[xM, diam, y, uidM, active, age, loops]]
            
            Where all bed particles share the same diam (diam1=...=diamM) and 
            y (y1=...=yM), all uids are negative, and to represent 'static-ness'
            active = 0, age = 0, and loops = 0. 

    """
    max_particles = int(math.ceil( bed_length / particle_diam ))
    bed_particles = np.zeros([max_particles, 7],dtype=float)
    
    particle_id = -1
    centre = (particle_diam/2)  
    state = 0
    age = 0
    loop_age = 0
    elevation = 0
    while not bed_complete(centre, bed_length):  
        # index with negative indices... bed particles are built from the final element to the first
        bed_particles[particle_id] = [centre, particle_diam, elevation, particle_id, state, age, loop_age]
        centre += particle_diam
        particle_id += -1 # Bed particles get negative IDs
    
    return bed_particles
def compute_available_vertices(model_particles, bed_particles, particle_diam, level_limit, lifted_particles=None)

Compute the avaliable vertices in the model stream.

An available vertex is an x location that a model particle is able to be entrained to. This function identifies the distinct elevations present in the stream then looks at subsets of particles in decesnding order of their elevation, in order to compute available vertices.

For each elevation group, if a particle is sitting on a vertex x, then x cannot be available (it is occupied) and it is considered nulled. Then vertices v created by two particles touching are considered. If v is not already nulled by a particle occupying the location at a higher level, then it is considered an available vertex. This ends once the bed particles (the lowest elevation) have been considered.

Args

model_particles
An n-7 NumPy array representing the stream's n model particles.
bed_particles
An m-7 NumPy array representing the stream's m bed particles.
lifted_particles
UID of particles that are 'lifted'. Lifted particles will not be considered as present in the stream when the available vertices are being calculated; their (x,y) location will not be considered as occupied.

Returns

available_vertices
A NumPy array with all available vertices in the stream.
Expand source code
def compute_available_vertices(model_particles, bed_particles, particle_diam, level_limit,
                               lifted_particles=None):
    """ Compute the avaliable vertices in the model stream.

    An available vertex is an x location that a 
    model particle is able to be entrained to.
    This function identifies the distinct elevations 
    present in the stream then looks at subsets of 
    particles in decesnding order of their elevation,
    in order to compute available vertices.
    
    For each elevation group, if a particle is sitting on a vertex
    x, then x cannot be available (it is occupied) and it
    is considered nulled. Then vertices v created by two 
    particles touching are considered. If v is not already 
    nulled by a particle occupying the location at a higher
    level, then it is considered an available vertex. This ends once the bed 
    particles (the lowest elevation) have been considered.
    

    Args:
        model_particles: An n-7 NumPy array representing the stream's 
            n model particles.
        bed_particles: An m-7 NumPy array representing the stream's m 
            bed particles. 
        lifted_particles: UID of particles that are 'lifted'. Lifted 
            particles will not be considered as present in the stream
            when the available vertices are being calculated; their
            (x,y) location will not be considered as occupied.
    
    Returns:
        available_vertices: A NumPy array with all available vertices in the stream. 
    """
    nulled_vertices = []
    avail_vertices = []
    
    # If we are lifting particles, we need to consider the subset of particles
    # that includes every particles _except_ the particles being 
    if lifted_particles is not None:  
        model_particles_lifted = np.delete(model_particles, 
                                           lifted_particles, 0)
        all_particles = np.concatenate((model_particles_lifted, 
                                        bed_particles), axis=0)
    else:
        all_particles = np.concatenate((model_particles, 
                                        bed_particles), axis=0)
    # Get unique model particle elevations in stream (descending)
    elevations = elevation_list(all_particles[:,2])
    
    for idx, elevation in enumerate(elevations):
        tmp_particles = all_particles[all_particles[:,2] == elevation]
        
        for particle in tmp_particles:    
            nulled_vertices.append(particle[0])
        
        right_vertices = tmp_particles[:,0] + (particle_diam / 2)
        left_vertices = tmp_particles[:,0] - (particle_diam / 2)
        tmp_shared_vertices = np.intersect1d(left_vertices, right_vertices)
        
        # Enforce level limit by nulling any vertex above limit:
        if len(elevations) == level_limit+1 and idx==0: 
            for vertex in tmp_shared_vertices:
                nulled_vertices.append(vertex)
        
        for vertex in tmp_shared_vertices:
            if vertex not in nulled_vertices:
                avail_vertices.append(vertex)
                
        del(tmp_shared_vertices)
        del(tmp_particles)
        
    available_vertices = np.array(avail_vertices)
    
    return available_vertices
def compute_hops(event_particle_ids, model_particles, mu, sigma, normal=False)

Given a list of event paritcles, this function will add a hop distance to current x locations of all event particles.

Current + Hop = desired hop distance. Hop values are randomly selected from a log-normal or normal distribution. For more information on the use of these distributions see THEORY.md in the project repository.

Args

event_particle_ids
A NumPy array of k uids representing the model particles that have been selected for entrainment.
model_particles
An n-7 NumPy array representing the stream's n model particles.
normal : default = False
Boolean flag for which distribution to sample Hop values from. True = sample from Normal, False = sample from log-Normal.

Returns

event_particles
A k-7 Numpy array representing each event particle with updated x locations (x=deried hop location).
Expand source code
def compute_hops(event_particle_ids, model_particles, mu, sigma, normal=False):
    """ Given a list of event paritcles, this function will 
    add a hop distance to current x locations of all event particles. 
    
    Current + Hop = desired hop distance. Hop values are randomly 
    selected from a log-normal or normal distribution. For more
    information on the use of these distributions see THEORY.md 
    in the project repository.
    
    Args:
        event_particle_ids: A NumPy array of k uids representing the model particles
            that have been selected for entrainment.
        model_particles: An n-7 NumPy array representing the stream's 
            n model particles.
        normal (default = False): Boolean flag for which distribution to sample
            Hop values from. True = sample from Normal, False = sample from log-Normal. 
    
    Returns:
        event_particles: A k-7 Numpy array representing each event particle
            with updated x locations (x=deried hop location).
    """
    event_particles = model_particles[event_particle_ids]
    if normal:
        s = np.random.normal(mu, sigma, len(event_particle_ids))
    else:
        s = np.random.lognormal(mu, sigma, len(event_particle_ids))
    s_hop = np.round(s, 1)
    s_hop = list(s_hop)
    event_particles[:,0] = event_particles[:,0] + s_hop
    
    return event_particles
def define_subregions(bed_length, num_subregions, iterations)

Define subregion list for model stream.

Args

bed_length
The length of the stream (int).
num_subregions
The number of subregions (int).
iterations
The number of iterations for the model run (int).

Returns

subregions_arr
Python array of initialized Subregion objects.
Expand source code
def define_subregions(bed_length, num_subregions, iterations):
    """ Define subregion list for model stream.
    
    Args:
        bed_length: The length of the stream (int). 
        num_subregions: The number of subregions (int).
        iterations: The number of iterations for the model run (int).

    Returns:
        subregions_arr: Python array of initialized Subregion objects. 
    """
    subregion_length = bed_length/num_subregions
    left_boundary = 0.0
    subregions_arr = []
    for region in range(num_subregions):  
        right_boundary = left_boundary + subregion_length   
        subregion = Subregion(f'subregion-{region}', left_boundary, right_boundary, iterations)
        left_boundary = right_boundary
        
        subregions_arr.append(subregion)
    
    return subregions_arr
def determine_num_particles(pack_frac, num_vertices)

Return the number of model particles to be created based on the packing fraction

Expand source code
def determine_num_particles(pack_frac, num_vertices):
    """Return the number of model particles to be created
    based on the packing fraction"""
    
    num_particles = num_vertices * pack_frac
    num_particles = int(math.ceil(num_particles))
    
    return num_particles
def elevation_list(elevations, desc=True)

Returns a sorted list of unique elevation values

Expand source code
def elevation_list(elevations, desc=True):
    """ Returns a sorted list of unique elevation values """
    ue = np.unique(elevations)
    if desc:
           ue = ue[::-1]
    return ue
def find_closest_vertex(desired_hop, available_vertices)

Find the closest downstream (greater than or equal) vertex in availbale vertices. If nothing exists, return -1.

Args

desired_hop
Float representing the desired hop location
available_location
A NumPy array with all available vertices in the stream.

Returns

vertex
The location of the closest available vertex that is >= desired_hop (float).
Expand source code
def find_closest_vertex(desired_hop, available_vertices):
    """ Find the closest downstream (greater than or equal) vertex
    in availbale vertices. If nothing exists, return -1.
    
    Args:
        desired_hop: Float representing the desired hop location
        available_location: A NumPy array with all available vertices in the stream. 
    
    Returns:
        vertex: The location of the closest available vertex that 
            is >= desired_hop (float).
    """    
    if available_vertices.size == 0:
        raise ValueError('Available vertices array is empty, cannot find closest vertex')
    if desired_hop < 0:
        raise ValueError('Desired hop is negative (invalid)')

    available_vertices = np.sort(available_vertices)
    forward_vertices = available_vertices[available_vertices >= desired_hop]
    
    if forward_vertices.size < 1:
        vertex = -1
    else:
        vertex = forward_vertices[0]
    return vertex    
def find_supports(particle, model_particles, bed_particles)

Find the 2 supporting particles for a given particle.

Provided a particle p at location x, this function will search the stream for particles that p would rest on, if being dropped at x. More generally, supporting particles are those particles which directly hold up a particle. Supporting particles will always have a centre location that is exactly a radius length away from p's centre.

Args

particle
1-7 NumPy array representing a model particle.
model_particles
An n-7 NumPy array representing the stream's n model particles.
bed_particles
An m-7 NumPy array representing the stream's m bed particles.

Returns

left_support
NumPy array representing the left supporting particle
right_support
NumPy array representing the right supporting particle
Expand source code
def find_supports(particle, model_particles, bed_particles):
    """ Find the 2 supporting particles for a given particle.

    Provided a particle p at location x, this function 
    will search the stream for particles that p would
    rest on, if being dropped at x. More generally, supporting particles 
    are those particles which directly hold up a particle. Supporting
    particles will always have a centre location that is 
    exactly a radius length away from p's centre.

    Args:
        particle: 1-7 NumPy array representing a model particle.
        model_particles: An n-7 NumPy array representing the stream's 
            n model particles.
        bed_particles: An m-7 NumPy array representing the stream's m 
            bed particles.

    Returns:
        left_support: NumPy array representing the left supporting particle
        right_support: NumPy array representing the right supporting particle
    """  
    all_particles = np.concatenate((model_particles, bed_particles), axis=0)
    # Define location where left and right supporting particles must sit
    # in order to be considered a supporting particle.
    # Note: This limits the model to using same-sized grains.
    left_center = particle[0] - (particle[1] / 2)
    right_center = particle[0] + (particle[1] / 2)
     
    l_candidates = all_particles[all_particles[:,0] == left_center]
    try:
        left_support = l_candidates[l_candidates[:,2] 
                                    == np.max(l_candidates[:,2])]
    except ValueError:
        error_msg = (
                     f'No left supporting particle at {left_center}' 
                     f'for particle {particle[3]}'
        )
        logging.error(error_msg)
        raise   
        
    r_candidates = all_particles[all_particles[:,0] == right_center] 
    try:
        right_support = r_candidates[r_candidates[:,2] == np.max(r_candidates[:,2])]
    except ValueError:
        error_msg = (
                     f'No right supporting particle at {right_center}' 
                     f'for particle {particle[3]}'
        )
        logging.error(error_msg)
        raise
    return left_support[0], right_support[0]
def get_event_particles(e_events, subregions, model_particles, level_limit, height_dependant=False)

Find and return list of particles to be entrained

Will loop through each subregion and select n = e_events model particles (within a subregion boundaries) at random to be entrained. No particle will be selected twice.

Args

e_events
The number of events requested per subregion (int).
subregions
Python array of initialized Subregion objects.
model_particles
An n-7 NumPy array representing the stream's n model particles.

Returns

event_particles

A NumPy array of k uids representing the model particles that have been selected for entrainment. For example::

[2.0, 5.0, 25.0]

Will represent that model particles with uids 2.0, 5.0 and 25.0 have been selected for entrainment.

Expand source code
def get_event_particles(e_events, subregions, model_particles, level_limit, height_dependant=False):
    """ Find and return list of particles to be entrained

    Will loop through each subregion and select n = e_events
    model particles (within a subregion boundaries) at random 
    to be entrained. No particle will be selected twice.

    Args:
        e_events: The number of events requested per subregion (int).
        subregions: Python array of initialized Subregion objects. 
        model_particles: An n-7 NumPy array representing the stream's n 
            model particles.

    Returns:
        event_particles: A NumPy array of k uids representing the model particles
            that have been selected for entrainment. For example::

                [2.0, 5.0, 25.0]

            Will represent that model particles with uids 2.0, 5.0 and
            25.0 have been selected for entrainment. 
    """
    if e_events == 0:
        e_events = 1 #???
    
    event_particles = []
    for subregion in subregions:
        # Take only particles in the boundaries of the current subregion
        subregion_particles = model_particles[
                (model_particles[:,0] >= subregion.leftBoundary())
              & (model_particles[:,0] <= subregion.rightBoundary())]
        # Take only particles that are in-stream (not ghost)
        in_stream_particles = subregion_particles[
                                                subregion_particles[:,0] != -1]
        # Take only particles that are 'active' 
        active_particles =  in_stream_particles[
                                                in_stream_particles[:,4] != 0]

        # Do not take any particles that have been selected for entrainment (i.e do not double select)
        # This only happens when particles rest on the boundary. 
        active_event, active_idx, event_idx = np.intersect1d(active_particles[:,3], event_particles, return_indices=True)
        active_particles = np.delete(active_particles, active_idx, axis=0)

        subregion_event_ids = []  
        if height_dependant: # any particle at the level limit must be entrained
            levels = elevation_list(subregion_particles[:,2], desc=False)
            tip_particles = []
            # find the tip particles -- these are the particles being entrained
            if len(levels) == level_limit: 
                tip_particles = active_particles[active_particles[:,2] == levels[level_limit-1]]
            for particle in tip_particles:
                subregion_event_ids.append(particle[3])
                active_particles = active_particles[active_particles[:,2] != particle[2]]
        # If there are not enough particles in the subregion to sample from, alter the sample size
        if e_events > len(active_particles):
            random_sample = random.sample(range(len(active_particles)), 
                                        len(active_particles))
        else: 
            random_sample = random.sample(range(len(active_particles)), 
                                        e_events)
        # TODO: change so that we don't rely on loop index to grab particle
        for index in random_sample:
            subregion_event_ids.append(int(active_particles[index][3])  )
        
        ghost_particles = np.where(model_particles[:,0] == -1)[0]
        for index in ghost_particles:
            model_particles[index][0] = 0 
            subregion_event_ids.append(index)
        
        if e_events != len(subregion_event_ids):
            msg = (
                     f'Requested {e_events} events in {subregion.getName()} ' 
                     f'but {len(subregion_event_ids)} are occuring'
            )
            logging.info(msg)
        event_particles = event_particles + subregion_event_ids
    event_particles = np.array(event_particles, dtype=np.intp)

    return event_particles
def increment_age(model_particles, e_event_ids)

Increment model particles' age, set event particles to age 0

Expand source code
def increment_age(model_particles, e_event_ids):
    """Increment model particles' age, set event particles to age 0"""
    
    model_particles[:,5] = model_particles[:,5] + 1 
    model_particles[e_event_ids, 5] = 0
    
    return model_particles
def move_model_particles(event_particles, model_particles, model_supp, bed_particles, available_vertices, h)

Move model particles in the stream.

Given an array of event particles and their desired hops, move each event particle in-stream based on the event particle's desired hop. Sometimes the desired hop will equal an available vertex and sometimes it will not. When it does not, move the particle to the closest vertex to the desired hop in the downstream direction.

Args

event_particles
A k-7 Numpy array representing event particle.
model_particles
An n-7 NumPy array representing the stream's n model particles.
model_supp
An n-2 NumPy array with the uids of the two particles supporting each model particle (e.g model_supp[j] = supports for model particle j).
bed_particles
An m-7 NumPy array representing the stream's m bed particles.
available_vertices
A NumPy array with all available vertices in the stream.
h
Geometric value used in calculations of particle placement (float). See in-line and project documentation for further explanation.

Returns

model_particles
The provided model_particles array (Args) but with updated (x,y) attributes based on placements.
model_supports
An updated model_supports (Args) based on placements. Note that only event particles will ever have their model supports updated.
Expand source code
def move_model_particles(event_particles, model_particles, model_supp, bed_particles, available_vertices, h):
    """ Move model particles in the stream.
    
    Given an array of event particles and their desired hops, move each
    event particle in-stream based on the event particle's desired hop. Sometimes
    the desired hop will equal an available vertex and sometimes it will not.
    When it does not, move the particle to the closest vertex to the 
    desired hop in the downstream direction.

    Args:
        event_particles: A k-7 Numpy array representing event particle.
        model_particles: An n-7 NumPy array representing the stream's 
            n model particles.
        model_supp: An n-2 NumPy array with the uids of the two particles supporting each 
            model particle (e.g model_supp[j] = supports for model particle j).  
        bed_particles: An m-7 NumPy array representing the stream's m 
            bed particles. 
        available_vertices: A NumPy array with all available vertices in the stream. 
        h: Geometric value used in calculations of particle placement (float). See
            in-line and project documentation for further explanation.
    
    Returns:
        model_particles: The provided model_particles array (Args) 
            but with updated (x,y) attributes based on placements. 
        model_supports: An updated model_supports (Args) based on 
            placements. Note that only event particles will ever 
            have their model supports updated.
    """
    # Randomly iterate over event particles
    for particle in np.random.permutation(event_particles):
        orig_x = model_particles[model_particles[:,3] == particle[3]][0][0]
        verified_hop = find_closest_vertex(particle[0], available_vertices)
        
        if verified_hop == -1:
            exceed_msg = (
                f'Particle {int(particle[3])} exceeded stream...'
                f'sending to -1 axis'
            )
            logging.info(exceed_msg) 
            particle[6] = particle[6] + 1
            particle[0] = verified_hop

            model_supp[int(particle[3])][0] = np.nan
            model_supp[int(particle[3])][1] = np.nan
        else:
            hop_msg = (
                f'Particle {int(particle[3])} entrained from {orig_x} '
                f'to {verified_hop}. Desired hop was: {particle[0]}'
            )
            logging.info(hop_msg)
            particle[0] = verified_hop
            available_vertices = available_vertices[available_vertices != verified_hop]

            placed_x, placed_y, left_supp, right_supp = place_particle(particle, model_particles, bed_particles, h)
            particle[0] = placed_x
            particle[2] = placed_y

            model_supp[int(particle[3])][0] = left_supp
            model_supp[int(particle[3])][1] = right_supp

        model_particles[model_particles[:,3] == particle[3]] = particle
    return model_particles, model_supp
def place_particle(particle, model_particles, bed_particles, h)

Calculate new y (elevation) of particle based on it's x (horizontal) location in stream.

Provided a particle p's location x in the stream, search for 2 supporting particles (s1, s2) that p will rest on when placed at x.

Calculate the y position of p based on the (x,y) of both s1 and s2. The computed x for p might be different up to some decimal point, so both x and y are rounded to 2 decimal places.

Args

particle
NumPy array representing the model particle being placed.
model_particles
An n-7 NumPy array representing the stream's n model particles.
bed_particles
An m-7 NumPy array representing the stream's m bed particles.

Returns

rounded_x
Rounded float of particle's new x location.
rounded_y
Rounded float of particle's new y location.
left_support
UID of the left support for the placed particle.
right_support
UID of the right support for the placed particle.
Expand source code
def place_particle(particle, model_particles, bed_particles, h):
    """ Calculate new y (elevation) of particle based on it's 
        x (horizontal) location in stream.
    
    Provided a particle p's location x in the stream, 
    search for 2 supporting particles (s1, s2) that p 
    will rest on when placed at x.
    
    Calculate the y position of p based on the (x,y) of both 
    s1 and s2. The computed x for p might be different up to some 
    decimal point, so both x and y are rounded to 2 decimal places.

    
    Args:
        particle: NumPy array representing the model particle being placed.
        model_particles: An n-7 NumPy array representing the stream's n model particles.
        bed_particles: An m-7 NumPy array representing the stream's m bed particles.

    Returns:
        rounded_x: Rounded float of particle's new x location.
        rounded_y: Rounded float of particle's new y location.
        left_support: UID of the left support for the placed particle.
        right_support: UID of the right support for the placed particle.
    
    """
    left_support, right_support = find_supports(particle, model_particles, bed_particles)
    rounded_x = round(particle[0], 2)
    rounded_y = round(np.add(h, left_support[2]), 2)
    return rounded_x, rounded_y, left_support[3], right_support[3]
def set_model_particles(bed_particles, available_vertices, particle_diam, pack_fraction, h)

Create array of n model particles and set each particle in-stream.

Model particles are randomly placed at available vertex locations (x,y) across the bed. Location and initial attribute values are stored in the returned NumPy array.

Args

bed_particles
An m-7 NumPy array representing the stream's m bed particles.
available_vertices
A NumPy array with all available vertices in the stream.
particle_diam
The diameter of all particles (float).
pack_fraction
Packing density value (float). See THEORY.md in project repo for more information.
h
Geometric value used in calculations of particle placement (float). See in-line and project documentation for further explanation.

Returns

model_particles

An n-7 NumPy array representing the stream's n model particles and their initial placement in the stream. For example::

[[x1, diam, y1, uid1, active, age, loops], ... ,[xN, diam, yN, uidN, active, age, loops]]

Where (xi, yi) pairs will define the centre location of each particle and no two particles will have the same (xi, yi) pair values. All uids are unique and are positive whole numbers. All particles will start with active = 1, age = 0, and loops = 0.

model_supp

An n-2 NumPy array with the uids of the two particles supporting each model particle. For example::

[[[-1,-2]], ... ,[-3,-4]]

The above example states that model particle with uid 0 (model_supp[0]) is supported by bed particles with uids -1 and -2. Similarly, the model particle with uid n (model_supp[n]) is supported by bed particles with uids -3 and -4.

Expand source code
def set_model_particles(bed_particles, available_vertices, particle_diam, pack_fraction, h):
    """ Create array of n model particles and set each particle in-stream.
    
    Model particles are randomly placed at available vertex
    locations (x,y) across the bed. Location and initial attribute
    values are stored in the returned NumPy array. 
    
    Args:
        bed_particles: An m-7 NumPy array representing the stream's m bed particles.
        available_vertices: A NumPy array with all available vertices in the stream. 
        particle_diam: The diameter of all particles (float).
        pack_fraction: Packing density value (float). See THEORY.md in project
            repo for more information.
        h: Geometric value used in calculations of particle placement (float). See
            in-line and project documentation for further explanation.
    
    Returns:
        model_particles: An n-7 NumPy array representing the stream's n model particles and their 
            initial placement in the stream. For example::
            
                [[x1, diam, y1, uid1, active, age, loops], ... ,[xN, diam, yN, uidN, active, age, loops]]

            Where (xi, yi) pairs will define the centre location of each particle and no two particles
            will have the same (xi, yi) pair values. All uids are unique and are positive whole numbers.
            All particles will start with active = 1, age = 0, and loops = 0.

        model_supp: An n-2 NumPy array with the uids of the two particles supporting each 
            model particle. For example::
        
                [[[-1,-2]], ... ,[-3,-4]]

            The above example states that model particle with uid 0 (model_supp[0]) is supported
            by bed particles with uids -1 and -2. Similarly, the model particle with uid n 
            (model_supp[n]) is supported by bed particles with uids -3 and -4. 
     """ 
    num_placement_loc = np.size(available_vertices)
    # determine the number of model particles that should be introduced into the stream bed
    num_particles = determine_num_particles(pack_fraction, num_placement_loc)
    # create an empty n-6 array to store model particle information
    model_particles = np.zeros([num_particles, 7], dtype='float')
    model_supp = np.zeros([num_particles, 2], dtype='float')
  
    for particle in range(num_particles):  
        # the following lines select a vertex to place the current particle at, 
        # and ensure that it is not already occupied by another particle
        random_idx = random.randint(0, np.size(available_vertices)-1)
        vertex = available_vertices[random_idx]
        available_vertices = available_vertices[available_vertices != vertex]

        # intialize the particle information
        model_particles[particle][0] = vertex 
        model_particles[particle][1] = particle_diam
        
        model_particles[particle][3] = particle # id number for each particle
        model_particles[particle][4] = 1 # each particle begins as active
        
        # place particle at the chosen vertex
        p_x, p_y, left_supp, right_supp  = place_particle(model_particles[particle], 
                                                            model_particles, 
                                                            bed_particles, 
                                                            h)
        model_particles[particle][0] = p_x
        model_particles[particle][2] = p_y
        model_particles[particle][5] = 0
        model_particles[particle][6] = 0

        model_supp[particle][0] = left_supp
        model_supp[particle][1] = right_supp
    
    return model_particles, model_supp
def update_flux(initial_positions, final_positions, iteration, subregions)

Update flux lists in each subregion.

Given arrays of initial and final positions, this function will update each subregion's flux list to indicate how many paticles crossed the subregions's downstream boundary in a given iteration.

Args

initial_positions
NumPy array of initial x locations.
final_positions
NumPy array of final (verified) x locations.
iteration
The iteration for the flux to be updated for (int).
subregions
Python array of Subregion objects.

Returns

subregions
Python array of Subregion objects with updated flux lists.
Expand source code
def update_flux(initial_positions, final_positions, iteration, subregions):
    """ Update flux lists in each subregion.

    Given arrays of initial and final positions, this function 
    will update each subregion's flux list to indicate how many 
    paticles crossed the subregions's downstream boundary in 
    a given iteration.

    Args:
        initial_positions: NumPy array of initial x locations.
        final_positions: NumPy array of final (verified) x locations.
        iteration: The iteration for the flux to be updated for (int).
        subregions: Python array of Subregion objects.

    Returns:
        subregions: Python array of Subregion objects with updated flux lists.
    """
    # This can _most definitely_ be made quicker but for now, it works
    if len(initial_positions) != len(final_positions):
        raise ValueError(f'Initial_positions and final_positions do not contain the same # of elements')
    
    for position in range(0, len(initial_positions)):

        initial_pos = initial_positions[position]
        final_pos = final_positions[position]

        for idx, subregion in enumerate(subregions):
            if (initial_pos >= subregion.leftBoundary()) and (subregion.rightBoundary() > initial_pos):
                start_idx = idx

        for subregion_idx in range(start_idx, len(subregions)):
            if final_pos >= subregions[subregion_idx].rightBoundary():
                subregions[subregion_idx].incrementFlux(iteration)
            elif final_pos == -1 and subregion_idx == len(subregions)-1:
                subregions[subregion_idx].incrementFlux(iteration)

    return subregions
def update_particle_states(model_particles, model_supports)

Set/update each model particle's state.

If any model particle p has a particle resting on it in the stream then p must be set to inactive indicated by a boolean 0.

If p does not have any particles resting on top of it then it is considered active indicated by a boolean 1.

Args

model_particles
An n-7 NumPy array representing the stream's n model particles.
model_supports
An n-2 NumPy array with the uids of the two particles supporting each model particle.
bed_particles
An m-7 NumPy array representing the stream's m bed particles.

Return values: model_particles: The provided model_particles array (Args) but with updated active (attribute 4) values.

Expand source code
def update_particle_states(model_particles, model_supports):
    """ Set/update each model particle's state. 
    
    If any model particle p has a particle 
    resting on it in the stream then p must 
    be set to inactive indicated by a boolean 0.
    
    If p does not have any particles resting
    on top of it then it is considered active 
    indicated by a boolean 1.
    
    Args:
        model_particles: An n-7 NumPy array representing the stream's 
            n model particles.
        model_supports: An n-2 NumPy array with the uids of the two 
            particles supporting each model particle.
        bed_particles: An m-7 NumPy array representing the stream's m bed
            particles.

    Return values:
        model_particles: The provided model_particles array (Args) 
            but with updated active (attribute 4) values.
    """
    # Start by setting all model particles to active then 
    # only set to inactive if there is a particle sitting on top
    model_particles[:,4] = 1
    in_stream_particles = model_particles[model_particles[:,0] != -1]
    inactive_left = np.intersect1d(in_stream_particles[:,3], model_supports[:,0])
    inactive_right = np.intersect1d(in_stream_particles[:,3], model_supports[:,1])

    if inactive_left.size != 0:
        model_particles[inactive_left.astype(int), 4] = 0
    if inactive_right.size != 0:
        model_particles[inactive_right.astype(int), 4] = 0

    return model_particles

Classes

class Subregion (name, left_boundary, right_boundary, iterations)

A subregion in the stream.

A Subregion is defined by left (upstream) and right (downstream) boundaries. Each subregion maintains a NumPy list which is used to record the number of model particles that pass the downstream boundary in a given iteration. For example::

flux_list = [0,3,2,1]

Means that 0 crossings happened in the first iteration, 3 happened in the 2nd, and so on. The list has length equal to the number of iterations for a model run.

Attributes

name
Name of the subregion.
left_boundary
Location of the left boundary (float).
right_boundary
Location of the right boundary (float).
iterations
The number of iterations for the model run.
Expand source code
class Subregion():
    """ A subregion in the stream.
    
    A Subregion is defined by left (upstream)
    and right (downstream) boundaries. Each
    subregion maintains a NumPy list which is 
    used to record the number of model particles that 
    pass the downstream boundary in a given iteration. 
    For example::

        flux_list = [0,3,2,1]

    Means that 0 crossings happened in the first iteration, 
    3 happened in the 2nd, and so on. The list has 
    length equal to the number of iterations for a model run.
    
    Attributes:
        name: Name of the subregion.
        left_boundary: Location of the left boundary (float).
        right_boundary: Location of the right boundary (float).
        iterations: The number of iterations for the model run.
    """
    def __init__(self, name, left_boundary, right_boundary, iterations):
        self.name = name
        self.left_boundary = left_boundary
        self.right_boundary = right_boundary
        self.flux_list = np.zeros(iterations, dtype=np.int64)
        
    def leftBoundary(self):
        """Returns subregion's left boundary"""
        return self.left_boundary
    
    def rightBoundary(self):
        """Returns subregion's right boundary"""
        return self.right_boundary
    
    def getName(self):
        """Returns subregion's name"""
        return self.name

    def incrementFlux(self, iteration):
        """Increments flux list by 1.

        Args:
            iteration: The iteration/index to increment by 1
        """
        self.flux_list[iteration] += 1
    
    def getFluxList(self):
        """Returns subregion's flux list"""
        return self.flux_list

Methods

def getFluxList(self)

Returns subregion's flux list

Expand source code
def getFluxList(self):
    """Returns subregion's flux list"""
    return self.flux_list
def getName(self)

Returns subregion's name

Expand source code
def getName(self):
    """Returns subregion's name"""
    return self.name
def incrementFlux(self, iteration)

Increments flux list by 1.

Args

iteration
The iteration/index to increment by 1
Expand source code
def incrementFlux(self, iteration):
    """Increments flux list by 1.

    Args:
        iteration: The iteration/index to increment by 1
    """
    self.flux_list[iteration] += 1
def leftBoundary(self)

Returns subregion's left boundary

Expand source code
def leftBoundary(self):
    """Returns subregion's left boundary"""
    return self.left_boundary
def rightBoundary(self)

Returns subregion's right boundary

Expand source code
def rightBoundary(self):
    """Returns subregion's right boundary"""
    return self.right_boundary