Generalized rulesets. Removed neighborhoods.
parent
1ab48b3c97
commit
37c0f9281a
|
@ -3,25 +3,6 @@
|
||||||
@author: jrpotter
|
@author: jrpotter
|
||||||
@date: June 01, 2015
|
@date: June 01, 2015
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def game_of_life(coordinate, grid, neighbors):
|
|
||||||
"""
|
|
||||||
Rules of the Game of Life.
|
|
||||||
|
|
||||||
Note we ignore the second component of the neighbors tuples since
|
|
||||||
life depends on all neighbors
|
|
||||||
"""
|
|
||||||
total = sum(map(lambda x: x[1], neighbors))
|
|
||||||
if grid[coordinate]:
|
|
||||||
if total < 2 or total > 3:
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
elif total == 3:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
||||||
import os, sys
|
import os, sys
|
||||||
|
@ -29,10 +10,32 @@ if __name__ == '__main__':
|
||||||
|
|
||||||
import cam
|
import cam
|
||||||
import ruleset as rs
|
import ruleset as rs
|
||||||
import neighborhood as nh
|
|
||||||
|
|
||||||
c = cam.CAM(1, (100, 100))
|
|
||||||
|
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.randomize()
|
c.randomize()
|
||||||
r = rs.Ruleset(rs.Rule.SATISFY)
|
|
||||||
n = nh.Neighborhood.moore(c.master, True)
|
r = rs.Ruleset(rs.Ruleset.Method.SATISFY)
|
||||||
c.start_plot(100, r, n, game_of_life)
|
offsets = rs.Configuration.moore(c.master)
|
||||||
|
r.addConfiguration(c.master, game_of_life, offsets)
|
||||||
|
|
||||||
|
c.start_plot(100, r, lambda *args: True)
|
||||||
|
|
97
src/cam.py
97
src/cam.py
|
@ -1,4 +1,9 @@
|
||||||
"""
|
"""
|
||||||
|
Top level module representing a Cellular Automata Machine.
|
||||||
|
|
||||||
|
The CAM consists of any number of cell planes that allow for increasingly complex cellular automata.
|
||||||
|
This is the top-level module that should be used by anyone wanting to work with fifth, and provides
|
||||||
|
all methods needed (i.e. supported) to interact/configure the cellular automata as desired.
|
||||||
|
|
||||||
@author: jrpotter
|
@author: jrpotter
|
||||||
@date: June 01, 2015
|
@date: June 01, 2015
|
||||||
|
@ -6,9 +11,7 @@
|
||||||
import time
|
import time
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
import camtools
|
|
||||||
import ruleset as rs
|
import ruleset as rs
|
||||||
import neighborhood as nh
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
@ -19,25 +22,68 @@ class CAM:
|
||||||
"""
|
"""
|
||||||
Represents a Cellular Automata Machine (CAM).
|
Represents a Cellular Automata Machine (CAM).
|
||||||
|
|
||||||
The CAM consists of any number of cell planes that allow for increasingly complex cellular automata.
|
A CAM consists of a series of "cell planes" which represent a separate numpy grid instance. There
|
||||||
This is the top-level module that should be used by anyone wanting to work with fifth, and provides
|
should always be at least one cell plane, dubbed the "master", since all other planes cannot be handled
|
||||||
all methods needed (i.e. supported) to interact/configure the cellular automata as desired.
|
directly, but instead mirror the master plane, and reflect these changes after a given number of
|
||||||
|
"ticks."
|
||||||
|
|
||||||
|
A tick represents an interval of time after which all states should be updated, and, therefore, all
|
||||||
|
cell planes should be updated. Certain planes may or may not change every tick, but instead on every
|
||||||
|
nth tick, allowing for more sophisticated views such as ECHOing and TRACE-ing.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, cps=1, dimen=(100, 100)):
|
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.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.planes = np.zeros((max(cps, 1),) + dimen, dtype=np.int32)
|
plane_count = max(cps, 1)
|
||||||
|
grid_dimen = (states,) * dimen
|
||||||
|
|
||||||
|
self.planes = np.zeros((plane_count,) + grid_dimen, dtype='int32')
|
||||||
self.master = self.planes[0]
|
self.master = self.planes[0]
|
||||||
|
|
||||||
|
|
||||||
def start_plot(self, clock, ruleset, neighborhood, *args):
|
def randomize(self, propagate=True):
|
||||||
"""
|
"""
|
||||||
Initiates the main loop.
|
Set the master grid to a random configuration.
|
||||||
|
|
||||||
The following function displays the actual graphical component (through use of matplotlib), and triggers the
|
If propagate is set to True, also immediately change all other cell planes to match.
|
||||||
next tick for every "clock" milliseconds.
|
"""
|
||||||
|
self.master[:] = np.random.random_integers(0, 1, self.master.shape)
|
||||||
|
for plane in self.planes[1:]:
|
||||||
|
plane[:] = self.master
|
||||||
|
|
||||||
|
|
||||||
|
def tick(self, rules, *args):
|
||||||
|
"""
|
||||||
|
Modify all states in a given CAM "simultaneously".
|
||||||
|
|
||||||
|
The tick function should be called whenever we want to change the current status of the grid.
|
||||||
|
Every time the tick is called, the ruleset is applied to each cell and the next set of states
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def start_plot(self, clock, rules, *args):
|
||||||
|
"""
|
||||||
|
Initiates main graphical loop.
|
||||||
|
|
||||||
|
The following function displays the graphical component (through use of matplotlib), and triggers the
|
||||||
|
next tick for every "clock" milliseconds. This should only be called if the automata is 2 or 3 dimensional.
|
||||||
"""
|
"""
|
||||||
fig, ax = plt.subplots()
|
fig, ax = plt.subplots()
|
||||||
|
|
||||||
|
@ -48,7 +94,7 @@ class CAM:
|
||||||
mshown = plt.matshow(self.master, fig.number)
|
mshown = plt.matshow(self.master, fig.number)
|
||||||
|
|
||||||
def animate(frame):
|
def animate(frame):
|
||||||
self.tick(ruleset, neighborhood, *args)
|
self.tick(rules, *args)
|
||||||
mshown.set_array(self.master)
|
mshown.set_array(self.master)
|
||||||
fig.canvas.draw()
|
fig.canvas.draw()
|
||||||
|
|
||||||
|
@ -58,33 +104,16 @@ class CAM:
|
||||||
plt.show()
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
def start_console(self, clock, ruleset, neighborhood, *args):
|
def start_console(self, clock, rules, *args):
|
||||||
"""
|
"""
|
||||||
|
Initates main console loop.
|
||||||
|
|
||||||
|
Works similarly to start_plot but prints out to the console.
|
||||||
|
TODO: Incorporate curses, instead of just printing repeatedly.
|
||||||
"""
|
"""
|
||||||
while True:
|
while True:
|
||||||
print(self.master)
|
print(self.master)
|
||||||
time.sleep(clock / 1000)
|
time.sleep(clock / 1000)
|
||||||
self.tick(ruleset, neighborhood, *args)
|
self.tick(rules, *args)
|
||||||
|
|
||||||
|
|
||||||
def tick(self, ruleset, neighborhood, *args):
|
|
||||||
"""
|
|
||||||
The tick function should be called whenever we want to change the current status of the grid.
|
|
||||||
|
|
||||||
Every time the tick is called, the ruleset is applied to each cell and the next configuration
|
|
||||||
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] = ruleset.call(i, self.master, neighborhood, *args)
|
|
||||||
self.master[:] = tmp
|
|
||||||
|
|
||||||
|
|
||||||
def randomize(self):
|
|
||||||
"""
|
|
||||||
Set the master grid to a random configuration.
|
|
||||||
"""
|
|
||||||
self.master[:] = np.random.random_integers(0, 1, self.master.shape)
|
|
||||||
|
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
"""
|
|
||||||
|
|
||||||
@author: jrpotter
|
|
||||||
@date: June 01, 2015
|
|
||||||
"""
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
def flatten(coordinates, grid):
|
|
||||||
"""
|
|
||||||
Given the coordinates of a matrix, returns the index of the flat matrix.
|
|
||||||
"""
|
|
||||||
index = 0
|
|
||||||
for i in range(len(coordinates)):
|
|
||||||
index += coordinates[i] * np.prod(grid.shape[i+1:], dtype=np.int32)
|
|
||||||
|
|
||||||
return index
|
|
||||||
|
|
||||||
|
|
||||||
def unflatten(index, grid):
|
|
||||||
"""
|
|
||||||
Given an index of a flat matrix, returns the corresponding coordinates.
|
|
||||||
"""
|
|
||||||
coordinates = []
|
|
||||||
for i in range(len(grid.shape)):
|
|
||||||
tmp = np.prod(grid.shape[i+1:], dtype=np.int32)
|
|
||||||
coordinates.append(index // tmp)
|
|
||||||
index -= tmp * coordinates[-1]
|
|
||||||
|
|
||||||
return tuple(coordinates)
|
|
||||||
|
|
||||||
|
|
||||||
def comp_add(coor1, coor2):
|
|
||||||
"""
|
|
||||||
Adds components of coordinates element-wise.
|
|
||||||
"""
|
|
||||||
return tuple(map(sum, zip(coor1, coor2)))
|
|
|
@ -1,123 +0,0 @@
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
@author: jrpotter
|
|
||||||
@date: May 31st, 2015
|
|
||||||
"""
|
|
||||||
import camtools as ct
|
|
||||||
import itertools as it
|
|
||||||
|
|
||||||
|
|
||||||
class Neighborhood:
|
|
||||||
"""
|
|
||||||
The following represents the cells that must be considered when applying a ruleset, as well as the
|
|
||||||
values expected in a ruleset.
|
|
||||||
|
|
||||||
Since neighborhoods can be made arbitrarily complex, we allow extending in all directions. For example,
|
|
||||||
the basic Moore neighborhood comprises of the 8 cells surrounding the center, but what if we wanted
|
|
||||||
these 8 and include the cell north of north? The following enables this:
|
|
||||||
|
|
||||||
m_neighborhood = Neighborhood.moore(2)
|
|
||||||
m_neighborhood.extend({(-2, 0): True})
|
|
||||||
|
|
||||||
This allows indexing at levels beyond 3D, which the Cells enumeration does not allow, though visualization
|
|
||||||
at this point isn't possible.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, wrap_around=True):
|
|
||||||
"""
|
|
||||||
Sets up an empty neighborhood.
|
|
||||||
|
|
||||||
Initially, no cells are included in a neighborhood. All neighborhoods must be extended.
|
|
||||||
Note the offsets have a tuple as a key representing the position being offsetted by, and as a value,
|
|
||||||
the current state the given cell at the offset is checked to be.
|
|
||||||
"""
|
|
||||||
self.offsets = {}
|
|
||||||
self.wrap_around = wrap_around
|
|
||||||
|
|
||||||
|
|
||||||
def neighbors(self, index, grid):
|
|
||||||
"""
|
|
||||||
Returns all cells in the given neighborhood.
|
|
||||||
|
|
||||||
The returned list of indices represent the index in question, the value at the given index, and
|
|
||||||
the expected value as defined in the offsets.
|
|
||||||
"""
|
|
||||||
indices = []
|
|
||||||
for key in sorted(self.offsets.keys()):
|
|
||||||
if self.wrap_around:
|
|
||||||
f_index = (key + index) % len(grid.flat)
|
|
||||||
indices.append((f_index, grid.flat[f_index], self.offsets[key]))
|
|
||||||
else:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return indices
|
|
||||||
|
|
||||||
|
|
||||||
def extend(self, offsets, grid, strict=False):
|
|
||||||
"""
|
|
||||||
Adds new offsets to the instance member offsets.
|
|
||||||
|
|
||||||
We complain if the strict flag is set to True and an offset has already been declared with a different value.
|
|
||||||
Note also that all offsets are indices of the flattened matrix. This allows for quick row indexing as opposed
|
|
||||||
to individual coordinates.
|
|
||||||
"""
|
|
||||||
f_offsets = {ct.flatten(k, grid): v for k, v in offsets.items()}
|
|
||||||
if not strict:
|
|
||||||
self.offsets.update(f_offsets)
|
|
||||||
else:
|
|
||||||
for k in f_offsets.keys():
|
|
||||||
value = self.offsets.get(k, None)
|
|
||||||
if value is None:
|
|
||||||
self.offsets[k] = offsets[k]
|
|
||||||
elif value != offsets[k]:
|
|
||||||
raise KeyError
|
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def moore(cls, grid, wrap_around=True, value=True):
|
|
||||||
"""
|
|
||||||
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
|
|
||||||
|
|
||||||
m_neighborhood = cls(wrap_around)
|
|
||||||
m_neighborhood.extend(offsets, grid)
|
|
||||||
|
|
||||||
return m_neighborhood
|
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def neumann(cls, grid, wrap_around=True, value=True):
|
|
||||||
"""
|
|
||||||
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
|
|
||||||
|
|
||||||
n_neighborhood = cls(wrap_around)
|
|
||||||
n_neighborhood.extend(offsets, grid)
|
|
||||||
|
|
||||||
return n_neighborhood
|
|
||||||
|
|
264
src/ruleset.py
264
src/ruleset.py
|
@ -1,97 +1,257 @@
|
||||||
"""
|
"""
|
||||||
|
The following determines the next state of a given cell in a CAM.
|
||||||
|
|
||||||
|
The ruleset takes in a collection of rules specifying neighborhoods, as well as the configurations of
|
||||||
|
said neighborhood that yield an "on" or "off" state on the cell a ruleset is being applied to.
|
||||||
|
|
||||||
@author: jrpotter
|
@author: jrpotter
|
||||||
@date: May 31st, 2015
|
@date: May 31st, 2015
|
||||||
"""
|
"""
|
||||||
import enum
|
import enum
|
||||||
import camtools
|
import itertools as it
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
class Rule(enum.Enum):
|
|
||||||
MATCH = 0
|
def flatten(coordinates, grid):
|
||||||
TOLERATE = 1
|
"""
|
||||||
SATISFY = 2
|
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:
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 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]
|
||||||
|
|
||||||
|
@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(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=ON):
|
||||||
|
"""
|
||||||
|
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=ON):
|
||||||
|
"""
|
||||||
|
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:
|
class Ruleset:
|
||||||
"""
|
"""
|
||||||
The following determines the next state of a given cell in a CAM.
|
The primary class of this module, which saves configurations of cells that yield the next state.
|
||||||
|
|
||||||
Given a neighborhood and a tolerance level, the ruleset determines whether a given cell should be on or off after a tick.
|
The ruleset will take in configurations defined by the user that specify how a cell's state should change,
|
||||||
For example, if the tolerance level is set to 100% (i.e. neighborhoods must exactly match desired neighborhood to be on),
|
depending on the given neighborhood and current state. For example, if I have a configuration that states
|
||||||
then the ruleset iterates through all neighbors and verifies a match.
|
|
||||||
|
|
||||||
For the sake of clarity, we consider a neighborhood to actually contain the "rules" for matching, and a ruleset to be the
|
[[0, 0, 0]
|
||||||
application of the rules as defined in the neighborhood. We state this since the actual expected values are declared in
|
,[1, 0, 1]
|
||||||
a neighborhood instance's offsets member.
|
,[1, 1, 1]
|
||||||
|
]
|
||||||
|
|
||||||
|
must match exactly for the center cell to be a 1, then each cell is checked for this configuration, and its
|
||||||
|
state is updated afterward (note the above is merely for clarity; a configuration is not defined as such). Note
|
||||||
|
configurations are checked until a match occurs, in a FIFO manner.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
class Method(enum.Enum):
|
||||||
|
"""
|
||||||
|
Specifies how a ruleset should be applied to a given cell.
|
||||||
|
|
||||||
|
* A match declares that a given configuration must match exactly for the cell to be considered on.
|
||||||
|
* A tolerance specifies that a configuration must match within a given percentage to be considered on.
|
||||||
|
* A specification allows the user to define a custom function which must return a boolean, declaring
|
||||||
|
whether a cell should be on or off. This function is given the current cell's state, as well as
|
||||||
|
the state of the cell's neighbors.
|
||||||
|
|
||||||
|
"""
|
||||||
|
MATCH = 0
|
||||||
|
TOLERATE = 1
|
||||||
|
SATISFY = 2
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, method):
|
def __init__(self, method):
|
||||||
"""
|
"""
|
||||||
|
@grid: Every ruleset is bound to a grid, which a ruleset is applied to.
|
||||||
|
@method: One of the values defined in the RulesetMethod enumeration. View class for description.
|
||||||
"""
|
"""
|
||||||
self.method = method
|
self.method = method
|
||||||
|
self.configurations = []
|
||||||
|
|
||||||
|
|
||||||
def matches(self, index, grid, neighborhood):
|
def addConfiguration(self, grid, next_state, offsets):
|
||||||
|
"""
|
||||||
|
Creates a configuration and saves said configuration.
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
|
||||||
|
@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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
for config in self.configurations:
|
||||||
|
|
||||||
|
# Determine the correct function to use
|
||||||
|
vfunc = None
|
||||||
|
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
|
||||||
|
|
||||||
|
# Apply the function if possible
|
||||||
|
if vfunc is not None:
|
||||||
|
passed, state = config.passes(f_index, grid, vfunc, *args)
|
||||||
|
if passed:
|
||||||
|
return state
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
return grid.flat[f_index]
|
||||||
|
|
||||||
|
|
||||||
|
def _matches(self, f_index, f_grid, indices, states):
|
||||||
"""
|
"""
|
||||||
Determines that neighborhood matches expectation exactly.
|
Determines that neighborhood matches expectation exactly.
|
||||||
|
|
||||||
Note this is just like the tolerate method with a tolerance of 1, but
|
Note this functions like the tolerate method with a tolerance of 1.
|
||||||
recoding allows for short circuiting.
|
|
||||||
"""
|
"""
|
||||||
residents = neighborhood.neighbors(index, grid)
|
return not np.count_nonzero(f_grid[indices] ^ states)
|
||||||
for resident in residents:
|
|
||||||
if grid[resident[0]] != resident[1]:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def tolerate(self, index, grid, neighborhood, 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.
|
||||||
|
|
||||||
We see that the percentage of actual matches are greater than or equal to the given tolerance level. If so, we
|
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.
|
consider this cell to be alive. Note tolerance must be a value 0 <= t <= 1.
|
||||||
"""
|
"""
|
||||||
matches = 0
|
non_matches = np.count_nonzero(f_grid[inices] ^ states)
|
||||||
residents = neighborhood.neighbors(index, grid)
|
return (non_matches / len(f_grid)) >= tolerance
|
||||||
for resident in residents:
|
|
||||||
if grid[resident[0]] == resident[1]:
|
|
||||||
matches += 1
|
|
||||||
|
|
||||||
return (matches / len(residents)) >= tolerance
|
|
||||||
|
|
||||||
|
|
||||||
def satisfies(self, index, grid, neighborhood, 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.
|
||||||
|
|
||||||
The passed function is supplied the list of 2-tuple elements, of which the first is a Cell and the second is
|
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.
|
the expected state as declared in the Neighborhood, as well as the grid and cell in question.
|
||||||
"""
|
"""
|
||||||
residents = neighborhood.neighbors(index, grid)
|
return valid_func(f_index, f_grid, indices, states)
|
||||||
coordinate = camtools.unflatten(index, grid)
|
|
||||||
|
|
||||||
return valid_func(coordinate, grid, residents)
|
|
||||||
|
|
||||||
|
|
||||||
def call(self, index, grid, neighborhood, *args):
|
|
||||||
"""
|
|
||||||
Allow for batch processing of rules.
|
|
||||||
|
|
||||||
We choose our processing function based on the specified rule and update every cell in the grid simultaneously
|
|
||||||
via a vectorization.
|
|
||||||
"""
|
|
||||||
if self.method == Rule.MATCH:
|
|
||||||
func = self.matches
|
|
||||||
elif self.method == Rule.TOLERATE:
|
|
||||||
func = self.tolerate
|
|
||||||
elif self.method == Rule.SATISFY:
|
|
||||||
func = self.satisfies
|
|
||||||
|
|
||||||
return int(func(index, grid, neighborhood, *args))
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue