Module test_logic

A module for unit tests of the logic module

Todo

  • automate the creation of particle arrays so that it isn't harcoded in each test func
Expand source code
"""
A module for unit tests of the logic module

Todo:
    * automate the creation of particle arrays
    so that it isn't harcoded in each test func
"""

import unittest
import numpy as np
from unittest.mock import Mock

from sbelt import logic

ATTR_COUNT = 7 # Number of attributes associated with a Particle
# For reference:
        # [0] = x-coord
        # [1] = diameter,
        # [2] = y-coord (elevation),
        # [3] = uid,
        # [4] = active (boolean)
        # [5] = age counter
        # [6] = loop age counter
  
class TestGetEventParticlesWithOneSubregion(unittest.TestCase):
    """
    Test that getting event particles with one Subregion 
    returns a valid list of event particles.
    
    A 'valid list' will change depending on the function.
    See function docstrings for more details.

    Attributes:
        test_length: the length of the bed
        num_particles: the number of model particles
        mock_sub_list: list of Mock-type subregions
        entrainment_events: number of entrainment events to request
            per subregion
        level_limit: random int representing level limit
    """
    def setUp(self):
        self.test_length = 10
        self.num_particles = 3

        mock_subregion = Mock()
        mock_subregion.leftBoundary.return_value = 0
        mock_subregion.rightBoundary.return_value = self.test_length
        mock_subregion.getName.return_value = 'Mock_Subregion'
        self.mock_sub_list = [mock_subregion]

        self.entrainment_events = 3
        self.level_limit = np.random.randint(0, np.random.randint(2, 10))

    def test_all_active_returns_valid_list(self): 
        """If there are N active particles in 1 subregion and N events requested
        per subregion then a valid list will be a list of all particles.
        """
        model_particles = np.zeros((self.num_particles, ATTR_COUNT))
        model_particles[:,3] = np.arange(self.num_particles) # unique ids
        model_particles[:,4] = np.ones(self.num_particles) # all active
        model_particles[:,0] = np.random.randint(
                                                self.test_length, 
                                                size=self.num_particles ) # random placement

        list = logic.get_event_particles(
                                        self.entrainment_events, 
                                        self.mock_sub_list, 
                                        model_particles, 
                                        self.level_limit )
        self.assertCountEqual(list, model_particles[:,3])

        # Height dependancy should not effect list results here
        hp_list = list = logic.get_event_particles(
                                        self.entrainment_events, 
                                        self.mock_sub_list, 
                                        model_particles, 
                                        self.level_limit,
                                        height_dependant=True )
        self.assertCountEqual(hp_list, model_particles[:,3])
        self.assertCountEqual(hp_list, list)
    
    def test_not_all_active_returns_list_of_2(self):
        """If there are N particles in 1 subregion and N-1 are _active_, 
        and if N events are requested per subregion then a valid list will be 
        a list of the two active particles.
        """
        mp_one_inactive = np.zeros((self.num_particles, ATTR_COUNT))
        mp_one_inactive[:,3] = np.arange(self.num_particles) 
        mp_one_inactive[0][4] = 1
        mp_one_inactive[1][4] = 1
        mp_one_inactive[:,0] = np.random.randint(self.test_length, size=self.num_particles)

        list = logic.get_event_particles(
                                        self.entrainment_events, 
                                        self.mock_sub_list, 
                                        mp_one_inactive, 
                                        self.level_limit )
        self.assertEqual(len(list), self.num_particles - 1)
        active_list = mp_one_inactive[mp_one_inactive[:,4] != 0]
        self.assertCountEqual(list, active_list[:,3])

    def test_none_active_returns_empty_list(self):
        """If there are N particles in 1 subregion and 0 are _active_ 
        and if N events are requested per subregion, then a valid list will be 
        an empty list.
        """
        np_none_active = np.zeros((self.num_particles, ATTR_COUNT))
        np_none_active[:,3] = np.arange(self.num_particles) 
        np_none_active[:,0] = np.random.randint(self.test_length, size=self.num_particles)

        empty_list = logic.get_event_particles(
                                        self.entrainment_events, 
                                        self.mock_sub_list, 
                                        np_none_active, 
                                        self.level_limit )
        self.assertEqual(len(empty_list), 0)
    

    def test_all_ghost_particles_returns_ghost_particles(self):
        """If there are N particles in 1 subregion and all N particles
        are 'ghost' particles (at -1), and if N particles are requested
        per subregion, then a valid list will be a list of all the
        ghost particles (all the particles).
        """
        np_all_ghost = np.zeros((self.num_particles, ATTR_COUNT))
        np_all_ghost[:,3] = np.arange(self.num_particles) 
        np_all_ghost[:,0] = -1

        ghost_list = logic.get_event_particles(
                                        self.entrainment_events, 
                                        self.mock_sub_list, 
                                        np_all_ghost, 
                                        self.level_limit )
        self.assertCountEqual(ghost_list, np_all_ghost[:,3])  

class TestGetEventParticlesWithNSubregions(unittest.TestCase):
    """
    Test that getting event particles with N Subregion 
    returns a valid list of event particles.
    
    A 'valid list' will change depending on the function.
    See function docstrings for more details.

    Attributes:
        test_length: the length of the bed
        num_particles: the number of model particles
        mock_sub_list_2: list of Mock-type subregions
        entrainment_events: number of entrainment events to request
            per subregion
        level_limit: random int representing level limit
    """
    def setUp(self):
        self.test_length = 20
        self.num_particles = 6
        mock_subregion_0 = Mock()
        mock_subregion_0.leftBoundary.return_value = 0
        mock_subregion_0.rightBoundary.return_value = self.test_length / 2
        mock_subregion_0.getName.return_value = 'Mock_Subregion_0'

        mock_subregion_1 = Mock()
        mock_subregion_1.leftBoundary.return_value = self.test_length / 2
        mock_subregion_1.rightBoundary.return_value = self.test_length
        mock_subregion_1.getName.return_value = 'Mock_Subregion_1'

        self.mock_sub_list_2 = [mock_subregion_0, mock_subregion_1]

        self.entrainment_events = 3
        self.level_limit = np.random.randint(0, np.random.randint(2, 10))


    def test_all_active_returns_3_per_subregion(self):
        """If there are M active particles in each of the N subregions and there
        are M events requested per subregion, then a valid list will be a 
        list of all M*N particles.
        """
        model_particles = np.zeros((self.num_particles, ATTR_COUNT))
        model_particles[:,3] = np.arange(self.num_particles) # unique ids
        model_particles[:,4] = np.ones(self.num_particles) # all active
        # Randomly place first three particles in Subregion 1 
        model_particles[0:3, 0] = np.random.randint(
                                                9, 
                                                size=3 )
        # Randomly place last three particles in Subregion 2
        model_particles[3:6, 0] = np.random.randint(
                                                11,
                                                self.test_length, 
                                                size=3 )

        list = logic.get_event_particles(
                                        self.entrainment_events, 
                                        self.mock_sub_list_2, 
                                        model_particles, 
                                        self.level_limit )

        self.assertCountEqual(list, model_particles[:,3])
        self.assertEqual(len(list), self.entrainment_events * 2)

        # Height dependancy should not effect list results here
        hp_list = list = logic.get_event_particles(
                                        self.entrainment_events, 
                                        self.mock_sub_list_2, 
                                        model_particles, 
                                        self.level_limit,
                                        height_dependant=True )
        self.assertCountEqual(hp_list, model_particles[:,3])
        self.assertCountEqual(hp_list, list)


    def test_active_in_1_subregion_returns_only_active(self):
        """If there are M active particles in each 1..K subregions and 0
        active in K+1...N subregions, and there are M events requested per 
        subregion, then a valid list will be a list of the M*K active particles.

        This is simplified down to only 2 subregions.
        """
        mp_half_active = np.zeros((self.num_particles, ATTR_COUNT))
        mp_half_active[:,3] = np.arange(self.num_particles) 
        mp_half_active[0:3, 4] = np.ones(int((self.num_particles/2))) # First half active

        mp_half_active[0:3, 0] = np.random.randint(10,size=3 )
        mp_half_active[3:6, 0] = np.random.randint(10, self.test_length, size=3 ) 

        list = logic.get_event_particles(
                                        self.entrainment_events, 
                                        self.mock_sub_list_2, 
                                        mp_half_active, 
                                        self.level_limit )

        active_particles = mp_half_active[mp_half_active[:,4] != 0] 

        self.assertCountEqual(list, active_particles[:,3])
        self.assertEqual(len(list), 3)
    
    def test_particle_on_boundary_is_not_returned_twice(self):
        """ Test that a particle resting on a boundary between
        Subregions (recall, other than upstream and downstream
        boundaries, Subregions share boundaries) will not 
        be selected for entrainment twice.
        """
        one_particle_on_boundary = np.zeros((1, ATTR_COUNT))
        one_particle_on_boundary[0][4] = 1
        one_particle_on_boundary[0][0] = 10
        # Use custom entrainment_event count for simplicity
        entrainment_events = 1

        list = logic.get_event_particles(
                                        entrainment_events, 
                                        self.mock_sub_list_2, 
                                        one_particle_on_boundary, 
                                        self.level_limit )
        self.assertEqual(len(list), 1)

# Test Define Subregions
class TestDefineSubregions(unittest.TestCase):
    """ Test define subregions module

    Attributes:
       bed_length: the length of the bed the subregion 
            is being defined on/in
       iterations: the number of iterations that a 
            subregion needs to maintain data for
    """
    def setUp(self):
        self.bed_length = 10
        self.iterations = 10

    def test_good_parameters_return_good_subregion_list(self):
        """ If the bed length is divisible by the number of
        subregions, and the iterations and bed length are valid
        int inputs then the function should return a list of 
        Subregion objects whose boundaries overlap exactly 
        with the length of the stream and each other. 
        For example:
            if bed_length = 2, num_subregions = 2 then a 
            valid list will be [subregion_1, subregion_2]
            where:
                subregion_1.leftBoundary() = 0
                subregion_1.leftBoundary() = 1
                subregion_2.leftBoundary() = 1
                subregion_2.rightBoundary() = 2             
        """
        subregion_count_even = 2
        left_boundary = 0
        middle_boundary = self.bed_length / 2
        right_boundary = self.bed_length

        subregion_list = logic.define_subregions(self.bed_length, 
                                                subregion_count_even, 
                                                self.iterations)
        # Check number of subregions
        self.assertEqual(len(subregion_list), 2) 
        # Check boundary definitions
        self.assertEqual(subregion_list[0].leftBoundary(), left_boundary)
        self.assertEqual(subregion_list[0].rightBoundary(), middle_boundary)
        self.assertEqual(subregion_list[1].leftBoundary(), middle_boundary)
        self.assertEqual(subregion_list[1].rightBoundary(), right_boundary)

        subregion_count_odd = 5
        sub_length = self.bed_length / subregion_count_odd

        left_boundary = 0
        middle_boundary_1 = left_boundary + sub_length*1
        middle_boundary_2 = left_boundary + sub_length*2
        middle_boundary_3 = left_boundary + sub_length*3
        middle_boundary_4 = left_boundary + sub_length*4
        right_boundary = self.bed_length

        subregion_list_odd = logic.define_subregions(self.bed_length, 
                                                subregion_count_odd, 
                                                self.iterations)
        # Check number of subregions
        self.assertEqual(len(subregion_list_odd), 5) 
        # Check boundary definitions
        self.assertEqual(subregion_list_odd[0].leftBoundary(), left_boundary)
        self.assertEqual(subregion_list_odd[0].rightBoundary(), middle_boundary_1)

        self.assertEqual(subregion_list_odd[1].leftBoundary(), middle_boundary_1)
        self.assertEqual(subregion_list_odd[1].rightBoundary(), middle_boundary_2)

        self.assertEqual(subregion_list_odd[2].leftBoundary(), middle_boundary_2)
        self.assertEqual(subregion_list_odd[2].rightBoundary(), middle_boundary_3)

        self.assertEqual(subregion_list_odd[3].leftBoundary(), middle_boundary_3)
        self.assertEqual(subregion_list_odd[3].rightBoundary(), middle_boundary_4)

        self.assertEqual(subregion_list_odd[4].leftBoundary(), middle_boundary_4)
        self.assertEqual(subregion_list_odd[4].rightBoundary(), right_boundary)
    
    def test_all_subregion_flux_are_init_0(self):
        """
        All values in flux_lists should be 0 at initialization.
        """
        subregion_count_even = 2
        empty_list = np.zeros(self.iterations, dtype=np.int64)
        subregion_list_even = logic.define_subregions(self.bed_length, 
                                                subregion_count_even, 
                                                self.iterations)
        
        for subregion in subregion_list_even:
            self.assertEqual(len(subregion.getFluxList()), self.iterations)
            self.assertCountEqual(subregion.getFluxList(), empty_list)

    # TODO: test incrementFlux() 

# Test Build Streambed
class TestBuildStreambed(unittest.TestCase):
    """ Test build_streambed module """
    def test_compat_diam_returns_good_particles(self):
        """ Test that bed is 'tightly packed' meaning
        that bed particles are place beside each other 
        and that no part of any particle exists beyond 
        the range [0, bed_length]. Also test that
        attributes are initialized to proper values.
        """
        stream_length = 100
        diameter = 0.5
        expected_number_particles = stream_length / diameter

        bed_particles = logic.build_streambed(stream_length, diameter)

        self.assertEqual(len(bed_particles), expected_number_particles)

        expected_centres = np.arange(diameter/2, expected_number_particles*diameter, step=diameter)
        expected_ids = np.arange(1, int(expected_number_particles)+1)*-1
        expected_attr = np.zeros(int(expected_number_particles))
        expected_diam = np.ones(int(expected_number_particles))*diameter

        # Reverse array when testing because bed_particles array is packed from index N to 0
        self.assertIsNone(np.testing.assert_array_equal(expected_centres[::-1], bed_particles[:,0]))
        self.assertIsNone(np.testing.assert_array_equal(expected_ids[::-1], bed_particles[:,3]))
        self.assertIsNone(np.testing.assert_array_equal(expected_diam[::-1], bed_particles[:,1]))

        # attributes y, active, age, and loop should all be 0
        for attribute_idx in [2, 4, 5, 6]:
            self.assertIsNone(np.testing.assert_array_equal(expected_attr, bed_particles[:,attribute_idx]))
        
        final_particle_idx = -len(bed_particles)
        final_particle_extent = bed_particles[final_particle_idx][0] + diameter/2
        self.assertEqual(final_particle_extent, stream_length)

class TestSetModelParticles(unittest.TestCase):
    """ Test the set_model_particles module

    Attributes:
        diam: diameter of all particles in the test
        pack_fraction: float representing packing density value
        h: float value derived from diam - used in geometric 
            placement of particles ontop of other particles
        bed_particles: An n-7 array representing the bed particles
            for this test 
        available_vertices: A numpy array of vertices a particle
            is allowed to be placed at. Created with np.arange()
    """

    def setUp(self):
        stream_length = 10
        self.diam = 0.5
        self.pack_fraction = 0.8
        # Directly from https://math.stackexchange.com/questions/2293201/
        # Variables used for geometric placement
        d = np.divide(np.multiply(np.divide(self.diam, 2), 
                                            self.diam), 
                                            self.diam)
        self.h = np.sqrt(np.square(self.diam) - np.square(d))

        # Mock a full bed_particles array
        num_bed_particles = int(stream_length/self.diam)
        bed_particles = np.zeros([num_bed_particles, ATTR_COUNT], dtype=float)
        bed_particles[:,0] = np.arange(self.diam/2, stream_length+(self.diam/2), step=self.diam)
        bed_particles[:,3] = np.arange(1, num_bed_particles+1)*-1
        self.bed_particles = bed_particles
        # Make all vertices created by the touching bed particles available
        # -----> 0.5, 1.0, 1.5, ... , 9.5 (with stream length 10)
        self.available_vertices = np.arange(self.diam, stream_length, step=self.diam)

    def test_model_particles_placed_at_valid_locations(self):
        """ Test that the function places particles
        only at vectors provided by available_vertices
        """
        model_particles, model_supports = logic.set_model_particles(self.bed_particles,   
                                                    self.available_vertices, 
                                                    self.diam, 
                                                    self.pack_fraction, 
                                                    self.h)
        # Particles should only be placed at available vertices
        self.assertTrue(set(model_particles[:,0]).issubset(self.available_vertices))  
        # All placements should be unique
        self.assertTrue(len(model_particles[:,0]) == len(set(model_particles[:,0])))
        # All ids should be unique
        # There should be no stacking
        self.assertEqual(len(set(model_particles[:,2])), 1)

    def test_all_model_particles_have_valid_initial_attributes(self):
        """ Test that the function produces particles 
        with valid initial attributes. Valid initial 
        attributes are unique IDs, correct diameter,
        active = 1, age = 0, loop = 0, and that
        all supports should be from the bed (id < 0)
        """
        model_particles, model_supports = logic.set_model_particles(self.bed_particles,   
                                                    self.available_vertices, 
                                                    self.diam, 
                                                    self.pack_fraction, 
                                                    self.h)
        # all diam = self.diam
        expected_diam = np.ones(len(model_particles)) * self.diam
        self.assertCountEqual(model_particles[:,1], expected_diam)

        # unique id's
        self.assertTrue(len(model_particles[:,3]) == len(set(model_particles[:,3])))
        
        # all model are active
        expected_activity = np.ones(len(model_particles)) 
        self.assertCountEqual(model_particles[:,4], expected_activity)

        # 0 age counter and loop age
        expected_age_and_loop = np.zeros(len(model_particles))
        self.assertCountEqual(model_particles[:,5], expected_age_and_loop)
        self.assertCountEqual(model_particles[:,6], expected_age_and_loop)

        # Supports should all be negative (resting on the bed)
        self.assertTrue(0, len(model_supports[model_supports > 0]))


class TestComputeAvailableVerticesLifted(unittest.TestCase):
    """ Test compute_available_vertices function with 
    the lifted argument set to True.

    Attributes:
        stream_length: int representing length of test stream
        diam: float representing diam of test particles
        bed_particles: An n-7 array representing the test bed particles
        expected_bed_vertices: An np array of the available vertices 
            that an empty bed should produce if all is working well
    """
    def setUp(self):
        # make bed particles
        self.stream_length = 5
        self.diam = 0.5

        # Mock a full bed_particles array
        num_bed_particles = int(self.stream_length/self.diam) # 10 bed particles
        bed_particles = np.zeros([num_bed_particles, ATTR_COUNT], dtype=float)
        bed_particles[:,0] = np.arange(self.diam/2, self.stream_length+(self.diam/2), step=self.diam)
        bed_particles[:,3] = np.arange(num_bed_particles) # unique ids
        self.bed_particles = bed_particles

        self.expected_bed_vertices = np.arange(self.diam, self.stream_length, step=self.diam)

    def test_only_bed_and_empty_lifted_returns_expected_bed_vert(self):
        """If there are no model particles, and the lifted array is 
        empty, then the available vertices should = expected_bed_vertices
        """
        level_limit = 3 # Arbitrary level limit
        empty_model_particles = np.empty((0, ATTR_COUNT))

        # Bed of length n should return n-1 available vertices
        available_vertices = logic.compute_available_vertices(empty_model_particles, self.bed_particles, self.diam, 
                                            level_limit=level_limit, lifted_particles=[])
        self.assertEqual(len(self.bed_particles)-1, len(available_vertices))
        self.assertCountEqual(available_vertices, self.expected_bed_vertices)
    
    def test_bed_and_all_model_lifted_returns_expected_bed_vertices(self):
        """If there are N model particles resting directly on the bed and 
        the lifted array has all N model particle ids in it, then the available 
        vertices should = expected_bed_vertices
        """
        level_limit = 3
        num_model_particles = 3

        model_particles = np.zeros([num_model_particles, ATTR_COUNT], dtype=float)
        # Particles will be at the first 3 available vertices
        model_particles[:,0] = self.expected_bed_vertices[0:3]
        model_particles[:,3] = np.arange(num_model_particles) 
        # Bed of length n should return n-1 available vertices
        available_vertices = logic.compute_available_vertices(model_particles, self.bed_particles, self.diam, 
                                            level_limit=level_limit, lifted_particles=model_particles[:,3].astype(int))
        
        self.assertEqual(len(available_vertices), len(self.bed_particles)-1)
        self.assertCountEqual(available_vertices, self.expected_bed_vertices)

    def test_not_touching_and_one_lifted_model_returns_valid_vertices(self):
        """ If there are N model particles resting directly on the bed and K are 
        lifted then the available vertices should be 
            expected_bed_vertices - (x locations of the N-K unlifted particles)
        """
        level_limit = 3
        num_model_particles = 3

        model_particles = np.zeros([num_model_particles, ATTR_COUNT], dtype=float)
        # Particles will be at the first 3 available vertices
        model_particles[:,0] = self.expected_bed_vertices[0:3] 
        model_particles[:,3] = np.arange(num_model_particles)

        # Lift first particle, keep later 2 particles -- t/f locations of first particles should be be available
        # and locations of second and third particle should not be avaliable
        available_vertices = logic.compute_available_vertices(model_particles, self.bed_particles, self.diam, 
                                        level_limit=level_limit, lifted_particles=model_particles[0][3].astype(int))

        expected_vertices = np.delete(self.expected_bed_vertices, [1,2])
        self.assertEqual(len(available_vertices), len(expected_vertices))
        self.assertCountEqual(available_vertices, expected_vertices)

