import random
import numpy as np
import pandas as pd
import simpy
from sim_tools.distributions import Exponential, Lognormal
from vidigi.resources import VidigiStore
from vidigi.logging import EventLogger
from vidigi.animation import animate_activity_log
from vidigi.utils import EventPosition, create_event_position_df
import plotly.io as pio
pio.renderers.default = "notebook"Feature Example: Event Logging Helpers
version 0.5.0 of vidigi added an EventLogger class, with various helper methods to simplify the process of generating the event logs that vidigi requires for the animation process.
In this notebook, we will add this logging into a simulation, also making use of the VidigiStore and its .populate() method to generate resources that have an ID attribute, allowing the vidigi animations to show individuals using a consistent resource.
Simple Example - 1 Resource Type, No Branching
In this example, we use the ‘HSMA’ style of model writing, with g, Patient, Model and Trial classes.
Only the Model and Trial classes are modified to make use of the Vidigi EventLogger.
Show the global parameter class code
# 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:
'''
Create a scenario to parameterise the simulation model
Parameters:
-----------
random_number_set: int, optional (default=DEFAULT_RNG_SET)
Set to control the initial seeds of each stream of pseudo
random numbers used in the model.
n_cubicles: int
The number of treatment cubicles
trauma_treat_mean: float
Mean of the trauma cubicle treatment distribution (Lognormal)
trauma_treat_var: float
Variance of the trauma cubicle treatment distribution (Lognormal)
arrival_rate: float
Set the mean of the exponential distribution that is used to sample the
inter-arrival time of patients
sim_duration: int
The number of time units the simulation will run for
number_of_runs: int
The number of times the simulation will be run with different random number streams
'''
random_number_set = 42
n_cubicles = 4
trauma_treat_mean = 40
trauma_treat_var = 5
arrival_rate = 5
sim_duration = 600
number_of_runs = 100Show the patient class code
class Patient:
'''
Class defining details for a patient entity
'''
def __init__(self, p_id):
'''
Constructor method
Params:
-----
identifier: int
a numeric identifier for the patient.
'''
self.id = p_id
self.arrival = -np.inf
self.wait_treat = -np.inf
self.total_time = -np.inf
self.treat_duration = -np.infIn the model class, we add the EventLogger as an attribute, then use the EventLogger’s methods to record the steps patients take in the model.
# Class representing our model of the clinic.
class Model:
'''
Simulates the simplest minor treatment process for a patient
1. Arrive
2. Examined/treated by nurse when one available
3. Discharged
'''
# 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()
# Store the passed in run number
self.run_number = run_number
# By passing in the env we've created, the logger will default to the simulation
# time when populating the time column of our event logs
self.logger = EventLogger(env=self.env, run_number=self.run_number)
# Create a patient counter (which we'll use as a patient ID)
self.patient_counter = 0
# Create an empty list to hold our patients
self.patients = []
# Create our distributions
self.init_distributions()
# Create our resources
self.init_resources()
def init_distributions(self):
self.patient_inter_arrival_dist = Exponential(mean = g.arrival_rate,
random_seed = self.run_number*g.random_number_set)
self.treat_dist = Lognormal(mean = g.trauma_treat_mean,
stdev = g.trauma_treat_var,
random_seed = self.run_number*g.random_number_set)
def init_resources(self):
'''
Init the number of resources
Resource list:
1. Nurses/treatment bays (same thing in this model)
'''
self.treatment_cubicles = VidigiStore(self.env, num_resources=g.n_cubicles)
# 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)
# Store patient in list for later easy access
self.patients.append(p)
# 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 = self.patient_inter_arrival_dist.sample()
# 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.
# 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):
self.logger.log_arrival(
entity_id=patient.id
)
self.arrival = self.env.now
self.logger.log_queue(
entity_id=patient.id,
event="treatment_wait_begins"
)
with self.treatment_cubicles.request() as req:
# Seize a treatment resource when available
treatment_resource = yield req
self.logger.log_resource_use_start(
entity_id=patient.id,
event="treatment_begins",
resource_id=treatment_resource.id_attribute
)
# sample treatment duration
self.treat_duration = self.treat_dist.sample()
yield self.env.timeout(self.treat_duration)
self.logger.log_resource_use_end(
entity_id=patient.id,
event="treatment_complete",
resource_id=treatment_resource.id_attribute
)
# total time in system
self.total_time = self.env.now - self.arrival
self.logger.log_departure(
entity_id=patient.id
)
# 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)We then create a trial class, where we will store the log file from each model run, allowing us to produce trial-level statistics.
class Trial:
def __init__(self):
self.all_event_logs = []
self.trial_results_df = pd.DataFrame()
self.run_trial()
# Method to run a trial
def run_trial(self):
print(f"{g.n_cubicles} nurses")
print("") ## Print a blank line
# 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(1, g.number_of_runs+1):
random.seed(run)
my_model = Model(run)
my_model.run()
self.all_event_logs.append(my_model.logger)
self.trial_results = pd.concat(
[run_results.to_dataframe() for run_results in self.all_event_logs]
)clinic_simulation = Trial()4 nurses
Log helpers
First, let’s extract just one of our logs from our trial. clinic_simulation.all_event_logs is a list, so clinic_simulation.all_event_logs[0] extracts the first run of our trial.
summary
The summary method gives a high-level overview of the number of entities, number of events, and overall duration of the observed events. This can be used as a quick sense-check.
clinic_simulation.all_event_logs[0].summary(){'total_events': 436,
'event_types': {'arrival_departure': 188,
'queue': 132,
'resource_use': 60,
'resource_use_end': 56},
'time_range': (0.0, 596.5008301269899),
'unique_entities': 132}
clinic_simulation.all_event_logs[1].summary(){'total_events': 399,
'event_types': {'arrival_departure': 169,
'queue': 112,
'resource_use': 61,
'resource_use_end': 57},
'time_range': (0.0, 598.3011331064208),
'unique_entities': 112}
get_events_by_entity
Here, we can pass in an entity ID to see its route through the model. This can help with debugging.
clinic_simulation.all_event_logs[0].get_events_by_entity(5)| entity_id | event_type | event | time | pathway | run_number | timestamp | resource_id | |
|---|---|---|---|---|---|---|---|---|
| 0 | 5 | arrival_departure | arrival | 37.024768 | None | 1 | None | NaN |
| 1 | 5 | queue | treatment_wait_begins | 37.024768 | None | 1 | None | NaN |
| 2 | 5 | resource_use | treatment_begins | 41.226014 | None | 1 | None | 1.0 |
| 3 | 5 | resource_use_end | treatment_complete | 72.356656 | None | 1 | None | 1.0 |
| 4 | 5 | arrival_departure | depart | 72.356656 | None | 1 | None | NaN |
Plotting entity timelines
We can also explore some simple plots of this.
clinic_simulation.all_event_logs[0].plot_entity_timeline(5) entity_id event_type event time
12 5 arrival_departure arrival 37.024768 \
13 5 queue treatment_wait_begins 37.024768
18 5 resource_use treatment_begins 41.226014
33 5 resource_use_end treatment_complete 72.356656
34 5 arrival_departure depart 72.356656
run_number resource_id
12 1 NaN
13 1 NaN
18 1 1.0
33 1 1.0
34 1 NaN
clinic_simulation.all_event_logs[0].plot_entity_timeline(5) entity_id event_type event time
12 5 arrival_departure arrival 37.024768 \
13 5 queue treatment_wait_begins 37.024768
18 5 resource_use treatment_begins 41.226014
33 5 resource_use_end treatment_complete 72.356656
34 5 arrival_departure depart 72.356656
run_number resource_id
12 1 NaN
13 1 NaN
18 1 1.0
33 1 1.0
34 1 NaN
clinic_simulation.trial_results| entity_id | event_type | event | time | run_number | resource_id | |
|---|---|---|---|---|---|---|
| 0 | 1 | arrival_departure | arrival | 0.000000 | 1 | NaN |
| 1 | 1 | queue | treatment_wait_begins | 0.000000 | 1 | NaN |
| 2 | 1 | resource_use | treatment_begins | 0.000000 | 1 | 1.0 |
| 3 | 2 | arrival_departure | arrival | 12.021043 | 1 | NaN |
| 4 | 2 | queue | treatment_wait_begins | 12.021043 | 1 | NaN |
| ... | ... | ... | ... | ... | ... | ... |
| 436 | 59 | arrival_departure | depart | 592.752922 | 100 | NaN |
| 437 | 62 | resource_use | treatment_begins | 592.752922 | 100 | 1.0 |
| 438 | 58 | resource_use_end | treatment_complete | 598.887715 | 100 | 4.0 |
| 439 | 58 | arrival_departure | depart | 598.887715 | 100 | NaN |
| 440 | 63 | resource_use | treatment_begins | 598.887715 | 100 | 4.0 |
42000 rows × 6 columns
There are two ways we could create our event position dataframe - either as a list of dictionaries, like so:
event_position_df = pd.DataFrame([
{'event': 'arrival',
'x': 50, 'y': 300,
'label': "Arrival" },
# Triage - minor and trauma
{'event': 'treatment_wait_begins',
'x': 205, 'y': 275,
'label': "Waiting for Treatment"},
{'event': 'treatment_begins',
'x': 205, 'y': 175,
'resource':'n_cubicles',
'label': "Being Treated"},
{'event': 'depart',
'x': 270, 'y': 70,
'label': "Exit"}
])Or using some vidigi helpers.
event_position_df = create_event_position_df([
EventPosition(event='arrival', x=50, y=300, label="Arrival"),
EventPosition(event='treatment_wait_begins', x=205, y=275, label="Waiting for Treatment"),
EventPosition(event='treatment_begins', x=205, y=175, label="Being Treated", resource='n_cubicles'),
EventPosition(event='depart', x=270, y=70, label="Exit")
])
event_position_df| event | x | y | label | resource | |
|---|---|---|---|---|---|
| 0 | arrival | 50 | 300 | Arrival | None |
| 1 | treatment_wait_begins | 205 | 275 | Waiting for Treatment | None |
| 2 | treatment_begins | 205 | 175 | Being Treated | n_cubicles |
| 3 | depart | 270 | 70 | Exit | None |
Let’s take a look at a sample of an event log for a single run.
clinic_simulation.trial_results[clinic_simulation.trial_results['run_number']==1]| entity_id | event_type | event | time | run_number | resource_id | |
|---|---|---|---|---|---|---|
| 0 | 1 | arrival_departure | arrival | 0.000000 | 1 | NaN |
| 1 | 1 | queue | treatment_wait_begins | 0.000000 | 1 | NaN |
| 2 | 1 | resource_use | treatment_begins | 0.000000 | 1 | 1.0 |
| 3 | 2 | arrival_departure | arrival | 12.021043 | 1 | NaN |
| 4 | 2 | queue | treatment_wait_begins | 12.021043 | 1 | NaN |
| ... | ... | ... | ... | ... | ... | ... |
| 431 | 56 | resource_use_end | treatment_complete | 596.143390 | 1 | 1.0 |
| 432 | 56 | arrival_departure | depart | 596.143390 | 1 | NaN |
| 433 | 60 | resource_use | treatment_begins | 596.143390 | 1 | 1.0 |
| 434 | 132 | arrival_departure | arrival | 596.500830 | 1 | NaN |
| 435 | 132 | queue | treatment_wait_begins | 596.500830 | 1 | NaN |
436 rows × 6 columns
animate_activity_log(
event_log=clinic_simulation.trial_results[clinic_simulation.trial_results['run_number']==1],
event_position_df= event_position_df,
entity_col_name="entity_id",
scenario=g(),
debug_mode=True,
setup_mode=False,
every_x_time_units=1,
include_play_button=True,
entity_icon_size=20,
resource_icon_size=20,
gap_between_entities=6,
gap_between_queue_rows=25,
plotly_height=700,
frame_duration=200,
plotly_width=1200,
override_x_max=300,
override_y_max=500,
limit_duration=g.sim_duration,
wrap_queues_at=25,
step_snapshot_max=125,
time_display_units="dhm",
display_stage_labels=False,
add_background_image="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_1_simplest_case/Simplest%20Model%20Background%20Image%20-%20Horizontal%20Layout.drawio.png",
)More Complex example - Multiple Resource Types, Branching
Again, in this example, we use the ‘HSMA’ style of model writing, with g, Patient, Model and Trial classes.
Only the Model and Trial classes are modified to make use of the Vidigi EventLogger.
However, here we have hidden all code due to its length - but you can click to expand it if you are interested in seeing how logging is added in this more complex example.
Show the import code
# Import additional required distributions
from sim_tools.distributions import Normal, Bernoulli, UniformShow the global parameter class code
# 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:
'''
Create a scenario to parameterise the simulation model
Parameters:
-----------
random_number_set: int, optional (default=DEFAULT_RNG_SET)
Set to control the initial seeds of each stream of pseudo
random numbers used in the model.
n_triage: int
The number of triage cubicles
n_reg: int
The number of registration clerks
n_exam: int
The number of examination rooms
n_trauma: int
The number of trauma bays for stablisation
n_cubicles_non_trauma_treat: int
The number of non-trauma treatment cubicles
n_cubicles_trauma_treat: int
The number of trauma treatment cubicles
triage_mean: float
Mean duration of the triage distribution (Exponential)
reg_mean: float
Mean duration of the registration distribution (Lognormal)
reg_var: float
Variance of the registration distribution (Lognormal)
exam_mean: float
Mean of the examination distribution (Normal)
exam_var: float
Variance of the examination distribution (Normal)
trauma_mean: float
Mean of the trauma stabilisation distribution (Exponential)
trauma_treat_mean: float
Mean of the trauma cubicle treatment distribution (Lognormal)
trauma_treat_var: float
Variance of the trauma cubicle treatment distribution (Lognormal)
non_trauma_treat_mean: float
Mean of the non trauma treatment distribution
non_trauma_treat_var: float
Variance of the non trauma treatment distribution
non_trauma_treat_p: float
Probability non trauma patient requires treatment
prob_trauma: float
probability that a new arrival is a trauma patient.
'''
random_number_set = 42
n_triage=2
n_reg=2
n_exam=3
n_trauma=4
n_cubicles_non_trauma_treat=4
n_cubicles_trauma_treat=5
triage_mean=6
reg_mean=8
reg_var=2
exam_mean=16
exam_var=3
trauma_mean=90
trauma_treat_mean=30
trauma_treat_var=4
non_trauma_treat_mean=13.3
non_trauma_treat_var=2
non_trauma_treat_p=0.6
prob_trauma=0.12
arrival_df="ed_arrivals.csv"
sim_duration = 600
number_of_runs = 100Show the patient class code
class Patient:
'''
Class defining details for a patient entity
'''
def __init__(self, p_id):
'''
Constructor method
Params:
-----
identifier: int
a numeric identifier for the patient.
'''
self.identifier = p_id
# Time of arrival in model/at centre
self.arrival = -np.inf
# Total time in pathway
self.total_time = -np.inf
# Shared waits
self.wait_triage = -np.inf
self.wait_reg = -np.inf
self.wait_treat = -np.inf
# Non-trauma pathway - examination wait
self.wait_exam = -np.inf
# Trauma pathway - stabilisation wait
self.wait_trauma = -np.inf
# Shared durations
self.triage_duration = -np.inf
self.reg_duration = -np.inf
self.treat_duration = -np.inf
# Non-trauma pathway - examination duration
self.exam_duration = -np.inf
# Trauma pathway - stabilisation duration
self.trauma_duration = -np.infShow the model code
# Class representing our model of the clinic.
class Model:
'''
Simulates the simplest minor treatment process for a patient
1. Arrive
2. Examined/treated by nurse when one available
3. Discharged
'''
# 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()
# Store the passed in run number
self.run_number = run_number
self.logger = EventLogger(env=self.env, run_number=self.run_number)
# Create a patient counter (which we'll use as a patient ID)
self.patient_counter = 0
self.trauma_patients = []
self.non_trauma_patients = []
# Create our resources
self.init_resources()
# Create our distributions
self.init_distributions()
def init_distributions(self):
# Create distributions
# Triage duration
self.triage_dist = Exponential(g.triage_mean,
random_seed=self.run_number*g.random_number_set)
# Registration duration (non-trauma only)
self.reg_dist = Lognormal(g.reg_mean,
np.sqrt(g.reg_var),
random_seed=self.run_number*g.random_number_set)
# Evaluation (non-trauma only)
self.exam_dist = Normal(g.exam_mean,
np.sqrt(g.exam_var),
random_seed=self.run_number*g.random_number_set)
# Trauma/stablisation duration (trauma only)
self.trauma_dist = Exponential(g.trauma_mean,
random_seed=self.run_number*g.random_number_set)
# Non-trauma treatment
self.nt_treat_dist = Lognormal(g.non_trauma_treat_mean,
np.sqrt(g.non_trauma_treat_var),
random_seed=self.run_number*g.random_number_set)
# treatment of trauma patients
self.treat_dist = Lognormal(g.trauma_treat_mean,
np.sqrt(g.non_trauma_treat_var),
random_seed=self.run_number*g.random_number_set)
# probability of non-trauma patient requiring treatment
self.nt_p_treat_dist = Bernoulli(g.non_trauma_treat_p,
random_seed=self.run_number*g.random_number_set)
# probability of non-trauma versus trauma patient
self.p_trauma_dist = Bernoulli(g.prob_trauma,
random_seed=self.run_number*g.random_number_set)
# init sampling for non-stationary poisson process
self.init_nspp()
def init_nspp(self):
# read arrival profile
self.arrivals = pd.read_csv(g.arrival_df) # pylint: disable=attribute-defined-outside-init
self.arrivals['mean_iat'] = 60 / self.arrivals['arrival_rate']
# maximum arrival rate (smallest time between arrivals)
self.lambda_max = self.arrivals['arrival_rate'].max() # pylint: disable=attribute-defined-outside-init
# thinning exponential
self.arrival_dist = Exponential(60.0 / self.lambda_max, # pylint: disable=attribute-defined-outside-init
random_seed=self.run_number*g.random_number_set)
# thinning uniform rng
self.thinning_rng = Uniform(low=0.0, high=1.0, # pylint: disable=attribute-defined-outside-init
random_seed=self.run_number*g.random_number_set)
def init_resources(self):
'''
Init the number of resources
and store in the arguments container object
Resource list:
1. Nurses/treatment bays (same thing in this model)
'''
# Shared Resources
self.triage_cubicles = VidigiStore(self.env, num_resources=g.n_triage)
self.registration_cubicles = VidigiStore(self.env, num_resources=g.n_reg)
# Non-trauma
self.exam_cubicles = VidigiStore(self.env, num_resources=g.n_exam)
self.non_trauma_treatment_cubicles = VidigiStore(self.env, g.n_cubicles_non_trauma_treat)
# Trauma
self.trauma_stabilisation_bays = VidigiStore(self.env, num_resources=g.n_trauma)
self.trauma_treatment_cubicles = VidigiStore(self.env, num_resources=g.n_cubicles_trauma_treat)
# 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:
t = int(self.env.now // 60) % self.arrivals.shape[0]
lambda_t = self.arrivals['arrival_rate'].iloc[t]
# set to a large number so that at least 1 sample taken!
u = np.Inf
interarrival_time = 0.0
# reject samples if u >= lambda_t / lambda_max
while u >= (lambda_t / self.lambda_max):
interarrival_time += self.arrival_dist.sample()
u = self.thinning_rng.sample()
# 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(interarrival_time)
# 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)
self.logger.log_arrival(entity_id=p.identifier,
pathway="Shared")
# sample if the patient is trauma or non-trauma
trauma = self.p_trauma_dist.sample()
# 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)
# and store patient in list for later easy access
if trauma:
# create and store a trauma patient to update KPIs.
self.trauma_patients.append(p)
self.env.process(self.attend_trauma_pathway(p))
else:
# create and store a non-trauma patient to update KPIs.
self.non_trauma_patients.append(p)
self.env.process(self.attend_non_trauma_pathway(p))
# A generator function that represents the pathway for a patient going
# through the clinic.
# The patient object is passed in to the generator function so we can
# extract information from / record information to it
def attend_non_trauma_pathway(self, patient):
'''
simulates the non-trauma/minor treatment process for a patient
1. request and wait for sign-in/triage
2. patient registration
3. examination
4a. percentage discharged
4b. remaining percentage treatment then discharge
'''
# record the time of arrival and entered the triage queue
patient.arrival = self.env.now
self.logger.log_queue(
entity_id=patient.identifier,
pathway='Non-Trauma',
event='triage_wait_begins'
)
###################################################
# request sign-in/triage
with self.triage_cubicles.request() as req:
triage_resource = yield req
# record the waiting time for triage
patient.wait_triage = self.env.now - patient.arrival
self.logger.log_resource_use_start(
entity_id=patient.identifier,
pathway='Non-Trauma',
event='triage_begins',
resource_id=triage_resource.id_attribute
)
# sample triage duration.
patient.triage_duration = self.triage_dist.sample()
yield self.env.timeout(patient.triage_duration)
self.logger.log_resource_use_end(
entity_id=patient.identifier,
pathway='Non-Trauma',
event='triage_complete',
resource_id=triage_resource.id_attribute
)
#########################################################
# record the time that entered the registration queue
start_wait = self.env.now
self.logger.log_queue(
entity_id=patient.identifier,
pathway='Non-Trauma',
event='MINORS_registration_wait_begins'
)
#########################################################
# request registration clerk
with self.registration_cubicles.request() as req:
registration_resource = yield req
# record the waiting time for registration
patient.wait_reg = self.env.now - start_wait
self.logger.log_resource_use_start(
entity_id=patient.identifier,
pathway='Non-Trauma',
event='MINORS_registration_begins',
resource_id=registration_resource.id_attribute
)
# sample registration duration.
patient.reg_duration = self.reg_dist.sample()
yield self.env.timeout(patient.reg_duration)
self.logger.log_resource_use_end(
entity_id=patient.identifier,
pathway='Non-Trauma',
event='MINORS_registration_complete',
resource_id=registration_resource.id_attribute
)
########################################################
# record the time that entered the evaluation queue
start_wait = self.env.now
self.logger.log_queue(
entity_id=patient.identifier,
pathway='Non-Trauma',
event='MINORS_examination_wait_begins'
)
#########################################################
# request examination resource
with self.exam_cubicles.request() as req:
examination_resource = yield req
# record the waiting time for examination to begin
patient.wait_exam = self.env.now - start_wait
self.logger.log_resource_use_start(
entity_id=patient.identifier,
pathway='Non-Trauma',
event='MINORS_examination_begins',
resource_id=examination_resource.id_attribute
)
# sample examination duration.
patient.exam_duration = self.exam_dist.sample()
yield self.env.timeout(patient.exam_duration)
self.logger.log_resource_use_end(
entity_id=patient.identifier,
pathway='Non-Trauma',
event='MINORS_examination_complete',
resource_id=examination_resource.id_attribute
)
############################################################################
# sample if patient requires treatment?
patient.require_treat = self.nt_p_treat_dist.sample() #pylint: disable=attribute-defined-outside-init
if patient.require_treat:
self.logger.log_event(
entity_id = patient.identifier,
pathway = 'Non-Trauma',
event = 'requires_treatment',
event_type = 'attribute_assigned'
)
# record the time that entered the treatment queue
start_wait = self.env.now
self.logger.log_queue(
entity_id = patient.identifier,
pathway='Non-Trauma',
event='MINORS_treatment_wait_begins'
)
###################################################
# request treatment cubicle
with self.non_trauma_treatment_cubicles.request() as req:
non_trauma_treatment_resource = yield req
# record the waiting time for treatment
patient.wait_treat = self.env.now - start_wait
self.logger.log_resource_use_start(
entity_id=patient.identifier,
pathway='Non-Trauma',
event='MINORS_treatment_begins',
resource_id=non_trauma_treatment_resource.id_attribute
)
# sample treatment duration.
patient.treat_duration = self.nt_treat_dist.sample()
yield self.env.timeout(patient.treat_duration)
self.logger.log_resource_use_end(
entity_id=patient.identifier,
pathway='Non-Trauma',
event='MINORS_treatment_complete',
resource_id=non_trauma_treatment_resource.id_attribute
)
##########################################################################
# Return to what happens to all patients, regardless of whether
# they were sampled as needing treatment
self.logger.log_departure(
entity_id=patient.identifier,
pathway='Non-Trauma'
)
# total time in system
patient.total_time = self.env.now - patient.arrival
def attend_trauma_pathway(self, patient):
'''
simulates the major treatment process for a patient
1. request and wait for sign-in/triage
2. trauma
3. treatment
'''
# record the time of arrival and entered the triage queue
patient.arrival = self.env.now
self.logger.log_queue(
entity_id = patient.identifier,
pathway = 'Trauma',
event = 'triage_wait_begins'
)
###################################################
# request sign-in/triage
with self.triage_cubicles.request() as req:
triage_resource = yield req
# record the waiting time for triage
patient.wait_triage = self.env.now - patient.arrival
self.logger.log_resource_use_start(
entity_id = patient.identifier,
pathway = 'Trauma',
event = 'triage_begins',
resource_id = triage_resource.id_attribute
)
# sample triage duration.
patient.triage_duration = self.triage_dist.sample()
yield self.env.timeout(patient.triage_duration)
self.logger.log_resource_use_end(
entity_id = patient.identifier,
pathway = 'Trauma',
event = 'triage_complete',
resource_id = triage_resource.id_attribute
)
###################################################
# record the time that entered the trauma queue
self.logger.log_queue(
entity_id = patient.identifier,
pathway = 'Trauma',
event = 'TRAUMA_stabilisation_wait_begins'
)
start_wait = self.env.now
###################################################
# request trauma room
with self.trauma_stabilisation_bays.request() as req:
trauma_resource = yield req
self.logger.log_resource_use_start(
entity_id = patient.identifier,
pathway = 'Trauma',
event = 'TRAUMA_stabilisation_begins',
resource_id = trauma_resource.id_attribute
)
# record the waiting time for trauma
patient.wait_trauma = self.env.now - start_wait
# sample stablisation duration.
patient.trauma_duration = self.trauma_dist.sample()
yield self.env.timeout(patient.trauma_duration)
self.logger.log_resource_use_end(
entity_id = patient.identifier,
pathway = 'Trauma',
event = 'TRAUMA_stabilisation_complete',
resource_id = trauma_resource.id_attribute
)
#######################################################
# record the time that patient entered the treatment queue
start_wait = self.env.now
self.logger.log_queue(
entity_id = patient.identifier,
pathway = 'Trauma',
event = 'TRAUMA_treatment_wait_begins'
)
########################################################
# request treatment cubicle
with self.trauma_treatment_cubicles.request() as req:
trauma_treatment_resource = yield req
# record the waiting time for trauma
patient.wait_treat = self.env.now - start_wait
self.logger.log_resource_use_start(
entity_id = patient.identifier,
pathway = 'Trauma',
event = 'TRAUMA_treatment_begins',
resource_id = trauma_treatment_resource.id_attribute
)
# sample treatment duration.
patient.treat_duration = self.trauma_dist.sample()
yield self.env.timeout(patient.treat_duration)
self.logger.log_resource_use_end(
entity_id = patient.identifier,
pathway = 'Trauma',
event = 'TRAUMA_treatment_complete',
resource_id = trauma_treatment_resource.id_attribute
)
self.logger.log_departure(
entity_id = patient.identifier,
pathway = 'Shared'
)
#########################################################
# total time in system
patient.total_time = self.env.now - patient.arrival
# 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)Show the trial class code
class Trial:
def __init__(self):
self.all_event_logs = []
self.trial_results_df = pd.DataFrame()
self.run_trial()
# 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(1, g.number_of_runs+1):
random.seed(run)
my_model = Model(run)
my_model.run()
self.all_event_logs.append(my_model.logger)
self.trial_results = pd.concat(
[run_results.to_dataframe() for run_results in self.all_event_logs]
)advanced_clinic_simulation = Trial()C:\Users\Sammi\AppData\Local\Temp\ipykernel_59416\2193437664.py:297: UserWarning:
Unrecognized event_type 'attribute_assigned'. Recommended values are: resource_use_end, resource_use, queue, arrival_departure.
advanced_clinic_simulation.all_event_logs[0].get_events_by_entity(5)| entity_id | event_type | event | time | pathway | run_number | timestamp | resource_id | |
|---|---|---|---|---|---|---|---|---|
| 0 | 5 | arrival_departure | arrival | 125.487189 | Shared | 1 | None | NaN |
| 1 | 5 | queue | triage_wait_begins | 125.487189 | Non-Trauma | 1 | None | NaN |
| 2 | 5 | resource_use | triage_begins | 125.487189 | Non-Trauma | 1 | None | 1.0 |
| 3 | 5 | resource_use_end | triage_complete | 126.005814 | Non-Trauma | 1 | None | 1.0 |
| 4 | 5 | queue | MINORS_registration_wait_begins | 126.005814 | Non-Trauma | 1 | None | NaN |
| 5 | 5 | resource_use | MINORS_registration_begins | 126.005814 | Non-Trauma | 1 | None | 1.0 |
| 6 | 5 | resource_use_end | MINORS_registration_complete | 131.600448 | Non-Trauma | 1 | None | 1.0 |
| 7 | 5 | queue | MINORS_examination_wait_begins | 131.600448 | Non-Trauma | 1 | None | NaN |
| 8 | 5 | resource_use | MINORS_examination_begins | 131.600448 | Non-Trauma | 1 | None | 1.0 |
| 9 | 5 | resource_use_end | MINORS_examination_complete | 149.229554 | Non-Trauma | 1 | None | 1.0 |
| 10 | 5 | attribute_assigned | requires_treatment | 149.229554 | Non-Trauma | 1 | None | NaN |
| 11 | 5 | queue | MINORS_treatment_wait_begins | 149.229554 | Non-Trauma | 1 | None | NaN |
| 12 | 5 | resource_use | MINORS_treatment_begins | 149.229554 | Non-Trauma | 1 | None | 2.0 |
| 13 | 5 | resource_use_end | MINORS_treatment_complete | 161.074127 | Non-Trauma | 1 | None | 2.0 |
| 14 | 5 | arrival_departure | depart | 161.074127 | Non-Trauma | 1 | None | NaN |
advanced_clinic_simulation.trial_results| entity_id | event_type | event | time | pathway | run_number | resource_id | |
|---|---|---|---|---|---|---|---|
| 0 | 1 | arrival_departure | arrival | 37.593555 | Shared | 1 | NaN |
| 1 | 1 | queue | triage_wait_begins | 37.593555 | Non-Trauma | 1 | NaN |
| 2 | 1 | resource_use | triage_begins | 37.593555 | Non-Trauma | 1 | 1.0 |
| 3 | 2 | arrival_departure | arrival | 51.835879 | Shared | 1 | NaN |
| 4 | 2 | queue | triage_wait_begins | 51.835879 | Non-Trauma | 1 | NaN |
| ... | ... | ... | ... | ... | ... | ... | ... |
| 1470 | 87 | resource_use_end | MINORS_examination_complete | 598.990148 | Non-Trauma | 100 | 1.0 |
| 1471 | 87 | attribute_assigned | requires_treatment | 598.990148 | Non-Trauma | 100 | NaN |
| 1472 | 87 | queue | MINORS_treatment_wait_begins | 598.990148 | Non-Trauma | 100 | NaN |
| 1473 | 87 | resource_use | MINORS_treatment_begins | 598.990148 | Non-Trauma | 100 | 1.0 |
| 1474 | 86 | resource_use | MINORS_examination_begins | 598.990148 | Non-Trauma | 100 | 1.0 |
146776 rows × 7 columns
Again, we could create our event position dataframe by passing in a list of positions…
event_position_df = pd.DataFrame([
{'event': 'arrival', 'x': 10, 'y': 250, 'label': "Arrival" },
# Triage - minor and trauma
{'event': 'triage_wait_begins',
'x': 160, 'y': 375, 'label': "Waiting for<br>Triage" },
{'event': 'triage_begins',
'x': 160, 'y': 315, 'resource':'n_triage', 'label': "Being Triaged" },
# Minors (non-trauma) pathway
{'event': 'MINORS_registration_wait_begins',
'x': 300, 'y': 145, 'label': "Waiting for<br>Registration" },
{'event': 'MINORS_registration_begins',
'x': 300, 'y': 85, 'resource':'n_reg', 'label':'Being<br>Registered' },
{'event': 'MINORS_examination_wait_begins',
'x': 465, 'y': 145, 'label': "Waiting for<br>Examination" },
{'event': 'MINORS_examination_begins',
'x': 465, 'y': 85, 'resource':'n_exam', 'label': "Being<br>Examined" },
{'event': 'MINORS_treatment_wait_begins',
'x': 630, 'y': 145, 'label': "Waiting for<br>Treatment" },
{'event': 'MINORS_treatment_begins',
'x': 630, 'y': 85, 'resource':'n_cubicles_non_trauma_treat', 'label': "Being<br>Treated" },
# Trauma pathway
{'event': 'TRAUMA_stabilisation_wait_begins',
'x': 300, 'y': 560, 'label': "Waiting for<br>Stabilisation" },
{'event': 'TRAUMA_stabilisation_begins',
'x': 300, 'y': 490, 'resource':'n_trauma', 'label': "Being<br>Stabilised" },
{'event': 'TRAUMA_treatment_wait_begins',
'x': 630, 'y': 560, 'label': "Waiting for<br>Treatment" },
{'event': 'TRAUMA_treatment_begins',
'x': 630, 'y': 490, 'resource':'n_cubicles_trauma_treat', 'label': "Being<br>Treated" },
{'event': 'depart',
'x': 670, 'y': 330, 'label': "Exit"}
])Or using the vidigi helpers.
event_position_df = create_event_position_df([
EventPosition(event='arrival', x=10, y=250, label="Arrival"),
# Triage - minor and trauma
EventPosition(event='triage_wait_begins', x=160, y=375, label="Waiting for<br>Triage"),
EventPosition(event='triage_begins', x=160, y=315, resource='n_triage', label="Being Triaged"),
# Minors (non-trauma) pathway
EventPosition(event='MINORS_registration_wait_begins', x=300, y=145, label="Waiting for<br>Registration"),
EventPosition(event='MINORS_registration_begins', x=300, y=85, resource='n_reg', label='Being<br>Registered'),
EventPosition(event='MINORS_examination_wait_begins', x=465, y=145, label="Waiting for<br>Examination"),
EventPosition(event='MINORS_examination_begins', x=465, y=85, resource='n_exam', label="Being<br>Examined"),
EventPosition(event='MINORS_treatment_wait_begins', x=630, y=145, label="Waiting for<br>Treatment"),
EventPosition(event='MINORS_treatment_begins', x=630, y=85, resource='n_cubicles_non_trauma_treat', label="Being<br>Treated"),
# Trauma pathway
EventPosition(event='TRAUMA_stabilisation_wait_begins', x=300, y=560, label="Waiting for<br>Stabilisation"),
EventPosition(event='TRAUMA_stabilisation_begins', x=300, y=490, resource='n_trauma', label="Being<br>Stabilised"),
EventPosition(event='TRAUMA_treatment_wait_begins', x=630, y=560, label="Waiting for<br>Treatment"),
EventPosition(event='TRAUMA_treatment_begins', x=630, y=490, resource='n_cubicles_trauma_treat', label="Being<br>Treated"),
EventPosition(event='depart', x=670, y=330, label="Exit")
])Finally, we’ll create the animation, remembering to filter to a single run when passing in our dataframe.
animate_activity_log(
event_log=advanced_clinic_simulation.trial_results[advanced_clinic_simulation.trial_results['run_number']==1],
event_position_df= event_position_df,
scenario=g(),
debug_mode=True,
setup_mode=False,
every_x_time_units=5,
include_play_button=True,
gap_between_entities=11,
gap_between_resources=15,
gap_between_resource_rows=30,
gap_between_queue_rows=30,
plotly_height=600,
plotly_width=1000,
override_x_max=700,
override_y_max=675,
entity_icon_size=10,
resource_icon_size=13,
text_size=15,
wrap_queues_at=10,
step_snapshot_max=20,
limit_duration=g.sim_duration,
time_display_units="dhm",
display_stage_labels=False,
add_background_image="https://raw.githubusercontent.com/Bergam0t/vidigi/refs/heads/main/examples/example_2_branching_multistep/Full%20Model%20Background%20Image%20-%20Horizontal%20Layout.drawio.png",
)The TrialLogger class
While we just put our list of event logs from each run of our trial into a list before, we can actual turn these into a vidigi TrialLogger class after the fact, which gives us access to even more helper functions.
We’ll continue using our more complex example here.
from vidigi.logging import TrialLogger
trial_logs = TrialLogger(advanced_clinic_simulation.all_event_logs)
trial_logs<vidigi.logging.TrialLogger at 0x13747e10290>
trial_logs.summary(){'number_of_runs': 100}
trial_logs.get_log_by_run(run=2)<vidigi.logging.EventLogger at 0x13747a2e1d0>
trial_logs.get_event_duration_stat(
first_event="TRAUMA_treatment_wait_begins",
second_event="TRAUMA_treatment_begins",
what="mean", exclude_incomplete=True
)1.45
trial_logs.get_event_duration_stat("TRAUMA_treatment_wait_begins", "TRAUMA_treatment_begins",
what="mean", exclude_incomplete=False)nan
trial_logs.get_event_duration_stat("TRAUMA_treatment_wait_begins", "TRAUMA_treatment_begins",
what="median", exclude_incomplete=True)0.0
trial_logs.get_event_duration_stat("TRAUMA_treatment_wait_begins", "TRAUMA_treatment_begins",
what="median", exclude_incomplete=False)nan
trial_logs.get_event_duration_stat("TRAUMA_treatment_wait_begins", "TRAUMA_treatment_begins",
what="count", exclude_incomplete=True)1127
trial_logs.get_event_duration_stat("TRAUMA_treatment_wait_begins", "TRAUMA_treatment_begins",
what="count", exclude_incomplete=False)1147
trial_logs.get_event_duration_stat("TRAUMA_treatment_wait_begins", "TRAUMA_treatment_begins",
what="quantile", exclude_incomplete=True,
q=0.25)0.0
trial_logs.get_event_duration_stat("TRAUMA_treatment_wait_begins", "TRAUMA_treatment_begins",
what="quantile", exclude_incomplete=True,
q=0.75)0.0
trial_logs.get_event_duration_stat("TRAUMA_treatment_wait_begins", "TRAUMA_treatment_begins",
what="served_count")1127
trial_logs.get_event_duration_stat("TRAUMA_treatment_wait_begins", "TRAUMA_treatment_begins",
what="summary"){'mean (of complete)': 1.45,
'median (of complete)': 0.0,
'min': 0.0,
'max': 94.2,
'unserved_count': 1147,
'served_count': 1127,
'unserved_rate': 0.02,
'served_rate': 0.98,
'unserved_count_mean_per_run': 11.47,
'served_count_mean_per_run': 11.27}
event_pairs = [
{"first_event": "triage_wait_begins",
"second_event":"triage_begins",
"label": "Triage Wait Length"},
{"first_event": "MINORS_registration_wait_begins",
"second_event":"MINORS_registration_begins",
"label": "Minors Registration Wait Length"},
{"first_event": "MINORS_examination_wait_begins",
"second_event":"MINORS_examination_begins",
"label": "Minors Examination Wait Length"},
{"first_event": "MINORS_treatment_wait_begins",
"second_event":"MINORS_treatment_begins",
"label": "Minors Treatment Wait Length"},
{"first_event": "TRAUMA_stabilisation_wait_begins",
"second_event":"TRAUMA_stabilisation_begins",
"label": "Trauma Stabilisation Wait Length"},
{"first_event": "TRAUMA_treatment_wait_begins",
"second_event":"TRAUMA_treatment_begins",
"label": "Trauma Treatment Wait Length"},
]
trial_logs.plot_metric_bar(
event_pairs,
what="mean",
title= "Mean step durations",
width=800
)trial_logs.plot_metric_bar(
event_pairs,
what="median",
title= "Median step durations",
width=800
)trial_logs.plot_metric_bar(
event_pairs,
what="max",
title= "Median step durations",
width=800
)trial_logs.plot_queue_size(["triage_wait_begins"],
limit_duration=g.sim_duration,
every_x_time_units=30, width=1000,
title="Triage Queue Length")trial_logs.plot_queue_size(
["triage_wait_begins", "MINORS_registration_wait_begins", "MINORS_examination_wait_begins",
"MINORS_examination_begins", "MINORS_treatment_wait_begins",
"TRAUMA_stabilisation_wait_begins", "TRAUMA_treatment_wait_begins"],
limit_duration=g.sim_duration, every_x_time_units=30, show_all_runs=False,
height=1200,
width=1200)trial_logs.plot_queue_size(
["triage_wait_begins", "MINORS_registration_wait_begins", "MINORS_examination_wait_begins",
"MINORS_examination_begins", "MINORS_treatment_wait_begins",
"TRAUMA_stabilisation_wait_begins", "TRAUMA_treatment_wait_begins"],
limit_duration=g.sim_duration, every_x_time_units=30, show_all_runs=True,
height=1200,
width=1200
)