Introduction to the SimPy Discrete-Event Simulation Package

   [SimPysmaller1.gif]

   Professor Norm Matloff
   University of California, Davis

Contents:

     * about SimPy 
     * where to obtain it
     * how to install it
     * Simpy overview
     * introduction to using SimPy
     * use of cancel()
     * debugging
     * more on random number generators
     * alternate approaches to a problem
     * monitors
     * SimPy internals
     * the GUI package
     * more information 

About SimPy:

   SimPy   (rhymes   with   "Blimpie")  is  a  public-domain  package  for
   process-oriented  discrete-event  simulation.  It  is  written  in, and
   called  from,  Python. I like the clean manner in which it is designed,
   and   the  use  of  Python  generators--and  for  that  matter,  Python
   itself--is  a  really  strong point. If you haven't used Python before,
   you  can learn enough about it to use SimPy quite quickly; see my quick
   tutorial on Python.

   Instead  of  using  threads,  as  is the case for most process-oriented
   simulation  packages,  SimPy  makes  novel  use  of Python's generators
   capability. (Python 2.2 or better is required. See my Python generators
   tutorial  if you wish to learn about generators, but you do not need to
   know  about  them  to  use  SimPy.)  Generators allow the programmer to
   specify  that  a  function  can  be  prematurely  exited and then later
   re-entered  at  the  point  of  last  exit,  enabling  coroutines.  The
   exit/re-entry  points  are  marked  by Python's yield keyword. Each new
   call  to  the function causes a resumption of execution of the function
   at  the  point  immediately  following  the last yield executed in that
   function.

   For  convenience,  I will refer to each coroutine (or, more accurately,
   each instance of a coroutine), as a thread.

Where to obtain it:

   Download it from SimPy's Sourceforge site.

How to install it:

   Create  a directory, say /usr/local/SimPy. You need to at least put the
   code  files  Simulation.*  and __init__.* in that directory, and I will
   assume   here   that  you  also  put  in  the  test  and  documentation
   subdirectories  which  come  with the package, say as subdirectories of
   /usr/local/SimPy.

   You'll  need  that  directory  to  be  in  your  Python  path, which is
   controlled by the PYTHONPATH environment variable. Set this in whatever
   manner  your  OS/shell  set  environment  variable.  For  example, in a
   csh/UNIX environment, type
setenv PYTHONPATH /usr/local/

   Modify accordingly for bash, Windows, etc.

   One  way  or  the other, you need to be set up so that Python finds the
   library  files  correctly.  Both  the  SimPy  example  programs and our
   example programs here include the line
from SimPy.Simulation import *

   which   instructs  the  Python  interpreter  to  look  for  the  module
   Simulation in the package SimPy. Given the setting of PYTHONPATH above,
   Python would look in /usr/local/ for a directory SimPy, i.e. look for a
   directory   /usr/local/SimPy,  and  then  look  for  Simulation.py  and
   __init__.py (or their .pyc compiled versions) within that directory.

   Test by copying testSimPy to some other directory and then running
python testSimPy.py

   Some  graphical  windows  will  pop  up,  and  after you remove them, a
   message like "Run 54 tests..." will appear.

