Interfacing SimPy to other packages
by Mike Mellor, Klaus Müller, and Tony
Vignaux
SimPy
Developers
SimPy can be interfaced with practically any Python-callable package
running under Python version 2.2 or later. Out of the box, SimPy
version 1.4 provides the Tk-based GUI package SimGUI and the
plotting package SimPlot (also Tk-based).
This document gives
examples of interfaces to an alternative GUI and alternative plotting
package. It is assumed
that the model builder is proficient with Python.
Table of Contents
There are many interface packages available to the Python community,
such as Tkinter, wxPython (including PythonCard), pyGTK, and pyQT.
We are currently using Tkinter for SimPy for a couple of
reasons. First, it is included in every Python distribution (Windows,
Linux, Unix, MAC, etc.). The only distribution that does not include
Tkinter, as far as we know, is the WinCE package currently developed
by Brad Clements. Tkinter interfaces written on one system will
generally work without modification on all systems. The other reason
for selecting Tkinter is ease of coding. Based on my experience, there
is no interface package that allows the model builder to create a
simple interface in fewer lines of code (and time) than
Tkinter. PythonCard may be easy to use, but it is not a standard part
of the Python distribution. Having said all that, the techniques used
to develop interfaces with Tkinter can easily be applied to any other
interface package.
As a quick aside, you can test your python installation to make sure
that
Tkinter is properly installed by the command line by opening an
interactive
shell (type "python" and hit "Enter"). At the prompt (">>>")
type
"from Tkinter import *" and hit "Enter" again. If you don't get an
error
message then you have Tkinter installed!
Types of Interfaces
There are three basic methods of interfacing with a model: by design, a
command
line interface (CLI), and a graphical user interface (GUI). All the
code
in this tutorial up to this point have implemented interface by design
-
the model builder must make all modifications to the model directly in
the
code. This is the fastest way to develop a model, but it is slower to
modify
the model parameters than the other methods. If the model is going to
be
used for more than one specific scenario, or if non-coders need to run
it,
then this method is not recommended. Simple CLI interfaces take a
little
more effort, but allow users to change variables without changing the
code.
The downside of CLI's is that they are fairly inflexible - not as "user
friendly"
as a GUI. A GUI is very flexible for the users - users only need to
change
the variables that they want to (if default values are present).
Components of Interfaces
There are four components of interface design in SimPy: inputs, flow
control,
outputs, and the structure of the model code. Models designed without
an
interface (a "by design" model) generally accept no run-time parameters
and
return output to the console that the user started the model from. Flow
control consists of the user typing "python model.py" on the command
line.
CLI interfaces prompt the user to input data or accept defaults. Flow
control
is another prompt, and the output goes to the command line. GUI
interfaces
generally allow the user to view and modify all the data from one
window,
control flow with a "Start" button, and output either to a console or
within
the GUI itself. The structure of the model code can be used with any of
the interfaces, but is required for the CLI and GUI interfaces.
Command Line Interfaces (CLIs)
(to be added later)
Graphical User Interfaces (GUIs)
This chapter deals with developing a GUI as an alternative to the
built-in SimGUI.
The first phase of developing a GUI interface in SimPy is
preparing the code
to support the GUI. To do this, we need to import Tkinter by adding the
following line at the top of the code:
Purists may say that you need only import the widgets that you plan to
use
and importing all slows down the code, while this may be true, it is
easier
to import * and have access to everything you need as the model
develops.
After adding this line, the next thing is to create a model function.
This
model function contains all the code that "drives" the model; for
example:
def model(): initialize() for i in range(10): worker = Process() activate(worker,worker.task,...)
|
This allows the interface to run the model by calling the model()
function. Notice that all the classes for processes, monitors and
resources
are not part of this function. They are called by the function as the
code
executes.
The next phase in developing an interface is to determine which
variables
need to be modifiable. In the Bank example, the model builder might
want
to vary the number of customers in a run. The first step is to modify
the
model() function:
def model(customers=10): initialize() for i in range(customers): worker = Process() activate(worker,worker.task,...)
|
Notice that a default value is assigned to customers. Default values
aid
the model developer by allowing the model to function as a "hard wired"
model
(useful when debugging code). Default values are also beneficial to a
user
- an example, like a picture, is worth a thousand words (in a user's
guide).
GUI Design
Once the model has been modified to support an interface, the next
phase
is to build the interface framework. The basic code for a Tkinter
interface
is:
#GUI Stuff root=Tk() Frame1=Frame(root)
# buttons, frames, etc. go here
Frame1.pack() Root.mainloop()
|
There are several books on interface design and Tkinter development, so
I
won't go into a philosophical discussion here. My technique is to
create
all buttons and entry fields using a grid layout in one or two frames
(input
and control), and put the output in a separate frame. Using the
previous
example, here's how to lay out the frames:
# GUI Stuff Here
def die(): ''' This allows you to kill the program by pressing a button instead of the "X" on the frame''' sys.exit()
root = Tk() FrameInput = Frame(root) FrameOutput = Frame(root) FrameControl = Frame(root)
# Input Widgets # None
# Output Widgets # None
# Control Widgets btnRun = Button(FrameControl, text = "Run Model", command = model) btnRun.grid(row = 0, column=0) btnQuit = Button(FrameControl, text = "Quit", command = die) btnQuit.grid(row = 0, column=1)
# Default Values # None
FrameInput.pack() FrameOutput.pack() FrameControl.pack()
root.mainloop()
|
This code creates the three frames and adds buttons to the control
frame.
Also, I added the "die" function, which will stop the model. Here's
what
it looks like in action:
Example 1a: Running GUI