class TestComputeAvailableVerticesNotLifted(unittest.TestCase):
    """ Test compute_available_vertices function with 
    the lifted argument set to False.

        Attributes:
            stream_length: int representing length of test stream
            diam: float representing diam of test particles
            bed_particles: An n-7 array representing the test bed particles
            expected_bed_vertices: An np array of the available vertices 
                that an empty bed should produce if all is working well
    """
    def setUp(self):
        # make bed particles
        self.stream_length = 5
        self.diam = 0.5

        # Mock a full bed_particles array
        num_bed_particles = int(self.stream_length/self.diam) # 10 bed particles
        bed_particles = np.zeros([num_bed_particles, ATTR_COUNT], dtype=float)
        bed_particles[:,0] = np.arange(self.diam/2, self.stream_length+(self.diam/2), step=self.diam)
        bed_particles[:,3] = np.arange(num_bed_particles) # unique ids
        self.bed_particles = bed_particles

        self.expected_bed_vertices = np.arange(self.diam, self.stream_length, step=self.diam)
    
    def test_only_bed_returns_expected_bed_vertices(self):
        """If there are no model particles then the
        available vertices should = expected_bed_vertices 
        """
        level_limit = 3 # Arbitrary level limit
        empty_model_particles = np.empty((0, ATTR_COUNT))

        # Bed of length n should return n-1 available vertices
        available_vertices = logic.compute_available_vertices(empty_model_particles, self.bed_particles, self.diam, 
                                            level_limit=level_limit)
        
        self.assertEqual(len(self.bed_particles)-1, len(available_vertices))
        self.assertCountEqual(available_vertices, self.expected_bed_vertices)

    def test_one_model_particle_returns_bed_available_minus_one(self):
        """If there is 1 model particle resting directly on the bed 
        then the available vertices should be
            expected_bed_vertices - (x location of the 1 particle)
        """
        level_limit = 3 # Arbitrary level limit
        one_particle = np.array([[self.diam, 0, 0, 0, 0, 0, 0]]) # at first resting spot
        available_vertices = logic.compute_available_vertices(one_particle, self.bed_particles, self.diam, 
                                            level_limit=level_limit)
        
        # Assert there is no available vertex at one_particle[0][0]
        self.assertNotIn(one_particle[0][0], available_vertices)
        self.assertEqual(len(self.bed_particles)-2, len(available_vertices))
        
        expected_vertices = np.delete(self.expected_bed_vertices, 0) # bed minus first available vertex
        self.assertCountEqual(available_vertices, expected_vertices)

    def test_m_model_particles_return_bed_available_minus_m(self):
        """If there are M model particles resting directly on the bed 
        then the available vertices should be
            expected_bed_vertices - (x location of the M particles)
        """
        level_limit = 3 # Arbitrary level limit
        m_particles = 4
        model_particles = np.zeros([m_particles, ATTR_COUNT], dtype=float)
        # Place m model particles 2 resting spots away from each other
        placement_idxs = [0, 2, 4, 6]
        model_particles[0][0] = self.expected_bed_vertices[placement_idxs[0]]
        model_particles[1][0] = self.expected_bed_vertices[placement_idxs[1]]
        model_particles[2][0] = self.expected_bed_vertices[placement_idxs[2]]
        model_particles[3][0] = self.expected_bed_vertices[placement_idxs[3]]

        available_vertices = logic.compute_available_vertices(model_particles, self.bed_particles, self.diam, 
                                            level_limit=level_limit)
        
        self.assertEqual(len(self.bed_particles)-m_particles-1, len(available_vertices))
        
        expected_vertices = np.delete(self.expected_bed_vertices, placement_idxs)
        self.assertCountEqual(available_vertices, expected_vertices)

    def test_no_available_vertices_returns_empty_array(self):
        """If the stream has no spots that a particle
        could rest validly then the returned available vertices
        should be an empty array. An example of this scenario is:

            The bed resting spots are fully saturated with model particles 
            BUT none are touching so no new vertices are being made
        """
        level_limit = 3 # Arbitrary level limit
        model_particles = np.zeros([len(self.bed_particles)-1, ATTR_COUNT], dtype=float)
        model_particles[:,0] = self.expected_bed_vertices

        available_vertices = logic.compute_available_vertices(model_particles, self.bed_particles, self.diam, 
                                            level_limit=level_limit)
        self.assertEqual(0, len(available_vertices))
    
    def test_two_touching_model_and_empty_bed_return_one_valid_vertex(self):
        """ If there are two particles at the same elevation and 
        their centres (x,y) are exactly a diam length away then 
        the two particles are touching. Touching particles should 
        one new vertex at the location the particles touch.
        
        NOTE: these tests use an empty bed array to directly test the behaviour
            of touching particles in a simpler manner (simpler elevation). If 
            the bed was not empty it would make no difference.
        """
        level_limit = 3 # Arbitrary level limit

        model_particles = np.zeros([2, ATTR_COUNT], dtype=float)
        model_particles[:,0] = np.arange(0.5, 1.5, step=self.diam) # These particles will be touching
        empty_bed = np.empty((0, ATTR_COUNT))

        available_vertices = logic.compute_available_vertices(model_particles, empty_bed, self.diam, 
                                            level_limit=level_limit)
        expected_new_vertex = 0.75
        self.assertEqual(len(available_vertices), 1)
        self.assertEqual(available_vertices, expected_new_vertex)
    
    def test_two_model_touching_at_diff_elev_return_no_vertex(self):
        """ If two particles have centres that are a diameter
        length away from each other but their elevations are different
        then they are NOT touching. They should not create a new vertex.
        
        NOTE: these tests use an empty bed array to directly test the behaviour
            of touching particles in a simpler manner (simpler elevation). If 
            the bed was not empty it would make no difference.
        """
        level_limit = 3 # Arbitrary level limit

        model_particles = np.zeros([2, ATTR_COUNT], dtype=float)
        model_particles[:,0] = np.arange(0.5, 1.5, step=self.diam) # These particles will be touching
        # Place them at different elevations
        model_particles[0][2] = 0
        model_particles[1][2] = 1
        empty_bed = np.empty((0, ATTR_COUNT))

        available_vertices = logic.compute_available_vertices(model_particles, empty_bed, self.diam, 
                                            level_limit=level_limit)
        
        self.assertEqual(len(available_vertices), 0)

    def test_3triangle_and_empty_bed_returns_empty_array(self):
        """ A 3-triangle of particles is: two particles touching 
        and one particle resting on the vertex created by the two 
        touching particles. A 3-triangle should not create
        any available vertices in the stream.
       
        NOTE: these tests use an empty bed array to directly test the behaviour
            of touching particles in a simpler manner (simpler elevation). If 
            the bed was not empty it would make no difference.
        """
        level_limit = 3 # Level limit > 2
        model_particles = np.zeros([3, ATTR_COUNT], dtype=float)
        # 3 triangle: 2 particles touching, 1 particle resting above/between 
        model_particles[0:2][:,0] = np.arange(0.5, 1.5, step=self.diam) 
        model_particles[2:][:,0] = np.add(0.5, np.divide(0.5, 2))

        d = np.divide(np.multiply(np.divide(self.diam, 2), 
                                        self.diam), 
                                        self.diam)
        h = np.sqrt(np.square(self.diam) - np.square(d))
        elevation = round(np.add(h, 0), 2)
        model_particles[2:][:,2] = elevation

        empty_bed = np.empty((0, ATTR_COUNT))

        available_vertices = logic.compute_available_vertices(model_particles, empty_bed, self.diam, 
                                            level_limit=level_limit)

        self.assertEqual(0, len(available_vertices))                    

    def test_above_level_limit_returns_empty_array(self):
        """ If the level limit is K and and there are
        K level/stacks of particles then no particles 
        touching at level K can create a new vertex.

        To simplify, if the level limit is 1, and if 
        there are N model particles resting directly
        on the bed, and if all model particles are
        touching, available vertices should be empty

         NOTE: these tests use an empty bed array to directly test the behaviour
            of touching particles in a more simple manner. If the bed was
            not empty we have to take into account the bed vertices.
        """
        level_limit = 1 # Only one level of stacking allowed
        model_particles = np.zeros([5, ATTR_COUNT], dtype=float)
        # 3 triangle: 2 particles touching, 1 particle resting above/between 
        model_particles[0:3][:,0] = np.arange(0.5, 2.0, step=self.diam) 
        model_particles[3:][:,0] = np.arange(0.75, 1.5, step=self.diam)
        
        d = np.divide(np.multiply(np.divide(self.diam, 2), 
                                        self.diam), 
                                        self.diam)
        h = np.sqrt(np.square(self.diam) - np.square(d))
        elevation = round(np.add(h, 0), 2)
        model_particles[3:][:,2] = elevation

        empty_bed = np.empty((0, ATTR_COUNT))

        available_vertices = logic.compute_available_vertices(model_particles, empty_bed, self.diam, 
                                            level_limit=level_limit)
        self.assertEqual(0, len(available_vertices))

class TestFindSupports(unittest.TestCase):
    """ Test the find_supports function

    Attributes:
        diam = float representing diameter of the test particles
    """
    def setUp(self):
        self.diam = 0.5

    def test_placement_returns_highest_elevation_supports(self):
        """ Given a particle at (x,y), if there are multple 
        particles with their locations (_x) exactly +- diameter
        away from x, then the returned support should be the 
        particle with the highest _y.
        """
        particle_placement = 1.0
        particle_height = 1.0

        particles = np.zeros((5, ATTR_COUNT), dtype=float)
        particles[:,1] = self.diam
        particles[4,0] = particle_placement
        particles[4,2] = particle_height
        particles[0:2,0] = particle_placement - self.diam/2
        particles[2:4,0] = particle_placement + self.diam/2
        particles[[0,2],2] = particle_height - 1.0
        particles[[1,3],2] = particle_height + 1.0
        empty_bed = np.empty((0, ATTR_COUNT))

        left_support, right_support = logic.find_supports(particles[4], particles, empty_bed)

        expected_left = particles[1]
        expected_right = particles[3]
        self.assertIsNone(np.testing.assert_array_equal(expected_left, left_support))
        self.assertIsNone(np.testing.assert_array_equal(expected_right, right_support))
    
    def test_no_supports_available_returns_value_error(self):
        """ If there is no valid support for a particle
        then a value error should be raised.
        """
        particle_placement = 1.0
        particle_height = 1.0

        particles = np.zeros((3, ATTR_COUNT), dtype=float)
        particles[:,1] = self.diam
        particles[2,0] = particle_placement
        particles[2,2] = particle_height
        particles[0,0] = particle_placement + self.diam
        particles[1,0] = particle_placement - self.diam # Centres lie outside threshold
        particles[[0,1],2] = particle_height - 1.0
        empty_bed = np.empty((0, ATTR_COUNT))

        # Both potential supporting particles outisde support threshold
        with self.assertRaises(ValueError):
            _, _ = logic.find_supports(particles[2], particles, empty_bed)
        
        # Only 1 potential supporting particles outisde support threshold
        particles[0,0] = particle_placement + self.diam/2 # right support within threshold

        with self.assertRaises(ValueError):
            _, _ = logic.find_supports(particles[2], particles, empty_bed)

        particles[0,0] = particle_placement + self.diam # right support outside threshold again
        particles[1,0] = particle_placement + self.diam/2 # left support within threshold

        with self.assertRaises(ValueError):
            _, _ = logic.find_supports(particles[2], particles, empty_bed)


class TestUpdateParticleStates(unittest.TestCase):
    """ Test the update_particle_states function.

    Attributes:
        diam: the diameter of the test particles
    """
    def setUp(self):
        self.diam = 0.5 

    def test_no_piles_returns_all_active(self): 
        """ If there are N model particles resting directly on 
        the bed and there are no model particles stacked
        ontop of other model particles then all N particles
        should be returned as active.
        """
        no_pile_particles = np.zeros((7, ATTR_COUNT))
        no_pile_particles[:,1] = self.diam
        no_pile_particles[:,2] = 1.0
        no_pile_particles[:,0] = np.arange(self.diam, 3.5+(self.diam), step=self.diam)

        bed = np.zeros([8, ATTR_COUNT], dtype=float)
        bed[:,1] = self.diam
        bed[:,3] = np.arange(1,9)*-1
        bed[:,0] = np.arange(self.diam/2, 4.0+(self.diam/2), step=self.diam)

        # hard-coded supports array. If there is only one layer of n model particles
        # resting on n+1 bed particles, then this is the form the supports will take
        # assuming model particle 0 is placed between bed particle 0 and 1, model 
        # particle 1 is placed between bed particle 1 and 2, etc.
        no_pile_supports = np.array([[-1, -2],[-2, -3],[-3, -4],[-4, -5],[-5, -6],[-6, -7],[-7, -8]])

        returned_particles = logic.update_particle_states(no_pile_particles, no_pile_supports)

        expected_active = np.ones((7, ATTR_COUNT))
        self.assertIsNone(np.testing.assert_array_equal(expected_active[:,4], returned_particles[:,4]))
    
    def test_2_layers_returns_top_layer_active(self):
        """ If there are N model particles resting
        directly on the bed and there are M model particles
        resting on top of those N model particles (so much
        so that every N particle is supporting a particle)
        then the N particles should be inactive and the 
        M particles should be active
        """
        # Test perfect stacks only return the top layer active
        # TODO: defince perfect stack? maybe call tight layers?
        two_layer_particles = np.zeros((13, ATTR_COUNT))
        two_layer_particles[:,1] = self.diam
        # First layer of particles set at elevation 1
        two_layer_particles[0:7,2] = 1.0
        two_layer_particles[0:7,0] = np.arange(self.diam, 3.5+(self.diam), step=self.diam)
        # Second layer of particles set at elevation 2
        two_layer_particles[7:13,2] = 2.0
        two_layer_particles[7:13,0] = np.arange(self.diam+self.diam/2, 3+(self.diam), step=self.diam)
        two_layer_particles[:,3] = np.arange(13)
    
        # Bed particles set at elevation 0
        bed = np.zeros([9, ATTR_COUNT], dtype=float)
        bed[:,3] = np.arange(1,10)*-1
        bed[:,1] = self.diam
        bed[:,0] = np.arange(self.diam/2, 4.5+(self.diam/2), step=self.diam)

        # the 7 particle in the first model particles layer will have bed particles as supports. The 6 
        # particles in the second layer will have the first 7 model particles as supports
        two_layer_supports = np.array([[-1, -2],[-2, -3],[-3, -4],[-4, -5],[-5, -6],[-6, -7], [-7, -8],
                                            [0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6]])

        returned_particles = logic.update_particle_states(two_layer_particles, two_layer_supports)
        expected_inactive = np.zeros(7)
        expected_active = np.ones(6)
        expected_active_state = np.concatenate((expected_inactive, expected_active))
        self.assertIsNone(np.testing.assert_array_equal(expected_active_state, returned_particles[:,4]))

    def test_some_piles_return_valid_active(self):
        """ Any model particle that is supporting
        another model particle should be inactive. Any
        model particle that is not supporting another
        model particle should be active. 
        """

        some_piles_particles = np.zeros((9, ATTR_COUNT))
        some_piles_particles[:,1] = self.diam
        some_piles_particles[:,3] = np.arange(9)
        # First 7 partciles set at elevation 1
        some_piles_particles[0:7,2] = 1.0
        some_piles_particles[0:7,0] = np.arange(self.diam, 3.5+(self.diam), step=self.diam)
        # Final 2 particles set at elevation 2
        some_piles_particles[7:9,2] = 2.0
        # Place first particle at first vertex formed by the first layer of model particles
        # Place second particle at 
        some_piles_particles[7:9,0] = np.array([some_piles_particles[0][0]+(self.diam/2), some_piles_particles[4][0]+(self.diam/2)])

        bed = np.zeros([9, ATTR_COUNT], dtype=float)
        bed[:,1] = self.diam
        bed[:,3] = np.arange(1,10)*-1
        bed[:,0] = np.arange(self.diam/2, 4.5+(self.diam/2), step=self.diam)

        some_piles_supports = np.array([[-1, -2],[-2, -3],[-3, -4],[-4, -5],[-5, -6],[-6, -7],
                                            [-7, -8], [0, 1], [4, 5]])
        
        returned_particles = logic.update_particle_states(some_piles_particles, some_piles_supports)
        expected_active_state = np.zeros(9)
        # Particles are supported by 0,1, 4, and 5. Every other particle (0-8) should be available
        expected_active_state[[2, 3, 6, 7, 8]] = 1
        self.assertIsNone(np.testing.assert_array_equal(expected_active_state, returned_particles[:,4]))

# TODO: assert the error messages are logged
class TestPlaceParticle(unittest.TestCase): 
    """ Test the place_particle function. 
    
    Attributes:
        diam: float representing diameter of the test particles
        h: float derived from diam that is used in the 
            geometric calculations of particle elevation 
    """
    def setUp(self):
        self.diam = 0.5
        d = np.divide(np.multiply(np.divide(self.diam, 2), 
                                        self.diam), 
                                        self.diam)
        self.h = np.sqrt(np.square(self.diam) - np.square(d))

    def test_bad_placement_raises_value_error(self):
        """ A bad placement should raise a value error. 
        In this case, a bad placement is any placement where a particle does
        not have a particle supporting it on the left and right sides.
        """
        # 1) Particle (arbitrary placement) with empty model and bed particles arrays
        bad_particle = np.zeros([1, ATTR_COUNT], dtype=float)
        bad_particle[:,1] = self.diam
        empty_bed = np.empty((0, ATTR_COUNT))
        empty_model = np.empty((0, ATTR_COUNT))
        
        with self.assertRaises(ValueError):
            placed_x, placed_y = logic.place_particle(bad_particle[0], empty_model, empty_bed, self.h)

        # 2) Particle placed at invalid (unsupported) location
        unsupported_particle = np.zeros([1, ATTR_COUNT], dtype=float)
        unsupported_particle[:,0] = 0.9
        unsupported_particle[:,1] = self.diam

        unsupported_model = np.zeros((2, ATTR_COUNT))
        unsupported_model[:,0] = np.arange(0.5, 1.5, step=self.diam) 

        with self.assertRaises(ValueError):
            _, _, _, _ = logic.place_particle(unsupported_particle[0], unsupported_model, empty_bed, self.h)

        # 3) Particle placed at location where all model particles' centres have the same location
        # (i.e placed on a tall single stack/tower of particles)
        stacked_unsupported_particle = np.zeros([1, ATTR_COUNT], dtype=float)
        stacked_unsupported_particle[:,0] = 0.5
        stacked_unsupported_particle[:,1] = self.diam

        single_stack_model = np.zeros((2, ATTR_COUNT))
        single_stack_model[:,0] = 0.5
        single_stack_model[:,2] = np.arange(0, 1.0, step=self.diam)

        with self.assertRaises(ValueError):
            _, _, _, _  = logic.place_particle(stacked_unsupported_particle[0], single_stack_model, empty_bed, self.h)

        # 4) Particle and model particle array are identical
        particle = np.zeros([1, ATTR_COUNT], dtype=float)
        particle[:,1] = self.diam

        empty_bed = np.empty((0, ATTR_COUNT))
        model = np.empty((1, ATTR_COUNT)) 
        model[0] = particle

        with self.assertRaises(ValueError):
            _, _, _, _  = logic.place_particle(particle[0], model, empty_bed, self.h)
        
    def test_good_placement_returns_valid_xy(self):
        """ A good/valid placement should return a geometrically
        correct x and y value. A good placement is a placement where
        a particle has a left and right supprting particle.

        Valid x and y is in accordance with the geometry 
        discussed here: https://math.stackexchange.com/questions/2293201/
        """
        base_elevation = 0
        left_id = 4.0
        right_id = 9.0
        expected_elevation = round(np.add(self.h, base_elevation), 2)

        placement = 0.75
        particle = np.zeros([1, ATTR_COUNT], dtype=float)
        particle[:,0] = placement

        # Create model particles with particle at index 0 being placed 
        # in between particles at index 1 and 2. 
        # particles at index 1 and 2 will be at the base_elevation
        model_particles = np.zeros([3, ATTR_COUNT], dtype=float)
        model_particles[:,1] = self.diam
        model_particles[0][0] = placement
        model_particles[1:][:,0] = np.array([placement - (self.diam/2), placement + (self.diam/2)]) 
        model_particles[1:][:,3] = np.array([left_id, right_id]) # set ids for expected supports
        model_particles[1:][:,2] = base_elevation
    
        particle = model_particles[0]
        # don't concern ourselves with the bed here
        empty_bed = np.empty((0, ATTR_COUNT))

        placed_x, placed_y, left_supp, right_supp = logic.place_particle(particle, model_particles, empty_bed, self.h)

        expected_left = left_id
        expected_right = right_id
        self.assertEqual(expected_left, left_supp)
        self.assertEqual(expected_right, right_supp)
        self.assertEqual(expected_elevation, placed_y)
        self.assertEqual(placement, placed_x)

class TestElevationList(unittest.TestCase):
    """ Test elevation_list function """
    def test_all_same_elev_returns_one_elev(self):
        same_elev = np.array((12.3, 12.3, 12.3, 12.3, 12.3))
        elev_list = logic.elevation_list(same_elev)
        asc_elev_list = logic.elevation_list(same_elev, desc=False)
        
        expected_list = np.array(12.3)
        self.assertIsNone(np.testing.assert_array_equal(expected_list, elev_list))
        self.assertIsNone(np.testing.assert_array_equal(expected_list, asc_elev_list))

    def test_all_unique_returns_same_list(self):
        unique_elev = np.arange(5, dtype=float)
        elev_list = logic.elevation_list(unique_elev)
        asc_elev_list = logic.elevation_list(unique_elev, desc=False)

        self.assertIsNone(np.testing.assert_array_equal(unique_elev[::-1], elev_list))
        self.assertIsNone(np.testing.assert_array_equal(unique_elev, asc_elev_list))
    
    def test_elevation_list_returns_unique_set(self):
        elev = np.array((12.3, 5.0, 12.3, 5.0, 1.0, 3.0))
        elev_list = logic.elevation_list(elev)
        asc_elev_list = logic.elevation_list(elev, desc=False)

        expected_desc_elev = np.array((12.3, 5.0, 3.0, 1.0))
        expected_asc_elev = expected_desc_elev[::-1]
        self.assertIsNone(np.testing.assert_array_equal(expected_desc_elev, elev_list))
        self.assertIsNone(np.testing.assert_array_equal(expected_asc_elev, asc_elev_list))

class TestComputeHops(unittest.TestCase): 
    """ Tests for the comput_hops function
    """
    def test_empty_event_particles_raises_index_error(self):
        """ If the event_particles array is empty 
        then the function should raise an indexError
        """
        mu = 0 
        sigma = 1
        model_particles = np.zeros((4,ATTR_COUNT), dtype=float)
        empty_event_particles = np.empty((0, ATTR_COUNT))

        with self.assertRaises(IndexError):
            event_particles = logic.compute_hops(empty_event_particles, model_particles, mu, sigma)
    
    def test_normal_updates_event_locations(self):
        # Testable values: 
        #   >> np.random.seed(0)
        #   >> np.random.normal(0, 1, 3)
        #   array([1.76405235, 0.40015721, 0.97873798])
        mu = 0
        sigma = 1
        model_particles = np.zeros((4, ATTR_COUNT), dtype=float)
        event_particles_idx = [0, 2, 3]

        np.random.seed(0)
        event_particles = logic.compute_hops(event_particles_idx, model_particles, mu, sigma, normal=True)
        self.assertCountEqual(np.round([1.76405235, 0.40015721, 0.97873798], 1), event_particles[:,0])

    def test_lognormal_updates_event_locations(self):
        # Testable values: 
        #   >>> np.random.seed(0)
        #   >>> np.random.lognormal(0, 0.25, 3)
        #   array([1.55428104, 1.10521435, 1.27721828])
        mu = 0
        sigma = 0.25
        model_particles = np.zeros((4, ATTR_COUNT), dtype=float)
        event_particles_idx = [0, 2, 3]

        np.random.seed(0)
        event_particles = logic.compute_hops(event_particles_idx, model_particles, mu, sigma, normal=False)
        self.assertCountEqual(np.round([1.55428104, 1.10521435, 1.27721828], 1), event_particles[:,0])