SimPy overview:

   Here are the major SimPy classes:
     * Process:  simulates  an  entity which evolves in time, e.g. one job
       which  needs  to  be  served by a machine; we will refer to it as a
       thread, even though it is not a formal Python thread
     * Resource: simulates something to be queued for, e.g. the machine
     * Monitor:  this  is  a  nice  class  which  optionally makes it more
       convenient to collect data

   Here are the major SimPy operations:
     * activate:  used  to  mark  a  thread  as  runnable when it is first
       created
     * simulate: starts the simulation
     * yield  hold:  used  to  indicate the passage of a certain amount of
       time within a thread
     * yield  request:  used to cause a thread to join a queue for a given
       resource
     * yield  release:  used to indicate that the thread is done using the
       given resource, thus enabling the next thread in the queue, if any,
       to use the resource
     * yield passivate: used to have a thread wait until "wakened" by some
       other thread
     * reactivate: does the "wakening" of a previously-passivated thread
     * cancel:    cancels    all    the    events    associated   with   a
       previously-passivated thread

   Here is how the flow of control goes from one function to another:

     * When  the  main  program calls simulate(), the main program blocks.
       The  simulation  itself  then begins, and the main program will not
       run again until the simulation ends.
     * Anytime  a  thread  executes yield, that thread will pause. SimPy's
       internal  functions  will  then  run,  and will restart some thread
       (possibly the same thread).
     * When a thread is finally restarted, its execution will resume right
       after whichever yield statement was executed last in this thread.

   Note  that  activate(),  reactivate()  and  cancel() do NOT result in a
   pause  to  the calling function. Such a pause occurs only when yield is
   invoked. In my opinion, this is a huge advantage, for two reasons:

     * your code is not cluttered up with a lot of lock/unlock operations
     * execution  is deterministic, which makes both writing and debugging
       the program much easier

Introduction to using SimPy:

   We  will  demonstrate the usage of SimPy by presenting three variations
   on  a  machine-repair  model.  The three SimPy programs are in my SimPy
   home    directory,    http://heather.cs.ucdavis.edu/~matloff/Sim/SimPy:
   MachRep1.py,  MachRep2.py  and  MachRep3.py  .  In  each  case,  we are
   modeling  a  system  consisting  of  two  machines which are subject to
   breakdown, but with different repair patterns:
     * MachRep1.py: There are two repairpersons, so that both machines can
       be repaired simultaneously if they are both down at once.
     * MachRep2.py:  Here  there  is  only  one  repairperson,  so if both
       machines  are down then one machine must queue for the repairperson
       while the other machine is being repaired.
     * MachRep3.py: Here there is only one repairperson, and he/she is not
       summoned until both machines are down.

   In  all  cases,  the  up  times  and  repair  times  are  assumed to be
   exponentially distributed with means 1.0 and 0.5, respectively.

  MachRep1.py:

   Let's look at "main":
UpRate = 1/1.0
RepairRate = 1/0.5
Rnd = Random(12345)
initialize()
for I in range(2):
   M = Machine()
   activate(M,M.Run(),delay=0.0)
MaxSimtime = 10000.0
simulate(until=MaxSimtime)
print "the percentage of up time was", Machine.TotalUpTime/(2*MaxSimtime)

   The heart of the code is
for I in range(2):
   M = Machine()
   activate(M,M.Run(),delay=0.0)

   Here  Machine  is  a  class  which  I  wrote,  as a subclass of SimPy's
   built-in class Process. Since we are simulating two machines, we create
   two  objects  of our Machine class. These will be the basis for our two
   machine threads.

   We call SimPy's built-in function activate() on each of the two Machine
   objects.  We  do  this  to  register  them as threads, and to state the
   Machine.Run()  is  the  function each thread will execute. It specifies
   how the entity evolves in time, in this case how the machine evolves in
   time.

   In general, I'll refer to this function (Machine.Run() in this example)
   as  the process execution method (PEM). (Functions in Python are called
   methods.)

   In  our  call  to  activate()  above,  the  last argument is the delay,
   typically  0.0. We want our threads to start right away. Note that this
   is  an  example  of  Python's  named  arguments  capability. In SimPy's
   internal  source  code for activate(), there is are five arguments, the
   fourth  of  which  is named delay. (If you wish to see this code, go to
   the file SimPy/Simulation.py and search for "def activate".) But in our
   call  to activate() here, we only provide three arguments, which happen
   to  be  the  first, second and fourth in the code for this function. By
   using Python's named arguments feature, we can skip some, which we did.
   The ones we skip take default values. There is no need to dwell on this
   point, but it is important that you are aware of why there are argument
   names in the midst of the call.

   The  call  to  SimPy's  built-in  function simulate() then gets all the
   threads  started.  The  argument is the amount of time to be simulated.
   (Again, we make use of a named argument.)

   Some other introductory comments about this piece of code:

     * The  object  Rnd  will allow us to generate random numbers, in this
       case exponentially distributed. We have arbitrarily initialized the
       seed to 12345.
     * We  will  be calling the function Random.expovariate(), which takes
       as  its  argument the reciprocal of the mean. So here we have taken
       the mean up time and repair times to be 1.0 and 0.5, respectively.
     * The call to initialize() is required for all SimPy programs.

   Note  again  that  once  we make the call to simulate() above, the next
   line,
