r
/
fifth
1
Fork 0
fifth/src/ruleset.py

135 lines
6.3 KiB
Python

"""
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.
@date: May 31st, 2015
"""
import enum
import numpy as np
import configuration as c
from bitarray import bitarray
class Ruleset:
"""
The primary class of this module, which saves configurations of cells that yield the next state.
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
[[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 order of the configurations list.
"""
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 a configuration to pass
* A tolerance specifies that a configuration must match within a given percentage to pass
* A specification allows the user to define a custom function which must return a boolean, declaring
whether a configuration passes. This function is given a neighborhood with all necessary information.
* Always passing allows the first configuration to always yield a success. It is redundant to add
any additional configurations in this case (in fact it is inefficient since neighborhoods are computer
in advance).
"""
MATCH = 0
TOLERATE = 1
SATISFY = 2
ALWAYS_PASS = 3
def __init__(self, method):
"""
A ruleset does not begin with any configurations; only a means of verifying them.
@method: One of the values defined in the Ruleset.Method enumeration. View class for description.
"""
self.method = method
self.configurations = []
def apply_to(self, plane, *args):
"""
Depending on the set method, applies ruleset to each cell in the plane.
@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.
"""
next_grid = []
# 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
# either all bits pass or configurations are exhausted
for flat_index, value in enumerate(plane.grid.flat):
next_row = bitarray(plane.N)
to_update = range(0, plane.N)
for config in self.configurations:
next_update = []
# After profiling with a previous version, I found that going through each index and totaling the number
# of active states was taking much longer than I liked. Instead, we compute as many neighborhoods as possible
# simultaneously, avoiding explicit summation via the "sum" function, at least for each state separately.
#
# Because the states are now represented as numbers, we instead convert each number to their binary representation
# and add the binary representations together. We do this in chunks of 9, depending on the number of offsets, so
# no overflowing of a single column can occur. We can then find the total of the ith neighborhood by checking the
# 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 = plane.grid.flat[(flat_index + flat_offset) % plane.N]
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
totals = [0] * plane.N
chunks = map(sum, [neighboring[i:i+9] for i in range(0, len(neighboring), 9)])
for chunk in chunks:
i_chunk = map(int, str(chunk).zfill(plane.N))
totals = map(sum, zip(totals, i_chunk))
totals = list(totals)
# 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 = c.Neighborhood(flat_index, bit_index, totals[bit_index])
success, state = config.passes(plane, neighborhood, vfunc, *args)
if success:
next_row[bit_index] = state
else:
next_update.append(bit_index)
# Apply next configuration to given indices
to_update = next_update
# We must update all states after each next state is computed
next_grid.append(next_row)
# Can now apply the updates simultaneously
for i in range(plane.grid.size):
plane.grid.flat[i] = next_grid[i]