figs/sm_SimPy_Logo.png

SimPy Manual

Authors:
Version:12.019
Date:2003-June-9
SimPy version:1.3
Web-site:http://simpy.sourceforge.net/
Python-Version:2.2, 2.3
Created:2003-April-6

A Manual for SimPy

SimPy is an open-source, efficient, process-based simulation language using Python as a base. The facilities it offers are Processes, Resources, and Monitors.

This describes version 1.3 of SimPy.

Important note for users of previous SimPy versions

SimPy 1.3 introduces a trace utility SimulationTrace which aids in developing, debugging, teaching, understanding and documenting SimPy programs. See Appendix 3. SimulationTrace, the SimPy tracing utility for details.

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 average values of 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()

Execution of the timing mechanism starts at the point 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:

  • there are no more events to execute (then now() is the time of the last event), or
  • the simulation time reaches endtime (then now() == endtime), or
  • the stopSimulation() command is executed (then now() is 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.

Processes

The main class of active objects for discrete-event simulation in SimPy are 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 two particular methods and may define 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
    
  • A process execution method It 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, naturally, 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 tehn announces it has 'Arrived':

    def go(self):
        print now(), self.i, 'Starting'
        yield hold,self,100.0
        print now(), self.i, 'Arrived'
    

This 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(p1,p1.go())
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.execute(args),at=t,delay=period,prior=boolean) will activate the execution method p.execute() 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 prrocess 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

A process (interruptor) can interrupt a second, active process (the victim). A process cannot interrupt itself.

  • self.interrupt(victim)

The interrupt is just a signal. After this statement, the interruptor 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 interruptor 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 a complete example of a simulation with interrupts. Notice that in the first yield hold, interrupts may occur, so a reaction to the interrupt (= repair) has been programmed:

    class Bus(Process):
      def __init__(self,name):
         Process.__init__(self,name)
    
      def operate(self,repairduration,triplength):  # process execution method
         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.

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 __init__(self):
       Process.__init__(self)

   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. An example is shown in Example ). This is an object 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 a source with random (exponential) intervals between activations.

A source object 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. 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 examples assumes that the Customer class has been defined with an execute method 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.schedule(),at=0.0)
simulate(until=100)

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 one of particular sort of Machines (modelled as a resource). If not enough Machines are available, the Task will have to wait until one becomes free. The Task will then have the use of a Machine for however long it needs. It is not available for other Tasks until it is released. These actions are all automatically taken care of by the SimPy resource.

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)

    • 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 discipling 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