print "the percentage of up time was", Machine.TotalUpTime/(2*MaxSimtime)

   won't be executed until simulated time reaches 10000.0.

   So,  let's  see what those two invocations of Machine.Run() will do. In
   order  to  do  this,  let's  first  take a look at the structure of our
   application-specific  class Machine which we have defined in our source
   code:
class Machine(Process):
   TotalUpTime = 0.0  # total up time for all machines
   NextID = 0  # next available ID number for Machine objects
   def __init__(self):
      Process.__init__(self)  # required in any Process subclass
      self.UpTime = 0.0  # amount of work this machine has done
      self.StartUpTime = 0.0  # time the current up period stated
      self.ID = Machine.NextID   # ID for this Machine object
      Machine.NextID += 1
   def Run(self):
      ...

   First  we  define  two  class variables, TotalUpTime and NextID. As the
   comment  shows,  TotalUpTime will be used to find the total up time for
   all machines, so that we can eventually find out what proportion of the
   time the machines are up.

   Next,  there  is the class' constructor function, __init__(). Since our
   class here, Machine, is a subclass of the SimPy built-in class Process,
   the  first  thing  we  must  do  is  call the latter's constructor; our
   program will not work if we forget this (it will also fail if we forget
   the argument self in either constructor).

   Finally,  we set several of the class' instance variables, explained in
   the comments. Note in particular the ID variable. You should always put
   in  some kind of variable like this, not necessarily because it is used
   in the simulation code itself, but rather as a debugging aid.

   Note that we did NOT need to protect the line
Machine.NextID += 1

   with a lock variable, as we would in pre-emptive thread systems. Again,
   this  is  because  a  SimPy  "thread" retains control until voluntarily
   relinquishing  it  via a yield; our thread here will NOT be interrupted
   in the midst of incrementing Machine.NextID.

   Now  let's  look at the details of Machine.Run(), where the main action
   of the simulation takes place:
   def Run(self):
      print "starting machine", self.ID
      while 1:
         self.StartUpTime = now()
         yield hold,self,Rnd.expovariate(UpRate)
         Machine.TotalUpTime += now() - self.StartUpTime
         yield hold,self,Rnd.expovariate(RepairRate)

   The  function  now() yields the current simulated time. We are starting
   this  machine in "up" mode, i.e. no failure has occurred yet. Remember,
   we  want  to record how much of the time each machine is up, so we need
   to  have  a  variable  which  shows  when  the current up time for this
   machine began. With this in mind, we had our code
self.StartUpTime = now()

   record the current time, so that later the code
Machine.TotalUpTime += now() - self.StartUpTime

   will calculate the duration of this latest uptime period,
now() - self.StartUpTime

   and then add it to the cumulative uptime total Machine.TotalUpTime.

   The  two invocations of the Python construct yield produce calls to the
   SimPy  method  hold(), which you will recall simulates the passage of a
   certain amount of time. The first call,
yield hold,self,Rnd.expovariate(UpRate)

   causes  this thread to pause for an exponentially-distributed amount of
   simulated  time,  simulating  an uptime period for this machine, at the
   end of which a breakdown occurs.

   The  term  yield  alludes  to  the  fact  that  this  thread physically
   relinquishes  control of the Python interpreter; another thread will be
   run.  Later, control will return to this thread, resuming exactly where
   the pause occurred.

   Then the second yield,
