SimPy: Simulating Systems in Pythonby Klaus Müller and Tony Vignaux
Simulating complex real-world systems is now possible with SimPy , an open source simulation package. SimPy, originally developed by the authors of this article, has been developed to production quality by a small team of enthusiastic open sourcerers around the world. As far as we know, it is the only existing discrete event Python simulation package. Actually, it is one of a very small number of fully object oriented simulation systems. This article is written with the explicit goal of whetting the appetite of simulation newbies to play with this powerful problem solving technique and to get even more users and developers for SimPy.
Why consider simulation? Simulation is a powerful tool for studying communication and computer systems among many other application areas. It is used in studying systems where events--such as the arrival of a message--can occur at random and where resources are limited giving the possibility of congestion and queues.
To demonstrate using SimPy for simulation we will study a simple scenario. The hypothetical SimPyCo software firm has a call center with a helpdesk. The call center has a PABX with a limited number of lines and the helpdesk itself has staff to talk to customers. The runaway sucess of their YpMis product has overwhelmed the help desk resources. Many customers are unhappy because it takes so long to get through to a helper. SimPyCo management must decide on measures to increase the helpdesk capacity. Should they increase the number of staff, the number of lines, both? They decide to use simulation to estimate how changing the numbers of lines or staff would change customer delays. We will use a constant call rate for this example, but the rate will vary in the real world, with busy and quiet periods.
When customers call they may have to wait for a free line. When a line is obtained, they hear the phone ringing and then may have to wait again for a staff member to answer, listening to "comforting" music. When the call is answered they explain their problem and get some sort of result. This also takes time. We model this last period as a fixed known time but in a real simulation it would be uncertain, modelled as a random variable. When the call finishes, the staff member and the line both become free for the next call.
To model "quality of service", we label a customer "happy" or "unhappy" depending on wait time. We will not attempt to model the the quality of the advice he receives. We will record the number of happy and unhappy customers along with their delays.
Setting up a simulation requires modeling the system under study,
capturing the essential entities and their interactions, and then
writing code to make the model executable. The more descriptive a
simulation language is, the easier this task can be
accomplished. SimPy builds on the expressive power of Python, with its
wonderful OO components. Python, however, lacked the capability to
simulate parallel processes efficiently and platform-independently.
Threads are just too heavy and cannot be used to implement the large
number of independent entities in complex systems. Luckily Python 2.2
introduced generators with which one can construct lightweight
coprocesses, objects which can execute in quasi-parallel. (See the
Charming Python article, Iterators
and simple generators.) SimPy uses generators to implement entity
processes. Generators are indicated by the
Simulated entities in SimPy are objects of the
class. Each contains a method which is a generator and controls its
actions. For example, an entity might be a message, with a method
that describes its dynamic behaviour, its arrival, its waiting for
service by nodes of a computer network, and its final departure.
To cause a process to suffer a delay between events, such as the time taken by a computer node to serve a message, the message method would contain a command such as:
t is the time taken for it to be served. Of
course, this can be randomly generated. An example is given later.
Entities do not always control their own processing time since they also suffer delays in waiting caused by congestion and queues. Congestion occurs when several messages need to use the same link or the same computers.
SimPy handles this by modeling resources, that is, facilities that can handle only a few entities at a time; for example, a communication line that can only handle one message at a time. Entities that arrive when the resource is busy must wait until a unit is available. Like us, they wait in a queue and, usually, the first one in the queue is served when a unit becomes free. As in real life, different entities might have different priorities and might even preempt a previous entity. To request a unit of a resource such as a line the message method would contain a command such as:
It would then either be assigned a line immediately or wait until one is available. This queueing process is handled automatically. Once the message has acquired the line, it holds it until it is ready to release it using:
and the line becomes available for use by another waiting message.
This type of simulation can apply to various situations:
- emergency medical evacuations,
- design of cellphone channels,
- evaluation of error correction systems in communication.
The Helpdesk Model
Figure 1 -- a diagram of the entities in our helpdesk model
Figure 1 shows the entities in SimPyCo's real world: customers, the helpdesk, the PABX, and the staff. This can be modeled by an active customer entity, an active customer arrival process, a shared resource "PABX" with multiple lines for which customers may have to queue, and another shared resource, "Staff". The load on this system is the arrival rate, and the service rate depends on the customers' service (or time) needs, the number of PABX lines, and the number of staff. The observable system output to be established is the percentage of unhappy customers (depending on total waiting time and customers' tolerance level).
Mapping model entities to SimPy entities is easy (see the bottom of
Fig. 1): customers and customer traffic are programmed as subclasses
Process class, and the shared resources PABX
and Staff as instances of SimPy's
In the program listing, line 1 imports the generators (from the
__future__ until Python 2.3 arrives) and the statement
starting online 2 imports the necessary SimPy simulation
routines. (Usually we import
* rather than this long
list.) The following line imports the standard Python random number
routines, needed for random arrivals.
1 from __future__ import generators 2 from SimPy.Simulation import Process,Resource, \ 3 now,initialize,activate,simulate, \ 4 hold,request,release 5 from random import Random,expovariate 6
Our active elements are the
established as a SimPy
Process starting in line 7. We set
up a number of Class attributes to count the served customers and
those that are unhappy. The
Process must have an
__init__ method where we link the object to the SimPy
scheduling system (line 15). Each is given a unique identifying name
7 class Customer(Process): 8 """Represents customer requiring service""" 9 customers=0 10 customersServed=0 11 customersHappy=0 12 toomuch=0.25 13 14 def __init__(self): 15 Process.__init__(self) 16 Customer.customers += 1 17 self.id="Customer " + str(Customer.customers) 18 19 def happy(self,waitTime): 20 """Is customer happy after service?""" 21 if waitTime < Customer.toomuch: 22 return "happy" 23 else: 24 return "unhappy" 25 26 def servTime(self): 27 return 0.1 # hr 28
serviceNeed method describes the activities making
up the life of the
Customer (line 29). You can see that
this is a Python generator by the presence of the
statements. Each of these may cause a simulated delay to the
customer. There may or may not be a delay, depending on the state of
the system at the (simulated) time of the call. Note that whenever we
say "time" we mean simulation time, not real time.
Line 30 stores the current time--the arrival time--in the local
arrive, so that waiting time can be calculated
29 def serviceNeed(self): 30 arrive=now() 31 yield request,self,phone # get a helpdesk line 32 yield request,self,staff # get a staff member 33 atlast=now() 34 yield hold,self,self.servTime() # get service 35 yield release,self,staff # customer done; free staff member 36 yield release,self,phone # hang up line 37 Customer.customersServed += 1 38 waited=atlast-arrive 39 if self.happy(waited)=="happy" : Customer.customersHappy += 1 40 print "%s %s leaves. Total wait time=%s. Customer %s"\ 41 %(now(),self.id,waited,self.happy(waited)) 42
The customer requests a phone line in the first
statement (line 31). If one is available the next action is executed
without delay. Otherwise the customer is automatically queued with
any others waiting for a line. Here we assume a first-come
first-served queue. Next, the customer requests a staff member (line
32). He is queued again if none are available, though he still holds
the line. The interaction begins after the customer connects to a
staffer. This is modeled as a simple delay (line 34). Before talking
starts we record the current time in variable
Pages: 1, 2