diff --git a/examples/highlife.py b/examples/highlife.py index 04c9057..32de68d 100644 --- a/examples/highlife.py +++ b/examples/highlife.py @@ -10,26 +10,11 @@ if __name__ == '__main__': sys.path.append(os.path.abspath('src')) import cam + import util as u import ruleset as rs - - def high_life(f_index, f_grid, indices, states, *args): - total = sum(f_grid[indices]) - if not f_grid[f_index]: - if total == 3 or total == 6 or total == 8: - return rs.Configuration.OFF - else: - if total == 2 or total == 3: - return rs.Configuration.ON - - return rs.Configuration.OFF - - c = cam.CAM(1, 100, 2) + p = u.CAMParser('B368/S23', c) + c.randomize() - - r = rs.Ruleset(rs.Ruleset.Method.SATISFY) - offsets = rs.Configuration.moore(c.master) - r.addConfiguration(c.master, high_life, offsets) - - c.start_plot(100, r, lambda *args: True) + c.start_plot(100, p.ruleset) diff --git a/examples/life.py b/examples/life.py index d522c0c..60daa6c 100644 --- a/examples/life.py +++ b/examples/life.py @@ -1,5 +1,5 @@ """ -B3/S34: Game of Life +B3/S23: Game of Life @author: jrpotter @date: June 01, 2015 @@ -10,33 +10,11 @@ if __name__ == '__main__': sys.path.append(os.path.abspath('src')) import cam + import util as u import ruleset as rs - - def game_of_life(f_index, f_grid, indices, states, *args): - """ - Rules of the Game of Life. - - Note we ignore the second component of the neighbors tuples since - life depends on all neighbors - """ - total = sum(f_grid[indices]) - if f_grid[f_index]: - if total < 2 or total > 3: - return rs.Configuration.OFF - else: - return rs.Configuration.ON - elif total == 3: - return rs.Configuration.ON - else: - return rs.Configuration.OFF - - c = cam.CAM(1, 100, 2) + p = u.CAMParser('B3/S23', c) + c.randomize() - - r = rs.Ruleset(rs.Ruleset.Method.SATISFY) - offsets = rs.Configuration.moore(c.master) - r.addConfiguration(c.master, game_of_life, offsets) - - c.start_plot(100, r, lambda *args: True) + c.start_plot(100, p.ruleset) diff --git a/examples/life_without_death.py b/examples/life_without_death.py index 8af58b3..e494827 100644 --- a/examples/life_without_death.py +++ b/examples/life_without_death.py @@ -10,22 +10,11 @@ if __name__ == '__main__': sys.path.append(os.path.abspath('src')) import cam + import util as u import ruleset as rs - - def lwd(f_index, f_grid, indices, states, *args): - total = sum(f_grid[indices]) - if not f_grid[f_index] and total == 3: - return rs.Configuration.ON - else: - return f_grid[f_index] - - c = cam.CAM(1, 100, 2) + p = u.CAMParser('B3/S012345678', c) + c.randomize() - - r = rs.Ruleset(rs.Ruleset.Method.SATISFY) - offsets = rs.Configuration.moore(c.master) - r.addConfiguration(c.master, lwd, offsets) - - c.start_plot(100, r, lambda *args: True) + c.start_plot(100, p.ruleset) diff --git a/examples/morley.py b/examples/morley.py index 6125046..245a456 100644 --- a/examples/morley.py +++ b/examples/morley.py @@ -10,26 +10,11 @@ if __name__ == '__main__': sys.path.append(os.path.abspath('src')) import cam + import util as u import ruleset as rs - - def morley(f_index, f_grid, indices, states, *args): - total = sum(f_grid[indices]) - if not f_grid[f_index]: - if total == 3 or total == 6 or total == 8: - return rs.Configuration.ON - else: - if total == 2 or total == 4 or total == 5: - return rs.Configuration.ON - - return rs.Configuration.OFF - - c = cam.CAM(1, 100, 2) + p = u.CAMParser('B368/S245', c) + c.randomize() - - r = rs.Ruleset(rs.Ruleset.Method.SATISFY) - offsets = rs.Configuration.moore(c.master) - r.addConfiguration(c.master, morley, offsets) - - c.start_plot(100, r, lambda *args: True) + c.start_plot(100, p.ruleset) diff --git a/examples/replicator.py b/examples/replicator.py index 59131ab..00bb3ef 100644 --- a/examples/replicator.py +++ b/examples/replicator.py @@ -10,26 +10,11 @@ if __name__ == '__main__': sys.path.append(os.path.abspath('src')) import cam + import util as u import ruleset as rs - - def replicator(f_index, f_grid, indices, states, *args): - total = sum(f_grid[indices]) - if not f_grid[f_index]: - if total % 2 == 1: - return rs.Configuration.ON - else: - if total % 2 == 1: - return rs.Configuration.ON - - return rs.Configuration.OFF - - c = cam.CAM(1, 100, 2) + p = u.CAMParser('B1357/S1357', c) + c.randomize() - - r = rs.Ruleset(rs.Ruleset.Method.SATISFY) - offsets = rs.Configuration.moore(c.master) - r.addConfiguration(c.master, replicator, offsets) - - c.start_plot(100, r, lambda *args: True) + c.start_plot(100, p.ruleset) diff --git a/examples/seeds.py b/examples/seeds.py index 6fc3ab0..cdc02f8 100644 --- a/examples/seeds.py +++ b/examples/seeds.py @@ -10,22 +10,11 @@ if __name__ == '__main__': sys.path.append(os.path.abspath('src')) import cam + import cam_util as u import ruleset as rs - - def seeds(f_index, f_grid, indices, states, *args): - total = sum(f_grid[indices]) - if not f_grid[f_index] and total == 2: - return rs.Configuration.ON - else: - return rs.Configuration.OFF - - c = cam.CAM(1, 100, 2) + p = u.CAMParser('B2/S', c) + c.randomize() - - r = rs.Ruleset(rs.Ruleset.Method.SATISFY) - offsets = rs.Configuration.moore(c.master) - r.addConfiguration(c.master, seeds, offsets) - - c.start_plot(100, r, lambda *args: True) + c.start_plot(100, p.ruleset) diff --git a/src/exceptions.py b/src/exceptions.py new file mode 100644 index 0000000..d2fa50c --- /dev/null +++ b/src/exceptions.py @@ -0,0 +1,25 @@ +""" + + +@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/ruleset.py b/src/ruleset.py index 57e6601..05266f1 100644 --- a/src/ruleset.py +++ b/src/ruleset.py @@ -12,22 +12,7 @@ import itertools as it import numpy as np - - -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 - +import util class Configuration: @@ -39,20 +24,11 @@ class Configuration: the next state of a cell depending on a configuration. """ - # Possible states a cell can take - # - # If a configuration passes, the cell's state will be on or off if ON or OFF was passed respectively. - # If IGNORE, then the state remains the same, but no further configurations will be checked by the - # ruleset. - ON = 1 - OFF = 0 - - def __init__(self, grid, next_state, offsets={}): """ @next_state: Represents the next state of a cell given a configuration passes. - This should be an [ON|OFF|Function that returns ON or Off] + 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 @@ -68,12 +44,11 @@ class Configuration: f_offsets = [] for k, v in offsets.items(): states.append(v) - f_offsets.append(flatten(k, grid)) + 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. @@ -96,9 +71,8 @@ class Configuration: else: return (success, self.next_state) - @classmethod - def moore(cls, grid, value=ON): + def moore(cls, grid, value=1): """ Returns a neighborhood corresponding to the Moore neighborhood. @@ -116,9 +90,8 @@ class Configuration: return offsets - @classmethod - def neumann(cls, grid, value=ON): + def neumann(cls, grid, value=1): """ Returns a neighborhood corresponding to the Von Neumann neighborhood. @@ -139,7 +112,6 @@ class Configuration: return offsets - class Ruleset: """ The primary class of this module, which saves configurations of cells that yield the next state. @@ -168,10 +140,10 @@ class Ruleset: the state of the cell's neighbors. """ - MATCH = 0 - TOLERATE = 1 - SATISFY = 2 - + MATCH = 0 + TOLERATE = 1 + SATISFY = 2 + ALWAYS_PASS = 3 def __init__(self, method): """ @@ -181,7 +153,6 @@ class Ruleset: self.method = method self.configurations = [] - def addConfiguration(self, grid, next_state, offsets): """ Creates a configuration and saves said configuration. @@ -189,7 +160,6 @@ class Ruleset: config = Configuration(grid, next_state, offsets) self.configurations.append(config) - def applyTo(self, f_index, grid, *args): """ Depending on a given method, applies ruleset to a cell. @@ -213,6 +183,8 @@ class Ruleset: vfunc = self._tolerates elif self.method == Ruleset.Method.SATISFY: vfunc = self._satisfies + elif self.method == Ruleset.Method.ALWAYS_PASS: + vfunc = lambda *args: True # Apply the function if possible if vfunc is not None: @@ -224,7 +196,6 @@ class Ruleset: return grid.flat[f_index] - def _matches(self, f_index, f_grid, indices, states): """ Determines that neighborhood matches expectation exactly. @@ -233,7 +204,6 @@ class Ruleset: """ 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. @@ -244,7 +214,6 @@ class Ruleset: non_matches = np.count_nonzero(f_grid[inices] ^ 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. @@ -254,4 +223,3 @@ class Ruleset: """ return valid_func(f_index, f_grid, indices, states) - diff --git a/src/util.py b/src/util.py new file mode 100644 index 0000000..0550e42 --- /dev/null +++ b/src/util.py @@ -0,0 +1,102 @@ +""" +A collection of utilities that can ease construction of CAMs. + +@author: jrpotter +@date: June 4th, 2015 +""" +import re + +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. + + Following notation is supported: + * MCell Notation (x/y) + * RLE Format (By/Sx) + + For reference: http://en.wikipedia.org/wiki/Life-like_cellular_automaton + """ + + RLE_FORMAT = r'B\d*/S\d*$' + MCELL_FORMAT = r'\d*/\d*$' + + def __init__(self, notation, cam): + """ + Parses the passed notation and saves values into members. + + @sfunc: Represents the function that returns the next given state. + @ruleset: A created ruleset that matches always + @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) + + 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") + + 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") + + else: + raise ce.InvalidFormat("No supported format passed to parser.") + + # Add configuration to given CAM + self.ruleset.addConfiguration(cam.master, self.sfunc, self.offsets) + + def _numasc(self, value): + """ + Check the given value is a string of ascending numbers. + """ + if all(map(str.isnumeric, value)): + return ''.join(sorted(value)) == value + else: + return False + + def _mcell(self, x, y): + """ + MCell Notation + + A rule is written as a string x/y where each of x and y is a sequence of distinct digits from 0 to 8, in + numerical order. The presence of a digit d in the x string means that a live cell with d live neighbors + survives into the next generation of the pattern, and the presence of d in the y string means that a dead + cell with d live neighbors becomes alive in the next generation. For instance, in this notation, + Conway's Game of Life is denoted 23/3 + """ + x, y = list(map(int, x)), list(map(int, y)) + def next_state(f_index, f_grid, indices, states, *args): + total = sum(f_grid[indices]) + if f_grid[f_index]: + return int(total in x) + else: + return int(total in y) + + return next_state +