diff --git a/src/configuration.py b/src/configuration.py index feda657..fab0fab 100644 --- a/src/configuration.py +++ b/src/configuration.py @@ -41,7 +41,7 @@ class Neighborhood: self.neighbors = bitarray() for offset in offsets: f_index = plane.flatten(offset) + self.index - self.neighbors.append(plane[f_index % len(plane.bits)]) + self.neighbors.append(plane.bits[f_index % len(plane.bits)]) self.total = len(self.neighbors) @@ -207,7 +207,7 @@ class Configuration: referring to the offsets checked in a given neighborhood) with an expected state value and a 'plane' key, corresponding to the plane in question. """ - self.offsets = bitarray() + self.offsets = [] self.sequence = bitarray() self.next_state = next_state if 'plane' in kwargs and 'offsets' in kwargs: @@ -222,8 +222,7 @@ class Configuration: of the value at the first coordinate. """ for coor, bit in offsets.items(): - f_index = plane.flatten(coor) - self.offsets.append(f_index) + self.offsets.append(coor) self.sequence.append(bit) def passes(self, plane, neighborhood, vfunc, *args): @@ -253,7 +252,7 @@ class Configuration: Note this behaves like the _tolerates method with a tolerance of 1. """ neighborhood.populate(plane, self.offsets) - return not self.sequence ^ neighborhood.neighbors + return (self.sequence ^ neighborhood.neighbors).count() == 0 def tolerates(self, plane, neighborhood, tolerance): """ @@ -264,7 +263,7 @@ class Configuration: """ neighborhood.populate(plane, self.offsets) non_matches = self.sequence ^ neighborhood.neighbors - return (non_matches / len(self.sequence)) >= tolerance + return ((len(self.sequence) - non_matches.count()) / len(self.sequence)) >= tolerance def satisfies(self, plane, neighborhood, valid_func, *args): """ diff --git a/src/plane.py b/src/plane.py index a3dc804..9998b66 100644 --- a/src/plane.py +++ b/src/plane.py @@ -7,23 +7,6 @@ from bitarray import bitarray from collections import deque -class Coordinate: - """ - Allow normilization between flat indices and offsets. - """ - - def __init__(self, index, plane): - """ - - """ - if type(index) is tuple: - self.index = index - self.flat = plane.flatten(index) - else: - self.flat = index - self.index = plane.unflatten(index) - - class Plane: """ Represents a cell plane, with underlying usage of bitarrays. diff --git a/src/ruleset.py b/src/ruleset.py index d1c8602..2475672 100644 --- a/src/ruleset.py +++ b/src/ruleset.py @@ -9,6 +9,7 @@ said neighborhood that yield an "on" or "off" state on the cell a ruleset is bei import enum import numpy as np import configuration as c + from bitarray import bitarray @@ -67,60 +68,17 @@ class Ruleset: # These are the states of configurations that pass (note if all configurations # fail for any state, the state remains the same) - next_states = plane.bits.copy() + next_plane = plane.bits.copy() # These are the states we attempt to apply a configuration to - # Since totals are computed simultaneously, we save which states do not pass - # for each configuration + # Since totals are computed for a configuration at once, we save + # which states do not pass for each configuration current_states = enumerate(plane.bits) for config in self.configurations: - totals = Neighborhood.get_totals(plane, config.offsets) + totals = c.Neighborhood.get_totals(plane, config.offsets) - - - for index, state in enumerate(plane.bits): - - - - - - - - - # 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): - - next_row = bitarray(plane.N) - to_update = range(0, plane.N) - for config in self.configurations: - - next_update = [] - - # 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 = plane.grid.flat[(flat_index + flat_offset) % plane.N] - cycled = neighbor[bit_offset:] + neighbor[:bit_offset] - neighboring.append(int(cycled.to01())) - - # Chunk into groups of 9 and sum all values - # These summations represent the total number of active states in a given neighborhood - totals = [0] * plane.N - chunks = map(sum, [neighboring[i:i+9] for i in range(0, len(neighboring), 9)]) - for chunk in chunks: - i_chunk = map(int, str(chunk).zfill(plane.N)) - totals = map(sum, zip(totals, i_chunk)) - totals = list(totals) + next_states = [] + for index, state in current_states: # Determine which function should be used to test success if self.method == Ruleset.Method.MATCH: @@ -132,24 +90,21 @@ class Ruleset: elif self.method == Ruleset.Method.ALWAYS_PASS: vfunc = lambda *args: True - # Apply change to all successful configurations + # Passes a mostly uninitialized neighborhood to the given function + # Note if you need actual states of the neighborhood, make sure to + # call the neighborhood's populate function + neighborhood = c.Neighborhood(index) + neighborhood.total = totals[index] - for bit_index in to_update: - neighborhood = c.Neighborhood(flat_index, bit_index, totals[bit_index]) - success, state = config.passes(plane, neighborhood, vfunc, *args) - if success: - next_row[bit_index] = state - else: - next_update.append(bit_index) + # Apply changes to for any successful configurations + success, to_state = config.passes(plane, neighborhood, vfunc, *args) + if success: + next_plane[index] = to_state + else: + next_states.append((index, state)) - # 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] + current_states = next_states + # All configurations tested, transition plane + plane.bits = next_plane diff --git a/tests/configuration_test.py b/tests/configuration_test.py index 9e01786..6c3a34a 100644 --- a/tests/configuration_test.py +++ b/tests/configuration_test.py @@ -3,7 +3,7 @@ sys.path.insert(0, os.path.join('..', 'src')) import plane import numpy as np -from neighborhood import Neighborhood +from configuration import Neighborhood from configuration import Configuration @@ -14,7 +14,7 @@ class TestConfiguration: def setUp(self): self.neighborhood = Neighborhood(0) - self.plane2d = plane.Plane((100, 100)) + self.plane2d = plane.Plane((10, 10)) self.config2d = Configuration(0, plane=self.plane2d, offsets={ (-1, -1): 1, (-1, 0): 1, @@ -51,4 +51,13 @@ class TestConfiguration: self.plane2d[[(-1, -1), (-1, 0), (1, -1), (0, 0)]] = 1 assert self.config2d.matches(self.plane2d, self.neighborhood) + def test_toleranceNeighborhood(self): + """ + + """ + assert not self.config2d.tolerates(self.plane2d, self.neighborhood, 0.5) + self.plane2d[(-1, -1)] = 1 + assert not self.config2d.tolerates(self.plane2d, self.neighborhood, 0.5) + self.plane2d[(-1, 0)] = 1 + assert self.config2d.tolerates(self.plane2d, self.neighborhood, 0.5) diff --git a/tests/neighborhood_test.py b/tests/neighborhood_test.py index 6fad96e..9dfee89 100644 --- a/tests/neighborhood_test.py +++ b/tests/neighborhood_test.py @@ -5,7 +5,7 @@ sys.path.insert(0, os.path.join('..', 'src')) import plane import numpy as np -from neighborhood import Neighborhood +from configuration import Neighborhood class TestNeighborhood: @@ -87,3 +87,16 @@ class TestNeighborhood: assert np.count_nonzero(np.array(t1)) == 200 assert np.count_nonzero(np.array(t2)) == 20000 + def test_neighborhoodPopulate(self): + """ + Neighborhood Populate. + """ + self.plane2d[10] = 1 + self.neigh2d.populate(self.plane2d, self.offsets2d) + assert self.neigh2d.neighbors.count() == 0 + self.plane2d[1] = 1 + self.neigh2d.populate(self.plane2d, self.offsets2d) + assert self.neigh2d.neighbors.count() == 1 + self.plane2d[self.offsets2d] = 1 + self.neigh2d.populate(self.plane2d, self.offsets2d) + assert self.neigh2d.neighbors.count() == 2