Animation of a SimPy Model with Branching and Multiple Steps

from ex_2_model_classes import Trial, g
from vidigi.animation import animate_activity_log
from vidigi.utils import create_event_position_df, EventPosition
import pandas as pd
import plotly.io as pio
pio.renderers.default = "notebook"
import os
import random
import numpy as np
import pandas as pd
import simpy
from sim_tools.distributions import Exponential, Lognormal, Uniform, Normal, Bernoulli
from vidigi.resources import populate_store

TRACE = False


def trace(msg, show=TRACE):
    """
    Utility function for printing a trace as the
    simulation model executes.
    Set the TRACE constant to False, to turn tracing off.

    Params:
    -------
    msg: str
        string to print to screen.
    """
    if show:
        print(msg)


# Class to store global parameter values.  We don't create an instance of this
# class - we just refer to the class blueprint itself to access the numbers
# inside.
class g:
    """
    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 representing patients coming in to the clinic.
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()

        self.event_log = []

        # 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()

        # Store the passed in run number
        self.run_number = run_number

        # Create a new Pandas DataFrame that will store some results against
        # the patient ID (which we'll use as the index).
        self.results_df = pd.DataFrame()
        self.results_df["Patient ID"] = [1]
        self.results_df["Queue Time Cubicle"] = [0.0]
        self.results_df["Time with Nurse"] = [0.0]
        self.results_df.set_index("Patient ID", inplace=True)

        # Create an attribute to store the mean queuing times across this run of
        # the model
        self.mean_q_time_cubicle = 0

        # 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 = simpy.Store(self.env)
        populate_store(
            num_resources=g.n_triage, simpy_store=self.triage_cubicles, sim_env=self.env
        )

        self.registration_cubicles = simpy.Store(self.env)
        populate_store(
            num_resources=g.n_reg,
            simpy_store=self.registration_cubicles,
            sim_env=self.env,
        )

        # Non-trauma
        self.exam_cubicles = simpy.Store(self.env)
        populate_store(
            num_resources=g.n_exam, simpy_store=self.exam_cubicles, sim_env=self.env
        )

        self.non_trauma_treatment_cubicles = simpy.Store(self.env)
        populate_store(
            num_resources=g.n_cubicles_non_trauma_treat,
            simpy_store=self.non_trauma_treatment_cubicles,
            sim_env=self.env,
        )

        # Trauma
        self.trauma_stabilisation_bays = simpy.Store(self.env)
        populate_store(
            num_resources=g.n_trauma,
            simpy_store=self.trauma_stabilisation_bays,
            sim_env=self.env,
        )

        self.trauma_treatment_cubicles = simpy.Store(self.env)
        populate_store(
            num_resources=g.n_cubicles_trauma_treat,
            simpy_store=self.trauma_treatment_cubicles,
            sim_env=self.env,
        )

    # 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)

            trace(f"patient {self.patient_counter} arrives at: {self.env.now:.3f}")
            self.event_log.append(
                {
                    "patient": self.patient_counter,
                    "pathway": "Shared",
                    "event": "arrival",
                    "event_type": "arrival_departure",
                    "time": self.env.now,
                }
            )

            # 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.event_log.append(
            {
                "patient": patient.identifier,
                "pathway": "Non-Trauma",
                "event_type": "queue",
                "event": "triage_wait_begins",
                "time": self.env.now,
            }
        )

        ###################################################
        # request sign-in/triage
        triage_resource = yield self.triage_cubicles.get()

        # record the waiting time for triage
        patient.wait_triage = self.env.now - patient.arrival
        trace(f"patient {patient.identifier} triaged to minors {self.env.now:.3f}")
        self.event_log.append(
            {
                "patient": patient.identifier,
                "pathway": "Non-Trauma",
                "event_type": "resource_use",
                "event": "triage_begins",
                "time": self.env.now,
                "resource_id": triage_resource.id_attribute,
            }
        )

        # sample triage duration.
        patient.triage_duration = self.triage_dist.sample()
        yield self.env.timeout(patient.triage_duration)

        trace(
            f"triage {patient.identifier} complete {self.env.now:.3f}; "
            f"waiting time was {patient.wait_triage:.3f}"
        )
        self.event_log.append(
            {
                "patient": patient.identifier,
                "pathway": "Non-Trauma",
                "event_type": "resource_use_end",
                "event": "triage_complete",
                "time": self.env.now,
                "resource_id": triage_resource.id_attribute,
            }
        )

        # Resource is no longer in use, so put it back in the store
        self.triage_cubicles.put(triage_resource)
        #########################################################

        # record the time that entered the registration queue
        start_wait = self.env.now
        self.event_log.append(
            {
                "patient": patient.identifier,
                "pathway": "Non-Trauma",
                "event_type": "queue",
                "event": "MINORS_registration_wait_begins",
                "time": self.env.now,
            }
        )

        #########################################################
        # request registration clerk
        registration_resource = yield self.registration_cubicles.get()

        # record the waiting time for registration
        patient.wait_reg = self.env.now - start_wait
        trace(f"registration of patient {patient.identifier} at {self.env.now:.3f}")
        self.event_log.append(
            {
                "patient": patient.identifier,
                "pathway": "Non-Trauma",
                "event_type": "resource_use",
                "event": "MINORS_registration_begins",
                "time": self.env.now,
                "resource_id": registration_resource.id_attribute,
            }
        )

        # sample registration duration.
        patient.reg_duration = self.reg_dist.sample()
        yield self.env.timeout(patient.reg_duration)

        trace(
            f"patient {patient.identifier} registered at"
            f"{self.env.now:.3f}; "
            f"waiting time was {patient.wait_reg:.3f}"
        )
        self.event_log.append(
            {
                "patient": patient.identifier,
                "pathway": "Non-Trauma",
                "event": "MINORS_registration_complete",
                "event_type": "resource_use_end",
                "time": self.env.now,
                "resource_id": registration_resource.id_attribute,
            }
        )
        # Resource is no longer in use, so put it back in the store
        self.registration_cubicles.put(registration_resource)
        ########################################################

        # record the time that entered the evaluation queue
        start_wait = self.env.now

        self.event_log.append(
            {
                "patient": patient.identifier,
                "pathway": "Non-Trauma",
                "event": "MINORS_examination_wait_begins",
                "event_type": "queue",
                "time": self.env.now,
            }
        )

        #########################################################
        # request examination resource
        examination_resource = yield self.exam_cubicles.get()

        # record the waiting time for examination to begin
        patient.wait_exam = self.env.now - start_wait
        trace(f"examination of patient {patient.identifier} begins {self.env.now:.3f}")
        self.event_log.append(
            {
                "patient": patient.identifier,
                "pathway": "Non-Trauma",
                "event": "MINORS_examination_begins",
                "event_type": "resource_use",
                "time": self.env.now,
                "resource_id": examination_resource.id_attribute,
            }
        )

        # sample examination duration.
        patient.exam_duration = self.exam_dist.sample()
        yield self.env.timeout(patient.exam_duration)

        trace(
            f"patient {patient.identifier} examination complete "
            f"at {self.env.now:.3f};"
            f"waiting time was {patient.wait_exam:.3f}"
        )
        self.event_log.append(
            {
                "patient": patient.identifier,
                "pathway": "Non-Trauma",
                "event": "MINORS_examination_complete",
                "event_type": "resource_use_end",
                "time": self.env.now,
                "resource_id": examination_resource.id_attribute,
            }
        )
        # Resource is no longer in use, so put it back in
        self.exam_cubicles.put(examination_resource)
        ############################################################################

        # 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.event_log.append(
                {
                    "patient": patient.identifier,
                    "pathway": "Non-Trauma",
                    "event": "requires_treatment",
                    "event_type": "attribute_assigned",
                    "time": self.env.now,
                }
            )

            # record the time that entered the treatment queue
            start_wait = self.env.now
            self.event_log.append(
                {
                    "patient": patient.identifier,
                    "pathway": "Non-Trauma",
                    "event": "MINORS_treatment_wait_begins",
                    "event_type": "queue",
                    "time": self.env.now,
                }
            )
            ###################################################
            # request treatment cubicle

            non_trauma_treatment_resource = yield self.non_trauma_treatment_cubicles.get()

            # record the waiting time for treatment
            patient.wait_treat = self.env.now - start_wait
            trace(
                f"treatment of patient {patient.identifier} begins {self.env.now:.3f}"
            )
            self.event_log.append(
                {
                    "patient": patient.identifier,
                    "pathway": "Non-Trauma",
                    "event": "MINORS_treatment_begins",
                    "event_type": "resource_use",
                    "time": self.env.now,
                    "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)

            trace(
                f"patient {patient.identifier} treatment complete "
                f"at {self.env.now:.3f};"
                f"waiting time was {patient.wait_treat:.3f}"
            )
            self.event_log.append(
                {
                    "patient": patient.identifier,
                    "pathway": "Non-Trauma",
                    "event": "MINORS_treatment_complete",
                    "event_type": "resource_use_end",
                    "time": self.env.now,
                    "resource_id": non_trauma_treatment_resource.id_attribute,
                }
            )

            # Resource is no longer in use, so put it back in the store
            self.non_trauma_treatment_cubicles.put(non_trauma_treatment_resource)
        ##########################################################################

        # Return to what happens to all patients, regardless of whether they were sampled as needing treatment
        self.event_log.append(
            {
                "patient": patient.identifier,
                "pathway": "Shared",
                "event": "depart",
                "event_type": "arrival_departure",
                "time": self.env.now,
            }
        )

        # 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.event_log.append(
            {
                "patient": patient.identifier,
                "pathway": "Trauma",
                "event_type": "queue",
                "event": "triage_wait_begins",
                "time": self.env.now,
            }
        )

        ###################################################
        # request sign-in/triage
        triage_resource = yield self.triage_cubicles.get()

        # record the waiting time for triage
        patient.wait_triage = self.env.now - patient.arrival

        trace(f"patient {patient.identifier} triaged to trauma {self.env.now:.3f}")
        self.event_log.append(
            {
                "patient": patient.identifier,
                "pathway": "Trauma",
                "event_type": "resource_use",
                "event": "triage_begins",
                "time": self.env.now,
                "resource_id": triage_resource.id_attribute,
            }
        )

        # sample triage duration.
        patient.triage_duration = self.triage_dist.sample()
        yield self.env.timeout(patient.triage_duration)

        trace(
            f"triage {patient.identifier} complete {self.env.now:.3f}; "
            f"waiting time was {patient.wait_triage:.3f}"
        )
        self.event_log.append(
            {
                "patient": patient.identifier,
                "pathway": "Trauma",
                "event_type": "resource_use_end",
                "event": "triage_complete",
                "time": self.env.now,
                "resource_id": triage_resource.id_attribute,
            }
        )

        # Resource is no longer in use, so put it back in the store
        self.triage_cubicles.put(triage_resource)
        ###################################################

        # record the time that entered the trauma queue
        start_wait = self.env.now
        self.event_log.append(
            {
                "patient": patient.identifier,
                "pathway": "Trauma",
                "event_type": "queue",
                "event": "TRAUMA_stabilisation_wait_begins",
                "time": self.env.now,
            }
        )

        ###################################################
        # request trauma room
        trauma_resource = yield self.trauma_stabilisation_bays.get()

        self.event_log.append(
            {
                "patient": patient.identifier,
                "pathway": "Trauma",
                "event_type": "resource_use",
                "event": "TRAUMA_stabilisation_begins",
                "time": self.env.now,
                "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)

        trace(f"stabilisation of patient {patient.identifier} at {self.env.now:.3f}")
        self.event_log.append(
            {
                "patient": patient.identifier,
                "pathway": "Trauma",
                "event_type": "resource_use_end",
                "event": "TRAUMA_stabilisation_complete",
                "time": self.env.now,
                "resource_id": trauma_resource.id_attribute,
            }
        )
        # Resource is no longer in use, so put it back in the store
        self.trauma_stabilisation_bays.put(trauma_resource)

        #######################################################

        # record the time that patient entered the treatment queue
        start_wait = self.env.now
        self.event_log.append(
            {
                "patient": patient.identifier,
                "pathway": "Trauma",
                "event_type": "queue",
                "event": "TRAUMA_treatment_wait_begins",
                "time": self.env.now,
            }
        )

        ########################################################
        # request treatment cubicle
        trauma_treatment_resource = yield self.trauma_treatment_cubicles.get()

        # record the waiting time for trauma
        patient.wait_treat = self.env.now - start_wait
        trace(f"treatment of patient {patient.identifier} at {self.env.now:.3f}")
        self.event_log.append(
            {
                "patient": patient.identifier,
                "pathway": "Trauma",
                "event_type": "resource_use",
                "event": "TRAUMA_treatment_begins",
                "time": self.env.now,
                "resource_id": trauma_treatment_resource.id_attribute,
            }
        )

        # sample treatment duration.
        patient.treat_duration = self.trauma_dist.sample()
        yield self.env.timeout(patient.treat_duration)

        trace(
            f"patient {patient.identifier} treatment complete {self.env.now:.3f}; "
            f"waiting time was {patient.wait_treat:.3f}"
        )
        self.event_log.append(
            {
                "patient": patient.identifier,
                "pathway": "Trauma",
                "event_type": "resource_use_end",
                "event": "TRAUMA_treatment_complete",
                "time": self.env.now,
                "resource_id": trauma_treatment_resource.id_attribute,
            }
        )
        self.event_log.append(
            {
                "patient": patient.identifier,
                "pathway": "Shared",
                "event": "depart",
                "event_type": "arrival_departure",
                "time": self.env.now,
            }
        )

        # Resource is no longer in use, so put it back in the store
        self.trauma_treatment_cubicles.put(trauma_treatment_resource)

        #########################################################

        # total time in system
        patient.total_time = self.env.now - patient.arrival

    # This method calculates results over a single run.  Here we just calculate
    # a mean, but in real world models you'd probably want to calculate more.
    def calculate_run_results(self):
        # Take the mean of the queuing times across patients in this run of the
        # model.
        self.mean_q_time_cubicle = self.results_df["Queue Time Cubicle"].mean()

    # The run method starts up the DES entity generators, runs the simulation,
    # and in turns calls anything we need to generate results for the run
    def run(self):
        # Start up our DES entity generators that create new patients.  We've
        # only got one in this model, but we'd need to do this for each one if
        # we had multiple generators.
        self.env.process(self.generator_patient_arrivals())

        # Run the model for the duration specified in g class
        self.env.run(until=g.sim_duration)

        # Now the simulation run has finished, call the method that calculates
        # run results
        self.calculate_run_results()

        self.event_log = pd.DataFrame(self.event_log)

        self.event_log["run"] = self.run_number

        return {"results": self.results_df, "event_log": self.event_log}


# Class representing a Trial for our simulation - a batch of simulation runs.
class Trial:
    # The constructor sets up a pandas dataframe that will store the key
    # results from each run against run number, with run number as the index.
    def __init__(self):
        self.df_trial_results = pd.DataFrame()
        self.df_trial_results["Run Number"] = [0]
        self.df_trial_results["Arrivals"] = [0]
        self.df_trial_results["Mean Queue Time Cubicle"] = [0.0]
        self.df_trial_results.set_index("Run Number", inplace=True)

        self.all_event_logs = []

    # Method to run a trial
    def run_trial(self):
        # Run the simulation for the number of runs specified in g class.
        # For each run, we create a new instance of the Model class and call its
        # run method, which sets everything else in motion.  Once the run has
        # completed, we grab out the stored run results (just mean queuing time
        # here) and store it against the run number in the trial results
        # dataframe.
        for run in range(g.number_of_runs):
            random.seed(run)

            my_model = Model(run)
            model_outputs = my_model.run()
            patient_level_results = model_outputs["results"]
            event_log = model_outputs["event_log"]

            self.df_trial_results.loc[run] = [
                len(patient_level_results),
                my_model.mean_q_time_cubicle,
            ]

            # print(event_log)

            self.all_event_logs.append(event_log)

        self.all_event_logs = pd.concat(self.all_event_logs)
my_trial = Trial()

my_trial.run_trial()
my_trial.all_event_logs.head(50)
patient pathway event event_type time resource_id run
0 1 Shared arrival arrival_departure 3.285355 NaN 0
1 1 Non-Trauma triage_wait_begins queue 3.285355 NaN 0
2 1 Non-Trauma triage_begins resource_use 3.285355 1.0 0
3 2 Shared arrival arrival_departure 3.289691 NaN 0
4 2 Non-Trauma triage_wait_begins queue 3.289691 NaN 0
5 2 Non-Trauma triage_begins resource_use 3.289691 2.0 0
6 1 Non-Trauma triage_complete resource_use_end 7.364946 1.0 0
7 1 Non-Trauma MINORS_registration_wait_begins queue 7.364946 NaN 0
8 1 Non-Trauma MINORS_registration_begins resource_use 7.364946 1.0 0
9 2 Non-Trauma triage_complete resource_use_end 9.407274 2.0 0
10 2 Non-Trauma MINORS_registration_wait_begins queue 9.407274 NaN 0
11 2 Non-Trauma MINORS_registration_begins resource_use 9.407274 2.0 0
12 1 Non-Trauma MINORS_registration_complete resource_use_end 15.418481 1.0 0
13 1 Non-Trauma MINORS_examination_wait_begins queue 15.418481 NaN 0
14 1 Non-Trauma MINORS_examination_begins resource_use 15.418481 1.0 0
15 2 Non-Trauma MINORS_registration_complete resource_use_end 17.104670 2.0 0
16 2 Non-Trauma MINORS_examination_wait_begins queue 17.104670 NaN 0
17 2 Non-Trauma MINORS_examination_begins resource_use 17.104670 2.0 0
18 1 Non-Trauma MINORS_examination_complete resource_use_end 31.636252 1.0 0
19 1 Shared depart arrival_departure 31.636252 NaN 0
20 2 Non-Trauma MINORS_examination_complete resource_use_end 32.875857 2.0 0
21 2 Non-Trauma requires_treatment attribute_assigned 32.875857 NaN 0
22 2 Non-Trauma MINORS_treatment_wait_begins queue 32.875857 NaN 0
23 2 Non-Trauma MINORS_treatment_begins resource_use 32.875857 1.0 0
24 3 Shared arrival arrival_departure 33.426168 NaN 0
25 3 Non-Trauma triage_wait_begins queue 33.426168 NaN 0
26 3 Non-Trauma triage_begins resource_use 33.426168 1.0 0
27 3 Non-Trauma triage_complete resource_use_end 33.545008 1.0 0
28 3 Non-Trauma MINORS_registration_wait_begins queue 33.545008 NaN 0
29 3 Non-Trauma MINORS_registration_begins resource_use 33.545008 1.0 0
30 4 Shared arrival arrival_departure 37.900548 NaN 0
31 4 Non-Trauma triage_wait_begins queue 37.900548 NaN 0
32 4 Non-Trauma triage_begins resource_use 37.900548 2.0 0
33 4 Non-Trauma triage_complete resource_use_end 37.914164 2.0 0
34 4 Non-Trauma MINORS_registration_wait_begins queue 37.914164 NaN 0
35 4 Non-Trauma MINORS_registration_begins resource_use 37.914164 2.0 0
36 3 Non-Trauma MINORS_registration_complete resource_use_end 42.359504 1.0 0
37 3 Non-Trauma MINORS_examination_wait_begins queue 42.359504 NaN 0
38 3 Non-Trauma MINORS_examination_begins resource_use 42.359504 3.0 0
39 4 Non-Trauma MINORS_registration_complete resource_use_end 45.938325 2.0 0
40 4 Non-Trauma MINORS_examination_wait_begins queue 45.938325 NaN 0
41 4 Non-Trauma MINORS_examination_begins resource_use 45.938325 1.0 0
42 2 Non-Trauma MINORS_treatment_complete resource_use_end 46.278797 1.0 0
43 2 Shared depart arrival_departure 46.278797 NaN 0
44 5 Shared arrival arrival_departure 51.770459 NaN 0
45 5 Non-Trauma triage_wait_begins queue 51.770459 NaN 0
46 5 Non-Trauma triage_begins resource_use 51.770459 1.0 0
47 5 Non-Trauma triage_complete resource_use_end 55.072516 1.0 0
48 5 Non-Trauma MINORS_registration_wait_begins queue 55.072516 NaN 0
49 5 Non-Trauma MINORS_registration_begins resource_use 55.072516 1.0 0
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")
])

setup_mode = True

setup_mode allows us to see how the coordinates of our plot relate to the positioning of our background image, allowing us to more accurately place our entities.

animate_activity_log(
        event_log=my_trial.all_event_logs[my_trial.all_event_logs['run']==1],
        event_position_df= event_position_df,
        scenario=g(),
        entity_col_name="patient",
        debug_mode=True,
        setup_mode=True,
        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 16:42:11
Iteration through time-unit-by-time-unit logs complete 16:42:12
Snapshot df concatenation complete at 16:42:12
Reshaped animation dataframe finished construction at 16:42:12
Placement dataframe started construction at 16:42:12
Placement dataframe finished construction at 16:42:12
Output animation generation complete at 16:42:13
Total Time Elapsed: 1.62 seconds

setup_mode = False

We can then rerun our plot with setup_mode=False to remove the grid lines and axis tick marks.

animate_activity_log(
        event_log=my_trial.all_event_logs[my_trial.all_event_logs['run']==1],
        event_position_df= event_position_df,
        scenario=g(),
        entity_col_name="patient",
        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 16:42:13
Iteration through time-unit-by-time-unit logs complete 16:42:13
Snapshot df concatenation complete at 16:42:13
Reshaped animation dataframe finished construction at 16:42:14
Placement dataframe started construction at 16:42:14
Placement dataframe finished construction at 16:42:14
Output animation generation complete at 16:42:14
Total Time Elapsed: 1.57 seconds

Visualising long queues

Finally, let’s rerun this with some build-up of queues by forcing faster arrivals and reducing the number of resources.

g.arrival_df = "ed_arrivals_more_frequent.csv"
g.n_cubicles_trauma_treat = 3
g.n_cubicles_non_trauma_treat = 2
g.n_exam = 2

my_trial = Trial()

my_trial.run_trial()
animate_activity_log(
        event_log=my_trial.all_event_logs[my_trial.all_event_logs['run']==1],
        event_position_df= event_position_df,
        scenario=g(),
        entity_col_name="patient",
        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",
        step_snapshot_limit_gauges=True
    )
Animation function called at 16:42:16
Iteration through time-unit-by-time-unit logs complete 16:42:16
Snapshot df concatenation complete at 16:42:16
Reshaped animation dataframe finished construction at 16:42:17
Placement dataframe started construction at 16:42:17
Placement dataframe finished construction at 16:42:17
Output animation generation complete at 16:42:18
Total Time Elapsed: 1.70 seconds
Back to top