class TestMoveModelParticles(unittest.TestCase):
    """ Unit tests for move_model_particles function.

    Attributes:
        diam: diameter for the test particles
        h: float derived from diam used in the
            geometric calculations of particle elevations
    """

    def setUp(self):
        self.diam = 0.5
        d = np.divide(np.multiply(np.divide(self.diam, 2), 
                                        self.diam), 
                                        self.diam)
        self.h = np.sqrt(np.square(self.diam) - np.square(d))

    def test_empty_event_particles_returns_no_changes(self):
        """ If there are no event particles (no particles
        being entrained) then no model particles should
        be altered by move_model_particles.
        """

        empty_event_particles = np.empty((0, ATTR_COUNT))
        model_particles = np.zeros((1, ATTR_COUNT), dtype=float)
        model_supports = np.array([[1.0 , 2.0]], dtype=float)
        empty_bed = np.empty((0, ATTR_COUNT))
        available_vertices = np.empty((0))

        moved_model, moved_supports = logic.move_model_particles(empty_event_particles, 
                                                                    model_particles,
                                                                    model_supports, 
                                                                    empty_bed, 
                                                                    available_vertices, 
                                                                    self.h)
        expected_supports = np.array([[1.0 , 2.0]], dtype=float)                                                       
        self.assertIsNone(np.testing.assert_array_equal(expected_supports, moved_supports))
        self.assertIsNone(np.testing.assert_array_equal(model_particles, moved_model))

    def test_event_particle_on_desired_vertex_returns_no_changes(self):
        """ If there is 1 event particle and it is placed on a vertex x, 
        and if x is an available vertex, then move_model_particles
        should not change the model_particles array.
        """
        placement = 5

        model_particles = np.zeros((1, ATTR_COUNT), dtype=float)
        model_particles[:,0] = placement
        model_particles[:,1] = self.diam

        one_event = model_particles[[0]]
        two_bed = np.zeros((2, ATTR_COUNT))
        two_bed[0][3] = -1
        two_bed[1][3] = -2
        two_bed[0][0] = placement - (self.diam/2)
        two_bed[1][0] = placement + (self.diam/2)
        model_supports = np.array([[two_bed[0][3], two_bed[1][3]]], dtype=float)
        available_vertices = np.array([placement])

        moved_model, moved_supports = logic.move_model_particles(one_event, 
                                                                model_particles,
                                                                model_supports, 
                                                                two_bed, 
                                                                available_vertices, 
                                                                self.h)
        expected_supports = np.array([[two_bed[0][3], two_bed[1][3]]], dtype=float)
        self.assertIsNone(np.testing.assert_array_equal(expected_supports, moved_supports))
        self.assertIsNone(np.testing.assert_array_equal(moved_model, model_particles))

    def test_looped_particle_returns_nan_supports_and_incremented_counter(self):
        """ If there is 1 event particle and it is 'looped' then it's supports
        should be updated to NaN and it's loop counter should be incremented by 1.
        """
        empty_bed = np.empty((0, ATTR_COUNT))
        available_vertices = np.arange((3)) 

        model_particles = np.zeros((1, ATTR_COUNT), dtype=float)
        placement = np.max(available_vertices) + 1 # assure that the particle is being placed beyond the largest vertex
        model_particles[:,0] = placement
        ghost_event = model_particles[[0]]
        model_supports = np.array([[1.0, 5.0]], dtype=float) # random values
        moved_model, moved_supports = logic.move_model_particles(ghost_event, 
                                                                    model_particles, 
                                                                    model_supports,
                                                                    empty_bed, 
                                                                    available_vertices, 
                                                                    self.h)
        expected_counter = 1
        expected_supports = np.empty((1,2))
        expected_supports[:] = np.nan
        self.assertIsNone(np.testing.assert_array_equal(expected_supports, moved_supports))
        self.assertEqual(expected_counter, moved_model[0][6])


class TestUpdateFlux(unittest.TestCase): # Easy
    """ Unit test for the update_flux function

    Attributes: 
        test_length: An int representing the length of the test stream
        mock_subregion: A Mock of Subregion
        one_mock_subregion: A list with mock_subregion
        mock_subregion_1: A Mock of Subregion pair with mock_subregion_2
        mock_subregion_2: A Mock of Subregion pair with mock_subregion_1
        two_mock_subregion: A list with mock_subregion_1 
            and mock_subregion_2
    """
    def setUp(self):

        self.test_length = 10

        # mock one subregion
        self.mock_subregion = Mock()
        self.mock_subregion.leftBoundary.return_value = 0
        self.mock_subregion.rightBoundary.return_value = self.test_length
        self.one_mock_subregion = [self.mock_subregion]
        # mock multiple (2) subregions    
        self.mock_subregion_1 = Mock()
        self.mock_subregion_1.leftBoundary.return_value = 0
        self.mock_subregion_1.rightBoundary.return_value = self.test_length/2
        self.mock_subregion_2 = Mock()
        self.mock_subregion_2.leftBoundary.return_value = self.test_length/2
        self.mock_subregion_2.rightBoundary.return_value = self.test_length
        self.two_mock_subregion = [self.mock_subregion_1, self.mock_subregion_2]

    def test_different_lengths_raise_value_error(self):
        """ If the amount of intial positions and 
        final positions is not equal then a ValueError
        should be raised.
        """
        iteration = 0
        init_pos = np.arange(2)
        final_pos = np.arange(3)

        with self.assertRaises(ValueError):
            _ = logic.update_flux(init_pos, final_pos, iteration, self.mock_subregion)

    def test_0_crossings_call_increment_0_times(self):
        iteration = 0
        # Over a single subregion (no internal crossings possible)
        # -- Only 1 particle moving
        init_pos = np.array([5])
        final_pos = init_pos + 1
        
        subregion = logic.update_flux(init_pos, final_pos, iteration, self.one_mock_subregion)
        self.mock_subregion.incrementFlux.assert_not_called()

        # -- Multiple particles moving
        init_pos = np.array([1, 3, 5, 7, 8])
        final_pos = init_pos + 1 # No final positions >= 10
        subregion = logic.update_flux(init_pos, final_pos, iteration, self.one_mock_subregion)
        self.mock_subregion.incrementFlux.assert_not_called()

        # Over multiple subregions (internal crossings possible)
        # -- Only 1 particle moving
        init_pos = np.array([7])
        final_pos = init_pos + 1
        
        subregion = logic.update_flux(init_pos, final_pos, iteration, self.two_mock_subregion)
        self.mock_subregion.incrementFlux.assert_not_called()

        # -- Multiple particles moving
        init_pos = np.array([1, 3, 6, 7, 8])
        final_pos = init_pos + 1 # No final positions >= 10
        subregion = logic.update_flux(init_pos, final_pos, iteration, self.one_mock_subregion)
        self.mock_subregion.incrementFlux.assert_not_called()

    def test_n_crossings_call_increment_n_times(self):
        # Over one subregion
        # -- Over 1 iteration
        iteration = 0
        init_pos = np.array([5, 6, 7, 8])
        final_pos = init_pos + 5 
        subregion = logic.update_flux(init_pos, final_pos, iteration, self.one_mock_subregion)
        self.assertEqual(4, self.mock_subregion.incrementFlux.call_count)
        self.mock_subregion.incrementFlux.assert_called_with(0) # Only iteration 0
    
        # -- Over multiple subregions
        iteration = 0
        init_pos = np.array([1, 2, 3, 6, 7, 8])
        final_pos = init_pos + 5 
        subregion = logic.update_flux(init_pos, final_pos, iteration, self.two_mock_subregion)
        
        self.assertEqual(3, self.mock_subregion_1.incrementFlux.call_count)
        self.assertEqual(3, self.mock_subregion_2.incrementFlux.call_count)
        
        self.mock_subregion_1.incrementFlux.assert_called_with(0)
        self.mock_subregion_2.incrementFlux.assert_called_with(0) 

    def test_particle_crossing_multiple_subregions_calls_increment_on_each(self):
        iteration = 0
        init_pos = np.array([1])
        final_pos = np.array([11]) # Will cross 5 and 10
        subregion = logic.update_flux(init_pos, final_pos, iteration, self.two_mock_subregion)
        self.mock_subregion_1.incrementFlux.assert_called_once_with(0)
        self.mock_subregion_2.incrementFlux.assert_called_once_with(0) 

    def test_ghost_particle_calls_increment_on_final_subregion(self):
        iteration = 0
        init_pos = np.array([1, 6])
        final_pos = np.array([-1, -1])
        subregion = logic.update_flux(init_pos, final_pos, iteration, self.one_mock_subregion)
        self.assertEqual(2, self.mock_subregion.incrementFlux.call_count)
        self.mock_subregion.incrementFlux.assert_called_with(0) 

    def tearDown(self):
        self.mock_subregion.reset_mock()
        self.mock_subregion_1.reset_mock()
        self.mock_subregion_2.reset_mock()


class TestFindClosestVertex(unittest.TestCase): # Easy 
    """ Unit test for the find_closest_vertex function. 
    """
    def test_empty_available_vertices_returns_value_error(self):
        hop = 12.3
        empty_vertices = np.empty(0, dtype=float)
        with self.assertRaises(ValueError):
            _ = logic.find_closest_vertex(hop, empty_vertices)

    def test_negative_desired_hop_returns_value_error(self):
        neg_hop = -4.0
        avail_vert = np.arange(5, dtype=float)
        with self.assertRaises(ValueError):
            _ = logic.find_closest_vertex(neg_hop, avail_vert)

    def test_particle_exceeding_vertices_returns_negative_1(self):
        hop = 5.2
        avail_vert = np.arange(5, dtype=float)
        closest_vertex = logic.find_closest_vertex(hop, avail_vert)
        self.assertEqual(-1, closest_vertex)
        
    def test_valid_hop_to_vertex_returns_vertex(self):
        hop = 3.0
        avail_vert = np.arange(6, dtype=float)
        closest_vertex = logic.find_closest_vertex(hop, avail_vert)
        self.assertEqual(3.0, closest_vertex)

        last_valid_hop = 5.0
        closest_vertex = logic.find_closest_vertex(last_valid_hop, avail_vert)
        self.assertEqual(5.0, closest_vertex)

    def test_valid_hop_to_nonvertex_returns_next_vertex(self):
        hop = 3.001
        avail_vert = np.arange(6, dtype=float)
        closest_vertex = logic.find_closest_vertex(hop, avail_vert)
        self.assertEqual(4.0, closest_vertex)


class TestIncrementAge(unittest.TestCase): 
    """ Unit test for the increment_age function. 
    """
    def test_empty_event_increases_all_ages_by_1(self):
        model_particles = np.zeros([3, ATTR_COUNT], dtype=float)
        model_particles[:,3] = np.arange(3)
        event_ids = []

        aged_model = logic.increment_age(model_particles, event_ids)
        # The unmoved particles start at age 0 and we test after one iteration
        # Therefore, ages should all be 1
        expected_age = np.ones(3, dtype=float) 
        self.assertCountEqual(expected_age, aged_model[:,5])

    def test_event_set_to_0_and_nonevent_to_1(self):
        starting_age = 2.0
        model_particles = np.zeros([3, ATTR_COUNT], dtype=float)
        model_particles[:,3] = np.arange(3)
        model_particles[:,5] = starting_age
        event_ids = np.array([0, 1])

        aged_model = logic.increment_age(model_particles, event_ids)
        self.assertEqual(starting_age + 1, aged_model[2,5])
        self.assertCountEqual([0.0, 0.0], aged_model[event_ids][:,5])
        

if __name__ == '__main__':
    unittest.main()

Classes

class TestBuildStreambed (methodName='runTest')

Test build_streambed module

Create an instance of the class that will use the named test method when executed. Raises a ValueError if the instance does not have a method with the specified name.

Expand source code
class TestBuildStreambed(unittest.TestCase):
    """ Test build_streambed module """
    def test_compat_diam_returns_good_particles(self):
        """ Test that bed is 'tightly packed' meaning
        that bed particles are place beside each other 
        and that no part of any particle exists beyond 
        the range [0, bed_length]. Also test that
        attributes are initialized to proper values.
        """
        stream_length = 100
        diameter = 0.5
        expected_number_particles = stream_length / diameter

        bed_particles = logic.build_streambed(stream_length, diameter)

        self.assertEqual(len(bed_particles), expected_number_particles)

        expected_centres = np.arange(diameter/2, expected_number_particles*diameter, step=diameter)
        expected_ids = np.arange(1, int(expected_number_particles)+1)*-1
        expected_attr = np.zeros(int(expected_number_particles))
        expected_diam = np.ones(int(expected_number_particles))*diameter

        # Reverse array when testing because bed_particles array is packed from index N to 0
        self.assertIsNone(np.testing.assert_array_equal(expected_centres[::-1], bed_particles[:,0]))
        self.assertIsNone(np.testing.assert_array_equal(expected_ids[::-1], bed_particles[:,3]))
        self.assertIsNone(np.testing.assert_array_equal(expected_diam[::-1], bed_particles[:,1]))

        # attributes y, active, age, and loop should all be 0
        for attribute_idx in [2, 4, 5, 6]:
            self.assertIsNone(np.testing.assert_array_equal(expected_attr, bed_particles[:,attribute_idx]))
        
        final_particle_idx = -len(bed_particles)
        final_particle_extent = bed_particles[final_particle_idx][0] + diameter/2
        self.assertEqual(final_particle_extent, stream_length)

Ancestors

  • unittest.case.TestCase

Methods

def test_compat_diam_returns_good_particles(self)

Test that bed is 'tightly packed' meaning that bed particles are place beside each other and that no part of any particle exists beyond the range [0, bed_length]. Also test that attributes are initialized to proper values.

Expand source code
def test_compat_diam_returns_good_particles(self):
    """ Test that bed is 'tightly packed' meaning
    that bed particles are place beside each other 
    and that no part of any particle exists beyond 
    the range [0, bed_length]. Also test that
    attributes are initialized to proper values.
    """
    stream_length = 100
    diameter = 0.5
    expected_number_particles = stream_length / diameter

    bed_particles = logic.build_streambed(stream_length, diameter)

    self.assertEqual(len(bed_particles), expected_number_particles)

    expected_centres = np.arange(diameter/2, expected_number_particles*diameter, step=diameter)
    expected_ids = np.arange(1, int(expected_number_particles)+1)*-1
    expected_attr = np.zeros(int(expected_number_particles))
    expected_diam = np.ones(int(expected_number_particles))*diameter

    # Reverse array when testing because bed_particles array is packed from index N to 0
    self.assertIsNone(np.testing.assert_array_equal(expected_centres[::-1], bed_particles[:,0]))
    self.assertIsNone(np.testing.assert_array_equal(expected_ids[::-1], bed_particles[:,3]))
    self.assertIsNone(np.testing.assert_array_equal(expected_diam[::-1], bed_particles[:,1]))

    # attributes y, active, age, and loop should all be 0
    for attribute_idx in [2, 4, 5, 6]:
        self.assertIsNone(np.testing.assert_array_equal(expected_attr, bed_particles[:,attribute_idx]))
    
    final_particle_idx = -len(bed_particles)
    final_particle_extent = bed_particles[final_particle_idx][0] + diameter/2
    self.assertEqual(final_particle_extent, stream_length)
class TestComputeAvailableVerticesLifted (methodName='runTest')

Test compute_available_vertices function with the lifted argument set to True.

Attributes

stream_length
int representing length of test stream
diam
float representing diam of test particles
bed_particles
An n-7 array representing the test bed particles
expected_bed_vertices
An np array of the available vertices that an empty bed should produce if all is working well

Create an instance of the class that will use the named test method when executed. Raises a ValueError if the instance does not have a method with the specified name.

Expand source code
class TestComputeAvailableVerticesLifted(unittest.TestCase):
    """ Test compute_available_vertices function with 
    the lifted argument set to True.

    Attributes:
        stream_length: int representing length of test stream
        diam: float representing diam of test particles
        bed_particles: An n-7 array representing the test bed particles
        expected_bed_vertices: An np array of the available vertices 
            that an empty bed should produce if all is working well
    """
    def setUp(self):
        # make bed particles
        self.stream_length = 5
        self.diam = 0.5

        # Mock a full bed_particles array
        num_bed_particles = int(self.stream_length/self.diam) # 10 bed particles
        bed_particles = np.zeros([num_bed_particles, ATTR_COUNT], dtype=float)
        bed_particles[:,0] = np.arange(self.diam/2, self.stream_length+(self.diam/2), step=self.diam)
        bed_particles[:,3] = np.arange(num_bed_particles) # unique ids
        self.bed_particles = bed_particles

        self.expected_bed_vertices = np.arange(self.diam, self.stream_length, step=self.diam)

    def test_only_bed_and_empty_lifted_returns_expected_bed_vert(self):
        """If there are no model particles, and the lifted array is 
        empty, then the available vertices should = expected_bed_vertices
        """
        level_limit = 3 # Arbitrary level limit
        empty_model_particles = np.empty((0, ATTR_COUNT))

        # Bed of length n should return n-1 available vertices
        available_vertices = logic.compute_available_vertices(empty_model_particles, self.bed_particles, self.diam, 
                                            level_limit=level_limit, lifted_particles=[])
        self.assertEqual(len(self.bed_particles)-1, len(available_vertices))
        self.assertCountEqual(available_vertices, self.expected_bed_vertices)
    
    def test_bed_and_all_model_lifted_returns_expected_bed_vertices(self):
        """If there are N model particles resting directly on the bed and 
        the lifted array has all N model particle ids in it, then the available 
        vertices should = expected_bed_vertices
        """
        level_limit = 3
        num_model_particles = 3

        model_particles = np.zeros([num_model_particles, ATTR_COUNT], dtype=float)
        # Particles will be at the first 3 available vertices
        model_particles[:,0] = self.expected_bed_vertices[0:3]
        model_particles[:,3] = np.arange(num_model_particles) 
        # Bed of length n should return n-1 available vertices
        available_vertices = logic.compute_available_vertices(model_particles, self.bed_particles, self.diam, 
                                            level_limit=level_limit, lifted_particles=model_particles[:,3].astype(int))
        
        self.assertEqual(len(available_vertices), len(self.bed_particles)-1)
        self.assertCountEqual(available_vertices, self.expected_bed_vertices)

    def test_not_touching_and_one_lifted_model_returns_valid_vertices(self):
        """ If there are N model particles resting directly on the bed and K are 
        lifted then the available vertices should be 
            expected_bed_vertices - (x locations of the N-K unlifted particles)
        """
        level_limit = 3
        num_model_particles = 3

        model_particles = np.zeros([num_model_particles, ATTR_COUNT], dtype=float)
        # Particles will be at the first 3 available vertices
        model_particles[:,0] = self.expected_bed_vertices[0:3] 
        model_particles[:,3] = np.arange(num_model_particles)

        # Lift first particle, keep later 2 particles -- t/f locations of first particles should be be available
        # and locations of second and third particle should not be avaliable
        available_vertices = logic.compute_available_vertices(model_particles, self.bed_particles, self.diam, 
                                        level_limit=level_limit, lifted_particles=model_particles[0][3].astype(int))

        expected_vertices = np.delete(self.expected_bed_vertices, [1,2])
        self.assertEqual(len(available_vertices), len(expected_vertices))
        self.assertCountEqual(available_vertices, expected_vertices)

Ancestors

  • unittest.case.TestCase

Methods

def setUp(self)

Hook method for setting up the test fixture before exercising it.

Expand source code
def setUp(self):
    # make bed particles
    self.stream_length = 5
    self.diam = 0.5

    # Mock a full bed_particles array
    num_bed_particles = int(self.stream_length/self.diam) # 10 bed particles
    bed_particles = np.zeros([num_bed_particles, ATTR_COUNT], dtype=float)
    bed_particles[:,0] = np.arange(self.diam/2, self.stream_length+(self.diam/2), step=self.diam)
    bed_particles[:,3] = np.arange(num_bed_particles) # unique ids
    self.bed_particles = bed_particles

    self.expected_bed_vertices = np.arange(self.diam, self.stream_length, step=self.diam)
def test_bed_and_all_model_lifted_returns_expected_bed_vertices(self)

If there are N model particles resting directly on the bed and the lifted array has all N model particle ids in it, then the available vertices should = expected_bed_vertices

Expand source code
def test_bed_and_all_model_lifted_returns_expected_bed_vertices(self):
    """If there are N model particles resting directly on the bed and 
    the lifted array has all N model particle ids in it, then the available 
    vertices should = expected_bed_vertices
    """
    level_limit = 3
    num_model_particles = 3

    model_particles = np.zeros([num_model_particles, ATTR_COUNT], dtype=float)
    # Particles will be at the first 3 available vertices
    model_particles[:,0] = self.expected_bed_vertices[0:3]
    model_particles[:,3] = np.arange(num_model_particles) 
    # Bed of length n should return n-1 available vertices
    available_vertices = logic.compute_available_vertices(model_particles, self.bed_particles, self.diam, 
                                        level_limit=level_limit, lifted_particles=model_particles[:,3].astype(int))
    
    self.assertEqual(len(available_vertices), len(self.bed_particles)-1)
    self.assertCountEqual(available_vertices, self.expected_bed_vertices)
def test_not_touching_and_one_lifted_model_returns_valid_vertices(self)

If there are N model particles resting directly on the bed and K are lifted then the available vertices should be expected_bed_vertices - (x locations of the N-K unlifted particles)

Expand source code
def test_not_touching_and_one_lifted_model_returns_valid_vertices(self):
    """ If there are N model particles resting directly on the bed and K are 
    lifted then the available vertices should be 
        expected_bed_vertices - (x locations of the N-K unlifted particles)
    """
    level_limit = 3
    num_model_particles = 3

    model_particles = np.zeros([num_model_particles, ATTR_COUNT], dtype=float)
    # Particles will be at the first 3 available vertices
    model_particles[:,0] = self.expected_bed_vertices[0:3] 
    model_particles[:,3] = np.arange(num_model_particles)

    # Lift first particle, keep later 2 particles -- t/f locations of first particles should be be available
    # and locations of second and third particle should not be avaliable
    available_vertices = logic.compute_available_vertices(model_particles, self.bed_particles, self.diam, 
                                    level_limit=level_limit, lifted_particles=model_particles[0][3].astype(int))

    expected_vertices = np.delete(self.expected_bed_vertices, [1,2])
    self.assertEqual(len(available_vertices), len(expected_vertices))
    self.assertCountEqual(available_vertices, expected_vertices)
