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

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