r
/
fifth
1
Fork 0

Created parser for generic life automata

master
Joshua Potter 2015-06-04 14:16:25 -04:00
parent ead7725eb1
commit bde75c012e
9 changed files with 163 additions and 157 deletions

View File

@ -10,26 +10,11 @@ if __name__ == '__main__':
sys.path.append(os.path.abspath('src')) sys.path.append(os.path.abspath('src'))
import cam import cam
import util as u
import ruleset as rs 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) c = cam.CAM(1, 100, 2)
p = u.CAMParser('B368/S23', c)
c.randomize() c.randomize()
c.start_plot(100, p.ruleset)
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)

View File

@ -1,5 +1,5 @@
""" """
B3/S34: Game of Life B3/S23: Game of Life
@author: jrpotter @author: jrpotter
@date: June 01, 2015 @date: June 01, 2015
@ -10,33 +10,11 @@ if __name__ == '__main__':
sys.path.append(os.path.abspath('src')) sys.path.append(os.path.abspath('src'))
import cam import cam
import util as u
import ruleset as rs 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) c = cam.CAM(1, 100, 2)
p = u.CAMParser('B3/S23', c)
c.randomize() c.randomize()
c.start_plot(100, p.ruleset)
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)

View File

@ -10,22 +10,11 @@ if __name__ == '__main__':
sys.path.append(os.path.abspath('src')) sys.path.append(os.path.abspath('src'))
import cam import cam
import util as u
import ruleset as rs 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) c = cam.CAM(1, 100, 2)
p = u.CAMParser('B3/S012345678', c)
c.randomize() c.randomize()
c.start_plot(100, p.ruleset)
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)

View File

@ -10,26 +10,11 @@ if __name__ == '__main__':
sys.path.append(os.path.abspath('src')) sys.path.append(os.path.abspath('src'))
import cam import cam
import util as u
import ruleset as rs 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) c = cam.CAM(1, 100, 2)
p = u.CAMParser('B368/S245', c)
c.randomize() c.randomize()
c.start_plot(100, p.ruleset)
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)

View File

@ -10,26 +10,11 @@ if __name__ == '__main__':
sys.path.append(os.path.abspath('src')) sys.path.append(os.path.abspath('src'))
import cam import cam
import util as u
import ruleset as rs 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) c = cam.CAM(1, 100, 2)
p = u.CAMParser('B1357/S1357', c)
c.randomize() c.randomize()
c.start_plot(100, p.ruleset)
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)

View File

@ -10,22 +10,11 @@ if __name__ == '__main__':
sys.path.append(os.path.abspath('src')) sys.path.append(os.path.abspath('src'))
import cam import cam
import cam_util as u
import ruleset as rs 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) c = cam.CAM(1, 100, 2)
p = u.CAMParser('B2/S', c)
c.randomize() c.randomize()
c.start_plot(100, p.ruleset)
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)

25
src/exceptions.py Normal file
View File

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

View File

@ -12,22 +12,7 @@ import itertools as it
import numpy as np import numpy as np
import util
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 Configuration: class Configuration:
@ -39,20 +24,11 @@ class Configuration:
the next state of a cell depending on a 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={}): def __init__(self, grid, next_state, offsets={}):
""" """
@next_state: Represents the next state of a cell given a configuration passes. @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. @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 Note N must be the same dimension as the grid's dimensions, as it specifies
@ -68,12 +44,11 @@ class Configuration:
f_offsets = [] f_offsets = []
for k, v in offsets.items(): for k, v in offsets.items():
states.append(v) states.append(v)
f_offsets.append(flatten(k, grid)) f_offsets.append(util.flatten(k, grid))
self.states = np.array(states) self.states = np.array(states)
self.offsets = np.array(f_offsets) self.offsets = np.array(f_offsets)
def passes(self, f_index, grid, vfunc, *args): def passes(self, f_index, grid, vfunc, *args):
""" """
Checks if a given configuration passes, and if so, returns the next state. Checks if a given configuration passes, and if so, returns the next state.
@ -96,9 +71,8 @@ class Configuration:
else: else:
return (success, self.next_state) return (success, self.next_state)
@classmethod @classmethod
def moore(cls, grid, value=ON): def moore(cls, grid, value=1):
""" """
Returns a neighborhood corresponding to the Moore neighborhood. Returns a neighborhood corresponding to the Moore neighborhood.
@ -116,9 +90,8 @@ class Configuration:
return offsets return offsets
@classmethod @classmethod
def neumann(cls, grid, value=ON): def neumann(cls, grid, value=1):
""" """
Returns a neighborhood corresponding to the Von Neumann neighborhood. Returns a neighborhood corresponding to the Von Neumann neighborhood.
@ -139,7 +112,6 @@ class Configuration:
return offsets return offsets
class Ruleset: class Ruleset:
""" """
The primary class of this module, which saves configurations of cells that yield the next state. The primary class of this module, which saves configurations of cells that yield the next state.
@ -171,7 +143,7 @@ class Ruleset:
MATCH = 0 MATCH = 0
TOLERATE = 1 TOLERATE = 1
SATISFY = 2 SATISFY = 2
ALWAYS_PASS = 3
def __init__(self, method): def __init__(self, method):
""" """
@ -181,7 +153,6 @@ class Ruleset:
self.method = method self.method = method
self.configurations = [] self.configurations = []
def addConfiguration(self, grid, next_state, offsets): def addConfiguration(self, grid, next_state, offsets):
""" """
Creates a configuration and saves said configuration. Creates a configuration and saves said configuration.
@ -189,7 +160,6 @@ 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, f_index, grid, *args):
""" """
Depending on a given method, applies ruleset to a cell. Depending on a given method, applies ruleset to a cell.
@ -213,6 +183,8 @@ class Ruleset:
vfunc = self._tolerates vfunc = self._tolerates
elif self.method == Ruleset.Method.SATISFY: elif self.method == Ruleset.Method.SATISFY:
vfunc = self._satisfies vfunc = self._satisfies
elif self.method == Ruleset.Method.ALWAYS_PASS:
vfunc = lambda *args: True
# Apply the function if possible # Apply the function if possible
if vfunc is not None: if vfunc is not None:
@ -224,7 +196,6 @@ class Ruleset:
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):
""" """
Determines that neighborhood matches expectation exactly. Determines that neighborhood matches expectation exactly.
@ -233,7 +204,6 @@ class Ruleset:
""" """
return not np.count_nonzero(f_grid[indices] ^ states) return not np.count_nonzero(f_grid[indices] ^ states)
def _tolerates(self, f_index, f_grid, indices, states, tolerance): def _tolerates(self, f_index, f_grid, indices, states, tolerance):
""" """
Determines that neighborhood matches expectation within tolerance. Determines that neighborhood matches expectation within tolerance.
@ -244,7 +214,6 @@ class Ruleset:
non_matches = np.count_nonzero(f_grid[inices] ^ states) non_matches = np.count_nonzero(f_grid[inices] ^ states)
return (non_matches / len(f_grid)) >= tolerance return (non_matches / len(f_grid)) >= tolerance
def _satisfies(self, f_index, f_grid, indices, states, valid_func): def _satisfies(self, f_index, f_grid, indices, states, valid_func):
""" """
Allows custom function to relay next state of given cell. 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) return valid_func(f_index, f_grid, indices, states)

102
src/util.py Normal file
View File

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