A Resource, r, has the following attributes:

  • The number of currently free units r.n
  • A waiting queue (list) of processes r.waitQ (FIFO by default) The number of Proceeses waiting is len(r.waitQ)
  • A queue (list) of processes holding units, r.activeQ. The number of Proceeses in the active queue is len(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 a unit is available.

    If there are enough units available, the requesting process will take one and continue its execution. The resource will record that the process is using a unit.

    If there are not enough available, 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 it is possible that the requesting process will pre-empt a lower-priority process already using a unit. (see 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
    

A Resource, r, has the following attributes:

  • The number of currently free units r.n
  • A waiting queue (list) of processes r.waitQ (FIFO by default) The number of Proceeses waiting is len(r.waitQ)
  • A queue (list) of processes holding units, r.activeQ. The number of Proceeses in the active queue is len(r.activeQ)

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

where 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.

A complete script. Four clients of 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

The output from Example:

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']

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, the one with the lowest priority is suspended, put at the front of the waitQ and the higher priority 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.

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)

Random Number Generation

Simulation requires the provision of pseudo-random numbers. SimPy uses the standard Python random module. We can have multiple random streams, as in Simscript and ModSim.

One imports the Class and methods needed. You must define a random variable object using:

  • from random import Random imports the Random class
  • g = Random() sets up the random variable object g.
  • 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.

Monitors

A Monitor is an object that can record simple statistics about values observed in the simulation. There are two modes of statistical gathering, tallying which is used to record observations of isolated values (such as waiting times) and accumulating which is used to record time averages of continuing values (such as numbers of customers in the system).

Monitors are not intended as a complete substitute for real statistical analysis but they have proved useful in developing simulations in SimPy.

The Monitor module of the SimPy package is separate from the Simulation module. To use Monitors, import the Monitor module:

  • from SimPy.Monitor import Monitor

Defining a monitor

To define a new Monitor object:

  • m=Monitor(name='')

Where name is the name of the monitor object.

Methods of the Monitor class include:

  • m.tally(x) adds the value x to the current totals in the Monitor. This is used for observing the mean and variance of a series of individual observations of x.
  • m.accum(x) adds the accumulated integral of time and x to the current totals. In fact it used the value of the previous value of x observed last time accumulate was used multiplied by the time difference. It is used to approximate the time integral of x.
  • m.reset(t) resets all the current totals. Sets the starting time to t or, if it is missing, to the current simulation time.

Simple statistics can be obtained from such a Monitor object.

  • m.count() returns the current number of observations.
  • m.mean() returns the simple average of the observations. If there are no observations, the message: "zero observations for mean()" is printed.
  • m.var() returns the variance of the observations. If there are no observations, the message: "zero observations for variance" is printed.
  • m.timeAverage(t) returns the time-average of accumulated values, measured at time t (the current simulation time if t is missing). If no time has elapsed, the message "zero time elapsed in timeAverage" is printed.
  • m.__str__() is a string that briefly describes the current state of the monitor. This can be used in a print statement.

Reminder: We tally variables that are individual observations, such as waiting times; we accumulate variables that are continuous in time s0 as to get a time averge. For example, the length of a queue does change discretely but it has some value at any time in contrast to a waiting time which is a mesurement taken at one time.

Here we establish a monitor to estimate the mean and variance of ten observations of an exponential random variate:

from SimPy.Monitor import Monitor
from random import Random

M = Monitor()
g = Random()

for i in range(10):
   x = g.expovariate(0.1)
   M.tally(x)

print 'mean= ',M.mean(), 'var= ',M.var()

Here the queue waiting for the Resource r is to be monitored to estimate the average of the number of processes waiting or using it. (This example is only fragmentary):

from __future__ import generators
from SimPy.Monitor import Monitor
from SimPy.Simulation import *

r = Resource(u)
M = Monitor()

   ...   # during  the simulation
   Qlength = len(r.waitQ)
   M.accum(Qlength) 
   ...

print 'mean= ',M.timeAverage()

Acknowledgments

We will be grateful for any corrections or suggestions for improvements to the document.

Appendix 1. SimPy 1.2 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: Zero observations for mean. No observations were made by the monitor before attempting to calculate the mean.
  • SimPy: Zero observations for variance. No observations were made by the monitor before attempting to calculate the variance.
  • SimPy: Zero time elapsed in timeAverage. No simulation time has elapsed before attempting to calculate the time Average for the monitor object.

Appendix 2. SimPy 1.3 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.

Appendix 3. SimulationTrace, the SimPy tracing utility

Introduction

The tracing utility 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).

Tracing SimPy programs

Tracing any SimPy program is as simple as replacing:

from SimPy.Simulation import *

with:

from SimPy.SimulationTrace import *

This will give a complete trace of all the scheduling statements executed during the program's execution.

An even nicer way is to replace this import by:

if __debug__:
        from SimPy.SimulationTrace import *
else:
        from SimPy.Simulation import *

This gives a trace during the development and debugging. If one then executes the program with python -O myprog.py, tracing is switched off, and no run-time overhead is incurred. (__debug__ is a global Python constant which is set to False by commandline options -O and -OO.)

For the same reason, any user call to trace methods should be written as:

if __debug__:
        trace.ttext("This will only show during debugging")

Here is an example (bank02.py from the Bank Tutorial):

Another example:

#!/usr/bin/env python
""" bank09.py: Simulate customers arriving
    at random, using a Source requesting service
    from several clerks but a single queue
    with a random servicetime
"""
from __future__ import generators
from SimPy.SimulationTrace  import *
from random import Random

