r
/
fifth
1
Fork 0

Established configuration/ruleset

master
Joshua Potter 2015-06-06 09:26:08 -04:00
parent cacbfe29ca
commit 929454ec41
3 changed files with 139 additions and 132 deletions

View File

@ -1,8 +1,27 @@
"""
A configuration defines an expectation of a cell's neighborhood, and the cell's new state if is passes
this expectation.
Multiple configurations are tested one after another in a ruleset, on every cell individually,
to determine what the next state of a given cell should be. If no configuration passes, the
cell remains the same. Otherwise it is either turned on or off. To see the usefulness of this,
consider the following:
[[0, 1, 1]
,[1, 1, 0] --> 1
,[0, 1, 1]
]
Here we're saying a cell's neighborhood must match exactly the above for the cell to remain a
one. But how do we allow two possibilities to yield a 1? We add an additional configuration!
Often times, a single configuration is perfectly fine, and the exact bits are irrelevant. This
is the case for all life-life automata for example. In this case, we create a configuration
with the ALWAYS_PASS flag set in the given ruleset the configuration is bundled in.
@author: jrpotter
@date: June 5th, 2015
"""
from collections import namedtuple
class Configuration:
"""
@ -13,30 +32,39 @@ class Configuration:
the next state of a cell depending on a configuration.
"""
def __init__(self, grid, next_state, offsets={}):
"""
# 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'])
def __init__(self, next_state, **kwargs):
"""
@next_state: Represents the next state of a cell given a configuration passes.
This should be an [0|1|Function that returns 0 or 1]
@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.
@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.
"""
self.offsets = []
self.next_state = next_state
if 'shape' in kwargs and 'offsets' in kwargs:
self.extend_offsets(kwargs['shape'], kwargs['offsets'])
# 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(util.flatten(k, grid))
def extend_offsets(shape, offsets):
"""
Allow for customizing of configuration.
self.states = np.array(states)
self.offsets = np.array(f_offsets)
For easier indexing, we convert the coordinates into a 2-tuple coordinate, where the first
index corresponds to the the index of the flat grid, and the second refers to the bit offset
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))
def passes(self, f_index, grid, vfunc, *args):
"""
@ -59,3 +87,5 @@ class Configuration:
return (success, self.next_state(f_index, grid.flat, indices, self.states, *args))
else:
return (success, self.next_state)

View File

@ -13,7 +13,9 @@ binary expansion will be 100 bits long (and padded with 0's if necessary).
@date: June 05, 2015
"""
import numpy as np
import bitmanip as bm
from bitarray import bitarray
class Plane:
"""
@ -24,7 +26,7 @@ class Plane:
numpy grid, without the same bloat as a straightforward N-dimensional grid of booleans for instance.
"""
def __init__(self, shape):
def __init__(self, shape, grid = None):
"""
Construction of a plane. There are three cases:
@ -32,81 +34,60 @@ class Plane:
If shape is length 1, we have a 1D plane. This is represented by a single number.
Otherwise, we have an N-D plane. Everything operates as expected.
"""
self.grid = grid
self.shape = shape
self.N = 0 if not len(shape) else shape[-1]
if len(shape) == 0:
self.grid = None
elif len(shape) == 1:
self.grid = 0
if self.grid is None:
if len(shape) == 1:
self.grid = self.N * bitarray('0')
else:
self.grid = np.zeros(shape[:-1], dtype=np.object)
self.grid = np.empty(shape[:-1], dtype=np.object)
for i in range(self.grid.size):
self.grid.flat[i] = bitarray(self.N)
def __getitem__(self, idx):
def __getitem__(self, index):
"""
Indices supported are the same as those of the numpy array, except for when accessing an individual bit.
When reaching the "last" dimension of the given array, we access the bit of the number at the second
to last dimension, since we are working in (N-1)-dimensional space. Unless this last dimension is reached,
we always return a plane object (otherwise an actual 0 or 1).
Note this function is much slower than accessing the grid directly. To forsake some convenience for the
considerable speed boost is understandable; access by plane only for this convenience.
"""
# Passed in coordinates, access incrementally
# Note this could be a tuple of slices or numbers
if type(idx) is tuple:
tmp = self
for i in idx:
tmp = tmp[i]
return tmp
# Given coordinates of a grid. This may or may not access the last dimension.
# If it does not, can simply return the new plane given the subset accessed.
# If it does, we access up to the bitarray, then access the desired bit(s)
if type(index) is tuple:
if len(index) == len(self.shape):
b_array = self.grid[index[:-1]]
return b_array[index[-1]]
else:
subgrid = self.grid[index]
return Plane(subgrid.shape + (self.N,), subgrid)
# Reached last dimension, return bits instead
# If we've reached the last dimension, the grid of the plane is just a bitarray
# (we remove an additional dimension and instead store the bitarray for space's sake).
# If a list of elements are passed, we must access them individually (bitarray does
# not have built in support for this).
elif len(self.shape) == 1:
bits = bm.bits_of(self.grid, self.shape[0])[idx]
if isinstance(idx, slice):
return list(map(int, bits))
if type(index) is list:
return list(map(lambda x: self.grid[x], index))
else:
return int(bits)
return self.grid[index]
# Simply relay to numpy methods
# We check if we encounter a list or number as opposed to a tuple, and allow further indexing if desired.
else:
tmp = self.grid[idx]
# Any other means of indexing simply indexes the grid as expected, and wraps
# the result in a plane object for consistent accessing. An attribute error
# can occur once we reach the last dimension, and try to determine the shape
# of a bitarray
tmp = self.grid[index]
try:
plane = Plane(tmp.shape + self.shape[-1:])
plane.grid = tmp.flat
return Plane(tmp.shape + (self.N,), tmp)
except AttributeError:
plane = Plane(self.shape[-1:])
plane.grid = tmp
return plane
def f_bits(self, f_index, str_type=True):
"""
Return the binary representation of the given number at the supplied index.
If the user wants a string type, we make sure to pad the returned number to reflect
the actual states at the given index.
"""
value = bin(self.planes[0].flat[f_index])[2:]
if not str_type:
return int(value)
else:
return "{}{}".format("0" * (self.shape[-1] - len(value)), value)
def _flatten(coordinates):
"""
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.
TODO: Delete this method?
"""
index = 0
gridprod = 1
for i in reversed(range(len(coordinates))):
index += coordinates[i] * gridprod
gridprod *= self.dimen[i]
return index
return Plane((self.N,), tmp)
def randomize(self):
"""
@ -117,14 +98,10 @@ class Plane:
"""
if len(self.shape) > 0:
import random as r
max_u = bm.max_unsigned(self.shape[-1])
max_u = 2**self.N - 1
if len(self.shape) == 1:
self.grid = r.randrange(0, max_u)
else:
self.grid = 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)
def bitmatrix(self):
"""
"""
pass

