r
/
fifth
1
Fork 0

Separated view from model

master
Joshua Potter 2015-06-16 06:43:58 -04:00
parent fe05bd7eaa
commit f1042d351f
3 changed files with 247 additions and 86 deletions

View File

@ -10,14 +10,10 @@ tracing/echoing/multi-dimensional displays
@date: June 01, 2015 @date: June 01, 2015
""" """
import enum
import plane import plane
import display
import time
import curses
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as ani
class CAM: 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 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. 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): def __init__(self, cps=1, states=100, dimen=2):
""" """
@ -49,7 +52,6 @@ class CAM:
self.ticks = [(0, 1)] self.ticks = [(0, 1)]
self.total = 0 self.total = 0
def tick(self, rules, *args): def tick(self, rules, *args):
""" """
Modify all states in a given CAM "simultaneously". Modify all states in a given CAM "simultaneously".
@ -64,83 +66,6 @@ class CAM:
if self.total % j == 0: if self.total % j == 0:
rules.apply_to(self.planes[i], *args) 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): def randomize(self):
""" """
Convenience function to randomize individual planes. Convenience function to randomize individual planes.
@ -149,4 +74,15 @@ class CAM:
for plane in self.planes[1:]: for plane in self.planes[1:]:
plane.grid = self.master.grid 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()

226
src/display.py Normal file
View File

@ -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()

View File

@ -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 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). binary expansion will be 100 bits long (and padded with 0's if necessary).
@author: jrpotter
@date: June 05, 2015 @date: June 05, 2015
""" """
import numpy as np import numpy as np