def test_only_bed_and_empty_lifted_returns_expected_bed_vert(self)

If there are no model particles, and the lifted array is empty, then the available vertices should = expected_bed_vertices

Expand source code
def test_only_bed_and_empty_lifted_returns_expected_bed_vert(self):
    """If there are no model particles, and the lifted array is 
    empty, then the available vertices should = expected_bed_vertices
    """
    level_limit = 3 # Arbitrary level limit
    empty_model_particles = np.empty((0, ATTR_COUNT))

    # Bed of length n should return n-1 available vertices
    available_vertices = logic.compute_available_vertices(empty_model_particles, self.bed_particles, self.diam, 
                                        level_limit=level_limit, lifted_particles=[])
    self.assertEqual(len(self.bed_particles)-1, len(available_vertices))
    self.assertCountEqual(available_vertices, self.expected_bed_vertices)
class TestComputeAvailableVerticesNotLifted (methodName='runTest')

Test compute_available_vertices function with the lifted argument set to False.

Attributes:
    stream_length: int representing length of test stream
    diam: float representing diam of test particles
    bed_particles: An n-7 array representing the test bed particles
    expected_bed_vertices: An np array of the available vertices 
        that an empty bed should produce if all is working well

Create an instance of the class that will use the named test method when executed. Raises a ValueError if the instance does not have a method with the specified name.

Expand source code
class TestComputeAvailableVerticesNotLifted(unittest.TestCase):
    """ Test compute_available_vertices function with 
    the lifted argument set to False.

        Attributes:
            stream_length: int representing length of test stream
            diam: float representing diam of test particles
            bed_particles: An n-7 array representing the test bed particles
            expected_bed_vertices: An np array of the available vertices 
                that an empty bed should produce if all is working well
    """
    def setUp(self):
        # make bed particles
        self.stream_length = 5
        self.diam = 0.5

        # Mock a full bed_particles array
        num_bed_particles = int(self.stream_length/self.diam) # 10 bed particles
        bed_particles = np.zeros([num_bed_particles, ATTR_COUNT], dtype=float)
        bed_particles[:,0] = np.arange(self.diam/2, self.stream_length+(self.diam/2), step=self.diam)
        bed_particles[:,3] = np.arange(num_bed_particles) # unique ids
        self.bed_particles = bed_particles

        self.expected_bed_vertices = np.arange(self.diam, self.stream_length, step=self.diam)
    
    def test_only_bed_returns_expected_bed_vertices(self):
        """If there are no model particles then the
        available vertices should = expected_bed_vertices 
        """
        level_limit = 3 # Arbitrary level limit
        empty_model_particles = np.empty((0, ATTR_COUNT))

        # Bed of length n should return n-1 available vertices
        available_vertices = logic.compute_available_vertices(empty_model_particles, self.bed_particles, self.diam, 
                                            level_limit=level_limit)
        
        self.assertEqual(len(self.bed_particles)-1, len(available_vertices))
        self.assertCountEqual(available_vertices, self.expected_bed_vertices)

    def test_one_model_particle_returns_bed_available_minus_one(self):
        """If there is 1 model particle resting directly on the bed 
        then the available vertices should be
            expected_bed_vertices - (x location of the 1 particle)
        """
        level_limit = 3 # Arbitrary level limit
        one_particle = np.array([[self.diam, 0, 0, 0, 0, 0, 0]]) # at first resting spot
        available_vertices = logic.compute_available_vertices(one_particle, self.bed_particles, self.diam, 
                                            level_limit=level_limit)
        
        # Assert there is no available vertex at one_particle[0][0]
        self.assertNotIn(one_particle[0][0], available_vertices)
        self.assertEqual(len(self.bed_particles)-2, len(available_vertices))
        
        expected_vertices = np.delete(self.expected_bed_vertices, 0) # bed minus first available vertex
        self.assertCountEqual(available_vertices, expected_vertices)

    def test_m_model_particles_return_bed_available_minus_m(self):
        """If there are M model particles resting directly on the bed 
        then the available vertices should be
            expected_bed_vertices - (x location of the M particles)
        """
        level_limit = 3 # Arbitrary level limit
        m_particles = 4
        model_particles = np.zeros([m_particles, ATTR_COUNT], dtype=float)
        # Place m model particles 2 resting spots away from each other
        placement_idxs = [0, 2, 4, 6]
        model_particles[0][0] = self.expected_bed_vertices[placement_idxs[0]]
        model_particles[1][0] = self.expected_bed_vertices[placement_idxs[1]]
        model_particles[2][0] = self.expected_bed_vertices[placement_idxs[2]]
        model_particles[3][0] = self.expected_bed_vertices[placement_idxs[3]]

        available_vertices = logic.compute_available_vertices(model_particles, self.bed_particles, self.diam, 
                                            level_limit=level_limit)
        
        self.assertEqual(len(self.bed_particles)-m_particles-1, len(available_vertices))
        
        expected_vertices = np.delete(self.expected_bed_vertices, placement_idxs)
        self.assertCountEqual(available_vertices, expected_vertices)

    def test_no_available_vertices_returns_empty_array(self):
        """If the stream has no spots that a particle
        could rest validly then the returned available vertices
        should be an empty array. An example of this scenario is:

            The bed resting spots are fully saturated with model particles 
            BUT none are touching so no new vertices are being made
        """
        level_limit = 3 # Arbitrary level limit
        model_particles = np.zeros([len(self.bed_particles)-1, ATTR_COUNT], dtype=float)
        model_particles[:,0] = self.expected_bed_vertices

        available_vertices = logic.compute_available_vertices(model_particles, self.bed_particles, self.diam, 
                                            level_limit=level_limit)
        self.assertEqual(0, len(available_vertices))
    
    def test_two_touching_model_and_empty_bed_return_one_valid_vertex(self):
        """ If there are two particles at the same elevation and 
        their centres (x,y) are exactly a diam length away then 
        the two particles are touching. Touching particles should 
        one new vertex at the location the particles touch.
        
        NOTE: these tests use an empty bed array to directly test the behaviour
            of touching particles in a simpler manner (simpler elevation). If 
            the bed was not empty it would make no difference.
        """
        level_limit = 3 # Arbitrary level limit

        model_particles = np.zeros([2, ATTR_COUNT], dtype=float)
        model_particles[:,0] = np.arange(0.5, 1.5, step=self.diam) # These particles will be touching
        empty_bed = np.empty((0, ATTR_COUNT))

        available_vertices = logic.compute_available_vertices(model_particles, empty_bed, self.diam, 
                                            level_limit=level_limit)
        expected_new_vertex = 0.75
        self.assertEqual(len(available_vertices), 1)
        self.assertEqual(available_vertices, expected_new_vertex)
    
    def test_two_model_touching_at_diff_elev_return_no_vertex(self):
        """ If two particles have centres that are a diameter
        length away from each other but their elevations are different
        then they are NOT touching. They should not create a new vertex.
        
        NOTE: these tests use an empty bed array to directly test the behaviour
            of touching particles in a simpler manner (simpler elevation). If 
            the bed was not empty it would make no difference.
        """
        level_limit = 3 # Arbitrary level limit

        model_particles = np.zeros([2, ATTR_COUNT], dtype=float)
        model_particles[:,0] = np.arange(0.5, 1.5, step=self.diam) # These particles will be touching
        # Place them at different elevations
        model_particles[0][2] = 0
        model_particles[1][2] = 1
        empty_bed = np.empty((0, ATTR_COUNT))

        available_vertices = logic.compute_available_vertices(model_particles, empty_bed, self.diam, 
                                            level_limit=level_limit)
        
        self.assertEqual(len(available_vertices), 0)

    def test_3triangle_and_empty_bed_returns_empty_array(self):
        """ A 3-triangle of particles is: two particles touching 
        and one particle resting on the vertex created by the two 
        touching particles. A 3-triangle should not create
        any available vertices in the stream.
       
        NOTE: these tests use an empty bed array to directly test the behaviour
            of touching particles in a simpler manner (simpler elevation). If 
            the bed was not empty it would make no difference.
        """
        level_limit = 3 # Level limit > 2
        model_particles = np.zeros([3, ATTR_COUNT], dtype=float)
        # 3 triangle: 2 particles touching, 1 particle resting above/between 
        model_particles[0:2][:,0] = np.arange(0.5, 1.5, step=self.diam) 
        model_particles[2:][:,0] = np.add(0.5, np.divide(0.5, 2))

        d = np.divide(np.multiply(np.divide(self.diam, 2), 
                                        self.diam), 
                                        self.diam)
        h = np.sqrt(np.square(self.diam) - np.square(d))
        elevation = round(np.add(h, 0), 2)
        model_particles[2:][:,2] = elevation

        empty_bed = np.empty((0, ATTR_COUNT))

        available_vertices = logic.compute_available_vertices(model_particles, empty_bed, self.diam, 
                                            level_limit=level_limit)

        self.assertEqual(0, len(available_vertices))                    

    def test_above_level_limit_returns_empty_array(self):
        """ If the level limit is K and and there are
        K level/stacks of particles then no particles 
        touching at level K can create a new vertex.

        To simplify, if the level limit is 1, and if 
        there are N model particles resting directly
        on the bed, and if all model particles are
        touching, available vertices should be empty

         NOTE: these tests use an empty bed array to directly test the behaviour
            of touching particles in a more simple manner. If the bed was
            not empty we have to take into account the bed vertices.
        """
        level_limit = 1 # Only one level of stacking allowed
        model_particles = np.zeros([5, ATTR_COUNT], dtype=float)
        # 3 triangle: 2 particles touching, 1 particle resting above/between 
        model_particles[0:3][:,0] = np.arange(0.5, 2.0, step=self.diam) 
        model_particles[3:][:,0] = np.arange(0.75, 1.5, step=self.diam)
        
        d = np.divide(np.multiply(np.divide(self.diam, 2), 
                                        self.diam), 
                                        self.diam)
        h = np.sqrt(np.square(self.diam) - np.square(d))
        elevation = round(np.add(h, 0), 2)
        model_particles[3:][:,2] = elevation

        empty_bed = np.empty((0, ATTR_COUNT))

        available_vertices = logic.compute_available_vertices(model_particles, empty_bed, self.diam, 
                                            level_limit=level_limit)
        self.assertEqual(0, len(available_vertices))

Ancestors

  • unittest.case.TestCase

Methods

def setUp(self)

Hook method for setting up the test fixture before exercising it.

Expand source code
def setUp(self):
    # make bed particles
    self.stream_length = 5
    self.diam = 0.5

    # Mock a full bed_particles array
    num_bed_particles = int(self.stream_length/self.diam) # 10 bed particles
    bed_particles = np.zeros([num_bed_particles, ATTR_COUNT], dtype=float)
    bed_particles[:,0] = np.arange(self.diam/2, self.stream_length+(self.diam/2), step=self.diam)
    bed_particles[:,3] = np.arange(num_bed_particles) # unique ids
    self.bed_particles = bed_particles

    self.expected_bed_vertices = np.arange(self.diam, self.stream_length, step=self.diam)
def test_3triangle_and_empty_bed_returns_empty_array(self)

A 3-triangle of particles is: two particles touching and one particle resting on the vertex created by the two touching particles. A 3-triangle should not create any available vertices in the stream.

NOTE: these tests use an empty bed array to directly test the behaviour of touching particles in a simpler manner (simpler elevation). If the bed was not empty it would make no difference.

Expand source code
def test_3triangle_and_empty_bed_returns_empty_array(self):
    """ A 3-triangle of particles is: two particles touching 
    and one particle resting on the vertex created by the two 
    touching particles. A 3-triangle should not create
    any available vertices in the stream.
   
    NOTE: these tests use an empty bed array to directly test the behaviour
        of touching particles in a simpler manner (simpler elevation). If 
        the bed was not empty it would make no difference.
    """
    level_limit = 3 # Level limit > 2
    model_particles = np.zeros([3, ATTR_COUNT], dtype=float)
    # 3 triangle: 2 particles touching, 1 particle resting above/between 
    model_particles[0:2][:,0] = np.arange(0.5, 1.5, step=self.diam) 
    model_particles[2:][:,0] = np.add(0.5, np.divide(0.5, 2))

    d = np.divide(np.multiply(np.divide(self.diam, 2), 
                                    self.diam), 
                                    self.diam)
    h = np.sqrt(np.square(self.diam) - np.square(d))
    elevation = round(np.add(h, 0), 2)
    model_particles[2:][:,2] = elevation

    empty_bed = np.empty((0, ATTR_COUNT))

    available_vertices = logic.compute_available_vertices(model_particles, empty_bed, self.diam, 
                                        level_limit=level_limit)

    self.assertEqual(0, len(available_vertices))                    
def test_above_level_limit_returns_empty_array(self)

If the level limit is K and and there are K level/stacks of particles then no particles touching at level K can create a new vertex.

To simplify, if the level limit is 1, and if there are N model particles resting directly on the bed, and if all model particles are touching, available vertices should be empty

NOTE: these tests use an empty bed array to directly test the behaviour of touching particles in a more simple manner. If the bed was not empty we have to take into account the bed vertices.

Expand source code
def test_above_level_limit_returns_empty_array(self):
    """ If the level limit is K and and there are
    K level/stacks of particles then no particles 
    touching at level K can create a new vertex.

    To simplify, if the level limit is 1, and if 
    there are N model particles resting directly
    on the bed, and if all model particles are
    touching, available vertices should be empty

     NOTE: these tests use an empty bed array to directly test the behaviour
        of touching particles in a more simple manner. If the bed was
        not empty we have to take into account the bed vertices.
    """
    level_limit = 1 # Only one level of stacking allowed
    model_particles = np.zeros([5, ATTR_COUNT], dtype=float)
    # 3 triangle: 2 particles touching, 1 particle resting above/between 
    model_particles[0:3][:,0] = np.arange(0.5, 2.0, step=self.diam) 
    model_particles[3:][:,0] = np.arange(0.75, 1.5, step=self.diam)
    
    d = np.divide(np.multiply(np.divide(self.diam, 2), 
                                    self.diam), 
                                    self.diam)
    h = np.sqrt(np.square(self.diam) - np.square(d))
    elevation = round(np.add(h, 0), 2)
    model_particles[3:][:,2] = elevation

    empty_bed = np.empty((0, ATTR_COUNT))

    available_vertices = logic.compute_available_vertices(model_particles, empty_bed, self.diam, 
                                        level_limit=level_limit)
    self.assertEqual(0, len(available_vertices))
def test_m_model_particles_return_bed_available_minus_m(self)

If there are M model particles resting directly on the bed then the available vertices should be expected_bed_vertices - (x location of the M particles)

Expand source code
def test_m_model_particles_return_bed_available_minus_m(self):
    """If there are M model particles resting directly on the bed 
    then the available vertices should be
        expected_bed_vertices - (x location of the M particles)
    """
    level_limit = 3 # Arbitrary level limit
    m_particles = 4
    model_particles = np.zeros([m_particles, ATTR_COUNT], dtype=float)
    # Place m model particles 2 resting spots away from each other
    placement_idxs = [0, 2, 4, 6]
    model_particles[0][0] = self.expected_bed_vertices[placement_idxs[0]]
    model_particles[1][0] = self.expected_bed_vertices[placement_idxs[1]]
    model_particles[2][0] = self.expected_bed_vertices[placement_idxs[2]]
    model_particles[3][0] = self.expected_bed_vertices[placement_idxs[3]]

    available_vertices = logic.compute_available_vertices(model_particles, self.bed_particles, self.diam, 
                                        level_limit=level_limit)
    
    self.assertEqual(len(self.bed_particles)-m_particles-1, len(available_vertices))
    
    expected_vertices = np.delete(self.expected_bed_vertices, placement_idxs)
    self.assertCountEqual(available_vertices, expected_vertices)
def test_no_available_vertices_returns_empty_array(self)

If the stream has no spots that a particle could rest validly then the returned available vertices should be an empty array. An example of this scenario is:

The bed resting spots are fully saturated with model particles 
BUT none are touching so no new vertices are being made
Expand source code
def test_no_available_vertices_returns_empty_array(self):
    """If the stream has no spots that a particle
    could rest validly then the returned available vertices
    should be an empty array. An example of this scenario is:

        The bed resting spots are fully saturated with model particles 
        BUT none are touching so no new vertices are being made
    """
    level_limit = 3 # Arbitrary level limit
    model_particles = np.zeros([len(self.bed_particles)-1, ATTR_COUNT], dtype=float)
    model_particles[:,0] = self.expected_bed_vertices

    available_vertices = logic.compute_available_vertices(model_particles, self.bed_particles, self.diam, 
                                        level_limit=level_limit)
    self.assertEqual(0, len(available_vertices))
def test_one_model_particle_returns_bed_available_minus_one(self)

If there is 1 model particle resting directly on the bed then the available vertices should be expected_bed_vertices - (x location of the 1 particle)

Expand source code
def test_one_model_particle_returns_bed_available_minus_one(self):
    """If there is 1 model particle resting directly on the bed 
    then the available vertices should be
        expected_bed_vertices - (x location of the 1 particle)
    """
    level_limit = 3 # Arbitrary level limit
    one_particle = np.array([[self.diam, 0, 0, 0, 0, 0, 0]]) # at first resting spot
    available_vertices = logic.compute_available_vertices(one_particle, self.bed_particles, self.diam, 
                                        level_limit=level_limit)
    
    # Assert there is no available vertex at one_particle[0][0]
    self.assertNotIn(one_particle[0][0], available_vertices)
    self.assertEqual(len(self.bed_particles)-2, len(available_vertices))
    
    expected_vertices = np.delete(self.expected_bed_vertices, 0) # bed minus first available vertex
    self.assertCountEqual(available_vertices, expected_vertices)
def test_only_bed_returns_expected_bed_vertices(self)

If there are no model particles then the available vertices should = expected_bed_vertices

Expand source code
def test_only_bed_returns_expected_bed_vertices(self):
    """If there are no model particles then the
    available vertices should = expected_bed_vertices 
    """
    level_limit = 3 # Arbitrary level limit
    empty_model_particles = np.empty((0, ATTR_COUNT))

    # Bed of length n should return n-1 available vertices
    available_vertices = logic.compute_available_vertices(empty_model_particles, self.bed_particles, self.diam, 
                                        level_limit=level_limit)
    
    self.assertEqual(len(self.bed_particles)-1, len(available_vertices))
    self.assertCountEqual(available_vertices, self.expected_bed_vertices)
def test_two_model_touching_at_diff_elev_return_no_vertex(self)

If two particles have centres that are a diameter length away from each other but their elevations are different then they are NOT touching. They should not create a new vertex.

NOTE: these tests use an empty bed array to directly test the behaviour of touching particles in a simpler manner (simpler elevation). If the bed was not empty it would make no difference.

Expand source code
def test_two_model_touching_at_diff_elev_return_no_vertex(self):
    """ If two particles have centres that are a diameter
    length away from each other but their elevations are different
    then they are NOT touching. They should not create a new vertex.
    
    NOTE: these tests use an empty bed array to directly test the behaviour
        of touching particles in a simpler manner (simpler elevation). If 
        the bed was not empty it would make no difference.
    """
    level_limit = 3 # Arbitrary level limit

    model_particles = np.zeros([2, ATTR_COUNT], dtype=float)
    model_particles[:,0] = np.arange(0.5, 1.5, step=self.diam) # These particles will be touching
    # Place them at different elevations
    model_particles[0][2] = 0
    model_particles[1][2] = 1
    empty_bed = np.empty((0, ATTR_COUNT))

    available_vertices = logic.compute_available_vertices(model_particles, empty_bed, self.diam, 
                                        level_limit=level_limit)
    
    self.assertEqual(len(available_vertices), 0)
def test_two_touching_model_and_empty_bed_return_one_valid_vertex(self)

If there are two particles at the same elevation and their centres (x,y) are exactly a diam length away then the two particles are touching. Touching particles should one new vertex at the location the particles touch.

NOTE: these tests use an empty bed array to directly test the behaviour of touching particles in a simpler manner (simpler elevation). If the bed was not empty it would make no difference.

Expand source code
def test_two_touching_model_and_empty_bed_return_one_valid_vertex(self):
    """ If there are two particles at the same elevation and 
    their centres (x,y) are exactly a diam length away then 
    the two particles are touching. Touching particles should 
    one new vertex at the location the particles touch.
    
    NOTE: these tests use an empty bed array to directly test the behaviour
        of touching particles in a simpler manner (simpler elevation). If 
        the bed was not empty it would make no difference.
    """
    level_limit = 3 # Arbitrary level limit

    model_particles = np.zeros([2, ATTR_COUNT], dtype=float)
    model_particles[:,0] = np.arange(0.5, 1.5, step=self.diam) # These particles will be touching
    empty_bed = np.empty((0, ATTR_COUNT))

    available_vertices = logic.compute_available_vertices(model_particles, empty_bed, self.diam, 
                                        level_limit=level_limit)
    expected_new_vertex = 0.75
    self.assertEqual(len(available_vertices), 1)
    self.assertEqual(available_vertices, expected_new_vertex)
class TestComputeHops (methodName='runTest')

Tests for the comput_hops function

Create an instance of the class that will use the named test method when executed. Raises a ValueError if the instance does not have a method with the specified name.

Expand source code
class TestComputeHops(unittest.TestCase): 
    """ Tests for the comput_hops function
    """
    def test_empty_event_particles_raises_index_error(self):
        """ If the event_particles array is empty 
        then the function should raise an indexError
        """
        mu = 0 
        sigma = 1
        model_particles = np.zeros((4,ATTR_COUNT), dtype=float)
        empty_event_particles = np.empty((0, ATTR_COUNT))

        with self.assertRaises(IndexError):
            event_particles = logic.compute_hops(empty_event_particles, model_particles, mu, sigma)
    
    def test_normal_updates_event_locations(self):
        # Testable values: 
        #   >> np.random.seed(0)
        #   >> np.random.normal(0, 1, 3)
        #   array([1.76405235, 0.40015721, 0.97873798])
        mu = 0
        sigma = 1
        model_particles = np.zeros((4, ATTR_COUNT), dtype=float)
        event_particles_idx = [0, 2, 3]

        np.random.seed(0)
        event_particles = logic.compute_hops(event_particles_idx, model_particles, mu, sigma, normal=True)
        self.assertCountEqual(np.round([1.76405235, 0.40015721, 0.97873798], 1), event_particles[:,0])

    def test_lognormal_updates_event_locations(self):
        # Testable values: 
        #   >>> np.random.seed(0)
        #   >>> np.random.lognormal(0, 0.25, 3)
        #   array([1.55428104, 1.10521435, 1.27721828])
        mu = 0
        sigma = 0.25
        model_particles = np.zeros((4, ATTR_COUNT), dtype=float)
        event_particles_idx = [0, 2, 3]

        np.random.seed(0)
        event_particles = logic.compute_hops(event_particles_idx, model_particles, mu, sigma, normal=False)
        self.assertCountEqual(np.round([1.55428104, 1.10521435, 1.27721828], 1), event_particles[:,0])

