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)
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
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
clinic_simulation.all_event_logs[0].plot_entity_timeline(5)
clinic_simulation.trial_results
entity_id event_type event time pathway run_number timestamp resource_id
0 1 arrival_departure arrival 0.000000 None 1 None NaN
1 1 queue treatment_wait_begins 0.000000 None 1 None NaN
2 1 resource_use treatment_begins 0.000000 None 1 None 1.0
3 2 arrival_departure arrival 12.021043 None 1 None NaN
4 2 queue treatment_wait_begins 12.021043 None 1 None NaN
... ... ... ... ... ... ... ... ...
436 59 arrival_departure depart 592.752922 None 100 None NaN
437 62 resource_use treatment_begins 592.752922 None 100 None 1.0
438 58 resource_use_end treatment_complete 598.887715 None 100 None 4.0
439 58 arrival_departure depart 598.887715 None 100 None NaN
440 63 resource_use treatment_begins 598.887715 None 100 None 4.0

42000 rows × 8 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 pathway run_number timestamp resource_id
0 1 arrival_departure arrival 0.000000 None 1 None NaN
1 1 queue treatment_wait_begins 0.000000 None 1 None NaN
2 1 resource_use treatment_begins 0.000000 None 1 None 1.0
3 2 arrival_departure arrival 12.021043 None 1 None NaN
4 2 queue treatment_wait_begins 12.021043 None 1 None NaN
... ... ... ... ... ... ... ... ...
431 56 resource_use_end treatment_complete 596.143390 None 1 None 1.0
432 56 arrival_departure depart 596.143390 None 1 None NaN
433 60 resource_use treatment_begins 596.143390 None 1 None 1.0
434 132 arrival_departure arrival 596.500830 None 1 None NaN
435 132 queue treatment_wait_begins 596.500830 None 1 None NaN

436 rows × 8 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",
    )
Animation function called at 11:17:19
Iteration through time-unit-by-time-unit logs complete 11:17:23
Snapshot df concatenation complete at 11:17:23
Reshaped animation dataframe finished construction at 11:17:23
Placement dataframe finished construction at 11:17:23
Output animation generation complete at 11:17:26
Total Time Elapsed: 7.24 seconds

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_56100\2193437664.py:297: UserWarning:

Unrecognized event_type 'attribute_assigned'. Recommended values are: arrival_departure, resource_use, resource_use_end, queue.
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 timestamp resource_id
0 1 arrival_departure arrival 37.593555 Shared 1 None NaN
1 1 queue triage_wait_begins 37.593555 Non-Trauma 1 None NaN
2 1 resource_use triage_begins 37.593555 Non-Trauma 1 None 1.0
3 2 arrival_departure arrival 51.835879 Shared 1 None NaN
4 2 queue triage_wait_begins 51.835879 Non-Trauma 1 None NaN
... ... ... ... ... ... ... ... ...
1470 87 resource_use_end MINORS_examination_complete 598.990148 Non-Trauma 100 None 1.0
1471 87 attribute_assigned requires_treatment 598.990148 Non-Trauma 100 None NaN
1472 87 queue MINORS_treatment_wait_begins 598.990148 Non-Trauma 100 None NaN
1473 87 resource_use MINORS_treatment_begins 598.990148 Non-Trauma 100 None 1.0
1474 86 resource_use MINORS_examination_begins 598.990148 Non-Trauma 100 None 1.0

146776 rows × 8 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",
    )
Animation function called at 11:17:30
Iteration through time-unit-by-time-unit logs complete 11:17:31
Snapshot df concatenation complete at 11:17:31
Reshaped animation dataframe finished construction at 11:17:31
Placement dataframe finished construction at 11:17:31
Output animation generation complete at 11:17:31
Total Time Elapsed: 1.64 seconds