Plane and testing
parent
a1737b3963
commit
cd7afe4bd3
202
src/plane.py
202
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')
|
||||
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.grid = np.empty(shape[:-1], dtype=np.object)
|
||||
for i in range(self.grid.size):
|
||||
self.grid.flat[i] = self.N * bitarray('0')
|
||||
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:
|
||||
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
|
||||
# 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)
|
||||
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:
|
||||
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):
|
||||
"""
|
||||
|
@ -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)])
|
||||
tmp = np.array(self.bits)
|
||||
return np.reshape(tmp, self.shape)
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
Loading…
Reference in New Issue