.. image:: images/sm_SimPy_Logo.png :align: left ========================= SimPy Manual ========================= :Authors: - Tony Vignaux - Klaus Muller :SimPy version: 1.5.1 :Date: 2004-December-1 :Web-site: http://simpy.sourceforge.net/ :Python-Version: 2.2, 2.3, 2.4 .. contents:: Contents :depth: 3 .. .. sectnum:: .. :depth: 2 SimPy is an efficient, process-based, open-source simulation language using Python as a base. The facilities it offers are Processes_, Resources_, and Monitors_. This describes version 1.5.1 of *SimPy*. A note for users of SimPy Versions 1.4.x ---------------------------------------- The SimPy API provided by version 1.4.x is unchanged and programs written to it work under version 1.5.1 as before. SimPy 1.5 has added two `Advanced synchronization/scheduling capabilities`_, namely event signalling and a general "wait until condition" construct. They can be used by four new commands: - *yield waitevent,self,event(s)* - *yield queueevent,self,event(s)* - *event.signal(some_payload)* - *yield waituntil,self,condition* Introduction ------------------- *SimPy* is a Python-based discrete-event simulation system. It uses parallel processes to model active components such as messages, customers, trucks, planes. *SimPy* provides a number of facilities for the simulation programmer. They include *Processes*, *Resources*, and, importantly, ways of recording the histories of chosen variables in *Monitors*. *Processes* are the basic component of a *SimPy* simulation script. A Process models an active component (for example, a Truck, a Customer, or a Message) which may have to queue for scarce Resources, to work for fixed or random times, and to interact with other components. A *SimPy* script consists of the declaration of one or more *Process* classes and the instantiation of process objects from them. Each such process describes how the object behaves, elapses time, uses logic, and waits for Resources. In addition, Resources and Monitors may be defined and used. Before attempting to use SimPy, you should know how to write Python code. In particular, you should be able to use and define classes of objects. Python is free and available on most machine types. We do not introduce it here. 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 that if Python 2.2 is used, the following must be placed at the top of all *SimPy* scripts: **from __future__ import generators** Simulation with *SimPy* ------------------------- All discrete-event simulation programs automatically maintain the current simulation time in a software clock. In *SimPy* this can be accessed using the **now()** function. This is used in controlling the simulation and in producing printed traces of its operation. While a simulation program runs, time steps forward from one *event* to the next. An event occurs whenever the state of the simulated system changes. For example, an arrival of a customer is an event. So is a departure. To use the event scheduling mechanism of *SimPy* we must import the Simulation module: **from SimPy.Simulation import *** Before any *SimPy* simulation statements, such as defining processes or resources, are issued, the following statement must appear in the script: **initialize()** Then there will be some SimPy statements, creating and activating objects. Execution of the timing mechanism itself starts when the following statement appears in the script: **simulate(until=endtime)** The simulation then starts, the timer routine seeking the first scheduled event. The simulation will run until one of the following states: * there are no more events to execute (*now()* == the time of the last event) * the simulation time reaches *endtime* (*now() == endtime*) * the *stopSimulation()* command is executed (*now()* == the time when *stopSimulation()* was called). The simulation can be stopped at any time using the command: **stopSimulation()** which immediately stops the execution of the simulation. Further statements can still be executed after exit from *simulate*. The following partial script shows only the *main* block in a simulation program. *Arrivals* is a Process class (previously defined) and *p* is established as an object of that class. Activating *p* has the effect of scheduling at least one event The *simulate(until=1000.0)* starts the program start and it will immediately jump to that first event. It will continue until it runs out of events to execute or the simulation time reaches 1000.0:: initialize() p = Arrivals(mean) activate(p,p.execute(),at=0.0) simulate(until=1000.0) Report() # when the simulation finishes Processes ------------------- The active objects for discrete-event simulation in *SimPy* are of classes that inherit from class *Process*. For example, if we are simulating a messaging system we would model a message as a *Process*. A message arrives in a computing network; it makes transitions between nodes, waits for service at each one, and eventually leaves the system. The Message class describes these actions in an *execute* method. Individual messages are created as the program runs and they go through their modelled lifetimes. Defining a process ~~~~~~~~~~~~~~~~~~~~ A process is a class that that inherits from the class *Process*. For example here is the header of the definition of a new Message process class: * **class Message(Process):** The user must define a Process Execution Method (PEM) and may define an *__init__* method and any others. * **__init__(self,...)**, where *...* indicates method arguments. This function initializes the Process object, setting values for any attributes. The first line of this method must be a call to the Class *__init__()* in the form: **Process.__init__(self,name='a_process')** Then other commands can be used to initialize attributes of the object. The *__init__()* method is called automatically when a new message is created. In this example of an *__init__()* method for a *Message* class we give each new message an integer identification number, *i*, and message length, *len* as instance variables:: def __init__(self,i,len): Process.__init__(self,name='Message'+str(i)) self.i = i self.len = len If you do not wish to set any attributes (other than a *name*, the *__init__* method may be dispensed with. * **A process execution method (PEM)** This describes the actions of the process object and must contain at least one of the *yield* statements, described later, to make it a Python generator function. It can have arguments. Typically this can be called *execute()* or *run()* but any name may be chosen. The execution method starts when the process is activated and the *simulate(until=...)* statement has been called. In this example of the process execution method for the same *Message* class, the message prints out the current time, its identification number and the word 'Starting'. After a simulated delay it then announces it has 'Arrived':: def go(self): print now(), self.i, 'Starting' yield hold,self,100.0 print now(), self.i, 'Arrived' A Process must be *activated* in order to start it operating (see `Starting and stopping SimPy Processes`_) Following is a complete, runnable, SimPy script. We declare a *Message* class and define *__init__()* and *go()* methods for it. Two *messages*, *p1* and *p2* are created. We do not actually use the *len* attribute in this example. *p1* and *p2* are activated to start at simulation times 0.0 and 6.0, respectively. Nothing happens until the *simulate(until=200)* statement. When they have both finished (at time 6.0+100.0=106.0) there will be no more events so the simulation will stop at that time:: from __future__ import generators from SimPy.Simulation import * class Message(Process): """ a simple Process """ def __init__(self,i,len): Process.__init__(self,name='Message'+str(i)) self.i = i self.len = len def go(self): print now(), self.i, 'Starting' yield hold,self,100.0 print now(), self.i, 'Arrived' initialize() p1 = Message(1,203) activate(p1,p1.go()) p2 = Message(2,33) activate(p2,p2.go(),at=6.0) simulate(until=200) print now() # will print 106.0 Elapsing time in a Process ~~~~~~~~~~~~~~~~~~~~~~~~~~ An execution method can cause time to elapse for a process using the *yield hold* command: * **yield hold,self,t** causes the object to wait for a delay of *t* time units (unless it is interrupted). It then continues its operation with the next statement. During the hold the object is suspended. * **yield passivate,self** suspends the process's operations indefinitely. This example of an execution method (*buy*) for a *Customer* class demonstrates that the method can have arguments which can be used in the activation. The Customer also has an identification attribute *id*. The *yield hold* is executed 4 times:: def buy(self,budget=0): print 'Here I am at the shops ',self.id t = 5.0 for i in range(4): yield hold,self,t print 'I just bought something ',self.id budget -= 10.00 print 'All I have left is ', budget,\ ' I am going home ',self.id, initialize() C = Customer(1) activate(C,C.buy(budget=100),at=10.0) simulate(until=100.0) Starting and stopping SimPy Processes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Once a Process object has been created, it is 'passive', i.e., it has no event scheduled. It must be *activated* to start the process execution method: * **activate(p,p.PEM(args)[,at=t][,delay=period][,prior=boolean])** will activate the execution method *p.PEM()* of Process instance, *p* with arguments *args*. The default action is to activate at the current time, otherwise one of the optional timing clauses operate. If *prior* is true, the process will be activated before any others at the specified time in the event list. The process can be suspended and reactivated: * **yield passivate,self** suspends the process itself. It becomes 'passive'. * **reactivate(p,at=t,delay=period,prior=boolean)** will reactivate a passive process, *p*. It becomes 'active'. The optional timing clauses work as for *activate*. A process cannot reactivate itself. If that is required, use *yield hold,self, . . .* instead. * **self.cancel(p)** deletes all scheduled future events for process *p*. Only 'active' processes can be cancelled. A process cannot cancel itself. If that is required, use *yield passivate,self* instead. *Note:* This new format replaces the *p.cancel()* form of earlier SimPy versions. When all statements in a process execution method have been completed, a process becomes 'terminated'. If the instance is still referenced, it becomes just a data container. Otherwise, it is automatically destroyed. And, finally, * **stopSimulation()** stops all simulation activity, even if some processes still have events scheduled. Asynchronous interruptions ~~~~~~~~~~~~~~~~~~~~~~~~~~ One process (the *interrupter*) can interrupt another, active, process (the *victim*). A process cannot interrupt itself. * **self.interrupt(victim)** The interrupt is just a signal. After this statement, the *interrupter* continues its current method. The *victim* must be *active*. An *active* process is one that has an event scheduled for it (that is, it is 'executing' a *yield hold,self,t*). If the *victim* is not active (that is it is either *passive* or *terminated*) the interrupt has no effect on it. As processes queuing for resources are *passive*, they cannot be interrupted. *Active* processes which have acquired a resource can be interrupted. If interrupted, the *victim* returns from its *yield hold* prematurely. It can sense if it has been interrupted by calling * **self.interrupted()** which returns *True* if it has been interrupted. It can then either continue in the current activity or switch to an alternative, making sure it tidies up the current state, such as releasing any resources it owns. When this is True: * **self.interruptCause** is a reference to the *interrupter* instance. * **self.interruptLeft** gives the time remaining in the interrupted *yield hold,* The interruption is reset at the *victim's* next call to a *yield hold,*. It can also be reset by calling * **self.interruptReset()** Here is an example of a simulation with interrupts. A bus is subject to breakdowns which are modelled as interruptions. Notice that in the first *yield hold*, interrupts may occur, so a reaction to the interrupt (= repair) has been programmed. The bus Process here does not require an __init__ method:: class Bus(Process): def operate(self,repairduration,triplength): # process execution method (PEM) tripleft = triplength while tripleft > 0: yield hold,self,tripleft # try to get through trip if self.interrupted(): print self.interruptCause.name, "at %s" %now() # breakdown tripleft=self.interruptLeft # yes; time to drive self.interruptReset() # end interrupt state reactivate(br,delay=repairduration) # delay any breakdowns yield hold,self,repairduration print "Bus repaired at %s" %now() else: break # no breakdown, bus arrived print "Bus has arrived at %s" %now() class Breakdown(Process): def __init__(self,myBus): Process.__init__(self,name="Breakdown "+myBus.name) self.bus=myBus def breakBus(self,interval): # process execution method while True: yield hold,self,interval if self.bus.terminated(): break self.interrupt(self.bus) initialize() b=Bus("Bus") activate(b,b.operate(repairduration=20,triplength=1000)) br=Breakdown(b) activate(br,br.breakBus(300)) print simulate(until=4000) The ouput from this example:: Breakdown Bus at 300 Bus repaired at 320 Breakdown Bus at 620 Bus repaired at 640 Breakdown Bus at 940 Bus repaired at 960 Bus has arrived at 1060 SimPy: No more events at time 1260 Where interrupts can occur, the process which may be the victim of interrupts must test for interrupt occurrence after every "yield hold" and react to it. If a process holds a resource when it gets interrupted, it continues holding the resource. Starting the simulation ~~~~~~~~~~~~~~~~~~~~~~~ Even activated processes will not start until the following statement has been executed: * **simulate(until=T)** starts the simulation going and it will continue until time *T* unless it runs out of events to execute or the command *stopSimulation()* is executed. .. ------example------------- Three messages are created and activated to start at different times. Message *m1*, length 3, is scheduled to start immediately . The absence of a time in the *schedule* arguments implies it should start now, at the current time. Message *m2* is of length 40 and will start at simulation time 100.0. Message *m3* is of length 50 and will start 100.0 time units after the current time. Finally, the simulation is started by the call of *simulate()*:: initialize() m1 = Message(1,30) m2 = Message(2,40) m3= Message(3,50) activate(m1,m1.execute()) activate(m1,m1.execute(),at=100.0) activate(m1,m1.execute(),delay=100.0) simulate(until=1000) .. ----------------------------------------------------------------------- A complete *SimPy* script ~~~~~~~~~~~~~~~~~~~~~~~~~~ This complete runnable script simulates a firework with a time fuse. I have put in a few extra *yield hold* commands for added suspense:: from __future__ import generators from SimPy.Simulation import * class Firework(Process): def execute(self): print now(), ' firework activated' yield hold,self, 10.0 for i in range(10): yield hold,self,1.0 print now(), ' tick' yield hold,self,10.0 print now(), ' Boom!!' initialize() f = Firework() activate(f,f.execute(),at=0.0) simulate(until=100) The output from Example . No formatting of the output was attempted so it looks a bit ragged:: 0.0 firework activated 11.0 tick 12.0 tick 13.0 tick 14.0 tick 15.0 tick 16.0 tick 17.0 tick 18.0 tick 19.0 tick 20.0 tick 30.0 Boom!! A source fragment ~~~~~~~~~~~~~~~~~~~ One useful program pattern is the *source*. This is an process with an execution method that generates events or activates other processes as a sequence -- it is a source of other processes. Random arrivals can be modelled using random (exponential) intervals between activations. The following example is of a source which activates a series of *customers* to arrive at regular intervals of 10.0 units of time. The sequence continues until the simulation time exceeds the specified *finishTime*. (Of course, to achieve *random'' arrivals of *customer*s the *yield hold* method should use an *exponential* random variate instead of, as here, a constant 10.0 value) The example assumes that the *Customer* class has been defined with a PEM called *run*:: class Source(Process): def __init__(self,finish): Process.__init__(self) self.finishTime = finish def execute(self): while now() < self.finishTime: c = Customer() ## new customer activate(c,c.run()) ## activate it now print now(), ' customer' yield hold,self,10.0 initialize() g = Source(33.0) activate(g,g.execute(),at=0.0) ## start the source simulate(until=100) .. ------example------------- Resources ------------------- A *resource* models a congestion point where there may be queueing. For example in a manufacturing plant, a *Task* (modelled as a *process*) needs work done at a *Machine* (modelled as a *resource*). If a *Machine* unit is not available, the *Task* will have to wait until one becomes free. The *Task* will then have the use of it for however long it needs. It is not available for other *Tasks* until released. These actions are all automatically taken care of by the *SimPy* *resource*. A resource can have a number of identical *units*. So there may be a number of identical *Machine* units. A process gets service by *requesting* a unit of the resource and, when it is finished, *releasing* it. A resource maintains a queue of waiting processes and another list of processes using it. These are defined and updated automatically. A Resource is established by the following statement: * **r=Resource(capacity=1, name='a_resource', unitName='units', qType=FIFO, preemptable=0, monitored=False)** - *capacity* is the number of identical units of the resource available. - *name* is the name by which the resource is known (eg *gasStation*) - *unitName* is the name of a unit of the resource (eg *pump*) - *qType* describes the queue discipline of the waiting queue of processes; typically, this is *FIFO* (First-in, First-out). and this is the presumed value. An alternative is *PriorityQ* - *preemptable* indicates, if it has a non-zero value, that a process being put into the *PriorityQ* may also pre-empt a lower-priority process already using a unit of the resource. This only has an effect when *qType == PriorityQ* - *monitored* indicates if the number of processes in the resource's queues (see below) are to be monitored (see Monitors_, below) A Resource, **r**, has the following attributes: - **r.n** The number of units that are currently free. - **r.waitQ** A waiting queue (list) of processes (FIFO by default) The number of Processes waiting is **len(r.waitQ)** - **r.activeQ** A queue (list) of processes holding units. The number of Processes in the active queue is **len(r.activeQ)** - **r.waitMon** A Monitor recording the number in *r.waitQ* - **r.actMon** A Monitor recording the number in *r.activeQ* A process can request and release a unit of resource, *r* using the following yield commands: * **yield request,self,r** requests a unit of resource, *r*. The process may be temporarily queued and suspended until one is available. If, or when, a unit is free, the requesting process will take one and continue its execution. The resource will record that the process is using a unit (that is, the process will be listed in *r.activeQ*) If one is not free , the the process will be automatically placed in the resource's waiting queue, *r.waitQ*, and suspended. When a unit eventually becomes available, the first process in the waiting queue, taking account of the priority order, will be allowed to take it. That process is then reactivated. If the resource has been defined as being a *priorityQ* with *preemption == 1* then the requesting process can pre-empt a lower-priority process already using a unit. (see `Requesting a resource with preemptive priority`_, below) * **yield release,self,r** releases the unit of *r*. This may have the side-effect of allocating the released unit to the next process in the Resource's waiting queue. In this example, the current Process requests and, if necessary waits for, a unit of a Resource, *r*. On acquisition it holds it while it pauses for a random time (exponentially distributed, mean 20.0) and then releases it again:: yield request,self,r yield hold,self,g.expovariate(1.0/20.0) yield release,self,r Requesting resources with priority ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If a Resource, *r* is defined with *priority* queueing (that is *qType==PriorityQ*) a request can be made for a unit by: * **yield request,self,r,priority** requests a unit with priority. *priority* is real or integer. Larger values of *priority* represent higher priorities and these will go to the head of the *r.waitQ* if there not enough units immediately. An example of a complete script where priorities are used. Four clients with different priorities request a resource unit from a server at the same time. They get the resource in the order set by their relative priorities:: from __future__ import generators from SimPy.Simulation import * class Client(Process): inClients=[] outClients=[] def __init__(self,name): Process.__init__(self,name) def getserved(self,servtime,priority,myServer): Client.inClients.append(self.name) print self.name, 'requests 1 unit at t=',now() yield request, self, myServer, priority yield hold, self, servtime yield release, self,myServer print self.name,'done at t=',now() Client.outClients.append(self.name) initialize() server=Resource(capacity=1,qType=PriorityQ) c1=Client(name='c1') ; c2=Client(name='c2') c3=Client(name='c3') ; c4=Client(name='c4') activate(c1,c1.getserved(servtime=100,priority=1,myServer=server)) activate(c2,c2.getserved(servtime=100,priority=2,myServer=server)) activate(c3,c3.getserved(servtime=100,priority=3,myServer=server)) activate(c4,c4.getserved(servtime=100,priority=4,myServer=server)) simulate(until=500) print 'Request order: ',Client.inClients print 'Service order: ',Client.outClients This program results in the following output:: c1 requests 1 unit at t= 0 c2 requests 1 unit at t= 0 c3 requests 1 unit at t= 0 c4 requests 1 unit at t= 0 c1 done at t= 100 c4 done at t= 200 c3 done at t= 300 c2 done at t= 400 Request order: ['c1', 'c2', 'c3', 'c4'] Service order: ['c1', 'c4', 'c3', 'c2'] .. ------example------------- Although *c1* has the lowest priority, it requests and gets the resource unit first. When it completes, *c4* has the highest priority of all waiting processes and gets the resource next, etc. Note that there is no preemption of processes being served. Requesting a resource with preemptive priority ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In some models, higher priority processes can preempt lower priority processes when all resource units have been allocated. A resource with preemption can be created by setting arguments *qType==PriorityQ* and *preemptable* non-zero. When a process requests a unit of resource and all units are in use it can preempt a lower priority process holding a resource unit. If there are several processes already active (that is, in the *activeQ*), the one with the lowest priority is suspended, put at the front of the *waitQ* and the preempting process gets its resource unit and is put into the *activeQ*. The preempted process is the next one to get a resource unit (unless another preemption occurs). The time for which the preempted process had the resource unit is taken into account when the process gets into the *activeQ* again. Thus, the total hold time is always the same, regardless of whether or not a process gets preempted. An example of a complete script. Two clients of different priority compete for the same resource unit:: from __future__ import generators from SimPy.Simulation import * class Client(Process): def __init__(self,name): Process.__init__(self,name) def getserved(self,servtime,priority,myServer): print self.name, 'requests 1 unit at t=',now() yield request, self, myServer, priority yield hold, self, servtime yield release, self,myServer print self.name,'done at t=',now() initialize() server=Resource(capacity=1,qType=PriorityQ,preemptable=1) c1=Client(name='c1') c2=Client(name='c2') activate(c1,c1.getserved(servtime=100,priority=1,myServer=server),at=0) activate(c2,c2.getserved(servtime=100,priority=9,myServer=server),at=50) simulate(until=500) The output from this program is:: c1 requests 1 unit at t= 0 c2 requests 1 unit at t= 50 c2 done at t= 150 c1 done at t= 200 Here, *c2* preempted *c1* at *t=50*. At that time, *c1* had held the resource for 50 of the total of 100 time units. *c1* got the resource back when *c2* completed at *t=150*. Monitoring a resource ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If *monitored* is set *True* for a resource, *r*, the length of the waiting queue, *len(r.waitQ)* and the active queue,*len(r.activeQ)* are Both monitored automatically (see Monitors_, below). This solves a problem, particularly for the waiting queue which cannot be monitored externally to the resource. The monitors are called *r.waitMon* and *r.actMon*, respectively. Complete time series for both queue lengths are maintained so that statistics, such as the time average can be found. In this example, the resource, *server* is monitored and the time-average of each is calculated:: from SimPy.Simulation import * class Client(Process): inClients=[] outClients=[] def __init__(self,name): Process.__init__(self,name) def getserved(self,servtime,myServer): print self.name, 'requests 1 unit at t=',now() yield request, self, myServer yield hold, self, servtime yield release, self,myServer print self.name,'done at t=',now() initialize() server=Resource(capacity=1,monitored=True) c1=Client(name='c1') ; c2=Client(name='c2') c3=Client(name='c3') ; c4=Client(name='c4') activate(c1,c1.getserved(servtime=100,myServer=server)) activate(c2,c2.getserved(servtime=100,myServer=server)) activate(c3,c3.getserved(servtime=100,myServer=server)) activate(c4,c4.getserved(servtime=100,myServer=server)) simulate(until=500) print 'Average waiting',server.waitMon.timeAverage() print 'Average in service',server.actMon.timeAverage() The output from this program is:: c1 requests 1 unit at t= 0 c2 requests 1 unit at t= 0 c3 requests 1 unit at t= 0 c4 requests 1 unit at t= 0 c1 done at t= 100 c2 done at t= 200 c3 done at t= 300 c4 done at t= 400 Average waiting 1.5 Average in service 1.0 .. --------------------------------------------------------------------- Random Number Generation ------------------------- Simulation usually needs pseudo-random numbers. *SimPy* uses the standard *Python* *random* module. Its documentation should be consulted for details. We can have multiple random streams, as in Simscript and ModSim. One *imports* the Class and methods needed: * **from random import Random** You must define a random variable object using: * **g = Random([seed])** sets up the random variable object *g* using *seed* to initialize the sequence. For example, *g= Random(111333)* sets up the random variable object *g* and initializes its seed to *111333*. A good range of distributions is available. For example: * **g.random()** returns the next random floating point number in the range [0.0, 1.0). * **g.expovariate(lambd)** returns a sample from the exponential distribution. *lambd* is 1.0 divided by the desired mean. (The parameter would be called *lambda*, but that is a reserved word in Python.) Returned values range from 0 to positive infinity. * **g.normalvariate(mu,sigma)** returns a sample from the normal distribution. *mu* is the mean, and *sigma* is the standard deviation. This example uses exponential and normal random variables. The *random* object, *g* is initialized with its initial seed set to 333555. *X* and *Y* are pseudo-random variates from the two distributions using the object *g*:: from random import Random g = Random(333555) X = g.expovariate(10.0) Y = g.normalvariate(100.0, 5.0) Advanced synchronization/scheduling capabilities -------------------------------------------------------------------------- (*SimPy* 1.5 and beyond) All scheduling constructs discussed so far are either time-based, i.e., they make processes wait until a certain time has passed, or use direct reactivation of processes. For a wide range of models, these constructs are totally satisfactory and sufficient. In some modeling situations, the *SimPy* scheduling constructs are too rich or too generic and could be replaced by simpler, safer constructs. *SimPy* 1.5 has introduced synchronization by events and signals as one such possible construct. On the other side, there are models which require synchronization/scheduling by other than time-related wait conditions. *SimPy* has introduced a general "wait until" to support clean implementation of such models. Signalling between processes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Event signalling is particularly useful in situations where processes must wait for completion of activities of unknown duration. This situation is often encountered, e.g. when modeling real time systems or operating systems. Events in *SimPy* are implemented by class **SimEvent**. This name was chosen because the term 'event' is already being used in Python for e.g. tkinter events or in Python's standard library module *signal -- Set handlers for asynchronous events*. An instance of a SimEvent is generated by something like **myEvent=SimEvent("MyEvent")**. Associated with a SimEvent are - a boolean **occurred** to show whether an event has happened (has been signalled) - a list **waits**, implementing a set of processes waiting for the event - a list **queues**, implementing a FIFO queue of processes queueing for the event - an attribute **signalparam** to receive an (optional) payload from the **signal** method Processes can *wait* for events by issuing: **yield waitevent,self,** can be: - an event variable, e.g. *myEvent*) - a tuple of events, e.g. *(myEvent,myOtherEvent,TimeOut)*, or - a list of events, e.g. *[myEvent,myOtherEvent,TimeOut]* If one of the events in ** has already happened, the process contines. The *occurred* flag of the event(s) is toggled to False. If none of the events in the ** has happened, the process is passivated after joining the set of processes waiting for all the events. Processes can *queue* for events by issuing: **yield queueevent,self,** (with as defined above) If one of the events in ** has already happened, the process contines. The *occurred* flag of the event(s) is toggled to False. If none of the events in the ** has happened, the process is passivated after joining the FIFO queue of processes queuing for all the events. The ocurrence of an event is signalled by: **.signal()** The ** is optional. It can be of any Python type. When issued, *signal* causes the *occurred* flag of the event to be toggled to True, if waiting set and and queue are empty. Otherwise, all processes in the event's *waits* list are reactivated at the current time, as well as the first process in its *queues* FIFO queue. Here is a small, complete *SimPy* script illustrating the new constructs:: from __future__ import generators from SimPy.Simulation import * class Waiter(Process): def waiting(self,myEvent): yield waitevent,self,myEvent print "%s: after waiting, event %s has happened"%(now(),myEvent.name) class Queuer(Process): def queueing(self,myEvent): yield queueevent,self,myEvent print "%s: after queueing, event %s has happened"%(now(),myEvent.name) print " just checking: event(s) %s fired"%([x.name for x in self.eventsFired]) class Signaller(Process): def sendSignals(self): yield hold,self,1 event1.signal() yield hold,self,1 event2.signal() yield hold,self,1 event1.signal() event2.signal() initialize() event1=SimEvent("event1"); event2=SimEvent("event2") s=Signaller(); activate(s,s.sendSignals()) w0=Waiter(); activate(w0,w0.waiting(event1)) w1=Waiter(); activate(w1,w1.waiting(event1)) w2=Waiter(); activate(w2,w2.waiting(event2)) q1=Queuer(); activate(q1,q1.queueing(event1)) q2=Queuer(); activate(q2,q2.queueing(event1)) simulate(until=10) When run, this produces:: 1: after waiting, event event1 has happened 1: after waiting, event event1 has happened 1: after queueing, event event1 has happened just checking: event(s) ['event1'] fired 2: after waiting, event event2 has happened 3: after queueing, event event1 has happened just checking: event(s) ['event1'] fired When *event1* fired at time 1, two processes (*w0* and *w1*)were waiting for it and both got reactivated. Two proceses were queueing for it(*q1* and *q2*), but only one got reactivated. The second queueing process got reactivated when event1 fired again. The 'just checking' line reflects the content of the process' *self.eventsFired* attribute. "wait until" synchronization -- waiting for any condition ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Simulation models where progress of a process depends on a general condition involving non-timerelated state-variables (such as "goodWeather OR (nrCustomers>50 AND price<22.50") are difficult to implement with *SimPy* constructs prior to version 1.5. They require *interrogative* scheduling, while all other *SimPy* synchronization constructs are *imperative*: after every *SimPy* event, the condition must be tested until it becomes True. Effectively, a new (hidden, system) process has to interrogate the value of the condition. Clearly, this is less runtime-efficient than the event-list scheduling used for the other *SimPy* constructs. The *SimPy* 1.5.1 implementation therefore only activates that interrogation process when there is a process waiting for a condition. When this is not the case, the runtime overhead is minimal (about 1 procent extra runtime). The new construct takes the form: **yield waituntil,self,** ** is a reference to a function without parameters which returns the state of condition to be waited for as a boolean value. Here is a simple program showing the use of *yield waituntil*:: from SimPy.Simulation import * import random class Player(Process): def __init__(self,lives=1): Process.__init__(self) self.lives=lives self.damage=0 def life(self): self.message="I survived alien attack!" def killed(): return self.damage>5 while True: yield waituntil,self,killed self.lives-=1; self.damage=0 if self.lives==0: self.message= "I was wiped out by alien at time %s!"%now() stopSimulation() class Alien(Process): def fight(self): while True: if random.randint(0,10)<2: #simulate firing target.damage+=1 #hit target yield hold,self,1 initialize() gameOver=100 target=Player(lives=3); activate(target,target.life()) shooter=Alien(); activate(shooter,shooter.fight()) simulate(until=gameOver) print target.message In summary, the "wait until" construct is the most powerful synchronization construct. It effectively generalizes all other SimPy synchronization constructs, i.e., it could replace all of them (but at a runtime cost). Monitors ----------------------------- A Monitor, a subclass of list, records a series of observed data values, *y*, and associated times, *t*. Simple averages can then be calculated from the series. Each Monitor observes one series of data values. For example we might use one Monitor to record the waiting times for customers and another to record the total number of customers in the shop. Because SimPy is a discrete-event system, the number of customers changes only at events and it is these that are recorded. Monitors are not intended as a substitute for real statistical analysis but they have proved useful in developing simulations in SimPy. Monitors are included in the Simulation module of the SimPy package. (In versions of SimPy 1.3 and earlier, the Monitor module was separate from the Simulation module and had to be imported independently. Previous programs still work as long as the *from SimPy.Monitor import Monitor* occurs after the importation from SimPy.Simulation) To define a new Monitor object: * **m=Monitor(name='', ylab='y', tlab='t')**, where *name* is the name of the monitor object, set to an empty string if it is missing. *ylab* and *tlab* are provided as labels for plotting graphs from the data held in the Monitor. Methods of the Monitor class include: * **m.observe(y [,t])** records the current value of the variable, *y* and time *t* (the current time, *now()*, if *t* is missing). * **m.reset([t])** resets the observations. The recorded time series is set to the empty list, *[]* and the starting time to *t* or, if it is missing, to the current simulation time, *now()*. the Monitor object, **m**, holds the recorded data as a list of data pairs. Each pair, *[t,y]*, records the time and the value of one observation. Simple data summaries can be obtained from such a *Monitor* object: * **m[i]** holds the **i** th observation as a list, *[ti, yi]* * **m.yseries()** a list of the recorded data values. * **m.tseries()** a list of the recorded times. * **m.total()** the sum of the *y* values * **m.count()** the current number of observations. This is the same as *len(m)*. * **m.mean()** the simple average of the observations (see the left-hand picture below) If there are no observations, the message: 'SimPy: No observations for mean' is printed. This is illustrated in the figure. .. figure:: images/Mon001.png :scale: 60 :alt: Standard mean value * **m.var()** the sample variance of the observations. (multiply by *n/(n-1)*, where *n=m.count()* to get an estimate of the population variance)If there are no observations, the message: 'SimPy: No observations for sample variance' is printed. * **m.timeAverage([t])** the time-average of the integrated *y* values, calculated from time 0 (or the last time *m.reset([t])* was called) to time *t* (the current simulation time if *t* is missing). It is assumed that *y* is continuous in time but changes in steps at the times that *observe(y)* is called. In calculating the time-average the area under the graph is calculated as shown in the right-hand figure below. If there are no observations, the message 'SimPy: No observations for timeAverage'. If no time has elapsed, the message 'SimPy: No elapsed time for timeAverage' is printed. .. figure:: images/Mon003.png :scale: 60 :alt: Time Average * **m.histogram(low=0.0,high=100.0,nbins=10)** is a *histogram* object (a derived class of *list*) which contains the number of *y* values in each of its bins. It is calculated from the monitored *y* values. A *histogram* can be graphed using the *plotHistogram* method in the `SimPlot`_ package. - *low* is the lowest value of the histogram - *high* is the highest value of the histogram - *nbins* is the number of bins beween *low* and *high* into which the histogram is to be divided. The number of *y* values in each of the divisions is counted into the appropriate bin. Another 2 bins are constructed, counting the number of *y* values *under* the *low* value and the number *over* the *high* value. There are *nbins+2* counts altogether. .. figure:: images/Mon002.png :scale: 60 :alt: Standard mean value * **m.__str__()** is a string that briefly describes the current state of the monitor. This can be used in a print statement. Note: The following methods are retained for backwards compatibility but are not recommended. They may be removed in future releases of SimPy. * **m.tally(y)** records the current value of *y* and the current time, *now()*. * **m.accum(y [,t])** records the current value of *y* and time *t* (the current time, *now()*, if *t* is missing). In this example we establish a Monitor to estimate the mean and variance of 1000 observations of an exponential random variate. A histogram with 30 bins (plus an *under* and an *over* count is also returned.:: from SimPy.Simulation import * from random import Random M = Monitor() g = Random() for i in range(1000): y = g.expovariate(0.1) M.observe(y) print 'mean= ',M.mean(), 'var= ',M.var() h = M.histogram(low=0.0, high=20, nbins=30) In this example, the number in the system, recorded as *N*, is being monitored to estimate the average number in the system (This example is only fragmentary):: from SimPy.Simulation import * M = Monitor() ... # upon an arrival of a job, increment N # the time used is now() N = N +1 M.observe(N) ... ... # upon a departure of a job N = N -1 M.observe(N) print 'mean= ',M.timeAverage() .. ------------------------------------------------------------------------- Other Links ------------------- Several `SimPy models`_ are included with the SimPy code distribution. .. _`SimPy models`: LISTOFMODELS.html Klaus Muller and Tony Vignaux, *SimPy: Simulating Systems in Python*, O'Reilly ONLamp.com, 2003-Feb-27, http://www.onlamp.com/pub/a/python/2003/02/27/simpy.html Norman Matloff, *Introduction to the SimPy Discrete-Event Simulation Package*, U Cal: Davis, 2003, http://heather.cs.ucdavis.edu/~matloff/simpy.html David Mertz, *Charming Python: SimPy simplifies complex models*, IBM Developer Works, Dec 2002, http://www-106.ibm.com/developerworks/linux/library/l-simpy.html Acknowledgments ------------------- We will be grateful for any corrections or suggestions for improvements to the document. Appendices ------------- A1. SimPy Error Messages ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Advisory messages +++++++++++++++++++ These messages are returned by *simulate()*, as in *message=simulate(until=123)*. Upon a normal end of a simulation, *simulate()* returns the message: - **SimPy: Normal exit**. This means that no errors have occurred and the simulation has run to the time specified by the *until* parameter. The following messages, returned by *simulate()*, are produced at a premature termination of the simulation but allow continuation of the program. - **SimPy: No more events at time x**. All processes were completed prior to the endtime given in *simulate(until=endtime)*. - **SimPy: No activities scheduled**. No activities were scheduled when *simulate()* was called. Fatal error messages ++++++++++++++++++++++ These messages are generated when SimPy-related fatal exceptions occur. They end the SimPy program. Fatal SimPy error messages are output to *sysout*. - **Fatal SimPy error: activating function which is not a generator (contains no 'yield')**. A process tried to (re)activate a function which is not a SimPy process (=Python generator). SimPy processes must contain at least one *yield . . .* statement. - **Fatal SimPy error: Simulation not initialized**. The SimPy program called *simulate()* before calling *initialize()*. 'Monitor' error messages ++++++++++++++++++++++++++ - **SimPy: No observations for mean**. No observations were made by the monitor before attempting to calculate the mean. - **SimPy: No observations for sample variance**. No observations were made by the monitor before attempting to calculate the sample variance. - **SimPy: No observations for timeAverage**, No observations were made by the monitor before attempting to calculate the time-average. - **SimPy: No elapsed time for timeAverage**. No simulation time has elapsed before attempting to calculate the time-average. A2. SimPy Process States ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From the point of the model builder, at any time, a SimPy process, *p*, can be in one of the following states: - **Active**: Waiting for a scheduled event. This state simulates an activity in the model. Simulated time passes in this state. The process state *p.active()* returns *True*. - **Passive**: Not active or terminated. Awaiting *(re-)activation* by another process. This state simulates a real world process which has not finished and is waiting for some trigger to continue. Does not change simulation time. *p.passive()* returns *True*. - **Terminated**: The process has executed all its action statements and continues as a data instance, if referenced. *p.terminated()* returns *True* Initially (upon creation of the Process instance), a process returns *passive*. In addition, a SimPy process, *p*, can be in the following (sub)states: - **Interrupted**: Active process has been interrupted by another process. It can immediately respond to the interrupt. This simulates an interruption of a simulated activity before its scheduled completion time. *p.interrupted()* returns *True*. - **Queuing**: Active process has requested a busy resource and is waiting (passive) to be reactivated upon resource availability. *p.queuing(a_resource)* returns *True*. .. ------------------------------------------------------------------------- A3. SimPlot, The Simpy plotting utility ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SimPlot_ provides an easy way to graph the results of simulation runs. .. _`SimPlot`: SimPlotManual/ManualPlotting.html A4. SimGUI, The Simpy Graphical User Interface ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ SimGUI_ provides a way for users to interact with a SimPy program, changing its parameters and examining the output. .. _`SimGUI`: SimGUIManual/SimGUImanual.html A5. SimulationTrace, the SimPy tracing utility ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ `SimulationTrace`_ has been developed to give users insight into the dynamics of the execution of SimPy simulation programs. It can help developers with testing and users with explaining SimPy models to themselves and others (e.g. for documentation or teaching purposes). .. _`SimulationTrace`: Tracing.html A6. SimulationStep, the SimPy event stepping utility ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ `SimulationStep`_ can assist with debugging models, interacting with them on an event-by-event basis, getting event-by-event output from a model (e.g. for plotting purposes), etc. It caters for: - running a simulation model, with calling a user-defined procedure after every event, - running a simulation model one event at a time by repeated calls, - starting and stopping the event stepping mode under program control. .. _`SimulationStep`: SimStepManual/SimStepManual.html A7. SimulationRT, a real-time synchronizing utility ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ `SimulationRT`_ allows synchronizing simulation time and real (wallclock) time. This capability can be used to implement e.g. interactive game applications or to demonstrate a model's execution in real time. .. _`SimulationRT`: SimRTManual.html :Created: 2003-April-6 .. Local Variables: mode: indented-text indent-tabs-mode: nil sentence-end-double-space: t fill-column: 70 End: