Nested files together slightly
parent
929454ec41
commit
e931af0020
|
@ -23,20 +23,117 @@ with the ALWAYS_PASS flag set in the given ruleset the configuration is bundled
|
|||
"""
|
||||
from collections import namedtuple
|
||||
|
||||
|
||||
class Neighborhood:
|
||||
"""
|
||||
Specifies the cells that should be considered when referencing a particular cell.
|
||||
|
||||
The neighborhood is a wrapper class that stores information regarding a particular cell.
|
||||
Offsets must be added separate from instantiation, since it isn't always necessary to
|
||||
perform this computation in the first place (for example, if an ALWAYS_PASS flag is passed
|
||||
as opposed to a MATCH flag).
|
||||
|
||||
It may be helpful to consider a configuration as a template of a neighborhood, and a neighborhood
|
||||
as an instantiation of a configuration (one with concrete values as opposed to templated ones).
|
||||
"""
|
||||
|
||||
def __init__(self, flat_index, bit_index, total):
|
||||
"""
|
||||
Initializes the center cell.
|
||||
|
||||
Offsetted cells belonging in the given neighborhood must be added separately.
|
||||
"""
|
||||
self.total = total
|
||||
self.bit_index = bit_index
|
||||
self.flat_index = flat_index
|
||||
|
||||
self.states = np.array([])
|
||||
self.bit_indices = np.array([])
|
||||
self.flat_indices = np.array([])
|
||||
|
||||
|
||||
def process_offsets(self, plane, offsets):
|
||||
"""
|
||||
Given the plane and offsets, determines the cells in the given neighborhood.
|
||||
|
||||
This is rather expensive to call on every cell in a grid, so should be used with caution.
|
||||
Namely, this is useful when we need to determine matches within a threshold, since total cells
|
||||
of a neighborhood are precomputed in the ruleset.
|
||||
|
||||
For example, if we need an exact match of a configuration, we have to first process all the
|
||||
offsets of a neighborhood to determine that it indeed matches the configuration (if this was
|
||||
not called, self.offsets would remain empty).
|
||||
"""
|
||||
flat_indices, bit_indices, _ = zip(*offsets)
|
||||
|
||||
states = []
|
||||
for i in range(len(flat_indices)):
|
||||
bit_index = bit_indices[i]
|
||||
flat_index = flat_indices[i]
|
||||
states.append(plane.grid.flat[flat_index][bit_index])
|
||||
|
||||
self.states = np.array(states)
|
||||
self.bit_indices = np.array(bit_indices)
|
||||
self.flat_indices = np.array(flat_indices)
|
||||
|
||||
|
||||
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.
|
||||
A configuration allows 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.
|
||||
returns the state referenced by the first configuration that passes.
|
||||
"""
|
||||
|
||||
# An offset contains the flat_offset, which refers to the bitarray of the plane.grid.flat that
|
||||
# a given offset is pointing to. The bit_offset refers to the index of the bitarray at the
|
||||
# given flat_offset. State describes the expected state at the given (flat_offset, bit_offset).
|
||||
|
||||
Offset = namedtuple('Offset', ['flat_offset', 'bit_offset', 'state'])
|
||||
|
||||
|
||||
@staticmethod
|
||||
def moore(plane, value=1):
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
@staticmethod
|
||||
def neumann(plane, value=1):
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
def __init__(self, next_state, **kwargs):
|
||||
"""
|
||||
@next_state: Represents the next state of a cell given a configuration passes.
|
||||
|
@ -44,14 +141,15 @@ class Configuration:
|
|||
@kwargs : If supplied, should be a dictionary containing an 'offsets' key, corresponding
|
||||
to a dictionary of offsets (they should be coordinates in N-dimensional space
|
||||
referring to the offsets checked in a given neighborhood) with an expected
|
||||
state value and a 'shape' key, corresponding to the shape of the grid in question.
|
||||
state value and a 'plane' key, corresponding to the plane in question.
|
||||
"""
|
||||
self.offsets = []
|
||||
self.next_state = next_state
|
||||
if 'shape' in kwargs and 'offsets' in kwargs:
|
||||
self.extend_offsets(kwargs['shape'], kwargs['offsets'])
|
||||
if 'plane' in kwargs and 'offsets' in kwargs:
|
||||
self.extend_offsets(kwargs['plane'], kwargs['offsets'])
|
||||
|
||||
def extend_offsets(shape, offsets):
|
||||
|
||||
def extend_offsets(self, plane, offsets):
|
||||
"""
|
||||
Allow for customizing of configuration.
|
||||
|
||||
|
@ -60,32 +158,63 @@ class Configuration:
|
|||
of the value at the first coordinate.
|
||||
"""
|
||||
for coor, bit in offsets.items():
|
||||
flat_index, gridprod = 0, 1
|
||||
for i in reversed(range(len(coor[:-1]))):
|
||||
flat_index += coor[i] * gridprod
|
||||
gridprod *= shape[i]
|
||||
self.offsets.append(Offset(flat_index, coor[-1], bit))
|
||||
flat_index, bit_index = plane.flatten(coor)
|
||||
self.offsets.append(Offset(flat_index, bit_index, bit))
|
||||
|
||||
def passes(self, f_index, grid, vfunc, *args):
|
||||
|
||||
def passes(self, plane, neighborhood, vfunc, *args):
|
||||
"""
|
||||
Checks if a given configuration passes, and if so, returns the next state.
|
||||
Determines whether a passed neighborhood satisfies the given configuration.
|
||||
|
||||
@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.
|
||||
The configuration is considered passing or failing based on the provided vfunc; if successful,
|
||||
the bit centered in the neighborhood should be set to the next state as determined by the
|
||||
configuration.
|
||||
|
||||
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.
|
||||
Note the distinction between success and next state
|
||||
vfunc denotes that the configuration passes; that is, a configuration determines the next
|
||||
state of a cell but should only be heeded if the configuration passes. The next state, which
|
||||
is either a 0, 1, or a function that returns a 0 or 1 is the actual new value of the cell.
|
||||
"""
|
||||
# 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))
|
||||
if not vfunc(plane, neighborhood, *args):
|
||||
return (False, None)
|
||||
elif callable(self.next_state):
|
||||
return (True, self.next_state(plane, neighborhood, *args))
|
||||
else:
|
||||
return (success, self.next_state)
|
||||
return (True, self.next_state)
|
||||
|
||||
|
||||
def matches(self, plane, neighborhood):
|
||||
"""
|
||||
Determines that neighborhood matches expectation exactly.
|
||||
|
||||
Note this behaves like the _tolerates method with a tolerance of 1.
|
||||
"""
|
||||
neighborhood.process_offsets(plane, self.offsets)
|
||||
bits = np.array([offset[2] for offset in self.offsets])
|
||||
|
||||
return not np.count_nonzero(bits ^ neighborhood.states)
|
||||
|
||||
|
||||
def tolerates(self, plane, neighborhood, 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.
|
||||
"""
|
||||
neighborhood.process_offsets(plane, self.offsets)
|
||||
bits = np.array([offset[2] for offset in self.offsets])
|
||||
non_matches = np.count_nonzero(bits ^ neighborhood.states)
|
||||
|
||||
return (non_matches / len(bits)) >= tolerance
|
||||
|
||||
|
||||
def satisfies(self, plane, neighborhood, valid_func, *args):
|
||||
"""
|
||||
Allows custom function to relay next state of given cell.
|
||||
|
||||
The passed function is passed the given plane and a neighborhood corresponding to the cell
|
||||
being processed at the moment.
|
||||
"""
|
||||
return valid_func(plane, neighborhood, *args)
|
||||
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
"""
|
||||
|
||||
|
||||
@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)
|
|
@ -1,58 +0,0 @@
|
|||
"""
|
||||
|
||||
@author: jrpotter
|
||||
@date: June 5th, 2015
|
||||
"""
|
||||
|
||||
class Neighborhood:
|
||||
"""
|
||||
|
||||
"""
|
||||
def __init__(self, f_index, b_offset, states, indices):
|
||||
"""
|
||||
|
||||
"""
|
||||
self.index = -1
|
||||
self.total = -1
|
||||
self.states = np.array([])
|
||||
self.indices = np.array([])
|
||||
|
||||
@classmethod
|
||||
def moore(cls, grid, value=1):
|
||||
"""
|
||||
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=1):
|
||||
"""
|
||||
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
|
|
@ -1,13 +1,32 @@
|
|||
"""
|
||||
A collection of utilities that can ease construction of CAMs.
|
||||
Parsers CAM languages for quick construction of CAMs.
|
||||
|
||||
For example, when considering CAMs of life, most follow the same formula; check
|
||||
the total number of cells in a given neighborhood and if there are a certain
|
||||
number around an off cell, turn it on, and vice versa. Thus the parser takes
|
||||
in a generic language regarding this and constructs the necessary functions for
|
||||
the user.
|
||||
|
||||
@author: jrpotter
|
||||
@date: June 4th, 2015
|
||||
"""
|
||||
import re
|
||||
|
||||
import ruleset as rs
|
||||
import exceptions as ce
|
||||
import ruleset as r
|
||||
import configuration as c
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class CAMParser:
|
||||
|
@ -33,28 +52,30 @@ class CAMParser:
|
|||
@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)
|
||||
self.offsets = c.Configuration.moore(cam.master)
|
||||
self.ruleset = r.Ruleset(rsRuleset.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")
|
||||
raise 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")
|
||||
raise InvalidFormat("Non-ascending values in RLE format")
|
||||
|
||||
else:
|
||||
raise ce.InvalidFormat("No supported format passed to parser.")
|
||||
raise InvalidFormat("No supported format passed to parser.")
|
||||
|
||||
# Add configuration to given CAM
|
||||
self.ruleset.addConfiguration(cam.master, self.sfunc, self.offsets)
|
||||
config = c.Configuration(self.sfunc, plane=cam.master, offsets=self.offsets)
|
||||
self.ruleset.configurations.append(config)
|
||||
|
||||
|
||||
def _numasc(self, value):
|
||||
"""
|
||||
|
@ -65,6 +86,7 @@ class CAMParser:
|
|||
else:
|
||||
return False
|
||||
|
||||
|
||||
def _mcell(self, x, y):
|
||||
"""
|
||||
MCell Notation
|
||||
|
|
16
src/plane.py
16
src/plane.py
|
@ -46,6 +46,7 @@ class Plane:
|
|||
for i in range(self.grid.size):
|
||||
self.grid.flat[i] = bitarray(self.N)
|
||||
|
||||
|
||||
def __getitem__(self, index):
|
||||
"""
|
||||
Indices supported are the same as those of the numpy array, except for when accessing an individual bit.
|
||||
|
@ -89,6 +90,7 @@ class Plane:
|
|||
except AttributeError:
|
||||
return Plane((self.N,), tmp)
|
||||
|
||||
|
||||
def randomize(self):
|
||||
"""
|
||||
Sets values of grid to random values.
|
||||
|
@ -105,3 +107,17 @@ class Plane:
|
|||
tmp = np.array([r.randrange(0, max_u) for i in range(len(self.grid))])
|
||||
self.grid = tmp.reshape(self.grid.shape)
|
||||
|
||||
|
||||
def flatten(self, coordinate):
|
||||
"""
|
||||
Converts a coordinate (which could be used to access a bit in a plane) and "flattens" it.
|
||||
|
||||
By this we mean we convert the coordinate into an index and bit offset corresponding to
|
||||
the plane grid converted to 1D (think numpy.ndarray.flat).
|
||||
"""
|
||||
flat_index, gridprod = 0, 1
|
||||
for i in reversed(range(len(coordinate[:-1]))):
|
||||
flat_index += coordinate[i] * gridprod
|
||||
gridprod *= shape[i]
|
||||
|
||||
return flat_index, coordinate[-1]
|
||||
|
|
|
@ -65,16 +65,6 @@ class Ruleset:
|
|||
"""
|
||||
next_grid = []
|
||||
|
||||
# Determine which function should be used to test success
|
||||
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
|
||||
elif self.method == Ruleset.Method.ALWAYS_PASS:
|
||||
vfunc = lambda *args: True
|
||||
|
||||
# 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
|
||||
|
@ -97,8 +87,9 @@ class Ruleset:
|
|||
# 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 = str(plane.grid.flat[flat_index + flat_offset])
|
||||
neighboring.append(int(neighbor[bit_offset+1:] + neighbor[:bit_offset]))
|
||||
neighbor = plane.grid.flat[flat_index + flat_offset]
|
||||
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
|
||||
|
@ -107,6 +98,16 @@ class Ruleset:
|
|||
for chunk in chunks:
|
||||
totals = list(map(sum, zip(totals, chunk)))
|
||||
|
||||
# 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 = Neighborhood(flat_index, bit_index, totals[bit_index])
|
||||
|
@ -126,30 +127,4 @@ class Ruleset:
|
|||
for i in range(plane.grid.size):
|
||||
plane.grid.flat[i] = next_grid[i]
|
||||
|
||||
def _matches(self, f_index, f_grid, indices, states):
|
||||
"""
|
||||
Determines that neighborhood matches expectation exactly.
|
||||
|
||||
Note this functions like the tolerate method with a tolerance of 1.
|
||||
"""
|
||||
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.
|
||||
|
||||
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.
|
||||
"""
|
||||
non_matches = np.count_nonzero(f_grid[indices] ^ 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.
|
||||
|
||||
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.
|
||||
"""
|
||||
return valid_func(f_index, f_grid, indices, states)
|
||||
|
||||
|
|
Loading…
Reference in New Issue