Advanced trace#

1. Imports#

The model used to demonstrate trace functionality is implemented in simpy

import simpy
simpy.__version__
'4.1.1'
import sim_tools
sim_tools.__version__
'0.7.1'
from sim_tools.distributions import (
    Exponential, 
    Normal, 
    Lognormal, 
    Bernoulli
)

from sim_tools.time_dependent import NSPPThinning
from sim_tools.trace import Traceable
import numpy as np
import pandas as pd
import itertools

2. Constants and defaults for modelling as-is#

2.1 Distribution parameters#

# sign-in/triage parameters
DEFAULT_TRIAGE_MEAN = 3.0

# registration parameters
DEFAULT_REG_MEAN = 5.0
DEFAULT_REG_VAR= 2.0

# examination parameters
DEFAULT_EXAM_MEAN = 16.0
DEFAULT_EXAM_VAR = 3.0
DEFAULT_EXAM_MIN = 0.5

# trauma/stabilisation
DEFAULT_TRAUMA_MEAN = 90.0

# Trauma treatment
DEFAULT_TRAUMA_TREAT_MEAN = 30.0
DEFAULT_TRAUMA_TREAT_VAR = 4.0

# Non trauma treatment
DEFAULT_NON_TRAUMA_TREAT_MEAN = 13.3
DEFAULT_NON_TRAUMA_TREAT_VAR = 2.0

# prob patient requires treatment given trauma
DEFAULT_NON_TRAUMA_TREAT_P = 0.60

# proportion of patients triaged as trauma
DEFAULT_PROB_TRAUMA = 0.12

2.2 Time dependent arrival rates data#

The data for arrival rates varies between clinic opening at 6am and closure at 12am.

# data are held in the Github repo and loaded from there.
NSPP_PATH = 'https://raw.githubusercontent.com/TomMonks/' \
 + 'open-science-for-sim/main/src/notebooks/01_foss_sim/data/ed_arrivals.csv'

2.3 Resource counts#

Inter count variables representing the number of resources at each activity in the processes.

DEFAULT_N_TRIAGE = 1
DEFAULT_N_REG = 1
DEFAULT_N_EXAM = 3
DEFAULT_N_TRAUMA = 2

# Non-trauma cubicles
DEFAULT_N_CUBICLES_1 = 1

# trauma pathway cubicles
DEFAULT_N_CUBICLES_2 = 1

2.4 Simulation model run settings#

# default random number SET
DEFAULT_RNG_SET = None
N_STREAMS = 20

# default results collection period
DEFAULT_RESULTS_COLLECTION_PERIOD = 60 * 19

# number of replications.
DEFAULT_N_REPS = 5

## set to True to show a trace of the simulation model (best used in single run mode)
DEBUG = False

# provide a list of integers that represent patient IDs.  Only these patients will be tracked
TRACKED = None

3. Model parameterisation#

For convienience a container class is used to hold the large number of model parameters. The Scenario class includes defaults these can easily be changed and at runtime to experiments with different designs.

def load_arrival_profile(profile_path):

       df = (pd.read_csv(profile_path)
              .reset_index()
              .rename(columns={'index':'t'})
              .assign(mean_iat = lambda x: 60.0 / x.arrival_rate,
                     t = lambda x: x.t * 60.0)
       )
       return df
