r
/
fifth
1
Fork 0

Plane and testing

master
Joshua Potter 2015-06-17 23:27:17 -04:00
parent a1737b3963
commit cd7afe4bd3
2 changed files with 211 additions and 91 deletions

View File

@ -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 For the sake of compactness, the use of numpy arrays have been completely abandoned as a representation
a 1D array of size (N-1)^m and reshape the grid if ever necessary. All bits of any given row is represented by of the data. This also allows for a bit more consistency throughout the library, where I've often used
a number whose binary representation expands to the given number. A 1 at index i in turn corresponds to an on the flat iterator provided by numpy, and other times used the actual array.
state at the ith index of the given row. This holds for 0 as well.
For example, given a 100 x 100 CAM, we represent this underneath as a 1-D array of 100 integers, each of which's The use of just a bitarray also means it is significantly more compact, indexing of a plane should be
binary expansion will be 100 bits long (and padded with 0's if necessary). 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 @date: June 05, 2015
""" """
import random
import operator
import numpy as np import numpy as np
from functools import reduce
from bitarray import bitarray from bitarray import bitarray
from collections import deque
class Plane: 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 The following maintains the shape of a contiguous block of memory, allowing the user to interact
representation of a grid. This allows accessing of bits in the same manner as one would access a with it as if it was a multidimensional array.
numpy grid, without the same bloat as a straightforward N-dimensional grid of booleans for instance.
""" """
def __init__(self, shape, grid = None): def __init__(self, shape, bits = None):
""" """
Construction of a plane. There are three cases: Construction of a plane. There are three cases:
If shape is the empty tuple, we have an undefined plane. Nothing is in it. First, the user may pass in there own custom bitarray to allow manipulating the
If shape is length 1, we have a 1D plane. This is represented by a single number. data in the same manner as it is done internally. When this happens, the product
Otherwise, we have an N-D plane. Everything operates as expected. of the shape parameter's components must be equivalent to the length of the
""" bitarray.
self.grid = grid
self.shape = shape
self.N = 0 if not len(shape) else shape[-1]
if self.grid is None: Otherwise, we determine a grid based off solely the shape parameter. If shape
if len(shape) == 1: is the empty tuple, we have an undefined plane. Consequently nothing is in it.
self.grid = self.N * bitarray('0')
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: else:
self.grid = np.empty(shape[:-1], dtype=np.object) self.bits = reduce(operator.mul, shape, 1) * bitarray('0')
for i in range(self.grid.size):
self.grid.flat[i] = self.N * bitarray('0')
# Check if a plane has been updated recently # Check if a plane has been updated recently
# This should be changed to True if it is ever "ticked."
self.dirty = False self.dirty = False
def __getitem__(self, index): 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 Unless accessing the "last" dimension of the given array, we return another plane with
to last dimension, since we are working in (N-1)-dimensional space. Unless this last dimension is reached, the sliced bitarray as the underlying bits parameter. This allows for chaining access operators.
we always return a plane object (otherwise an actual 0 or 1). 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 If the "last" dimension is reached, we simply return the bit at the given index.
considerable speed boost is understandable; access by plane only for this convenience.
""" """
# Given coordinates of a grid. This may or may not access the last dimension. # 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 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 type(index) is tuple:
if len(index) == len(self.shape): offset = sum([x*y for (x,y) in zip(index, self.offsets)])
b_array = self.grid[index[:-1]] if len(index) == self.N:
return b_array[index[-1]] return self.bits[offset]
else: else:
subgrid = self.grid[index] remain = self.shape[len(index):]
return Plane(subgrid.shape + (self.N,), subgrid) 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 # A list accessor allows one to access multiple elements at the offsets specified
# (we remove an additional dimension and instead store the bitarray for space's sake). # by the list elements. For example, for a plane P, P[[1, 4, 6]] returns a list
# If a list of elements are passed, we must access them individually (bitarray does # containing the 1, 4, and 6th element.
# not have built in support for this). elif type(index) is list:
elif len(self.shape) == 1: elements = []
if type(index) is list: for idx in index:
return list(map(lambda x: self.grid[x], 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: else:
return self.grid[index] delta = self.offsets[0]
offset = index * delta
return Plane(self.shape[1:], self.bits[offset:offset+delta])
# Any other means of indexing simply indexes the grid as expected, and wraps def __setitem__(self, index, value):
# 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 Assigns a bit or slice of bits to a given index.
# of a bitarray
tmp = self.grid[index]
try:
return Plane(tmp.shape + (self.N,), tmp)
except AttributeError:
return Plane((self.N,), tmp)
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:
shift = self.offsets[len(index)-1]
self.bits[offset:offset+shift] = value
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): 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 the same value. I'm not really too interested in figuring this out, so I use the alternate
method below. method below.
""" """
if len(self.shape) > 0: if self.N > 0:
import random as r max_unsigned = reduce(operator.mul, self.shape, 1)
max_u = 2**self.N - 1 sequence = bin(random.randrange(0, max_unsigned))[2:]
gen = lambda: bin(r.randrange(0, max_u))[2:] self.bits = bitarray(sequence.zfill(max_unsigned))
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))
def matrix(self):
def flatten(self, coordinate):
""" """
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 This should not be used for computation! This is merely a convenience method
the plane grid converted to 1D (think numpy.ndarray.flat). for displaying out to matplotlib via the AxesImages plotting methods.
""" """
flat_index, gridprod = 0, 1 tmp = np.array(self.bits)
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) return np.reshape(tmp, self.shape)

96
tests/plane_test.py Normal file
View File

@ -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