In this post, we'll take the first small steps towards actually making a GUI for a simulation to run the ideal gas in and not just watch some numbers printed in a terminal. I'll be using Qt4 and the PyQt4 bindings. If you have a fairly recent linux distribution, you can apt-get install yourself to success very fast. Remember to also install the Qt-Designer app so you visually can layout your controls.
We'll start out simple by constructing buttons, textedits and a frame. I use a regular QWidget for my main form. The end goal for a user is to push the 1) setup button to initialize particles and 2) push the run button and make the simulation run. The simulation (i.e. the particles) should be displayed in the frame via matplotlib. It looks like this
To move on from here, there is a nifty little tool called pyuic4 which will convert your .ui file into a python class with the name Simulator_UI. I always just parse it down to a file called simulator_ui.py. The code is
$ pyuic4 simulator.ui > simulator_ui.py
The simulator_ui class contains the framework for the QWidget form we will be using. The most clever approach I could think of was to subclass the simulator_ui class to separate the "setting up the form" code with the "what happens when I push a button" code. The QWidget I'll use to represent the window the user will see and I call it Simulator. It imports the Simulator_UI class, subclasses it and sets up the GUI. The two buttons are hooked up to either initialize new particles or run for some steps
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from PyQt4 import QtGui, QtCore | |
from simulator_ui import Ui_main | |
from particlecanvas import ParticleCanvas | |
class MainForm(QtGui.QMainWindow): | |
def __init__(self, parent=None): | |
QtGui.QWidget.__init__(self,parent) | |
self.ui = Ui_main() | |
self.ui.setupUi(self) | |
QtCore.QObject.connect(self.ui.btnSetup, QtCore.SIGNAL("clicked()"), self.on_setup_clicked) | |
QtCore.QObject.connect(self.ui.btnRun, QtCore.SIGNAL("clicked()"), self.on_run_clicked) | |
self.c = ParticleCanvas(self.ui.frame) | |
def on_setup_clicked(self): | |
self.c.reinitialize(int(self.ui.txtNPart.text())) | |
def on_run_clicked(self): | |
for i in range(int(self.ui.txtNSteps.text())): | |
self.c.move() | |
self.c.redraw() |
The bread and butter of this application is the ParticleCanvas. Its tailored to draw and move the particles around. Look at its code
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from numpy import array, where, abs | |
from numpy.random import random | |
from canvas import Canvas | |
class ParticleCanvas(Canvas): | |
def __init__(self,parent,dpi=100.0): | |
Canvas.__init__(self,parent,dpi) | |
self.pos = None | |
self.vel = None | |
def reinitialize(self, npart): | |
self.pos = 2.0*(random((2,npart))-0.5) | |
self.vel = 2.0*(random((2,npart))-0.5) | |
self.particle_plot = None | |
self.redraw() | |
def move(self): | |
dt = 0.01 | |
# change velocity if they go beyond box | |
self.vel[where(abs(self.pos + dt*self.vel)>1.0)] *= -1.0 | |
self.pos += dt*self.vel | |
def on_draw(self): | |
if self.pos is None or self.vel is None: return | |
if self.particle_plot is None: | |
self.particle_plot, = self.axes.plot(self.pos[0],self.pos[1], 'ro', animated=True) | |
self.particle_plot.set_xdata(self.pos[0]) | |
self.particle_plot.set_ydata(self.pos[1]) | |
self.axes.draw_artist(self.particle_plot) |
You see that it subclasses the Canvas class which is my best bet on how one should make the code for fast blitting of a matplotlib canvas. A class which subclasses the Canvas class is only responsible for redrawing the actual plot (and do it fast). However, I would gladly appreciate comments of you have a better way to implement it
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from time import sleep | |
import matplotlib | |
import matplotlib.pyplot as plt | |
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas | |
from matplotlib.figure import Figure | |
matplotlib.rcParams.update({'font.size': 8}) | |
class Canvas(FigureCanvas): | |
def __init__(self,parent,dpi=100.0): | |
size = parent.size() | |
self.dpi = dpi | |
self.width = size.width() / dpi | |
self.height = size.height() / dpi | |
self.figure = Figure(figsize=(self.width, self.height), dpi=self.dpi, facecolor='w', edgecolor='k', frameon=False) | |
self.figure.subplots_adjust(left=0.05, bottom=0.05, right=0.95, top=0.95, wspace=None, hspace=None) | |
self.axes = self.figure.add_subplot(111) | |
self.axes.axis((-1,1,-1,1)) | |
FigureCanvas.__init__(self, self.figure) | |
self.updateGeometry() | |
self.draw() | |
self.cc = self.copy_from_bbox(self.axes.bbox) | |
self.particle_plot = None | |
self.setParent(parent) | |
self.blit(self.axes.bbox) | |
def on_pre_draw(self): | |
self.restore_region(self.cc) | |
def on_draw(self): | |
raise NotImplementedError | |
def on_post_draw(self): | |
self.blit(self.axes.bbox) | |
sleep(0.005) | |
def redraw(self): | |
self.on_pre_draw() | |
self.on_draw() | |
self.on_post_draw() |
Finally, the code that makes it all run is just my main executable which is defined like this
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
import sys | |
from PyQt4.QtGui import QApplication | |
from simulator import MainForm | |
def main(): | |
app = QApplication(sys.argv) | |
form = MainForm() | |
form.show() | |
app.exec_() | |
if __name__ == "__main__": | |
main() |
The final result looks like this
We now have a finished simulation GUI. However, the main problem is that the GUI and the simulation code is intimately hooked up so the GUI will freeze at some point. Fortunately, it leaves a new post on the horizon.
*I never understood anything of the find command, however.
It was very useful.
ReplyDeleteThanks a lot.