Revising changes throughout codebase
parent
dd6d4936fb
commit
5202d62498
|
@ -1,5 +1,4 @@
|
|||
"""
|
||||
A series of functions related to bit manipulation of numbers.
|
||||
|
||||
@author: jrpotter
|
||||
@date: June 5th, 2015
|
||||
|
@ -11,7 +10,7 @@ def max_unsigned(bit_count):
|
|||
return 2**bit_count - 1
|
||||
|
||||
|
||||
def bits_of(value, size):
|
||||
def bits_of(value, size = 0):
|
||||
"""
|
||||
|
||||
"""
|
||||
|
@ -20,3 +19,9 @@ def bits_of(value, size):
|
|||
return "{}{}".format("0" * (size - len(base)), base)
|
||||
else:
|
||||
return base
|
||||
|
||||
def cycle(itr, offset):
|
||||
"""
|
||||
|
||||
"""
|
||||
return itr[:offset] + itr[offset+1:]
|
||||
|
|
44
src/cam.py
44
src/cam.py
|
@ -9,11 +9,6 @@ all methods needed (i.e. supported) to interact/configure the cellular automata
|
|||
@date: June 01, 2015
|
||||
"""
|
||||
import time
|
||||
import copy
|
||||
|
||||
import ruleset as rs
|
||||
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.animation as ani
|
||||
|
||||
|
@ -34,33 +29,18 @@ class CAM:
|
|||
|
||||
def __init__(self, cps=1, states=100, dimen=2):
|
||||
"""
|
||||
|
||||
@cps: Cell planes. By default this is 1, but can be any positive number. Any non-positive number
|
||||
is assumed to be 1.
|
||||
|
||||
@states: The number of cells that should be included in any dimension. The number of total states
|
||||
will be cps * states^dimen
|
||||
|
||||
@dimen: The dimensions of the cellular automata. For example, for an N-tuple array, the dimension is N.
|
||||
|
||||
"""
|
||||
plane_count = max(cps, 1)
|
||||
grid_dimen = (states,) * dimen
|
||||
|
||||
self.planes = np.zeros((plane_count,) + grid_dimen, dtype='int32')
|
||||
self.master = self.planes[0]
|
||||
|
||||
|
||||
def randomize(self, propagate=True):
|
||||
"""
|
||||
Set the master grid to a random configuration.
|
||||
|
||||
If propagate is set to True, also immediately change all other cell planes to match.
|
||||
"""
|
||||
self.master[:] = np.random.random_integers(0, 1, self.master.shape)
|
||||
for plane in self.planes[1:]:
|
||||
plane[:] = self.master
|
||||
|
||||
self.planes = [Plane(grid_dimen) for i in range(cps)]
|
||||
self.ticks = [(0, 1)]
|
||||
self.total = 0
|
||||
|
||||
def tick(self, rules, *args):
|
||||
"""
|
||||
|
@ -71,12 +51,10 @@ class CAM:
|
|||
is placed into the master grid. Depending on the timing specifications set by the user, this
|
||||
may also change secondary cell planes (the master is always updated on each tick).
|
||||
"""
|
||||
tmp = np.copy(self.master)
|
||||
for i in range(len(self.master.flat)):
|
||||
tmp.flat[i] = rules.applyTo(i, self.master, *args)
|
||||
|
||||
self.master[:] = tmp
|
||||
|
||||
self.total += 1
|
||||
for i, j in self.ticks:
|
||||
if self.total % j == 0:
|
||||
rules.applyTo(self.planes[i], *args)
|
||||
|
||||
def start_plot(self, clock, rules, *args):
|
||||
"""
|
||||
|
@ -91,18 +69,17 @@ class CAM:
|
|||
ax.get_xaxis().set_visible(False)
|
||||
ax.get_yaxis().set_visible(False)
|
||||
|
||||
mshown = plt.matshow(self.master, fig.number, cmap='Greys')
|
||||
mshown = plt.matshow(self.planes[0].bits(), fig.number, cmap='Greys')
|
||||
|
||||
def animate(frame):
|
||||
self.tick(rules, *args)
|
||||
mshown.set_array(self.master)
|
||||
mshown.set_array(self.planes[0].bits())
|
||||
return [mshown]
|
||||
|
||||
ani.FuncAnimation(fig, animate, interval=clock)
|
||||
plt.axis('off')
|
||||
plt.show()
|
||||
|
||||
|
||||
def start_console(self, clock, rules, *args):
|
||||
"""
|
||||
Initates main console loop.
|
||||
|
@ -111,8 +88,7 @@ class CAM:
|
|||
TODO: Incorporate curses, instead of just printing repeatedly.
|
||||
"""
|
||||
while True:
|
||||
print(self.master)
|
||||
print(self.planes[0].bits())
|
||||
time.sleep(clock / 1000)
|
||||
self.tick(rules, *args)
|
||||
|
||||
|
||||
|
|
|
@ -59,43 +59,3 @@ class Configuration:
|
|||
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
|
||||
|
|
|
@ -8,4 +8,51 @@ class Neighborhood:
|
|||
"""
|
||||
|
||||
"""
|
||||
pass
|
||||
def __init__(self, index, offsets):
|
||||
"""
|
||||
|
||||
"""
|
||||
self.index = -1
|
||||
self.total = -1
|
||||
self.states = np.array([])
|
||||
self.indices = np.array([])
|
||||
|
||||
@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
|
||||
|
|
37
src/plane.py
37
src/plane.py
|
@ -32,14 +32,14 @@ class Plane:
|
|||
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.shape = tuple(shape)
|
||||
self.shape = shape
|
||||
|
||||
if len(shape) == 0:
|
||||
self.grid = None
|
||||
elif len(shape) == 1:
|
||||
self.grid = 0
|
||||
else:
|
||||
self.grid = np.zeros((np.prod(shape[:-1]),), dtype=np.object)
|
||||
self.grid = np.zeros(shape[:-1], dtype=np.object)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
"""
|
||||
|
@ -67,12 +67,9 @@ class Plane:
|
|||
return int(bits)
|
||||
|
||||
# Simply relay to numpy methods
|
||||
# We check if we reach an actual number as opposed to a tuple
|
||||
# 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
|
||||
# We check if we encounter a list or number as opposed to a tuple, and allow further indexing if desired.
|
||||
else:
|
||||
tmp = np.reshape(self.grid, self.shape[:-1])[idx]
|
||||
tmp = self.grid[idx]
|
||||
try:
|
||||
plane = Plane(tmp.shape + self.shape[-1:])
|
||||
plane.grid = tmp.flat
|
||||
|
@ -82,11 +79,26 @@ class Plane:
|
|||
|
||||
return plane
|
||||
|
||||
def f_bits(self, f_index, str_type=True):
|
||||
"""
|
||||
Return the binary representation of the given number at the supplied index.
|
||||
|
||||
If the user wants a string type, we make sure to pad the returned number to reflect
|
||||
the actual states at the given index.
|
||||
"""
|
||||
value = bin(self.planes[0].flat[f_index])[2:]
|
||||
if not str_type:
|
||||
return int(value)
|
||||
else:
|
||||
return "{}{}".format("0" * (self.shape[-1] - len(value)), value)
|
||||
|
||||
|
||||
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.
|
||||
TODO: Delete this method?
|
||||
"""
|
||||
index = 0
|
||||
gridprod = 1
|
||||
|
@ -98,9 +110,18 @@ class Plane:
|
|||
|
||||
def randomize(self):
|
||||
"""
|
||||
Sets values of grid to random values.
|
||||
|
||||
Since numbers of the grid may be larger than numpy can handle natively (i.e. too big
|
||||
for C long types), we use the python random module instead.
|
||||
"""
|
||||
self.grid = np.random.random_integers(0, bm.max_unsigned(dimen), self.grid.shape)
|
||||
if len(self.shape) > 0:
|
||||
import random as r
|
||||
max_u = bm.max_unsigned(self.shape[-1])
|
||||
if len(self.shape) == 1:
|
||||
self.grid = r.randrange(0, max_u)
|
||||
else:
|
||||
self.grid = np.array([r.randrange(0, max_u) for i in range(len(self.grid))])
|
||||
|
||||
def bitmatrix(self):
|
||||
"""
|
||||
|
|
|
@ -61,22 +61,53 @@ class Ruleset:
|
|||
config = Configuration(grid, next_state, offsets)
|
||||
self.configurations.append(config)
|
||||
|
||||
def applyTo(self, f_index, grid, *args):
|
||||
def applyTo(self, plane, *args):
|
||||
"""
|
||||
Depending on a given method, applies ruleset to a cell.
|
||||
Depending on the set method, applies ruleset to each cell in the plane.
|
||||
|
||||
@cell_index: The index of the cell in question, as offset by self.grid.flat. That means the index should be
|
||||
a single number (not a tuple!).
|
||||
|
||||
@args: If our method is TOLERATE, we pass in a value in set [0, 1]. This specifies the threshold between a
|
||||
passing (i.e. percentage of matches in a configuration is > arg) and failing. If our method is SATISFY,
|
||||
arg should be a function returning a BOOL, which takes in a current cell's value, and the
|
||||
value of its neighbors.
|
||||
Note we first compute all neighborhoods in a batch manner and then test that a configuration
|
||||
passes on the supplied neighborhood.
|
||||
|
||||
@args: If our method is TOLERATE, we pass in a value in set [0, 1]. This specifies the threshold between a
|
||||
passing (i.e. percentage of matches in a configuration is > arg) and failing. If our method is SATISFY,
|
||||
arg should be a function returning a BOOL, which takes in a current cell's value, and the
|
||||
value of its neighbors.
|
||||
"""
|
||||
master = plane.grid.flat
|
||||
|
||||
for config in self.configurations:
|
||||
|
||||
# Determine the correct function to use
|
||||
# Construct neighborhoods
|
||||
#
|
||||
# After profiling with a previous version, I found that going through each index and totaling the number
|
||||
# of active states was taking much longer than I liked. Instead, we compute as many neighborhoods as possible
|
||||
# simultaneously, avoiding explicit summation via the "sum" function, at least for each state separately.
|
||||
#
|
||||
# Because the states are now represented as numbers, we instead convert each number to their binary representation
|
||||
# and add the binary representations together. We do this in chunks of 9, depending on the number of offsets, so
|
||||
# no overflowing of a single column can occur. We can then find the total of the ith neighborhood by checking the
|
||||
# sum of the ith index of the summation of every 9 chunks of numbers (this is done a row at a time).
|
||||
|
||||
# TODO: Config offsets should be flat index, bit offset
|
||||
|
||||
|
||||
|
||||
neighborhoods = []
|
||||
|
||||
values = []
|
||||
for f_index, offset in config.offsets:
|
||||
val = plane.f_bits([f_index])
|
||||
values.append(int(val[offset+1:] + val[:offset]))
|
||||
|
||||
# Chunk into groups of 9 and sum all values
|
||||
chunks = [values[i:i+9] for i in range(0, len(values), 9)]
|
||||
summands = map(sum, chunks)
|
||||
|
||||
# Construct neighborhoods for each value in list
|
||||
|
||||
|
||||
|
||||
|
||||
if self.method == Ruleset.Method.MATCH:
|
||||
vfunc = self._matches
|
||||
elif self.method == Ruleset.Method.TOLERATE:
|
||||
|
@ -91,6 +122,7 @@ class Ruleset:
|
|||
if passed:
|
||||
return state
|
||||
|
||||
# If no configuration passes, we leave the state unchanged
|
||||
return grid.flat[f_index]
|
||||
|
||||
def _matches(self, f_index, f_grid, indices, states):
|
||||
|
|
Loading…
Reference in New Issue