diff --git a/examples/life.py b/examples/life.py index b8aacba..dd9a162 100644 --- a/examples/life.py +++ b/examples/life.py @@ -15,22 +15,5 @@ if __name__ == '__main__': c = cam.CAM(1, 100, 2) p = cam_parser.CAMParser('B3/S23', c) - #c.randomize() - - # Glider Gun 9x36 - from bitarray import bitarray - row = [1<<11 - ,1<<13|1<<11 - ,1<<23|1<<22|1<<15|1<<14|1<<1|1<<0 - ,1<<24|1<<20|1<<15|1<<14|1<<1|1<<0 - ,1<<35|1<<34|1<<25|1<<19|1<<15|1<<14 - ,1<<35|1<<34|1<<25|1<<21|1<<19|1<<18|1<<13|1<<11 - ,1<<25|1<<19|1<<11 - ,1<<24|1<<20 - ,1<<23|1<<22 - ] - - for i in range(9): - c.master.grid[35+i][12:48] = bitarray(bin(row[i])[2:].zfill(36)) - - c.start(cam.CAM.Show.CONSOLE, clock=50, rules=p.ruleset) + c.randomize() + c.start(cam.CAM.Show.WINDOW, clock=50, rules=p.ruleset) diff --git a/src/cam_parser.py b/src/cam_parser.py index 76e70a4..81cc8c0 100644 --- a/src/cam_parser.py +++ b/src/cam_parser.py @@ -1,37 +1,17 @@ -""" -Parsers CAM languages for quick construction of CAMs. - -For example, when considering CAMs of life, most follow the same formula; check -the total number of cells in a given neighborhood and if there are a certain -number around an off cell, turn it on, and vice versa. Thus the parser takes -in a generic language regarding this and constructs the necessary functions for -the user. - -@date: June 4th, 2015 -""" import re - import ruleset as r import configuration as c -class InvalidFormat(Exception): - """ - Called when parsing an invalid format. - - For example, in MCell and RLE, numbers should be in ascending order. - """ - - def __init__(self, value): - self.value = value - - def __str__(self): - return repr(self.value) - - class CAMParser: """ - The following builds rulesets based on the passed string. + Parses CAM languages for quick construction of CAMs. + + For example, when considering CAMs of life, most follow the same formula; check + the total number of cells in a given neighborhood and if there are a certain + number around an off cell, turn it on, and vice versa. Thus the parser takes + in a generic language regarding this and constructs the necessary functions for + the user. Following notation is supported: * MCell Notation (x/y) @@ -60,23 +40,22 @@ class CAMParser: if all(map(self._numasc, [x, y])): self.sfunc = self._mcell(x, y) else: - raise InvalidFormat("Non-ascending values in MCELL format") + raise ValueError("Non-ascending values in MCELL format") elif re.match(CAMParser.RLE_FORMAT, notation): B, S = map(lambda x: x[1:], notation.split('/')) if all(map(self._numasc, [B, S])): self.sfunc = self._mcell(S, B) else: - raise InvalidFormat("Non-ascending values in RLE format") + raise ValueError("Non-ascending values in RLE format") else: - raise InvalidFormat("No supported format passed to parser.") + raise ValueError("No supported format passed to parser.") # Add configuration to given CAM config = c.Configuration(self.sfunc, plane=cam.master, offsets=self.offsets) self.ruleset.configurations.append(config) - def _numasc(self, value): """ Check the given value is a string of ascending numbers. @@ -86,7 +65,6 @@ class CAMParser: else: return False - def _mcell(self, x, y): """ MCell Notation @@ -99,7 +77,7 @@ class CAMParser: """ x, y = list(map(int, x)), list(map(int, y)) def next_state(plane, neighborhood, *args): - if plane.grid.flat[neighborhood.flat_index][neighborhood.bit_index]: + if plane.bits[neighborhood.flat_index]: return int(neighborhood.total in x) else: return int(neighborhood.total in y) diff --git a/src/configuration.py b/src/configuration.py index fab0fab..5631eed 100644 --- a/src/configuration.py +++ b/src/configuration.py @@ -20,15 +20,15 @@ class Neighborhood: perform this computation in the first place (for example, if an ALWAYS_PASS flag is passed as opposed to a MATCH flag). """ - def __init__(self, index): + def __init__(self, flat_index): """ Initializes the center cell. Offsetted cells belonging in the given neighborhood must be added separately. """ self.total = 0 - self.index = index self.neighbors = bitarray() + self.flat_index = flat_index def populate(self, plane, offsets): """ @@ -40,8 +40,8 @@ class Neighborhood: """ self.neighbors = bitarray() for offset in offsets: - f_index = plane.flatten(offset) + self.index - self.neighbors.append(plane.bits[f_index % len(plane.bits)]) + index = plane.flatten(offset) + self.flat_index + self.neighbors.append(plane.bits[index % len(plane.bits)]) self.total = len(self.neighbors) diff --git a/src/display.py b/src/display.py index a044462..e8b49df 100644 --- a/src/display.py +++ b/src/display.py @@ -1,20 +1,7 @@ -""" -Allow for displaying 2D/3D CAMs. - -Two means of viewing the CAM are provided: either through the console via the curses -library or through a GUI display via the matplotlib library. Note the console display -only supports 2D CAMs while the GUI supports 2D/3D automata. - -Both methods allow for the ability to display multiple cell planes at a time, with -additional support for ECHOs and TRACing. - -@date: June 16th, 2015 -""" import sys import time import curses import itertools - import numpy as np import matplotlib.pyplot as plt import matplotlib.animation as ani @@ -22,7 +9,14 @@ import matplotlib.animation as ani class _Display: """ - The base class for visualization of the CAM. + Allow for displaying 2D/3D CAMs. + + Two means of viewing the CAM are provided: either through the console via the curses + library or through a GUI display via the matplotlib library. Note the console display + only supports 2D CAMs while the GUI supports 2D/3D automata. + + Both methods allow for the ability to display multiple cell planes at a time, with + additional support for ECHOs and TRACing. """ def __init__(self, cam, clock, rules, *args): @@ -87,7 +81,7 @@ class ConsoleDisplay(_Display): # Construct the necessary planes self.overlays = [] - for i, pl in enumerate(self.cam.planes): + for i, plane in enumerate(self.cam.planes): pad = curses.newpad(self.width+1, self.height+1) self.overlays.append(pad) if i > 0: @@ -116,7 +110,7 @@ class ConsoleDisplay(_Display): elif ch == curses.KEY_RIGHT: self.x = (self.x - 1) % self.width - def _draw_overlay(self, overlay, pl): + def _draw_overlay(self, overlay, plane): """ Draw the grid onto the overlay. @@ -126,18 +120,22 @@ class ConsoleDisplay(_Display): overlay.clear() line = 0 - grid = np.append(pl.grid[self.y:], pl.grid[:self.y]) - for bits in grid.flat: + for i in range(plane.shape[0]): overlay.move(line, 0) line += 1 - # We go through and group the bits apart so + # Make sure to account for movement + y_offset = ((i + self.y) % plane.shape[0]) * plane.shape[1] + bits = plane.bits[y_offset:y_offset+plane.shape[1]] cycle = bits[self.x:] + bits[:self.x] + + # Draw only active states for k, g in itertools.groupby(cycle): values = list(g) if any(values): overlay.addstr('+' * len(values)) else: + pass y, x = overlay.getyx() overlay.move(y, x + len(values)) @@ -161,10 +159,10 @@ class ConsoleDisplay(_Display): self._shift(self.stdscr.getch()) # Cycle around grid - for i, pl in enumerate(self.cam.planes): - if pl.dirty: - pl.dirty = False - self._draw_overlay(self.overlays[i], pl) + for i, plane in enumerate(self.cam.planes): + if plane.dirty: + plane.dirty = False + self._draw_overlay(self.overlays[i], plane) self.overlays[i].noutrefresh(0, 0, 0, 0, max_y-1, max_x-1) # Prepare for next loop @@ -203,7 +201,7 @@ class WindowDisplay(_Display): # for proper superimposition self.matrices = [] for plane in self.cam.planes: - mshown = plt.matshow(plane.bits(), self.fig.number, cmap='Greys') + mshown = plt.matshow(plane.matrix(), self.fig.number, cmap='Greys') self.matrices.append(mshown) def _valid(self): @@ -223,7 +221,7 @@ class WindowDisplay(_Display): """ self.cam.tick(self.rules, *self.tick_args) if len(self.cam.master.shape) == 2: - self.matrices[0].set_array(self.cam.master.bits()) + self.matrices[0].set_array(self.cam.master.matrix()) return [self.matrices[0]] else: pass diff --git a/src/plane.py b/src/plane.py index 9998b66..4909e6c 100644 --- a/src/plane.py +++ b/src/plane.py @@ -147,9 +147,9 @@ class Plane: method below. """ if self.N > 0: - max_unsigned = reduce(operator.mul, self.shape, 1) - sequence = bin(random.randrange(0, max_unsigned))[2:] - self.bits = bitarray(sequence.zfill(max_unsigned)) + bit_count = reduce(operator.mul, self.shape, 1) + sequence = bin(random.randrange(0, 2**bit_count-1))[2:] + self.bits = bitarray(sequence.zfill(bit_count)) def flatten(self, coordinates): """ diff --git a/src/ruleset.py b/src/ruleset.py index 2475672..fe5c991 100644 --- a/src/ruleset.py +++ b/src/ruleset.py @@ -1,11 +1,3 @@ -""" -The following determines the next state of a given cell in a CAM. - -The ruleset takes in a collection of rules specifying neighborhoods, as well as the configurations of -said neighborhood that yield an "on" or "off" state on the cell a ruleset is being applied to. - -@date: May 31st, 2015 -""" import enum import numpy as np import configuration as c @@ -75,21 +67,22 @@ class Ruleset: # which states do not pass for each configuration current_states = enumerate(plane.bits) for config in self.configurations: + totals = c.Neighborhood.get_totals(plane, config.offsets) + # Determine which function should be used to test success + if self.method == Ruleset.Method.MATCH: + vfunc = config.matches + elif self.method == Ruleset.Method.TOLERATE: + vfunc = config.tolerates + elif self.method == Ruleset.Method.SATISFY: + vfunc = config.satisfies + elif self.method == Ruleset.Method.ALWAYS_PASS: + vfunc = lambda *args: True + next_states = [] for index, state in current_states: - # Determine which function should be used to test success - if self.method == Ruleset.Method.MATCH: - vfunc = config.matches - elif self.method == Ruleset.Method.TOLERATE: - vfunc = config.tolerates - elif self.method == Ruleset.Method.SATISFY: - vfunc = config.satisfies - elif self.method == Ruleset.Method.ALWAYS_PASS: - vfunc = lambda *args: True - # 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 diff --git a/tests/ruleset_test.py b/tests/ruleset_test.py new file mode 100644 index 0000000..32a57eb --- /dev/null +++ b/tests/ruleset_test.py @@ -0,0 +1,76 @@ +import os, sys +sys.path.insert(0, os.path.join('..', 'src')) + +import plane +import ruleset as r +import configuration as c + +class TestRuleset: + """ + + """ + def setUp(self): + """ + + """ + self.plane2d = plane.Plane((100, 100)) + self.config = c.Configuration(1, plane=self.plane2d, offsets={ + (-1, 0): 1, + (-1, 1): 0, + ( 0, 1): 1, + ( 1, 1): 1, + }) + + def test_alwaysPassRuleset(self): + """ + + """ + # No configurations + tmp_r = r.Ruleset(r.Ruleset.Method.ALWAYS_PASS) + tmp_p = self.plane2d.bits.copy() + tmp_r.apply_to(self.plane2d) + assert tmp_p == self.plane2d.bits + + # One configuration + tmp_r.configurations.append(self.config) + tmp_r.apply_to(self.plane2d) + assert tmp_p != self.plane2d.bits + assert self.plane2d.bits.count() == 100 * 100 + + def test_matchRuleset(self): + """ + + """ + # No configurations + tmp_r = r.Ruleset(r.Ruleset.Method.MATCH) + tmp_p = self.plane2d.bits.copy() + tmp_r.apply_to(self.plane2d) + assert tmp_p == self.plane2d.bits + + # One configuration + tmp_r.configurations.append(self.config) + tmp_r.apply_to(self.plane2d) + assert tmp_p == self.plane2d.bits + self.plane2d[[(-1, 0), (0, 1), (1, 1)]] = 1 + tmp_r.apply_to(self.plane2d) + assert self.plane2d.bits.count() == 4 + + def test_tolerateRuleset(self): + """ + + """ + # No configurations + tmp_r = r.Ruleset(r.Ruleset.Method.TOLERATE) + tmp_p = self.plane2d.bits.copy() + tmp_r.apply_to(self.plane2d) + assert tmp_p == self.plane2d.bits + + # One configuration + tmp_r.configurations.append(self.config) + tmp_r.apply_to(self.plane2d, 0.5) + assert tmp_p == self.plane2d.bits + + self.plane2d[(-1, 0)] = 1 + tmp_r.apply_to(self.plane2d, 0.5) + assert self.plane2d.bits.count() == 4 +