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 @date: June 5th, 2015
""" """
from collections import namedtuple
class Configuration: class Configuration:
""" """
@ -13,30 +32,39 @@ class Configuration:
the next state of a cell depending on a 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. @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] This should be an [0|1|Function that returns 0 or 1]
@kwargs : If supplied, should be a dictionary containing an 'offsets' key, corresponding
@offsets: A dictionary of offsets containing N-tuple keys and [-1, 0, 1] values. to a dictionary of offsets (they should be coordinates in N-dimensional space
Note N must be the same dimension as the grid's dimensions, as it specifies referring to the offsets checked in a given neighborhood) with an expected
the offset from any given cell in the grid. state value and a 'shape' key, corresponding to the shape of the grid in question.
""" """
self.offsets = []
self.next_state = next_state 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 def extend_offsets(shape, offsets):
# to N-ary tuples). This also allows for multiple index accessing via the numpy list indexing """
# method Allow for customizing of configuration.
states = []
f_offsets = []
for k, v in offsets.items():
states.append(v)
f_offsets.append(util.flatten(k, grid))
self.states = np.array(states) For easier indexing, we convert the coordinates into a 2-tuple coordinate, where the first
self.offsets = np.array(f_offsets) 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): 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)) return (success, self.next_state(f_index, grid.flat, indices, self.states, *args))
else: else:
return (success, self.next_state) 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 @date: June 05, 2015
""" """
import numpy as np import numpy as np
import bitmanip as bm
from bitarray import bitarray
class Plane: class Plane:
""" """
@ -24,7 +26,7 @@ class Plane:
numpy grid, without the same bloat as a straightforward N-dimensional grid of booleans for instance. 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: 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. 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. Otherwise, we have an N-D plane. Everything operates as expected.
""" """
self.grid = grid
self.shape = shape self.shape = shape
self.N = 0 if not len(shape) else shape[-1]
if len(shape) == 0: if self.grid is None:
self.grid = None if len(shape) == 1:
elif len(shape) == 1: self.grid = self.N * bitarray('0')
self.grid = 0 else:
else: self.grid = np.empty(shape[:-1], dtype=np.object)
self.grid = np.zeros(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. 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 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, 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). 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 # Given coordinates of a grid. This may or may not access the last dimension.
# Note this could be a tuple of slices or numbers # If it does not, can simply return the new plane given the subset accessed.
if type(idx) is tuple: # If it does, we access up to the bitarray, then access the desired bit(s)
tmp = self if type(index) is tuple:
for i in idx: if len(index) == len(self.shape):
tmp = tmp[i] b_array = self.grid[index[:-1]]
return tmp return b_array[index[-1]]
# Reached last dimension, return bits instead
elif len(self.shape) == 1:
bits = bm.bits_of(self.grid, self.shape[0])[idx]
if isinstance(idx, slice):
return list(map(int, bits))
else: else:
return int(bits) subgrid = self.grid[index]
return Plane(subgrid.shape + (self.N,), subgrid)
# Simply relay to numpy methods # If we've reached the last dimension, the grid of the plane is just a bitarray
# We check if we encounter a list or number as opposed to a tuple, and allow further indexing if desired. # (we remove an additional dimension and instead store the bitarray for space's sake).
else: # If a list of elements are passed, we must access them individually (bitarray does
tmp = self.grid[idx] # not have built in support for this).
try: elif len(self.shape) == 1:
plane = Plane(tmp.shape + self.shape[-1:]) if type(index) is list:
plane.grid = tmp.flat return list(map(lambda x: self.grid[x], index))
except AttributeError: else:
plane = Plane(self.shape[-1:]) return self.grid[index]
plane.grid = tmp
return plane # 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
def f_bits(self, f_index, str_type=True): # can occur once we reach the last dimension, and try to determine the shape
""" # of a bitarray
Return the binary representation of the given number at the supplied index. tmp = self.grid[index]
try:
If the user wants a string type, we make sure to pad the returned number to reflect return Plane(tmp.shape + (self.N,), tmp)
the actual states at the given index. except AttributeError:
""" return Plane((self.N,), tmp)
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
def randomize(self): def randomize(self):
""" """
@ -117,14 +98,10 @@ class Plane:
""" """
if len(self.shape) > 0: if len(self.shape) > 0:
import random as r import random as r
max_u = bm.max_unsigned(self.shape[-1]) max_u = 2**self.N - 1
if len(self.shape) == 1: if len(self.shape) == 1:
self.grid = r.randrange(0, max_u) self.grid = r.randrange(0, max_u)
else: 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.method = method
self.configurations = [] 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. 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 arg should be a function returning a BOOL, which takes in a current cell's value, and the
value of its neighbors. value of its neighbors.
""" """
next_grid = []
# Determine which function should be used to test success # Determine which function should be used to test success
if self.method == Ruleset.Method.MATCH: if self.method == Ruleset.Method.MATCH:
@ -74,57 +75,56 @@ class Ruleset:
elif self.method == Ruleset.Method.ALWAYS_PASS: elif self.method == Ruleset.Method.ALWAYS_PASS:
vfunc = lambda *args: True vfunc = lambda *args: True
# Find the set of neighborhoods for each given configuration # We apply our method a row at a time, to take advantage of being able to sum the totals
neighborhoods = [self._construct_neighborhoods(plane, config) for c in self.configurations] # of a neighborhood in a batch manner. We try to apply a configuration to every bit of a
for f_idx, value in enumerate(self.plane.flat): # row, mark those that fail, and try the next configuration on the failed bits until
for b_offset in len(self.plane.shape[-1]): # either all bits pass or configurations are exhausted
for c_idx, config in enumerate(self.configurations): for flat_index, value in enumerate(plane.grid.flat):
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
def _construct_neighborhoods(self, plane, config): next_row = bitarray(self.N)
""" to_update = range(0, self.N)
Construct neighborhoods for config in self.configurations:
After profiling with a previous version, I found that going through each index and totaling the number next_update = []
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 # After profiling with a previous version, I found that going through each index and totaling the number
and add the binary representations together. We do this in chunks of 9, depending on the number of offsets, so # of active states was taking much longer than I liked. Instead, we compute as many neighborhoods as possible
no overflowing of a single column can occur. We can then find the total of the ith neighborhood by checking the # simultaneously, avoiding explicit summation via the "sum" function, at least for each state separately.
sum of the ith index of the summation of every 9 chunks of numbers (this is done a row at a time). #
# 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]))
TODO: Config offsets should be flat offset, bit offset # Chunk into groups of 9 and sum all values
""" # These summations represent the total number of active states in a given neighborhood
neighborhoods = [] totals = [0] * self.N
chunks = map(sum, [offset_totals[i:i+9] for i in range(0, len(neighboring), 9)])
for chunk in chunks:
totals = list(map(sum, zip(totals, chunk)))
for f_idx, row in enumerate(plane.grid.flat): # 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)
# Construct the current neighborhoods of each bit beforehand # Apply next configuration to given indices
row_neighborhoods = [Neighborhood(f_idx, i) for i in range(plane.shape[-1])] to_update = next_update
# Note: config's offsets contain the index of the number in the plane's flat iterator # We must update all states after each next state is computed
# and the offset of the bit referring to the actual state in the given neighborhood next_grid.append(next_row)
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]))
# Chunk into groups of 9 and sum all values # Can now apply the updates simultaneously
# These summations represent the total number of states in a given neighborhood for i in range(plane.grid.size):
chunks = map(sum, [offset_totals[i:i+9] for i in range(0, len(offset_totals), 9)]) plane.grid.flat[i] = next_grid[i]
for chunk in chunks:
for i in range(len(row_neighborhoods)):
row_neighborhoods[i].total += int(chunk[i])
# Lastly, keep neighborhoods 1D, to easily relate to the flat plane grid
neighborhoods += row_neighborhoods
return neighborhoods
def _matches(self, f_index, f_grid, indices, states): def _matches(self, f_index, f_grid, indices, states):
""" """