Designing plane
parent
2f844b2b12
commit
863d974281
|
@ -17,4 +17,4 @@ if __name__ == '__main__':
|
||||||
p = u.CAMParser('B3/S23', c)
|
p = u.CAMParser('B3/S23', c)
|
||||||
|
|
||||||
c.randomize()
|
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
|
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:
|
class CAMParser:
|
||||||
"""
|
"""
|
||||||
The following builds rulesets based on the passed string.
|
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 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:
|
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
|
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.
|
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
|
return (non_matches / len(f_grid)) >= tolerance
|
||||||
|
|
||||||
def _satisfies(self, f_index, f_grid, indices, states, valid_func):
|
def _satisfies(self, f_index, f_grid, indices, states, valid_func):
|
||||||
|
|
Loading…
Reference in New Issue