The Bank: an example of a SimPy Simulation

G A Vignaux

Abstract

The Bank is a demonstration of the use of SimPy in developing and using a simple simulation of a practical problem, a multi-server bank.

This is Revision: 1.18 .

Contents

1  Introduction
2  Our Bank
3  A Customer
    3.1  A non-random Customer
    3.2  A Random Customer
4  More Customers
    4.1  Many non-random Customers
    4.2  Many Random Customers
5  A Counter
    5.1  One Counter
    5.2  A random service time
6  Several Counters
    6.1  Several Counters but a Single Queue
    6.2  Several Counters with individual queues
7  Monitors and Gathering Statistics
    7.1  Monitors
    7.2  Multiple runs
8  Final Remarks
9  Acknowledgments

1  Introduction

This document works through the development of a simulation of a bank using SimPy.

SimPy is a Python-based discrete-event simulation system. It uses parallel processes to model active components such as messages, customers, trucks, planes. It provides a number of facilities for the simulation programmer: Processes, Resources, and, just as important, ways of recording the average values of variables in Monitors .

Before attempting to use SimPy you should have some familiarity with the the Python language. In particular, you should be able to use and define classes of objects. Python is free and available on most machine types. You can find out more about it and download it from the Python web-site, http://www.Python.org. This document assumes Python 2.2 or later. NOTE: Until Python 2.3 arrives, the following import statement must be placed at the top of all SimPy scripts: from __future__ import generators

SimPy itself can be obtained from:
http://sourceforge.net/projects/simpy/.

2  Our Bank

We are going to model a simple bank with a number of tellers and customers arriving at random. All simulations need to answer a question; in this case we are to investigate how increasing the number of tellers reduces the queueing time for customers.

We will develop the model step-by-step, starting out very simply.

3  A Customer

We first model a single customer who arrives at the bank for a visit, looks around for a time and then leaves. There are no bank counters in this model. First we will assume his arrival time and time in the bank are known. Then we will allow the customer to arrive at a random time.

3.1  A non-random Customer

We define a Customer class which will derive from the SimPy Process  class. Examine Example 3.1 which, except for the line numbers I have added, is a complete runnable Python script.

Line 1 is a Python comment indicating to the Unix shell which Python interpreter to use. Line 2 is a normal Python documentation string.

Lines 3 and 4 import generators (from the __future__as Python 2.2 does not yet have generators built-in) and the SimPy simulation code.

Example 3.1    The initial customer model. The customer visits the bank at simulation time 5.0 and leaves after 10.0. We will take time units to be minutes. Except for the line numbers this program is a runnable Python SimPy program.
  1 #! /usr/local/bin/python
  2 """ Simulate a single customer """
  3 from __future__ import generators  
  4 from SimPy.Simulation  import *
  5 
  6 class Customer(Process):
  7     """ Customer arrives, looks around and leaves """
  8     def __init__(self,name):
  9         Process.__init__(self)
 10         self.name = name
 11         
 12     def visit(self,timeInBank=0):       
 13         print "%7.4f %s: Here I am"%(now(),self.name)
 14         yield hold,self,timeInBank
 15         print "%7.4f %s: I must leave"%(now(),self.name)
 16 
 17 def model():
 18     initialize()
 19     c=Customer(name="Klaus")
 20     activate(c,c.visit(timeInBank=10.0),delay=5.0)
 21     simulate(until=100.0)
 22 
 23 model()

Now examine the Customer class definition, lines 6-15. It defines our customer and has the two required methods: an __init__method (line 8) and an action method (visit) (line 12). An __init__ method must call Process.__init__(self) and can then initialize any instance variables needed by the class objects. Here, on line 10, we give the customer a name which will be used when we run the simulation.

The visit action method, lines 12-15, executes the customer's activities. When he arrives (it will turn out to be a 'he' in this model), he will print out the simulation time, now(), and his name (line 13). The function now() can be used at any time in the simulation to find the current simulation time. The name will be set when the customer is created in the main() routine.

He then stays in the bank for a simulation period timeInBank (line 14). This is achieved in the yield hold,self,timeInBank statement. This is the first of the special simulation commands that SimPy offers. The yield statements shows that the customer's visit method is a Python generator.

After a simulation time of timeInBank, the program's execution returns to the line after the yield statement, line 15. Here he again print out the current simulation time and his name. This completes the declaration of the Customer class.

Lines 17-23 declare the model routine and then call it. The name of this function could be anything, of course, (for example main(), or bank()). Indeed, the code could even have been written in-line rather than embedded in a function but when we come to carry out series of experiments we will find it is better to structure the model this way.

Line 18 calls initialize() which sets up the simulation system ready to receive activate calls. Then, in line 19, we create a customer with name Klaus. We activate Klaus in line 20. We specify the active object(c) to be activated, the call of the action routine (c.visit(timeInBank=10.0)) and the time it is to be activated (with, here, a after=5.0). This will activate klaus after a delay from the current time, in this case at the start of the simulation, 0.0, of 5.0 minutes. The call of an action routine such as c.visit can specify the values of arguments, here the timeInBank.