load_arrival_profile(NSPP_PATH)
t period arrival_rate mean_iat
0 0.0 6AM-7AM 2.366667 25.352113
1 60.0 7AM-8AM 2.800000 21.428571
2 120.0 8AM-9AM 8.833333 6.792453
3 180.0 9AM-10AM 10.433333 5.750799
4 240.0 10AM-11AM 14.800000 4.054054
5 300.0 11AM-12PM 26.266667 2.284264
6 360.0 12PM-1PM 31.400000 1.910828
7 420.0 1PM-2PM 18.066667 3.321033
8 480.0 2PM-3PM 16.466667 3.643725
9 540.0 3PM-4PM 12.033333 4.986150
10 600.0 4PM-5PM 11.600000 5.172414
11 660.0 5PM-6PM 28.866667 2.078522
12 720.0 6PM-7PM 18.033333 3.327172
13 780.0 7PM-8PM 11.500000 5.217391
14 840.0 8PM-9PM 5.300000 11.320755
15 900.0 9PM-10PM 4.066667 14.754098
16 960.0 10PM-11PM 2.200000 27.272727
17 1020.0 11PM-12AM 2.100000 28.571429
class Scenario:
    '''
    Container class for scenario parameters/arguments
    
    Passed to a model and its process classes
    '''
    def __init__(self, random_number_set=DEFAULT_RNG_SET,
                 n_triage=DEFAULT_N_TRIAGE,
                 n_reg=DEFAULT_N_REG,
                 n_exam=DEFAULT_N_EXAM,
                 n_trauma=DEFAULT_N_TRAUMA,
                 n_cubicles_1=DEFAULT_N_CUBICLES_1,
                 n_cubicles_2=DEFAULT_N_CUBICLES_2,
                 triage_mean=DEFAULT_TRIAGE_MEAN,
                 reg_mean=DEFAULT_REG_MEAN,
                 reg_var=DEFAULT_REG_VAR,
                 exam_mean=DEFAULT_EXAM_MEAN,
                 exam_var=DEFAULT_EXAM_VAR,
                 exam_min=DEFAULT_EXAM_MIN,
                 trauma_mean=DEFAULT_TRAUMA_MEAN,
                 trauma_treat_mean=DEFAULT_TRAUMA_TREAT_MEAN,
                 trauma_treat_var=DEFAULT_TRAUMA_TREAT_VAR,
                 non_trauma_treat_mean=DEFAULT_NON_TRAUMA_TREAT_MEAN,
                 non_trauma_treat_var=DEFAULT_NON_TRAUMA_TREAT_VAR,
                 non_trauma_treat_p=DEFAULT_NON_TRAUMA_TREAT_P,
                 prob_trauma=DEFAULT_PROB_TRAUMA,
                 debug=DEBUG,
                 tracked=TRACKED):
        '''
        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 stabilisation
            
        n_cubicles_1: int
            The number of non-trauma treatment cubicles
            
        n_cubicles_2: 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)

        exam_min: float
            The minimum value that an examination can take (Truncated 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.
            
        debug: int
            Set to true to display a trace of simulated events in the model
            Note this is best used in single_run mode.
            
        tracked: list
            List of integers that represent patient IDs.
            Used with debug. If debug is True then only display trace for specific
            patients.
        '''
        # sampling
        self.random_number_set = random_number_set
        
        # store parameters for sampling
        self.triage_mean = triage_mean
        self.reg_mean = reg_mean
        self.reg_var = reg_var
        self.exam_mean= exam_mean
        self.exam_var = exam_var
        self.exam_min = exam_min
        self.trauma_mean = trauma_mean
        self.trauma_treat_mean = trauma_treat_mean
        self.trauma_treat_var = trauma_treat_var
        self.non_trauma_treat_mean = non_trauma_treat_mean
        self.non_trauma_treat_var = non_trauma_treat_var
        self.non_trauma_treat_p = non_trauma_treat_p
        self.prob_trauma = prob_trauma
                
        self.init_sampling()
        
        # count of each type of resource
        self.init_resourse_counts(n_triage, n_reg, n_exam, n_trauma,
                                  n_cubicles_1, n_cubicles_2)
        
        # debug/trace information
        self.debug = debug
        self.tracked = tracked
    
    def set_random_no_set(self, random_number_set):
        '''
        Controls the random sampling 
        Parameters:
        ----------
        random_number_set: int
            Used to control the set of pseudo random numbers
            used by the distributions in the simulation.
        '''
        self.random_number_set = random_number_set
        self.init_sampling()

    def init_resourse_counts(self, n_triage, n_reg, n_exam, n_trauma,
                             n_cubicles_1, n_cubicles_2):
        '''
        Init the counts of resources to default values...
        '''
        self.n_triage = n_triage
        self.n_reg = n_reg
        self.n_exam = n_exam
        self.n_trauma = n_trauma
        
        # non-trauma (1), trauma (2) treatment cubicles
        self.n_cubicles_1 = n_cubicles_1
        self.n_cubicles_2 = n_cubicles_2

    def init_sampling(self):
        '''
        Create the distributions used by the model and initialise 
        the random seeds of each.
        '''       
        # MODIFICATION. Better method for producing n non-overlapping streams
        seed_sequence = np.random.SeedSequence(self.random_number_set)
    
        # Generate n high quality child seeds
        self.seeds = seed_sequence.spawn(N_STREAMS)
        
        # create distributions
        
        # Triage duration
        self.triage_dist = Exponential(self.triage_mean, 
                                       random_seed=self.seeds[0])
        
        # Registration duration (non-trauma only)
        self.reg_dist = Lognormal(self.reg_mean, 
                                  np.sqrt(self.reg_var),
                                  random_seed=self.seeds[1])
        
        # Evaluation (non-trauma only)
        self.exam_dist = Normal(self.exam_mean,
                                np.sqrt(self.exam_var),
                                minimum=self.exam_min,
                                random_seed=self.seeds[2])
        
        # Trauma/stablisation duration (trauma only)
        self.trauma_dist = Exponential(self.trauma_mean, 
                                       random_seed=self.seeds[3])
        
        # Non-trauma treatment
        self.nt_treat_dist = Lognormal(self.non_trauma_treat_mean, 
                                       np.sqrt(self.non_trauma_treat_var),
                                       random_seed=self.seeds[4])
        
        # treatment of trauma patients
        self.treat_dist = Lognormal(self.trauma_treat_mean, 
                                    np.sqrt(self.trauma_treat_var),
                                    random_seed=self.seeds[5])
        
        # probability of non-trauma patient requiring treatment
        self.nt_p_treat_dist = Bernoulli(self.non_trauma_treat_p, 
                                         random_seed=self.seeds[6])
        
        
        # probability of non-trauma versus trauma patient
        self.p_trauma_dist = Bernoulli(self.prob_trauma, 
                                       random_seed=self.seeds[7])
        
        # init sampling for non-stationary poisson process
        self.init_nspp()
        
    def init_nspp(self):
        
        # read arrival profile
        self.arrivals = load_arrival_profile(NSPP_PATH)

        # maximum arrival rate (smallest time between arrivals)
        self.lambda_max = self.arrivals['arrival_rate'].max()
        
        # NSPP thinning distribution
        self.thinning_dist = NSPPThinning(self.arrivals,
                                          self.seeds[8],
                                           self.seeds[9])

