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.

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"

Simple Example - 1 Resource Type, No Branching

# 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 = 100
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.inf
# 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

# Import additional required distributions
from sim_tools.distributions import Normal, Bernoulli, Uniform
# 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 = 100
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.inf
# 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)
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.

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("TRAUMA_treatment_wait_begins", "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)
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)
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
     )