yield hold,self,Rnd.expovariate(RepairRate)

   works   similarly,   pausing   execution   of   the   thread   for   an
   exponentially-distributed amount of time to simulate the repair time.

   In  other words, the while loop within Run() simulates a repeated cycle
   of up time, down time, up time, down time, ... for this machine.

   Important  restriction:  Some  PEMs may be rather lengthy, and thus you
   will  probably  want  to apply top-down program design and break up one
   monolithic  PEM  into  smaller functions, i.e. smaller functions within
   the  Process  subclass containing the PEM. In other words, you may name
   your  PEM  Run(),  and  then  have  Run()  in  turn  call  some smaller
   functions.  This is of course highly encouraged. However, you must make
   sure that you do not invoke yield in those subprograms; it must be used
   only  in  the  PEM  itself. Otherwise the Python interpreter would lose
   track  of  where  to  return  the  next  time  the  PEM  were to resume
   execution.

   It is very important to understand how control transfers back and forth
   among  the threads. Say for example that machine 0's first uptime lasts
   1.2  and  its  first  downtime  lasts  0.9,  while  for  machine  1 the
   corresponding times are 0.6 and 0.8. The simulation of course starts at
   time 0.0. Then here is what will happen:

     * The  thread  for machine 0 will generate the value 1.2, then yield.
       SimPy's  internal  event  list  will  now  show that the thread for
       machine  0  is waiting until time 0.0+1.2 = 1.2. (You normally will
       not  see  the  event  list yourself, but it can be inspected, as we
       discuss in our section on debugging below.)
     * The  thread  for machine 1 (the only available choice at this time)
       will  now  run,  generating  the  value 0.6, then yielding. SimPy's
       event  list  will now show that the thread for machine 0 is waiting
       until time 0.6.
     * SimPy  advances  the  simulated time clock to the earliest event in
       the  event  list, which is for time 0.6. It removes this event from
       the  event  list,  and then resumes the thread corresponding to the
       0.6 event, i.e. the thread for machine 1.
     * The latter generates the value 0.8, then yields. SimPy's event list
       will  now  show that the thread for machine 0 is waiting until time
       0.6+0.8 = 1.4.
     * SimPy  advances  the  simulated time clock to the earliest event in
       the  event  list, which is for time 1.2. It removes this event from
       the  event  list,  and then resumes the thread corresponding to the
       1.2 event, i.e. the thread for machine 0.
     * Etc.

   When  the  simulation  ends,  control returns to the line following the
   call to simulate(), where the result is printed out:
print "the percentage of up time was", Machine.TotalUpTime/(2*MaxSimtime)

   We  divide by 2 here because there are two machines, and we are finding
   the overall percentage of up time for them collectively.

  MachRep2.py:

   Since  this  model  now includes a queuing element, we add an object of
   the SimPy class Resource:
RepairPerson = Resource(1)

   with  the  "1"  meaning  that  there  is  just  1 repairperson. Then in
   Machine.Run() we do the following when an uptime period ends:
yield request,self,RepairPerson
yield hold,self,Rnd.expovariate(RepairRate)
yield release,self,RepairPerson

   Here is what those yield lines do:

     * The  first  yield  requests  access  to the repairperson. This will
       return  immediately if the repairperson is not busy now; otherwise,
       this  thread  will  be suspended until the repairperson is free, at
       which time the thread will be resumed.
     * The  second  yield  simulates the passage of time, representing the
       repair time.
     * The  third  yield releases the repairperson. If another machine had
       been in the queue, awaiting repair--with its thread suspended while
       executing  the  first  yield--it  would  now  attain  access to the
       repairperson, and its thread would now execute the second yield.

   Suppose  for instance the thread simulating machine 1 reaches the first
   yield  slightly  before  the thread for machine 0 does. Then the thread
   for machine 1 will immediately go to the second yield, while the thread
   for machine 0 will be suspended at the first yield. When the thread for
   machine  1 finally executes the third yield, then SimPy's internal code
   will  notice that the thread for machine 0 had been queued, waiting for
   the repairperson, and would now reactivate that thread.

   Note the line