The next step is to add some user inputs. The easiest way to do this is
to add a label and an entry box. Notice the last line of the code
snippet
- it provides a default value for the entry box. Defaults will allow
the
model to run without having to retype the data for each run, and gives
the
user an idea of the type/range of data used.
Example 2: gui_02.py snippet
# Input Widgets lblCustomers = Label(FrameInput, text = "Enter the number of customers for this run: ") lblCustomers.grid(row = 0, column = 0) entCustomers = Entry(FrameInput) entCustomers.grid(row = 0, column = 1)
# Default Values entCustomers.insert(0,"50")
|
And here is the revised GUI:
Example 2b. Running GUI

Based on the previous example, it is a simple matter to add the rest of
the
user inputs, as shown below:
Example 3: gui_03.py snippet
# Input Widgets lblCustomers = Label(FrameInput, text = "Enter the number of customers for this run: ") lblCustomers.grid(row = 0, column = 0) entCustomers = Entry(FrameInput) entCustomers.grid(row = 0, column = 1)
lblArr = Label(FrameInput, text = "Enter the interarrival rate: ") lblArr.grid(row = 1, column = 0) entArr = Entry(FrameInput) entArr.grid(row = 1, column = 1)
lblWait = Label(FrameInput, text = "Enter the mean processing time: ") lblWait.grid(row = 2, column = 0) entWait = Entry(FrameInput) entWait.grid(row = 2, column = 1)
lblDuration = Label(FrameInput, text = "Enter the duration of this run: ") lblDuration.grid(row = 3, column = 0) entDuration = Entry(FrameInput) entDuration.grid(row = 3, column = 1)
# Default Values entCustomers.insert(0,"50") entArr.insert(0,"10") entWait.insert(0,"12") entDuration.insert(0,"1000")
|
Now the interface has all the desired inputs:
Example 3a. Running GUI