Ancestors

  • unittest.case.TestCase

Methods

def test_empty_event_particles_raises_index_error(self)

If the event_particles array is empty then the function should raise an indexError

Expand source code
def test_empty_event_particles_raises_index_error(self):
    """ If the event_particles array is empty 
    then the function should raise an indexError
    """
    mu = 0 
    sigma = 1
    model_particles = np.zeros((4,ATTR_COUNT), dtype=float)
    empty_event_particles = np.empty((0, ATTR_COUNT))

    with self.assertRaises(IndexError):
        event_particles = logic.compute_hops(empty_event_particles, model_particles, mu, sigma)
def test_lognormal_updates_event_locations(self)
Expand source code
def test_lognormal_updates_event_locations(self):
    # Testable values: 
    #   >>> np.random.seed(0)
    #   >>> np.random.lognormal(0, 0.25, 3)
    #   array([1.55428104, 1.10521435, 1.27721828])
    mu = 0
    sigma = 0.25
    model_particles = np.zeros((4, ATTR_COUNT), dtype=float)
    event_particles_idx = [0, 2, 3]

    np.random.seed(0)
    event_particles = logic.compute_hops(event_particles_idx, model_particles, mu, sigma, normal=False)
    self.assertCountEqual(np.round([1.55428104, 1.10521435, 1.27721828], 1), event_particles[:,0])
def test_normal_updates_event_locations(self)
Expand source code
def test_normal_updates_event_locations(self):
    # Testable values: 
    #   >> np.random.seed(0)
    #   >> np.random.normal(0, 1, 3)
    #   array([1.76405235, 0.40015721, 0.97873798])
    mu = 0
    sigma = 1
    model_particles = np.zeros((4, ATTR_COUNT), dtype=float)
    event_particles_idx = [0, 2, 3]

    np.random.seed(0)
    event_particles = logic.compute_hops(event_particles_idx, model_particles, mu, sigma, normal=True)
    self.assertCountEqual(np.round([1.76405235, 0.40015721, 0.97873798], 1), event_particles[:,0])
class TestDefineSubregions (methodName='runTest')

Test define subregions module

Attributes

bed_length
the length of the bed the subregion is being defined on/in
iterations
the number of iterations that a subregion needs to maintain data for

Create an instance of the class that will use the named test method when executed. Raises a ValueError if the instance does not have a method with the specified name.

Expand source code
class TestDefineSubregions(unittest.TestCase):
    """ Test define subregions module

    Attributes:
       bed_length: the length of the bed the subregion 
            is being defined on/in
       iterations: the number of iterations that a 
            subregion needs to maintain data for
    """
    def setUp(self):
        self.bed_length = 10
        self.iterations = 10

    def test_good_parameters_return_good_subregion_list(self):
        """ If the bed length is divisible by the number of
        subregions, and the iterations and bed length are valid
        int inputs then the function should return a list of 
        Subregion objects whose boundaries overlap exactly 
        with the length of the stream and each other. 
        For example:
            if bed_length = 2, num_subregions = 2 then a 
            valid list will be [subregion_1, subregion_2]
            where:
                subregion_1.leftBoundary() = 0
                subregion_1.leftBoundary() = 1
                subregion_2.leftBoundary() = 1
                subregion_2.rightBoundary() = 2             
        """
        subregion_count_even = 2
        left_boundary = 0
        middle_boundary = self.bed_length / 2
        right_boundary = self.bed_length

        subregion_list = logic.define_subregions(self.bed_length, 
                                                subregion_count_even, 
                                                self.iterations)
        # Check number of subregions
        self.assertEqual(len(subregion_list), 2) 
        # Check boundary definitions
        self.assertEqual(subregion_list[0].leftBoundary(), left_boundary)
        self.assertEqual(subregion_list[0].rightBoundary(), middle_boundary)
        self.assertEqual(subregion_list[1].leftBoundary(), middle_boundary)
        self.assertEqual(subregion_list[1].rightBoundary(), right_boundary)

        subregion_count_odd = 5
        sub_length = self.bed_length / subregion_count_odd

        left_boundary = 0
        middle_boundary_1 = left_boundary + sub_length*1
        middle_boundary_2 = left_boundary + sub_length*2
        middle_boundary_3 = left_boundary + sub_length*3
        middle_boundary_4 = left_boundary + sub_length*4
        right_boundary = self.bed_length

        subregion_list_odd = logic.define_subregions(self.bed_length, 
                                                subregion_count_odd, 
                                                self.iterations)
        # Check number of subregions
        self.assertEqual(len(subregion_list_odd), 5) 
        # Check boundary definitions
        self.assertEqual(subregion_list_odd[0].leftBoundary(), left_boundary)
        self.assertEqual(subregion_list_odd[0].rightBoundary(), middle_boundary_1)

        self.assertEqual(subregion_list_odd[1].leftBoundary(), middle_boundary_1)
        self.assertEqual(subregion_list_odd[1].rightBoundary(), middle_boundary_2)

        self.assertEqual(subregion_list_odd[2].leftBoundary(), middle_boundary_2)
        self.assertEqual(subregion_list_odd[2].rightBoundary(), middle_boundary_3)

        self.assertEqual(subregion_list_odd[3].leftBoundary(), middle_boundary_3)
        self.assertEqual(subregion_list_odd[3].rightBoundary(), middle_boundary_4)

        self.assertEqual(subregion_list_odd[4].leftBoundary(), middle_boundary_4)
        self.assertEqual(subregion_list_odd[4].rightBoundary(), right_boundary)
    
    def test_all_subregion_flux_are_init_0(self):
        """
        All values in flux_lists should be 0 at initialization.
        """
        subregion_count_even = 2
        empty_list = np.zeros(self.iterations, dtype=np.int64)
        subregion_list_even = logic.define_subregions(self.bed_length, 
                                                subregion_count_even, 
                                                self.iterations)
        
        for subregion in subregion_list_even:
            self.assertEqual(len(subregion.getFluxList()), self.iterations)
            self.assertCountEqual(subregion.getFluxList(), empty_list)

Ancestors

  • unittest.case.TestCase

Methods

def setUp(self)

Hook method for setting up the test fixture before exercising it.

Expand source code
def setUp(self):
    self.bed_length = 10
    self.iterations = 10
def test_all_subregion_flux_are_init_0(self)

All values in flux_lists should be 0 at initialization.

Expand source code
def test_all_subregion_flux_are_init_0(self):
    """
    All values in flux_lists should be 0 at initialization.
    """
    subregion_count_even = 2
    empty_list = np.zeros(self.iterations, dtype=np.int64)
    subregion_list_even = logic.define_subregions(self.bed_length, 
                                            subregion_count_even, 
                                            self.iterations)
    
    for subregion in subregion_list_even:
        self.assertEqual(len(subregion.getFluxList()), self.iterations)
        self.assertCountEqual(subregion.getFluxList(), empty_list)
def test_good_parameters_return_good_subregion_list(self)

If the bed length is divisible by the number of subregions, and the iterations and bed length are valid int inputs then the function should return a list of Subregion objects whose boundaries overlap exactly with the length of the stream and each other. For example: if bed_length = 2, num_subregions = 2 then a valid list will be [subregion_1, subregion_2] where: subregion_1.leftBoundary() = 0 subregion_1.leftBoundary() = 1 subregion_2.leftBoundary() = 1 subregion_2.rightBoundary() = 2

Expand source code
def test_good_parameters_return_good_subregion_list(self):
    """ If the bed length is divisible by the number of
    subregions, and the iterations and bed length are valid
    int inputs then the function should return a list of 
    Subregion objects whose boundaries overlap exactly 
    with the length of the stream and each other. 
    For example:
        if bed_length = 2, num_subregions = 2 then a 
        valid list will be [subregion_1, subregion_2]
        where:
            subregion_1.leftBoundary() = 0
            subregion_1.leftBoundary() = 1
            subregion_2.leftBoundary() = 1
            subregion_2.rightBoundary() = 2             
    """
    subregion_count_even = 2
    left_boundary = 0
    middle_boundary = self.bed_length / 2
    right_boundary = self.bed_length

    subregion_list = logic.define_subregions(self.bed_length, 
                                            subregion_count_even, 
                                            self.iterations)
    # Check number of subregions
    self.assertEqual(len(subregion_list), 2) 
    # Check boundary definitions
    self.assertEqual(subregion_list[0].leftBoundary(), left_boundary)
    self.assertEqual(subregion_list[0].rightBoundary(), middle_boundary)
    self.assertEqual(subregion_list[1].leftBoundary(), middle_boundary)
    self.assertEqual(subregion_list[1].rightBoundary(), right_boundary)

    subregion_count_odd = 5
    sub_length = self.bed_length / subregion_count_odd

    left_boundary = 0
    middle_boundary_1 = left_boundary + sub_length*1
    middle_boundary_2 = left_boundary + sub_length*2
    middle_boundary_3 = left_boundary + sub_length*3
    middle_boundary_4 = left_boundary + sub_length*4
    right_boundary = self.bed_length

    subregion_list_odd = logic.define_subregions(self.bed_length, 
                                            subregion_count_odd, 
                                            self.iterations)
    # Check number of subregions
    self.assertEqual(len(subregion_list_odd), 5) 
    # Check boundary definitions
    self.assertEqual(subregion_list_odd[0].leftBoundary(), left_boundary)
    self.assertEqual(subregion_list_odd[0].rightBoundary(), middle_boundary_1)

    self.assertEqual(subregion_list_odd[1].leftBoundary(), middle_boundary_1)
    self.assertEqual(subregion_list_odd[1].rightBoundary(), middle_boundary_2)

    self.assertEqual(subregion_list_odd[2].leftBoundary(), middle_boundary_2)
    self.assertEqual(subregion_list_odd[2].rightBoundary(), middle_boundary_3)

    self.assertEqual(subregion_list_odd[3].leftBoundary(), middle_boundary_3)
    self.assertEqual(subregion_list_odd[3].rightBoundary(), middle_boundary_4)

    self.assertEqual(subregion_list_odd[4].leftBoundary(), middle_boundary_4)
    self.assertEqual(subregion_list_odd[4].rightBoundary(), right_boundary)
class TestElevationList (methodName='runTest')

Test elevation_list function

Create an instance of the class that will use the named test method when executed. Raises a ValueError if the instance does not have a method with the specified name.

Expand source code
class TestElevationList(unittest.TestCase):
    """ Test elevation_list function """
    def test_all_same_elev_returns_one_elev(self):
        same_elev = np.array((12.3, 12.3, 12.3, 12.3, 12.3))
        elev_list = logic.elevation_list(same_elev)
        asc_elev_list = logic.elevation_list(same_elev, desc=False)
        
        expected_list = np.array(12.3)
        self.assertIsNone(np.testing.assert_array_equal(expected_list, elev_list))
        self.assertIsNone(np.testing.assert_array_equal(expected_list, asc_elev_list))

    def test_all_unique_returns_same_list(self):
        unique_elev = np.arange(5, dtype=float)
        elev_list = logic.elevation_list(unique_elev)
        asc_elev_list = logic.elevation_list(unique_elev, desc=False)

        self.assertIsNone(np.testing.assert_array_equal(unique_elev[::-1], elev_list))
        self.assertIsNone(np.testing.assert_array_equal(unique_elev, asc_elev_list))
    
    def test_elevation_list_returns_unique_set(self):
        elev = np.array((12.3, 5.0, 12.3, 5.0, 1.0, 3.0))
        elev_list = logic.elevation_list(elev)
        asc_elev_list = logic.elevation_list(elev, desc=False)

        expected_desc_elev = np.array((12.3, 5.0, 3.0, 1.0))
        expected_asc_elev = expected_desc_elev[::-1]
        self.assertIsNone(np.testing.assert_array_equal(expected_desc_elev, elev_list))
        self.assertIsNone(np.testing.assert_array_equal(expected_asc_elev, asc_elev_list))

Ancestors

  • unittest.case.TestCase

Methods

def test_all_same_elev_returns_one_elev(self)
Expand source code
def test_all_same_elev_returns_one_elev(self):
    same_elev = np.array((12.3, 12.3, 12.3, 12.3, 12.3))
    elev_list = logic.elevation_list(same_elev)
    asc_elev_list = logic.elevation_list(same_elev, desc=False)
    
    expected_list = np.array(12.3)
    self.assertIsNone(np.testing.assert_array_equal(expected_list, elev_list))
    self.assertIsNone(np.testing.assert_array_equal(expected_list, asc_elev_list))
def test_all_unique_returns_same_list(self)
Expand source code
def test_all_unique_returns_same_list(self):
    unique_elev = np.arange(5, dtype=float)
    elev_list = logic.elevation_list(unique_elev)
    asc_elev_list = logic.elevation_list(unique_elev, desc=False)

    self.assertIsNone(np.testing.assert_array_equal(unique_elev[::-1], elev_list))
    self.assertIsNone(np.testing.assert_array_equal(unique_elev, asc_elev_list))
def test_elevation_list_returns_unique_set(self)
Expand source code
def test_elevation_list_returns_unique_set(self):
    elev = np.array((12.3, 5.0, 12.3, 5.0, 1.0, 3.0))
    elev_list = logic.elevation_list(elev)
    asc_elev_list = logic.elevation_list(elev, desc=False)

    expected_desc_elev = np.array((12.3, 5.0, 3.0, 1.0))
    expected_asc_elev = expected_desc_elev[::-1]
    self.assertIsNone(np.testing.assert_array_equal(expected_desc_elev, elev_list))
    self.assertIsNone(np.testing.assert_array_equal(expected_asc_elev, asc_elev_list))
class TestFindClosestVertex (methodName='runTest')

Unit test for the find_closest_vertex function.

Create an instance of the class that will use the named test method when executed. Raises a ValueError if the instance does not have a method with the specified name.

Expand source code
class TestFindClosestVertex(unittest.TestCase): # Easy 
    """ Unit test for the find_closest_vertex function. 
    """
    def test_empty_available_vertices_returns_value_error(self):
        hop = 12.3
        empty_vertices = np.empty(0, dtype=float)
        with self.assertRaises(ValueError):
            _ = logic.find_closest_vertex(hop, empty_vertices)

    def test_negative_desired_hop_returns_value_error(self):
        neg_hop = -4.0
        avail_vert = np.arange(5, dtype=float)
        with self.assertRaises(ValueError):
            _ = logic.find_closest_vertex(neg_hop, avail_vert)

    def test_particle_exceeding_vertices_returns_negative_1(self):
        hop = 5.2
        avail_vert = np.arange(5, dtype=float)
        closest_vertex = logic.find_closest_vertex(hop, avail_vert)
        self.assertEqual(-1, closest_vertex)
        
    def test_valid_hop_to_vertex_returns_vertex(self):
        hop = 3.0
        avail_vert = np.arange(6, dtype=float)
        closest_vertex = logic.find_closest_vertex(hop, avail_vert)
        self.assertEqual(3.0, closest_vertex)

        last_valid_hop = 5.0
        closest_vertex = logic.find_closest_vertex(last_valid_hop, avail_vert)
        self.assertEqual(5.0, closest_vertex)

    def test_valid_hop_to_nonvertex_returns_next_vertex(self):
        hop = 3.001
        avail_vert = np.arange(6, dtype=float)
        closest_vertex = logic.find_closest_vertex(hop, avail_vert)
        self.assertEqual(4.0, closest_vertex)

Ancestors

  • unittest.case.TestCase

Methods

def test_empty_available_vertices_returns_value_error(self)
Expand source code
def test_empty_available_vertices_returns_value_error(self):
    hop = 12.3
    empty_vertices = np.empty(0, dtype=float)
    with self.assertRaises(ValueError):
        _ = logic.find_closest_vertex(hop, empty_vertices)
def test_negative_desired_hop_returns_value_error(self)
Expand source code
def test_negative_desired_hop_returns_value_error(self):
    neg_hop = -4.0
    avail_vert = np.arange(5, dtype=float)
    with self.assertRaises(ValueError):
        _ = logic.find_closest_vertex(neg_hop, avail_vert)
def test_particle_exceeding_vertices_returns_negative_1(self)
Expand source code
def test_particle_exceeding_vertices_returns_negative_1(self):
    hop = 5.2
    avail_vert = np.arange(5, dtype=float)
    closest_vertex = logic.find_closest_vertex(hop, avail_vert)
    self.assertEqual(-1, closest_vertex)
def test_valid_hop_to_nonvertex_returns_next_vertex(self)
Expand source code
def test_valid_hop_to_nonvertex_returns_next_vertex(self):
    hop = 3.001
    avail_vert = np.arange(6, dtype=float)
    closest_vertex = logic.find_closest_vertex(hop, avail_vert)
    self.assertEqual(4.0, closest_vertex)
def test_valid_hop_to_vertex_returns_vertex(self)
Expand source code
def test_valid_hop_to_vertex_returns_vertex(self):
    hop = 3.0
    avail_vert = np.arange(6, dtype=float)
    closest_vertex = logic.find_closest_vertex(hop, avail_vert)
    self.assertEqual(3.0, closest_vertex)

    last_valid_hop = 5.0
    closest_vertex = logic.find_closest_vertex(last_valid_hop, avail_vert)
    self.assertEqual(5.0, closest_vertex)
class TestFindSupports (methodName='runTest')

Test the find_supports function

Attributes

diam = float representing diameter of the test particles Create an instance of the class that will use the named test method when executed. Raises a ValueError if the instance does not have a method with the specified name.

Expand source code
class TestFindSupports(unittest.TestCase):
    """ Test the find_supports function

    Attributes:
        diam = float representing diameter of the test particles
    """
    def setUp(self):
        self.diam = 0.5

    def test_placement_returns_highest_elevation_supports(self):
        """ Given a particle at (x,y), if there are multple 
        particles with their locations (_x) exactly +- diameter
        away from x, then the returned support should be the 
        particle with the highest _y.
        """
        particle_placement = 1.0
        particle_height = 1.0

        particles = np.zeros((5, ATTR_COUNT), dtype=float)
        particles[:,1] = self.diam
        particles[4,0] = particle_placement
        particles[4,2] = particle_height
        particles[0:2,0] = particle_placement - self.diam/2
        particles[2:4,0] = particle_placement + self.diam/2
        particles[[0,2],2] = particle_height - 1.0
        particles[[1,3],2] = particle_height + 1.0
        empty_bed = np.empty((0, ATTR_COUNT))

        left_support, right_support = logic.find_supports(particles[4], particles, empty_bed)

        expected_left = particles[1]
        expected_right = particles[3]
        self.assertIsNone(np.testing.assert_array_equal(expected_left, left_support))
        self.assertIsNone(np.testing.assert_array_equal(expected_right, right_support))
    
    def test_no_supports_available_returns_value_error(self):
        """ If there is no valid support for a particle
        then a value error should be raised.
        """
        particle_placement = 1.0
        particle_height = 1.0

        particles = np.zeros((3, ATTR_COUNT), dtype=float)
        particles[:,1] = self.diam
        particles[2,0] = particle_placement
        particles[2,2] = particle_height
        particles[0,0] = particle_placement + self.diam
        particles[1,0] = particle_placement - self.diam # Centres lie outside threshold
        particles[[0,1],2] = particle_height - 1.0
        empty_bed = np.empty((0, ATTR_COUNT))

        # Both potential supporting particles outisde support threshold
        with self.assertRaises(ValueError):
            _, _ = logic.find_supports(particles[2], particles, empty_bed)
        
        # Only 1 potential supporting particles outisde support threshold
        particles[0,0] = particle_placement + self.diam/2 # right support within threshold

        with self.assertRaises(ValueError):
            _, _ = logic.find_supports(particles[2], particles, empty_bed)

        particles[0,0] = particle_placement + self.diam # right support outside threshold again
        particles[1,0] = particle_placement + self.diam/2 # left support within threshold

        with self.assertRaises(ValueError):
            _, _ = logic.find_supports(particles[2], particles, empty_bed)

Ancestors

  • unittest.case.TestCase

Methods

def setUp(self)

Hook method for setting up the test fixture before exercising it.

Expand source code
def setUp(self):
    self.diam = 0.5
def test_no_supports_available_returns_value_error(self)

If there is no valid support for a particle then a value error should be raised.

Expand source code
def test_no_supports_available_returns_value_error(self):
    """ If there is no valid support for a particle
    then a value error should be raised.
    """
    particle_placement = 1.0
    particle_height = 1.0

    particles = np.zeros((3, ATTR_COUNT), dtype=float)
    particles[:,1] = self.diam
    particles[2,0] = particle_placement
    particles[2,2] = particle_height
    particles[0,0] = particle_placement + self.diam
    particles[1,0] = particle_placement - self.diam # Centres lie outside threshold
    particles[[0,1],2] = particle_height - 1.0
    empty_bed = np.empty((0, ATTR_COUNT))

    # Both potential supporting particles outisde support threshold
    with self.assertRaises(ValueError):
        _, _ = logic.find_supports(particles[2], particles, empty_bed)
    
    # Only 1 potential supporting particles outisde support threshold
    particles[0,0] = particle_placement + self.diam/2 # right support within threshold

    with self.assertRaises(ValueError):
        _, _ = logic.find_supports(particles[2], particles, empty_bed)

    particles[0,0] = particle_placement + self.diam # right support outside threshold again
    particles[1,0] = particle_placement + self.diam/2 # left support within threshold

    with self.assertRaises(ValueError):
        _, _ = logic.find_supports(particles[2], particles, empty_bed)
def test_placement_returns_highest_elevation_supports(self)

Given a particle at (x,y), if there are multple particles with their locations (_x) exactly +- diameter away from x, then the returned support should be the particle with the highest _y.

Expand source code
def test_placement_returns_highest_elevation_supports(self):
    """ Given a particle at (x,y), if there are multple 
    particles with their locations (_x) exactly +- diameter
    away from x, then the returned support should be the 
    particle with the highest _y.
    """
    particle_placement = 1.0
    particle_height = 1.0

    particles = np.zeros((5, ATTR_COUNT), dtype=float)
    particles[:,1] = self.diam
    particles[4,0] = particle_placement
    particles[4,2] = particle_height
    particles[0:2,0] = particle_placement - self.diam/2
    particles[2:4,0] = particle_placement + self.diam/2
    particles[[0,2],2] = particle_height - 1.0
    particles[[1,3],2] = particle_height + 1.0
    empty_bed = np.empty((0, ATTR_COUNT))

    left_support, right_support = logic.find_supports(particles[4], particles, empty_bed)

    expected_left = particles[1]
    expected_right = particles[3]
    self.assertIsNone(np.testing.assert_array_equal(expected_left, left_support))
    self.assertIsNone(np.testing.assert_array_equal(expected_right, right_support))