if RepairPerson.n == 1:

   Here  n  is  a  member variable in SimPy's class Resource. It gives the
   number  of  items  in the resource currently free. In our case here, it
   enables  us  to keep a count of how many breakdowns are lucky enough to
   get  immediate  access  to the repairperson. We later use that count in
   our output:
print "proportion of times repair was immediate:"
print float(Machine.NImmedRep)/Machine.NRep

   The  same  class  contains the member variable waitQ, which is a Python
   list  which  contains the queue for the resource. This may be useful in
   debugging,  or  if  you need to implement a special priority discipline
   other than the ones offered by SimPy.

  MachRep3.py:

   Recall  that in this model, the repairperson is not summoned until both
   machines  are down. We add a class variable Machine.NUp which we use to
   record  the  number  of  machines  currently up, and then use it in the
   following  code,  which is executed when an uptime period for a machine
   ends:
Machine.NUp -= 1
if Machine.NUp == 1:
   yield passivate,self
elif RepairPerson.n == 1:
   reactivate(M[1-self.ID])
yield request,self,RepairPerson

   We   first   update   the   number  of  up  machines  (by  decrementing
   Machine.NUp).  Then  if  we  find that there is still one other machine
   remaining  up,  this  thread must pause, to simulate the fact that this
   broken  machine  must wait until the other machine goes down before the
   repairperson  is  summoned.  The  way  this  pause is implemented is to
   invoke  yield  with  the  operand  passivate. Later the other machine's
   thread will execute the reactivate() statement on this thread, "waking"
   it.

   But  there is a subtlety here. Suppose the following sequence of events
   occur:

     * machine 1 goes down
     * machine 0 goes down
     * the repairperson arrives
     * machine 0 starts repair
     * machine 0 finishes repair
     * machine 1 starts repair
     * machine 0 goes down again

   The point is that when the thread for machine 0 now executes
if Machine.NUp == 1:

   the  answer  will be no, since Machine.NUp will be 0. But that is not a
   situation  in  which  this thread should waken the other one. Hence the
   need for the elif condition.

Use of cancel():

   In many simulation programs, a thread is waiting for one of two events;
   whichever  occurs  first  will trigger a resumption of execution of the
   thread.   The   thread   will  typically  want  to  ignore  the  other,
   later-occurring event. We can use cancel() to cancel the latter event.

   An example of this is in the program TimeOut.py . The model consists of
   a  network  node which transmits but also sets a timeout period. If the
   node  times  out, it assumes the message it had sent was lost, and will
   send   again.   We  wish  to  determine  the  percentage  of  attempted
   transmissions    which    result   in   timeouts.   The   timeout   and
   transmission/acknowledgement  times  are  assumed  to  be exponentially
   distributed.  (The former is unusual for a timeout period, but I wanted
   a  simple  model  that  could be verified analytically via Markov chain
   analysis.)

   The main driver here is a class Node, whose PEM code includes the lines
while 1:
   self.NMsgs += 1
   TO = TimeOut()
   activate(TO,TO.Run(),delay=0.0)
   ACK = Acknowledge()
   activate(ACK,ACK.Run(),delay=0.0)
   yield passivate,self
   if self.ReactivatedCode == 1:
      self.NTimeOuts += 1

   The  node, an object Nd of class Node, sets up a timeout by creating an
   object   TO   of   TimeOut  class,  and  sets  up  a  transmission  and
   acknowledgement  by  creating  an object ACK of Acknowledge class. Then
   the node passivates itself, allowing the TO and ACK to do their work.

   Here's what TO does:
