Collecting results from a single run#
A tool like simpy
allows you to collect your data flexibly using an approach that makes sense to you! Some options are:
Code an auditor / observer process. This process will periodically observe the state of the system. We can use this to collect information on current state at time t. For example, how many patients are queuing and how many have a call in progress between by time of day.
Store process metrics during a run and perform calculations at the end of a run. For example, if you want to calculate mean patient waiting time then store each patient waiting time in a list and calculate the mean at the end of the run.
Conduct and audit or calculate running statistics as the simulation executes an event. For example, as a patient completes a call we can calculate a running mean of waiting times and a running total of the operators are taking calls. The latter measure can then be used to calculate server utilisation. You could also use this approach to audit queue length where the queue length is recorded each time request for a resource is made (and/or when a resource is released).
1. Imports#
import simpy
import numpy as np
import itertools
2. Calculating mean waiting time#
The second strategy to results collection is to store either a reference to a quantitative value (e.g. waiting time) during the run. Once the run is complete you will need to include a procedure for computing the metric of interest. An advantage of this strategy is that it is very simple, captures all data, and has minimal computational overhead during a model run! A potential disadvantage is that for complex simulation you may end up storing a large amount of data in memory. In these circumstances, it may be worth exploring event driven strategies to reduce memory requirements.
In our example, we will store each patient’s waiting time in a python list. At the end of the run we will loop through these references and calculate mean waiting time and operator utilisation.
To do this I’m going to declare a list with notebook level scope. The service
function will then append a waiting_time
for a caller to the list each time the caller enters service.
2.1 Notebook level variables for results collection.#
We will create a python dictionary called results
to store result collection variables. This means that it is simple to add new variables in at a later date.
The dictionary has notebook level scope. This means that any functions or class in the notebook can access and/or append to the list access via the key waiting_times
.
results = {}
results['waiting_times'] = []
2.2 A helper function#
We will create a helper function called trace that wraps print
. We can set a variable called TRACE
that switches printing patient level results on and off.
def trace(msg):
'''
Turing printing of events on and off.
Params:
-------
msg: str
string to print to screen.
'''
if TRACE:
print(msg)
2.3 Service and arrival functions#
The only modification we need to make is to the service
function. We will add in a line of code to record the waiting_time
of the caller as they enter service.
def service(identifier, operators, env):
'''
simulates the service process for a call operator
1. request and wait for a call operator
2. phone triage (triangular)
3. exit system
Params:
------
identifier: int
A unique identifer for this caller
operators: simpy.Resource
The pool of call operators that answer calls
These are shared across resources.
env: simpy.Environment
The current environent the simulation is running in
We use this to pause and restart the process after a delay.
'''
# record the time that call entered the queue
start_wait = env.now
# request an operator
with operators.request() as req:
yield req
# record the waiting time for call to be answered
waiting_time = env.now - start_wait
# MODIFICATION - store the waiting time.
results['waiting_times'].append(waiting_time)
trace(f'operator answered call {identifier} at ' \
+ f'{env.now:.3f}')
# sample call duration.
call_duration = np.random.triangular(left=5.0, mode=7.0,
right=10.0)
# schedule process to begin again after call_duration
yield env.timeout(call_duration)
# print out information for patient.
trace(f'call {identifier} ended {env.now:.3f}; ' \
+ f'waiting time was {waiting_time:.3f}')
def arrivals_generator(env, operators):
'''
IAT is exponentially distributed
Parameters:
------
env: simpy.Environment
The simpy environment for the simulation
operators: simpy.Resource
the pool of call operators.
'''
# use itertools as it provides an infinite loop
# with a counter variable that we can use for unique Ids
for caller_count in itertools.count(start=1):
# 100 calls per hour (units = hours).
# Time between calls is 1/100
inter_arrival_time = np.random.exponential(60/100)
yield env.timeout(inter_arrival_time)
trace(f'call arrives at: {env.now:.3f}')
# create a new simpy process for this caller.
# we pass in the caller id, the operator resources, and env.
env.process(service(caller_count, operators, env))
# model parameters
RUN_LENGTH = 1000
N_OPERATORS = 13
# MODIFICATION - turn off caller level results.
TRACE = False
# create simpy environment and operator resources
env = simpy.Environment()
operators = simpy.Resource(env, capacity=N_OPERATORS)
env.process(arrivals_generator(env, operators))
env.run(until=RUN_LENGTH)
print(f'end of run. simulation clock time = {env.now}')
# MODIFICATION calculate results on notebook level variables.
mean_wt = np.mean(results['waiting_times'])
print(f'Mean waiting time was {mean_wt:.2f}')
end of run. simulation clock time = 1000
Mean waiting time was 4.73