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 |
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.
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.
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
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.
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.
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:
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
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)
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:
The process can be suspended and reactivated:
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,
A process (interruptor) can interrupt a second, active process (the victim). A process cannot interrupt itself.
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
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.
Even activated processes will not start until the following statement has been executed:
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!!
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)
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:
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:
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.
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)
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:
A good range of distributions is available, for example:
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:
To define a new Monitor object:
Where name is the name of the monitor object.
Methods of the Monitor class include:
Simple statistics can be obtained from such a Monitor object.
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()
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
We will be grateful for any corrections or suggestions for improvements to the document.
These messages are returned by simulate(), as in message=simulate(until=123).
Upon a normal end of a simulation, simulate() returns the message:
The following messages, returned by simulate(), are produced at a premature termination of the simulation but allow continuation of the program.
These messages are generated when SimPy-related fatal exceptions occur. They end the SimPy program. Fatal SimPy error messages are output to sysout.
From the point of the model builder, at any time, a SimPy process, p, can be in one of the following states:
Initially (upon creation of the Process instance), a process returns passive.
In addition, a SimPy process, p, can be in the following (sub)states:
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.
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 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 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.
The trace parameters can be reset to their default values by calling trace.treset().
Calling trace.tstart() enables the tracing, and trace.tstop() disables it. Neither call changes any tracing parameters.
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.
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).