The last thing to do is to add the output widget. While there are many
better
widgets available in the Python Mega-Widgets (PMW) collection, the text
widget
will suffice for simple text output. Adding the text box is easy. What
requires a little more attention is modifying the code in the classes
and
functions to send their output to the text box instead of the command
line.
Here is the required change:
Old Code:
print "%7.4f %s: Here I am. %s "%(now(),self.name,Qlength)
|
New Code:
txtOutput.insert(END, "\n%7.4f %s: Here I am. %s "%(now(),self.name,Qlength))
|
Two key points here. First, the keyword "END" will append the data to
the
end of the output (you could also add the data at the beginning, but
that
wouldn't make much sense). Second, the "\n" inserts a line break, so
that
the data will go on a new line in the text widget. Here is the modified
GUI code (the modifications to the classes are included in the complete
gui_04.py
file):
Example 4: gui_04.py snippet
# Output Widgets txtOutput = Text(FrameOutput) txtOutput.pack()
|
And the running GUI looks like this:
Example 4a. Running GUI

This model generates a lot of text. While scroll bars are not used,
(PMW
has a scrolling text widget) you can scroll through the text using the
arrow
keys. Here is an example of the GUI after an integration:
Example 4b. Running GUI After an Iteration

What you see in this screen shot is the GUI after running two
iterations.
The break in the text is inserted at the beginning of an iteration.
Simulation programs normally produce large quantities of output which
needs
to be visualized, e.g. by plotting. These plots can help with
determining the warm-up
period of a simulation.
This chapter deals with using plotting packages as an alternative to
the built-in SimPlot.
SciPy is an open source library
of scientific tools for Python. SciPy supplements the popular Numeric
module, gathering a variety of high level science and engineering
modules together as a single package.
SciPy includes modules for graphics and plotting, optimization,
integration, special functions, signal and image processing, genetic
algorithms, ODE solvers, and others.
One of the three plotting packages which SciPy provides is gplt.
It is Gnuplot-based and has a wide range of features (see the SciPy
Plotting Tutorial).
Installing gplt
Download SciPy from http://www.scipy.org/site_content/download_list
and install it.
The gplt module requires that Gnuplot is installed on your machine.
On Windows, SciPy automatically installs Gnuplot along with a necessary
"helper" program for this platform. Most Linux distributions come with
Gnuplot, and it is readily available on most other platforms.
An example from the Bank Tutorial
As an example of how to use gplt with SimPy, here is a modified
version of
bank12.py from the Bank Tutorial:
#! /usr/local/bin/python """ Based on bank12.py in Bank Tutorial. """ from __future__ import generators from scipy import * ## (1) from SimPy.Simulation import * from SimPy.Monitor import * from random import Random
class Source(Process): """ Source generates customers randomly""" def __init__(self,seed=333): Process.__init__(self) self.SEED = seed
def generate(self,number,interval): rv = Random(self.SEED) for i in range(number): c = Customer(name = "Customer%02d"%(i,)) activate(c,c.visit(timeInBank=12.0)) t = rv.expovariate(1.0/interval) yield hold,self,t
class Customer(Process): """ Customer arrives, is served and leaves """ def __init__(self,name): Process.__init__(self) self.name = name def visit(self,timeInBank=0): arrive=now() yield request,self,counter wait=now()-arrive wate.append(wait) tme.append(now()) waitMonitor.tally(wait) tib = counterRV.expovariate(1.0/timeInBank) yield hold,self,tib yield release,self,counter
def model(counterseed=3939393): global counter,counterRV,waitMonitor counter = Resource(name="Clerk",capacity = 1) counterRV = Random(counterseed) waitMonitor = Monitor() initialize() sourceseed=1133 source = Source(seed = sourceseed) activate(source,source.generate(100,10.0),0.0) ob=Observer() activate(ob,ob.observe()) simulate(until=2000.0) return waitMonitor.mean()
class Observer(Process): ## (2) def __init__(self): Process.__init__(self)
def observe(self): while True: yield hold,self,5 q.append(len(counter.waitQ)) t.append(now()) q=[] t=[] wate=[] tme=[] model()
gplt.plot(t,q,'notitle with impulses') ## (3) gplt.title("Bank12: queue length over time") ## (4) gplt.xtitle("time") ## (5) gplt.ytitle("queue length before counter") ## (6)
## (7) gplt.png(r"c:\python22\simpy\development\futureversions\plotting\scipy\bank12plot1.png")
#Histogram h={} for d in q: try: h[d]+=1 except: h[d]=1
d=h.items() d.sort() a=[] for i in d: a.append(i[1]) gplt.plot(a,"notitle with histeps") gplt.title("Bank12: Frequency of counter queue length") gplt.xtitle("queuelength") gplt.ytitle("frequency") gplt.png(r"c:\python22\simpy\development\futureversions\plotting\scipy\bank12plot2.png")
|
At (1), the scipy library is imported, including gplt.
At (2), a process to collect the data to be plotted by sampling at
fixed time
intervals is define.
At (3), the basic plot of queue length over time is generated.
At (4), the plot title is inserted.
At (5) and (6), the titles for the plot axis are inserted.
At (7), the PNG file is generated and saved.
gplt.plot takes the following parameters: 'impulses', 'dots',
'fsteps', 'histeps',
'lines', 'linespoint', 'points', 'steps'. Try them all out! See http://www.ucc.ie/gnuplot/gnuplot.html#5109
for details.
Running the program above results in two PNG files. The first (. . .
/bank12plot.png) shows the queue length over time:

The second output file (. . ./bank12plot2.png) is a histogram of the
frequency of the length of the queue:

VPython
is one of many Python plotting packages. It supports both 3D graphics
and
simple plotting. The latter will be used in this section.
Vpython requires OpenGL. It runs on Windows, Linux, Macintosh (System 9
and
X11 on OS X), and UNIX (SGI). It can be downloaded from here and
is
distributed free of charge.
With VPython, plots can be built up either incrementally, i.e., by
calls distributed
over the simulation program, or in one go, after a run or set of runs
from
output collected into a data-structure (list, array, file, . . ). In
the
two examples here, the latter approach will be taken.
Both examples are based on the program bank12.py from the SimPy Bank
Tutorial.
Waiting times
The first example (x.1) gathers the mean waiting times for bank clients
from
10 replication runs and shows them in a histogram (As VPython does not
have
a save function for graphics, this was produced by screen capture)

The SimPy program is as follows:
Listing 1.
#! /usr/local/bin/python """ bank12VPhisto.py: Simulate customers arriving at random, using a Source requesting service from two clerks but a single queue with a random servicetime Uses a Monitor object to record waiting times Set up 10 replications Plot mean wait-time histogram """ from __future__ import generators from SimPy.Simulation import * from SimPy.Monitor import * from random import Random from visual.graph import * ## (1)
class Source(Process): """ Source generates customers randomly""" def __init__(self,seed=333): Process.__init__(self) self.SEED = seed
def generate(self,number,interval): rv = Random(self.SEED) for i in range(number): c = Customer(name = "Customer%02d"%(i,)) activate(c,c.visit(timeInBank=12.0)) t = rv.expovariate(1.0/interval) yield hold,self,t
class Customer(Process): """ Customer arrives, is served and leaves """ def __init__(self,name): Process.__init__(self) self.name = name def visit(self,timeInBank=0): arrive=now() yield request,self,counter wait=now()-arrive waitMonitor.tally(wait) tib = counterRV.expovariate(1.0/timeInBank) yield hold,self,tib yield release,self,counter
def model(counterseed=3939393): global counter,counterRV,waitMonitor counter = Resource(name="Clerk",capacity = 2) counterRV = Random(counterseed) waitMonitor = Monitor() initialize() sourceseed=1133 source = Source(seed = sourceseed) activate(source,source.generate(100,10.0),0.0) simulate(until=2000.0) return waitMonitor.mean()
def plot(result): ## (2) waitGraph = gdisplay(width=600, height=200, title='Bank model; 2 clerks; 10 runs', xtitle='mean wait time', ytitle='nr of observations',foreground=color.black, background=color.white) ## (3) waitMeans=ghistogram(gdisplay=waitGraph,bins=arange(0,6.0,0.5), color=color.blue) ## (4) waitMeans.plot(data=result)
result = [model(counterseed=cs) for cs in \ [13939393,31555999,777999555,319999771,33777999, 123321123,9993331212,75577123,8543311,2355221, 1212121,55555555,98127633,35353535,888111333, 7766775577,11993335,333444555,77754121,8771133]]
plot(result) ## (6)
|
The program structure is simple:
At (1), the plotting part of VPython is imported.
At (2), the graph window is defined in terms of size, position,
and title for the title bar of the graph window and titles
for the x and y axes before a graph plotting object: is created.
At (3), the type of the plot (histogram) and its bin and color
attributes are set.
At (4), the result list is plotted.
Queue lengths
The second example (x.2) gathers the waiting queue length for bank
clients
over time and plots it as a line graph:

The SimPy program that produced this is as follows:
Listing 2.
#! /usr/local/bin/python """ bank12VPwait.py: Simulate customers arriving at random, using a Source requesting service from one clerk with a random servicetime. Plots the waitQ length over time. """ from __future__ import generators from SimPy.Simulation import * from random import Random from visual.graph import * ## (1)
class Source(Process): """ Source generates customers randomly""" def __init__(self,seed=333): Process.__init__(self) self.SEED = seed
def generate(self,number,interval): rv = Random(self.SEED) for i in range(number): qLength.collect((now(),len(counter.waitQ))) ## (2) c = Customer(name = "Customer%02d"%(i,)) activate(c,c.visit(timeInBank=12.0)) t = rv.expovariate(1.0/interval) yield hold,self,t
class Customer(Process): """ Customer arrives, is served and leaves """ def __init__(self,name): Process.__init__(self) self.name = name def visit(self,timeInBank=0): global qLength arrive=now() yield request,self,counter tib = counterRV.expovariate(1.0/timeInBank) yield hold,self,tib yield release,self,counter
def model(counterseed=3939393): global counter,counterRV,waitMonitor counter = Resource(name="Clerk",capacity = 1) counterRV = Random(counterseed) initialize() sourceseed=1133 source = Source(seed = sourceseed) activate(source,source.generate(100,10.0),0.0) simulate(until=2000.0)
def plot(result): ## (3) qGraph = gdisplay(width=600, height=200, title='Bank model; 1 clerk', xtitle='time', ytitle='Queue length', foreground=color.black, background=color.white) ## (4) nrInQ = gcurve(gdisplay=qGraph, color=color.red) last = 0 for i in result: nrInQ.plot(pos=(i[0],last)) ## (5) nrInQ.plot(pos=(i[0],i[1])) ## (6) last = i[1]
class Recorder: ## (7) def __init__(self): self.collected = []
def collect(self,data): self.collected.append(data) qLength=Recorder() model() plot(qLength.collected)
|
The program structure:
At (1), the plotting part of VPython is imported.
At (2), time and queue length are collected.
At (3), the graph window is defined.
At (4), the graph (line diagram) is defined.
At (5) and (6), the curve is plotted.
SimPy
Developers