Established configuration/ruleset
parent
cacbfe29ca
commit
929454ec41
|
@ -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)
|
||||
|
||||
|
||||
|
|
109
src/plane.py
109
src/plane.py
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
Loading…
Reference in New Issue