class TestGetEventParticlesWithNSubregions (methodName='runTest')

Test that getting event particles with N Subregion returns a valid list of event particles.

A 'valid list' will change depending on the function. See function docstrings for more details.

Attributes

test_length
the length of the bed
num_particles
the number of model particles
mock_sub_list_2
list of Mock-type subregions
entrainment_events
number of entrainment events to request per subregion
level_limit
random int representing level limit

Create an instance of the class that will use the named test method when executed. Raises a ValueError if the instance does not have a method with the specified name.

Expand source code
class TestGetEventParticlesWithNSubregions(unittest.TestCase):
    """
    Test that getting event particles with N Subregion 
    returns a valid list of event particles.
    
    A 'valid list' will change depending on the function.
    See function docstrings for more details.

    Attributes:
        test_length: the length of the bed
        num_particles: the number of model particles
        mock_sub_list_2: list of Mock-type subregions
        entrainment_events: number of entrainment events to request
            per subregion
        level_limit: random int representing level limit
    """
    def setUp(self):
        self.test_length = 20
        self.num_particles = 6
        mock_subregion_0 = Mock()
        mock_subregion_0.leftBoundary.return_value = 0
        mock_subregion_0.rightBoundary.return_value = self.test_length / 2
        mock_subregion_0.getName.return_value = 'Mock_Subregion_0'

        mock_subregion_1 = Mock()
        mock_subregion_1.leftBoundary.return_value = self.test_length / 2
        mock_subregion_1.rightBoundary.return_value = self.test_length
        mock_subregion_1.getName.return_value = 'Mock_Subregion_1'

        self.mock_sub_list_2 = [mock_subregion_0, mock_subregion_1]

        self.entrainment_events = 3
        self.level_limit = np.random.randint(0, np.random.randint(2, 10))


    def test_all_active_returns_3_per_subregion(self):
        """If there are M active particles in each of the N subregions and there
        are M events requested per subregion, then a valid list will be a 
        list of all M*N particles.
        """
        model_particles = np.zeros((self.num_particles, ATTR_COUNT))
        model_particles[:,3] = np.arange(self.num_particles) # unique ids
        model_particles[:,4] = np.ones(self.num_particles) # all active
        # Randomly place first three particles in Subregion 1 
        model_particles[0:3, 0] = np.random.randint(
                                                9, 
                                                size=3 )
        # Randomly place last three particles in Subregion 2
        model_particles[3:6, 0] = np.random.randint(
                                                11,
                                                self.test_length, 
                                                size=3 )

        list = logic.get_event_particles(
                                        self.entrainment_events, 
                                        self.mock_sub_list_2, 
                                        model_particles, 
                                        self.level_limit )

        self.assertCountEqual(list, model_particles[:,3])
        self.assertEqual(len(list), self.entrainment_events * 2)

        # Height dependancy should not effect list results here
        hp_list = list = logic.get_event_particles(
                                        self.entrainment_events, 
                                        self.mock_sub_list_2, 
                                        model_particles, 
                                        self.level_limit,
                                        height_dependant=True )
        self.assertCountEqual(hp_list, model_particles[:,3])
        self.assertCountEqual(hp_list, list)


    def test_active_in_1_subregion_returns_only_active(self):
        """If there are M active particles in each 1..K subregions and 0
        active in K+1...N subregions, and there are M events requested per 
        subregion, then a valid list will be a list of the M*K active particles.

        This is simplified down to only 2 subregions.
        """
        mp_half_active = np.zeros((self.num_particles, ATTR_COUNT))
        mp_half_active[:,3] = np.arange(self.num_particles) 
        mp_half_active[0:3, 4] = np.ones(int((self.num_particles/2))) # First half active

        mp_half_active[0:3, 0] = np.random.randint(10,size=3 )
        mp_half_active[3:6, 0] = np.random.randint(10, self.test_length, size=3 ) 

        list = logic.get_event_particles(
                                        self.entrainment_events, 
                                        self.mock_sub_list_2, 
                                        mp_half_active, 
                                        self.level_limit )

        active_particles = mp_half_active[mp_half_active[:,4] != 0] 

        self.assertCountEqual(list, active_particles[:,3])
        self.assertEqual(len(list), 3)
    
    def test_particle_on_boundary_is_not_returned_twice(self):
        """ Test that a particle resting on a boundary between
        Subregions (recall, other than upstream and downstream
        boundaries, Subregions share boundaries) will not 
        be selected for entrainment twice.
        """
        one_particle_on_boundary = np.zeros((1, ATTR_COUNT))
        one_particle_on_boundary[0][4] = 1
        one_particle_on_boundary[0][0] = 10
        # Use custom entrainment_event count for simplicity
        entrainment_events = 1

        list = logic.get_event_particles(
                                        entrainment_events, 
                                        self.mock_sub_list_2, 
                                        one_particle_on_boundary, 
                                        self.level_limit )
        self.assertEqual(len(list), 1)

Ancestors

  • unittest.case.TestCase

Methods

def setUp(self)

Hook method for setting up the test fixture before exercising it.

Expand source code
def setUp(self):
    self.test_length = 20
    self.num_particles = 6
    mock_subregion_0 = Mock()
    mock_subregion_0.leftBoundary.return_value = 0
    mock_subregion_0.rightBoundary.return_value = self.test_length / 2
    mock_subregion_0.getName.return_value = 'Mock_Subregion_0'

    mock_subregion_1 = Mock()
    mock_subregion_1.leftBoundary.return_value = self.test_length / 2
    mock_subregion_1.rightBoundary.return_value = self.test_length
    mock_subregion_1.getName.return_value = 'Mock_Subregion_1'

    self.mock_sub_list_2 = [mock_subregion_0, mock_subregion_1]

    self.entrainment_events = 3
    self.level_limit = np.random.randint(0, np.random.randint(2, 10))
def test_active_in_1_subregion_returns_only_active(self)

If there are M active particles in each 1..K subregions and 0 active in K+1…N subregions, and there are M events requested per subregion, then a valid list will be a list of the M*K active particles.

This is simplified down to only 2 subregions.

Expand source code
def test_active_in_1_subregion_returns_only_active(self):
    """If there are M active particles in each 1..K subregions and 0
    active in K+1...N subregions, and there are M events requested per 
    subregion, then a valid list will be a list of the M*K active particles.

    This is simplified down to only 2 subregions.
    """
    mp_half_active = np.zeros((self.num_particles, ATTR_COUNT))
    mp_half_active[:,3] = np.arange(self.num_particles) 
    mp_half_active[0:3, 4] = np.ones(int((self.num_particles/2))) # First half active

    mp_half_active[0:3, 0] = np.random.randint(10,size=3 )
    mp_half_active[3:6, 0] = np.random.randint(10, self.test_length, size=3 ) 

    list = logic.get_event_particles(
                                    self.entrainment_events, 
                                    self.mock_sub_list_2, 
                                    mp_half_active, 
                                    self.level_limit )

    active_particles = mp_half_active[mp_half_active[:,4] != 0] 

    self.assertCountEqual(list, active_particles[:,3])
    self.assertEqual(len(list), 3)
def test_all_active_returns_3_per_subregion(self)

If there are M active particles in each of the N subregions and there are M events requested per subregion, then a valid list will be a list of all M*N particles.

Expand source code
def test_all_active_returns_3_per_subregion(self):
    """If there are M active particles in each of the N subregions and there
    are M events requested per subregion, then a valid list will be a 
    list of all M*N particles.
    """
    model_particles = np.zeros((self.num_particles, ATTR_COUNT))
    model_particles[:,3] = np.arange(self.num_particles) # unique ids
    model_particles[:,4] = np.ones(self.num_particles) # all active
    # Randomly place first three particles in Subregion 1 
    model_particles[0:3, 0] = np.random.randint(
                                            9, 
                                            size=3 )
    # Randomly place last three particles in Subregion 2
    model_particles[3:6, 0] = np.random.randint(
                                            11,
                                            self.test_length, 
                                            size=3 )

    list = logic.get_event_particles(
                                    self.entrainment_events, 
                                    self.mock_sub_list_2, 
                                    model_particles, 
                                    self.level_limit )

    self.assertCountEqual(list, model_particles[:,3])
    self.assertEqual(len(list), self.entrainment_events * 2)

    # Height dependancy should not effect list results here
    hp_list = list = logic.get_event_particles(
                                    self.entrainment_events, 
                                    self.mock_sub_list_2, 
                                    model_particles, 
                                    self.level_limit,
                                    height_dependant=True )
    self.assertCountEqual(hp_list, model_particles[:,3])
    self.assertCountEqual(hp_list, list)
def test_particle_on_boundary_is_not_returned_twice(self)

Test that a particle resting on a boundary between Subregions (recall, other than upstream and downstream boundaries, Subregions share boundaries) will not be selected for entrainment twice.

Expand source code
def test_particle_on_boundary_is_not_returned_twice(self):
    """ Test that a particle resting on a boundary between
    Subregions (recall, other than upstream and downstream
    boundaries, Subregions share boundaries) will not 
    be selected for entrainment twice.
    """
    one_particle_on_boundary = np.zeros((1, ATTR_COUNT))
    one_particle_on_boundary[0][4] = 1
    one_particle_on_boundary[0][0] = 10
    # Use custom entrainment_event count for simplicity
    entrainment_events = 1

    list = logic.get_event_particles(
                                    entrainment_events, 
                                    self.mock_sub_list_2, 
                                    one_particle_on_boundary, 
                                    self.level_limit )
    self.assertEqual(len(list), 1)
class TestGetEventParticlesWithOneSubregion (methodName='runTest')

Test that getting event particles with one Subregion returns a valid list of event particles.

A 'valid list' will change depending on the function. See function docstrings for more details.

Attributes

test_length
the length of the bed
num_particles
the number of model particles
mock_sub_list
list of Mock-type subregions
entrainment_events
number of entrainment events to request per subregion
level_limit
random int representing level limit

Create an instance of the class that will use the named test method when executed. Raises a ValueError if the instance does not have a method with the specified name.

Expand source code
class TestGetEventParticlesWithOneSubregion(unittest.TestCase):
    """
    Test that getting event particles with one Subregion 
    returns a valid list of event particles.
    
    A 'valid list' will change depending on the function.
    See function docstrings for more details.

    Attributes:
        test_length: the length of the bed
        num_particles: the number of model particles
        mock_sub_list: list of Mock-type subregions
        entrainment_events: number of entrainment events to request
            per subregion
        level_limit: random int representing level limit
    """
    def setUp(self):
        self.test_length = 10
        self.num_particles = 3

        mock_subregion = Mock()
        mock_subregion.leftBoundary.return_value = 0
        mock_subregion.rightBoundary.return_value = self.test_length
        mock_subregion.getName.return_value = 'Mock_Subregion'
        self.mock_sub_list = [mock_subregion]

        self.entrainment_events = 3
        self.level_limit = np.random.randint(0, np.random.randint(2, 10))

    def test_all_active_returns_valid_list(self): 
        """If there are N active particles in 1 subregion and N events requested
        per subregion then a valid list will be a list of all particles.
        """
        model_particles = np.zeros((self.num_particles, ATTR_COUNT))
        model_particles[:,3] = np.arange(self.num_particles) # unique ids
        model_particles[:,4] = np.ones(self.num_particles) # all active
        model_particles[:,0] = np.random.randint(
                                                self.test_length, 
                                                size=self.num_particles ) # random placement

        list = logic.get_event_particles(
                                        self.entrainment_events, 
                                        self.mock_sub_list, 
                                        model_particles, 
                                        self.level_limit )
        self.assertCountEqual(list, model_particles[:,3])

        # Height dependancy should not effect list results here
        hp_list = list = logic.get_event_particles(
                                        self.entrainment_events, 
                                        self.mock_sub_list, 
                                        model_particles, 
                                        self.level_limit,
                                        height_dependant=True )
        self.assertCountEqual(hp_list, model_particles[:,3])
        self.assertCountEqual(hp_list, list)
    
    def test_not_all_active_returns_list_of_2(self):
        """If there are N particles in 1 subregion and N-1 are _active_, 
        and if N events are requested per subregion then a valid list will be 
        a list of the two active particles.
        """
        mp_one_inactive = np.zeros((self.num_particles, ATTR_COUNT))
        mp_one_inactive[:,3] = np.arange(self.num_particles) 
        mp_one_inactive[0][4] = 1
        mp_one_inactive[1][4] = 1
        mp_one_inactive[:,0] = np.random.randint(self.test_length, size=self.num_particles)

        list = logic.get_event_particles(
                                        self.entrainment_events, 
                                        self.mock_sub_list, 
                                        mp_one_inactive, 
                                        self.level_limit )
        self.assertEqual(len(list), self.num_particles - 1)
        active_list = mp_one_inactive[mp_one_inactive[:,4] != 0]
        self.assertCountEqual(list, active_list[:,3])

    def test_none_active_returns_empty_list(self):
        """If there are N particles in 1 subregion and 0 are _active_ 
        and if N events are requested per subregion, then a valid list will be 
        an empty list.
        """
        np_none_active = np.zeros((self.num_particles, ATTR_COUNT))
        np_none_active[:,3] = np.arange(self.num_particles) 
        np_none_active[:,0] = np.random.randint(self.test_length, size=self.num_particles)

        empty_list = logic.get_event_particles(
                                        self.entrainment_events, 
                                        self.mock_sub_list, 
                                        np_none_active, 
                                        self.level_limit )
        self.assertEqual(len(empty_list), 0)
    

    def test_all_ghost_particles_returns_ghost_particles(self):
        """If there are N particles in 1 subregion and all N particles
        are 'ghost' particles (at -1), and if N particles are requested
        per subregion, then a valid list will be a list of all the
        ghost particles (all the particles).
        """
        np_all_ghost = np.zeros((self.num_particles, ATTR_COUNT))
        np_all_ghost[:,3] = np.arange(self.num_particles) 
        np_all_ghost[:,0] = -1

        ghost_list = logic.get_event_particles(
                                        self.entrainment_events, 
                                        self.mock_sub_list, 
                                        np_all_ghost, 
                                        self.level_limit )
        self.assertCountEqual(ghost_list, np_all_ghost[:,3])  

Ancestors

  • unittest.case.TestCase

Methods

def setUp(self)

Hook method for setting up the test fixture before exercising it.

Expand source code
def setUp(self):
    self.test_length = 10
    self.num_particles = 3

    mock_subregion = Mock()
    mock_subregion.leftBoundary.return_value = 0
    mock_subregion.rightBoundary.return_value = self.test_length
    mock_subregion.getName.return_value = 'Mock_Subregion'
    self.mock_sub_list = [mock_subregion]

    self.entrainment_events = 3
    self.level_limit = np.random.randint(0, np.random.randint(2, 10))
def test_all_active_returns_valid_list(self)

If there are N active particles in 1 subregion and N events requested per subregion then a valid list will be a list of all particles.

Expand source code
def test_all_active_returns_valid_list(self): 
    """If there are N active particles in 1 subregion and N events requested
    per subregion then a valid list will be a list of all particles.
    """
    model_particles = np.zeros((self.num_particles, ATTR_COUNT))
    model_particles[:,3] = np.arange(self.num_particles) # unique ids
    model_particles[:,4] = np.ones(self.num_particles) # all active
    model_particles[:,0] = np.random.randint(
                                            self.test_length, 
                                            size=self.num_particles ) # random placement

    list = logic.get_event_particles(
                                    self.entrainment_events, 
                                    self.mock_sub_list, 
                                    model_particles, 
                                    self.level_limit )
    self.assertCountEqual(list, model_particles[:,3])

    # Height dependancy should not effect list results here
    hp_list = list = logic.get_event_particles(
                                    self.entrainment_events, 
                                    self.mock_sub_list, 
                                    model_particles, 
                                    self.level_limit,
                                    height_dependant=True )
    self.assertCountEqual(hp_list, model_particles[:,3])
    self.assertCountEqual(hp_list, list)
def test_all_ghost_particles_returns_ghost_particles(self)

If there are N particles in 1 subregion and all N particles are 'ghost' particles (at -1), and if N particles are requested per subregion, then a valid list will be a list of all the ghost particles (all the particles).

Expand source code
def test_all_ghost_particles_returns_ghost_particles(self):
    """If there are N particles in 1 subregion and all N particles
    are 'ghost' particles (at -1), and if N particles are requested
    per subregion, then a valid list will be a list of all the
    ghost particles (all the particles).
    """
    np_all_ghost = np.zeros((self.num_particles, ATTR_COUNT))
    np_all_ghost[:,3] = np.arange(self.num_particles) 
    np_all_ghost[:,0] = -1

    ghost_list = logic.get_event_particles(
                                    self.entrainment_events, 
                                    self.mock_sub_list, 
                                    np_all_ghost, 
                                    self.level_limit )
    self.assertCountEqual(ghost_list, np_all_ghost[:,3])  
def test_none_active_returns_empty_list(self)

If there are N particles in 1 subregion and 0 are active and if N events are requested per subregion, then a valid list will be an empty list.

Expand source code
def test_none_active_returns_empty_list(self):
    """If there are N particles in 1 subregion and 0 are _active_ 
    and if N events are requested per subregion, then a valid list will be 
    an empty list.
    """
    np_none_active = np.zeros((self.num_particles, ATTR_COUNT))
    np_none_active[:,3] = np.arange(self.num_particles) 
    np_none_active[:,0] = np.random.randint(self.test_length, size=self.num_particles)

    empty_list = logic.get_event_particles(
                                    self.entrainment_events, 
                                    self.mock_sub_list, 
                                    np_none_active, 
                                    self.level_limit )
    self.assertEqual(len(empty_list), 0)
def test_not_all_active_returns_list_of_2(self)

If there are N particles in 1 subregion and N-1 are active, and if N events are requested per subregion then a valid list will be a list of the two active particles.

Expand source code
def test_not_all_active_returns_list_of_2(self):
    """If there are N particles in 1 subregion and N-1 are _active_, 
    and if N events are requested per subregion then a valid list will be 
    a list of the two active particles.
    """
    mp_one_inactive = np.zeros((self.num_particles, ATTR_COUNT))
    mp_one_inactive[:,3] = np.arange(self.num_particles) 
    mp_one_inactive[0][4] = 1
    mp_one_inactive[1][4] = 1
    mp_one_inactive[:,0] = np.random.randint(self.test_length, size=self.num_particles)

    list = logic.get_event_particles(
                                    self.entrainment_events, 
                                    self.mock_sub_list, 
                                    mp_one_inactive, 
                                    self.level_limit )
    self.assertEqual(len(list), self.num_particles - 1)
    active_list = mp_one_inactive[mp_one_inactive[:,4] != 0]
    self.assertCountEqual(list, active_list[:,3])
class TestIncrementAge (methodName='runTest')

Unit test for the increment_age function.

Create an instance of the class that will use the named test method when executed. Raises a ValueError if the instance does not have a method with the specified name.

Expand source code
class TestIncrementAge(unittest.TestCase): 
    """ Unit test for the increment_age function. 
    """
    def test_empty_event_increases_all_ages_by_1(self):
        model_particles = np.zeros([3, ATTR_COUNT], dtype=float)
        model_particles[:,3] = np.arange(3)
        event_ids = []

        aged_model = logic.increment_age(model_particles, event_ids)
        # The unmoved particles start at age 0 and we test after one iteration
        # Therefore, ages should all be 1
        expected_age = np.ones(3, dtype=float) 
        self.assertCountEqual(expected_age, aged_model[:,5])

    def test_event_set_to_0_and_nonevent_to_1(self):
        starting_age = 2.0
        model_particles = np.zeros([3, ATTR_COUNT], dtype=float)
        model_particles[:,3] = np.arange(3)
        model_particles[:,5] = starting_age
        event_ids = np.array([0, 1])

        aged_model = logic.increment_age(model_particles, event_ids)
        self.assertEqual(starting_age + 1, aged_model[2,5])
        self.assertCountEqual([0.0, 0.0], aged_model[event_ids][:,5])

Ancestors

  • unittest.case.TestCase

Methods

def test_empty_event_increases_all_ages_by_1(self)
Expand source code
def test_empty_event_increases_all_ages_by_1(self):
    model_particles = np.zeros([3, ATTR_COUNT], dtype=float)
    model_particles[:,3] = np.arange(3)
    event_ids = []

    aged_model = logic.increment_age(model_particles, event_ids)
    # The unmoved particles start at age 0 and we test after one iteration
    # Therefore, ages should all be 1
    expected_age = np.ones(3, dtype=float) 
    self.assertCountEqual(expected_age, aged_model[:,5])
def test_event_set_to_0_and_nonevent_to_1(self)
Expand source code
def test_event_set_to_0_and_nonevent_to_1(self):
    starting_age = 2.0
    model_particles = np.zeros([3, ATTR_COUNT], dtype=float)
    model_particles[:,3] = np.arange(3)
    model_particles[:,5] = starting_age
    event_ids = np.array([0, 1])

    aged_model = logic.increment_age(model_particles, event_ids)
    self.assertEqual(starting_age + 1, aged_model[2,5])
    self.assertCountEqual([0.0, 0.0], aged_model[event_ids][:,5])
class TestMoveModelParticles (methodName='runTest')

Unit tests for move_model_particles function.

Attributes

diam
diameter for the test particles
h
float derived from diam used in the geometric calculations of particle elevations

Create an instance of the class that will use the named test method when executed. Raises a ValueError if the instance does not have a method with the specified name.

