From cd7afe4bd3288e57d139aa10a5c474e743620ac5 Mon Sep 17 00:00:00 2001 From: Joshua Potter Date: Wed, 17 Jun 2015 23:27:17 -0400 Subject: [PATCH] Plane and testing --- src/plane.py | 206 +++++++++++++++++++++++++------------------- tests/plane_test.py | 96 +++++++++++++++++++++ 2 files changed, 211 insertions(+), 91 deletions(-) create mode 100644 tests/plane_test.py diff --git a/src/plane.py b/src/plane.py index 50215b2..1d11307 100644 --- a/src/plane.py +++ b/src/plane.py @@ -1,97 +1,145 @@ """ -Wrapper of a numpy array of bits. +Wrapper of a bitarray. -For the sake of efficiency, rather than work with an (m x m x ... x m) N-dimensional grid, we instead work with -a 1D array of size (N-1)^m and reshape the grid if ever necessary. All bits of any given row is represented by -a number whose binary representation expands to the given number. A 1 at index i in turn corresponds to an on -state at the ith index of the given row. This holds for 0 as well. +For the sake of compactness, the use of numpy arrays have been completely abandoned as a representation +of the data. This also allows for a bit more consistency throughout the library, where I've often used +the flat iterator provided by numpy, and other times used the actual array. -For example, given a 100 x 100 CAM, we represent this underneath as a 1-D array of 100 integers, each of which's -binary expansion will be 100 bits long (and padded with 0's if necessary). +The use of just a bitarray also means it is significantly more compact, indexing of a plane should be +more efficient, and the entire association between an N-1 dimensional grid with the current shape of +the plane is no longer a concern. @date: June 05, 2015 """ +import random +import operator import numpy as np +from functools import reduce from bitarray import bitarray +from collections import deque class Plane: """ - Represents a bit plane, with underlying usage of numpy arrays. + Represents a cell plane, with underlying usage of bitarrays. - The following allows conversion between our given representation of a grid, and the user's expected - representation of a grid. This allows accessing of bits in the same manner as one would access a - numpy grid, without the same bloat as a straightforward N-dimensional grid of booleans for instance. + The following maintains the shape of a contiguous block of memory, allowing the user to interact + with it as if it was a multidimensional array. """ - def __init__(self, shape, grid = None): + def __init__(self, shape, bits = None): """ Construction of a plane. There are three cases: - If shape is the empty tuple, we have an undefined plane. Nothing is in it. - If shape is length 1, we have a 1D plane. This is represented by a single number. - Otherwise, we have an N-D plane. Everything operates as expected. - """ - self.grid = grid - self.shape = shape - self.N = 0 if not len(shape) else shape[-1] + First, the user may pass in there own custom bitarray to allow manipulating the + data in the same manner as it is done internally. When this happens, the product + of the shape parameter's components must be equivalent to the length of the + bitarray. - if self.grid is None: - if len(shape) == 1: - self.grid = self.N * bitarray('0') - else: - self.grid = np.empty(shape[:-1], dtype=np.object) - for i in range(self.grid.size): - self.grid.flat[i] = self.N * bitarray('0') + Otherwise, we determine a grid based off solely the shape parameter. If shape + is the empty tuple, we have an undefined plane. Consequently nothing is in it. + + Lastly, the most common usage will be to define an N-D plane, where N is equivalent + to the number of components in the shape. For example, @shape = (100,100,100) means + we have a 3-D grid with 1000000 cells in total (not necessarily in the CAM, just + the plane in question). + """ + # Keep track of dimensionality + self.shape = shape + self.N = len(shape) + + # Preprocess all index offsets instead of performing them each time accessing occurs + prod = 1 + self.offsets = deque() + for d in reversed(shape): + self.offsets.appendleft(prod) + prod *= d + + # Allow the user to override grid construction + if bits is not None: + if len(bits) != reduce(operator.mul, shape, 1): + raise ValueError("Shape with incorrect dimensionality") + self.bits = bits + # Generate bitarray automatically + else: + self.bits = reduce(operator.mul, shape, 1) * bitarray('0') # Check if a plane has been updated recently + # This should be changed to True if it is ever "ticked." self.dirty = False - def __getitem__(self, index): """ - Indices supported are the same as those of the numpy array, except for when accessing an individual bit. + Indexing of a plane mirrors that of a numpy array. - When reaching the "last" dimension of the given array, we access the bit of the number at the second - to last dimension, since we are working in (N-1)-dimensional space. Unless this last dimension is reached, - we always return a plane object (otherwise an actual 0 or 1). + Unless accessing the "last" dimension of the given array, we return another plane with + the sliced bitarray as the underlying bits parameter. This allows for chaining access operators. + That being said, it is preferable to use a tuple for accessing as opposed to a sequence of + bracket indexing, as this does not create as many planes in the process. - Note this function is much slower than accessing the grid directly. To forsake some convenience for the - considerable speed boost is understandable; access by plane only for this convenience. + If the "last" dimension is reached, we simply return the bit at the given index. """ - # Given coordinates of a grid. This may or may not access the last dimension. # If it does not, can simply return the new plane given the subset accessed. - # If it does, we access up to the bitarray, then access the desired bit(s) + # If it does, we return the actual bit. if type(index) is tuple: - if len(index) == len(self.shape): - b_array = self.grid[index[:-1]] - return b_array[index[-1]] + offset = sum([x*y for (x,y) in zip(index, self.offsets)]) + if len(index) == self.N: + return self.bits[offset] else: - subgrid = self.grid[index] - return Plane(subgrid.shape + (self.N,), subgrid) + remain = self.shape[len(index):] + shift = self.offsets[len(index)-1] + return Plane(remain, bits[offset:offset+shift]) - # If we've reached the last dimension, the grid of the plane is just a bitarray - # (we remove an additional dimension and instead store the bitarray for space's sake). - # If a list of elements are passed, we must access them individually (bitarray does - # not have built in support for this). - elif len(self.shape) == 1: - if type(index) is list: - return list(map(lambda x: self.grid[x], index)) + # A list accessor allows one to access multiple elements at the offsets specified + # by the list elements. For example, for a plane P, P[[1, 4, 6]] returns a list + # containing the 1, 4, and 6th element. + elif type(index) is list: + elements = [] + for idx in index: + elements.append(self[idx]) + return elements + + # Otherwise we were passed a simply number and we access the element like normal + # (making sure to consider the shape of the plane of course) + elif self.N == 1: + return self.bits[index] + else: + delta = self.offsets[0] + offset = index * delta + return Plane(self.shape[1:], self.bits[offset:offset+delta]) + + def __setitem__(self, index, value): + """ + Assigns a bit or slice of bits to a given index. + + Very similar to __getitem__ with a couple of caveats. Value should always + be a single bit (either 0 or 1), but the index can still be a tuple, list, + etc. The given value is assigned to all components of a given index. + + For example, with a plane P with shape (100, 100), P[0] = 1 sets the first + 100 elements (the 100 bits in the first row) to 1. + """ + if type(index) is tuple: + offset = sum([x*y for (x,y) in zip(index, self.offsets)]) + if len(index) == self.N: + self.bits[offset] = value else: - return self.grid[index] + shift = self.offsets[len(index)-1] + self.bits[offset:offset+shift] = value - # Any other means of indexing simply indexes the grid as expected, and wraps - # the result in a plane object for consistent accessing. An attribute error - # can occur once we reach the last dimension, and try to determine the shape - # of a bitarray - tmp = self.grid[index] - try: - return Plane(tmp.shape + (self.N,), tmp) - except AttributeError: - return Plane((self.N,), tmp) + elif type(index) is list: + elements = [] + for idx in index: + self[idx] = value + elif self.N == 1: + self.bits[index] = value + else: + delta = self.offsets[0] + offset = index * delta + self.bits[offset:offset+delta] = value def randomize(self): """ @@ -103,42 +151,18 @@ class Plane: the same value. I'm not really too interested in figuring this out, so I use the alternate method below. """ - if len(self.shape) > 0: - import random as r - max_u = 2**self.N - 1 - gen = lambda: bin(r.randrange(0, max_u))[2:] - if len(self.shape) == 1: - self.grid = bitarray(gen().zfill(self.N)) - else: - for i in range(self.grid.size): - self.grid.flat[i] = bitarray(gen().zfill(self.N)) + if self.N > 0: + max_unsigned = reduce(operator.mul, self.shape, 1) + sequence = bin(random.randrange(0, max_unsigned))[2:] + self.bits = bitarray(sequence.zfill(max_unsigned)) - - def flatten(self, coordinate): + def matrix(self): """ - Converts a coordinate (which could be used to access a bit in a plane) and "flattens" it. + Convert bitarray into a corresponding numpy matrix. - By this we mean we convert the coordinate into an index and bit offset corresponding to - the plane grid converted to 1D (think numpy.ndarray.flat). + This should not be used for computation! This is merely a convenience method + for displaying out to matplotlib via the AxesImages plotting methods. """ - flat_index, gridprod = 0, 1 - for i in reversed(range(len(coordinate[:-1]))): - flat_index += coordinate[i] * gridprod - gridprod *= self.shape[i] - - return flat_index, coordinate[-1] - - - def bits(self): - """ - Expands out bitarray into individual bits. - - This is useful for display in matplotlib for example, but does take a dimension more space. - """ - if len(self.shape) == 1: - return np.array(self.grid) - else: - tmp = np.array([list(self.grid.flat[i]) for i in range(self.grid.size)]) - return np.reshape(tmp, self.shape) - + tmp = np.array(self.bits) + return np.reshape(tmp, self.shape) diff --git a/tests/plane_test.py b/tests/plane_test.py new file mode 100644 index 0000000..a103fc9 --- /dev/null +++ b/tests/plane_test.py @@ -0,0 +1,96 @@ +import os, sys +sys.path.insert(0, os.path.join('..', 'src')) + +import plane + + +class TestProperties: + """ + + """ + def setUp(self): + self.plane2d = plane.Plane((100, 100)) + self.plane3d = plane.Plane((100, 100, 100)) + + def test_bitsLength(self): + """ + Bit expansion. + """ + assert len(self.plane2d.bits) == 100 * 100 + assert len(self.plane3d.bits) == 100 * 100 * 100 + + def test_randomize(self): + """ + Randomization. + """ + bits2d = self.plane2d.bits + bits3d = self.plane3d.bits + self.plane2d.randomize() + self.plane3d.randomize() + + assert bits2d != self.plane2d.bits + assert bits3d != self.plane3d.bits + assert len(self.plane2d.bits) == 100 * 100 + assert len(self.plane3d.bits) == 100 * 100 * 100 + + +class TestIndexing: + """ + + """ + def setUp(self): + self.plane2d = plane.Plane((100, 100)) + self.plane3d = plane.Plane((100, 100, 100)) + + def test_tupleAssignment(self): + """ + Tuple Assignment. + """ + self.plane2d[(1, 0)] = 1 + self.plane3d[(1, 0, 0)] = 1 + + def test_listAssignment(self): + """ + List Assignment. + """ + self.plane2d[[0]] = 1 + self.plane3d[[0]] = 1 + self.plane2d[[[(4, 5)], 5, (2, 2)]] = 1 + self.plane3d[[[(4, 5)], 5, (2, 2)]] = 1 + + def test_singleAssignment(self): + """ + Single Assignment. + """ + self.plane2d[0][0] = 1 + self.plane3d[0][0] = 1 + + def test_tupleAccessing(self): + """ + Tuple Accessing. + """ + self.plane2d[(1, 0)] = 1 + assert self.plane2d[(1, 0)] == 1 + + self.plane3d[(1, 0)] = 1 + for i in range(10): + assert self.plane3d[(1, 0, i)] == 1 + + def test_listAccessing(self): + """ + List Accessing. + """ + self.plane2d[[(0, 4), (1, 5)]] = 1 + assert self.plane2d[[(0, 4), (1, 5), (1, 6)]] == [1, 1, 0] + + self.plane3d[[(0, 4)]] = 1 + assert self.plane3d[[(0, 4, 1), (0, 4, 9), (1, 6, 0)]] == [1, 1, 0] + + def test_singleAccessing(self): + """ + Single Accessing. + """ + self.plane2d[0] = 1 + for i in range(10): + assert self.plane2d[0][i] == 1 +