Finally the call of simulate(until=100.0 in line 21 will start the simulation. It will run until the simulation time is 100.0 unless stopped beforehand. A simulation can be stopped, either by the command stopSimulation() or by running out of events to execute (as will happen here).

So far we have been declaring a class, its methods, and one routine. The call of model() in line 23 starts the script running. The trace printed out by the print commands (Example 3.2) show its execution. The program finishes at simulation time 15.0 because there are no further events to be executed. At the end of the visit routine, the customer has no more actions and no other objects or customers are active.

Example 3.2    The output from bank01.py.

 5.0000 Klaus: Here I am
15.0000 Klaus: I must leave

3.2  A Random Customer

Now we extend the model to allow our customer to arrive at a random simulated time. We modify the previous model (Example 3.3).

Example 3.3    The randomly arriving customer.
  1 #! /usr/local/bin/python
  2 """ A single customer arrives at random time"""
  3 from __future__ import generators
  4 from SimPy.Simulation  import *
  5 from random import Random
  6 
  7 class Customer(Process):
  8     """ Customer arrives at a random time,
  9     looks around  and then leaves
 10     """
 11     def __init__(self,name):
 12         Process.__init__(self)
 13         self.name = name
 14         
 15     def visit(self,timeInBank=0):       
 16         print "%7.4f %s: Here I am"%(now(),self.name)
 17         yield hold,self,timeInBank
 18         print "%7.4f %s: I must leave"%(now(),self.name)
 19 
 20 def model():
 21     rv = Random(1133)
 22     initialize()
 23     c=Customer(name="Klaus")
 24     t = rv.expovariate(1.0/5.0)
 25     activate(c,c.visit(timeInBank=10.0),delay=t)
 26     simulate(until=100.0)
 27 
 28 model()

The change occurs in lines 5, 21, 24, and 25 of model(). In line 5 we import from the standard Python random module. We need Random to create a random variable object - this is done in line 21 using a random number seed of 1133. In later models we will use the seed as an argument to the model routine. We need expovariate to generate an exponential random variate from that object - this is done in line 24. Line 25 uses the random sample, t as the delay argument to the activate call. The result is shown in Example 3.4 where we see the customer now arrives at time 4.3657. Changing the seed value would change that time.

Example 3.4    The output for the random Customer.

 4.3657 Klaus: Here I am
14.3657 Klaus: I must leave

4  More Customers

Our simulation does little so far. Eventually, we need multiple customers to arrive at random and be served for times that can also be random. Let us next consider several customers. We first go back to the simple deterministic model.

We find that the program is almost as easy as before. Example 4.1 shows the extension of the original model (that is Example 3.1). Example 4.2 shows its trace. The only change is in lines 18-25 where we create, name, and activate three customers in model() and a change in the argument of simulate(until=100). Customer Tony is created second but as it is activated at simulation time 2.0 he will start before Customer Klaus. Each of the customers stays for a different timeinbank.

Example 4.1    The bank model with several customers.
  1 #! /usr/local/bin/python
  2 """ Simulate a single customer """
  3 from __future__ import generators
  4 from SimPy.Simulation  import *
  5 
  6 class Customer(Process):
  7     """ Customer arrives, looks around and leaves """
  8     def __init__(self,name):
  9         Process.__init__(self)
 10         self.name = name
 11         
 12     def visit(self,timeInBank=0):       
 13         print "%7.4f %s: Here I am"%(now(),self.name)
 14         yield hold,self,timeInBank
 15         print "%7.4f %s: I must leave"%(now(),self.name)
 16 
 17 def model():
 18     initialize()
 19     c1=Customer(name="Klaus")
 20     activate(c1,c1.visit(timeInBank=10.0),delay=5.0)
 21     c2=Customer(name="Tony")
 22     activate(c2,c2.visit(timeInBank=8.0),delay=2.0)
 23     c3=Customer(name="Evelyn")
 24     activate(c3,c3.visit(timeInBank=20.0),delay=12.0)
 25     simulate(until=400.0)
 26 
 27 model()

The trace produced by the program is shown in Example 4.2. Again the simulation finishes before the till=100.0 of the simulate call.

Example 4.2    The output from bank02.py.

 2.0000 Tony: Here I am
 5.0000 Klaus: Here I am
10.0000 Tony: I must leave
12.0000 Evelyn: Here I am
15.0000 Klaus: I must leave
32.0000 Evelyn: I must leave

4.1  Many non-random Customers

Another change will allow us to have multiple customers. As it is tedious to give a specially chosen name to each customer, we will instead call them Customer00, Customer01, .... We will also use a separate class to create and activate these customers. We will call this class a Source.

Example 4.3 shows the new program. Lines 6-15 show the Source class. There is the compulsory __init__method (lines 8-9) and a Python generator, here called generate (lines 11-15). This method has a couple of arguments, number and interval, the time between customer arrivals. It consists of a loop that creates a sequence of numbered Customers from 0 to number-1. Upon creation each is activated at the current simulation time (the final argument of the activate statement is missing). We also specify how long the customer is to stay in the bank. To keep it simple, all customers will stay exactly 12 minutes. When a new customer is activated, the Source holds for a fixed time (yield hold,self, interval) before creating the next one.

The Source is created in line 30 and activated at line 31 where the number of customers is set to 5 and the interval to 10.0. Once it starts, at time 0.0 it creates customers at intervals and each customer then operates independently of others.

Example 4.3    The bank model with several customers.
  1 #! /usr/local/bin/python
  2 """ Simulate several customers using a Source """
  3 from __future__ import generators
  4 from SimPy.Simulation  import *
  5 
  6 class Source(Process):
  7     """ Source generates customers regularly"""
  8     def __init__(self):
  9         Process.__init__(self)
 10 
 11     def generate(self,number,interval):       
 12         for i in range(number):
 13             c = Customer(name = "Customer%02d"%(i,))
 14             activate(c,c.visit(timeInBank=12.0))
 15             yield hold,self,interval
 16 
 17 class Customer(Process):
 18     """ Customer arrives, looks round and leaves """
 19     def __init__(self,name):
 20         Process.__init__(self)
 21         self.name = name
 22         
 23     def visit(self,timeInBank=0):       
 24         print "%7.4f %s: Here I am"%(now(),self.name)
 25         yield hold,self,timeInBank
 26         print "%7.4f %s: I must leave"%(now(),self.name)
 27 
 28 def model():
 29     initialize()
 30     source=Source()
 31     activate(source,source.generate(5,10.0),0.0)
 32     simulate(until=400.0)
 33 
 34 model()

The result of a run of this program is shown in Example 4.4.

Example 4.4    The output from bank03.py.

 0.0000 Customer00: Here I am
10.0000 Customer01: Here I am
12.0000 Customer00: I must leave
20.0000 Customer02: Here I am
22.0000 Customer01: I must leave
30.0000 Customer03: Here I am
32.0000 Customer02: I must leave
40.0000 Customer04: Here I am
42.0000 Customer03: I must leave
52.0000 Customer04: I must leave

4.2  Many Random Customers

We extend this model to allow our customers to arrive ``at random''. In simulation this is usually interpreted as meaning that the times between customer arrivals are distributed as exponential random variates. Thus there is little change in our program, shown in Example 4.5.

Example 4.5    The bank model with several customers who arrive randomly
  1 #! /usr/local/bin/python
  2 """ Simulate several customers arriving
  3 at random, using a Source
  4 """
  5 from __future__ import generators
  6 from SimPy.Simulation  import *
  7 from random import Random                      
  8 
  9 class Source(Process):
 10     """ Source generates customers randomly"""
 11     def __init__(self,seed=333):               
 12         Process.__init__(self)
 13         self.SEED = seed                       
 14 
 15     def generate(self,number,interval):       
 16         rv = Random(self.SEED)                 
 17         for i in range(number):
 18             c = Customer(name = "Customer%02d"%(i,))
 19             activate(c,c.visit(timeInBank=12.0))
 20             t = rv.expovariate(1.0/interval)   
 21             yield hold,self,t                  
 22 
 23 class Customer(Process):
 24     """ Customer arrives, looks round and leaves """
 25     def __init__(self,name):
 26         Process.__init__(self)
 27         self.name = name
 28         
 29     def visit(self,timeInBank=0):       
 30         print "%7.4f %s: Here I am"%(now(),self.name)
 31         yield hold,self,timeInBank
 32         print "%7.4f %s: I must leave"%(now(),self.name)
 33 
 34 def model():
 35     initialize()
 36     source=Source(seed = 1133)                 
 37     activate(source,source.generate(5,10.0),0.0) 
 38     simulate(until=400.0)
 39 
 40 model()

We import the random routines we need in line 7. I decided to set up the random number used to generate the customers as an attribute of the Source class, with its seed being supplied by the user when a Source is created (lines 11, 13, and 36). The random variate itself is created when the generate method is activated (line 16). The exponential random variate is generated in line 20 using interval as the mean of the distribution and used in line 21. This gives an exponential delay between two arrivals and hence pseudo-random arrivals, as specified.

A number of different ways of handling the random variables could have been chosen. For example, set up the random variable in the model routine and pass it to the Source as an argument either in the __init__  method or in the generate call on line 37. Or, somewhat less elegant, establish the random variable as a global object. The important factor is that if we wish to do serious comparisons of systems, we need control over the random variates and hence control over the seeds. Thus we must be able to run identical models with different seeds or different models with identical seeds. Either requires us to be able to provide the seeds as control parameters of the run. Here, of course it is just assigned in line 36 but it is clear it could have been read in or provided in a GUI form.

The result of a run of this program is shown in Example 4.6.

Example 4.6    The output from bank06.py.

 0.0000 Customer00: Here I am
 8.7314 Customer01: Here I am
12.0000 Customer00: I must leave
17.4985 Customer02: Here I am
20.7314 Customer01: I must leave
29.4985 Customer02: I must leave
34.3887 Customer03: Here I am
42.1872 Customer04: Here I am
46.3887 Customer03: I must leave
54.1872 Customer04: I must leave

5  A Counter

Now we extend the bank, and the activities of the customers by installing a counter with a clerk where banking takes place. So far, it has been more like an art gallery, the customers entering, looking around, and leaving. We need a object to represent the counter and SimPy provides a Resource class for this purpose. The actions of a Resource are simple: customers request a clerk, if she is free he gets service but others are blocked until the clerk becomes free again. This happens when the customer completes service and releases the clerk. If a customer requests service and the clerk is busy, the customer joins a queue until it is their turn to be served.

5.1  One Counter

The next model, shown in Example 5.1, is a development of the previous one with random arrivals but they need to use a single counter for a fixed time.

Example 5.1    The bank model with customers who arrive randomly to be served at a single counter.
  1 #! /usr/local/bin/python
  2 """ bank07.py: Simulate customers arriving
  3 at random, using a Source
  4 requesting service from a clerk.
  5 """
  6 from __future__ import generators
  7 from SimPy.Simulation  import *
  8 from random import Random
  9 
 10 class Source(Process):
 11     """ Source generates customers randomly"""
 12     def __init__(self,seed=333):
 13         Process.__init__(self)
 14         self.SEED = seed
 15 
 16     def generate(self,number,interval):       
 17         rv = Random(self.SEED)
 18         for i in range(number):
 19             c = Customer(name = "Customer%02d"%(i,))
 20             activate(c,c.visit(timeInBank=12.0))
 21             t = rv.expovariate(1.0/interval)
 22             yield hold,self,t
 23 
 24 class Customer(Process):
 25     """ Customer arrives, is served and  leaves """
 26     def __init__(self,name):
 27         Process.__init__(self)
 28         self.name = name
 29         
 30     def visit(self,timeInBank=0):       
 31         arrive=now()                            
 32         print "%7.4f %s: Here I am     "%(now(),self.name)
 33         yield request,self,counter              
 34         wait=now()-arrive                       
 35         print "%7.4f %s: Waited %6.3f"%(now(),self.name,wait)
 36         yield hold,self,timeInBank
 37         yield release,self,counter               
 38         print "%7.4f %s: Finished      "%(now(),self.name)
 39 
 40 def model():
 41     global counter                               
 42     counter = Resource(name="Karen")             
 43     initialize()
 44     source=Source(seed = 1133)
 45     activate(source,source.generate(5,10.0),0.0) 
 46     simulate(until=400.0)
 47 
 48 model()

The counter is created in lines 41-42. I have chosen to make this a global object so it can be referred to by the Customer class. One alternative would have been to create it globally outside model or to carry it as an argument of the Customer class.

The actions involving the counter are the yield statements in lines 33 and 37 where we request it and then, later release it.

To show the effect of the counter on the activities of the customers, I have added line 31 that records when the customer arrived and line 34 that records the time between arrival and getting the counter. Line 34 is after the yield request command and will be reached only when the request is satisfied. It is before the yield hold that corresponds to the service-time. Queueing may have taken place and wait will record how long the customer waited. This technique of saving the arrival time in a variable to record the time taken in systems is common in simulations. So the print statement also prints out how long the customer waited and we see that, except for the first customer (we still only have five customers, see line 45) all the customers have to wait. Example 5.2 displays the output.

Example 5.2    The output from bank07.py.

 0.0000 Customer00: Here I am     
 0.0000 Customer00: Waited  0.000
 8.7314 Customer01: Here I am     
12.0000 Customer00: Finished      
12.0000 Customer01: Waited  3.269
17.4985 Customer02: Here I am     
24.0000 Customer01: Finished      
24.0000 Customer02: Waited  6.501
34.3887 Customer03: Here I am     
36.0000 Customer02: Finished      
36.0000 Customer03: Waited  1.611
42.1872 Customer04: Here I am     
48.0000 Customer03: Finished      
48.0000 Customer04: Waited  5.813
60.0000 Customer04: Finished      

5.2  A random service time

We retain the one counter but bring in another source of variability: we make the service time a random variable as well as the inter-arrival time. As is traditional in the study of queues we first assume an exponential service time with a mean of timeInBank.

Example 5.3    The bank model with customers who arrive randomly to be served at a single counter taking a random time for service.
  1 #! /usr/local/bin/python
  2 """ bank08.py: Simulate customers arriving
  3     at random, using a Source requesting service
  4     from a clerk with a random servicetime
  5 """
  6 from __future__ import generators
  7 from SimPy.Simulation  import *
  8 from random import Random
  9 
 10 class Source(Process):
 11     """ Source generates customers randomly"""
 12     def __init__(self,seed=333):
 13         Process.__init__(self)
 14         self.SEED = seed
 15 
 16     def generate(self,number,interval):       
 17         rv = Random(self.SEED)
 18         for i in range(number):
 19             c = Customer(name = "Customer%02d"%(i,))
 20             activate(c,c.visit(timeInBank=12.0))
 21             t = rv.expovariate(1.0/interval)
 22             yield hold,self,t
 23 
 24 class Customer(Process):
 25     """ Customer arrives, is served and leaves """
 26     def __init__(self,name):
 27         Process.__init__(self)
 28         self.name = name
 29         
 30     def visit(self,timeInBank=0):       
 31         arrive=now()
 32         print "%7.4f %s: Here I am     "%(now(),self.name)
 33         yield request,self,counter
 34         wait=now()-arrive
 35         print "%7.4f %s: Waited %6.3f"%(now(),self.name,wait)
 36         tib = counterRV.expovariate(1.0/timeInBank)  
 37         yield hold,self,tib                          
 38         yield release,self,counter
 39         print "%7.4f %s: Finished      "%(now(),self.name)
 40 
 41 def model(counterseed=3939393):                      
 42     global counter,counterRV                         
 43     counter = Resource(name="Karen")
 44     counterRV = Random(counterseed)                  
 45     initialize()
 46     sourceseed = 1133
 47     source = Source(seed = sourceseed)
 48     activate(source,source.generate(5,10.0),0.0)
 49     simulate(until=400.0)
 50 
 51 model()

This time we will define the random variable to be used for the service time in the model function. We will set the seed for this random variable as an argument to model in anticipation of future use. The resulting program is shown in Example 5.3. The argument, counterseed is set up in line 41. The corresponding random variable, counterRV is created in line 44. I have declared it global (line 42) so we can use it in the Customer class without passing it as a parameter through the Source and the Customer calls.

It is used in lines 36-37 of the visit() method of Customer. We obtain a sample in line 36 and use it in line 37. Example 5.4 shows the trace from this program.

Example 5.4    The output from bank08.py.

 0.0000 Customer00: Here I am     
 0.0000 Customer00: Waited  0.000
 8.7314 Customer01: Here I am     
 8.9036 Customer00: Finished      
 8.9036 Customer01: Waited  0.172
17.4985 Customer02: Here I am     
30.5712 Customer01: Finished      
30.5712 Customer02: Waited 13.073
34.3887 Customer03: Here I am     
42.1872 Customer04: Here I am     
73.0871 Customer02: Finished      
73.0871 Customer03: Waited 38.698
83.9733 Customer03: Finished      
83.9733 Customer04: Waited 41.786
88.1023 Customer04: Finished      

This model with random arrivals and exponential service times is known as the M/M/1 queue and can be solved analytically.

6  Several Counters

When we introduce several counters we must decide the queue discipline. Are customers going to form a single queue or are they going to form separate queues in front of each counter? Then there are complications - will they be allowed to switch lanes (jockey)? We look first at a single-queue with several counters then at several isolated queues.

6.1  Several Counters but a Single Queue

Example 6.1    The bank model with customers who arrive randomly to be served at a group of counters taking a random time for service. A single queue is assumed.
  1 #! /usr/local/bin/python
  2 """ bank09.py: Simulate customers arriving
  3     at random, using a Source requesting service
  4     from several clerks but a single queue
  5     with a random servicetime
  6 """
  7 from __future__ import generators
  8 from SimPy.Simulation  import *
  9 from random import Random
 10 
 11 class Source(Process):
 12     """ Source generates customers randomly"""
 13     def __init__(self,seed=333):
 14         Process.__init__(self)
 15         self.SEED = seed
 16 
 17     def generate(self,number,interval):       
 18         rv = Random(self.SEED)
 19         for i in range(number):
 20             c = Customer(name = "Customer%02d"%(i,))
 21             activate(c,c.visit(timeInBank=12.0))
 22             t = rv.expovariate(1.0/interval)
 23             yield hold,self,t
 24 
 25 class Customer(Process):
 26     """ Customer arrives, is served and leaves """
 27     def __init__(self,name):
 28         Process.__init__(self)
 29         self.name = name
 30         
 31     def visit(self,timeInBank=0):       
 32         arrive=now()
 33         print "%7.4f %s: Here I am "%(now(),self.name)
 34         yield request,self,counter
 35         wait=now()-arrive
 36         print "%7.4f %s: Waited %6.3f"%(now(),self.name,wait)
 37         tib = counterRV.expovariate(1.0/timeInBank)
 38         yield hold,self,tib
 39         yield release,self,counter
 40         print "%7.4f %s: Finished"%(now(),self.name)
 41 
 42 def model(counterseed=3939393):
 43     global counter,counterRV
 44     counter = Resource(name="Clerk",capacity = 2) 
 45     counterRV = Random(counterseed)
 46     initialize()
 47     sourceseed = 1133
 48     source = Source(seed = sourceseed)
 49     activate(source,source.generate(5,10.0),0.0)
 50     simulate(until=400.0)
 51 
 52 model()

The only difference between this model and the single-server model is in line 44 (Example 6.1) I have increased the capacity of the counter resource to 2 and, because both clerks cannot be called ``Karen'' I have specified the name ``Clerk''. The waiting times, however, are very different (Example 6.2). The second server has cut the waiting times down severely.

Example 6.2    The output from bank09.py.

 0.0000 Customer00: Here I am 
 0.0000 Customer00: Waited  0.000
 8.7314 Customer01: Here I am 
 8.7314 Customer01: Waited  0.000
 8.9036 Customer00: Finished
17.4985 Customer02: Here I am 
17.4985 Customer02: Waited  0.000
30.3991 Customer01: Finished
34.3887 Customer03: Here I am 
34.3887 Customer03: Waited  0.000
42.1872 Customer04: Here I am 
45.2748 Customer03: Finished
45.2748 Customer04: Waited  3.088
49.4038 Customer04: Finished
60.0144 Customer02: Finished

6.2  Several Counters with individual queues

Each counter is now assumed to have its own queue. The obvious modelling technique therefore is to make each counter a separate resource. Then we have to decide how a customer will choose which queue to join. In practice, a customer will join the counter with the fewest customers. An alternative (and one that can be handled analytically) is to allow him to join a queue ''at random''. This may mean that one queue is long while another server is idle.

Here we allow the customer to choose the counter with the fewest customers. If there are more than one that satisfies this criterion he will choose the first. We need to find the number of customers at each counter. To make the programming clearer, I define a Python function, NoInSystem(R) (lines 25-28 in Example 6.3) which sums the number waiting and the number being served for a particular counter, R. This function is used in line 38 to get a list of the numbers at each counter. I have modified the trace printout, line 39 to display the state of the system when the customer arrives. It is then obvious which counter he will join. We then choose the shortest queue in lines 40-41 (the variable join). The remaining program is the same as before.

Example 6.3    The bank model with customers who arrive randomly to be served at two counters taking a random time for service. A customer chooses the shortest queue to join.
  1 #! /usr/local/bin/python
  2 """ bank10.py: Simulate customers arriving
  3     at random, using a Source, requesting service
  4     from two counters each with their own queue
  5     random servicetime
  6 """
  7 from __future__ import generators
  8 from SimPy.Simulation  import *
  9 from random import Random
 10 
 11 class Source(Process):
 12     """ Source generates customers randomly"""
 13     def __init__(self,seed=333):
 14         Process.__init__(self)
 15         self.SEED = seed
 16 
 17     def generate(self,number,interval):       
 18         rv = Random(self.SEED)
 19         for i in range(number):
 20             c = Customer(name = "Customer%02d"%(i,))
 21             activate(c,c.visit(timeInBank=12.0))
 22             t = rv.expovariate(1.0/interval)
 23             yield hold,self,t
 24 
 25 def NoInSystem(R):                                   
 26     """ The number of customers in the resource R
 27     in waitQ and active Q"""
 28     return (len(R.waitQ)+len(R.activeQ))             
 29 
 30 class Customer(Process):
 31     """ Customer arrives, is served and leaves """
 32     def __init__(self,name):
 33         Process.__init__(self)
 34         self.name = name
 35         
 36     def visit(self,timeInBank=0):       
 37         arrive=now()
 38         Qlength = [NoInSystem(counter[i]) for i in range(Nc)]              
 39         print "%7.4f %s: Here I am. %s   "%(now(),self.name,Qlength)       
 40         for i in range(Nc):                                                
 41             if Qlength[i] ==0 or Qlength[i]==min(Qlength): join =i ; break 
 42         yield request,self,counter[join]
 43         wait=now()-arrive
 44         print "%7.4f %s: Waited %6.3f"%(now(),self.name,wait)
 45         tib = counterRV.expovariate(1.0/timeInBank)
 46         yield hold,self,tib
 47         yield release,self,counter[join]
 48         print "%7.4f %s: Finished    "%(now(),self.name)
 49 
 50 def model(counterseed=3939393):
 51     global Nc,counter,counterRV
 52     Nc = 2
 53     counter = [Resource(name="Clerk0"),Resource(name="Clerk1")]
 54     counterRV = Random(counterseed)
 55     initialize()
 56     sourceseed = 1133
 57     source = Source(seed = sourceseed)
 58     activate(source,source.generate(5,10.0),0.0)
 59     simulate(until=400.0)
 60 
 61 model()

The results, displayed in Example 6.4, show how the customers choose the counter with the smallest number. For this sample, the fifth customer waits 17.827 minutes which is longer than the 3.088 minutes he waited in the previous 2-server model, There are, however, too few arrivals in these runs, limited to five customers, to draw any general conclusions about the relative efficiencies of the two systems.

Example 6.4    The output from bank10.py. The additional list in the trace when a customer arrives shows the number of customers at each of the two counters

 0.0000 Customer00: Here I am. [0, 0]   
 0.0000 Customer00: Waited  0.000
 8.7314 Customer01: Here I am. [1, 0]   
 8.7314 Customer01: Waited  0.000
 8.9036 Customer00: Finished    
17.4985 Customer02: Here I am. [0, 1]   
17.4985 Customer02: Waited  0.000
30.3991 Customer01: Finished    
34.3887 Customer03: Here I am. [1, 0]   
34.3887 Customer03: Waited  0.000
42.1872 Customer04: Here I am. [1, 1]   
45.2748 Customer03: Finished    
60.0144 Customer02: Finished    
60.0144 Customer04: Waited 17.827
64.1434 Customer04: Finished    

7  Monitors and Gathering Statistics

The traces of output that have been displayed so far are valuable for checking that the simulation is operating correctly but would become too much if we simulate a whole day. We do need to get results from our simulation to answer the original questions. What, then, is the best way to gather results?

One way is to analyze the traces elsewhere, piping the trace output, or a modified version of it, into a program such as R for statistical analysis, or into a file for later examination by a spreadsheet.

Another useful way of dealing with the results is to provide a graphical output. I do not have space to examine this thoroughly here (but see Section 8).

7.1  Monitors

SimPy offers an easy way to gather small quantities of statistics such as averages: the Monitor class. These maintain statistical totals for chosen variables.

To demonstrate the use of Monitors let us observe the average waiting times for our customers. In the program in Example 7.1 I have removed the trace statements because I will run the simulations for many more arrivals. In practice, I would make the printouts controlled by a variable, say, TRACE which is set in model(). This would aid in debugging but would not complicate the data analysis. However, I have not done this here.

Example 7.1    The bank model with customers who arrive randomly to be served at two counters taking a random time for service. A customer chooses the shortest queue to join. A Monitor variable records some statistics. The trace has been commented out.
  1 #! /usr/local/bin/python
  2 """ bank11.py: Simulate customers arriving
  3     at random, using a Source, requesting service
  4     from two counters each with their own queue
  5     random servicetime.
  6     Uses a Monitor object to record waiting times
  7 """
  8 from __future__ import generators
  9 from SimPy.Simulation  import *
 10 from SimPy.Monitor import *                 
 11 from random import Random
 12 
 13 class Source(Process):
 14     """ Source generates customers randomly"""
 15     def __init__(self,seed=333):
 16         Process.__init__(self)
 17         self.SEED = seed
 18 
 19     def generate(self,number,interval):       
 20         rv = Random(self.SEED)
 21         for i in range(number):
 22             c = Customer(name = "Customer%02d"%(i,))
 23             activate(c,c.visit(timeInBank=12.0))
 24             t = rv.expovariate(1.0/interval)
 25             yield hold,self,t
 26 
 27 def NoInSystem(R):
 28     """ The number of customers in the resource R
 29     in waitQ and active Q"""
 30     return (len(R.waitQ)+len(R.activeQ))
 31 
 32 class Customer(Process):
 33     """ Customer arrives, is served and leaves """
 34     def __init__(self,name):
 35         Process.__init__(self)
 36         self.name = name
 37         
 38     def visit(self,timeInBank=0):       
 39         arrive=now()
 40         Qlength = [NoInSystem(counter[i]) for i in range(Nc)]
 41         ##print "%7.4f %s: Here I am. %s   "%(now(),self.name,Qlength)
 42         for i in range(Nc):
 43             if Qlength[i] ==0 or Qlength[i]==min(Qlength): join =i ; break
 44         yield request,self,counter[join]
 45         wait=now()-arrive
 46         waitMonitor.tally(wait)                                 
 47         ##print "%7.4f %s: Waited %6.3f"%(now(),self.name,wait)
 48         tib = counterRV.expovariate(1.0/timeInBank)
 49         yield hold,self,tib
 50         yield release,self,counter[join]
 51         ##print "%7.4f %s: Finished    "%(now(),self.name)
 52 
 53 def model(counterseed=3939393):
 54     global Nc,counter,counterRV,waitMonitor                      
 55     Nc = 2
 56     counter = [Resource(name="Clerk0"),Resource(name="Clerk1")]
 57     counterRV = Random(counterseed)
 58     waitMonitor = Monitor()                                      
 59     initialize()
 60     sourceseed = 1133
 61     source = Source(seed = sourceseed)
 62     activate(source,source.generate(50,10.0),0.0)                
 63     simulate(until=2000.0)                                       
 64     print "Average wait for %4d was %6.2f"%(waitMonitor.count(), waitMonitor.mean()) 
 65 
 66 model()

The Monitor class is imported at line 10. It is separate from the rest of SimPy. The monitor waitMonitor is created in line 58 and declared to be global in line 54. It gathers statistics about waiting times in line 46 where it tallies the observed queueing times. At the end of the program, line 64, the simple results are printed. The trace statements have been commented out. In this run I have decided to run 50 customers (line 62) and have increased the until argument in line 63 to 2000.

Example 7.2    The output from bank11.py. The long trace has been eliminated.

Average wait for   50 was   5.18

The average waiting time of 5.18 minutes for 50 customers in a 2-counter, 2-queue system is more reliable than the times we were measuring before. This should be compared with the equivalent results of 4.44 minutes when there is a 2-counter, single-queue system. this difference is not very convincing. We should replicate the runs using different random number seeds.

7.2  Multiple runs

The advantage of the way the programs have been set up with the main part of the program in a model() routine will now become apparent. We will not need to make any changes to run a series of replications:

Example 7.3    The bank model with customers who arrive randomly to be served at two counters taking a random time for service. A customer chooses the shortest queue to join. A Monitor variable records some statistics. A number of replications are run using a list comprehension (program bank12.py).
  1 #! /usr/local/bin/python
  2 """ bank12.py: Simulate customers arriving
  3     at random, using a Source requesting service
  4     from two clerks but a single queue
  5     with a random servicetime
  6     Uses a Monitor object to record waiting times
  7     Set up 4 replications 
  8 """ 
  9 from __future__ import generators
 10 from SimPy.Simulation  import *
 11 from SimPy.Monitor import *
 12 from random import Random
 13 
 14 class Source(Process):
 15     """ Source generates customers randomly"""
 16     def __init__(self,seed=333):
 17         Process.__init__(self)
 18         self.SEED = seed
 19 
 20     def generate(self,number,interval):       
 21         rv = Random(self.SEED)
 22         for i in range(number):
 23             c = Customer(name = "Customer%02d"%(i,))
 24             activate(c,c.visit(timeInBank=12.0))
 25             t = rv.expovariate(1.0/interval)
 26             yield hold,self,t
 27 
 28 class Customer(Process):
 29     """ Customer arrives, is served and leaves """
 30     def __init__(self,name):
 31         Process.__init__(self)
 32         self.name = name
 33         
 34     def visit(self,timeInBank=0):       
 35         arrive=now()
 36         ##print "%7.4f %s: Here I am "%(now(),self.name)
 37         yield request,self,counter
 38         wait=now()-arrive
 39         waitMonitor.tally(wait)
 40         ##print "%7.4f %s: Waited %6.3f"%(now(),self.name,wait)
 41         tib = counterRV.expovariate(1.0/timeInBank)
 42         yield hold,self,tib
 43         yield release,self,counter
 44         ##print "%7.4f %s: Finished"%(now(),self.name)
 45 
 46 def model(counterseed=3939393):
 47     global counter,counterRV,waitMonitor
 48     counter = Resource(name="Clerk",capacity = 2)
 49     counterRV = Random(counterseed)
 50     waitMonitor = Monitor()
 51     initialize()
 52     sourceseed=1133
 53     source = Source(seed = sourceseed)
 54     activate(source,source.generate(50,10.0),0.0)
 55     simulate(until=2000.0)
 56     return waitMonitor.mean()
 57 
 58 result = [model(counterseed=cs) for cs in [13939393,31555999,777999555,319999771]] 
 59 for x in result: print "meanwait = %7.4f"%(x,)                                     

The only change is lines 58-59 where we run the model for four different random-number seeds to get a set of replications. The next line prints out the answers with a limited number of significant digits.

Example 7.4    The output from bank12.py. The mean waiting times from four independent runs.

meanwait =  0.4927
meanwait =  3.2158
meanwait =  3.2599
meanwait =  1.4707

The results show some variation. Remember that the system is only operating for 50 customers so it is possible the system may not be in steady-state.

8  Final Remarks

This introduction is too long and the examples seem to be getting longer. There is much more to say about simulation with SimPy but no space. I finish with a list of topics for other documents:

GUI input.
Graphical input of simulation parameters could be an advantage in many cases. SimPy allows this and programs have been developed (see program MM1.py in the examples in the SimPy distribution)
Graphical Output.
Similarly, graphical output of results can also be of value, not least in debugging simulation programs and checking for steady-state conditions.
Statistical Output.
The Monitor class is of some value in presenting results but more powerful methods of analysis are often needed. One solution is to output the trace and read hat into a large-scale statistical system such as R.
Interactions between processes.
Often the simple-minded Resource model is insufficient. For example a tug may be needed to carry out many tasks in a port model but demands on it may have to be queued. Here we can drop back to the older, Simula inspired, methods of process interaction. In these methods a process suspends itself after joining a waiting queue (modelled as a simple Python list) and is reactivated when the tug, say, is available.

9  Acknowledgments

I thank those developers and users of SimPy who have improved this document by sending their comments. I will be grateful for further corrections or suggestions. Could you send them to me: vignaux at users.sourceforge.net.


File translated from TEX by TTH, version 2.60.
On 20 Nov 2002, 16:55.