Generalized rulesets. Removed neighborhoods.
parent
1ab48b3c97
commit
37c0f9281a
|
@ -3,25 +3,6 @@
|
|||
@author: jrpotter
|
||||
@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__':
|
||||
|
||||
import os, sys
|
||||
|
@ -29,10 +10,32 @@ if __name__ == '__main__':
|
|||
|
||||
import cam
|
||||
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()
|
||||
r = rs.Ruleset(rs.Rule.SATISFY)
|
||||
n = nh.Neighborhood.moore(c.master, True)
|
||||
c.start_plot(100, r, n, game_of_life)
|
||||
|
||||
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)
|
||||
|
|
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
|
||||
@date: June 01, 2015
|
||||
|
@ -6,9 +11,7 @@
|
|||
import time
|
||||
import copy
|
||||
|
||||
import camtools
|
||||
import ruleset as rs
|
||||
import neighborhood as nh
|
||||
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
|
@ -19,25 +22,68 @@ class CAM:
|
|||
"""
|
||||
Represents a Cellular Automata Machine (CAM).
|
||||
|
||||
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.
|
||||
A CAM consists of a series of "cell planes" which represent a separate numpy grid instance. There
|
||||
should always be at least one cell plane, dubbed the "master", since all other planes cannot be handled
|
||||
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]
|
||||
|
||||
|
||||
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
|
||||
next tick for every "clock" milliseconds.
|
||||
If propagate is set to True, also immediately change all other cell planes to match.
|
||||
"""
|
||||
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()
|
||||
|
||||
|
@ -48,7 +94,7 @@ class CAM:
|
|||
mshown = plt.matshow(self.master, fig.number)
|
||||
|
||||
def animate(frame):
|
||||
self.tick(ruleset, neighborhood, *args)
|
||||
self.tick(rules, *args)
|
||||
mshown.set_array(self.master)
|
||||
fig.canvas.draw()
|
||||
|
||||
|
@ -58,33 +104,16 @@ class CAM:
|
|||
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:
|
||||
print(self.master)
|
||||
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
|
||||
@date: May 31st, 2015
|
||||
"""
|
||||
import enum
|
||||
import camtools
|
||||
import itertools as it
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
class Rule(enum.Enum):
|
||||
MATCH = 0
|
||||
TOLERATE = 1
|
||||
SATISFY = 2
|
||||
|
||||
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:
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
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.
|
||||
For example, if the tolerance level is set to 100% (i.e. neighborhoods must exactly match desired neighborhood to be on),
|
||||
then the ruleset iterates through all neighbors and verifies a match.
|
||||
The ruleset will take in configurations defined by the user that specify how a cell's state should change,
|
||||
depending on the given neighborhood and current state. For example, if I have a configuration that states
|
||||
|
||||
For the sake of clarity, we consider a neighborhood to actually contain the "rules" for matching, and a ruleset to be the
|
||||
application of the rules as defined in the neighborhood. We state this since the actual expected values are declared in
|
||||
a neighborhood instance's offsets member.
|
||||
[[0, 0, 0]
|
||||
,[1, 0, 1]
|
||||
,[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):
|
||||
"""
|
||||
|
||||
@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.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.
|
||||
|
||||
Note this is just like the tolerate method with a tolerance of 1, but
|
||||
recoding allows for short circuiting.
|
||||
Note this functions like the tolerate method with a tolerance of 1.
|
||||
"""
|
||||
residents = neighborhood.neighbors(index, grid)
|
||||
for resident in residents:
|
||||
if grid[resident[0]] != resident[1]:
|
||||
return False
|
||||
|
||||
return True
|
||||
return not np.count_nonzero(f_grid[indices] ^ states)
|
||||
|
||||
|
||||
def tolerate(self, index, grid, neighborhood, tolerance):
|
||||
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.
|
||||
"""
|
||||
matches = 0
|
||||
residents = neighborhood.neighbors(index, grid)
|
||||
for resident in residents:
|
||||
if grid[resident[0]] == resident[1]:
|
||||
matches += 1
|
||||
|
||||
return (matches / len(residents)) >= tolerance
|
||||
non_matches = np.count_nonzero(f_grid[inices] ^ states)
|
||||
return (non_matches / len(f_grid)) >= 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.
|
||||
|
||||
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.
|
||||
"""
|
||||
residents = neighborhood.neighbors(index, grid)
|
||||
coordinate = camtools.unflatten(index, grid)
|
||||
|
||||
return valid_func(coordinate, grid, residents)
|
||||
return valid_func(f_index, f_grid, indices, states)
|
||||
|
||||
|
||||
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