6. Patient Pathways Process Logic#

simpy uses a process based worldview. We can easily create whatever logic - simple or complex for the model. Here the process logic for trauma and non-trauma patients is seperated into two classes TraumaPathway and NonTraumaPathway.

class TraumaPathway(Traceable):
    '''
    Encapsulates the process a patient with severe injuries or illness.
    
    These patients are signed into the ED and triaged as having severe injuries
    or illness.
    
    Patients are stabilised in resus (trauma) and then sent to Treatment.  
    Following treatment they are discharged.
    '''
    def __init__(self, identifier, env, args):
        '''
        Constructor method
        
        Params:
        -----
        identifier: int
            a numeric identifier for the patient.
            
        env: simpy.Environment
            the simulation environment
            
        args: Scenario
            Container class for the simulation parameters
            
        '''
        # initialise Trace
        super().__init__(debug=args.debug)
        
        self.identifier = identifier
        self.env = env
        self.args = args
        
        # metrics
        self.arrival = -np.inf
        self.wait_triage = -np.inf
        self.wait_trauma = -np.inf
        self.wait_treat = -np.inf
        self.total_time = -np.inf
        
        self.triage_duration = -np.inf
        self.trauma_duration = -np.inf
        self.treat_duration = -np.inf
        
    def execute(self):
        '''
        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
        self.arrival = self.env.now

        self.trauma_arrival()
        
        # request sign-in/triage 
        with self.args.triage.request() as req:
            yield req
            # record the waiting time for triage
            self.wait_triage = self.env.now - self.arrival     
            
            # triage begin event
            self.triage_begin()
        
            # sample triage duration.
            self.triage_duration = self.args.triage_dist.sample()
            yield self.env.timeout(self.triage_duration)
            
            # triage complete event
            self.triage_complete()
            
        # record the time that entered the trauma queue
        start_wait = self.env.now
    
        # request trauma room 
        with self.args.trauma.request() as req:
            yield req
            
            # record the waiting time for trauma
            self.wait_trauma = self.env.now - start_wait
            
            # trauma begin event
            self.trauma_begin()
            
            # sample stablisation duration.
            self.trauma_duration = self.args.trauma_dist.sample()
            yield self.env.timeout(self.trauma_duration)
            
            # trauma complete event
            self.trauma_complete()
            
        # record the time that entered the treatment queue
        start_wait = self.env.now
    
        # request treatment cubicle 
        with self.args.cubicle_2.request() as req:
            yield req
            
            # record the waiting time for trauma
            self.wait_treat = self.env.now - start_wait
            self.treatment_begin()
            
            # sample treatment duration.
            self.treat_duration = self.args.treat_dist.sample()
            yield self.env.timeout(self.treat_duration)
            
            self.treatment_complete()
        
        # total time in system
        self.total_time = self.env.now - self.arrival     
    
    def trauma_arrival(self):
        '''Trauma arrival event'''
        self.trace(self.env.now, 'arrival at centre 🚑', self.identifier)
    
    def triage_begin(self):
        '''Enter triage event'''
        self.trace(self.env.now, f'enter triage. Waiting time: {self.wait_triage:.3f}', 
                   self.identifier)
          
    def trauma_begin(self):
        '''Enter trauma stabilisation'''
        self.trace(self.env.now, f'enter stabilisation. Waiting time: {self.wait_trauma:.3f}', 
                   self.identifier)
        
    def treatment_begin(self):
        '''Enter trauma treatment post stabilisation'''
        self.trace(self.env.now, f'enter treatment. Waiting time: {self.wait_treat:.3f}', 
                   self.identifier)
    
    def triage_complete(self):
        '''Triage complete event'''
        self.trace(self.env.now, 'triage complete', self.identifier)
          
    def trauma_complete(self):
        '''Patient stay in trauma is complete.'''
        self.trace(self.env.now, 'stabilisation complete.', self.identifier)
    
    def treatment_complete(self):
        '''Treatment complete event'''
        self.trace(self.env.now, f'patient {self.identifier} treatment complete; '
                   f'waiting time was {self.wait_treat:.3f}', self.identifier)
    
    def _trace_config(self):
        '''Override trace config'''
        config = {
            "name":"Trauma", 
            "name_colour":"bold magenta", 
            "time_colour":'bold magenta', 
            "tracked":self.args.tracked
        }
        return config
class NonTraumaPathway(Traceable):
    '''
    Encapsulates the process a patient with minor injuries and illness.
    
    These patients are signed into the ED and triaged as having minor 
    complaints and streamed to registration and then examination. 
    
    Post examination 40% are discharged while 60% proceed to treatment.  
    Following treatment they are discharged.
    '''
    def __init__(self, identifier, env, args):
        '''
        Constructor method
        
        Params:
        -----
        identifier: int
            a numeric identifier for the patient.
            
        env: simpy.Environment
            the simulation environment
            
        args: Scenario
            Container class for the simulation parameters
            
        '''
        super().__init__(debug=args.debug)
        self.identifier = identifier
        self.env = env
        self.args = args
        
        # triage resource
        self.triage = args.triage
        
        # metrics
        self.arrival = -np.inf
        self.wait_triage = -np.inf
        self.wait_reg = -np.inf
        self.wait_exam = -np.inf
        self.wait_treat = -np.inf
        self.total_time = -np.inf
        
        self.triage_duration = -np.inf
        self.reg_duration = -np.inf
        self.exam_duration = -np.inf
        self.treat_duration = -np.inf
        
        
    def execute(self):
        '''
        simulates the non-trauma/minor treatment process for a patient
        
        1. request and wait for sign-in/triage
        2. patient registration
        3. examination
        4. split patients
        4.1 % discharged
        4.2 % treatment then discharge
        '''
        # record the time of arrival and entered the triage queue
        self.arrival = self.env.now
        
        # trace arrival
        self.trace(self.env.now, 'arrival to centre.', self.identifier)

        # request sign-in/triage 
        with self.triage.request() as req:
            yield req
            
            # record the waiting time for triage
            self.wait_triage = self.env.now - self.arrival
            
            # trace enter triage
            self.trace(self.env.now, 'entering triage. '
                       f'Waiting time: {self.wait_triage:.3f} mins', 
                       self.identifier)
                        
                        
            # sample triage duration.
            self.triage_duration = self.args.triage_dist.sample()
            yield self.env.timeout(self.triage_duration)
            
            # trace exit triage
            self.trace(self.env.now, 'triage complete', self.identifier)
                        
        # record the time that entered the registration queue
        start_wait = self.env.now
    
        # request registration clert 
        with self.args.registration.request() as req:
            yield req
            
            # record the waiting time for registration
            self.wait_reg = self.env.now - start_wait
            
            # trace begin registration
            self.trace(self.env.now, 'starting patient registration.', self.identifier)
            
            # sample registration duration.
            self.reg_duration = self.args.reg_dist.sample()
            yield self.env.timeout(self.reg_duration)
            
            # registration complete...
            self.trace(self.env.now, 'patient registered;'
                       f'waiting time was {self.wait_triage:.3f}', 
                       self.identifier)
            
        # record the time that entered the evaluation queue
        start_wait = self.env.now
    
        # request examination resource
        with self.args.exam.request() as req:
            yield req
            
            # record the waiting time for registration
            self.wait_exam = self.env.now - start_wait
            
             # trace begin examination
            self.trace(self.env.now, f'enter examination. Waiting time: {self.wait_exam:.3f}', 
                       self.identifier)
            
            # sample examination duration.
            self.exam_duration = self.args.exam_dist.sample()
            yield self.env.timeout(self.exam_duration)
            
            # trace exit examination
            self.trace(self.env.now, 'examination complete.', self.identifier)
            
        # sample if patient requires treatment?
        self.require_treat = self.args.nt_p_treat_dist.sample()
        
        if self.require_treat:
        
            # record the time that entered the treatment queue
            start_wait = self.env.now
    
            # request treatment cubicle
            with self.args.cubicle_1.request() as req:
                yield req

                # record the waiting time for treatment
                self.wait_treat = self.env.now - start_wait
                
                # trace enter treatment
                self.trace(self.env.now, f'enter treatment. Waiting time:{self.wait_treat:.3f}',
                           self.identifier)
        
                # sample treatment duration.
                self.treat_duration = self.args.nt_treat_dist.sample()
                yield self.env.timeout(self.treat_duration)

                # trace exit treatment
                self.trace(self.env.now, 'treatment complete â›”', self.identifier)
                
        # total time in system
        self.total_time = self.env.now - self.arrival      
        
    def _trace_config(self):
        '''Override trace config'''
        config = {
            "name":"Non-trauma", 
            "name_colour":"bold green", 
            "time_colour":'bold green', 
            "message_colour":'black',
            "tracked":self.args.tracked
        }
        return config
    

7. Main model class#

The main class that a user interacts with to run the model is TreatmentCentreModel. This implements a .run() method, contains a simple algorithm for the non-stationary poission process for patients arrivals and inits instances of TraumaPathway or NonTraumaPathway depending on the arrival type.

class TreatmentCentreModel:
    '''
    The treatment centre model
    
    Patients arrive at random to a treatment centre, are triaged
    and then processed in either a trauma or non-trauma pathway.
    '''
    def __init__(self, args):
        self.env = simpy.Environment()
        self.args = args
        self.init_resources()
        
        self.patients = []
        self.trauma_patients = []
        self.non_trauma_patients = []

        self.rc_period = None
        self.results = None
        
    def init_resources(self):
        '''
        Init the number of resources
        and store in the arguments container object
        
        Resource list:
        1. Sign-in/triage bays
        2. registration clerks
        3. examination bays
        4. trauma bays
        5. non-trauma cubicles (1)
        6. trauma cubicles (2)
         
        '''
        # sign/in triage
        self.args.triage = simpy.Resource(self.env, 
                                          capacity=self.args.n_triage)
        
        # registration
        self.args.registration = simpy.Resource(self.env, 
                                                capacity=self.args.n_reg)
        
        # examination
        self.args.exam = simpy.Resource(self.env, 
                                        capacity=self.args.n_exam)
        
        # trauma
        self.args.trauma = simpy.Resource(self.env, 
                                          capacity=self.args.n_trauma)
        
        # non-trauma treatment
        self.args.cubicle_1 = simpy.Resource(self.env, 
                                              capacity=self.args.n_cubicles_1)
            
        # trauma treatment
        self.args.cubicle_2 = simpy.Resource(self.env, 
                                              capacity=self.args.n_cubicles_2)
        
        
        
    def run(self, results_collection_period=DEFAULT_RESULTS_COLLECTION_PERIOD):
        '''
        Conduct a single run of the model in its current 
        configuration
        
        
        Parameters:
        ----------
        results_collection_period, float, optional
            default = DEFAULT_RESULTS_COLLECTION_PERIOD
            
        warm_up, float, optional (default=0)
        
            length of initial transient period to truncate
            from results.
            
        Returns:
        --------
            None
        '''
        # setup the arrival generator process
        self.env.process(self.arrivals_generator())
        
        # store rc perio
        self.rc_period = results_collection_period
        
        # run
        self.env.run(until=results_collection_period)
        
    
    def arrivals_generator(self):  
        ''' 
        Simulate the arrival of patients to the model
        
        Patients either follow a TraumaPathway or
        NonTraumaPathway simpy process.
        
        Non stationary arrivals implemented via Thinning acceptance-rejection 
        algorithm.
        '''
        for patient_count in itertools.count():

            # sample iat via thinning
            interarrival_time = self.args.thinning_dist.sample(self.env.now)

            # delay until next arrival
            yield self.env.timeout(interarrival_time)
            
            # sample if the patient is trauma or non-trauma
            trauma = self.args.p_trauma_dist.sample()
            if trauma:
                # create and store a trauma patient to update KPIs.
                new_patient = TraumaPathway(patient_count, self.env, self.args)
                self.trauma_patients.append(new_patient)
            else:
                # create and store a non-trauma patient to update KPIs.
                new_patient = NonTraumaPathway(patient_count, self.env, self.args)
                self.non_trauma_patients.append(new_patient)
                
            # start the pathway process for the patient
            self.env.process(new_patient.execute())

8. Logic to process end of run results.#

the class SimulationSummary accepts a TraumaCentreModel. At the end of a run it can be used calculate mean queuing times and the percentage of the total run that a resource was in use.

class SimulationSummary:
    '''
    End of run result processing logic of the simulation model
    '''
    def __init__(self, model):
        '''
        Constructor
        
        Params:
        ------
        model: TraumaCentreModel
            The model.
        '''
        self.model = model
        self.args = model.args
        self.results = None
    
    def process_run_results(self):
        '''
        Calculates statistics at end of run.
        '''
        self.results = {}
        # list of all patients 
        patients = self.model.non_trauma_patients + self.model.trauma_patients
        
        # mean triage times (both types of patient)
        mean_triage_wait = self.get_mean_metric('wait_triage', patients)
        
        # triage utilisation (both types of patient)
        triage_util = self.get_resource_util('triage_duration', 
                                             self.args.n_triage, 
                                             patients)
        
        # mean waiting time for registration (non_trauma)
        mean_reg_wait = self.get_mean_metric('wait_reg', 
                                             self.model.non_trauma_patients)
        
        # registration utilisation (trauma)
        reg_util = self.get_resource_util('reg_duration', 
                                          self.args.n_reg, 
                                          self.model.non_trauma_patients)
        
        # mean waiting time for examination (non_trauma)
        mean_wait_exam = self.get_mean_metric('wait_exam', 
                                              self.model.non_trauma_patients)
        
        # examination utilisation (non-trauma)
        exam_util = self.get_resource_util('exam_duration', 
                                            self.args.n_exam, 
                                            self.model.non_trauma_patients)
        
        
        # mean waiting time for treatment (non-trauma) 
        mean_treat_wait = self.get_mean_metric('wait_treat', 
                                               self.model.non_trauma_patients)
        
        # treatment utilisation (non_trauma)
        treat_util1 = self.get_resource_util('treat_duration', 
                                             self.args.n_cubicles_1, 
                                             self.model.non_trauma_patients)
        
        # mean total time (non_trauma)
        mean_total = self.get_mean_metric('total_time', 
                                          self.model.non_trauma_patients)
                                    
        # mean waiting time for trauma 
        mean_trauma_wait = self.get_mean_metric('wait_trauma', 
                                                self.model.trauma_patients)
        
        # trauma utilisation (trauma)
        trauma_util = self.get_resource_util('trauma_duration', 
                                             self.args.n_trauma, 
                                             self.model.trauma_patients)
        
        # mean waiting time for treatment (rauma) 
        mean_treat_wait2 = self.get_mean_metric('wait_treat', 
                                                self.model.trauma_patients)
        
        # treatment utilisation (trauma)
        treat_util2 = self.get_resource_util('treat_duration', 
                                             self.args.n_cubicles_2, 
                                             self.model.trauma_patients)

        # mean total time (trauma)
        mean_total2 = self.get_mean_metric('total_time', 
                                           self.model.trauma_patients)
        
        
        self.results = {'00_arrivals':len(patients),
                        '01a_triage_wait': mean_triage_wait,
                        '01b_triage_util': triage_util,
                        '02a_registration_wait':mean_reg_wait,
                        '02b_registration_util': reg_util,
                        '03a_examination_wait':mean_wait_exam,
                        '03b_examination_util': exam_util,
                        '04a_treatment_wait(non_trauma)':mean_treat_wait,
                        '04b_treatment_util(non_trauma)':treat_util1,
                        '05_total_time(non-trauma)':mean_total,
                        '06a_trauma_wait':mean_trauma_wait,
                        '06b_trauma_util':trauma_util,
                        '07a_treatment_wait(trauma)':mean_treat_wait2,
                        '07b_treatment_util(trauma)':treat_util2,
                        '08_total_time(trauma)':mean_total2,
                        '09_throughput': self.get_throughput(patients)}
        
    def get_mean_metric(self, metric, patients):
        '''
        Calculate mean of the performance measure for the
        select cohort of patients,
        
        Only calculates metrics for patients where it has been 
        measured.
        
        Params:
        -------
        metric: str
            The name of the metric e.g. 'wait_treat'
            
        patients: list
            A list of patients
        '''
        mean = np.array([getattr(p, metric) for p in patients 
                         if getattr(p, metric) > -np.inf]).mean()
        return mean
    
    
    def get_resource_util(self, metric, n_resources, patients):
        '''
        Calculate proportion of the results collection period
        where a resource was in use.
        
        Done by tracking the duration by patient.
        
        Only calculates metrics for patients where it has been 
        measured.
        
        Params:
        -------
        metric: str
            The name of the metric e.g. 'treatment_duration'
            
        patients: list
            A list of patients
        '''
        total = np.array([getattr(p, metric) for p in patients 
                         if getattr(p, metric) > -np.inf]).sum() 
        
        return total / (self.model.rc_period * n_resources)
    
    def get_throughput(self, patients):
        '''
        Returns the total number of patients that have successfully
        been processed and discharged in the treatment centre
        (they have a total time record)
        
        Params:
        -------
        patients: list
            list of all patient objects simulated.
        
        Returns:
        ------
        float
        '''
        return len([p for p in patients if p.total_time > -np.inf])
    
    def summary_frame(self):
        '''
        Returns run results as a pandas.DataFrame
        
        Returns:
        -------
        pd.DataFrame
        '''
        #append to results df
        if self.results is None:
            self.process_run_results()

        df = pd.DataFrame({'1':self.results})
        df = df.T
        df.index.name = 'rep'
        return df
    

9. Model execution#

We note that there are many ways to setup a simpy model and execute it (that is part of its fantastic flexibility). The organisation of code we show below is based on our experience of using the package in practice. The approach also allows for easy parallisation over multiple CPU cores using joblib.

We include two functions. single_run() and multiple_replications. The latter is used to repeatedly call and process the results from single_run.

def single_run(scenario, rc_period=DEFAULT_RESULTS_COLLECTION_PERIOD, 
               random_no_set=DEFAULT_RNG_SET):
    '''
    Perform a single run of the model and return the results
    
    Parameters:
    -----------
    
    scenario: Scenario object
        The scenario/paramaters to run
        
    rc_period: int
        The length of the simulation run that collects results
        
    random_no_set: int or None, optional (default=DEFAULT_RNG_SET)
        Controls the set of random seeds used by the stochastic parts of the 
        model.  Set to different ints to get different results.  Set to None
        for a random set of seeds.
        
    Returns:
    --------
        pandas.DataFrame:
        results from single run.
    '''  
        
    # set random number set - this controls sampling for the run.
    scenario.set_random_no_set(random_no_set)

    # create an instance of the model
    model = TreatmentCentreModel(scenario)

    # run the model
    model.run(results_collection_period=rc_period)
        
    # run results
    summary = SimulationSummary(model)
    summary_df = summary.summary_frame()
    
    return summary_df
def multiple_replications(scenario, rc_period=DEFAULT_RESULTS_COLLECTION_PERIOD, 
                          n_reps=5):
    '''
    Perform multiple replications of the model.
    
    Params:
    ------
    scenario: Scenario
        Parameters/arguments to configurethe model
    
    rc_period: float, optional (default=DEFAULT_RESULTS_COLLECTION_PERIOD)
        results collection period.  
        the number of minutes to run the model to collect results

    n_reps: int, optional (default=DEFAULT_N_REPS)
        Number of independent replications to run.
        
    Returns:
    --------
    pandas.DataFrame
    '''

    results = [single_run(scenario, rc_period, random_no_set=rep) 
               for rep in range(n_reps)]
    
    #format and return results in a dataframe
    df_results = pd.concat(results)
    df_results.index = np.arange(1, len(df_results)+1)
    df_results.index.name = 'rep'
    return df_results

9.1 Single run of the model#

The script below performs a single replication of the simulation model.

Try:

  • Changing the random_no_set of the single_run call.

  • Assigning the value True to debug

  • Tracking patient arrivals 10 and 19 through the model by setting tracked=[10, 19]

# create the default scenario. 
args = Scenario(debug=True, tracked=[10, 19])

# use the single_run() func
# try changing `random_no_set` to see different run results
print('Running simulation ...', end=' => ')
results = single_run(args, random_no_set=42)
print('simulation complete.')

# show results (transpose replication for easier view)
results.T
Running simulation ... => 
[169.77]:arrival to centre.
[175.62]:entering triage. Waiting time: 5.849 mins
[175.79]:triage complete
[188.70]:starting patient registration.
[193.51]:patient registered;waiting time was 5.849
[193.51]:enter examination. Waiting time: 0.000
[204.96]:arrival at centre 🚑
[208.94]:examination complete.
[208.94]:enter treatment. Waiting time:0.000
[214.79]:enter triage. Waiting time: 9.833
[221.03]:triage complete
[221.63]:treatment complete â›”
[384.82]:enter stabilisation. Waiting time: 163.799
[441.65]:stabilisation complete.
[473.10]:enter treatment. Waiting time: 31.447
[505.03]:patient 19 treatment complete; waiting time was 31.447
simulation complete.
rep 1
00_arrivals 209.000000
01a_triage_wait 16.622674
01b_triage_util 0.527512
02a_registration_wait 111.161345
02b_registration_util 0.801061
03a_examination_wait 24.927965
03b_examination_util 0.851285
04a_treatment_wait(non_trauma) 172.435861
04b_treatment_util(non_trauma) 0.845652
05_total_time(non-trauma) 248.848441
06a_trauma_wait 201.144403
06b_trauma_util 0.919830
07a_treatment_wait(trauma) 22.620880
07b_treatment_util(trauma) 0.493576
08_total_time(trauma) 310.384648
09_throughput 155.000000

9.2 Disabling trace for multiple independent replications#

Important: set debug=False in Scenario for multiple replications. This will ensure that each replication is not traced.

args = Scenario(debug=False)

#run multiple replications.
results  = multiple_replications(args, n_reps=50)

print("All replications complete.")

# summarise the results (2.dp)
results.mean().round(2)
All replications complete.
00_arrivals                       227.72
01a_triage_wait                    35.24
01b_triage_util                     0.61
02a_registration_wait             105.57
02b_registration_util               0.84
03a_examination_wait               25.55
03b_examination_util                0.85
04a_treatment_wait(non_trauma)    136.66
04b_treatment_util(non_trauma)      0.87
05_total_time(non-trauma)         234.34
06a_trauma_wait                   151.68
06b_trauma_util                     0.83
07a_treatment_wait(trauma)         14.31
07b_treatment_util(trauma)          0.50
08_total_time(trauma)             292.28
09_throughput                     162.16
dtype: float64