Expand source code
class TestMoveModelParticles(unittest.TestCase):
    """ Unit tests for move_model_particles function.

    Attributes:
        diam: diameter for the test particles
        h: float derived from diam used in the
            geometric calculations of particle elevations
    """

    def setUp(self):
        self.diam = 0.5
        d = np.divide(np.multiply(np.divide(self.diam, 2), 
                                        self.diam), 
                                        self.diam)
        self.h = np.sqrt(np.square(self.diam) - np.square(d))

    def test_empty_event_particles_returns_no_changes(self):
        """ If there are no event particles (no particles
        being entrained) then no model particles should
        be altered by move_model_particles.
        """

        empty_event_particles = np.empty((0, ATTR_COUNT))
        model_particles = np.zeros((1, ATTR_COUNT), dtype=float)
        model_supports = np.array([[1.0 , 2.0]], dtype=float)
        empty_bed = np.empty((0, ATTR_COUNT))
        available_vertices = np.empty((0))

        moved_model, moved_supports = logic.move_model_particles(empty_event_particles, 
                                                                    model_particles,
                                                                    model_supports, 
                                                                    empty_bed, 
                                                                    available_vertices, 
                                                                    self.h)
        expected_supports = np.array([[1.0 , 2.0]], dtype=float)                                                       
        self.assertIsNone(np.testing.assert_array_equal(expected_supports, moved_supports))
        self.assertIsNone(np.testing.assert_array_equal(model_particles, moved_model))

    def test_event_particle_on_desired_vertex_returns_no_changes(self):
        """ If there is 1 event particle and it is placed on a vertex x, 
        and if x is an available vertex, then move_model_particles
        should not change the model_particles array.
        """
        placement = 5

        model_particles = np.zeros((1, ATTR_COUNT), dtype=float)
        model_particles[:,0] = placement
        model_particles[:,1] = self.diam

        one_event = model_particles[[0]]
        two_bed = np.zeros((2, ATTR_COUNT))
        two_bed[0][3] = -1
        two_bed[1][3] = -2
        two_bed[0][0] = placement - (self.diam/2)
        two_bed[1][0] = placement + (self.diam/2)
        model_supports = np.array([[two_bed[0][3], two_bed[1][3]]], dtype=float)
        available_vertices = np.array([placement])

        moved_model, moved_supports = logic.move_model_particles(one_event, 
                                                                model_particles,
                                                                model_supports, 
                                                                two_bed, 
                                                                available_vertices, 
                                                                self.h)
        expected_supports = np.array([[two_bed[0][3], two_bed[1][3]]], dtype=float)
        self.assertIsNone(np.testing.assert_array_equal(expected_supports, moved_supports))
        self.assertIsNone(np.testing.assert_array_equal(moved_model, model_particles))

    def test_looped_particle_returns_nan_supports_and_incremented_counter(self):
        """ If there is 1 event particle and it is 'looped' then it's supports
        should be updated to NaN and it's loop counter should be incremented by 1.
        """
        empty_bed = np.empty((0, ATTR_COUNT))
        available_vertices = np.arange((3)) 

        model_particles = np.zeros((1, ATTR_COUNT), dtype=float)
        placement = np.max(available_vertices) + 1 # assure that the particle is being placed beyond the largest vertex
        model_particles[:,0] = placement
        ghost_event = model_particles[[0]]
        model_supports = np.array([[1.0, 5.0]], dtype=float) # random values
        moved_model, moved_supports = logic.move_model_particles(ghost_event, 
                                                                    model_particles, 
                                                                    model_supports,
                                                                    empty_bed, 
                                                                    available_vertices, 
                                                                    self.h)
        expected_counter = 1
        expected_supports = np.empty((1,2))
        expected_supports[:] = np.nan
        self.assertIsNone(np.testing.assert_array_equal(expected_supports, moved_supports))
        self.assertEqual(expected_counter, moved_model[0][6])

Ancestors

  • unittest.case.TestCase

Methods

def setUp(self)

Hook method for setting up the test fixture before exercising it.

Expand source code
def setUp(self):
    self.diam = 0.5
    d = np.divide(np.multiply(np.divide(self.diam, 2), 
                                    self.diam), 
                                    self.diam)
    self.h = np.sqrt(np.square(self.diam) - np.square(d))
def test_empty_event_particles_returns_no_changes(self)

If there are no event particles (no particles being entrained) then no model particles should be altered by move_model_particles.

Expand source code
def test_empty_event_particles_returns_no_changes(self):
    """ If there are no event particles (no particles
    being entrained) then no model particles should
    be altered by move_model_particles.
    """

    empty_event_particles = np.empty((0, ATTR_COUNT))
    model_particles = np.zeros((1, ATTR_COUNT), dtype=float)
    model_supports = np.array([[1.0 , 2.0]], dtype=float)
    empty_bed = np.empty((0, ATTR_COUNT))
    available_vertices = np.empty((0))

    moved_model, moved_supports = logic.move_model_particles(empty_event_particles, 
                                                                model_particles,
                                                                model_supports, 
                                                                empty_bed, 
                                                                available_vertices, 
                                                                self.h)
    expected_supports = np.array([[1.0 , 2.0]], dtype=float)                                                       
    self.assertIsNone(np.testing.assert_array_equal(expected_supports, moved_supports))
    self.assertIsNone(np.testing.assert_array_equal(model_particles, moved_model))
def test_event_particle_on_desired_vertex_returns_no_changes(self)

If there is 1 event particle and it is placed on a vertex x, and if x is an available vertex, then move_model_particles should not change the model_particles array.

Expand source code
def test_event_particle_on_desired_vertex_returns_no_changes(self):
    """ If there is 1 event particle and it is placed on a vertex x, 
    and if x is an available vertex, then move_model_particles
    should not change the model_particles array.
    """
    placement = 5

    model_particles = np.zeros((1, ATTR_COUNT), dtype=float)
    model_particles[:,0] = placement
    model_particles[:,1] = self.diam

    one_event = model_particles[[0]]
    two_bed = np.zeros((2, ATTR_COUNT))
    two_bed[0][3] = -1
    two_bed[1][3] = -2
    two_bed[0][0] = placement - (self.diam/2)
    two_bed[1][0] = placement + (self.diam/2)
    model_supports = np.array([[two_bed[0][3], two_bed[1][3]]], dtype=float)
    available_vertices = np.array([placement])

    moved_model, moved_supports = logic.move_model_particles(one_event, 
                                                            model_particles,
                                                            model_supports, 
                                                            two_bed, 
                                                            available_vertices, 
                                                            self.h)
    expected_supports = np.array([[two_bed[0][3], two_bed[1][3]]], dtype=float)
    self.assertIsNone(np.testing.assert_array_equal(expected_supports, moved_supports))
    self.assertIsNone(np.testing.assert_array_equal(moved_model, model_particles))
def test_looped_particle_returns_nan_supports_and_incremented_counter(self)

If there is 1 event particle and it is 'looped' then it's supports should be updated to NaN and it's loop counter should be incremented by 1.

Expand source code
def test_looped_particle_returns_nan_supports_and_incremented_counter(self):
    """ If there is 1 event particle and it is 'looped' then it's supports
    should be updated to NaN and it's loop counter should be incremented by 1.
    """
    empty_bed = np.empty((0, ATTR_COUNT))
    available_vertices = np.arange((3)) 

    model_particles = np.zeros((1, ATTR_COUNT), dtype=float)
    placement = np.max(available_vertices) + 1 # assure that the particle is being placed beyond the largest vertex
    model_particles[:,0] = placement
    ghost_event = model_particles[[0]]
    model_supports = np.array([[1.0, 5.0]], dtype=float) # random values
    moved_model, moved_supports = logic.move_model_particles(ghost_event, 
                                                                model_particles, 
                                                                model_supports,
                                                                empty_bed, 
                                                                available_vertices, 
                                                                self.h)
    expected_counter = 1
    expected_supports = np.empty((1,2))
    expected_supports[:] = np.nan
    self.assertIsNone(np.testing.assert_array_equal(expected_supports, moved_supports))
    self.assertEqual(expected_counter, moved_model[0][6])
class TestPlaceParticle (methodName='runTest')

Test the place_particle function.

Attributes

diam
float representing diameter of the test particles
h
float derived from diam that is used in the geometric calculations of particle elevation

Create an instance of the class that will use the named test method when executed. Raises a ValueError if the instance does not have a method with the specified name.

Expand source code
class TestPlaceParticle(unittest.TestCase): 
    """ Test the place_particle function. 
    
    Attributes:
        diam: float representing diameter of the test particles
        h: float derived from diam that is used in the 
            geometric calculations of particle elevation 
    """
    def setUp(self):
        self.diam = 0.5
        d = np.divide(np.multiply(np.divide(self.diam, 2), 
                                        self.diam), 
                                        self.diam)
        self.h = np.sqrt(np.square(self.diam) - np.square(d))

    def test_bad_placement_raises_value_error(self):
        """ A bad placement should raise a value error. 
        In this case, a bad placement is any placement where a particle does
        not have a particle supporting it on the left and right sides.
        """
        # 1) Particle (arbitrary placement) with empty model and bed particles arrays
        bad_particle = np.zeros([1, ATTR_COUNT], dtype=float)
        bad_particle[:,1] = self.diam
        empty_bed = np.empty((0, ATTR_COUNT))
        empty_model = np.empty((0, ATTR_COUNT))
        
        with self.assertRaises(ValueError):
            placed_x, placed_y = logic.place_particle(bad_particle[0], empty_model, empty_bed, self.h)

        # 2) Particle placed at invalid (unsupported) location
        unsupported_particle = np.zeros([1, ATTR_COUNT], dtype=float)
        unsupported_particle[:,0] = 0.9
        unsupported_particle[:,1] = self.diam

        unsupported_model = np.zeros((2, ATTR_COUNT))
        unsupported_model[:,0] = np.arange(0.5, 1.5, step=self.diam) 

        with self.assertRaises(ValueError):
            _, _, _, _ = logic.place_particle(unsupported_particle[0], unsupported_model, empty_bed, self.h)

        # 3) Particle placed at location where all model particles' centres have the same location
        # (i.e placed on a tall single stack/tower of particles)
        stacked_unsupported_particle = np.zeros([1, ATTR_COUNT], dtype=float)
        stacked_unsupported_particle[:,0] = 0.5
        stacked_unsupported_particle[:,1] = self.diam

        single_stack_model = np.zeros((2, ATTR_COUNT))
        single_stack_model[:,0] = 0.5
        single_stack_model[:,2] = np.arange(0, 1.0, step=self.diam)

        with self.assertRaises(ValueError):
            _, _, _, _  = logic.place_particle(stacked_unsupported_particle[0], single_stack_model, empty_bed, self.h)

        # 4) Particle and model particle array are identical
        particle = np.zeros([1, ATTR_COUNT], dtype=float)
        particle[:,1] = self.diam

        empty_bed = np.empty((0, ATTR_COUNT))
        model = np.empty((1, ATTR_COUNT)) 
        model[0] = particle

        with self.assertRaises(ValueError):
            _, _, _, _  = logic.place_particle(particle[0], model, empty_bed, self.h)
        
    def test_good_placement_returns_valid_xy(self):
        """ A good/valid placement should return a geometrically
        correct x and y value. A good placement is a placement where
        a particle has a left and right supprting particle.

        Valid x and y is in accordance with the geometry 
        discussed here: https://math.stackexchange.com/questions/2293201/
        """
        base_elevation = 0
        left_id = 4.0
        right_id = 9.0
        expected_elevation = round(np.add(self.h, base_elevation), 2)

        placement = 0.75
        particle = np.zeros([1, ATTR_COUNT], dtype=float)
        particle[:,0] = placement

        # Create model particles with particle at index 0 being placed 
        # in between particles at index 1 and 2. 
        # particles at index 1 and 2 will be at the base_elevation
        model_particles = np.zeros([3, ATTR_COUNT], dtype=float)
        model_particles[:,1] = self.diam
        model_particles[0][0] = placement
        model_particles[1:][:,0] = np.array([placement - (self.diam/2), placement + (self.diam/2)]) 
        model_particles[1:][:,3] = np.array([left_id, right_id]) # set ids for expected supports
        model_particles[1:][:,2] = base_elevation
    
        particle = model_particles[0]
        # don't concern ourselves with the bed here
        empty_bed = np.empty((0, ATTR_COUNT))

        placed_x, placed_y, left_supp, right_supp = logic.place_particle(particle, model_particles, empty_bed, self.h)

        expected_left = left_id
        expected_right = right_id
        self.assertEqual(expected_left, left_supp)
        self.assertEqual(expected_right, right_supp)
        self.assertEqual(expected_elevation, placed_y)
        self.assertEqual(placement, placed_x)

Ancestors

  • unittest.case.TestCase

Methods

def setUp(self)

Hook method for setting up the test fixture before exercising it.

Expand source code
def setUp(self):
    self.diam = 0.5
    d = np.divide(np.multiply(np.divide(self.diam, 2), 
                                    self.diam), 
                                    self.diam)
    self.h = np.sqrt(np.square(self.diam) - np.square(d))
def test_bad_placement_raises_value_error(self)

A bad placement should raise a value error. In this case, a bad placement is any placement where a particle does not have a particle supporting it on the left and right sides.

Expand source code
def test_bad_placement_raises_value_error(self):
    """ A bad placement should raise a value error. 
    In this case, a bad placement is any placement where a particle does
    not have a particle supporting it on the left and right sides.
    """
    # 1) Particle (arbitrary placement) with empty model and bed particles arrays
    bad_particle = np.zeros([1, ATTR_COUNT], dtype=float)
    bad_particle[:,1] = self.diam
    empty_bed = np.empty((0, ATTR_COUNT))
    empty_model = np.empty((0, ATTR_COUNT))
    
    with self.assertRaises(ValueError):
        placed_x, placed_y = logic.place_particle(bad_particle[0], empty_model, empty_bed, self.h)

    # 2) Particle placed at invalid (unsupported) location
    unsupported_particle = np.zeros([1, ATTR_COUNT], dtype=float)
    unsupported_particle[:,0] = 0.9
    unsupported_particle[:,1] = self.diam

    unsupported_model = np.zeros((2, ATTR_COUNT))
    unsupported_model[:,0] = np.arange(0.5, 1.5, step=self.diam) 

    with self.assertRaises(ValueError):
        _, _, _, _ = logic.place_particle(unsupported_particle[0], unsupported_model, empty_bed, self.h)

    # 3) Particle placed at location where all model particles' centres have the same location
    # (i.e placed on a tall single stack/tower of particles)
    stacked_unsupported_particle = np.zeros([1, ATTR_COUNT], dtype=float)
    stacked_unsupported_particle[:,0] = 0.5
    stacked_unsupported_particle[:,1] = self.diam

    single_stack_model = np.zeros((2, ATTR_COUNT))
    single_stack_model[:,0] = 0.5
    single_stack_model[:,2] = np.arange(0, 1.0, step=self.diam)

    with self.assertRaises(ValueError):
        _, _, _, _  = logic.place_particle(stacked_unsupported_particle[0], single_stack_model, empty_bed, self.h)

    # 4) Particle and model particle array are identical
    particle = np.zeros([1, ATTR_COUNT], dtype=float)
    particle[:,1] = self.diam

    empty_bed = np.empty((0, ATTR_COUNT))
    model = np.empty((1, ATTR_COUNT)) 
    model[0] = particle

    with self.assertRaises(ValueError):
        _, _, _, _  = logic.place_particle(particle[0], model, empty_bed, self.h)
def test_good_placement_returns_valid_xy(self)

A good/valid placement should return a geometrically correct x and y value. A good placement is a placement where a particle has a left and right supprting particle.

Valid x and y is in accordance with the geometry discussed here: https://math.stackexchange.com/questions/2293201/

Expand source code
def test_good_placement_returns_valid_xy(self):
    """ A good/valid placement should return a geometrically
    correct x and y value. A good placement is a placement where
    a particle has a left and right supprting particle.

    Valid x and y is in accordance with the geometry 
    discussed here: https://math.stackexchange.com/questions/2293201/
    """
    base_elevation = 0
    left_id = 4.0
    right_id = 9.0
    expected_elevation = round(np.add(self.h, base_elevation), 2)

    placement = 0.75
    particle = np.zeros([1, ATTR_COUNT], dtype=float)
    particle[:,0] = placement

    # Create model particles with particle at index 0 being placed 
    # in between particles at index 1 and 2. 
    # particles at index 1 and 2 will be at the base_elevation
    model_particles = np.zeros([3, ATTR_COUNT], dtype=float)
    model_particles[:,1] = self.diam
    model_particles[0][0] = placement
    model_particles[1:][:,0] = np.array([placement - (self.diam/2), placement + (self.diam/2)]) 
    model_particles[1:][:,3] = np.array([left_id, right_id]) # set ids for expected supports
    model_particles[1:][:,2] = base_elevation

    particle = model_particles[0]
    # don't concern ourselves with the bed here
    empty_bed = np.empty((0, ATTR_COUNT))

    placed_x, placed_y, left_supp, right_supp = logic.place_particle(particle, model_particles, empty_bed, self.h)

    expected_left = left_id
    expected_right = right_id
    self.assertEqual(expected_left, left_supp)
    self.assertEqual(expected_right, right_supp)
    self.assertEqual(expected_elevation, placed_y)
    self.assertEqual(placement, placed_x)
class TestSetModelParticles (methodName='runTest')

Test the set_model_particles module

Attributes

diam
diameter of all particles in the test
pack_fraction
float representing packing density value
h
float value derived from diam - used in geometric placement of particles ontop of other particles
bed_particles
An n-7 array representing the bed particles for this test
available_vertices
A numpy array of vertices a particle is allowed to be placed at. Created with np.arange()

Create an instance of the class that will use the named test method when executed. Raises a ValueError if the instance does not have a method with the specified name.

Expand source code
class TestSetModelParticles(unittest.TestCase):
    """ Test the set_model_particles module

    Attributes:
        diam: diameter of all particles in the test
        pack_fraction: float representing packing density value
        h: float value derived from diam - used in geometric 
            placement of particles ontop of other particles
        bed_particles: An n-7 array representing the bed particles
            for this test 
        available_vertices: A numpy array of vertices a particle
            is allowed to be placed at. Created with np.arange()
    """

    def setUp(self):
        stream_length = 10
        self.diam = 0.5
        self.pack_fraction = 0.8
        # Directly from https://math.stackexchange.com/questions/2293201/
        # Variables used for geometric placement
        d = np.divide(np.multiply(np.divide(self.diam, 2), 
                                            self.diam), 
                                            self.diam)
        self.h = np.sqrt(np.square(self.diam) - np.square(d))

        # Mock a full bed_particles array
        num_bed_particles = int(stream_length/self.diam)
        bed_particles = np.zeros([num_bed_particles, ATTR_COUNT], dtype=float)
        bed_particles[:,0] = np.arange(self.diam/2, stream_length+(self.diam/2), step=self.diam)
        bed_particles[:,3] = np.arange(1, num_bed_particles+1)*-1
        self.bed_particles = bed_particles
        # Make all vertices created by the touching bed particles available
        # -----> 0.5, 1.0, 1.5, ... , 9.5 (with stream length 10)
        self.available_vertices = np.arange(self.diam, stream_length, step=self.diam)

    def test_model_particles_placed_at_valid_locations(self):
        """ Test that the function places particles
        only at vectors provided by available_vertices
        """
        model_particles, model_supports = logic.set_model_particles(self.bed_particles,   
                                                    self.available_vertices, 
                                                    self.diam, 
                                                    self.pack_fraction, 
                                                    self.h)
        # Particles should only be placed at available vertices
        self.assertTrue(set(model_particles[:,0]).issubset(self.available_vertices))  
        # All placements should be unique
        self.assertTrue(len(model_particles[:,0]) == len(set(model_particles[:,0])))
        # All ids should be unique
        # There should be no stacking
        self.assertEqual(len(set(model_particles[:,2])), 1)

    def test_all_model_particles_have_valid_initial_attributes(self):
        """ Test that the function produces particles 
        with valid initial attributes. Valid initial 
        attributes are unique IDs, correct diameter,
        active = 1, age = 0, loop = 0, and that
        all supports should be from the bed (id < 0)
        """
        model_particles, model_supports = logic.set_model_particles(self.bed_particles,   
                                                    self.available_vertices, 
                                                    self.diam, 
                                                    self.pack_fraction, 
                                                    self.h)
        # all diam = self.diam
        expected_diam = np.ones(len(model_particles)) * self.diam
        self.assertCountEqual(model_particles[:,1], expected_diam)

        # unique id's
        self.assertTrue(len(model_particles[:,3]) == len(set(model_particles[:,3])))
        
        # all model are active
        expected_activity = np.ones(len(model_particles)) 
        self.assertCountEqual(model_particles[:,4], expected_activity)

        # 0 age counter and loop age
        expected_age_and_loop = np.zeros(len(model_particles))
        self.assertCountEqual(model_particles[:,5], expected_age_and_loop)
        self.assertCountEqual(model_particles[:,6], expected_age_and_loop)

        # Supports should all be negative (resting on the bed)
        self.assertTrue(0, len(model_supports[model_supports > 0]))

Ancestors

  • unittest.case.TestCase

Methods

def setUp(self)

Hook method for setting up the test fixture before exercising it.

Expand source code
def setUp(self):
    stream_length = 10
    self.diam = 0.5
    self.pack_fraction = 0.8
    # Directly from https://math.stackexchange.com/questions/2293201/
    # Variables used for geometric placement
    d = np.divide(np.multiply(np.divide(self.diam, 2), 
                                        self.diam), 
                                        self.diam)
    self.h = np.sqrt(np.square(self.diam) - np.square(d))

    # Mock a full bed_particles array
    num_bed_particles = int(stream_length/self.diam)
    bed_particles = np.zeros([num_bed_particles, ATTR_COUNT], dtype=float)
    bed_particles[:,0] = np.arange(self.diam/2, stream_length+(self.diam/2), step=self.diam)
    bed_particles[:,3] = np.arange(1, num_bed_particles+1)*-1
    self.bed_particles = bed_particles
    # Make all vertices created by the touching bed particles available
    # -----> 0.5, 1.0, 1.5, ... , 9.5 (with stream length 10)
    self.available_vertices = np.arange(self.diam, stream_length, step=self.diam)
def test_all_model_particles_have_valid_initial_attributes(self)

Test that the function produces particles with valid initial attributes. Valid initial attributes are unique IDs, correct diameter, active = 1, age = 0, loop = 0, and that all supports should be from the bed (id < 0)

Expand source code
def test_all_model_particles_have_valid_initial_attributes(self):
    """ Test that the function produces particles 
    with valid initial attributes. Valid initial 
    attributes are unique IDs, correct diameter,
    active = 1, age = 0, loop = 0, and that
    all supports should be from the bed (id < 0)
    """
    model_particles, model_supports = logic.set_model_particles(self.bed_particles,   
                                                self.available_vertices, 
                                                self.diam, 
                                                self.pack_fraction, 
                                                self.h)
    # all diam = self.diam
    expected_diam = np.ones(len(model_particles)) * self.diam
    self.assertCountEqual(model_particles[:,1], expected_diam)

    # unique id's
    self.assertTrue(len(model_particles[:,3]) == len(set(model_particles[:,3])))
    
    # all model are active
    expected_activity = np.ones(len(model_particles)) 
    self.assertCountEqual(model_particles[:,4], expected_activity)

    # 0 age counter and loop age
    expected_age_and_loop = np.zeros(len(model_particles))
    self.assertCountEqual(model_particles[:,5], expected_age_and_loop)
    self.assertCountEqual(model_particles[:,6], expected_age_and_loop)

    # Supports should all be negative (resting on the bed)
    self.assertTrue(0, len(model_supports[model_supports > 0]))