class Source(Process):
    """ Source generates customers randomly"""
    def __init__(self,seed=333):
        Process.__init__(self)
        self.SEED = seed

    def generate(self,number,interval):       
        rv = Random(self.SEED)
        for i in range(number):
            c = Customer(name = "Customer%02d"%(i,))
            activate(c,c.visit(timeInBank=12.0))
            t = rv.expovariate(1.0/interval)
            yield hold,self,t

class Customer(Process):
    """ Customer arrives, is served and leaves """
    def __init__(self,name):
        Process.__init__(self)
        self.name = name
        
    def visit(self,timeInBank=0):       
        arrive=now()
        print "%7.4f %s: Here I am "%(now(),self.name)
        yield request,self,counter
        wait=now()-arrive
        print "%7.4f %s: Waited %6.3f"%(now(),self.name,wait)
        tib = counterRV.expovariate(1.0/timeInBank)
        yield hold,self,tib
        yield release,self,counter
        print "%7.4f %s: Finished"%(now(),self.name)

def model(counterseed=3939393):
    global counter,counterRV
    counter = Resource(name="Clerk",capacity = 2) #Lcapacity
    counterRV = Random(counterseed)
    initialize()
    sourceseed = 1133
    source = Source(seed = sourceseed)
    activate(source,source.generate(5,10.0),0.0)
    simulate(until=400.0)

model()

This produces:
  0 activate <a_process> at time: 0 prior: 0
0 activate <Customer00> at time: 0 prior: 0
0 hold <a_process> delay: 8.73140489458
 0.0000 Customer00: Here I am 
0 request <Customer00> <Clerk>  priority: default 
. . .waitQ: [] 
. . .activeQ: ['Customer00']
 0.0000 Customer00: Waited  0.000
0 hold <Customer00> delay: 8.90355092634
8.73140489458 activate <Customer01> at time: 8.73140489458 prior: 0
8.73140489458 hold <a_process> delay: 8.76709801376
 8.7314 Customer01: Here I am 
8.73140489458 request <Customer01> <Clerk>  priority: default 
. . .waitQ: [] 
. . .activeQ: ['Customer00', 'Customer01']
 8.7314 Customer01: Waited  0.000
8.73140489458 hold <Customer01> delay: 21.6676883425
8.90355092634 release <Customer00> <Clerk> 
. . .waitQ: [] 
. . .activeQ: ['Customer01']
 8.9036 Customer00: Finished
8.90355092634 <Customer00> terminated
17.4985029083 activate <Customer02> at time: 17.4985029083 prior: 0

. . . . . 

