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
|
@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)
|
||||||
|
|
||||||
|
|
||||||
|
|
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
|
@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.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.
|
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]]
|
||||||
|
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:
|
elif len(self.shape) == 1:
|
||||||
bits = bm.bits_of(self.grid, self.shape[0])[idx]
|
if type(index) is list:
|
||||||
if isinstance(idx, slice):
|
return list(map(lambda x: self.grid[x], index))
|
||||||
return list(map(int, bits))
|
|
||||||
else:
|
else:
|
||||||
return int(bits)
|
return self.grid[index]
|
||||||
|
|
||||||
# Simply relay to numpy methods
|
# Any other means of indexing simply indexes the grid as expected, and wraps
|
||||||
# We check if we encounter a list or number as opposed to a tuple, and allow further indexing if desired.
|
# the result in a plane object for consistent accessing. An attribute error
|
||||||
else:
|
# can occur once we reach the last dimension, and try to determine the shape
|
||||||
tmp = self.grid[idx]
|
# of a bitarray
|
||||||
|
tmp = self.grid[index]
|
||||||
try:
|
try:
|
||||||
plane = Plane(tmp.shape + self.shape[-1:])
|
return Plane(tmp.shape + (self.N,), tmp)
|
||||||
plane.grid = tmp.flat
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
plane = Plane(self.shape[-1:])
|
return Plane((self.N,), tmp)
|
||||||
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
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
|
@ -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
|
||||||
TODO: Config offsets should be flat offset, bit offset
|
# 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
|
||||||
neighborhoods = []
|
# sum of the ith index of the summation of every 9 chunks of numbers (this is done a row at a time).
|
||||||
|
neighboring = []
|
||||||
for f_idx, row in enumerate(plane.grid.flat):
|
for flat_offset, bit_offset in config.offsets:
|
||||||
|
neighbor = str(plane.grid.flat[flat_index + flat_offset])
|
||||||
# Construct the current neighborhoods of each bit beforehand
|
neighboring.append(int(neighbor[bit_offset+1:] + neighbor[:bit_offset]))
|
||||||
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]))
|
|
||||||
|
|
||||||
# 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 states in a given neighborhood
|
# These summations represent the total number of active states in a given neighborhood
|
||||||
chunks = map(sum, [offset_totals[i:i+9] for i in range(0, len(offset_totals), 9)])
|
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 chunk in chunks:
|
||||||
for i in range(len(row_neighborhoods)):
|
totals = list(map(sum, zip(totals, chunk)))
|
||||||
row_neighborhoods[i].total += int(chunk[i])
|
|
||||||
|
|
||||||
# Lastly, keep neighborhoods 1D, to easily relate to the flat plane grid
|
# Apply change to all successful configurations
|
||||||
neighborhoods += row_neighborhoods
|
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):
|
def _matches(self, f_index, f_grid, indices, states):
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in New Issue