Recursive statistics in SimPy
Virgil Stokes <vs <at> it.uu.se>
2007-10-23 14:21:22 GMT
I have attached a SimPy program that I hope you will find useful for
calculating the mean and variance of sample paths. Please feel free to
modify the code as needed. If you find any problems in running it please
let me know and I will try to resolve them as soon as possible.
--V. Stokes
Systems & Control
Uppsala University
"""MMc5.py
This is a SimPy implementation for a general M/M/c queueing system. The main
purpose is to show how recursive algorithms can be implemented to estimate the
mean and variance for some of the common performance measures of queueing
systems. I also show how a "trace" of state changes can be implemented without
using SimPy.SimulationTrace.
System under study
Customers arrive at random to a c-server queueing system (bank) and have
service times from an exponential distribution. A single FIFO queue can form
when the tellers are busy. Simulations are used to estimate the following
performance measures over the time interval [TA,TB]:
L -- average number in system
Lq -- average number in queue
W -- average time in system for those that entered and departed system,
Wq -- average time in queue for those that entered the queue
WQ -- average time in queue for all customers that entered the system
Ws -- average time in service for those that entered service and finished service
Note:
1) Recursive algorithms are used to estimate each performance measure and their
variance
2) Does not use Tally() or Monitor()
3) Implementation allows for multiple realizations (runs)
4) Lq (mean number in the queue) was incorrect in the original (using
Monitor() or Tally()). This has been corrected.
5) Tracing can be activated (TRACING = True) for a detailed output of
state changes in the system
6) I encourage the user to modify the code given here as needed for
their specific application. Feedback on the code (positive or
negative) would be appreciated (vs <at> it.uu.se) and I will try to
respond to all queries on the code as quickly as possible.
7) A description (including the derivation) of these recursive algorithms
is given in the following document:
Stokes, V.P. (2007) Recursive Statistics in DES (vers. 3.1).
Unpublished
which is available on request (PDF format), from the author (vs <at> it.uu.se)
8) Thanks to T. Vignaux and K. Muller
Assumptions:
1) Time for a customer to move from arrival to queue = 0
2) Time for a customer to move from queue to service = 0
3) Service times and inter-arrival times are independent
4) Service times for all tellers are from a common exponential pdf
5) Inter-arrival times are from an exponential pdf
Original: SimPy (1.8) example (MMC.py) in SimPyModels directory
Mods: V. P. Stokes (vs <at> it.uu.se)
Version History: 20071022.1,20070930.1,20070927.1,20070925.1,20070711.1
"""
from SimPy.Simulation import *
from time import gmtime, strftime, localtime, clock
import random
class CT_Tally(object):
def __init__(self,tA=0.0,tB=10**10):
self._tprev = 0.0;
self._xprev = 0.0;
self._ave = 0.0;
self._var_t = 0.0;
self.strt,self.stop = False,False
self.tA,self.tB = tA,tB
self.list = []
def recursiveStats(self,dt,t):
# Update mean, variance*t
fact = dt/t
t1 = self._xprev - self._ave;
t2 = fact*t1
self._var_t += self._tprev*t2*t1 #recursive evaluation of variance*t for CRV
self._ave += t2 #recursive evaluation of mean for CRV
def observe(self, xnow, t=None):
''' Recursive evaluation of mean and variance of a CTSP.
Can be used for: L(t), Lq(t)
'''
if self.stop:
return
if t is None:
t = now()
tt = t
if not self.strt:
if tt >= self.tA:
# Check for very special case --- state does not change over [tA,tB]
if tt >= self.tB:
self.stop = True
self._ave = self._xprev
self._var_t = 0.0
return
dt = tt - self.tA # initial dt (t = dt)
t = dt
self.strt = True
else:
t = 0.0
else:
if tt >= self.tB:
# At tB or outside [tA,tB] --- this will be final dt
dt = self.tB - (self._tprev + self.tA)
t = self._tprev + dt
self.stop = True
else:
# Inside [tA,tB]
dt = tt - (self._tprev + self.tA)
t = tt - self.tA
if self.strt:
if t > 0.0:
self.recursiveStats(dt,t)
self._xprev = xnow
self._tprev = t
def stats(self):
t = now()
if self.tB > t:
self.tB = t
# Integral over t in [tA,tB]
tint = self.tB - self.tA
if not self.stop:
# Final calculation
dt = self.tB - (self._tprev+self.tA)
self.recursiveStats(dt,tint)
# return mean and standard deviation (sd)
return (self._ave,(self._var_t/tint)**0.5)
class DT_Tally(object):
def __init__(self,tA=0.0,tB=10**10):
self._kprev = 0;
self._ave = 0.0;
self._var_k = 0.0;
self.stop,self.strt = False,False
self.tA,self.tB = tA,tB
self.list = []
def recursiveStats(self,dt):
k = self._kprev +1
fact = 1.0/k
t1 = dt-self._ave; t2 = fact*t1
self._var_k += (k-1)*t2*t1 #recursive evaluation of variance*k for DRV
self._ave += t2 #recursive evaluation of mean for DRV
self._kprev = k
def removeFromList(self,unitnum):
for j, x in enumerate(self.list):
if unitnum == x[1]:
# Ok! found a match -- remove it from list
self.list.pop(j)
break
def observe(self,inTime=None,otTime=None,unitnum=None):
''' Recursive evaluation of mean and variance of a DTSP.
Can be used for: W(k), Wq(k)
'''
if otTime is None:
# Customer entered the system, or queue, or service
if inTime <= self.tB:
# Add this customer to the .list
self.list += [[inTime,unitnum]]
return
else:
# Customer departed from the system, or queue, or service
if inTime > self.tB:
self.stop = True
return
if otTime < self.tA:
self.removeFromList(unitnum)
return
# Customer is departing (system, queue, or server)
if not self.stop:
# Define dt for Tally of stats on this cutomer
if inTime < self.tA:
if otTime > self.tB:
dt = self.tB - self.tA
else:
dt = otTime - self.tA
else:
if otTime > self.tB:
dt = self.tB - inTime
else:
dt = otTime - inTime
# Recursive stats (mean and variance)
self.recursiveStats(dt)
# Remove unitnum entry from .list containing unitnum and its inTime
self.removeFromList(unitnum)
def stats(self):
t = now()
if self.tB > t:
self.tB = t
if not self.stop:
# Process each customer that is still in the system (has not departed)
for x in self.list:
if x[0] < self.tA:
dt = self.tB - self.tA
else:
dt = self.tB - x[0]
self.recursiveStats(dt)
# Return mean and standard deviation (sd)
if self._kprev > 0:
return self._ave,(self._var_k/self._kprev)**0.5
else:
return(0.0,0.0)
## Model components -------------------------------------------------------------
class Generator(Process):
""" generates a customer arrival at a random time """
def execute(self,maxNumber,arrate,srrate):
''' Create customers with inter-arrival times from an
exponential distribution (arrival rate = arrate) with
service times from an exponential distribution (service
rate = srrate).
'''
# The following loop is the "source" for customers
for i in range(maxNumber):
Customernum = i+1
L = Customer("Customer-%4d" %Customernum) # customer ID
# OBS! number can now be accessed by .waitQ[k].number (k = 0,...,len-1)
L.number = Customernum
# put customer in event scheduler list
activate(L,L.LifeCycle(srrate,Customernum))
# define event for next customer's arrival
yield hold,self,RVS[intarr_indx].expovariate(arrate)
class Customer(Process):
''' Customers request a bank teller and keep
the teller occupied for ST ~ Exp(srate)
Note -- there is considerable code in this class for "nice" formatting
of trace output
'''
L = 0 # number of customers in system
Lq = 0 # number of customers in queue
def LifeCycle(self,srate,custnum):
Customer.outST = False
# Customer has arrived
arrTime=now()
Customer.L += 1
# Tally number of customers in system
mL.observe(Customer.L)
# Tally time of customers in system
mW.observe(inTime=arrTime,unitnum = custnum)
# Tally time of customers (all) in queue
mWQ.observe(inTime=arrTime,unitnum = custnum)
ns = len(server.activeQ) # number of customers being served
Customer.appendstr = ""
if ns == c:
# all servers are occuptied -> customer will be queued
Customer.Lq += 1
Customer.appendstr = "Q"
# Tally time of customers (only those that enter queue) in queue
mWq.observe(inTime=arrTime,unitnum = custnum)
# Tally number of customers in queue
mLq.observe(Customer.Lq)
self.trace("Arrival ")
Customer.appendstr =""
# Looks for a free bank teller (may be put in queue)
yield request,self,server
# Customer gets a teller (could have been queued)
aservtime = now()
# Tally queueing time over all customers!
mWQ.observe(inTime=arrTime,otTime=aservtime,unitnum=custnum)
if aservtime != arrTime:
pass
# Tally queueing time only for those that queued!
mWq.observe(inTime=arrTime,otTime=aservtime,unitnum=custnum)
# Starts service (gets a teller) -- activeQ increased
Customer.st = RVS[sertim_indx].expovariate(srate)
# Tally time that customer starts service
mST.observe(inTime=aservtime,unitnum=custnum)
Customer.outST = True
self.trace("Start Service")
Customer.outST = False
# Tally number of customers in the queue
mLq.observe(Customer.Lq)
# Teller-customer will now be busy (service by teller)
yield hold,self,Customer.st
# Finishes service after Customer.st time
fservtime = now()
# Tally service time for customer
mST.observe(inTime=aservtime,otTime=fservtime,unitnum=custnum)
# Teller to be released by this customer
yield release,self,server
# Leaves teller (teller now free to service next customer)
rservtime = now()
Customer.L -= 1 # customer removed from system
# Tally number of customers in system
mL.observe(Customer.L) # tally number of customers in system
# Tally number of customers in queue
mLq.observe(Customer.Lq)
# Tally time for customer in the system
mW.observe(inTime=arrTime,otTime=rservtime,unitnum=custnum)
if Customer.Lq > 0:
# Take customer from queue
Customer.Lq -= 1
self.trace("Finished ")
def trace(self,message):
if TRACING:
if Customer.outST:
FMT="%9.4f %13s %13s (%4d,%4d) ST = %8.4f"
print FMT%(now(),self.name,message,Customer.Lq,Customer.L,Customer.st)
else:
FMT="%9.4f %13s %13s (%4d,%4d) %s"
print FMT%(now(),self.name,message,Customer.Lq,Customer.L,Customer.appendstr)
## Experiment data --------------------------------------------------------------
timeunits = "minutes"
# The following parameters can be changed as needed
TRACING = True # For control of trace output
#TRACING = False
c = 2 # number of servers in M/M/c system
#c = 1
stime = 1.0 # mean service time [minutes]
stime = 12.0
arate = 2.0 # mean arrival rate [# per minute]
arate = 0.1
delayFirstCustomer = False # first customers arrives at simTime = 0
delayFirstCustomer = True
simTime = 100.0
maxCustomers= 1000 # maximum number of customers to be simulated
srate = 1.0/stime
# RN streams
nstreams = 2
intarr_indx = 0 # for inter-arrival times
sertim_indx = 1 # for service times
# Need to define a "large" floating point number
Big = 10**10 # larger than the time for any event during the simulation
TA = 0.0
#TA = 25
#TA = 62
#TA = 92
#TB = 100
#TB = 10
#TB = 21
#TB = 70
#TB = 80
#TB = 30
TB = Big
if TB <= TA:
print "Illegal interval for TAStats!"
sys.exit(0)
nRuns = 4
nRuns = 13
#timestamp = strftime("%a, %d %b %Y %H:%M:%S (UTC)", gmtime())
timestamp = strftime("%a, %d %b %Y %H:%M:%S (Stockholm)", localtime())
print "* SimPy (1.8) Simulation: %s"%timestamp
print 'M/M/%1d'%c
print "Time units = %s"%timeunits
if TB == Big:
print "Time interval = [%8.4f, Big]"%TA
else:
print "Time interval = [%8.4f,%8.4f]"%(TA,TB)
print "Mean arrival rate = %7.4f"%arate
print "Mean service time = %6.4f"%stime
print "rho = (arrival rate)/(%1d*(service rate)) = %7.4f" %(c,arate/(c*srate))
print "Maximum number of customers = %3d"%maxCustomers
print "Maximum simulation time = %8.2f"%simTime
print "Number of runs = %4d"%nRuns
seedlist = [0,nRuns] # used to generate unique seeds for each run
## Simulation (experiment loop) -------------------------------------------------
tstrt = clock()
for irun in range(nRuns):
print "\nRun: %4d"%(irun+1)
# Here the user can define rules for the selection of seeds for each RN
# stream to be used.
# Note, the following is a rather simple method to get a unique set of seeds
for indx in range(len(seedlist)):
seedlist[indx] += 1
# initialize RN streams with seeds
RVS = [random.Random(seedlist[0]), random.Random(seedlist[1])]
for i in range(nstreams):
if i > 0:
print "%20d"%seedlist[i]
else:
print "Seeds: %13d"%seedlist[i]
if TRACING: print " Lq, L"
# Instantiate my own "Tally" objects for the performance measures
mL =CT_Tally(tA=TA,tB=TB) # number of customers in system (L(t) )
mLq =CT_Tally(tA=TA,tB=TB) # number of customers in queue (Lq(2))
mST =DT_Tally(tA=TA,tB=TB) # time in service (Ws(k))
mWq =DT_Tally(tA=TA,tB=TB) # time in queue for customers that enter queue (Wq(k))
mWQ =DT_Tally(tA=TA,tB=TB) # time in queue for customers that enter system (WQ(k))
mW =DT_Tally(tA=TA,tB=TB) # time in system (W(k) )
# c-tellers (resources)
server = Resource(capacity=c, name='Bank teller')
initialize()
g = Generator('CustomerGen')
# Bank opens at t = 0 and the first customer will either arrive when the bank
# opens or after a random delay.
if delayFirstCustomer:
# Random delay until first customer arrives
delayFirst = RVS[intarr_indx].expovariate(arate)
else:
# No delay until first customer arrives
delayFirst = 0
# Put the first customer in the event list for the event scheduler
activate(g,g.execute(maxNumber=maxCustomers, arrate=arate, srrate=srate),
delay=delayFirst)
# Run the simulation...
simulate(until=simTime)
## Output Results ---------------------------------------------------------
print "********** R E S U L T S **********"
print " Parameter mean ( sd )"
print "Estimated L = %8.4f (%8.4f)"%(mL.stats())
print "Estimated Lq = %8.4f (%8.4f)"%(mLq.stats())
print "Estimated Ws = %8.4f (%8.4f)"%(mST.stats())
print "Estimated Wq = %8.4f (%8.4f)"%(mWq.stats())
print "Estimated WQ = %8.4f (%8.4f)"%(mWQ.stats())
print "Estimated W = %8.4f (%8.4f)"%(mW.stats())
tstop = clock()
print "\n* Simulation time = %10.4f [secs]"%(tstop - tstrt)
-------------------------------------------------------------------------
This SF.net email is sponsored by: Splunk Inc.
Still grepping through log files to find problems? Stop.
Now Search log events and configuration files using AJAX and a browser.
Download your FREE copy of Splunk now >> http://get.splunk.com/
_______________________________________________
Simpy-users mailing list
Simpy-users <at> lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/simpy-users