yield hold,self,Rnd.expovariate(TORate)
Nd.ReactivatedCode = 1
reactivate(Nd)
self.cancel(ACK)

   It  holds  a  random  timeout  time,  then sets a flag in Nd to let the
   latter  know  that it was the timeout which occurred first, rather than
   the  acknowledgement.  Then  it  reactivates Nd and cancels ACK. ACK of
   course   has   similar   code  for  handling  the  case  in  which  the
   acknowledgement occurs before the timeout.

   Note  that  in our case here, we want the thread to go out of existence
   when  canceled.  The  cancel()  function  does  not make that occur. It
   simply removes the pending events associated with the given thread. The
   thread  is still there, and in some applications we may want the thread
   to  continue execution. We can make this happen by calling reactivate()
   after  cancel(); the thread will then resume execution at the statement
   following whichever yield it had executed last.

Debugging:

   As usual, do yourself a big favor and use a debugging tool, rather than
   just  adding  print  statements.  Use the debugging tool's own built-in
   print  facility  for whatever printing you need. See my debugging slide
   show  for  general  tips  on  debugging.  I  have some points on Python
   debugging in particular in my quick tutorial on Python.

   There  is  one  additional  debugging  action  needed  in  the  case of
   simulation.   It   is  not  actually  for  debugging,  but  rather  for
   verification  that  the  program is running correctly, but makes use of
   your debugging tool anyway.

   In  simulation  situations, one typically does not have good test cases
   to  use  to check our code. After all, the reason we are simulating the
   system  in the first place is because we don't know the quantity we are
   finding  via  simulation.  So  in  simulation contexts, the only way to
   really check whether your code is correct is to use your debugging tool
   to  step  through  the  code  for  a  certain amount of simulated time,
   verifying  that  the  events  which  occur  jibe  with  the model being
   simulated. I recommend doing this first starting at time 0.0, and later
   again  at some fairly large time. The latter is important, as some bugs
   only show up after the simulation has been running for a long time.

   I  will  assume  here  that you are using the pdb debugging tool. It is
   rather  primitive,  but  it  can  be used effectively, if used via DDD.
   Again, see my quick introduction to Python for details.

   Your ability to debug SimPy programs will be greatly enhanced by having
   some degree of familiarity with SimPy's internal operations. You should
   review  the  overview  section  of  this SimPy tutorial, concerning how
   control  transfers  among various SimPy functions, and always keep this
   in mind. You may even wish to read the section below on SimPy internals
   .

   Note, by the way, that many SimPy internal entity names begin with _ or
   __. In the former case, you must explicitly ask for access, e.g.
from SimPy.Simulation import _e

   while in the latter case you must demangle the names.

   With  any  process-oriented  simulation package, you must deal with the
   context  switches,  i.e. the sudden changes from one thread to another.
   Consider  for  example  what happens when you execute your code in pdb,
   and reach a line like
-> yield hold,self,Rnd.expovariate(ArrvRate)

   Let's  see  what  will  now happen with the debugging tool. First let's
   issue  pdb's n ("next") command, which skips over function calls, so as
   to  skip  over  the  call to expovariate. We will still be on the yield
   line:
(Pdb) n
--Return--
> /usr/home/matloff/Tmp/tmp6/HwkIII1.py(14)Run()->(1234,  yield hold,self,Rnd.ex
povariate(ArrvRate)

   If  we  issue  the n command again, the hold operation will be started,
   which  (as explained in the section on SimPy internals below) causes us
   to enter SimPy's holdfunc() method:
(Pdb) n
> /usr/local/SimPy/Simulation.py(388)holdfunc()
-
. holdfunc(a):

   This  presents  a  problem.  We  don't want to traipse through all that
   SimPy internals code.

   One  way around this would be to put breakpoints after every yield, and
   then simply issue the continue command, c each time we hit a yield.

   Another possibility would be to use the debugger's command which allows
   us  to  exit  a function from within. In the case of pdb, this is the r
   ("return") command. We issue the command twice:
(Pdb) r
--Return--
> /usr/local/SimPy/Simulation.py(389)holdfunc()->None
-> a[0][1]._hold(a)
(Pdb) r
> /usr/home/matloff/Tmp/tmp6/HwkIII1.py(29)Run()->(1234, , 0.45785058071658913)
-> yield hold,self,Rnd.expovariate(ExpRate)

   Again,  pdb  is  not  a  fancy  debugging  tool,  but  it really can be
   effective  if used well. Here for instance is something I recommend you
   use within pdb when debugging a SimPy application:
alias c c;;l;;now()

   This  replaces  pdb's  continue command by the sequence: continue; list
   the  current and neighboring code statements; and print out the current
   simulated  time. Try it! I think you'll find it very useful. If so, you
   might  put  it  in  your  .pdbrc startup file, say in each directory in
   which  you  are  doing  SimPy  work. Of course, you can also extend the
   alias temporarily to automatically print out certain variables too each
   time  execution  pauses  (and then unalias it later when you don't need
   it).

   The  debugging process will be much easier if it is repeatable, i.e. if
   successive  runs  of the program give the same output. In order to have
   this  occur, you need to use random.Random() to initialize the seed for
   Python's  random  number  generator.  See  the section on random number
   generators below.

   Here  is  another  trick  which  you may find useful. You can print out
   SimPy's  internal  event  list with the following code (either from the
   debugging tool or within your program itself):
