<jupyter_start><jupyter_text>Computer Simulation Lab 2. Modelling more complex pathways**In the lab you will learn how to :*** design and code more complex health pathways in SimPy* build a computer simulation model of a minor injury unit.* use python functions to organisation your experimentation and scenarios. Organisation of the notebook.The main exercise in this notebook requires you to code the logic of a Minor Injury Unit. The notebook is provided with a number of classes and code to assist you in your model coding.  * Distribution classes e.g. the `Exponential` distribution.* A `Scenario` class to enable you to pass parameters to the model.* Template classes to represent the Minor Injury Unit and the process patients follow.**A video introduction (~30 mins) to the notebook, MIU problem and exercises is available [here](https://recapexeter.cloud.panopto.eu/Panopto/Pages/Viewer.aspx?id=83928f04-e118-48be-a2a4-ac7c00be5d5e)**-----  ImportsPlease use the provided `hds_stoch` environment for this work.<jupyter_code>#!pip install simpy

import simpy
simpy.__version__
import numpy as np
import pandas as pd
import itertools
import math
import matplotlib.pyplot as plt<jupyter_output><empty_output><jupyter_text>Problem description: Modelling a minor injury unitThe exercises in this notebook will focus on a model of a minor injury unit (MIU).  In the UK, the NHS provides MIU's provide assessment and treatment for non-life threatening injuries.  For example, cuts and broken bones. The process flow below displays a high level process map of a MIU.  * Patients arrive following a Poisson process, wait for nurse triage in one **triage bay**. * After triage is complete the patient waits for an assessment and treatment cubicle (there are **six**) where they are attended to by a nurse (here we assume the nurse is not a rate limiting step and we do not model nurse movement or availability).  * Around **45%** of patients require diagnostics (e.g. an x-ray or other imaging) in addition to an assessment.  * Around **75%** of diagnostics tests are booked by the nurse at the start of consultation with the remaining beginning their wait after the initial assessment.* Patients who undergo diagnostics are seen by a nurse for a 2nd time following the completion of their diagnostics.The duration of activities are:| Activity 	| Activity name        	| Distribution 	| Mean (mins) 	| Standard Dev (minutes) 	||----------	|----------------------	|--------------	|-------------	|------------------------	|| 1        	| IAT                  	| Exponential  	| 5.0          	|                        	|| 2        	| 1st Assessment            	| Lognormal        	| 10          	| 3                      	|| 3       	| Diagnostic waiting time  	| Exponential        	| 30          	|                       	|| 4       	| 2nd Asessment  	| Lognormal        	| 10          	| 5                      	|| Activity 	| Activity name        	| Distribution 	| Low 	| Mode 	| High ||----------	|----------------------	|--------------	|-------------	|------------------------	| || 5        	| Triage                  	| Triangular  	| 2.0          	|   5.0                     	| 10.0|| 6        	| Diagnostics           	| Triangular        	| 10.0          	| 15.0                      	|20.0 |![image](images/minor_injury_process_flow.png)  Distribution classesTo help you build your model, the notebook includes some pre-written distribution classes that you may wish to use to setup sampling.  You are free to use these, but you can choose not too if you have an approach you prefer.<jupyter_code>class Exponential:
    '''
    Convenience class for the exponential distribution.
    packages up distribution parameters, seed and random generator.
    '''
    def __init__(self, mean, random_seed=None):
        '''
        Constructor
        
        Params:
        ------
        mean: float
            The mean of the exponential distribution
        
        random_seed: int, optional (default=None)
            A random seed to reproduce samples.  If set to none then a unique
            sample is created.
        '''
        self.rand = np.random.default_rng(seed=random_seed)
        self.mean = mean
        
    def sample(self, size=None):
        '''
        Generate a sample from the exponential distribution
        
        Params:
        -------
        size: int, optional (default=None)
            the number of samples to return.  If size=None then a single
            sample is returned.
        '''
        return self.rand.exponential(self.mean, size=size)

class Triangular:
    '''
    Convenience class for the triangular distribution.
    packages up distribution parameters, seed and random generator.
    '''
    def __init__(self, low, mode, high, random_seed=None):
        self.rand = np.random.default_rng(seed=random_seed)
        self.low = low
        self.high = high
        self.mode = mode
        
    def sample(self, size=None):
        return self.rand.triangular(self.low, self.mode, self.high, size=size)
    
class Bernoulli:
    '''
    Convenience class for the Bernoulli distribution.
    packages up distribution parameters, seed and random generator.
    '''
    def __init__(self, p, random_seed=None):
        '''
        Constructor
        
        Params:
        ------
        p: float
            probability of drawing a 1
        
        random_seed: int, optional (default=None)
            A random seed to reproduce samples.  If set to none then a unique
            sample is created.
        '''
        self.rand = np.random.default_rng(seed=random_seed)
        self.p = p
        
    def sample(self, size=None):
        '''
        Generate a sample from the exponential distribution
        
        Params:
        -------
        size: int, optional (default=None)
            the number of samples to return.  If size=None then a single
            sample is returned.
        '''
        return self.rand.binomial(n=1, p=self.p, size=size)

class Lognormal:
    """
    Encapsulates a lognormal distirbution
    """
    def __init__(self, mean, stdev, random_seed=None):
        """
        Params:
        -------
        mean = mean of the lognormal distribution
        stdev = standard dev of the lognormal distribution
        """
        self.rand = np.random.default_rng(seed=random_seed)
        mu, sigma = self.normal_moments_from_lognormal(mean, stdev**2)
        self.mu = mu
        self.sigma = sigma
        
    def normal_moments_from_lognormal(self, m, v):
        '''
        Returns mu and sigma of normal distribution
        underlying a lognormal with mean m and variance v
        source: https://blogs.sas.com/content/iml/2014/06/04/simulate-lognormal
        -data-with-specified-mean-and-variance.html

        Params:
        -------
        m = mean of lognormal distribution
        v = variance of lognormal distribution
                
        Returns:
        -------
        (float, float)
        '''
        phi = math.sqrt(v + m**2)
        mu = math.log(m**2/phi)
        sigma = math.sqrt(math.log(phi**2/m**2))
        return mu, sigma
        
    def sample(self):
        """
        Sample from the normal distribution
        """
        return self.rand.lognormal(self.mu, self.sigma)
# example code to use the distributions

# each distribution has a unique constructor you can pass in a seed to control sampling
triage_dist = Triangular(2, 5, 10, random_seed=42)

# each distribution has a identical `sample` method signature
delay = triage_dist.sample()
print(delay)<jupyter_output>6.993048377881435<jupyter_text>Utility functionsIn the simulation the `trace` function is used instead of `print`.  You can then control if a simulation outputs a trace of events by setting `TRACE = True` or turning event tracing off by setting `TRACE = False`.<jupyter_code>def trace(msg):
    '''
    Utility function for printing simulation
    set the TRACE constant to FALSE to 
    turn tracing off.
    
    Params:
    -------
    msg: str
        string to print to screen.
    '''
    if TRACE:
        print(msg)<jupyter_output><empty_output><jupyter_text>Model parameters and the `Scenario` class* The constants below provides hard coded data representing the base case or 'as-is' state of the minor injury unit.   * In the cell below the parameters you will find a `Scenario` class.  This makes use of the default parameters to set up the base case scenario.  Remember that its good practice to pass all of your parameters to your simulation model in a **container**.  A class is a flexible way to achieve this aim.<jupyter_code># These are the parameters for a base case model run.
# Note if you change these parameters then your model will run a new 'scenario' 

# resource counts
N_CUBICLES = 6
N_BAYS = 1

# time between arrivals in minutes (exponential)
MEAN_IAT = 5

# triage (triangular)
TRIAGE_LOW = 2
TRIAGE_MODE = 5
TRIAGE_HIGH = 10

# assessment (lognormal)
ASSESS_MEAN = 10
ASSESS_STD = 3

# 2nd assessment (lognormal)
ASSESS_MEAN_2 = 15
ASSESS_STD_2 = 5

# diagnostics (bernoulli)
PROB_DIAG = 0.45 

# diagnostics waiting time (exp)
DIAG_WT_MEAN = 20

# diagnostics process time (triangular)
DIAG_PT_LOW = 10
DIAG_PT_MODE = 15
DIAG_PT_HIGH = 20

# % in parallel (bernoulli)
P_PARALLEL = 0.75

# SEEDS to reproduce results of a single run
REPRODUCIBLE_RUN = True
    
if REPRODUCIBLE_RUN:
    SEEDS = [42, 101, 1066, 1966, 2013, 999, 1444, 2016]
else:
    SEEDS = [None, None, None, None, None, None, None, None]
class Scenario:
    '''
    Parameter container class for minor injury unit model.
    '''
    def __init__(self, name=None):
        '''
        The init method sets up our defaults. 
        
        Params:
        -------
        
        name - str or None
            optional name for scenario
        '''
        # optional name
        self.name = name
        
        # triage bays
        self.triage_bays = N_BAYS
        
        # assessment and treatment cubicles
        self.minor_cubicles = N_CUBICLES
        
        # inter-arrival distribution
        self.arrival_dist = Exponential(MEAN_IAT, random_seed=SEEDS[0])
        
        # triage distribution
        self.triage_dist = Triangular(TRIAGE_LOW, TRIAGE_MODE, TRIAGE_HIGH, 
                                      random_seed=SEEDS[1])
        
        # assessment distribution
        self.assessment_dist = Lognormal(ASSESS_MEAN, ASSESS_STD, 
                                         random_seed=SEEDS[2])
        
        # diagnostics: prob that patient needs imaging etc.
        self.n_diagnostics_minor = Bernoulli(PROB_DIAG, random_seed=SEEDS[3])
        
        # waiting and process time dists for diagnostics
        self.diag_wait_dist = Exponential(DIAG_WT_MEAN, random_seed=SEEDS[4])
        self.diag_dist = Triangular(DIAG_PT_LOW, DIAG_PT_MODE, DIAG_PT_HIGH,
                                    random_seed=SEEDS[5])
        
        # prob diagnostics done in parallel to nurse assessment
        self.p_diag_parallel = Bernoulli(P_PARALLEL, random_seed=SEEDS[6])
        
        # 2nd assessment distribution
        self.assessment_dist_2 = Lognormal(ASSESS_MEAN_2, ASSESS_STD_2, 
                                           random_seed=SEEDS[7])<jupyter_output><empty_output><jupyter_text>--- Exercise 1 : Model building**Task:** * Code the logic for the process that a patient undergoes the minor injury unit.* Run the model using the provided script.* Modify the script that runs the model so that it calculates the following performance measures:  * Time in the system (from arrival to departure)  * Time to nurse (from arrival to start of assessment 1)  * Time to triage (from arrival to start of triage)  * Four hour target (if that patient spent less than 4 hours in the MIU).  **General Hints:*** You have been provided with skeleton code and classes to help you complete this task.  * The class `MinorPatient` has its `__init__` method completed for you.    * The method accepts `args` as a parameter.  This is an instance of `Scenario` and contains the distributions used by `MinorPatient` for sampling etc.  * Note that the contructor sets up some performance metrics for the patient e.g. `self.time_to_triage`* You have been provided with a `MinorInjuryUnit` class to use as the main model object.  This class generates patient arrivals and creates instances of `MinorPatient`.  Note that all patients are saved in list `self.patients`* Remember to go back to the process map at the start of the notebook to remind you about logic.**HINTS - Modelling the triage process:*** The first thing to code is a triage.  The patient needs to request a resource, wait for it to be available and then sample a triage duration from the correct distribution.  * Remember that to request a resource you need to follow this simpy pattern:```python>> with self.triage_bays.request() as req:>>     yield req```* Remember that simpy works by using **python generators**. Use the **yield** keyword in combination `simpy.Environment.timeout()` method to create delays in the process. For example, ```python>> triage_delay = self.triage_dist.sample()>> yield self.env.timeout(triage_delay)```**HINTS - Modelling assessment 1 and diagnostics processes**:* The tricky part of this modelling exercise is to model a percentage of diagnostics tests that occur in parallel to nurse assessment.  Some advice:  * For this exercise, you should assume that assessment 1 time begins the moment a patient is assigned a cubicle.  * Remember only 45% of patients need diagnostics and the nurse orders 75% of diagnostics tests to run in parallel to assessment 1.  * **Keep it simple** and build the model up incrementally.  I.e. build a simple model that runs and then add more detail to it.    * A first task could be to model the process where assessment is always followed by diagnostics.  You could then limit that to a percentage of patients.   * When diagnostics are in parallel you then you only need to model a delay that is the maximum of either assessment time or diagnostic waiting time + diagnostics duration.* A patient keeps their cubicle while undergoing diagnostics.  Don't allow the cubicle to be allocated to a new patient.**HINTS - tracking time in the model*** You will need to keep track of how long it takes patients to complete processes in the model e.g. time to triage and time to first nurse assessment.* An example of recording a time in the model is as follows:```python>> arrival_time = self.env.now```* When you need to calculate a **time to something** you just need to take the difference between the current time and the arrival time. E.g.```python>> self.time_to_triage = self.env.now - arrival_time```**HINTS - calculating end of run performance measures**:* In this exercise it is easiest to calculate your performance measures at the end of the model run.* `MinorInjuryUnit` holds a list of `MinorPatient` objects called `patients`.  Iterate (loop) over the list and access the relevant performance measure of each patient.  You can then take the mean.  For example, assume that you have an `MinorInjuryUnit`object assigned to the variable `model`.  To calculate the time to triage:```python>> mean_time_to_triage = 0.0>> for patient in model.patients:>>     mean_time_to_triage += patient.time_to_triage>> mean_time_to_triage = mean_time_to_triage / len(model.patients)```alternatively you could use a list comprehension, cast to a `numpy.ndarray` and call the `mean()` method.```python>> mean_time_to_triage = np.array([patient.time_to_triage                                    for patient in model.patients]).mean()```<jupyter_code>class MinorPatient:
    '''
    Simulates the process for a minor injury patient
    '''
    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
            The input data for the scenario
        '''
        # patient id and environment
        self.identifier = identifier
        self.env = env

        # triage parameters
        self.triage_bays = args.triage_bays
        self.triage_dist = args.triage_dist
    
        # minor assessment parameters
        self.minor_cubicles = args.minor_cubicles
        self.assessment_dist = args.assessment_dist
        self.assessment_dist_2 = args.assessment_dist_2
        
        # diagnostics: prob that patient needs imaging etc.
        self.n_diagnostics_dist = args.n_diagnostics_minor
        
        # prob diagnostics done in parallel to nurse assessment
        self.p_diag_parallel = args.p_diag_parallel
        
        # waiting and process time dists for diagnostics
        self.diag_wait_dist = args.diag_wait_dist 
        self.diag_dist = args.diag_dist
                
        # individual patient metrics
        self.time_to_triage = 0.0
        self.time_to_nurse = 0.0
        self.time_in_system = 0.0
        self.four_hour_target = 0.0
        
    
    def assessment(self):
        '''
        simulates the process for minor emergencies
        
        1. request and wait for triage
        2. request and wait for a minor cubicle
        3. minor assessment
        4. Diagnostics (in parallel to or after 3.)
        5. 2nd assessment if the patient has undergone diagnostics
        6. exit system.
        
        '''
        # your code here...
        pass
            
                    
class MinorInjuryUnit:  
    '''
    Model of an minor injury unit
    '''
    def __init__(self, env, args):
        '''
        Contructor
        
        Params:
        -------
        env: simpy.Environment
        
        args: Scenario
            container class for simulation model inputs.
        '''
        self.env = env
        self.args = args 
        self.init_model_resources(args)
        self.patients = []
        
        
    def init_model_resources(self, args):
        '''
        Setup the simpy resource objects
        
        Params:
        ------
        args - Scenario
            Simulation Parameter Container
        '''
        args.triage_bays = simpy.Resource(self.env, 
                                          capacity=args.triage_bays)
        args.minor_cubicles = simpy.Resource(self.env, 
                                             capacity=args.minor_cubicles)
        
            
    def arrivals_generator(self):
        '''
        IAT is exponentially distributed

        Parameters:
        ------
        env: simpy.Environment

        args: Scenario
            Container class for model data inputs
        '''
        for patient_count in itertools.count(start=1):
            inter_arrival_time = self.args.arrival_dist.sample()
            yield self.env.timeout(inter_arrival_time)

            trace(f'patient {patient_count} arrives at: {self.env.now:.3f}')

            # create a new minor patient and pass in env and args
            new_patient = MinorPatient(patient_count, self.env, self.args)
            
            # keep a record of the patient for results calculation
            self.patients.append(new_patient)
            
            # init the minor injury process for this patient
            self.env.process(new_patient.assessment())
            

# example solution ....
class MinorPatient:
    '''
    Patient in the minor ED process
    '''
    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
            The input data for the scenario
        '''
        # patient id and environment
        self.identifier = identifier
        self.env = env

        # triage parameters
        self.triage_bays = args.triage_bays
        self.triage_dist = args.triage_dist
    
        # minor assessment parameters
        self.minor_cubicles = args.minor_cubicles
        self.assessment_dist = args.assessment_dist
        self.assessment_dist_2 = args.assessment_dist_2
        
        # diagnostics: prob that patient needs imaging etc.
        self.n_diagnostics_dist = args.n_diagnostics_minor
        
        # prob diagnostics done in parallel to nurse assessment
        self.p_diag_parallel = args.p_diag_parallel
        
        # waiting and process time dists for diagnostics
        self.diag_wait_dist = args.diag_wait_dist 
        self.diag_dist = args.diag_dist
                
        # individual patient metrics
        self.time_to_triage = 0.0
        self.time_to_nurse = 0.0
        self.time_in_system = 0.0
        self.four_hour_target = 0.0
        
    
    def assessment(self):
        '''
        simulates the process for minor emergencies
        
        1. request and wait for triage
        2. request and wait for a minor cubicle
        3. minor assessment
        4. exit system.
        
        '''
        # record the time that patient entered the system
        arrival_time = self.env.now

        # request an operator 
        with self.triage_bays.request() as req:
            yield req
                        
            trace(f'minor triage {self.identifier} at {self.env.now:.3f}')
            
            # time to triage
            self.time_to_triage = self.env.now - arrival_time
            
            # sample triage duration.
            triage_duration = self.triage_dist.sample()
           
            yield self.env.timeout(triage_duration)            
            
            trace(f'triage ended {self.identifier} ended {self.env.now:.3f}; '
                      + f' waiting time was {self.env.now - arrival_time:.3f}')
            
        
        # get cubicle
        with self.minor_cubicles.request() as req:
            yield req
            
            # record time to first being seen by a nurse
            self.time_to_nurse = self.env.now - arrival_time
            
            trace(f'minor assessment {self.identifier} at {self.env.now:.3f}; '
                      + f' time to nurse was {self.time_to_nurse:.3f}')
            
            # sample for patient pathway
            assess_time, n_diagnostics, \
                parallel, diag_wt, diag_pt, assess_time_2 = self.sample_all()
            
            # patient undergoing diagnostics?
            if n_diagnostics > 0:

                #in parallel to minor assessment?
                if parallel:
                    
                    trace(f'parallel diagnostics patient {self.identifier}')
                    
                    #use the longest delay for assessment 1
                    activity_duration = max(assess_time, diag_wt + diag_pt)
                    
                else:
                    
                    trace(f'diagnostics following assessment for '
                          + f'patient {self.identifier}')
                    
                    #use the total delay for assessment 1
                    activity_duration = assess_time + diag_wt + diag_pt 
            else:
                activity_duration = assess_time
            
          
            # assessment 1 delay
            yield self.env.timeout(activity_duration)
            
            if n_diagnostics > 0:
                #assessment 2 delay
                
                trace(f'{self.identifier} enters 2nd assessment'
                      + f' at {self.env.now:.3f}')
                         
                yield self.env.timeout(assess_time_2)
                
                trace(f'{self.identifier} completes 2nd assessment'
                      + f' at {self.env.now:.3f}')
            
            # time in system for this patient
            self.time_in_system = self.env.now - arrival_time
            
            trace(f'{self.identifier} departs at {self.env.now:.3f}; '
                      + f' time in system{self.time_in_system:.3f}')
            
            if self.time_in_system <= (4 * 60):
                self.four_hour_target = 1
            
                        
    def sample_all(self):
        '''
        Sample assessment time, if the patient requires diagnostics,
        if the diags are done in parallel to assessment and 
        '''

        # sample assessment time
        assess_time = self.assessment_dist.sample()

        # will the patient need diagnostics?
        n_diagnostics = self.n_diagnostics_dist.sample()

        if n_diagnostics > 0:
            # will the diagnostics be in parallel with assessment
            parallel = self.p_diag_parallel.sample()

            # diagnostic waiting time
            diag_wt = self.diag_wait_dist.sample()

            # diagnostic process time
            diag_pt = self.diag_dist.sample()
            
            # second assessment time
            assess_time_2 = self.assessment_dist_2.sample()

            return (assess_time, n_diagnostics, parallel, diag_wt, diag_pt,
                    assess_time_2)
        else:
            return assess_time, n_diagnostics, _, _, _, 0<jupyter_output><empty_output><jupyter_text>Script to run the model<jupyter_code># run length in minutes
RUN_LENGTH = 1440

# Turn off tracing
TRACE = False

# create simpy environment
env = simpy.Environment()

# base case scenario with default parameters
default_args = Scenario()

# create the model
model = MinorInjuryUnit(env, default_args)

# setup the process
env.process(model.arrivals_generator())

env.run(until=RUN_LENGTH)
print(f'end of run. simulation clock time = {env.now}')

####### Your code here ######################################
# Calculate and printout Performance metrics

# Hint: loop through the collection of patients in model and calculate the 
# mean of each of the performance measures.

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


# Example solution

# run length in minutes
RUN_LENGTH = 1440

# Turn off tracing
TRACE = False

# create simpy environment
env = simpy.Environment()

# base case scenario with default parameters
default_args = Scenario()

# create the model
model = MinorInjuryUnit(env, default_args)

# setup the process
env.process(model.arrivals_generator())

env.run(until=RUN_LENGTH)
print(f'end of run. simulation clock time = {env.now}')

####### Your code here ######################################
# Calculate and printout Performance metrics
# 1. mean time in system
mean_time_in_system = np.array([patient.time_in_system 
                                for patient in model.patients]).mean()


# 2. mean time to first treatment
mean_time_to_nurse = np.array([patient.time_to_nurse 
                               for patient in model.patients]).mean()

# 3. mean time to triage
mean_time_to_triage = np.array([patient.time_to_triage 
                               for patient in model.patients]).mean()

# 4. four hour target
four_hours = np.array([patient.four_hour_target 
                       for patient in 
                       model.patients]).sum() / len(model.patients)

print('\nSingle run results\n------------------')
print(f'Mean Minutes in System: {mean_time_in_system:.2f}')
print(f'Mean Minutes to Nurse: {mean_time_to_nurse:.2f}')
print(f'Mean Minutes to Triage: {mean_time_to_triage:.2f}')
print(f'Proportion discharged before 4 hrs: {four_hours:.2f}')

###################################################################<jupyter_output>end of run. simulation clock time = 1440

Single run results
------------------
Mean Minutes in System: 144.77
Mean Minutes to Nurse: 126.07
Mean Minutes to Triage: 118.30
Proportion discharged before 4 hrs: 0.61<jupyter_text>--- Exercise 2: Using the model for experimentationNow that you have your model you can use it for experimentation!  We are going to investigate the following scenarios* 1 extra triage bay* 1 extra cubicle* 5 extra cubicles* 2 extra triage bay + 1 extra cubicle Using functions to organise experimentation.So far we have run our model from a script.  In practice, it is a good idea to use functions to help you efficiently run scenarios.   `single_model_run()`The function `single_model_run` has been provided to help you do that.  Take a look at it first.  Note that it is very similar to your previous script.   It accepts a parameter `scenario` which contains all of the parameters you wish to use for the run.  When the model run is complete it returns the performance desired performance measures. The separates the creation of the scenarios from your model run code. `get_scenarios()`A sensible way to organise experimentation is to create all of scenarios in advance, store them in a python `list` or `dict` and then loop through the passing each to the `single_model_run` function.   The function `get_scenarios` has been coded for you, and contains an example of creating scenarios.  As part of the exercise you will need to complete the code and add in the extra scenarios.> Note in practice if you are working with big complex models you may want to store all of your scenarios and parameters in a CSV file.  You can then read them in.  Here we have a simple model so we are hard coding everything for clarity.**Task*** Read and check you understand the functions and script provided.* Complete the `get_scenarios()` function so that you have results for all of the scenarios.* Run all of the scenarios!<jupyter_code>def single_model_run(scenario, run_length):
    '''
    Perform a single run of the model and return the results
    
    Params:
    -------
    
    env = Simpy.Environment
    
    scenario - Scenario object
        The scenario/paramaters to run
        
    run_length - int
        The length of the simulation run.
        
        
    Returns:
        Tuple:
        (mean_time_in_system, mean_time_to_nurse, mean_time_to_triage,
         four_hours)
    '''
    env = simpy.Environment()
    # create the model and pass in scenario
    model = MinorInjuryUnit(env, scenario)

    # setup the process
    env.process(model.arrivals_generator())
    
    # run
    env.run(until=run_length)
    
    # Performance metrics

    # 1. mean time in system
    mean_time_in_system = np.array([patient.time_in_system 
                                    for patient in model.patients]).mean()


    # 2. mean time to first treatment
    mean_time_to_nurse = np.array([patient.time_to_nurse 
                                   for patient in model.patients]).mean()

    # 3. mean time to triage
    mean_time_to_triage = np.array([patient.time_to_triage 
                                   for patient in model.patients]).mean()


    # 4. four hour target
    four_hours = np.array([patient.four_hour_target 
                           for patient in 
                           model.patients]).sum() / len(model.patients)
        
    # return results.
    return (mean_time_in_system, mean_time_to_nurse, mean_time_to_triage,
            four_hours)
# setup scenarios
def get_scenarios():
    # sometime useful to store scenarios in a list or dict
    scenarios = {}
    # default scenario
    scenarios['base'] = Scenario()
    scenarios['base'].name = 'base'

    # this scenario has an extra triage bay
    scenario_1 = Scenario()
    scenario_1.triage_bays = N_BAYS + 1
    scenarios['extra_triage_capacity'] = scenario_1
    scenarios['extra_triage_capacity'].name = 'extra_triage_capacity'
    
    #################### Your code here ###############################
    
    # Scenarios to add:
    # 1. 1 extra minor cubicle
    # 2. 5 extra minor cubicles
    # 3. 1 extra triage bay
    # 4. 1 extra minor cubicle and 1 extra triage bay
    
    # your code here..
    # extra minor cubicle
    # scenario_2 = Scenario()
    # scenario_2.minor_cubicles = ...
    
    
    # your code here..
    # 5 extra minor cubicles

    
    # your code here
    # 1 extra cubicle and 1 extra triage bay

        
    return scenarios
# setup scenarios example solution
def get_scenarios():
    # sometime useful to store scenarios in a list or dict
    scenarios = {}
    # default scenario
    scenarios['base'] = Scenario()
    scenarios['base'].name = 'base'

    # this scenario has an extra triage bay
    scenario_1 = Scenario()
    scenario_1.triage_bays = N_BAYS + 1
    scenarios['extra_triage_capacity'] = scenario_1
    scenarios['extra_triage_capacity'].name = 'extra_triage_capacity'
    
    #################### Your code here ###############################
    
    # Scenarios to add:
    # 1. 1 extra minor cubicle
    # 2. 5 extra minor cubicles
    # 3. 1 extra triage bay
    # 4. 1 extra minor cubicle and 1 extra triage bay
    
    # extra minor cubicle
    scenario_2 = Scenario()
    scenario_2.minor_cubicles = N_CUBICLES + 1
    scenarios['extra_cubicle'] = scenario_2
    scenarios['extra_cubicle'].name = 'extra_cubicle'
    
    # 5 extra minor cubicles
    scenario_3 = Scenario()
    scenario_3.minor_cubicles = N_CUBICLES + 5
    scenarios['5_extra_cubicles'] = scenario_3
    scenarios['5_extra_cubicles'].name = '5_extra_cubicles'
    
    # 1 extra cubicle and 1 extra triage bay
    scenario_4 = Scenario()
    scenario_4.minor_cubicles = N_CUBICLES + 1
    scenario_4.triage_bays = N_BAYS + 1
    scenarios['combination'] = scenario_4
    scenarios['combination'].name = 'combination'
        
    return scenarios<jupyter_output><empty_output><jupyter_text>script to run the scenarios.<jupyter_code>RUN_LENGTH = 1440
TRACE = False

# load scenarios using your function
scenarios = get_scenarios()

# create simpy environment
env = simpy.Environment()

# loop through each scenario and store results in a dict
results = {}
for name, scenario in scenarios.items():
    print(f'simulating scenario: {name}')
    results[name] = single_model_run(scenario, RUN_LENGTH)
print('All simulations complete.\n\n')

# convert results to dataframe and printout
results = pd.DataFrame(results).T
results.columns = ['Time in System', 'Time to Nurse', 'Time to Triage', 
                   '4 Hr performance']
results<jupyter_output>simulating scenario: base
simulating scenario: extra_triage_capacity
simulating scenario: extra_cubicle
simulating scenario: 5_extra_cubicles
simulating scenario: combination
All simulations complete.
{'fraction_non_alphanumeric': 0.08594835572024086, 'fraction_numerical': 0.010103056970819825, 'mean_word_length': 2.2675936435868334, 'pattern_counts': {'":': 0, '<': 31, '<?xml version=': 0, '>': 56, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 26, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '8', 'dir': 'jupyter-structured-clean-dedup', 'id': '244844', 'license': 'MIT', 'n_tokens_mistral': 9642, 'n_tokens_neox': 9092, 'n_words': 3324, 'path': 'labs/simulation/lab2/simpy_lab2_SOLUTIONS.ipynb', 'repo_name': 'health-data-science-OR/stochastic_systems', 'size': '34544'}