Revising changes throughout codebase
parent
dd6d4936fb
commit
5202d62498
|
@ -1,5 +1,4 @@
|
||||||
"""
|
"""
|
||||||
A series of functions related to bit manipulation of numbers.
|
|
||||||
|
|
||||||
@author: jrpotter
|
@author: jrpotter
|
||||||
@date: June 5th, 2015
|
@date: June 5th, 2015
|
||||||
|
@ -11,7 +10,7 @@ def max_unsigned(bit_count):
|
||||||
return 2**bit_count - 1
|
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)
|
return "{}{}".format("0" * (size - len(base)), base)
|
||||||
else:
|
else:
|
||||||
return base
|
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
|
@date: June 01, 2015
|
||||||
"""
|
"""
|
||||||
import time
|
import time
|
||||||
import copy
|
|
||||||
|
|
||||||
import ruleset as rs
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
import matplotlib.animation as ani
|
import matplotlib.animation as ani
|
||||||
|
|
||||||
|
@ -34,33 +29,18 @@ class CAM:
|
||||||
|
|
||||||
def __init__(self, cps=1, states=100, dimen=2):
|
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
|
@cps: Cell planes. By default this is 1, but can be any positive number. Any non-positive number
|
||||||
is assumed to be 1.
|
is assumed to be 1.
|
||||||
|
|
||||||
@states: The number of cells that should be included in any dimension. The number of total states
|
@states: The number of cells that should be included in any dimension. The number of total states
|
||||||
will be cps * states^dimen
|
will be cps * states^dimen
|
||||||
|
|
||||||
@dimen: The dimensions of the cellular automata. For example, for an N-tuple array, the dimension is N.
|
@dimen: The dimensions of the cellular automata. For example, for an N-tuple array, the dimension is N.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
plane_count = max(cps, 1)
|
plane_count = max(cps, 1)
|
||||||
grid_dimen = (states,) * dimen
|
grid_dimen = (states,) * dimen
|
||||||
|
|
||||||
self.planes = np.zeros((plane_count,) + grid_dimen, dtype='int32')
|
self.planes = [Plane(grid_dimen) for i in range(cps)]
|
||||||
self.master = self.planes[0]
|
self.ticks = [(0, 1)]
|
||||||
|
self.total = 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
|
|
||||||
|
|
||||||
|
|
||||||
def tick(self, rules, *args):
|
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
|
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).
|
may also change secondary cell planes (the master is always updated on each tick).
|
||||||
"""
|
"""
|
||||||
tmp = np.copy(self.master)
|
self.total += 1
|
||||||
for i in range(len(self.master.flat)):
|
for i, j in self.ticks:
|
||||||
tmp.flat[i] = rules.applyTo(i, self.master, *args)
|
if self.total % j == 0:
|
||||||
|
rules.applyTo(self.planes[i], *args)
|
||||||
self.master[:] = tmp
|
|
||||||
|
|
||||||
|
|
||||||
def start_plot(self, clock, rules, *args):
|
def start_plot(self, clock, rules, *args):
|
||||||
"""
|
"""
|
||||||
|
@ -91,18 +69,17 @@ class CAM:
|
||||||
ax.get_xaxis().set_visible(False)
|
ax.get_xaxis().set_visible(False)
|
||||||
ax.get_yaxis().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):
|
def animate(frame):
|
||||||
self.tick(rules, *args)
|
self.tick(rules, *args)
|
||||||
mshown.set_array(self.master)
|
mshown.set_array(self.planes[0].bits())
|
||||||
return [mshown]
|
return [mshown]
|
||||||
|
|
||||||
ani.FuncAnimation(fig, animate, interval=clock)
|
ani.FuncAnimation(fig, animate, interval=clock)
|
||||||
plt.axis('off')
|
plt.axis('off')
|
||||||
plt.show()
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
def start_console(self, clock, rules, *args):
|
def start_console(self, clock, rules, *args):
|
||||||
"""
|
"""
|
||||||
Initates main console loop.
|
Initates main console loop.
|
||||||
|
@ -111,8 +88,7 @@ class CAM:
|
||||||
TODO: Incorporate curses, instead of just printing repeatedly.
|
TODO: Incorporate curses, instead of just printing repeatedly.
|
||||||
"""
|
"""
|
||||||
while True:
|
while True:
|
||||||
print(self.master)
|
print(self.planes[0].bits())
|
||||||
time.sleep(clock / 1000)
|
time.sleep(clock / 1000)
|
||||||
self.tick(rules, *args)
|
self.tick(rules, *args)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -59,43 +59,3 @@ class Configuration:
|
||||||
return (success, self.next_state(f_index, grid.flat, indices, self.states, *args))
|
return (success, self.next_state(f_index, grid.flat, indices, self.states, *args))
|
||||||
else:
|
else:
|
||||||
return (success, self.next_state)
|
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.
|
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.
|
Otherwise, we have an N-D plane. Everything operates as expected.
|
||||||
"""
|
"""
|
||||||
self.shape = tuple(shape)
|
self.shape = shape
|
||||||
|
|
||||||
if len(shape) == 0:
|
if len(shape) == 0:
|
||||||
self.grid = None
|
self.grid = None
|
||||||
elif len(shape) == 1:
|
elif len(shape) == 1:
|
||||||
self.grid = 0
|
self.grid = 0
|
||||||
else:
|
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):
|
def __getitem__(self, idx):
|
||||||
"""
|
"""
|
||||||
|
@ -67,12 +67,9 @@ class Plane:
|
||||||
return int(bits)
|
return int(bits)
|
||||||
|
|
||||||
# Simply relay to numpy methods
|
# Simply relay to numpy methods
|
||||||
# We check if we reach an actual number as opposed to a tuple
|
# We check if we encounter a list or number as opposed to a tuple, and allow further indexing if desired.
|
||||||
# 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
|
|
||||||
else:
|
else:
|
||||||
tmp = np.reshape(self.grid, self.shape[:-1])[idx]
|
tmp = self.grid[idx]
|
||||||
try:
|
try:
|
||||||
plane = Plane(tmp.shape + self.shape[-1:])
|
plane = Plane(tmp.shape + self.shape[-1:])
|
||||||
plane.grid = tmp.flat
|
plane.grid = tmp.flat
|
||||||
|
@ -82,11 +79,26 @@ class Plane:
|
||||||
|
|
||||||
return 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):
|
def _flatten(coordinates):
|
||||||
"""
|
"""
|
||||||
Given the coordinates of a matrix, returns the index of the flat matrix.
|
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.
|
This is merely a convenience function to convert between N-dimensional space to 1D.
|
||||||
|
TODO: Delete this method?
|
||||||
"""
|
"""
|
||||||
index = 0
|
index = 0
|
||||||
gridprod = 1
|
gridprod = 1
|
||||||
|
@ -98,9 +110,18 @@ class Plane:
|
||||||
|
|
||||||
def randomize(self):
|
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):
|
def bitmatrix(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -61,22 +61,53 @@ class Ruleset:
|
||||||
config = Configuration(grid, next_state, offsets)
|
config = Configuration(grid, next_state, offsets)
|
||||||
self.configurations.append(config)
|
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
|
Note we first compute all neighborhoods in a batch manner and then test that a configuration
|
||||||
a single number (not a tuple!).
|
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
|
@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,
|
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
|
arg should be a function returning a BOOL, which takes in a current cell's value, and the
|
||||||
value of its neighbors.
|
value of its neighbors.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
master = plane.grid.flat
|
||||||
|
|
||||||
for config in self.configurations:
|
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:
|
if self.method == Ruleset.Method.MATCH:
|
||||||
vfunc = self._matches
|
vfunc = self._matches
|
||||||
elif self.method == Ruleset.Method.TOLERATE:
|
elif self.method == Ruleset.Method.TOLERATE:
|
||||||
|
@ -91,6 +122,7 @@ class Ruleset:
|
||||||
if passed:
|
if passed:
|
||||||
return state
|
return state
|
||||||
|
|
||||||
|
# If no configuration passes, we leave the state unchanged
|
||||||
return grid.flat[f_index]
|
return grid.flat[f_index]
|
||||||
|
|
||||||
def _matches(self, f_index, f_grid, indices, states):
|
def _matches(self, f_index, f_grid, indices, states):
|
||||||
|
|
Loading…
Reference in New Issue