from SimPy.Simulation import _e
...
print _e.events

   And  as  mentioned  earlier,  you  can  print  out the wait queue for a
   Resource object, etc.

   The  newer  versions  of  SimPy  include  special  versions of the file
   Simulation.py,  called SimulationTrace.py and SimulationEvent.py, which
   you  may find useful in your debugging sessions. Largely, what these do
   is to formalize and automate some of the tips I've given above. See the
   documentation in the file Tracing.html.

More on random number generators:

   The  Python  method random.expovariate(), as seen earlier, generates an
   exponentially-distributed  random  variate with the given argument. The
   latter is the "event rate," i.e. the reciprocal of the mean.

   The method random.uniform() generates a uniform random variate from the
   continuous  interval  (a,b),  where  a  and  b are the arguments to the
   method.

   The  method random.randrange(a,b) generates a random integer in the set
   {a,a+1,a+2,...,b-1},  each  member of the set having equal probability,
   again where a and b are the arguments to the method.

   As  mentioned  earlier,  at  least  for  the purposes of debugging, you
   should  not  allow SimPy to set the initial random number seed for you.
   For  consistency, set it yourself, using random.Random(), as you see in
   my  examples.  I use arguments like 12345, but it really doesn't matter
   much.

Alternate approaches to a problem:

   It's  always  good  to keep in mind that there may be several different
   ways  to  write  a  SimPy  program.  For  example, consider the program
   MdMd1.py  mentioned  above,  which  models a discrete-time M/M/1 queue.
   There  we had Server as a Process class, and handled the simple queuing
   on  our own. But instead, in the program MdMd1Alt.py, modeling the same
   discrete-time  M/M/1 queue, we have Server as a Resource. We have a new
   Process  class,  Job,  and  each  time  a  job arrives, a Job object is
   created which then uses the Server Resource.

Monitors:

   Monitors  make  it  more convenient to collect data for your simulation
   output.  They  are  implemented  using  the  class  Monitor, which is a
   subclass of the Python list type.

   For  example, you have a variable X in some line in your SimPy code and
   you  wish  to  record all values X takes on during the simulation. Then
   you would set up an object of type Monitor, say named XMon, in order to
   remind  yourself  that  this  is  a monitor for X. Each time you have a
   value of X to record, you would have a line like
