Designing plane
parent
2f844b2b12
commit
863d974281
|
@ -17,4 +17,4 @@ if __name__ == '__main__':
|
|||
p = u.CAMParser('B3/S23', c)
|
||||
|
||||
c.randomize()
|
||||
c.start_plot(200, p.ruleset)
|
||||
c.start_plot(400, p.ruleset)
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
"""
|
||||
A series of functions related to bit manipulation of numbers.
|
||||
|
||||
@author: jrpotter
|
||||
@date: June 5th, 2015
|
||||
"""
|
||||
def max_unsigned(bit_count):
|
||||
"""
|
||||
|
||||
"""
|
||||
return 2**bit_count - 1
|
||||
|
||||
|
||||
def bits_of(value, size):
|
||||
"""
|
||||
|
||||
"""
|
||||
base = bin(value)[2:]
|
||||
if size > len(base):
|
||||
return "{}{}".format("0" * (size - len(base)), base)
|
||||
else:
|
||||
return base
|
|
@ -0,0 +1,101 @@
|
|||
"""
|
||||
|
||||
@author: jrpotter
|
||||
@date: June 5th, 2015
|
||||
"""
|
||||
|
||||
class Configuration:
|
||||
"""
|
||||
Represents an expected neighborhood; to be compared to an actual neighborhood in a CAM.
|
||||
|
||||
A configuration allows exact specification of a neighborhood, not the actual state of a neighborhood.
|
||||
It is merely used for reference by a ruleset, which takes in a series of configurations and
|
||||
the next state of a cell depending on a configuration.
|
||||
"""
|
||||
|
||||
def __init__(self, grid, next_state, offsets={}):
|
||||
"""
|
||||
|
||||
@next_state: Represents the next state of a cell given a configuration passes.
|
||||
This should be an [0|1|Function that returns 0 or 1]
|
||||
|
||||
@offsets: A dictionary of offsets containing N-tuple keys and [-1, 0, 1] values.
|
||||
Note N must be the same dimension as the grid's dimensions, as it specifies
|
||||
the offset from any given cell in the grid.
|
||||
|
||||
"""
|
||||
self.next_state = next_state
|
||||
|
||||
# The grid we work with is flattened, so that we can simply access single indices (as opposed
|
||||
# to N-ary tuples). This also allows for multiple index accessing via the numpy list indexing
|
||||
# method
|
||||
states = []
|
||||
f_offsets = []
|
||||
for k, v in offsets.items():
|
||||
states.append(v)
|
||||
f_offsets.append(util.flatten(k, grid))
|
||||
|
||||
self.states = np.array(states)
|
||||
self.offsets = np.array(f_offsets)
|
||||
|
||||
def passes(self, f_index, grid, vfunc, *args):
|
||||
"""
|
||||
Checks if a given configuration passes, and if so, returns the next state.
|
||||
|
||||
@vfunc is an arbitrary function that takes in a flattened grid, a list of indices, and a list of values (which,
|
||||
if zipped with indices, correspond to the expected value of the cell at the given index). The function should
|
||||
merely verify that a list of indices "passes" some expectation.
|
||||
|
||||
For example, if an "exact match" function is passed, it should merely verify that the cells at the passed indices
|
||||
exactly match the exact expectated cells in the list of values. It will return True or False depending.
|
||||
"""
|
||||
# We ensure all indices are within the given grid
|
||||
indices = (f_index + self.offsets) % grid.size
|
||||
|
||||
# Note the distinction between success and next_state here; vfunc (validity function) tells whether the given
|
||||
# configuration passes. If it does, no other configurations need to be checked and the next state is returned.
|
||||
success = vfunc(f_index, grid.flat, indices, self.states, *args)
|
||||
if callable(self.next_state):
|
||||
return (success, self.next_state(f_index, grid.flat, indices, self.states, *args))
|
||||
else:
|
||||
return (success, self.next_state)
|
||||
|
||||
@classmethod
|
||||
def moore(cls, grid, value=1):
|
||||
"""
|
||||
Returns a neighborhood corresponding to the Moore neighborhood.
|
||||
|
||||
The Moore neighborhood consists of all adjacent cells. In 2D, these correspond to the 8 touching cells
|
||||
N, NE, E, SE, S, SW, S, and NW. In 3D, this corresponds to all cells in the "backward" and "forward"
|
||||
layer that adjoin the nine cells in the "center" layer. This concept can be extended to N dimensions.
|
||||
|
||||
Note the center cell is excluded, so the total number of offsets are 3^N - 1.
|
||||
"""
|
||||
offsets = {}
|
||||
variants = ([-1, 0, 1],) * len(grid.shape)
|
||||
for current in it.product(*variants):
|
||||
if any(current):
|
||||
offsets[current] = value
|
||||
|
||||
return offsets
|
||||
|
||||
@classmethod
|
||||
def neumann(cls, grid, value=1):
|
||||
"""
|
||||
Returns a neighborhood corresponding to the Von Neumann neighborhood.
|
||||
|
||||
The Von Neumann neighborhood consists of adjacent cells that directly share a face with the current cell.
|
||||
In 2D, these correspond to the 4 touching cells N, S, E, W. In 3D, we include the "backward" and "forward"
|
||||
cell. This concept can be extended to N dimensions.
|
||||
|
||||
Note the center cell is excluded, so the total number of offsets are 2N.
|
||||
"""
|
||||
offsets = {}
|
||||
variant = [0] * len(grid.shape)
|
||||
for i in range(len(variant)):
|
||||
for j in [-1, 1]:
|
||||
variant[i] = j
|
||||
offsets[tuple(variant)] = value
|
||||
variant[i] = 0
|
||||
|
||||
return offsets
|
|
@ -0,0 +1,11 @@
|
|||
"""
|
||||
|
||||
@author: jrpotter
|
||||
@date: June 5th, 2015
|
||||
"""
|
||||
|
||||
class Neighborhood:
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
|
@ -10,21 +10,6 @@ import ruleset as rs
|
|||
import exceptions as ce
|
||||
|
||||
|
||||
def flatten(coordinates, grid):
|
||||
"""
|
||||
Given the coordinates of a matrix, returns the index of the flat matrix.
|
||||
|
||||
This is merely a convenience function to convert between N-dimensional space to 1D.
|
||||
"""
|
||||
index = 0
|
||||
gridprod = 1
|
||||
for i in reversed(range(len(coordinates))):
|
||||
index += coordinates[i] * gridprod
|
||||
gridprod *= grid.shape[i]
|
||||
|
||||
return index
|
||||
|
||||
|
||||
class CAMParser:
|
||||
"""
|
||||
The following builds rulesets based on the passed string.
|
|
@ -0,0 +1,110 @@
|
|||
"""
|
||||
Wrapper of a numpy array of bits.
|
||||
|
||||
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 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).
|
||||
|
||||
@author: jrpotter
|
||||
@date: June 05, 2015
|
||||
"""
|
||||
import numpy as np
|
||||
import bitmanip as bm
|
||||
|
||||
class Plane:
|
||||
"""
|
||||
Represents a bit plane, with underlying usage of numpy arrays.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
def __init__(self, size, N):
|
||||
"""
|
||||
Construction of a plane. There are three cases:
|
||||
|
||||
If N == 0: We have an undefined plane. Nothing is in it.
|
||||
If N == 1: We have a 1D plane. This is represented by a single number.
|
||||
Otherwise: We have an N-D plane. Everything operates as expected.
|
||||
|
||||
If N happens to be negative, an exception is thrown.
|
||||
"""
|
||||
if N < 0:
|
||||
raise ValueError('Negative dimension nonsensical')
|
||||
elif N == 0:
|
||||
self.shape = ()
|
||||
self.grid = np.array([], dtype=np.object)
|
||||
elif N == 1:
|
||||
self.shape = (size,)
|
||||
self.grid = np.array([0], dtype=np.object)
|
||||
else:
|
||||
self.shape = (size,) * N
|
||||
self.grid = np.zeros((size**(N-1),), dtype=np.object)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
"""
|
||||
Indices supported are the same as those of the numpy array, except for when accessing an individual bit.
|
||||
|
||||
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).
|
||||
"""
|
||||
|
||||
# Passed in coordinates, access incrementally
|
||||
# Note this could be a tuple of slices or numbers
|
||||
if type(idx) in [tuple]:
|
||||
tmp = self
|
||||
for i in idx:
|
||||
tmp = tmp[i]
|
||||
return tmp
|
||||
|
||||
# Reached last dimension, return bits instead
|
||||
elif len(self.shape) == 1:
|
||||
bits = bm.bits_of(self.grid[0], self.shape[0])[idx]
|
||||
if isinstance(idx, slice):
|
||||
return list(map(int, bits))
|
||||
else:
|
||||
return int(bits)
|
||||
|
||||
# Simply relay to numpy methods
|
||||
# Note this doesn't necessarily return a grid in the same notion but
|
||||
# does still allow further indexing if desired. In addition, we can
|
||||
# be confident idx is either a list or a number so the final dimension
|
||||
# cannot be accessed from here
|
||||
# TODO: Reconsider this...
|
||||
else:
|
||||
full = np.reshape(self.grid, self.shape[:-1])[idx]
|
||||
tmp = cls(1, len(self.shape) - 1)
|
||||
tmp.grid = full.flat
|
||||
return tmp
|
||||
|
||||
def _flatten(coordinates):
|
||||
"""
|
||||
Given the coordinates of a matrix, returns the index of the flat matrix.
|
||||
|
||||
This is merely a convenience function to convert between N-dimensional space to 1D.
|
||||
"""
|
||||
index = 0
|
||||
gridprod = 1
|
||||
for i in reversed(range(len(coordinates))):
|
||||
index += coordinates[i] * gridprod
|
||||
gridprod *= self.dimen[i]
|
||||
|
||||
return index
|
||||
|
||||
def randomize(self):
|
||||
"""
|
||||
|
||||
"""
|
||||
self.grid = np.random.random_integers(0, bm.max_unsigned(dimen), self.grid.shape)
|
||||
|
||||
def bitmatrix(self):
|
||||
"""
|
||||
|
||||
"""
|
||||
pass
|
101
src/ruleset.py
101
src/ruleset.py
|
@ -12,105 +12,6 @@ import itertools as it
|
|||
|
||||
import numpy as np
|
||||
|
||||
import util
|
||||
|
||||
|
||||
class Configuration:
|
||||
"""
|
||||
Represents an expected neighborhood; to be compared to an actual neighborhood in a CAM.
|
||||
|
||||
A configuration allows exact specification of a neighborhood, not the actual state of a neighborhood.
|
||||
It is merely used for reference by a ruleset, which takes in a series of configurations and
|
||||
the next state of a cell depending on a configuration.
|
||||
"""
|
||||
|
||||
def __init__(self, grid, next_state, offsets={}):
|
||||
"""
|
||||
|
||||
@next_state: Represents the next state of a cell given a configuration passes.
|
||||
This should be an [0|1|Function that returns 0 or 1]
|
||||
|
||||
@offsets: A dictionary of offsets containing N-tuple keys and [-1, 0, 1] values.
|
||||
Note N must be the same dimension as the grid's dimensions, as it specifies
|
||||
the offset from any given cell in the grid.
|
||||
|
||||
"""
|
||||
self.next_state = next_state
|
||||
|
||||
# The grid we work with is flattened, so that we can simply access single indices (as opposed
|
||||
# to N-ary tuples). This also allows for multiple index accessing via the numpy list indexing
|
||||
# method
|
||||
states = []
|
||||
f_offsets = []
|
||||
for k, v in offsets.items():
|
||||
states.append(v)
|
||||
f_offsets.append(util.flatten(k, grid))
|
||||
|
||||
self.states = np.array(states)
|
||||
self.offsets = np.array(f_offsets)
|
||||
|
||||
def passes(self, f_index, grid, vfunc, *args):
|
||||
"""
|
||||
Checks if a given configuration passes, and if so, returns the next state.
|
||||
|
||||
@vfunc is an arbitrary function that takes in a flattened grid, a list of indices, and a list of values (which,
|
||||
if zipped with indices, correspond to the expected value of the cell at the given index). The function should
|
||||
merely verify that a list of indices "passes" some expectation.
|
||||
|
||||
For example, if an "exact match" function is passed, it should merely verify that the cells at the passed indices
|
||||
exactly match the exact expectated cells in the list of values. It will return True or False depending.
|
||||
"""
|
||||
# We ensure all indices are within the given grid
|
||||
indices = (f_index + self.offsets) % grid.size
|
||||
|
||||
# Note the distinction between success and next_state here; vfunc (validity function) tells whether the given
|
||||
# configuration passes. If it does, no other configurations need to be checked and the next state is returned.
|
||||
success = vfunc(f_index, grid.flat, indices, self.states, *args)
|
||||
if callable(self.next_state):
|
||||
return (success, self.next_state(f_index, grid.flat, indices, self.states, *args))
|
||||
else:
|
||||
return (success, self.next_state)
|
||||
|
||||
@classmethod
|
||||
def moore(cls, grid, value=1):
|
||||
"""
|
||||
Returns a neighborhood corresponding to the Moore neighborhood.
|
||||
|
||||
The Moore neighborhood consists of all adjacent cells. In 2D, these correspond to the 8 touching cells
|
||||
N, NE, E, SE, S, SW, S, and NW. In 3D, this corresponds to all cells in the "backward" and "forward"
|
||||
layer that adjoin the nine cells in the "center" layer. This concept can be extended to N dimensions.
|
||||
|
||||
Note the center cell is excluded, so the total number of offsets are 3^N - 1.
|
||||
"""
|
||||
offsets = {}
|
||||
variants = ([-1, 0, 1],) * len(grid.shape)
|
||||
for current in it.product(*variants):
|
||||
if any(current):
|
||||
offsets[current] = value
|
||||
|
||||
return offsets
|
||||
|
||||
@classmethod
|
||||
def neumann(cls, grid, value=1):
|
||||
"""
|
||||
Returns a neighborhood corresponding to the Von Neumann neighborhood.
|
||||
|
||||
The Von Neumann neighborhood consists of adjacent cells that directly share a face with the current cell.
|
||||
In 2D, these correspond to the 4 touching cells N, S, E, W. In 3D, we include the "backward" and "forward"
|
||||
cell. This concept can be extended to N dimensions.
|
||||
|
||||
Note the center cell is excluded, so the total number of offsets are 2N.
|
||||
"""
|
||||
offsets = {}
|
||||
variant = [0] * len(grid.shape)
|
||||
for i in range(len(variant)):
|
||||
for j in [-1, 1]:
|
||||
variant[i] = j
|
||||
offsets[tuple(variant)] = value
|
||||
variant[i] = 0
|
||||
|
||||
return offsets
|
||||
|
||||
|
||||
class Ruleset:
|
||||
"""
|
||||
|
@ -207,7 +108,7 @@ class Ruleset:
|
|||
We see that the percentage of actual matches are greater than or equal to the given tolerance level. If so, we
|
||||
consider this cell to be alive. Note tolerance must be a value 0 <= t <= 1.
|
||||
"""
|
||||
non_matches = np.count_nonzero(f_grid[inices] ^ states)
|
||||
non_matches = np.count_nonzero(f_grid[indices] ^ states)
|
||||
return (non_matches / len(f_grid)) >= tolerance
|
||||
|
||||
def _satisfies(self, f_index, f_grid, indices, states, valid_func):
|
||||
|
|
Loading…
Reference in New Issue