The trace contains all calls of scheduling statements (yield . . ., activate(), reactivate(), cancel() and also the termination of processes (at completion of all their scheduling statements). For yield request and yield release calls, it provides also the queue status (waiting customers in waitQ and customers being served in activeQ.

trace.tchange(): Changing the tracing

trace is an instance to the Trace class defined in SimulationTrace.py. This gets imported.

The tracing can be changed at runtime by calling trace.tchange() with one or more of the following named parameters:

start:

changes the tracing start time. Default is 0. Example: trace.tchange(start=222.2) to start tracing at simulation time 222.2.

end :

changes the tracing end time. Default is a very large number (hopefully past any simulation endtime you will ever use. Example: trace.tchange(end=33) to stop tracing at time 33.

toTrace:

changes the commands to be traced. Default is ["hold","activate","cancel", "reactivate","passivate","request","release","interrupt","terminated"]. Value must be a list containing one or more of those values in the default. Note: "terminated" causes tracing of all process terminations. Example: trace.tchange(toTrace=["hold","activate"]) traces only the yield hold and activate() statements.

outfile:

redirects the trace out put to a file (default is sys.stdout). Value must be a file object open for writing. Example: trace.tchange(outfile=open(r"c:python22bank02trace.txt","w"))

All these parameters can be combined. Example: trace.tchange(start=45.0,toTrace=["terminated"]) will trace all process terminations from time 45.0 till the end of the simulation.

The changes become effective at the time trace.tchange() is called. This implies for example that, if the call trace.tchange(start=50) is made at time 100, it has no effect before now()==100.

treset(): Resetting the trace to default values

The trace parameters can be reset to their default values by calling trace.treset().

trace.tstart(), trace.tend(): Enabling/disabling the trace

Calling trace.tstart() enables the tracing, and trace.tstop() disables it. Neither call changes any tracing parameters.

trace.ttext(): Annotating the trace

The event-by-event trace output is already very useful in showing the sequence in which SimPy's quasi-parallel processes are executed.

For documentation, publishing or teaching purposes, it is even more useful if the trace output can be intermingled with output which not only shows the command executed, but also contextual information such as the values of state variables. If one outputs the reason why a specific scheduling command is executed, the trace can give a natural language description of the simulation scenario.

For such in-line annotation, the trace.ttext(<string>) method is available. It provides a string which is output together with the trace of the next scheduling statement. This string is valid only for the scheduling statement following it.

Example:

class Bus(Process):
     def __init__(self,name):
         Process.__init__(self,name)

     def operate(self,repairduration=0):
         tripleft = 1000
         while tripleft > 0:
             trace.ttext("Try to go for %s"%tripleft)
             yield hold,self,tripleft
             if self.interrupted():
                 tripleft=self.interruptLeft
                 self.interruptReset()
                 trace.ttext("Start repair taking %s time units"%repairduration)
                 yield hold,self,repairduration
             else:
                 break # no breakdown, ergo bus arrived
         trace.ttext("<%s> has arrived"%self.name)

 class Breakdown(Process):
     def __init__(self,myBus):
         Process.__init__(self,name="Breakdown "+myBus.name)
         self.bus=myBus

     def breakBus(self,interval):

         while True:
             trace.ttext("Breakdown process waiting for %s"%interval)
             yield hold,self,interval
             if self.bus.terminated(): break
             trace.ttext("Breakdown of %s"%self.bus.name)
             self.interrupt(self.bus)
             
 print"\n\n+++test_interrupt"
 initialize()
 b=Bus("Bus 1")
 trace.ttext("Start %s"%b.name)
 activate(b,b.operate(repairduration=20))
 br=Breakdown(b)
 trace.ttext("Start the Breakdown process for %s"%b.name)
 activate(br,br.breakBus(200))
 trace.start=100
 print simulate(until=4000)

 This produces:

 0 activate <Bus 1> at time: 0 prior: 0
 ---- Start Bus 1
 0 activate <Breakdown Bus 1> at time: 0 prior: 0
 ---- Start the Breakdown process for Bus 1
 200 reactivate <Bus 1> time: 200 prior: 0
 200 interrupt by: <Breakdown Bus 1> of: <Bus 1>
 ---- Breakdown of Bus 1
 200 hold <Breakdown Bus 1> delay: 200
 ---- Breakdown process waiting for 200
 200 hold <Bus 1> delay: 20
 ---- Start repair taking 20 time units
 220 hold <Bus 1> delay: 800
 ---- Try to go for 800
 400 reactivate <Bus 1> time: 400 prior: 0
 400 interrupt by: <Breakdown Bus 1> of: <Bus 1>
 ---- Breakdown of Bus 1
 400 hold <Breakdown Bus 1> delay: 200
 ---- Breakdown process waiting for 200
 400 hold <Bus 1> delay: 20
 ---- Start repair taking 20 time units
 420 hold <Bus 1> delay: 620

  . . . . . 

The line starting with "----" is the comment related to the command traced in the preceding output line.

Nice output of class instances

After the import of SimPy.SimulationTrace, all instances of classes Process and Resource (and all their subclasses) have a nice string representation like so:

   >>> class Bus(Process):
...         def __init__(self,id):
...                 Process.__init__(self,name=id)
...                 self.typ="Bus"
...                 
>>> b=Bus("Line 15")
>>> b
<Instance of Bus, id 21860960:
     .name=Line 15
     .typ=Bus
>
>>> 

This can be handy in statements like trace.ttext("Status of %s"%b).