From 929454ec41d195ed04a20cacebece2a5b00e2370 Mon Sep 17 00:00:00 2001 From: Joshua Potter Date: Sat, 6 Jun 2015 09:26:08 -0400 Subject: [PATCH] Established configuration/ruleset --- src/configuration.py | 66 +++++++++++++++++------- src/plane.py | 117 +++++++++++++++++-------------------------- src/ruleset.py | 88 ++++++++++++++++---------------- 3 files changed, 139 insertions(+), 132 deletions(-) diff --git a/src/configuration.py b/src/configuration.py index 98dda5b..54aa7fe 100644 --- a/src/configuration.py +++ b/src/configuration.py @@ -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) + + diff --git a/src/plane.py b/src/plane.py index 7339e2f..75b93d2 100644 --- a/src/plane.py +++ b/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 - else: - self.grid = np.zeros(shape[:-1], dtype=np.object) + if self.grid is None: + if len(shape) == 1: + self.grid = self.N * bitarray('0') + else: + 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 - - # 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)) + # 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: - return int(bits) + subgrid = self.grid[index] + return Plane(subgrid.shape + (self.N,), subgrid) - # 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] - try: - plane = Plane(tmp.shape + self.shape[-1:]) - plane.grid = tmp.flat - except AttributeError: - plane = Plane(self.shape[-1:]) - plane.grid = tmp + # 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: + if type(index) is list: + return list(map(lambda x: self.grid[x], index)) + else: + return self.grid[index] - 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 + # 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: + return Plane(tmp.shape + (self.N,), tmp) + except AttributeError: + 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 diff --git a/src/ruleset.py b/src/ruleset.py index 58c38c0..0357b05 100644 --- a/src/ruleset.py +++ b/src/ruleset.py @@ -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). + # 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])) - TODO: Config offsets should be flat offset, bit offset - """ - neighborhoods = [] + # Chunk into groups of 9 and sum all values + # 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: + 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 - row_neighborhoods = [Neighborhood(f_idx, i) for i in range(plane.shape[-1])] + # Apply next configuration to given indices + to_update = next_update - # 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])) + # We must update all states after each next state is computed + next_grid.append(next_row) - # 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)]) - 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 + # 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): """