From 5202d62498949b9b97489d042bd724e3dbfb038b Mon Sep 17 00:00:00 2001 From: Joshua Potter Date: Fri, 5 Jun 2015 20:45:57 -0400 Subject: [PATCH] Revising changes throughout codebase --- src/bitmanip.py | 9 ++++++-- src/cam.py | 44 +++++++++---------------------------- src/configuration.py | 40 ---------------------------------- src/neighborhood.py | 49 ++++++++++++++++++++++++++++++++++++++++- src/plane.py | 37 ++++++++++++++++++++++++------- src/ruleset.py | 52 +++++++++++++++++++++++++++++++++++--------- 6 files changed, 136 insertions(+), 95 deletions(-) diff --git a/src/bitmanip.py b/src/bitmanip.py index 49e8bac..fd154cd 100644 --- a/src/bitmanip.py +++ b/src/bitmanip.py @@ -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:] diff --git a/src/cam.py b/src/cam.py index d737bf5..f818c82 100644 --- a/src/cam.py +++ b/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) - diff --git a/src/configuration.py b/src/configuration.py index bf67655..98dda5b 100644 --- a/src/configuration.py +++ b/src/configuration.py @@ -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 diff --git a/src/neighborhood.py b/src/neighborhood.py index 4de1b02..ccb8f48 100644 --- a/src/neighborhood.py +++ b/src/neighborhood.py @@ -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 diff --git a/src/plane.py b/src/plane.py index 4ece270..7339e2f 100644 --- a/src/plane.py +++ b/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): """ diff --git a/src/ruleset.py b/src/ruleset.py index 61f3082..d57240c 100644 --- a/src/ruleset.py +++ b/src/ruleset.py @@ -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):