r
/
fifth
1
Fork 0

Nested files together slightly

master
Joshua Potter 2015-06-06 17:27:02 -04:00
parent 929454ec41
commit e931af0020
6 changed files with 217 additions and 158 deletions

View File

@ -23,20 +23,117 @@ with the ALWAYS_PASS flag set in the given ruleset the configuration is bundled
""" """
from collections import namedtuple 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: class Configuration:
""" """
Represents an expected neighborhood; to be compared to an actual neighborhood in a CAM. 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 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 # 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 # 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). # given flat_offset. State describes the expected state at the given (flat_offset, bit_offset).
Offset = namedtuple('Offset', ['flat_offset', 'bit_offset', 'state']) 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): def __init__(self, next_state, **kwargs):
""" """
@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.
@ -44,14 +141,15 @@ class Configuration:
@kwargs : If supplied, should be a dictionary containing an 'offsets' key, corresponding @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 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 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.offsets = []
self.next_state = next_state self.next_state = next_state
if 'shape' in kwargs and 'offsets' in kwargs: if 'plane' in kwargs and 'offsets' in kwargs:
self.extend_offsets(kwargs['shape'], kwargs['offsets']) self.extend_offsets(kwargs['plane'], kwargs['offsets'])
def extend_offsets(shape, offsets):
def extend_offsets(self, plane, offsets):
""" """
Allow for customizing of configuration. Allow for customizing of configuration.
@ -60,32 +158,63 @@ class Configuration:
of the value at the first coordinate. of the value at the first coordinate.
""" """
for coor, bit in offsets.items(): for coor, bit in offsets.items():
flat_index, gridprod = 0, 1 flat_index, bit_index = plane.flatten(coor)
for i in reversed(range(len(coor[:-1]))): self.offsets.append(Offset(flat_index, bit_index, bit))
flat_index += coor[i] * gridprod
gridprod *= shape[i]
self.offsets.append(Offset(flat_index, coor[-1], 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, The configuration is considered passing or failing based on the provided vfunc; if successful,
if zipped with indices, correspond to the expected value of the cell at the given index). The function should the bit centered in the neighborhood should be set to the next state as determined by the
merely verify that a list of indices "passes" some expectation. configuration.
For example, if an "exact match" function is passed, it should merely verify that the cells at the passed indices Note the distinction between success and next state
exactly match the exact expectated cells in the list of values. It will return True or False depending. 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 if not vfunc(plane, neighborhood, *args):
indices = (f_index + self.offsets) % grid.size return (False, None)
elif callable(self.next_state):
# Note the distinction between success and next_state here; vfunc (validity function) tells whether the given return (True, self.next_state(plane, neighborhood, *args))
# 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: 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)

View File

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

View File

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

View File

@ -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 @date: June 4th, 2015
""" """
import re import re
import ruleset as rs import ruleset as r
import exceptions as ce 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: class CAMParser:
@ -33,28 +52,30 @@ class CAMParser:
@offsets: Represents the Moore neighborhood corresponding to the given CAM @offsets: Represents the Moore neighborhood corresponding to the given CAM
""" """
self.sfunc = None self.sfunc = None
self.offsets = rs.Configuration.moore(cam.master) self.offsets = c.Configuration.moore(cam.master)
self.ruleset = rs.Ruleset(rs.Ruleset.Method.ALWAYS_PASS) self.ruleset = r.Ruleset(rsRuleset.Method.ALWAYS_PASS)
if re.match(CAMParser.MCELL_FORMAT, notation): if re.match(CAMParser.MCELL_FORMAT, notation):
x, y = notation.split('/') x, y = notation.split('/')
if all(map(self._numasc, [x, y])): if all(map(self._numasc, [x, y])):
self.sfunc = self._mcell(x, y) self.sfunc = self._mcell(x, y)
else: 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): elif re.match(CAMParser.RLE_FORMAT, notation):
B, S = map(lambda x: x[1:], notation.split('/')) B, S = map(lambda x: x[1:], notation.split('/'))
if all(map(self._numasc, [B, S])): if all(map(self._numasc, [B, S])):
self.sfunc = self._mcell(S, B) self.sfunc = self._mcell(S, B)
else: else:
raise ce.InvalidFormat("Non-ascending values in RLE format") raise InvalidFormat("Non-ascending values in RLE format")
else: else:
raise ce.InvalidFormat("No supported format passed to parser.") raise InvalidFormat("No supported format passed to parser.")
# Add configuration to given CAM # 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): def _numasc(self, value):
""" """
@ -65,6 +86,7 @@ class CAMParser:
else: else:
return False return False
def _mcell(self, x, y): def _mcell(self, x, y):
""" """
MCell Notation MCell Notation

View File

@ -46,6 +46,7 @@ class Plane:
for i in range(self.grid.size): for i in range(self.grid.size):
self.grid.flat[i] = bitarray(self.N) self.grid.flat[i] = bitarray(self.N)
def __getitem__(self, index): def __getitem__(self, index):
""" """
Indices supported are the same as those of the numpy array, except for when accessing an individual bit. 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: except AttributeError:
return Plane((self.N,), tmp) return Plane((self.N,), tmp)
def randomize(self): def randomize(self):
""" """
Sets values of grid to random values. 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))]) tmp = np.array([r.randrange(0, max_u) for i in range(len(self.grid))])
self.grid = tmp.reshape(self.grid.shape) 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]

View File

@ -65,16 +65,6 @@ class Ruleset:
""" """
next_grid = [] 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 # 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 # 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 # 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). # sum of the ith index of the summation of every 9 chunks of numbers (this is done a row at a time).
neighboring = [] neighboring = []
for flat_offset, bit_offset in config.offsets: for flat_offset, bit_offset in config.offsets:
neighbor = str(plane.grid.flat[flat_index + flat_offset]) neighbor = plane.grid.flat[flat_index + flat_offset]
neighboring.append(int(neighbor[bit_offset+1:] + neighbor[:bit_offset])) cycled = neighbor[bit_offset:] + neighbor[:bit_offset]
neighboring.append(int(cycled.to01()))
# Chunk into groups of 9 and sum all values # Chunk into groups of 9 and sum all values
# These summations represent the total number of active states in a given neighborhood # These summations represent the total number of active states in a given neighborhood
@ -107,6 +98,16 @@ class Ruleset:
for chunk in chunks: for chunk in chunks:
totals = list(map(sum, zip(totals, chunk))) 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 # Apply change to all successful configurations
for bit_index in to_update: for bit_index in to_update:
neighborhood = Neighborhood(flat_index, bit_index, totals[bit_index]) neighborhood = Neighborhood(flat_index, bit_index, totals[bit_index])
@ -126,30 +127,4 @@ class Ruleset:
for i in range(plane.grid.size): for i in range(plane.grid.size):
plane.grid.flat[i] = next_grid[i] 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)