diff --git a/src/configuration.py b/src/configuration.py index 54aa7fe..444492b 100644 --- a/src/configuration.py +++ b/src/configuration.py @@ -23,20 +23,117 @@ with the ALWAYS_PASS flag set in the given ruleset the configuration is bundled """ from collections import namedtuple + +class Neighborhood: + """ + Specifies the cells that should be considered when referencing a particular cell. + + The neighborhood is a wrapper class that stores information regarding a particular cell. + Offsets must be added separate from instantiation, since it isn't always necessary to + perform this computation in the first place (for example, if an ALWAYS_PASS flag is passed + as opposed to a MATCH flag). + + It may be helpful to consider a configuration as a template of a neighborhood, and a neighborhood + as an instantiation of a configuration (one with concrete values as opposed to templated ones). + """ + + def __init__(self, flat_index, bit_index, total): + """ + Initializes the center cell. + + Offsetted cells belonging in the given neighborhood must be added separately. + """ + self.total = total + self.bit_index = bit_index + self.flat_index = flat_index + + self.states = np.array([]) + self.bit_indices = np.array([]) + self.flat_indices = np.array([]) + + + def process_offsets(self, plane, offsets): + """ + Given the plane and offsets, determines the cells in the given neighborhood. + + This is rather expensive to call on every cell in a grid, so should be used with caution. + Namely, this is useful when we need to determine matches within a threshold, since total cells + of a neighborhood are precomputed in the ruleset. + + For example, if we need an exact match of a configuration, we have to first process all the + offsets of a neighborhood to determine that it indeed matches the configuration (if this was + not called, self.offsets would remain empty). + """ + flat_indices, bit_indices, _ = zip(*offsets) + + states = [] + for i in range(len(flat_indices)): + bit_index = bit_indices[i] + flat_index = flat_indices[i] + states.append(plane.grid.flat[flat_index][bit_index]) + + self.states = np.array(states) + self.bit_indices = np.array(bit_indices) + self.flat_indices = np.array(flat_indices) + + 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. + A configuration allows 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. + returns the state referenced by the first configuration that passes. """ # An offset contains the flat_offset, which refers to the bitarray of the plane.grid.flat that # a given offset is pointing to. The bit_offset refers to the index of the bitarray at the # given flat_offset. State describes the expected state at the given (flat_offset, bit_offset). + Offset = namedtuple('Offset', ['flat_offset', 'bit_offset', 'state']) + + @staticmethod + def moore(plane, 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 + + + @staticmethod + def neumann(plane, 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 + + def __init__(self, next_state, **kwargs): """ @next_state: Represents the next state of a cell given a configuration passes. @@ -44,14 +141,15 @@ class Configuration: @kwargs : If supplied, should be a dictionary containing an 'offsets' key, corresponding to a dictionary of offsets (they should be coordinates in N-dimensional space referring to the offsets checked in a given neighborhood) with an expected - state value and a 'shape' key, corresponding to the shape of the grid in question. + state value and a 'plane' key, corresponding to the plane in question. """ self.offsets = [] self.next_state = next_state - if 'shape' in kwargs and 'offsets' in kwargs: - self.extend_offsets(kwargs['shape'], kwargs['offsets']) + if 'plane' in kwargs and 'offsets' in kwargs: + self.extend_offsets(kwargs['plane'], kwargs['offsets']) - def extend_offsets(shape, offsets): + + def extend_offsets(self, plane, offsets): """ Allow for customizing of configuration. @@ -60,32 +158,63 @@ class Configuration: of the value at the first coordinate. """ for coor, bit in offsets.items(): - flat_index, gridprod = 0, 1 - for i in reversed(range(len(coor[:-1]))): - flat_index += coor[i] * gridprod - gridprod *= shape[i] - self.offsets.append(Offset(flat_index, coor[-1], bit)) + flat_index, bit_index = plane.flatten(coor) + self.offsets.append(Offset(flat_index, bit_index, bit)) - def passes(self, f_index, grid, vfunc, *args): + + def passes(self, plane, neighborhood, vfunc, *args): """ - Checks if a given configuration passes, and if so, returns the next state. + Determines whether a passed neighborhood satisfies the given configuration. - @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. + The configuration is considered passing or failing based on the provided vfunc; if successful, + the bit centered in the neighborhood should be set to the next state as determined by the + configuration. - 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. + Note the distinction between success and next state + vfunc denotes that the configuration passes; that is, a configuration determines the next + state of a cell but should only be heeded if the configuration passes. The next state, which + is either a 0, 1, or a function that returns a 0 or 1 is the actual new value of the cell. """ - # 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)) + if not vfunc(plane, neighborhood, *args): + return (False, None) + elif callable(self.next_state): + return (True, self.next_state(plane, neighborhood, *args)) else: - return (success, self.next_state) + return (True, self.next_state) + def matches(self, plane, neighborhood): + """ + Determines that neighborhood matches expectation exactly. + + Note this behaves like the _tolerates method with a tolerance of 1. + """ + neighborhood.process_offsets(plane, self.offsets) + bits = np.array([offset[2] for offset in self.offsets]) + + return not np.count_nonzero(bits ^ neighborhood.states) + + + def tolerates(self, plane, neighborhood, tolerance): + """ + Determines that neighborhood matches expectation within tolerance. + + 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. + """ + neighborhood.process_offsets(plane, self.offsets) + bits = np.array([offset[2] for offset in self.offsets]) + non_matches = np.count_nonzero(bits ^ neighborhood.states) + + return (non_matches / len(bits)) >= tolerance + + + def satisfies(self, plane, neighborhood, valid_func, *args): + """ + Allows custom function to relay next state of given cell. + + The passed function is passed the given plane and a neighborhood corresponding to the cell + being processed at the moment. + """ + return valid_func(plane, neighborhood, *args) + diff --git a/src/exceptions.py b/src/exceptions.py deleted file mode 100644 index d2fa50c..0000000 --- a/src/exceptions.py +++ /dev/null @@ -1,25 +0,0 @@ -""" - - -@author: jrpotter -@date: June 4th, 2015 -""" - -class InvalidFormat(Exception): - """ - Called when parsing an invalid format. - - For example, in MCell and RLE, numbers should be in ascending order. - """ - - def __init__(self, value): - """ - - """ - self.value = value - - def __str__(self): - """ - - """ - return repr(self.value) diff --git a/src/neighborhood.py b/src/neighborhood.py deleted file mode 100644 index d8c89e9..0000000 --- a/src/neighborhood.py +++ /dev/null @@ -1,58 +0,0 @@ -""" - -@author: jrpotter -@date: June 5th, 2015 -""" - -class Neighborhood: - """ - - """ - def __init__(self, f_index, b_offset, states, indices): - """ - - """ - 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/parser.py b/src/parser.py index c5c33f5..e3c56d5 100644 --- a/src/parser.py +++ b/src/parser.py @@ -1,13 +1,32 @@ """ -A collection of utilities that can ease construction of CAMs. +Parsers CAM languages for quick construction of CAMs. + +For example, when considering CAMs of life, most follow the same formula; check +the total number of cells in a given neighborhood and if there are a certain +number around an off cell, turn it on, and vice versa. Thus the parser takes +in a generic language regarding this and constructs the necessary functions for +the user. -@author: jrpotter @date: June 4th, 2015 """ import re -import ruleset as rs -import exceptions as ce +import ruleset as r +import configuration as c + + +class InvalidFormat(Exception): + """ + Called when parsing an invalid format. + + For example, in MCell and RLE, numbers should be in ascending order. + """ + + def __init__(self, value): + self.value = value + + def __str__(self): + return repr(self.value) class CAMParser: @@ -33,28 +52,30 @@ class CAMParser: @offsets: Represents the Moore neighborhood corresponding to the given CAM """ self.sfunc = None - self.offsets = rs.Configuration.moore(cam.master) - self.ruleset = rs.Ruleset(rs.Ruleset.Method.ALWAYS_PASS) + self.offsets = c.Configuration.moore(cam.master) + self.ruleset = r.Ruleset(rsRuleset.Method.ALWAYS_PASS) if re.match(CAMParser.MCELL_FORMAT, notation): x, y = notation.split('/') if all(map(self._numasc, [x, y])): self.sfunc = self._mcell(x, y) else: - raise ce.InvalidFormat("Non-ascending values in MCELL format") + raise InvalidFormat("Non-ascending values in MCELL format") elif re.match(CAMParser.RLE_FORMAT, notation): B, S = map(lambda x: x[1:], notation.split('/')) if all(map(self._numasc, [B, S])): self.sfunc = self._mcell(S, B) else: - raise ce.InvalidFormat("Non-ascending values in RLE format") + raise InvalidFormat("Non-ascending values in RLE format") else: - raise ce.InvalidFormat("No supported format passed to parser.") + raise InvalidFormat("No supported format passed to parser.") # Add configuration to given CAM - self.ruleset.addConfiguration(cam.master, self.sfunc, self.offsets) + config = c.Configuration(self.sfunc, plane=cam.master, offsets=self.offsets) + self.ruleset.configurations.append(config) + def _numasc(self, value): """ @@ -65,6 +86,7 @@ class CAMParser: else: return False + def _mcell(self, x, y): """ MCell Notation diff --git a/src/plane.py b/src/plane.py index 75b93d2..d865f7f 100644 --- a/src/plane.py +++ b/src/plane.py @@ -46,6 +46,7 @@ class Plane: for i in range(self.grid.size): self.grid.flat[i] = bitarray(self.N) + def __getitem__(self, index): """ Indices supported are the same as those of the numpy array, except for when accessing an individual bit. @@ -89,6 +90,7 @@ class Plane: except AttributeError: return Plane((self.N,), tmp) + def randomize(self): """ Sets values of grid to random values. @@ -105,3 +107,17 @@ class Plane: tmp = np.array([r.randrange(0, max_u) for i in range(len(self.grid))]) self.grid = tmp.reshape(self.grid.shape) + + def flatten(self, coordinate): + """ + Converts a coordinate (which could be used to access a bit in a plane) and "flattens" it. + + By this we mean we convert the coordinate into an index and bit offset corresponding to + the plane grid converted to 1D (think numpy.ndarray.flat). + """ + flat_index, gridprod = 0, 1 + for i in reversed(range(len(coordinate[:-1]))): + flat_index += coordinate[i] * gridprod + gridprod *= shape[i] + + return flat_index, coordinate[-1] diff --git a/src/ruleset.py b/src/ruleset.py index 0357b05..237159c 100644 --- a/src/ruleset.py +++ b/src/ruleset.py @@ -65,16 +65,6 @@ class Ruleset: """ next_grid = [] - # Determine which function should be used to test success - if self.method == Ruleset.Method.MATCH: - vfunc = self._matches - elif self.method == Ruleset.Method.TOLERATE: - vfunc = self._tolerates - elif self.method == Ruleset.Method.SATISFY: - vfunc = self._satisfies - elif self.method == Ruleset.Method.ALWAYS_PASS: - vfunc = lambda *args: True - # We apply our method a row at a time, to take advantage of being able to sum the totals # of a neighborhood in a batch manner. We try to apply a configuration to every bit of a # row, mark those that fail, and try the next configuration on the failed bits until @@ -97,8 +87,9 @@ class Ruleset: # sum of the ith index of the summation of every 9 chunks of numbers (this is done a row at a time). neighboring = [] for flat_offset, bit_offset in config.offsets: - neighbor = str(plane.grid.flat[flat_index + flat_offset]) - neighboring.append(int(neighbor[bit_offset+1:] + neighbor[:bit_offset])) + neighbor = plane.grid.flat[flat_index + flat_offset] + cycled = neighbor[bit_offset:] + neighbor[:bit_offset] + neighboring.append(int(cycled.to01())) # Chunk into groups of 9 and sum all values # These summations represent the total number of active states in a given neighborhood @@ -107,6 +98,16 @@ class Ruleset: for chunk in chunks: totals = list(map(sum, zip(totals, chunk))) + # Determine which function should be used to test success + if self.method == Ruleset.Method.MATCH: + vfunc = config.matches + elif self.method == Ruleset.Method.TOLERATE: + vfunc = config.tolerates + elif self.method == Ruleset.Method.SATISFY: + vfunc = config.satisfies + elif self.method == Ruleset.Method.ALWAYS_PASS: + vfunc = lambda *args: True + # Apply change to all successful configurations for bit_index in to_update: neighborhood = Neighborhood(flat_index, bit_index, totals[bit_index]) @@ -126,30 +127,4 @@ class Ruleset: for i in range(plane.grid.size): plane.grid.flat[i] = next_grid[i] - def _matches(self, f_index, f_grid, indices, states): - """ - Determines that neighborhood matches expectation exactly. - - Note this functions like the tolerate method with a tolerance of 1. - """ - return not np.count_nonzero(f_grid[indices] ^ states) - - def _tolerates(self, f_index, f_grid, indices, states, tolerance): - """ - Determines that neighborhood matches expectation within tolerance. - - 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[indices] ^ states) - return (non_matches / len(f_grid)) >= tolerance - - def _satisfies(self, f_index, f_grid, indices, states, valid_func): - """ - Allows custom function to relay next state of given cell. - - The passed function is supplied the list of 2-tuple elements, of which the first is a Cell and the second is - the expected state as declared in the Neighborhood, as well as the grid and cell in question. - """ - return valid_func(f_index, f_grid, indices, states)