From f1042d351f8e61c422961132f9a655c9a575c76d Mon Sep 17 00:00:00 2001 From: Joshua Potter Date: Tue, 16 Jun 2015 06:43:58 -0400 Subject: [PATCH] Separated view from model --- src/cam.py | 106 +++++------------------ src/display.py | 226 +++++++++++++++++++++++++++++++++++++++++++++++++ src/plane.py | 1 - 3 files changed, 247 insertions(+), 86 deletions(-) create mode 100644 src/display.py diff --git a/src/cam.py b/src/cam.py index f0f17cf..fb096d5 100644 --- a/src/cam.py +++ b/src/cam.py @@ -10,14 +10,10 @@ tracing/echoing/multi-dimensional displays @date: June 01, 2015 """ +import enum + import plane - -import time -import curses -import numpy as np -import matplotlib.pyplot as plt -import matplotlib.animation as ani - +import display class CAM: """ @@ -32,6 +28,13 @@ class CAM: updated. should be updated, Certain planes may or may not change every tick, but instead on every nth tick, allowing for more sophisticated views such as ECHOing and TRACing. """ + class Show(enum.Enum): + """ + Display method. + """ + NONE = 0 + CONSOLE = 1 + WINDOW = 2 def __init__(self, cps=1, states=100, dimen=2): """ @@ -49,7 +52,6 @@ class CAM: self.ticks = [(0, 1)] self.total = 0 - def tick(self, rules, *args): """ Modify all states in a given CAM "simultaneously". @@ -64,83 +66,6 @@ class CAM: if self.total % j == 0: rules.apply_to(self.planes[i], *args) - - def start_plot(self, clock, rules, *args): - """ - Initiates main graphical loop. - - The following function displays the graphical component (through use of matplotlib), and triggers the - next tick for every "clock" milliseconds. This should only be called if the automata is 2 or 3 dimensional. - """ - fig, ax = plt.subplots() - - ax.set_frame_on(False) - ax.get_xaxis().set_visible(False) - ax.get_yaxis().set_visible(False) - - mshown = plt.matshow(self.master.bits(), fig.number, cmap='Greys') - - def animate(frame): - self.tick(rules, *args) - mshown.set_array(self.master.bits()) - return [mshown] - - ani.FuncAnimation(fig, animate, interval=clock) - - plt.axis('off') - plt.show() - - def _console_run(self, stdscr, clock, rules, *args): - """ - The following displays all bits onto the console. - - Since overflow of the window is most probable, we create a pad allowing the user to navigate - the scene via the arrow keys. Note this does wrap around, so one can go left (for example) indefinitely. - For multiple bit planes, curses.panels are used. - """ - y, x = 0, 0 - max_y, max_x = stdscr.getmaxyx() - width, height = self.master.shape - - pad = curses.newpad(width+1, height+1) - pad.nodelay(1) - pad.keypad(1) - - while True: - - # Allow navigating plane - c = pad.getch() - if c == curses.KEY_UP: - y = (y + 1) % height - elif c == curses.KEY_DOWN: - y = (y - 1) % height - elif c == curses.KEY_LEFT: - x = (x + 1) % width - elif c == curses.KEY_RIGHT: - x = (x - 1) % width - - # Cycle around grid - grid = self.master.grid - grid = np.append(grid[y:], grid[:y]) - - # Draw out to console - line = 0 - for bits in grid.flat: - pad.move(line, 0) - pad.addstr((bits[x:] + bits[:x]).to01()) - line += 1 - pad.refresh(0, 0, 0, 0, max_y-1, max_x-1) - time.sleep(clock / 1000) - self.tick(rules, *args) - - def start_console(self, clock, rules, *args): - """ - Initates main console loop. - - Works similarly to start_plot but prints out to the console. - """ - curses.wrapper(self._console_run, clock, rules, *args) - def randomize(self): """ Convenience function to randomize individual planes. @@ -149,4 +74,15 @@ class CAM: for plane in self.planes[1:]: plane.grid = self.master.grid + def start(self, show, **kwargs): + """ + Delegate how to initiate running the CAM. + """ + if show == CAM.Show.NONE: + while True: + self.tick(**kwargs) + elif show == CAM.Show.CONSOLE: + ConsoleDisplay(self, **kwargs).run() + elif show == CAM.Show.WINDOW: + WindowDisplay(self, **kwargs).run() diff --git a/src/display.py b/src/display.py new file mode 100644 index 0000000..ef6069d --- /dev/null +++ b/src/display.py @@ -0,0 +1,226 @@ +""" +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 time +import curses +import curses.panel + +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.animation as ani + + +class _Display: + """ + The base class for visualization of the CAM. + """ + + def __init__(self, cam, clock, rules, *args): + """ + Test for valid CAM and setup. + """ + self.cam = cam + if not self._valid(): + raise ValueError("Invalid Dimension for Display") + + self.clock = clock + self.rules = rules + self.tick_args = *args + + def _valid(self): + """ + Ensures passed cam is supported for display type. + """ + return True + + def run(self): + """ + Initiate the main display loop. + """ + pass + + +class ConsoleDisplay(_Display): + """ + Displays CAM onto console via the curses library. + + Note a couple concepts go hand in hand with displaying all the bits: + + First, it is unlikely the entirety of a CAM can be displayed on the screen + at a time, so instead the use of a pad is supported, allowing navigation + via the arrow keys. + + Second, to provide support for color mapping and multiple planes, we use panels + which allow for exactly this overlaying we are trying to simulate. + """ + + def __init__(self, cam, clock, rules, *args): + """ + Here we initialize the curses library, and begin construction of the necessary panels. + """ + super().__init__(cam, clock, rules, *args) + + # Basic Curses Setup + self.stdscr = curses.initscr() + curses.noecho() + curses.cbreak() + stdscr.keypad(True) + + # Specifies the offsets the grid are taken at + self.x = 0 + self.y = 0 + + # Note we actually don't use the stdscr, but keeping + # reference for future use + width, height = self.cam.master.shape + self.pad = curses.newpad(width+1, height+1) + self.pad.nodelay(1) + self.pad.keypad(1) + + # Construct the necessary planes + self.panels = [] + for plane in self.cam.planes: + self.panels.append(curses.panel.new_panel(self.pad)) + + def _valid(self): + """ + Ensures only 2D CAMs are accepted. + """ + return len(self.cam.master.shape) == 2 + + def _shift(self, ch): + """ + Move all panels over by specified amount. + + Note we want functionality to wrap around, so we make + sure to mod based on which direction we've gone. Directionality + is determined by the passed character value. + """ + if ch == curses.KEY_UP: + self.y = (self.y + 1) % height + elif ch == curses.KEY_DOWN: + self.y = (self.y - 1) % height + elif ch == curses.KEY_LEFT: + self.x = (self.x + 1) % width + elif ch == curses.KEY_RIGHT: + self.x = (self.x - 1) % width + + def run(self): + """ + Commence actual loop. + + The following draws out all panels, and, in the case of an exception + (which could be user thrown by Ctrl-C), restores the terminal back + to a usable state. + """ + while True: + try: + # Note the user can change the size of the terminal, + # so we query for these values every time + max_y, max_x = stdscr.getmaxyx() + + # Navigate the plane + # Note in the __init__ method, this was set to not block + self._shift(pad.getch()) + + # Cycle around grid + grid = self.cam.master.grid + grid = np.append(grid[y:], grid[:y]) + + # Draw out to console + line = 0 + for bits in grid.flat: + pad.move(line, 0) + pad.addstr((bits[x:] + bits[:x]).to01()) + line += 1 + + # Draw out to screen + curses.panels.update_panels() + pad.refresh(0, 0, 0, 0, max_y-1, max_x-1) + + # Prepare for next loop + time.sleep(self.clock / 1000) + self.tick(self.rules, *self.tick_args) + + except: + curses.nocbreak() + stdscr.keypad(False) + curses.echo() + curses.endwin() + + +class WindowDisplay(_Display): + """ + Displays CAM onto window via the matplotlib library. + + We use the AxesImage object in matplotlib and constantly animate + the graph to display the automata. Unlike the curses library, this + class also provides support for 3D display, though note this is + much more intensive. + """ + def __init__(self, cam, clock, rules, *args): + """ + Initialize matplotlib objects. + """ + super().__init__(cam, clock, rules, *args) + + # Keep local reference for convenience + self.fig, self.ax = plt.subplots() + + # Note we draw out planes in the reverse direction + # for proper superimposition + self.matrices = [] + for plane in self.cam.planes: + mshown = plt.matshow(plane.bits(), fig.number, cmap='Greys') + self.matrices.append(mshown) + + def _valid(self): + """ + Ensures only 2D/3D CAMs are accepted. + """ + return 2 <= len(self.cam.master.shape) <= 3 + + def _animate(self, frame): + """ + Display the next state of the automaton. + + Note that the framerate must be considered; one shouldn't just try to run + the animation as fast as possible, as this callback will only be bottled up. + The limiting factor is the tick method, which should be fast enough for reasonably + sized CAMs (100x100 runs in <50 ms on my computer), but runs in quadratic time. + """ + self.cam.tick(self.rules, *self.tick_args) + if len(self.cam.master.shape) == 2: + self.mshown.set_array(self.cam.master.bits()) + return [self.mshown] + else: + pass + + def run(self): + """ + Commence actual loop. + + The following expands out each plane (from a bitarray to a matrix of bits) + which are then displayed out via the animate function. We simply superimpose + the necessary plots for the desired overlaying. + """ + if len(self.cam.master.shape) == 2: + self.ax.set_frame_on(False) + self.ax.get_xaxis().set_visible(False) + self.ax.get_yaxis().set_visible(False) + else: + pass + + ani.FuncAnimation(self.fig, self.animate, interval=self.clock) + plt.axis('off') + plt.show() + diff --git a/src/plane.py b/src/plane.py index 4d5a9b3..3ed8881 100644 --- a/src/plane.py +++ b/src/plane.py @@ -9,7 +9,6 @@ state at the ith index of the given row. This holds for 0 as well. For example, given a 100 x 100 CAM, we represent this underneath as a 1-D array of 100 integers, each of which's binary expansion will be 100 bits long (and padded with 0's if necessary). -@author: jrpotter @date: June 05, 2015 """ import numpy as np