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