def test_model_particles_placed_at_valid_locations(self)

Test that the function places particles only at vectors provided by available_vertices

Expand source code
def test_model_particles_placed_at_valid_locations(self):
    """ Test that the function places particles
    only at vectors provided by available_vertices
    """
    model_particles, model_supports = logic.set_model_particles(self.bed_particles,   
                                                self.available_vertices, 
                                                self.diam, 
                                                self.pack_fraction, 
                                                self.h)
    # Particles should only be placed at available vertices
    self.assertTrue(set(model_particles[:,0]).issubset(self.available_vertices))  
    # All placements should be unique
    self.assertTrue(len(model_particles[:,0]) == len(set(model_particles[:,0])))
    # All ids should be unique
    # There should be no stacking
    self.assertEqual(len(set(model_particles[:,2])), 1)
class TestUpdateFlux (methodName='runTest')

Unit test for the update_flux function

Attributes: test_length: An int representing the length of the test stream mock_subregion: A Mock of Subregion one_mock_subregion: A list with mock_subregion mock_subregion_1: A Mock of Subregion pair with mock_subregion_2 mock_subregion_2: A Mock of Subregion pair with mock_subregion_1 two_mock_subregion: A list with mock_subregion_1 and mock_subregion_2

Create an instance of the class that will use the named test method when executed. Raises a ValueError if the instance does not have a method with the specified name.

Expand source code
class TestUpdateFlux(unittest.TestCase): # Easy
    """ Unit test for the update_flux function

    Attributes: 
        test_length: An int representing the length of the test stream
        mock_subregion: A Mock of Subregion
        one_mock_subregion: A list with mock_subregion
        mock_subregion_1: A Mock of Subregion pair with mock_subregion_2
        mock_subregion_2: A Mock of Subregion pair with mock_subregion_1
        two_mock_subregion: A list with mock_subregion_1 
            and mock_subregion_2
    """
    def setUp(self):

        self.test_length = 10

        # mock one subregion
        self.mock_subregion = Mock()
        self.mock_subregion.leftBoundary.return_value = 0
        self.mock_subregion.rightBoundary.return_value = self.test_length
        self.one_mock_subregion = [self.mock_subregion]
        # mock multiple (2) subregions    
        self.mock_subregion_1 = Mock()
        self.mock_subregion_1.leftBoundary.return_value = 0
        self.mock_subregion_1.rightBoundary.return_value = self.test_length/2
        self.mock_subregion_2 = Mock()
        self.mock_subregion_2.leftBoundary.return_value = self.test_length/2
        self.mock_subregion_2.rightBoundary.return_value = self.test_length
        self.two_mock_subregion = [self.mock_subregion_1, self.mock_subregion_2]

    def test_different_lengths_raise_value_error(self):
        """ If the amount of intial positions and 
        final positions is not equal then a ValueError
        should be raised.
        """
        iteration = 0
        init_pos = np.arange(2)
        final_pos = np.arange(3)

        with self.assertRaises(ValueError):
            _ = logic.update_flux(init_pos, final_pos, iteration, self.mock_subregion)

    def test_0_crossings_call_increment_0_times(self):
        iteration = 0
        # Over a single subregion (no internal crossings possible)
        # -- Only 1 particle moving
        init_pos = np.array([5])
        final_pos = init_pos + 1
        
        subregion = logic.update_flux(init_pos, final_pos, iteration, self.one_mock_subregion)
        self.mock_subregion.incrementFlux.assert_not_called()

        # -- Multiple particles moving
        init_pos = np.array([1, 3, 5, 7, 8])
        final_pos = init_pos + 1 # No final positions >= 10
        subregion = logic.update_flux(init_pos, final_pos, iteration, self.one_mock_subregion)
        self.mock_subregion.incrementFlux.assert_not_called()

        # Over multiple subregions (internal crossings possible)
        # -- Only 1 particle moving
        init_pos = np.array([7])
        final_pos = init_pos + 1
        
        subregion = logic.update_flux(init_pos, final_pos, iteration, self.two_mock_subregion)
        self.mock_subregion.incrementFlux.assert_not_called()

        # -- Multiple particles moving
        init_pos = np.array([1, 3, 6, 7, 8])
        final_pos = init_pos + 1 # No final positions >= 10
        subregion = logic.update_flux(init_pos, final_pos, iteration, self.one_mock_subregion)
        self.mock_subregion.incrementFlux.assert_not_called()

    def test_n_crossings_call_increment_n_times(self):
        # Over one subregion
        # -- Over 1 iteration
        iteration = 0
        init_pos = np.array([5, 6, 7, 8])
        final_pos = init_pos + 5 
        subregion = logic.update_flux(init_pos, final_pos, iteration, self.one_mock_subregion)
        self.assertEqual(4, self.mock_subregion.incrementFlux.call_count)
        self.mock_subregion.incrementFlux.assert_called_with(0) # Only iteration 0
    
        # -- Over multiple subregions
        iteration = 0
        init_pos = np.array([1, 2, 3, 6, 7, 8])
        final_pos = init_pos + 5 
        subregion = logic.update_flux(init_pos, final_pos, iteration, self.two_mock_subregion)
        
        self.assertEqual(3, self.mock_subregion_1.incrementFlux.call_count)
        self.assertEqual(3, self.mock_subregion_2.incrementFlux.call_count)
        
        self.mock_subregion_1.incrementFlux.assert_called_with(0)
        self.mock_subregion_2.incrementFlux.assert_called_with(0) 

    def test_particle_crossing_multiple_subregions_calls_increment_on_each(self):
        iteration = 0
        init_pos = np.array([1])
        final_pos = np.array([11]) # Will cross 5 and 10
        subregion = logic.update_flux(init_pos, final_pos, iteration, self.two_mock_subregion)
        self.mock_subregion_1.incrementFlux.assert_called_once_with(0)
        self.mock_subregion_2.incrementFlux.assert_called_once_with(0) 

    def test_ghost_particle_calls_increment_on_final_subregion(self):
        iteration = 0
        init_pos = np.array([1, 6])
        final_pos = np.array([-1, -1])
        subregion = logic.update_flux(init_pos, final_pos, iteration, self.one_mock_subregion)
        self.assertEqual(2, self.mock_subregion.incrementFlux.call_count)
        self.mock_subregion.incrementFlux.assert_called_with(0) 

    def tearDown(self):
        self.mock_subregion.reset_mock()
        self.mock_subregion_1.reset_mock()
        self.mock_subregion_2.reset_mock()

Ancestors

  • unittest.case.TestCase

Methods

def setUp(self)

Hook method for setting up the test fixture before exercising it.

Expand source code
def setUp(self):

    self.test_length = 10

    # mock one subregion
    self.mock_subregion = Mock()
    self.mock_subregion.leftBoundary.return_value = 0
    self.mock_subregion.rightBoundary.return_value = self.test_length
    self.one_mock_subregion = [self.mock_subregion]
    # mock multiple (2) subregions    
    self.mock_subregion_1 = Mock()
    self.mock_subregion_1.leftBoundary.return_value = 0
    self.mock_subregion_1.rightBoundary.return_value = self.test_length/2
    self.mock_subregion_2 = Mock()
    self.mock_subregion_2.leftBoundary.return_value = self.test_length/2
    self.mock_subregion_2.rightBoundary.return_value = self.test_length
    self.two_mock_subregion = [self.mock_subregion_1, self.mock_subregion_2]
def tearDown(self)

Hook method for deconstructing the test fixture after testing it.

Expand source code
def tearDown(self):
    self.mock_subregion.reset_mock()
    self.mock_subregion_1.reset_mock()
    self.mock_subregion_2.reset_mock()
def test_0_crossings_call_increment_0_times(self)
Expand source code
def test_0_crossings_call_increment_0_times(self):
    iteration = 0
    # Over a single subregion (no internal crossings possible)
    # -- Only 1 particle moving
    init_pos = np.array([5])
    final_pos = init_pos + 1
    
    subregion = logic.update_flux(init_pos, final_pos, iteration, self.one_mock_subregion)
    self.mock_subregion.incrementFlux.assert_not_called()

    # -- Multiple particles moving
    init_pos = np.array([1, 3, 5, 7, 8])
    final_pos = init_pos + 1 # No final positions >= 10
    subregion = logic.update_flux(init_pos, final_pos, iteration, self.one_mock_subregion)
    self.mock_subregion.incrementFlux.assert_not_called()

    # Over multiple subregions (internal crossings possible)
    # -- Only 1 particle moving
    init_pos = np.array([7])
    final_pos = init_pos + 1
    
    subregion = logic.update_flux(init_pos, final_pos, iteration, self.two_mock_subregion)
    self.mock_subregion.incrementFlux.assert_not_called()

    # -- Multiple particles moving
    init_pos = np.array([1, 3, 6, 7, 8])
    final_pos = init_pos + 1 # No final positions >= 10
    subregion = logic.update_flux(init_pos, final_pos, iteration, self.one_mock_subregion)
    self.mock_subregion.incrementFlux.assert_not_called()
def test_different_lengths_raise_value_error(self)

If the amount of intial positions and final positions is not equal then a ValueError should be raised.

Expand source code
def test_different_lengths_raise_value_error(self):
    """ If the amount of intial positions and 
    final positions is not equal then a ValueError
    should be raised.
    """
    iteration = 0
    init_pos = np.arange(2)
    final_pos = np.arange(3)

    with self.assertRaises(ValueError):
        _ = logic.update_flux(init_pos, final_pos, iteration, self.mock_subregion)
def test_ghost_particle_calls_increment_on_final_subregion(self)
Expand source code
def test_ghost_particle_calls_increment_on_final_subregion(self):
    iteration = 0
    init_pos = np.array([1, 6])
    final_pos = np.array([-1, -1])
    subregion = logic.update_flux(init_pos, final_pos, iteration, self.one_mock_subregion)
    self.assertEqual(2, self.mock_subregion.incrementFlux.call_count)
    self.mock_subregion.incrementFlux.assert_called_with(0) 
def test_n_crossings_call_increment_n_times(self)
Expand source code
def test_n_crossings_call_increment_n_times(self):
    # Over one subregion
    # -- Over 1 iteration
    iteration = 0
    init_pos = np.array([5, 6, 7, 8])
    final_pos = init_pos + 5 
    subregion = logic.update_flux(init_pos, final_pos, iteration, self.one_mock_subregion)
    self.assertEqual(4, self.mock_subregion.incrementFlux.call_count)
    self.mock_subregion.incrementFlux.assert_called_with(0) # Only iteration 0

    # -- Over multiple subregions
    iteration = 0
    init_pos = np.array([1, 2, 3, 6, 7, 8])
    final_pos = init_pos + 5 
    subregion = logic.update_flux(init_pos, final_pos, iteration, self.two_mock_subregion)
    
    self.assertEqual(3, self.mock_subregion_1.incrementFlux.call_count)
    self.assertEqual(3, self.mock_subregion_2.incrementFlux.call_count)
    
    self.mock_subregion_1.incrementFlux.assert_called_with(0)
    self.mock_subregion_2.incrementFlux.assert_called_with(0) 
def test_particle_crossing_multiple_subregions_calls_increment_on_each(self)
Expand source code
def test_particle_crossing_multiple_subregions_calls_increment_on_each(self):
    iteration = 0
    init_pos = np.array([1])
    final_pos = np.array([11]) # Will cross 5 and 10
    subregion = logic.update_flux(init_pos, final_pos, iteration, self.two_mock_subregion)
    self.mock_subregion_1.incrementFlux.assert_called_once_with(0)
    self.mock_subregion_2.incrementFlux.assert_called_once_with(0) 
class TestUpdateParticleStates (methodName='runTest')

Test the update_particle_states function.

Attributes

diam
the diameter of the test particles

Create an instance of the class that will use the named test method when executed. Raises a ValueError if the instance does not have a method with the specified name.

Expand source code
class TestUpdateParticleStates(unittest.TestCase):
    """ Test the update_particle_states function.

    Attributes:
        diam: the diameter of the test particles
    """
    def setUp(self):
        self.diam = 0.5 

    def test_no_piles_returns_all_active(self): 
        """ If there are N model particles resting directly on 
        the bed and there are no model particles stacked
        ontop of other model particles then all N particles
        should be returned as active.
        """
        no_pile_particles = np.zeros((7, ATTR_COUNT))
        no_pile_particles[:,1] = self.diam
        no_pile_particles[:,2] = 1.0
        no_pile_particles[:,0] = np.arange(self.diam, 3.5+(self.diam), step=self.diam)

        bed = np.zeros([8, ATTR_COUNT], dtype=float)
        bed[:,1] = self.diam
        bed[:,3] = np.arange(1,9)*-1
        bed[:,0] = np.arange(self.diam/2, 4.0+(self.diam/2), step=self.diam)

        # hard-coded supports array. If there is only one layer of n model particles
        # resting on n+1 bed particles, then this is the form the supports will take
        # assuming model particle 0 is placed between bed particle 0 and 1, model 
        # particle 1 is placed between bed particle 1 and 2, etc.
        no_pile_supports = np.array([[-1, -2],[-2, -3],[-3, -4],[-4, -5],[-5, -6],[-6, -7],[-7, -8]])

        returned_particles = logic.update_particle_states(no_pile_particles, no_pile_supports)

        expected_active = np.ones((7, ATTR_COUNT))
        self.assertIsNone(np.testing.assert_array_equal(expected_active[:,4], returned_particles[:,4]))
    
    def test_2_layers_returns_top_layer_active(self):
        """ If there are N model particles resting
        directly on the bed and there are M model particles
        resting on top of those N model particles (so much
        so that every N particle is supporting a particle)
        then the N particles should be inactive and the 
        M particles should be active
        """
        # Test perfect stacks only return the top layer active
        # TODO: defince perfect stack? maybe call tight layers?
        two_layer_particles = np.zeros((13, ATTR_COUNT))
        two_layer_particles[:,1] = self.diam
        # First layer of particles set at elevation 1
        two_layer_particles[0:7,2] = 1.0
        two_layer_particles[0:7,0] = np.arange(self.diam, 3.5+(self.diam), step=self.diam)
        # Second layer of particles set at elevation 2
        two_layer_particles[7:13,2] = 2.0
        two_layer_particles[7:13,0] = np.arange(self.diam+self.diam/2, 3+(self.diam), step=self.diam)
        two_layer_particles[:,3] = np.arange(13)
    
        # Bed particles set at elevation 0
        bed = np.zeros([9, ATTR_COUNT], dtype=float)
        bed[:,3] = np.arange(1,10)*-1
        bed[:,1] = self.diam
        bed[:,0] = np.arange(self.diam/2, 4.5+(self.diam/2), step=self.diam)

        # the 7 particle in the first model particles layer will have bed particles as supports. The 6 
        # particles in the second layer will have the first 7 model particles as supports
        two_layer_supports = np.array([[-1, -2],[-2, -3],[-3, -4],[-4, -5],[-5, -6],[-6, -7], [-7, -8],
                                            [0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6]])

        returned_particles = logic.update_particle_states(two_layer_particles, two_layer_supports)
        expected_inactive = np.zeros(7)
        expected_active = np.ones(6)
        expected_active_state = np.concatenate((expected_inactive, expected_active))
        self.assertIsNone(np.testing.assert_array_equal(expected_active_state, returned_particles[:,4]))

    def test_some_piles_return_valid_active(self):
        """ Any model particle that is supporting
        another model particle should be inactive. Any
        model particle that is not supporting another
        model particle should be active. 
        """

        some_piles_particles = np.zeros((9, ATTR_COUNT))
        some_piles_particles[:,1] = self.diam
        some_piles_particles[:,3] = np.arange(9)
        # First 7 partciles set at elevation 1
        some_piles_particles[0:7,2] = 1.0
        some_piles_particles[0:7,0] = np.arange(self.diam, 3.5+(self.diam), step=self.diam)
        # Final 2 particles set at elevation 2
        some_piles_particles[7:9,2] = 2.0
        # Place first particle at first vertex formed by the first layer of model particles
        # Place second particle at 
        some_piles_particles[7:9,0] = np.array([some_piles_particles[0][0]+(self.diam/2), some_piles_particles[4][0]+(self.diam/2)])

        bed = np.zeros([9, ATTR_COUNT], dtype=float)
        bed[:,1] = self.diam
        bed[:,3] = np.arange(1,10)*-1
        bed[:,0] = np.arange(self.diam/2, 4.5+(self.diam/2), step=self.diam)

        some_piles_supports = np.array([[-1, -2],[-2, -3],[-3, -4],[-4, -5],[-5, -6],[-6, -7],
                                            [-7, -8], [0, 1], [4, 5]])
        
        returned_particles = logic.update_particle_states(some_piles_particles, some_piles_supports)
        expected_active_state = np.zeros(9)
        # Particles are supported by 0,1, 4, and 5. Every other particle (0-8) should be available
        expected_active_state[[2, 3, 6, 7, 8]] = 1
        self.assertIsNone(np.testing.assert_array_equal(expected_active_state, returned_particles[:,4]))

Ancestors

  • unittest.case.TestCase

Methods

def setUp(self)

Hook method for setting up the test fixture before exercising it.

Expand source code
def setUp(self):
    self.diam = 0.5 
def test_2_layers_returns_top_layer_active(self)

If there are N model particles resting directly on the bed and there are M model particles resting on top of those N model particles (so much so that every N particle is supporting a particle) then the N particles should be inactive and the M particles should be active

Expand source code
def test_2_layers_returns_top_layer_active(self):
    """ If there are N model particles resting
    directly on the bed and there are M model particles
    resting on top of those N model particles (so much
    so that every N particle is supporting a particle)
    then the N particles should be inactive and the 
    M particles should be active
    """
    # Test perfect stacks only return the top layer active
    # TODO: defince perfect stack? maybe call tight layers?
    two_layer_particles = np.zeros((13, ATTR_COUNT))
    two_layer_particles[:,1] = self.diam
    # First layer of particles set at elevation 1
    two_layer_particles[0:7,2] = 1.0
    two_layer_particles[0:7,0] = np.arange(self.diam, 3.5+(self.diam), step=self.diam)
    # Second layer of particles set at elevation 2
    two_layer_particles[7:13,2] = 2.0
    two_layer_particles[7:13,0] = np.arange(self.diam+self.diam/2, 3+(self.diam), step=self.diam)
    two_layer_particles[:,3] = np.arange(13)

    # Bed particles set at elevation 0
    bed = np.zeros([9, ATTR_COUNT], dtype=float)
    bed[:,3] = np.arange(1,10)*-1
    bed[:,1] = self.diam
    bed[:,0] = np.arange(self.diam/2, 4.5+(self.diam/2), step=self.diam)

    # the 7 particle in the first model particles layer will have bed particles as supports. The 6 
    # particles in the second layer will have the first 7 model particles as supports
    two_layer_supports = np.array([[-1, -2],[-2, -3],[-3, -4],[-4, -5],[-5, -6],[-6, -7], [-7, -8],
                                        [0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6]])

    returned_particles = logic.update_particle_states(two_layer_particles, two_layer_supports)
    expected_inactive = np.zeros(7)
    expected_active = np.ones(6)
    expected_active_state = np.concatenate((expected_inactive, expected_active))
    self.assertIsNone(np.testing.assert_array_equal(expected_active_state, returned_particles[:,4]))
def test_no_piles_returns_all_active(self)

If there are N model particles resting directly on the bed and there are no model particles stacked ontop of other model particles then all N particles should be returned as active.

Expand source code
def test_no_piles_returns_all_active(self): 
    """ If there are N model particles resting directly on 
    the bed and there are no model particles stacked
    ontop of other model particles then all N particles
    should be returned as active.
    """
    no_pile_particles = np.zeros((7, ATTR_COUNT))
    no_pile_particles[:,1] = self.diam
    no_pile_particles[:,2] = 1.0
    no_pile_particles[:,0] = np.arange(self.diam, 3.5+(self.diam), step=self.diam)

    bed = np.zeros([8, ATTR_COUNT], dtype=float)
    bed[:,1] = self.diam
    bed[:,3] = np.arange(1,9)*-1
    bed[:,0] = np.arange(self.diam/2, 4.0+(self.diam/2), step=self.diam)

    # hard-coded supports array. If there is only one layer of n model particles
    # resting on n+1 bed particles, then this is the form the supports will take
    # assuming model particle 0 is placed between bed particle 0 and 1, model 
    # particle 1 is placed between bed particle 1 and 2, etc.
    no_pile_supports = np.array([[-1, -2],[-2, -3],[-3, -4],[-4, -5],[-5, -6],[-6, -7],[-7, -8]])

    returned_particles = logic.update_particle_states(no_pile_particles, no_pile_supports)

    expected_active = np.ones((7, ATTR_COUNT))
    self.assertIsNone(np.testing.assert_array_equal(expected_active[:,4], returned_particles[:,4]))
def test_some_piles_return_valid_active(self)

Any model particle that is supporting another model particle should be inactive. Any model particle that is not supporting another model particle should be active.

Expand source code
def test_some_piles_return_valid_active(self):
    """ Any model particle that is supporting
    another model particle should be inactive. Any
    model particle that is not supporting another
    model particle should be active. 
    """

    some_piles_particles = np.zeros((9, ATTR_COUNT))
    some_piles_particles[:,1] = self.diam
    some_piles_particles[:,3] = np.arange(9)
    # First 7 partciles set at elevation 1
    some_piles_particles[0:7,2] = 1.0
    some_piles_particles[0:7,0] = np.arange(self.diam, 3.5+(self.diam), step=self.diam)
    # Final 2 particles set at elevation 2
    some_piles_particles[7:9,2] = 2.0
    # Place first particle at first vertex formed by the first layer of model particles
    # Place second particle at 
    some_piles_particles[7:9,0] = np.array([some_piles_particles[0][0]+(self.diam/2), some_piles_particles[4][0]+(self.diam/2)])

    bed = np.zeros([9, ATTR_COUNT], dtype=float)
    bed[:,1] = self.diam
    bed[:,3] = np.arange(1,10)*-1
    bed[:,0] = np.arange(self.diam/2, 4.5+(self.diam/2), step=self.diam)

    some_piles_supports = np.array([[-1, -2],[-2, -3],[-3, -4],[-4, -5],[-5, -6],[-6, -7],
                                        [-7, -8], [0, 1], [4, 5]])
    
    returned_particles = logic.update_particle_states(some_piles_particles, some_piles_supports)
    expected_active_state = np.zeros(9)
    # Particles are supported by 0,1, 4, and 5. Every other particle (0-8) should be available
    expected_active_state[[2, 3, 6, 7, 8]] = 1
    self.assertIsNone(np.testing.assert_array_equal(expected_active_state, returned_particles[:,4]))