Interfacing SimPy to other packages


by Mike Mellor, Klaus Müller, and Tony Vignaux

SimPy Developers

Bank12 queue length

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

1. User Interfaces

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:
from Tkinter import *

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
Run Model widget

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
revised widget gui_02

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
gui_04

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
gui_05

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
gui_05b

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.

2. Output visualization

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.

Using gplt from SciPy

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 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:

Using VPython

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)

Bank12 histogram

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 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:

Bank12 queue length

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