r
/
fifth
1
Fork 0

Revising changes throughout codebase

master
Joshua Potter 2015-06-05 20:45:57 -04:00
parent dd6d4936fb
commit 5202d62498
6 changed files with 136 additions and 95 deletions

View File

@ -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:]

View File

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

View File

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

View File

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

View File

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

View File

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