The Experiment
class#
For a detailed walk through of building a basic
Experiment
class from scratch see the introduction to experiments notebook
The overall design of a class representing a experiment will depend on the simulation study, and to some extent user preference. Here we advocate two design principals:
Make use of default values for input parameters, either from constant variables, or read in from file.
Keep the code inside
Experiment
independent of the simulation software used.
Challenges with principal 2!
By independent we mean do not include any functions, classes or variables that are imported from a simulation package inside Experiment
. However, what is stored in Experiment
may vary depending on the simulation package that you are using.
1. Imports#
import numpy as np
2. Notebook level variables, constants, and default values#
Here we will create a set of constant/default values for our Experiment
class, but you could also consider reading these in from a file.
# default resources
N_OPERATORS = 13
# number of nurses available
N_NURSES = 9
# default mean inter-arrival time (exp)
MEAN_IAT = 60 / 100
## default service time parameters (triangular)
CALL_LOW = 5.0
CALL_MODE = 7.0
CALL_HIGH = 10.0
# nurse distribution parameters
NURSE_CALL_LOW = 10.0
NURSE_CALL_HIGH = 20.0
CHANCE_CALLBACK = 0.4
# Seeds for arrival and service time distributions (for repeatable single run)
ARRIVAL_SEED = 42
CALL_SEED = 101
# additional seeds for new activities
CALLBACK_SEED = 1966
NURSE_SEED = 2020
3. Distribution classes#
The model requires four distribution classes (Uniform
,Bernoulli
, Triangular
, and Exponential
) to encapsulate the random number generation, parameters and random seeds used in the sampling.
For an introduction to these concepts see the random sampling notebook
class Bernoulli():
'''
Convenience class for the Bernoulli distribution.
packages up distribution parameters, seed and random generator.
Use the Bernoulli distribution to sample success or failure.
'''
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.
Returns:
-------
float or np.ndarray (if size >=1)
'''
return self.rand.binomial(n=1, p=self.p, size=size)
class Uniform():
'''
Convenience class for the Uniform distribution.
packages up distribution parameters, seed and random generator.
'''
def __init__(self, low, high, random_seed=None):
'''
Constructor
Params:
------
low: float
lower range of the uniform
high: float
upper range of the uniform
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.low = low
self.high = high
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.
Returns:
-------
float or np.ndarray (if size >=1)
'''
return self.rand.uniform(low=self.low, high=self.high, 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):
'''
Constructor. Accepts and stores parameters of the triangular dist
and a random seed.
Params:
------
low: float
The smallest values that can be sampled
mode: float
The most frequently sample value
high: float
The highest value that can be sampled
random_seed: int, optional (default=None)
Used with params to create a series of repeatable samples.
'''
self.rand = np.random.default_rng(seed=random_seed)
self.low = low
self.high = high
self.mode = mode
def sample(self, size=None):
'''
Generate one or more samples from the triangular distribution
Params:
--------
size: int
the number of samples to return. If size=None then a single
sample is returned.
Returns:
-------
float or np.ndarray (if size >=1)
'''
return self.rand.triangular(self.low, self.mode, self.high, size=size)
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.
Returns:
-------
float or np.ndarray (if size >=1)
'''
return self.rand.exponential(self.mean, size=size)
As an example this is how you would create a Exponential
distribution:
arrival_dist = Exponential(mean=10.0, random_seed=42)
arrival_dist.sample()
24.042086039659946
3. Experiment class#
The design below uses python’s optional arguments to provide default parameterisation for models. To modify parameters a user simply needs to pass in the appropriate argument when creating the model.
Note that the class only needs to represent an simulation experiment it does not necessarily need to be called
Experiment
. For example, in the past I regularly usedScenario
.
Alternative design
Here we do not give the user the option to vary the type of sampling distribution used for activities in the model. However, these could be parameters for the model. This would reduce the number of arguments accepted by the constructor (pro!), but at the cost of requiring streamlit logic that is a little bit more complicated (con).
class Experiment:
'''
Parameter class for 111 simulation model
'''
def __init__(self, n_operators=N_OPERATORS, n_nurses=N_NURSES,
mean_iat=MEAN_IAT, call_low=CALL_LOW, call_mode=CALL_MODE,
call_high=CALL_HIGH, chance_callback=CHANCE_CALLBACK,
nurse_call_low=NURSE_CALL_LOW, nurse_call_high=NURSE_CALL_HIGH,
arrival_seed=None, call_seed=None,
callback_seed=None, nurse_seed=None):
'''
The init method sets up our defaults, resource counts, distributions
and result collection objects.
'''
# no. resources
self.n_operators = n_operators
self.n_nurses = n_nurses
# create distribution objects
self.arrival_dist = Exponential(mean_iat, random_seed=arrival_seed)
self.call_dist = Triangular(call_low, call_mode, call_high,
random_seed=call_seed)
self.callback_dist = Bernoulli(chance_callback,
random_seed=callback_seed)
self.nurse_dist = Uniform(nurse_call_low, nurse_call_high,
random_seed=nurse_seed)
# resources
# these variable are placeholders.
self.operators = None
self.nurses = None
# initialise results to zero
self.init_results_variables()
def init_results_variables(self):
'''
Initialise all of the experiment variables used in results
collection. This method is called at the start of each run
of the model
'''
# variable used to store results of experiment
self.results = {}
self.results['waiting_times'] = []
# total operator usage time for utilisation calculation.
self.results['total_call_duration'] = 0.0
# nurse sub process results collection
self.results['nurse_waiting_times'] = []
self.results['total_nurse_call_duration'] = 0.0
4. Example usage#
Usage of Experiment
is very simple. For example to create the default experiment we use the following code:
default_experiment = Experiment()
# check number of nurses
default_experiment.n_nurses
9
# sample from the arrival distribution
default_experiment.arrival_dist.sample()
0.14771155939684844
Remember that default_experiment
is an instance of the class Experiment
. It is an object. This means we can easily create multiple experiments each with different parameters.
default_experiment = Experiment()
extra_operator = Experiment(n_operators=14)
extra_operator_and_nurse = Experiment(n_operators=14, n_nurses=10)
print(default_experiment.n_operators)
print(default_experiment.n_nurses)
13
9
print(extra_operator.n_operators)
print(extra_operator.n_nurses)
14
9
print(extra_operator_and_nurse.n_operators)
print(extra_operator_and_nurse.n_nurses)
14
10