From 863d97428113d294922dc6761a50e9b495b9f124 Mon Sep 17 00:00:00 2001 From: Joshua Potter Date: Fri, 5 Jun 2015 08:57:14 -0400 Subject: [PATCH] Designing plane --- examples/life.py | 2 +- src/bitmanip.py | 22 ++++++++ src/configuration.py | 101 ++++++++++++++++++++++++++++++++++ src/neighborhood.py | 11 ++++ src/{util.py => parser.py} | 15 ----- src/plane.py | 110 +++++++++++++++++++++++++++++++++++++ src/ruleset.py | 101 +--------------------------------- 7 files changed, 246 insertions(+), 116 deletions(-) create mode 100644 src/bitmanip.py create mode 100644 src/configuration.py create mode 100644 src/neighborhood.py rename src/{util.py => parser.py} (88%) create mode 100644 src/plane.py diff --git a/examples/life.py b/examples/life.py index 0b480de..fa10655 100644 --- a/examples/life.py +++ b/examples/life.py @@ -17,4 +17,4 @@ if __name__ == '__main__': p = u.CAMParser('B3/S23', c) c.randomize() - c.start_plot(200, p.ruleset) + c.start_plot(400, p.ruleset) diff --git a/src/bitmanip.py b/src/bitmanip.py new file mode 100644 index 0000000..49e8bac --- /dev/null +++ b/src/bitmanip.py @@ -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 diff --git a/src/configuration.py b/src/configuration.py new file mode 100644 index 0000000..bf67655 --- /dev/null +++ b/src/configuration.py @@ -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 diff --git a/src/neighborhood.py b/src/neighborhood.py new file mode 100644 index 0000000..4de1b02 --- /dev/null +++ b/src/neighborhood.py @@ -0,0 +1,11 @@ +""" + +@author: jrpotter +@date: June 5th, 2015 +""" + +class Neighborhood: + """ + + """ + pass diff --git a/src/util.py b/src/parser.py similarity index 88% rename from src/util.py rename to src/parser.py index 0550e42..c5c33f5 100644 --- a/src/util.py +++ b/src/parser.py @@ -10,21 +10,6 @@ import ruleset as rs 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: """ The following builds rulesets based on the passed string. diff --git a/src/plane.py b/src/plane.py new file mode 100644 index 0000000..86ecf68 --- /dev/null +++ b/src/plane.py @@ -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 diff --git a/src/ruleset.py b/src/ruleset.py index b22d910..61f3082 100644 --- a/src/ruleset.py +++ b/src/ruleset.py @@ -12,105 +12,6 @@ import itertools as it 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: """ @@ -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 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 def _satisfies(self, f_index, f_grid, indices, states, valid_func):