View File

@ -54,7 +54,7 @@ class Ruleset:
self.method = method
self.configurations = []
def applyTo(self, plane, *args):
def apply_to(self, plane, *args):
"""
Depending on the set method, applies ruleset to each cell in the plane.
@ -63,6 +63,7 @@ class Ruleset:
arg should be a function returning a BOOL, which takes in a current cell's value, and the
value of its neighbors.
"""
next_grid = []
# Determine which function should be used to test success
if self.method == Ruleset.Method.MATCH:
@ -74,57 +75,56 @@ class Ruleset:
elif self.method == Ruleset.Method.ALWAYS_PASS:
vfunc = lambda *args: True
# Find the set of neighborhoods for each given configuration
neighborhoods = [self._construct_neighborhoods(plane, config) for c in self.configurations]
for f_idx, value in enumerate(self.plane.flat):
for b_offset in len(self.plane.shape[-1]):
for c_idx, config in enumerate(self.configurations):
n_idx = f_idx * self.plane.shape[-1] + b_offset
passed, state = config.passes(neighborhoods[c_idx][n_idx], vfunc, *args)
if passed:
plane[f_idx][b_offset] = state
break
# 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):
def _construct_neighborhoods(self, plane, config):
"""
Construct neighborhoods
next_row = bitarray(self.N)
to_update = range(0, self.N)
for config in self.configurations:
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.
next_update = []
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).
TODO: Config offsets should be flat offset, bit offset
"""
neighborhoods = []
for f_idx, row in enumerate(plane.grid.flat):
# Construct the current neighborhoods of each bit beforehand
row_neighborhoods = [Neighborhood(f_idx, i) for i in range(plane.shape[-1])]
# Note: config's offsets contain the index of the number in the plane's flat iterator
# and the offset of the bit referring to the actual state in the given neighborhood
offset_totals = []
for f_offset, b_offset in config.offsets:
row_offset = plane.f_bits(f_idx + f_offset)
offset_totals.append(int(row_offset[b_offset+1:] + row_offset[:b_offset]))
# 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 = str(plane.grid.flat[flat_index + flat_offset])
neighboring.append(int(neighbor[bit_offset+1:] + neighbor[:bit_offset]))
# Chunk into groups of 9 and sum all values
# These summations represent the total number of states in a given neighborhood
chunks = map(sum, [offset_totals[i:i+9] for i in range(0, len(offset_totals), 9)])
# These summations represent the total number of active states in a given neighborhood
totals = [0] * self.N
chunks = map(sum, [offset_totals[i:i+9] for i in range(0, len(neighboring), 9)])
for chunk in chunks:
for i in range(len(row_neighborhoods)):
row_neighborhoods[i].total += int(chunk[i])
totals = list(map(sum, zip(totals, chunk)))
# Lastly, keep neighborhoods 1D, to easily relate to the flat plane grid
neighborhoods += row_neighborhoods
# Apply change to all successful configurations
for bit_index in to_update:
neighborhood = Neighborhood(flat_index, bit_index, totals[bit_index])
success, state = config.passes(neighborhood, vfunc, *args)
if success:
next_row[bit_index] = state
else:
next_update.append(bit_index)
return neighborhoods
# 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]
def _matches(self, f_index, f_grid, indices, states):
"""