XMon.observe(X)

   which  would add the value, and the current simulated time, to the list
   in XMon. (So, XMon's main data item is an array of pairs.)

   The  Monitor  class  also includes member functions that operate on the
   list. For example, you can compute the mean of X:
print "the mean of X was", XMon.mean()

   The   function  Monitor.timeAverage()  takes  the  time-value  product.
   Suppose  for  instance  you  wish  to  find  the long-run average queue
   length.  Say the queue lengths were as follows: 2 between times 0.0 and
   1.4,  3  between  times 1.4 and 2.1, 2 between times 2.1 and 4.9, and 1
   between 4.9 and 5.3. Then the average would be
(2 x 1.4 + 3 x 0.7 + 2 x 2.8 + 1 x 0.4)/5.3 = 2.06

   How  would you arrange for this computation to be done in your program?
   Each  time  the  queue changes length, you would call Monitor.observe()
   with  the length as argument, resulting in Monitor recording the length
   and  the  current  simulated time (now()). When the simulation ends, at
   time 5.3, the monitor will consist of this list of pairs:
[ [0.0,2], [1.4,3], [2.1,2], [4.9,1] ]

   The  function  timeAverage()  would  then  compute  the  value 2.06, as
   desired.  That  last  0.4  in  that  average  is  taken into account by
   timeAverage()  in  that  the  most  recent  time,  say  the  end of the
   simulation  or  at  least the time at which timeAverage() is called, is
   5.3, and 5.3 - 4.9 = 0.4.

SimPy internals:

   Your  programming  and  debugging of SimPy applications will be greatly
   enhanced  if  you  know  at  least  a  little  about  how  SimPy  works
   internally.

   SimPy's  internals  (other  than  the Monitor material) are in the file
   Simulation.py.  To illustrate how they work, say in a SimPy application
   program  we  have  an  object  X  with  PEM  Run(), and that the latter
   includes a line
yield hold,self,1.2

   What will happen when that line is executed? To see, let's first take a
   look at how the PEM gets started in the first place.

   When the program calls activate(), i.e.
activate(X,X.Run(),delay=0.0)

   activate()  creates  a  dummy  event  with  the  current  time  (stored
   internally  by SimPy in the variable _t) and adds it to the SimPy event
   list,  _e.events.  (Addition  of an event to the event list is done via
   the  SimPy internal function _post().) Note that X.Run() is NOT running
   yet at this point.

   Let's  assume  for convenience that, as in our examples above, the call
   to  activate()  is  in our "main" program. Later in the "main" program,
   there  will  be  a  call to simulate() The simulate() function consists
   mainly  of  a  loop which iterates until the simulation is finished. In
   pseudocode form, the loop looks like this:
while simulation not finished
   take earliest event from event list
   update simulated time to the time of that event
   call the function which produced that event

   Recall  that  for  our  function  X.Run(),  the  call to activate() had
   resulted  in  a dummy event for this function being placed in the event
   list.  When simulate() pulls that dummy event from the event list, this
   will  result  in X.Run() being called, as you can see in the pseudocode
   above. Now X.Run() will be running (and simulate() will not be running,
   of course).

   When  X.Run()  reaches its yield statement (shown above, but reproduced
   here for convenience),
yield hold,self,1.2

   this  causes  X.Run() to return the Python tuple (hold,self,1.2) to the
   caller   of  X.Run()--which  was  simulate().  SimPy  has  an  internal
   dictionary dispatch:
dispatch={hold:holdfunc,request:requestfunc,release:releasefunc, \\
passivate:passivatefunc}

   Since  simulate()  received  the  parameter  hold  when X.Run() invoked
   yield,  it  will  do  the  dictionary  lookup  based  on  hold, finding
   holdfunc,  and  thus  will call the SimPy internal function holdfunc().
   The  latter  will call _hold(), which will create an event with time at
   the  indicated time (in our example here, 1.2 amount of time later). It
   will then use _post() to add this event to the event list.

   The  simulate()  function  will  then continue with its looping as seen
   above.  Eventually  it  will  pull  that hold event for X.Run() off the
   event list, and again as seen in the pseudocode above, it will call the
   function  associated with this event, i.e. X.Run(). That causes X.Run()
   to  resume  execution  after  the  yield  statement which produced that
   event. Etc.

The GUI package:

   SimPy  now has nice GUI tools, greatly enhancing the usefulness of this
   excellent simulation package. See the documentation for details.

More information:

   SimPy  comes  with  quite a bit of documentation, including a number of
   examples.

   A wealth of further information is available in the SimPy Wiki.