class g:
= 5
patient_inter = 6
mean_n_consult_time = 1
number_of_nurses = 120
sim_duration = 1 ##NEW - single run number_of_runs
- 1
- Just run the model once.
trace()
function is based on that in HEP by Alison Harper SimLogger
class is adapted from the Python DES RAP Template by Amy Heather When working with your model, it can be hard to know whether it is working correctly. There are a range of different approaches we can take. These include:
print()
statements.trace()
) to control the print()
statements.logging
module.Where code examples are provided, this chapter uses the model from the chapter “An Example SimPy Model”.
Throughout the code, anything new that’s been added will be followed by the comment ##NEW
- so look out for that in the following code chunks.
In each of these examples, we will just run the model once.
print()
statementsTo get a running record of what is happening in your model, you can add print()
statements at key points.
For example, we can add:
For another example using print()
statements, see the “Reneging, Balking and Jockeying” chapter where they are used, for example, to record:
We will modify the Model
class to add print statements within the generator_patient_arrivals()
and attend_clinic()
functions.
def generator_patient_arrivals(self):
while True:
self.patient_counter += 1
p = Patient(self.patient_counter)
##NEW - Print message stating patient ID and arrival time
print(f"Patient {p.id} arrives at: {self.env.now:.3f}.")
self.env.process(self.attend_clinic(p))
sampled_inter = random.expovariate(1.0 / g.patient_inter)
yield self.env.timeout(sampled_inter)
def attend_clinic(self, patient):
start_q_nurse = self.env.now
with self.nurse.request() as req:
yield req
end_q_nurse = self.env.now
patient.q_time_nurse = end_q_nurse - start_q_nurse
sampled_nurse_act_time = random.expovariate(1.0 /
g.mean_n_consult_time)
##NEW - Print message with patient wait and consultation length
print(
f"Patient {patient.id} waits for {patient.q_time_nurse:.3f} " +
f"and is seen at {end_q_nurse}. Consultation length: " +
f"{sampled_nurse_act_time:.3f}.")
self.results_df.at[patient.id, "Q Time Nurse"] = (
patient.q_time_nurse)
self.results_df.at[patient.id, "Time with Nurse"] = (
sampled_nurse_act_time)
yield self.env.timeout(sampled_nurse_act_time)
The full updated code for the model is given below.
import simpy
import random
import pandas as pd
# Class to store global parameter values. We don't create an instance of this
# class - we just refer to the class blueprint itself to access the numbers
# inside.
class g:
patient_inter = 5
mean_n_consult_time = 6
number_of_nurses = 1
sim_duration = 120
number_of_runs = 1 ##NEW - single run
# Class representing patients coming in to the clinic. Here, patients have
# two attributes that they carry with them - their ID, and the amount of time
# they spent queuing for the nurse. The ID is passed in when a new patient is
# created.
class Patient:
def __init__(self, p_id):
self.id = p_id
self.q_time_nurse = 0
# Class representing our model of the clinic.
class Model:
# Constructor to set up the model for a run. We pass in a run number when
# we create a new model.
def __init__(self, run_number):
# Create a SimPy environment in which everything will live
self.env = simpy.Environment()
# Create a patient counter (which we'll use as a patient ID)
self.patient_counter = 0
# Create a SimPy resource to represent a nurse, that will live in the
# environment created above. The number of this resource we have is
# specified by the capacity, and we grab this value from our g class.
self.nurse = simpy.Resource(self.env, capacity=g.number_of_nurses)
# Store the passed in run number
self.run_number = run_number
# Create a new Pandas DataFrame that will store some results against
# the patient ID (which we'll use as the index).
self.results_df = pd.DataFrame()
self.results_df["Patient ID"] = [1]
self.results_df["Q Time Nurse"] = [0.0]
self.results_df["Time with Nurse"] = [0.0]
self.results_df.set_index("Patient ID", inplace=True)
# Create an attribute to store the mean queuing time for the nurse
# across this run of the model
self.mean_q_time_nurse = 0
# A generator function that represents the DES generator for patient
# arrivals
def generator_patient_arrivals(self):
# We use an infinite loop here to keep doing this indefinitely whilst
# the simulation runs
while True:
# Increment the patient counter by 1 (this means our first patient
# will have an ID of 1)
self.patient_counter += 1
# Create a new patient - an instance of the Patient Class we
# defined above. Remember, we pass in the ID when creating a
# patient - so here we pass the patient counter to use as the ID.
p = Patient(self.patient_counter)
##NEW - Print message stating patient ID and arrival time
print(f"Patient {p.id} arrives at: {self.env.now:.3f}.")
# Tell SimPy to start up the attend_clinic generator function with
# this patient (the generator function that will model the
# patient's journey through the system)
self.env.process(self.attend_clinic(p))
# Randomly sample the time to the next patient arriving. Here, we
# sample from an exponential distribution (common for inter-arrival
# times), and pass in a lambda value of 1 / mean. The mean
# inter-arrival time is stored in the g class.
sampled_inter = random.expovariate(1.0 / g.patient_inter)
# Freeze this instance of this function in place until the
# inter-arrival time we sampled above has elapsed. Note - time in
# SimPy progresses in "Time Units", which can represent anything
# you like (just make sure you're consistent within the model)
yield self.env.timeout(sampled_inter)
# A generator function that represents the pathway for a patient going
# through the clinic. Here the pathway is extremely simple - a patient
# arrives, waits to see a nurse, and then leaves.
# The patient object is passed in to the generator function so we can
# extract information from / record information to it
def attend_clinic(self, patient):
# Record the time the patient started queuing for a nurse
start_q_nurse = self.env.now
# This code says request a nurse resource, and do all of the following
# block of code with that nurse resource held in place (and therefore
# not usable by another patient)
with self.nurse.request() as req:
# Freeze the function until the request for a nurse can be met.
# The patient is currently queuing.
yield req
# When we get to this bit of code, control has been passed back to
# the generator function, and therefore the request for a nurse has
# been met. We now have the nurse, and have stopped queuing, so we
# can record the current time as the time we finished queuing.
end_q_nurse = self.env.now
# Calculate the time this patient was queuing for the nurse, and
# record it in the patient's attribute for this.
patient.q_time_nurse = end_q_nurse - start_q_nurse
# Now we'll randomly sample the time this patient with the nurse.
# Here, we use an Exponential distribution for simplicity, but you
# would typically use a Log Normal distribution for a real model
# (we'll come back to that). As with sampling the inter-arrival
# times, we grab the mean from the g class, and pass in 1 / mean
# as the lambda value.
sampled_nurse_act_time = random.expovariate(1.0 /
g.mean_n_consult_time)
##NEW - Print message with patient wait and consultation length
print(
f"Patient {patient.id} waits for {patient.q_time_nurse:.3f} " +
f"and is seen at {end_q_nurse}. Consultation length: " +
f"{sampled_nurse_act_time:.3f}.")
# Here we'll store the queuing time for the nurse and the sampled
# time to spend with the nurse in the results DataFrame against the
# ID for this patient. In real world models, you may not want to
# bother storing the sampled activity times - but as this is a
# simple model, we'll do it here.
# We use a handy property of pandas called .at, which works a bit
# like .loc. .at allows us to access (and therefore change) a
# particular cell in our DataFrame by providing the row and column.
# Here, we specify the row as the patient ID (the index), and the
# column for the value we want to update for that patient.
self.results_df.at[patient.id, "Q Time Nurse"] = (
patient.q_time_nurse)
self.results_df.at[patient.id, "Time with Nurse"] = (
sampled_nurse_act_time)
# Freeze this function in place for the activity time we sampled
# above. This is the patient spending time with the nurse.
yield self.env.timeout(sampled_nurse_act_time)
# When the time above elapses, the generator function will return
# here. As there's nothing more that we've written, the function
# will simply end. This is a sink. We could choose to add
# something here if we wanted to record something - e.g. a counter
# for number of patients that left, recording something about the
# patients that left at a particular sink etc.
# This method calculates results over a single run. Here we just calculate
# a mean, but in real world models you'd probably want to calculate more.
def calculate_run_results(self):
# Take the mean of the queuing times for the nurse across patients in
# this run of the model.
self.mean_q_time_nurse = self.results_df["Q Time Nurse"].mean()
# The run method starts up the DES entity generators, runs the simulation,
# and in turns calls anything we need to generate results for the run
def run(self):
# Start up our DES entity generators that create new patients. We've
# only got one in this model, but we'd need to do this for each one if
# we had multiple generators.
self.env.process(self.generator_patient_arrivals())
# Run the model for the duration specified in g class
self.env.run(until=g.sim_duration)
# Now the simulation run has finished, call the method that calculates
# run results
self.calculate_run_results()
# Print the run number with the patient-level results from this run of
# the model
print (f"Run Number {self.run_number}")
print (self.results_df)
# Class representing a Trial for our simulation - a batch of simulation runs.
class Trial:
# The constructor sets up a pandas dataframe that will store the key
# results from each run (just the mean queuing time for the nurse here)
# against run number, with run number as the index.
def __init__(self):
self.df_trial_results = pd.DataFrame()
self.df_trial_results["Run Number"] = [0]
self.df_trial_results["Mean Q Time Nurse"] = [0.0]
self.df_trial_results.set_index("Run Number", inplace=True)
# Method to print out the results from the trial. In real world models,
# you'd likely save them as well as (or instead of) printing them
def print_trial_results(self):
print ("Trial Results")
print (self.df_trial_results)
# Method to run a trial
def run_trial(self):
# Run the simulation for the number of runs specified in g class.
# For each run, we create a new instance of the Model class and call its
# run method, which sets everything else in motion. Once the run has
# completed, we grab out the stored run results (just mean queuing time
# here) and store it against the run number in the trial results
# dataframe.
for run in range(g.number_of_runs):
my_model = Model(run)
my_model.run()
self.df_trial_results.loc[run] = [my_model.mean_q_time_nurse]
# Once the trial (ie all runs) has completed, print the final results
self.print_trial_results()
Running the model, we’ll see the following output…
Patient 1 arrives at: 0.000.
Patient 1 waits for 0.000 and is seen at 0. Consultation length: 10.724.
Patient 2 arrives at: 0.150.
Patient 3 arrives at: 5.309.
Patient 4 arrives at: 6.371.
Patient 5 arrives at: 7.962.
Patient 2 waits for 10.574 and is seen at 10.724016899760096. Consultation length: 0.200.
Patient 3 waits for 5.615 and is seen at 10.923618999278593. Consultation length: 0.086.
Patient 4 waits for 4.639 and is seen at 11.010046767046207. Consultation length: 8.038.
Patient 5 waits for 11.086 and is seen at 19.04810698254277. Consultation length: 2.322.
Patient 6 arrives at: 23.236.
Patient 6 waits for 0.000 and is seen at 23.23637375971412. Consultation length: 2.582.
Patient 7 arrives at: 24.742.
Patient 7 waits for 1.076 and is seen at 25.818359178321167. Consultation length: 4.804.
Patient 8 arrives at: 28.219.
Patient 8 waits for 2.404 and is seen at 30.622399428034093. Consultation length: 0.806.
Patient 9 arrives at: 31.606.
Patient 9 waits for 0.000 and is seen at 31.6058048759237. Consultation length: 5.891.
Patient 10 arrives at: 37.518.
Patient 10 waits for 0.000 and is seen at 37.518138074459074. Consultation length: 4.926.
Patient 11 arrives at: 43.757.
Patient 11 waits for 0.000 and is seen at 43.75672280585052. Consultation length: 3.754.
Patient 12 arrives at: 47.837.
Patient 12 waits for 0.000 and is seen at 47.83716109080116. Consultation length: 7.447.
Patient 13 arrives at: 51.575.
Patient 13 waits for 3.709 and is seen at 55.283961262528265. Consultation length: 1.232.
Patient 14 arrives at: 56.303.
Patient 14 waits for 0.212 and is seen at 56.51591826312143. Consultation length: 10.233.
Patient 15 arrives at: 57.747.
Patient 16 arrives at: 60.308.
Patient 17 arrives at: 61.744.
Patient 15 waits for 9.002 and is seen at 66.74892482841393. Consultation length: 2.210.
Patient 18 arrives at: 67.211.
Patient 19 arrives at: 67.843.
Patient 16 waits for 8.651 and is seen at 68.95866493071166. Consultation length: 0.340.
Patient 17 waits for 7.555 and is seen at 69.2986542797349. Consultation length: 5.792.
Patient 20 arrives at: 72.047.
Patient 21 arrives at: 74.530.
Patient 18 waits for 7.880 and is seen at 75.09038363936328. Consultation length: 11.580.
Patient 22 arrives at: 76.224.
Patient 23 arrives at: 76.378.
Patient 24 arrives at: 78.546.
Patient 25 arrives at: 84.580.
Patient 19 waits for 18.827 and is seen at 86.66992435703258. Consultation length: 0.061.
Patient 20 waits for 14.684 and is seen at 86.73073461746797. Consultation length: 3.013.
Patient 21 waits for 15.213 and is seen at 89.74391099742128. Consultation length: 0.320.
Patient 22 waits for 13.840 and is seen at 90.06408226252125. Consultation length: 3.324.
Patient 23 waits for 17.010 and is seen at 93.38789010749436. Consultation length: 0.945.
Patient 24 waits for 15.787 and is seen at 94.33269125218459. Consultation length: 7.140.
Patient 25 waits for 16.893 and is seen at 101.47273147144615. Consultation length: 1.942.
Patient 26 arrives at: 101.587.
Patient 27 arrives at: 102.331.
Patient 26 waits for 1.828 and is seen at 103.4147428371654. Consultation length: 2.334.
Patient 28 arrives at: 104.156.
Patient 27 waits for 3.418 and is seen at 105.74893838517372. Consultation length: 6.063.
Patient 29 arrives at: 106.335.
Patient 30 arrives at: 108.924.
Patient 31 arrives at: 109.592.
Patient 32 arrives at: 110.299.
Patient 33 arrives at: 110.538.
Patient 28 waits for 7.656 and is seen at 111.81212331183262. Consultation length: 0.322.
Patient 29 waits for 5.799 and is seen at 112.13404701043532. Consultation length: 6.933.
Patient 34 arrives at: 117.495.
Patient 30 waits for 10.143 and is seen at 119.06730549093024. Consultation length: 0.289.
Patient 31 waits for 9.764 and is seen at 119.35603766629579. Consultation length: 2.345.
Run Number 0
Q Time Nurse Time with Nurse
Patient ID
1 0.000000 10.724017
2 10.574291 0.199602
3 5.614699 0.086428
4 4.638661 8.038060
5 11.086170 2.321768
6 0.000000 2.581985
7 1.076460 4.804040
8 2.403821 0.806189
9 0.000000 5.891071
10 0.000000 4.926228
11 0.000000 3.754320
12 0.000000 7.446800
13 3.709055 1.231957
14 0.212443 10.233007
15 9.001625 2.209740
16 8.651064 0.339989
17 7.554838 5.791729
18 7.879701 11.579541
19 18.826528 0.060810
20 14.684001 3.013176
21 15.213417 0.320171
22 13.839593 3.323808
23 17.009595 0.944801
24 15.786730 7.140040
25 16.892926 1.942011
26 1.827984 2.334196
27 3.417988 6.063185
28 7.656284 0.321924
29 5.799302 6.933258
30 10.142826 0.288732
31 9.764481 2.344893
Trial Results
Mean Q Time Nurse
Run Number
0 7.20208
trace()
to control the print()
statementsThis output is helpful when debugging a single run of the model, but the behaviour is undesirable when running multiple replications.
We can write a function which will toggle whether to run the print()
statements or not.
In our parameter class, we add a parameter trace
which will control whether the print()
statements are executed or not.
class g:
patient_inter = 5
mean_n_consult_time = 6
number_of_nurses = 1
sim_duration = 120
number_of_runs = 1 ##NEW - single run
trace = True ##NEW - controls whether the print statements are executed
trace
parameter which will determine whether the print statements are executed.
We then define a new function trace()
which will only run if g.trace
is true.
##NEW
def trace(msg):
"""
If TRUE will return all patient-level message outputs.
Arguments:
msg (string):
Message output.
"""
if g.trace:
print(msg)
trace()
which will print messages if g.trace
is true.
Then, in our model, we alter our print()
statements so that the message is input to the trace()
function.
def generator_patient_arrivals(self):
while True:
self.patient_counter += 1
p = Patient(self.patient_counter)
##NEW - Print message stating patient ID and arrival time
trace(f"Patient {p.id} arrives at: {self.env.now:.3f}.")
self.env.process(self.attend_clinic(p))
sampled_inter = random.expovariate(1.0 / g.patient_inter)
yield self.env.timeout(sampled_inter)
def attend_clinic(self, patient):
start_q_nurse = self.env.now
with self.nurse.request() as req:
yield req
end_q_nurse = self.env.now
patient.q_time_nurse = end_q_nurse - start_q_nurse
sampled_nurse_act_time = random.expovariate(1.0 /
g.mean_n_consult_time)
##NEW - Print message with patient wait and consultation length
trace(
f"Patient {patient.id} waits for {patient.q_time_nurse:.3f} " +
f"and is seen at {end_q_nurse}. Consultation length: " +
f"{sampled_nurse_act_time:.3f}.")
self.results_df.at[patient.id, "Q Time Nurse"] = (
patient.q_time_nurse)
self.results_df.at[patient.id, "Time with Nurse"] = (
sampled_nurse_act_time)
yield self.env.timeout(sampled_nurse_act_time)
print()
statements to trace()
.
The full updated code for the model is given below.
import simpy
import random
import pandas as pd
# Class to store global parameter values. We don't create an instance of this
# class - we just refer to the class blueprint itself to access the numbers
# inside.
class g:
patient_inter = 5
mean_n_consult_time = 6
number_of_nurses = 1
sim_duration = 120
number_of_runs = 1 ##NEW - single run
trace = True ##NEW - controls whether the print statements are executed
##NEW
def trace(msg):
"""
If TRUE will return all patient-level message outputs.
Arguments:
msg (string):
Message output.
"""
if g.trace:
print(msg)
# Class representing patients coming in to the clinic. Here, patients have
# two attributes that they carry with them - their ID, and the amount of time
# they spent queuing for the nurse. The ID is passed in when a new patient is
# created.
class Patient:
def __init__(self, p_id):
self.id = p_id
self.q_time_nurse = 0
# Class representing our model of the clinic.
class Model:
# Constructor to set up the model for a run. We pass in a run number when
# we create a new model.
def __init__(self, run_number):
# Create a SimPy environment in which everything will live
self.env = simpy.Environment()
# Create a patient counter (which we'll use as a patient ID)
self.patient_counter = 0
# Create a SimPy resource to represent a nurse, that will live in the
# environment created above. The number of this resource we have is
# specified by the capacity, and we grab this value from our g class.
self.nurse = simpy.Resource(self.env, capacity=g.number_of_nurses)
# Store the passed in run number
self.run_number = run_number
# Create a new Pandas DataFrame that will store some results against
# the patient ID (which we'll use as the index).
self.results_df = pd.DataFrame()
self.results_df["Patient ID"] = [1]
self.results_df["Q Time Nurse"] = [0.0]
self.results_df["Time with Nurse"] = [0.0]
self.results_df.set_index("Patient ID", inplace=True)
# Create an attribute to store the mean queuing time for the nurse
# across this run of the model
self.mean_q_time_nurse = 0
# A generator function that represents the DES generator for patient
# arrivals
def generator_patient_arrivals(self):
# We use an infinite loop here to keep doing this indefinitely whilst
# the simulation runs
while True:
# Increment the patient counter by 1 (this means our first patient
# will have an ID of 1)
self.patient_counter += 1
# Create a new patient - an instance of the Patient Class we
# defined above. Remember, we pass in the ID when creating a
# patient - so here we pass the patient counter to use as the ID.
p = Patient(self.patient_counter)
##NEW - Print message stating patient ID and arrival time
trace(f"Patient {p.id} arrives at: {self.env.now:.3f}.")
# Tell SimPy to start up the attend_clinic generator function with
# this patient (the generator function that will model the
# patient's journey through the system)
self.env.process(self.attend_clinic(p))
# Randomly sample the time to the next patient arriving. Here, we
# sample from an exponential distribution (common for inter-arrival
# times), and pass in a lambda value of 1 / mean. The mean
# inter-arrival time is stored in the g class.
sampled_inter = random.expovariate(1.0 / g.patient_inter)
# Freeze this instance of this function in place until the
# inter-arrival time we sampled above has elapsed. Note - time in
# SimPy progresses in "Time Units", which can represent anything
# you like (just make sure you're consistent within the model)
yield self.env.timeout(sampled_inter)
# A generator function that represents the pathway for a patient going
# through the clinic. Here the pathway is extremely simple - a patient
# arrives, waits to see a nurse, and then leaves.
# The patient object is passed in to the generator function so we can
# extract information from / record information to it
def attend_clinic(self, patient):
# Record the time the patient started queuing for a nurse
start_q_nurse = self.env.now
# This code says request a nurse resource, and do all of the following
# block of code with that nurse resource held in place (and therefore
# not usable by another patient)
with self.nurse.request() as req:
# Freeze the function until the request for a nurse can be met.
# The patient is currently queuing.
yield req
# When we get to this bit of code, control has been passed back to
# the generator function, and therefore the request for a nurse has
# been met. We now have the nurse, and have stopped queuing, so we
# can record the current time as the time we finished queuing.
end_q_nurse = self.env.now
# Calculate the time this patient was queuing for the nurse, and
# record it in the patient's attribute for this.
patient.q_time_nurse = end_q_nurse - start_q_nurse
# Now we'll randomly sample the time this patient with the nurse.
# Here, we use an Exponential distribution for simplicity, but you
# would typically use a Log Normal distribution for a real model
# (we'll come back to that). As with sampling the inter-arrival
# times, we grab the mean from the g class, and pass in 1 / mean
# as the lambda value.
sampled_nurse_act_time = random.expovariate(1.0 /
g.mean_n_consult_time)
##NEW - Print message with patient wait and consultation length
trace(
f"Patient {patient.id} waits for {patient.q_time_nurse:.3f} " +
f"and is seen at {end_q_nurse}. Consultation length: " +
f"{sampled_nurse_act_time:.3f}.")
# Here we'll store the queuing time for the nurse and the sampled
# time to spend with the nurse in the results DataFrame against the
# ID for this patient. In real world models, you may not want to
# bother storing the sampled activity times - but as this is a
# simple model, we'll do it here.
# We use a handy property of pandas called .at, which works a bit
# like .loc. .at allows us to access (and therefore change) a
# particular cell in our DataFrame by providing the row and column.
# Here, we specify the row as the patient ID (the index), and the
# column for the value we want to update for that patient.
self.results_df.at[patient.id, "Q Time Nurse"] = (
patient.q_time_nurse)
self.results_df.at[patient.id, "Time with Nurse"] = (
sampled_nurse_act_time)
# Freeze this function in place for the activity time we sampled
# above. This is the patient spending time with the nurse.
yield self.env.timeout(sampled_nurse_act_time)
# When the time above elapses, the generator function will return
# here. As there's nothing more that we've written, the function
# will simply end. This is a sink. We could choose to add
# something here if we wanted to record something - e.g. a counter
# for number of patients that left, recording something about the
# patients that left at a particular sink etc.
# This method calculates results over a single run. Here we just calculate
# a mean, but in real world models you'd probably want to calculate more.
def calculate_run_results(self):
# Take the mean of the queuing times for the nurse across patients in
# this run of the model.
self.mean_q_time_nurse = self.results_df["Q Time Nurse"].mean()
# The run method starts up the DES entity generators, runs the simulation,
# and in turns calls anything we need to generate results for the run
def run(self):
# Start up our DES entity generators that create new patients. We've
# only got one in this model, but we'd need to do this for each one if
# we had multiple generators.
self.env.process(self.generator_patient_arrivals())
# Run the model for the duration specified in g class
self.env.run(until=g.sim_duration)
# Now the simulation run has finished, call the method that calculates
# run results
self.calculate_run_results()
# Print the run number with the patient-level results from this run of
# the model
print (f"Run Number {self.run_number}")
print (self.results_df)
# Class representing a Trial for our simulation - a batch of simulation runs.
class Trial:
# The constructor sets up a pandas dataframe that will store the key
# results from each run (just the mean queuing time for the nurse here)
# against run number, with run number as the index.
def __init__(self):
self.df_trial_results = pd.DataFrame()
self.df_trial_results["Run Number"] = [0]
self.df_trial_results["Mean Q Time Nurse"] = [0.0]
self.df_trial_results.set_index("Run Number", inplace=True)
# Method to print out the results from the trial. In real world models,
# you'd likely save them as well as (or instead of) printing them
def print_trial_results(self):
print ("Trial Results")
print (self.df_trial_results)
# Method to run a trial
def run_trial(self):
# Run the simulation for the number of runs specified in g class.
# For each run, we create a new instance of the Model class and call its
# run method, which sets everything else in motion. Once the run has
# completed, we grab out the stored run results (just mean queuing time
# here) and store it against the run number in the trial results
# dataframe.
for run in range(g.number_of_runs):
my_model = Model(run)
my_model.run()
self.df_trial_results.loc[run] = [my_model.mean_q_time_nurse]
# Once the trial (ie all runs) has completed, print the final results
self.print_trial_results()
If we set g.trace = False
, we will see none of the patient messages are printed (and only our results from print_trial_results()
are).
Run Number 0
Q Time Nurse Time with Nurse
Patient ID
1 0.000000 0.515033
2 0.000000 16.105676
3 13.725436 6.471818
4 16.432416 1.761055
5 15.016513 8.896223
6 3.109157 0.881718
7 0.000000 8.643518
8 8.225063 9.053969
9 12.012615 6.340599
10 14.435000 2.933931
11 17.155207 0.689076
12 13.000332 0.799066
13 8.791537 0.923040
14 0.000000 4.718291
15 4.311246 9.033027
16 9.024959 1.792199
17 8.051979 2.034575
18 0.000000 4.907508
19 0.501197 2.779236
20 0.000000 4.841451
Trial Results
Mean Q Time Nurse
Run Number
0 7.189633
Meanwhile, if g.trace = True
…
Patient 1 arrives at: 0.000.
Patient 1 waits for 0.000 and is seen at 0. Consultation length: 0.949.
Patient 2 arrives at: 2.523.
Patient 2 waits for 0.000 and is seen at 2.5228663887406113. Consultation length: 0.583.
Patient 3 arrives at: 4.029.
Patient 3 waits for 0.000 and is seen at 4.028668137656526. Consultation length: 3.192.
Patient 4 arrives at: 9.921.
Patient 4 waits for 0.000 and is seen at 9.921203417940536. Consultation length: 0.018.
Patient 5 arrives at: 12.577.
Patient 5 waits for 0.000 and is seen at 12.577134658714382. Consultation length: 2.112.
Patient 6 arrives at: 13.534.
Patient 7 arrives at: 14.184.
Patient 6 waits for 1.156 and is seen at 14.689547869979073. Consultation length: 3.523.
Patient 7 waits for 4.029 and is seen at 18.212652512617925. Consultation length: 4.645.
Patient 8 arrives at: 18.886.
Patient 8 waits for 3.972 and is seen at 22.8581446432479. Consultation length: 7.315.
Patient 9 arrives at: 32.497.
Patient 9 waits for 0.000 and is seen at 32.49747626810915. Consultation length: 1.357.
Patient 10 arrives at: 34.010.
Patient 10 waits for 0.000 and is seen at 34.0102442456301. Consultation length: 2.387.
Patient 11 arrives at: 37.645.
Patient 11 waits for 0.000 and is seen at 37.64471969428669. Consultation length: 0.221.
Patient 12 arrives at: 41.007.
Patient 12 waits for 0.000 and is seen at 41.00722601865192. Consultation length: 16.012.
Patient 13 arrives at: 41.038.
Patient 13 waits for 15.981 and is seen at 57.01906588571015. Consultation length: 29.464.
Patient 14 arrives at: 68.510.
Patient 15 arrives at: 74.225.
Patient 14 waits for 17.974 and is seen at 86.48342300181554. Consultation length: 5.123.
Patient 16 arrives at: 87.589.
Patient 15 waits for 17.382 and is seen at 91.6067331429726. Consultation length: 0.934.
Patient 16 waits for 4.951 and is seen at 92.54060165275669. Consultation length: 7.241.
Patient 17 arrives at: 92.939.
Patient 18 arrives at: 94.248.
Patient 17 waits for 6.842 and is seen at 99.78143093758239. Consultation length: 6.249.
Patient 19 arrives at: 100.160.
Patient 18 waits for 11.782 and is seen at 106.03032597390029. Consultation length: 5.033.
Patient 20 arrives at: 110.124.
Patient 19 waits for 10.904 and is seen at 111.06378953900895. Consultation length: 6.669.
Patient 21 arrives at: 114.464.
Patient 20 waits for 7.609 and is seen at 117.73288671184125. Consultation length: 2.026.
Patient 21 waits for 5.295 and is seen at 119.75916138885063. Consultation length: 8.047.
Run Number 0
Q Time Nurse Time with Nurse
Patient ID
1 0.000000 0.949483
2 0.000000 0.583333
3 0.000000 3.191868
4 0.000000 0.018307
5 0.000000 2.112413
6 1.155695 3.523105
7 4.028798 4.645492
8 3.971928 7.315200
9 0.000000 1.356641
10 0.000000 2.387203
11 0.000000 0.221378
12 0.000000 16.011840
13 15.981472 29.464357
14 17.973793 5.123310
15 17.381797 0.933869
16 4.951300 7.240829
17 6.842006 6.248895
18 11.782468 5.033464
19 10.903800 6.669097
20 7.608666 2.026275
21 5.295051 8.047447
Trial Results
Mean Q Time Nurse
Run Number
0 5.136989
The logging module is a step up from the use of print()
statements.
It enables us to choose between printing the messages or saving them to a .log
file.
It can also be extended with different types of log message (e.g. INFO
, WARNING
, ERROR
), customised with different colours, and more. Here, we just demonstrate a simple implementation.
First, we need to update our imports.
import logging ##NEW
import sys ##NEW
import time ##NEW
import simpy
import random
import pandas as pd
logging
which we will use to create logs.
sys
which is required when setting up the handler for logging to the console.
time
which we will use is it is desired to save logs to a file with the current date and time.
Next, we will create a new class called SimLogger
. This accepts three inputs when setting up:
log_to_console
- which determines whether to print log messages.log_to_file
- which determines whether to save the log to a file.file_path
- if saving to file, the path to use.The class configures handlers for logging (_configure_logging()
), and then has a log()
method which will be used to save messages to the log in our model.
##NEW
class SimLogger:
"""
Provides log of events as the simulation runs.
"""
def __init__(self, log_to_console=False, log_to_file=False,
file_path=("../outputs/logs/" +
f"{time.strftime('%Y-%m-%d_%H-%M-%S')}.log")
):
"""
Initialise the Logger class.
Arguments:
log_to_console (boolean):
Whether to print log messages to the console.
log_to_file (boolean):
Whether to save log to a file.
file_path (str):
Path to save log to file. Note, if you use an existing .log
file name, it will append to that log. Defaults to filename
based on current date and time, and folder '../outputs/log/'.
"""
self.log_to_console = log_to_console
self.log_to_file = log_to_file
self.file_path = file_path
self.logger = None
# If logging enabled (either printing to console, file or both), then
# create logger and configure settings
if self.log_to_console or self.log_to_file:
self.logger = logging.getLogger(__name__)
self._configure_logging()
def _configure_logging(self):
"""
Configure the logger.
"""
# Ensure any existing handlers are removed to avoid duplication
for handler in self.logger.handlers[:]:
self.logger.removeHandler(handler)
# Add handlers for saving messages to file and/or printing to console
handlers = []
if self.log_to_file:
# In write mode, meaning will overwrite existing log of same name
# (append mode 'a' would add to the end of the log)
handlers.append(logging.FileHandler(self.file_path, mode='w'))
if self.log_to_console:
handlers.append(logging.StreamHandler(sys.stdout))
# Add handlers directly to the logger
for handler in handlers:
self.logger.addHandler(handler)
# Set logging level and format. If don't set level info, it would
# only show log messages which are warning, error or critical.
self.logger.setLevel(logging.INFO)
formatter = logging.Formatter("%(message)s")
for handler in handlers:
handler.setFormatter(formatter)
def log(self, msg):
"""
Log a message if logging is enabled.
Arguments:
msg (str):
Message to log.
"""
if self.log_to_console or self.log_to_file:
self.logger.info(msg)
SimLogger
class which accepts three inputs, and will call the _configure_logging()
method
print()
or trace()
above.
In our g
class, we will add an instance of the logging class.
class g:
patient_inter = 5
mean_n_consult_time = 6
number_of_nurses = 1
sim_duration = 120
number_of_runs = 1 ##NEW - single run
##NEW - instance of the SimLogger class
logger = SimLogger(log_to_console = True,
log_to_file = True,
file_path = "./outputs/example_log.log")
SimLogger
instance.
For our messages in Model
, we now change print()
or trace()
instead to g.logger.log()
.
def generator_patient_arrivals(self):
while True:
self.patient_counter += 1
p = Patient(self.patient_counter)
##NEW - Log message stating patient ID and arrival time
g.logger.log(f"Patient {p.id} arrives at: {self.env.now:.3f}.")
self.env.process(self.attend_clinic(p))
sampled_inter = random.expovariate(1.0 / g.patient_inter)
yield self.env.timeout(sampled_inter)
def attend_clinic(self, patient):
start_q_nurse = self.env.now
with self.nurse.request() as req:
yield req
end_q_nurse = self.env.now
patient.q_time_nurse = end_q_nurse - start_q_nurse
sampled_nurse_act_time = random.expovariate(1.0 /
g.mean_n_consult_time)
##NEW - Log message with patient wait and consultation length
g.logger.log(
f"Patient {patient.id} waits for {patient.q_time_nurse:.3f} " +
f"and is seen at {end_q_nurse}. Consultation length: " +
f"{sampled_nurse_act_time:.3f}.")
self.results_df.at[patient.id, "Q Time Nurse"] = (
patient.q_time_nurse)
self.results_df.at[patient.id, "Time with Nurse"] = (
sampled_nurse_act_time)
yield self.env.timeout(sampled_nurse_act_time)
print()
/trace()
to log()
.
The full updated code for the model is given below.
import logging ##NEW
import sys ##NEW
import time ##NEW
import simpy
import random
import pandas as pd
##NEW
class SimLogger:
"""
Provides log of events as the simulation runs.
"""
def __init__(self, log_to_console=False, log_to_file=False,
file_path=("../outputs/logs/" +
f"{time.strftime('%Y-%m-%d_%H-%M-%S')}.log")
):
"""
Initialise the Logger class.
Arguments:
log_to_console (boolean):
Whether to print log messages to the console.
log_to_file (boolean):
Whether to save log to a file.
file_path (str):
Path to save log to file. Note, if you use an existing .log
file name, it will append to that log. Defaults to filename
based on current date and time, and folder '../outputs/log/'.
"""
self.log_to_console = log_to_console
self.log_to_file = log_to_file
self.file_path = file_path
self.logger = None
# If logging enabled (either printing to console, file or both), then
# create logger and configure settings
if self.log_to_console or self.log_to_file:
self.logger = logging.getLogger(__name__)
self._configure_logging()
def _configure_logging(self):
"""
Configure the logger.
"""
# Ensure any existing handlers are removed to avoid duplication
for handler in self.logger.handlers[:]:
self.logger.removeHandler(handler)
# Add handlers for saving messages to file and/or printing to console
handlers = []
if self.log_to_file:
# In write mode, meaning will overwrite existing log of same name
# (append mode 'a' would add to the end of the log)
handlers.append(logging.FileHandler(self.file_path, mode='w'))
if self.log_to_console:
handlers.append(logging.StreamHandler(sys.stdout))
# Add handlers directly to the logger
for handler in handlers:
self.logger.addHandler(handler)
# Set logging level and format. If don't set level info, it would
# only show log messages which are warning, error or critical.
self.logger.setLevel(logging.INFO)
formatter = logging.Formatter("%(message)s")
for handler in handlers:
handler.setFormatter(formatter)
def log(self, msg):
"""
Log a message if logging is enabled.
Arguments:
msg (str):
Message to log.
"""
if self.log_to_console or self.log_to_file:
self.logger.info(msg)
# Class to store global parameter values. We don't create an instance of this
# class - we just refer to the class blueprint itself to access the numbers
# inside.
class g:
patient_inter = 5
mean_n_consult_time = 6
number_of_nurses = 1
sim_duration = 120
number_of_runs = 1 ##NEW - single run
##NEW - instance of the SimLogger class
logger = SimLogger(log_to_console = True,
log_to_file = True,
file_path = "./outputs/example_log.log")
# Class representing patients coming in to the clinic. Here, patients have
# two attributes that they carry with them - their ID, and the amount of time
# they spent queuing for the nurse. The ID is passed in when a new patient is
# created.
class Patient:
def __init__(self, p_id):
self.id = p_id
self.q_time_nurse = 0
# Class representing our model of the clinic.
class Model:
# Constructor to set up the model for a run. We pass in a run number when
# we create a new model.
def __init__(self, run_number):
# Create a SimPy environment in which everything will live
self.env = simpy.Environment()
# Create a patient counter (which we'll use as a patient ID)
self.patient_counter = 0
# Create a SimPy resource to represent a nurse, that will live in the
# environment created above. The number of this resource we have is
# specified by the capacity, and we grab this value from our g class.
self.nurse = simpy.Resource(self.env, capacity=g.number_of_nurses)
# Store the passed in run number
self.run_number = run_number
# Create a new Pandas DataFrame that will store some results against
# the patient ID (which we'll use as the index).
self.results_df = pd.DataFrame()
self.results_df["Patient ID"] = [1]
self.results_df["Q Time Nurse"] = [0.0]
self.results_df["Time with Nurse"] = [0.0]
self.results_df.set_index("Patient ID", inplace=True)
# Create an attribute to store the mean queuing time for the nurse
# across this run of the model
self.mean_q_time_nurse = 0
# A generator function that represents the DES generator for patient
# arrivals
def generator_patient_arrivals(self):
# We use an infinite loop here to keep doing this indefinitely whilst
# the simulation runs
while True:
# Increment the patient counter by 1 (this means our first patient
# will have an ID of 1)
self.patient_counter += 1
# Create a new patient - an instance of the Patient Class we
# defined above. Remember, we pass in the ID when creating a
# patient - so here we pass the patient counter to use as the ID.
p = Patient(self.patient_counter)
##NEW - Log message stating patient ID and arrival time
g.logger.log(f"Patient {p.id} arrives at: {self.env.now:.3f}.")
# Tell SimPy to start up the attend_clinic generator function with
# this patient (the generator function that will model the
# patient's journey through the system)
self.env.process(self.attend_clinic(p))
# Randomly sample the time to the next patient arriving. Here, we
# sample from an exponential distribution (common for inter-arrival
# times), and pass in a lambda value of 1 / mean. The mean
# inter-arrival time is stored in the g class.
sampled_inter = random.expovariate(1.0 / g.patient_inter)
# Freeze this instance of this function in place until the
# inter-arrival time we sampled above has elapsed. Note - time in
# SimPy progresses in "Time Units", which can represent anything
# you like (just make sure you're consistent within the model)
yield self.env.timeout(sampled_inter)
# A generator function that represents the pathway for a patient going
# through the clinic. Here the pathway is extremely simple - a patient
# arrives, waits to see a nurse, and then leaves.
# The patient object is passed in to the generator function so we can
# extract information from / record information to it
def attend_clinic(self, patient):
# Record the time the patient started queuing for a nurse
start_q_nurse = self.env.now
# This code says request a nurse resource, and do all of the following
# block of code with that nurse resource held in place (and therefore
# not usable by another patient)
with self.nurse.request() as req:
# Freeze the function until the request for a nurse can be met.
# The patient is currently queuing.
yield req
# When we get to this bit of code, control has been passed back to
# the generator function, and therefore the request for a nurse has
# been met. We now have the nurse, and have stopped queuing, so we
# can record the current time as the time we finished queuing.
end_q_nurse = self.env.now
# Calculate the time this patient was queuing for the nurse, and
# record it in the patient's attribute for this.
patient.q_time_nurse = end_q_nurse - start_q_nurse
# Now we'll randomly sample the time this patient with the nurse.
# Here, we use an Exponential distribution for simplicity, but you
# would typically use a Log Normal distribution for a real model
# (we'll come back to that). As with sampling the inter-arrival
# times, we grab the mean from the g class, and pass in 1 / mean
# as the lambda value.
sampled_nurse_act_time = random.expovariate(1.0 /
g.mean_n_consult_time)
##NEW - Log message with patient wait and consultation length
g.logger.log(
f"Patient {patient.id} waits for {patient.q_time_nurse:.3f} " +
f"and is seen at {end_q_nurse}. Consultation length: " +
f"{sampled_nurse_act_time:.3f}.")
# Here we'll store the queuing time for the nurse and the sampled
# time to spend with the nurse in the results DataFrame against the
# ID for this patient. In real world models, you may not want to
# bother storing the sampled activity times - but as this is a
# simple model, we'll do it here.
# We use a handy property of pandas called .at, which works a bit
# like .loc. .at allows us to access (and therefore change) a
# particular cell in our DataFrame by providing the row and column.
# Here, we specify the row as the patient ID (the index), and the
# column for the value we want to update for that patient.
self.results_df.at[patient.id, "Q Time Nurse"] = (
patient.q_time_nurse)
self.results_df.at[patient.id, "Time with Nurse"] = (
sampled_nurse_act_time)
# Freeze this function in place for the activity time we sampled
# above. This is the patient spending time with the nurse.
yield self.env.timeout(sampled_nurse_act_time)
# When the time above elapses, the generator function will return
# here. As there's nothing more that we've written, the function
# will simply end. This is a sink. We could choose to add
# something here if we wanted to record something - e.g. a counter
# for number of patients that left, recording something about the
# patients that left at a particular sink etc.
# This method calculates results over a single run. Here we just calculate
# a mean, but in real world models you'd probably want to calculate more.
def calculate_run_results(self):
# Take the mean of the queuing times for the nurse across patients in
# this run of the model.
self.mean_q_time_nurse = self.results_df["Q Time Nurse"].mean()
# The run method starts up the DES entity generators, runs the simulation,
# and in turns calls anything we need to generate results for the run
def run(self):
# Start up our DES entity generators that create new patients. We've
# only got one in this model, but we'd need to do this for each one if
# we had multiple generators.
self.env.process(self.generator_patient_arrivals())
# Run the model for the duration specified in g class
self.env.run(until=g.sim_duration)
# Now the simulation run has finished, call the method that calculates
# run results
self.calculate_run_results()
# Print the run number with the patient-level results from this run of
# the model
print (f"Run Number {self.run_number}")
print (self.results_df)
# Class representing a Trial for our simulation - a batch of simulation runs.
class Trial:
# The constructor sets up a pandas dataframe that will store the key
# results from each run (just the mean queuing time for the nurse here)
# against run number, with run number as the index.
def __init__(self):
self.df_trial_results = pd.DataFrame()
self.df_trial_results["Run Number"] = [0]
self.df_trial_results["Mean Q Time Nurse"] = [0.0]
self.df_trial_results.set_index("Run Number", inplace=True)
# Method to print out the results from the trial. In real world models,
# you'd likely save them as well as (or instead of) printing them
def print_trial_results(self):
print ("Trial Results")
print (self.df_trial_results)
# Method to run a trial
def run_trial(self):
# Run the simulation for the number of runs specified in g class.
# For each run, we create a new instance of the Model class and call its
# run method, which sets everything else in motion. Once the run has
# completed, we grab out the stored run results (just mean queuing time
# here) and store it against the run number in the trial results
# dataframe.
for run in range(g.number_of_runs):
my_model = Model(run)
my_model.run()
self.df_trial_results.loc[run] = [my_model.mean_q_time_nurse]
# Once the trial (ie all runs) has completed, print the final results
self.print_trial_results()
We will run the model with log_to_console
and log_to_file
both enabled.
You’ll see that the logs are printed as before…
Patient 1 arrives at: 0.000.
Patient 1 waits for 0.000 and is seen at 0. Consultation length: 20.445.
Patient 2 arrives at: 3.228.
Patient 3 arrives at: 4.778.
Patient 4 arrives at: 11.542.
Patient 5 arrives at: 17.416.
Patient 6 arrives at: 18.391.
Patient 2 waits for 17.217 and is seen at 20.444720365535566. Consultation length: 1.084.
Patient 3 waits for 16.751 and is seen at 21.528518818320162. Consultation length: 14.875.
Patient 7 arrives at: 27.049.
Patient 8 arrives at: 28.655.
Patient 4 waits for 24.862 and is seen at 36.40390758388942. Consultation length: 4.679.
Patient 5 waits for 23.667 and is seen at 41.082495475966006. Consultation length: 2.006.
Patient 6 waits for 24.698 and is seen at 43.08886529597139. Consultation length: 7.335.
Patient 7 waits for 23.376 and is seen at 50.42416096680088. Consultation length: 5.859.
Patient 9 arrives at: 53.623.
Patient 10 arrives at: 55.255.
Patient 8 waits for 27.629 and is seen at 56.283553427328705. Consultation length: 12.748.
Patient 11 arrives at: 60.851.
Patient 12 arrives at: 63.044.
Patient 13 arrives at: 65.438.
Patient 14 arrives at: 67.370.
Patient 9 waits for 15.409 and is seen at 69.03203453539484. Consultation length: 2.009.
Patient 10 waits for 15.786 and is seen at 71.04113540869592. Consultation length: 4.370.
Patient 15 arrives at: 72.523.
Patient 16 arrives at: 73.250.
Patient 11 waits for 14.560 and is seen at 75.41091250233481. Consultation length: 3.733.
Patient 12 waits for 16.100 and is seen at 79.14375033636888. Consultation length: 3.217.
Patient 17 arrives at: 79.729.
Patient 18 arrives at: 82.031.
Patient 19 arrives at: 82.323.
Patient 13 waits for 16.923 and is seen at 82.36092079540045. Consultation length: 11.472.
Patient 20 arrives at: 90.199.
Patient 14 waits for 26.463 and is seen at 93.83269016595533. Consultation length: 1.226.
Patient 15 waits for 22.536 and is seen at 95.05869993983505. Consultation length: 9.555.
Patient 21 arrives at: 98.437.
Patient 22 arrives at: 99.959.
Patient 23 arrives at: 102.641.
Patient 16 waits for 31.363 and is seen at 104.61344903851816. Consultation length: 1.378.
Patient 17 waits for 26.263 and is seen at 105.99147015629434. Consultation length: 16.713.
Patient 24 arrives at: 113.694.
Patient 25 arrives at: 118.770.
Run Number 0
Q Time Nurse Time with Nurse
Patient ID
1 0.000000 20.444720
2 17.217182 1.083798
3 16.750729 14.875389
4 24.862249 4.678588
5 23.666877 2.006370
6 24.698140 7.335296
7 23.375503 5.859392
8 27.629033 12.748481
9 15.409151 2.009101
10 15.785961 4.369777
11 14.560386 3.732838
12 16.099564 3.217170
13 16.922809 11.471769
14 26.462548 1.226010
15 22.535522 9.554749
16 31.363139 1.378021
17 26.262731 16.713489
Trial Results
Mean Q Time Nurse
Run Number
0 20.211854
…but also, a .log
file has been generated containing the logs:
Patient 1 arrives at: 0.000.
Patient 1 waits for 0.000 and is seen at 0. Consultation length: 20.445.
Patient 2 arrives at: 3.228.
Patient 3 arrives at: 4.778.
Patient 4 arrives at: 11.542.
Patient 5 arrives at: 17.416.
Patient 6 arrives at: 18.391.
Patient 2 waits for 17.217 and is seen at 20.444720365535566. Consultation length: 1.084.
Patient 3 waits for 16.751 and is seen at 21.528518818320162. Consultation length: 14.875.
Patient 7 arrives at: 27.049.
Patient 8 arrives at: 28.655.
Patient 4 waits for 24.862 and is seen at 36.40390758388942. Consultation length: 4.679.
Patient 5 waits for 23.667 and is seen at 41.082495475966006. Consultation length: 2.006.
Patient 6 waits for 24.698 and is seen at 43.08886529597139. Consultation length: 7.335.
Patient 7 waits for 23.376 and is seen at 50.42416096680088. Consultation length: 5.859.
Patient 9 arrives at: 53.623.
Patient 10 arrives at: 55.255.
Patient 8 waits for 27.629 and is seen at 56.283553427328705. Consultation length: 12.748.
Patient 11 arrives at: 60.851.
Patient 12 arrives at: 63.044.
Patient 13 arrives at: 65.438.
Patient 14 arrives at: 67.370.
Patient 9 waits for 15.409 and is seen at 69.03203453539484. Consultation length: 2.009.
Patient 10 waits for 15.786 and is seen at 71.04113540869592. Consultation length: 4.370.
Patient 15 arrives at: 72.523.
Patient 16 arrives at: 73.250.
Patient 11 waits for 14.560 and is seen at 75.41091250233481. Consultation length: 3.733.
Patient 12 waits for 16.100 and is seen at 79.14375033636888. Consultation length: 3.217.
Patient 17 arrives at: 79.729.
Patient 18 arrives at: 82.031.
Patient 19 arrives at: 82.323.
Patient 13 waits for 16.923 and is seen at 82.36092079540045. Consultation length: 11.472.
Patient 20 arrives at: 90.199.
Patient 14 waits for 26.463 and is seen at 93.83269016595533. Consultation length: 1.226.
Patient 15 waits for 22.536 and is seen at 95.05869993983505. Consultation length: 9.555.
Patient 21 arrives at: 98.437.
Patient 22 arrives at: 99.959.
Patient 23 arrives at: 102.641.
Patient 16 waits for 31.363 and is seen at 104.61344903851816. Consultation length: 1.378.
Patient 17 waits for 26.263 and is seen at 105.99147015629434. Consultation length: 16.713.
Patient 24 arrives at: 113.694.
Patient 25 arrives at: 118.770.
For a more detailed logging implementation, see the Python DES RAP Template - the file notebooks/logs.ipynb is a good place to start.
That implementation includes:
sanitise_object
function which removes object references when logging things like a simpy.Resource
.rich
module.Testing is the process of evaluating a model to ensure it works as expected, gives reliable results, and can handle different conditions.
By checking for errors and unexpected results, it helps improve the quality of the model, catch errors and prevent future issues.
Testing is explored in more detail in its own chapter: Chapter 29.
Building up our own event logs give us a very clear picture of what is happening to every entity throughout our model.
They are a valuable debugging technique, and by structuring them correctly, we can start to build up a bank of code that can be used to debug very different models with no or minimal changes to our code that processes the event logs. They can also then be used for building animated visuals of the flow of entities through our model.
As this is a more involved approach, it has been placed in its own chapter here.