# Load required packages
library(R6) # nolint: undesirable_function_linter
Parameters from script
🔗 Reproducibility guidelines:
- Heather et al. 2025: Avoid hard-coded parameters.
1 Introduction
Discrete-event simulations (DES) require many parameters - like arrival rates, resource times, and probabilities - which often need to be changed for different scenarios and analyses. Managing these parameters well makes your simulations easier to update, track, and reuse.
This page focuses on the storage of parameters within your scripts. If you want to see how to store parameters in a separate file, see the parameters from file page.
On this page, we explain:
- The limitations with hardcoding parameters or global variables.
- Two recommended practices: grouping parameters into a dedicated object and passing that object explicitly to your model.
- Strategies for handling a large number of parameters as your model grows.
2 What not to do: hardcoding parameters
Hardcoding means writing parameter values directly into your code. For example:
def model():
# Hardcoded parameter values
= 5
interarrival_time = 20
consultation_time = 0.3
transfer_prob # ...rest of the model...
<- function() {
model # Hardcoded parameter values
<- 5
interarrival_time <- 20
consultation_time <- 0.3
transfer_prob # ...rest of the model...
}
This makes it very difficult to change the values. Modellers might choose to…
1. Edit parameters directly in the script.
This involves manually changing parameter values in the script each time you want to run a new scenario. Problems with this approach include:
- ❌ Not a reproducible analytical pipeline. Alot of manual intervention is required to re-run the model with different parameters.
- ❌ Error-prone. It would be easy to make mistakes or forget to update all relevant values.
- ❌ Parameters can get lost. If you lost your notes or forget to record what you used, you won’t know what values were used for past runs.
2. Duplicate scripts for each scenario.
This involves copying the entire script, changing parameters in each copy, and running them separately. Problems with this approach include:
- ❌ Code duplication. This means any changes - like bug fixes or improvements to the model - must be made to every copy. This is tedious and there is a risk that some copies are missed or updated incorrectly.
- ❌ Hard to keep track. With multiple script copies, it can become difficult to know which scripts correspond to which scenarios, and which parameters were used (as have to delve into the model code to identify them).
3 A slight improvement: global parameters
A better (but still limited) approach is to define all parameters at the top of your script.
This way, parameters are no longer hidden within the code. It becomes much easier to see which parameters are being used, and to change the values used.
# Parameters for base case
= 5
INTERARRIVAL_TIME = 20
CONSULTATION_TIME = 0.3
TRANSFER_PROB
def model():
# Use the global parameters
# ...
# Parameters for base case
<- 5
interarrival_time <- 20
consultation_time <- 0.3
transfer_prob
<- function() {
model # Use the global parameters
# ...
}
For scenarios, you would define the same global variables with alternative values:
# Scenario 1
= 6
INTERARRIVAL_TIME = 20
CONSULTATION_TIME = 0.3 TRANSFER_PROB
# Scenario 2
= 5
INTERARRIVAL_TIME = 20
CONSULTATION_TIME = 0.4 TRANSFER_PROB
# Scenario 1
<- 6
interarrival_time <- 20
consultation_time <- 0.3 transfer_prob
# Scenario 2
<- 5
interarrival_time <- 20
consultation_time <- 0.4 transfer_prob
The improvements are that parameters are:
- ✅ No longer hardcoded. Within the model, it refers to the variable name rather than a specific value,which means we are able to now list the values in one outside the model logic.
- ✅ Centralised. All parameters are in one place, making them easier to find and change.
However, there are still several disadvantages:
- ❌ Still inflexible. In order to re-run the model with different scenarios, you would still need to do the approaches above - editing code directly or duplicating scripts for each scenario.
- ❌ Not scalable. As the number of scenarios or parameters grows, managing all these global variables becomes messy.
4 Recommendation for managing parameters in DES
4.1 Two critical practices
To manage parameters effectively, you need to:
- Group parameters into a dedicated object.
- Pass these objects explicitly to your model.
Why?
- ✅ Clear parameter sets. Every scenario has its own object with all the parameters needed. This can be easily viewed, and comes in handy in logs to easily print a copy of all parameters used for a scenario.
- ✅ No global variables. By explicitly passing our parameters, we avoid accidental parameter reuse between scenarios (which is a possibility with global variables!).
- ✅ Fewer inputs. If all parameters are in one object, then we can just pass that as a single input to our model function/class, reducing the number of arguments we need to pass.
It’s important to use both of these practices.
If you only do option 1 (group parameters, but use as globals), parameters might accidentally be modified elsewhere, or one scenario’s parameters might affect another’s.
# BAD: Parameters are grouped but still global
= Parameters()
global_params
def simulate():
# Uses global_params.interarrival_time... 😬
...
# BAD: Parameters are grouped but still global
<- list()
global_params
<- function() {
simulate # Uses global_params$interarrival_time ... 😬
... }
If you only do option 2 (pass parameters, but don’t group them), you end up with messy, error-prone code that’s hard to maintain:
# BAD: Parameters are passed but disorganised
def simulate(interarrival_time, consultation_time, transfer_prob, ...):
# 10+ parameters? Hard to track!
...
# BAD: Parameters are passed but disorganised
<- function(interarrival_time, consultation_time, transfer_prob, ...) {
simulate # 10+ parameters? Hard to track!
... }
4.2 Three implementation options
There are three implementation options: dictionary, function or class.
Using a dictionary
# pylint: disable=missing-module-docstring
# Base case
= {
base_params "interarrival_time": 5,
"consultation_time": 20,
"transfer_prob": 0.3,
}print(base_params)
{'interarrival_time': 5, 'consultation_time': 20, 'transfer_prob': 0.3}
# Create a scenario by copying and tweaking only what's needed
= base_params.copy()
scenario1 "interarrival_time"] = 6
scenario1[print(scenario1)
{'interarrival_time': 6, 'consultation_time': 20, 'transfer_prob': 0.3}
Using a function
def create_params(
=5, consultation_time=20, transfer_prob=0.3
interarrival_time
):"""
Generate parameter dictionary.
Parameters
----------
interarrival_time : float
Time between arrivals (minutes).
consultation_time : float
Length of consultation (minutes).
transfer_prob : float
Transfer probability (0-1).
Returns
-------
Dictionary containing parameters.
"""
return {
"interarrival_time": interarrival_time,
"consultation_time": consultation_time,
"transfer_prob": transfer_prob
}
# Base case
= create_params()
base_params print(base_params)
{'interarrival_time': 5, 'consultation_time': 20, 'transfer_prob': 0.3}
# Create a scenario with altered inter-arrival time
= create_params(interarrival_time=6)
scenario1 print(scenario1)
{'interarrival_time': 6, 'consultation_time': 20, 'transfer_prob': 0.3}
Using a class
class Parameters: # pylint: disable=too-few-public-methods
"""
Parameter class.
"""
def __init__(
self, interarrival_time=5, consultation_time=20, transfer_prob=0.3
):"""
Initialise Parameters instance.
Parameters
----------
interarrival_time : float
Time between arrivals (minutes).
consultation_time : float
Length of consultation (minutes).
transfer_prob : float
Transfer probability (0-1).
"""
self.interarrival_time = interarrival_time
self.consultation_time = consultation_time
self.transfer_prob = transfer_prob
# Base case
= Parameters()
base_params print(base_params.__dict__)
{'interarrival_time': 5, 'consultation_time': 20, 'transfer_prob': 0.3}
# Create a scenario with altered inter-arrival time
= Parameters(interarrival_time=6)
scenario1 print(scenario1.__dict__)
{'interarrival_time': 6, 'consultation_time': 20, 'transfer_prob': 0.3}
There are three implementation options: list, function or R6 class.
Using a list
# Base case
<- list(
base_params interarrival_time = 5L,
consultation_time = 20L,
transfer_prob = 0.3
) base_params
$interarrival_time
[1] 5
$consultation_time
[1] 20
$transfer_prob
[1] 0.3
# Create a scenario by copying and tweaking only what's needed
<- base_params
scenario1 $interarrival_time <- 6L
scenario1 scenario1
$interarrival_time
[1] 6
$consultation_time
[1] 20
$transfer_prob
[1] 0.3
Using a function
#' Generate parameter list.
#'
#' @param interarrival_time Numeric. Time between arrivals (minutes).
#' @param consultation_time Numeric. Length of consultation (minutes).
#' @param transfer_prob Numeric. Transfer probability (0-1).
#'
#' @return A named list of parameters.
<- function(interarrival_time = 5L,
create_params consultation_time = 20L,
transfer_prob = 0.3) {
list(
interarrival_time = interarrival_time,
consultation_time = consultation_time,
transfer_prob = transfer_prob
)
}
# Base case
<- create_params()
base_params base_params
$interarrival_time
[1] 5
$consultation_time
[1] 20
$transfer_prob
[1] 0.3
# Create a scenario with altered inter-arrival time
<- create_params(interarrival_time = 6L)
scenario1 scenario1
$interarrival_time
[1] 6
$consultation_time
[1] 20
$transfer_prob
[1] 0.3
Using a R6 class (include a get_params
method to easily return a list of parameters).
#' @title Parameter class.
#'
#' @field interarrival_time Numeric. Time between arrivals (minutes).
#' @field consultation_time Numeric. Length of consultation (minutes).
#' @field transfer_prob Numeric. Transfer probability (0-1).
<- R6Class( # nolint: object_name_linter
Parameters classname = "Parameters",
public = list(
interarrival_time = NULL,
consultation_time = NULL,
transfer_prob = NULL,
initialize = function(
interarrival_time = 5L,
consultation_time = 20L,
transfer_prob = 0.3
) {$interarrival_time <- interarrival_time
self$consultation_time <- consultation_time
self$transfer_prob <- transfer_prob
self
},
#' @description
#' Returns parameters as a named list.
get_params = function() {
# Get all non-function fields
<- ls(self)
all_names <- vapply(
is_not_function
all_names,function(x) !is.function(self[[x]]),
FUN.VALUE = logical(1L)
)<- all_names[is_not_function]
param_names mget(param_names, envir = self)
}
)
)
# Base case
<- Parameters$new()
base_params $get_params() base_params
$consultation_time
[1] 20
$interarrival_time
[1] 5
$transfer_prob
[1] 0.3
# Create a scenario with altered inter-arrival time
<- Parameters$new(interarrival_time = 6L)
scenario1 $get_params() scenario1
$consultation_time
[1] 20
$interarrival_time
[1] 6
$transfer_prob
[1] 0.3
4.3 Which option is best?
The most robust approach is to use a function or class to manage your parameters.
- ✅ Functions and classes make it easy to create variations for different scenarios, since you simply change the inputs when you define a new scenario. For example, you can create a new scenario by only specifying the parameter you want to change, while all other parameters remain at their default values.
- ❌ With a dictionary, you have to make a copy of the base dictionary and then manually change individual values for each scenario. This can become cumbersome as the number of parameters or scenarios grows - and is just a bit more clunky!
- ❌ With a list, you have to make a copy of the base list and then manually change individual values for each scenario. This can become cumbersome as the number of parameters or scenarios grows - and is just a bit more clunky!
Your choice may be further informed by options for parameter validation, where classes can be superior as you can incorporate validation within the class, as discussed on the parameter validation page.
It’s worth noting though that, while R does support object-oriented programming (i.e. using classes), these are less commonly used in typical R code.
5 Handling a large number of parameters
You may need to manage many parameters - for example, if you have several patient types and/or units each with their own arrival times, resource times, and so on.
We have suggested a few strategies you could use…
5.1 Using a single function or class for all parameters
This can be convenient for smaller models, though can get unwieldly as the number of parameters grow, including potentially quite long parameter names! Although you could return a nested object to address this.
Function example (with nested dictionaries):
def create_params( # pylint: disable=function-redefined, too-many-arguments, too-many-positional-arguments
=5, adult_consultation=20, adult_transfer=0.3,
adult_interarrival=7, child_consultation=15, child_transfer=0.2,
child_interarrival=10, elderly_consultation=30, elderly_transfer=0.5
elderly_interarrival
):"""
Generate parameter dictionary.
Parameters
----------
adult_interarrival : float
Time between adult patient arrivals (minutes).
adult_consultation : float
Length of adult patient consultation (minutes).
adult_transfer : float
Adult patient transfer probability (0-1).
child_interarrival : float
Time between child patient arrivals (minutes).
child_consultation : float
Length of child patient consultation (minutes).
child_transfer : float
Child patient transfer probability (0-1).
elderly_interarrival : float
Time between elderly patient arrivals (minutes).
elderly_consultation : float
Length of elderly patient consultation (minutes).
elderly_transfer : float
Elderly patient transfer probability (0-1).
Returns
-------
Dictionary with three keys ("adult", "child", "elderly"), each containing a
dictionary of parameters.
"""
return {
"adult": {
"interarrival_time": adult_interarrival,
"consultation_time": adult_consultation,
"transfer_prob": adult_transfer
},"child": {
"interarrival_time": child_interarrival,
"consultation_time": child_consultation,
"transfer_prob": child_transfer
},"elderly": {
"interarrival_time": elderly_interarrival,
"consultation_time": elderly_consultation,
"transfer_prob": elderly_transfer
}
}
# Example usage
= create_params()
base_params print(base_params)
{'adult': {'interarrival_time': 5, 'consultation_time': 20, 'transfer_prob': 0.3}, 'child': {'interarrival_time': 7, 'consultation_time': 15, 'transfer_prob': 0.2}, 'elderly': {'interarrival_time': 10, 'consultation_time': 30, 'transfer_prob': 0.5}}
Class example:
class Parameters: # pylint: disable=function-redefined, too-many-instance-attributes, too-many-arguments, too-many-positional-arguments, too-few-public-methods
"""
Parameter class.
"""
def __init__(
self,
=5, adult_consultation=20, adult_transfer=0.3,
adult_interarrival=7, child_consultation=15, child_transfer=0.2,
child_interarrival=10, elderly_consultation=30, elderly_transfer=0.5
elderly_interarrival
):"""
Initialise Parameters instance.
Parameters
----------
adult_interarrival : float
Time between adult patient arrivals (minutes).
adult_consultation : float
Length of adult patient consultation (minutes).
adult_transfer : float
Adult patient transfer probability (0-1).
child_interarrival : float
Time between child patient arrivals (minutes).
child_consultation : float
Length of child patient consultation (minutes).
child_transfer : float
Child patient transfer probability (0-1).
elderly_interarrival : float
Time between elderly patient arrivals (minutes).
elderly_consultation : float
Length of elderly patient consultation (minutes).
elderly_transfer : float
Elderly patient transfer probability (0-1).
"""
# Adult parameters
self.adult_interarrival = adult_interarrival
self.adult_consultation = adult_consultation
self.adult_transfer = adult_transfer
# Child parameters
self.child_interarrival = child_interarrival
self.child_consultation = child_consultation
self.child_transfer = child_transfer
# Elderly parameters
self.elderly_interarrival = elderly_interarrival
self.elderly_consultation = elderly_consultation
self.elderly_transfer = elderly_transfer
# Example usage
= Parameters()
base_params print(base_params.__dict__)
{'adult_interarrival': 5, 'adult_consultation': 20, 'adult_transfer': 0.3, 'child_interarrival': 7, 'child_consultation': 15, 'child_transfer': 0.2, 'elderly_interarrival': 10, 'elderly_consultation': 30, 'elderly_transfer': 0.5}
Function example (with nested lists):
#' Generate parameter list.
#'
#' @param adult_interarrival Numeric. Time between adult patient arrivals
#' (minutes).
#' @param adult_consultation Numeric. Length of adult patient consultation
#' (minutes).
#' @param adult_transfer Numeric. Adult patient transfer probability (0-1).
#' @param child_interarrival Numeric. Time between child patient arrivals
#' (minutes).
#' @param child_consultation Numeric. Length of child patient consultation
#' (minutes).
#' @param child_transfer Numeric. Child patient transfer probability (0-1).
#' @param elderly_interarrival Numeric. Time between elderly patient arrivals
#' (minutes).
#' @param elderly_consultation Numeric. Length of elderly patient consultation
#' (minutes).
#' @param elderly_transfer Numeric. Elderly patient transfer probability (0-1).
#'
#' @return A named list with three keys ("adult", "child", "elderly"), each
#' containing a list of parameters.
<- function(
create_params adult_interarrival = 5L, adult_consultation = 20L, adult_transfer = 0.3,
child_interarrival = 7L, child_consultation = 15L, child_transfer = 0.2,
elderly_interarrival = 10L, elderly_consultation = 30L,
elderly_transfer = 0.5
) {list(
adult = list(
interarrival_time = adult_interarrival,
consultation_time = adult_consultation,
transfer_prob = adult_transfer
),child = list(
interarrival_time = child_interarrival,
consultation_time = child_consultation,
transfer_prob = child_transfer
),elderly = list(
interarrival_time = elderly_interarrival,
consultation_time = elderly_consultation,
transfer_prob = elderly_transfer
)
)
}
# Example usage
<- create_params()
base_params base_params
$adult
$adult$interarrival_time
[1] 5
$adult$consultation_time
[1] 20
$adult$transfer_prob
[1] 0.3
$child
$child$interarrival_time
[1] 7
$child$consultation_time
[1] 15
$child$transfer_prob
[1] 0.2
$elderly
$elderly$interarrival_time
[1] 10
$elderly$consultation_time
[1] 30
$elderly$transfer_prob
[1] 0.5
R6 class example:
#' @title Parameter class.
#'
#' @field adult_interarrival Numeric. Time between adult patient arrivals
#' (minutes).
#' @field adult_consultation Numeric. Length of adult patient consultation
#' (minutes).
#' @field adult_transfer Numeric. Adult patient transfer probability (0-1).
#' @field child_interarrival Numeric. Time between child patient arrivals
#' (minutes).
#' @field child_consultation Numeric. Length of child patient consultation
#' (minutes).
#' @field child_transfer Numeric. Child patient transfer probability (0-1).
#' @field elderly_interarrival Numeric. Time between elderly patient arrivals
#' (minutes).
#' @field elderly_consultation Numeric. Length of elderly patient consultation
#' (minutes).
#' @field elderly_transfer Numeric. Elderly patient transfer probability (0-1).
<- R6Class( # nolint: object_name_linter
Parameters classname = "Parameters",
public = list(
# Adult parameters
adult_interarrival = NULL,
adult_consultation = NULL,
adult_transfer = NULL,
# Child parameters
child_interarrival = NULL,
child_consultation = NULL,
child_transfer = NULL,
# Elderly parameters
elderly_interarrival = NULL,
elderly_consultation = NULL,
elderly_transfer = NULL,
#' @description
#' Initialise Parameters instance.
initialize = function(
adult_interarrival = 5L, adult_consultation = 20L,
adult_transfer = 0.3, child_interarrival = 7L,
child_consultation = 15L, child_transfer = 0.2,
elderly_interarrival = 10L, elderly_consultation = 30L,
elderly_transfer = 0.5
) {# Adult parameters
$adult_interarrival <- adult_interarrival
self$adult_consultation <- adult_consultation
self$adult_transfer <- adult_transfer
self
# Child parameters
$child_interarrival <- child_interarrival
self$child_consultation <- child_consultation
self$child_transfer <- child_transfer
self
# Elderly parameters
$elderly_interarrival <- elderly_interarrival
self$elderly_consultation <- elderly_consultation
self$elderly_transfer <- elderly_transfer
self
},
#' @description
#' Returns parameters as a named list.
#'
#' @return A named list containing all parameter values.
get_params = function() {
# Get all non-function fields
<- ls(self)
all_names <- vapply(
is_not_function
all_names,function(x) !is.function(self[[x]]),
FUN.VALUE = logical(1L)
)<- all_names[is_not_function]
param_names mget(param_names, envir = self)
}
)
)
# Example usage
<- Parameters$new()
base_params $get_params() base_params
$adult_consultation
[1] 20
$adult_interarrival
[1] 5
$adult_transfer
[1] 0.3
$child_consultation
[1] 15
$child_interarrival
[1] 7
$child_transfer
[1] 0.2
$elderly_consultation
[1] 30
$elderly_interarrival
[1] 10
$elderly_transfer
[1] 0.5
5.2 Using multiple functions or classes
Alternatively, parameters can be split into logical groups (e.g., patient type, parameter type), with each group having its own function or class. These groups are then combined into a single parameter set.
For simulations with a large number of parameters, this approach simplifies input management. It also enables validation to be performed for each individual function/class.
Function example:
def create_arrivals(adult=5, child=7, elderly=10):
"""
Generate dictionary of inter-arrival times (minutes).
Parameters
----------
adult : float
Time between arrivals for adults.
child : float
Time between arrivals for children.
elderly : float
Time between arrivals for elderly people.
Returns
-------
Dictionary of inter-arrival times for each patient type.
"""
return {
"adult": adult,
"child": child,
"elderly": elderly
}
def create_consultations(adult=20, child=15, elderly=30):
"""
Generate dictionary of consultation times (minutes).
Parameters
----------
adult : float
Consultation duration for adults.
child : float
Consultation duration for children.
elderly : float
Consultation duration for elderly people.
Returns
-------
Dictionary of consultation times for each patient type.
"""
return {
"adult": adult,
"child": child,
"elderly": elderly
}
def create_transfers(adult=0.3, child=0.2, elderly=0.5):
"""
Generate dictionary of transfer probabilities.
Parameters
----------
adult : float
Transfer probability for adults.
child : float
Transfer probability for children.
elderly : float
Transfer probability for elderly people.
Returns
-------
Dictionary of transfer probabilities for each patient type.
"""
return {
"adult": adult,
"child": child,
"elderly": elderly
}
def create_parameters(
=None,
arrivals=None,
consultations=None
transfers
):"""
Generate complete parameter dictionary for simulation.
Uses None as default argument, as setting e.g. arrivals=create_arrivals()
is dangerous, since it creates a single dictionary shared by subsequent
calls.
Parameters
----------
arrivals : dict
Dictionary of inter-arrival times (minutes) for each patient type.
consultations : dict
Dictionary of consultation durations (minutes) for each patient type.
transfers : dict
Dictionary of transfer probabilities (0-1) for each patient type.
Returns
-------
Nested dictionary with three keys ("arrivals", "consultations",
"transfers"), each containing a dictionary of parameters.
"""
# Create the individual dictionaries if none provided
= create_arrivals() if arrivals is None else arrivals
arrivals = (
consultations if consultations is None else consultations)
create_consultations() = create_transfers() if transfers is None else transfers
transfers # Create dictionary
return {
"arrivals": arrivals,
"consultations": consultations,
"transfers": transfers
}
# Example usage
= create_parameters()
base_params print(base_params)
{'arrivals': {'adult': 5, 'child': 7, 'elderly': 10}, 'consultations': {'adult': 20, 'child': 15, 'elderly': 30}, 'transfers': {'adult': 0.3, 'child': 0.2, 'elderly': 0.5}}
Class example:
class Arrivals: # pylint: disable=too-few-public-methods
"""
Inter-arrival times (minutes).
"""
def __init__(self, adult=5, child=7, elderly=10):
"""
Initialise Arrivals instance.
Parameters
----------
adult : float
Time between arrivals for adults.
child : float
Time between arrivals for children.
elderly : float
Time between arrivals for elderly people.
"""
self.adult = adult
self.child = child
self.elderly = elderly
class Consultations: # pylint: disable=too-few-public-methods
"""
Consultation times (minutes).
"""
def __init__(self, adult=20, child=15, elderly=30):
"""
Initialise Consultations instance.
Parameters
----------
adult : float
Consultation duration for adults.
child : float
Consultation duration for children.
elderly : float
Consultation duration for elderly people.
"""
self.adult = adult
self.child = child
self.elderly = elderly
class Transfers: # pylint: disable=too-few-public-methods
"""
Transfer probabilities (0-1).
"""
def __init__(self, adult=0.3, child=0.2, elderly=0.5):
"""
Initialise Transfers instance.
Parameters
----------
adult : float
Transfer probability for adults (0-1).
child : float
Transfer probability for children (0-1).
elderly : float
Transfer probability for elderly people (0-1).
"""
self.adult = adult
self.child = child
self.elderly = elderly
class Parameters(): # pylint: disable=function-redefined, too-few-public-methods
"""
Complete parameter class.
"""
def __init__(
self,
=Arrivals(),
arrivals=Consultations(),
consultations=Transfers()
transfers
):"""
Initialise Parameters instance.
Parameters
----------
arrivals : dict
Class with inter-arrival times (minutes) for each patient type.
consultations : dict
Class with consultation durations (minutes) for each patient type.
transfers : dict
Class with transfer probabilities (0-1) for each patient type.
"""
self.arrivals = arrivals
self.consultations = consultations
self.transfers = transfers
# Example usage
= Parameters()
base_params print(base_params.arrivals.__dict__)
{'adult': 5, 'child': 7, 'elderly': 10}
print(base_params.consultations.__dict__)
{'adult': 20, 'child': 15, 'elderly': 30}
print(base_params.transfers.__dict__)
{'adult': 0.3, 'child': 0.2, 'elderly': 0.5}
Function example:
#' Generate list of inter-arrival times (minutes).
#'
#' @param adult Numeric. Time between adult patient arrivals.
#' @param child Numeric. Time between child patient arrivals.
#' @param elderly Numeric. Time between elderly patient arrivals.
#'
#' @return A named list of inter-arrival times for each patient type.
<- function(adult = 5L, child = 7L, elderly = 10L) {
create_arrivals list(
adult = adult,
child = child,
elderly = elderly
)
}
#' Generate list of consultation times (minutes).
#'
#' @param adult Numeric. Consultation duration for adults.
#' @param child Numeric. Consultation duration for children.
#' @param elderly Numeric. Consultation duration for elderly people.
#'
#' @return A named list of consultation times for each patient type.
<- function(adult = 20L, child = 15L, elderly = 30L) {
create_consultations list(
adult = adult,
child = child,
elderly = elderly
)
}
#' Generate list of transfer probabilities (0-1).
#'
#' @param adult Numeric. Transfer probability for adults.
#' @param child Numeric. Transfer probability for children.
#' @param elderly Numeric. Transfer probability for elderly people.
#'
#' @return A named list of transfer probabilities for each patient type.
<- function(adult = 0.3, child = 0.2, elderly = 0.5) {
create_transfers list(
adult = adult,
child = child,
elderly = elderly
)
}
#' Generate complete parameter list for simulation.
#'
#' @param arrivals List. Named list of inter-arrival times (minutes) for each
#' patient type.
#' @param consultations List. Named list of consultation durations (minutes)
#' for each patient type.
#' @param transfers List. Named list of transfer probabilities (0-1) for each
#' patient type.
#'
#' @return A named list with three keys ("arrivals", "consultations",
#' "transfers"), each containing a list of parameters for each patient type.
<- function(
create_parameters arrivals = create_arrivals(),
consultations = create_consultations(),
transfers = create_transfers()
) {list(
arrivals = arrivals,
consultations = consultations,
transfers = transfers
)
}
# Example usage
<- create_parameters()
base_params base_params
$arrivals
$arrivals$adult
[1] 5
$arrivals$child
[1] 7
$arrivals$elderly
[1] 10
$consultations
$consultations$adult
[1] 20
$consultations$child
[1] 15
$consultations$elderly
[1] 30
$transfers
$transfers$adult
[1] 0.3
$transfers$child
[1] 0.2
$transfers$elderly
[1] 0.5
R6 class example (with modified get_params()
method to extract fields from each component class):
#' @title Arrivals class.
#' @description Inter-arrival times (minutes).
#'
#' @field adult Numeric. Time between adult patient arrivals (minutes).
#' @field child Numeric. Time between child patient arrivals (minutes).
#' @field elderly Numeric. Time between elderly patient arrivals (minutes).
<- R6Class( # nolint: object_name_linter
Arrivals classname = "Arrivals",
public = list(
adult = NULL,
child = NULL,
elderly = NULL,
#' @description
#' Initialise Arrivals instance.
initialize = function(adult = 5L, child = 7L, elderly = 10L) {
$adult <- adult
self$child <- child
self$elderly <- elderly
self
}
)
)
#' @title Consultations class.
#' @description Consultation times (minutes).
#'
#' @field adult Numeric. Length of adult patient consultation (minutes).
#' @field child Numeric. Length of child patient consultation (minutes).
#' @field elderly Numeric. Length of elderly patient consultation (minutes).
<- R6Class( # nolint: object_name_linter
Consultations classname = "Consultations",
public = list(
adult = NULL,
child = NULL,
elderly = NULL,
#' @description
#' Initialise Consultations instance.
initialize = function(adult = 20L, child = 15L, elderly = 30L) {
$adult <- adult
self$child <- child
self$elderly <- elderly
self
}
)
)
#' @title Transfers class.
#' @description Transfer probabilities (0-1).
#'
#' @field adult Numeric. Adult patient transfer probability (0-1).
#' @field child Numeric. Child patient transfer probability (0-1).
#' @field elderly Numeric. Elderly patient transfer probability (0-1).
<- R6Class( # nolint: object_name_linter
Transfers classname = "Transfers",
public = list(
adult = NULL,
child = NULL,
elderly = NULL,
#' @description
#' Initialise Transfers instance.
initialize = function(adult = 0.3, child = 0.2, elderly = 0.5) {
$adult <- adult
self$child <- child
self$elderly <- elderly
self
}
)
)
#' @title Parameters class.
#' @description Complete parameter class for simulation.
#'
#' @field arrivals Arrivals. Instance with inter-arrival times for each patient
#' type.
#' @field consultations Consultations. Instance with consultation durations for
#' each patient type.
#' @field transfers Transfers. Instance with transfer probabilities for each
#' patient type.
<- R6Class( # nolint: object_name_linter
Parameters classname = "Parameters",
public = list(
arrivals = NULL,
consultations = NULL,
transfers = NULL,
#' @description
#' Initialise Parameters instance.
initialize = function(
arrivals = Arrivals$new(),
consultations = Consultations$new(),
transfers = Transfers$new()
) {$arrivals <- arrivals
self$consultations <- consultations
self$transfers <- transfers
self
},
#' @description
#' Returns parameters as a named list.
#'
#' @return A named list containing all parameter values organised by type.
get_params = function() {
# Get all non-function fields from self
<- ls(self)
all_names <- vapply(
is_not_function
all_names,function(x) !is.function(self[[x]]),
FUN.VALUE = logical(1L)
)<- all_names[is_not_function]
instance_names
# Get the instances
<- mget(instance_names, envir = self)
instances
# Apply parameter extraction to each instance using lapply
lapply(instances, function(instance) {
if (inherits(instance, "R6")) {
# Extract non-function fields from R6 instance
<- ls(instance)
all_fields <- vapply(
is_not_function
all_fields,function(x) !is.function(instance[[x]]),
FUN.VALUE = logical(1L)
)<- all_fields[is_not_function]
field_names mget(field_names, envir = instance)
else {
} # Return non-R6 objects as-is
instance
}
})
}
)
)
# Example usage
<- Parameters$new()
base_params $get_params() base_params
$arrivals
$arrivals$adult
[1] 5
$arrivals$child
[1] 7
$arrivals$elderly
[1] 10
$consultations
$consultations$adult
[1] 20
$consultations$child
[1] 15
$consultations$elderly
[1] 30
$transfers
$transfers$adult
[1] 0.3
$transfers$child
[1] 0.2
$transfers$elderly
[1] 0.5
6 Examples
This section contains full code examples for our example conceptual models.
Show/Hide example 1: 🩺 Nurse visit simulation
This example is from simulation/param.py
in pydesrap_mms.
"""
Param.
"""
from .simlogger import SimLogger
# pylint: disable=too-many-instance-attributes,too-few-public-methods
class Param:
"""
Default parameters for simulation.
Attributes
----------
_initialising : bool
Whether the object is currently initialising.
patient_inter : float
Mean inter-arrival time between patients in minutes.
mean_n_consult_time : float
Mean nurse consultation time in minutes.
number_of_nurses : float
Number of available nurses.
warm_up_period : int
Duration of the warm-up period in minutes.
data_collection_period : int
Duration of data collection period in minutes.
number_of_runs : int
The number of runs (i.e. replications).
audit_interval : int
How frequently to audit resource utilisation, in minutes.
scenario_name : int|float|str
Label for the scenario.
cores : int
Number of CPU cores to use for parallel execution. For all
available cores, set to -1. For sequential execution, set to 1.
logger : logging.Logger
The logging instance used for logging messages.
"""
# pylint: disable=too-many-arguments,too-many-positional-arguments
def __init__(
self,
=4,
patient_inter=10,
mean_n_consult_time=5,
number_of_nurses=1440*27, # 27 days
warm_up_period=1440*30, # 30 days
data_collection_period=31,
number_of_runs=120, # Every 2 hours
audit_interval=0,
scenario_name=-1,
cores=SimLogger(log_to_console=False, log_to_file=False)
logger
):"""
Initialise instance of parameters class.
Parameters
----------
patient_inter : float, optional
Mean inter-arrival time between patients in minutes.
mean_n_consult_time : float, optional
Mean nurse consultation time in minutes.
number_of_nurses : float, optional
Number of available nurses.
warm_up_period : int, optional
Duration of the warm-up period in minutes.
data_collection_period : int, optional
Duration of data collection period in minutes.
number_of_runs : int, optional
The number of runs (i.e. replications).
audit_interval : int, optional
How frequently to audit resource utilisation, in minutes.
scenario_name : int|float|str, optional
Label for the scenario.
cores : int, optional
Number of CPU cores to use for parallel execution.
logger : logging.Logger, optional
The logging instance used for logging messages.
"""
# Disable restriction on attribute modification during initialisation
object.__setattr__(self, "_initialising", True)
self.patient_inter = patient_inter
self.mean_n_consult_time = mean_n_consult_time
self.number_of_nurses = number_of_nurses
self.warm_up_period = warm_up_period
self.data_collection_period = data_collection_period
self.number_of_runs = number_of_runs
self.audit_interval = audit_interval
self.scenario_name = scenario_name
self.cores = cores
self.logger = logger
# Re-enable attribute checks after initialisation
object.__setattr__(self, "_initialising", False)
def __setattr__(self, name, value):
"""
Prevent addition of new attributes.
Parameters
----------
name : str
The name of the attribute to set.
value : Any
The value to assign to the attribute.
Raises
------
AttributeError
If `name` is not an existing attribute and an attempt is made
to add it to the instance.
"""
# Skip the check if the object is still initialising
# pylint: disable=maybe-no-member
if hasattr(self, "_initialising") and self._initialising:
super().__setattr__(name, value)
else:
# Check if attribute of that name is already present
if name in self.__dict__:
super().__setattr__(name, value)
else:
raise AttributeError(
f"Cannot add new attribute '{name}' - only possible to "
f"modify existing attributes: {self.__dict__.keys()}"
)
This example is from R/parameters.R
in rdesrap_mms.
If you’re building the nurse M/M/s model: Create a file called parameters.R
within your R
folder, and copy this code into that file.
#' Create a named list of default model parameters (which can be altered).
#'
#' When input to model(), valid_inputs() will fetch the inputs to this
#' function and compare them against the provided list, to ensure no
#' new keys have been add to the list.
#'
#' @param patient_inter Mean inter-arrival time between patients in minutes.
#' @param mean_n_consult_time Mean nurse consultation time in minutes.
#' @param number_of_nurses Number of available nurses (int).
#' @param warm_up_period Duration of warm-up period in minutes.
#' @param data_collection_period Duration of data collection period in
#' minutes.
#' @param number_of_runs Number of simulation runs (int).
#' @param scenario_name Label for the scenario (int|float|string).
#' @param cores Number of cores to use for parallel execution (int).
#' @param log_to_console Whether to print activity log to console.
#' @param log_to_file Whether to save activity log to file.
#' @param file_path Path to save log to file.
#'
#' @return A named list containing the parameters for the model.
#' @export
<- function(
parameters patient_inter = 4L,
mean_n_consult_time = 10L,
number_of_nurses = 5L,
warm_up_period = 0L,
data_collection_period = 80L,
number_of_runs = 100L,
scenario_name = NULL,
cores = 1L,
log_to_console = FALSE,
log_to_file = FALSE,
file_path = NULL
) {return(as.list(environment()))
}
Show/Hide example 2: 🧠 Stroke pathway simulation
This example is from simulation/parameters.py
in pydesrap_stroke.
"""
Stroke pathway simulation parameters.
It includes arrival rates, length of stay distributions, and routing
probabilities between different care settings.
"""
import time
from simulation.logging import SimLogger
class RestrictAttributesMeta(type):
"""
Metaclass for attribute restriction.
A metaclass modifies class construction. It intercepts instance creation
via __call__, adding the _initialised flag after __init__ completes. This
is later used by RestrictAttributes to enforce attribute restrictions.
"""
def __call__(cls, *args, **kwargs):
# Create instance using the standard method
= super().__call__(*args, **kwargs)
instance # Set the "_initialised" flag to True, marking end of initialisation
"_initialised"] = True
instance.__dict__[return instance
class RestrictAttributes(metaclass=RestrictAttributesMeta):
"""
Base class that prevents the addition of new attributes after
initialisation.
This class uses RestrictAttributesMeta as its metaclass to implement
attribute restriction. It allows for safe initialisation of attributes
during the __init__ method, but prevents the addition of new attributes
afterwards.
The restriction is enforced through the custom __setattr__ method, which
checks if the attribute already exists before allowing assignment.
"""
def __setattr__(self, name, value):
"""
Prevent addition of new attributes.
Parameters
----------
name: str
The name of the attribute to set.
value: any
The value to assign to the attribute.
Raises
------
AttributeError
If `name` is not an existing attribute and an attempt is made
to add it to the class instance.
"""
# Check if the instance is initialised and the attribute doesn"t exist
if hasattr(self, "_initialised") and not hasattr(self, name):
# Get a list of existing attributes for the error message
= ", ".join(self.__dict__.keys())
existing raise AttributeError(
f"Cannot add new attribute '{name}' - only possible to " +
f"modify existing attributes: {existing}."
)# If checks pass, set the attribute using the standard method
object.__setattr__(self, name, value)
class ASUArrivals(RestrictAttributes):
"""
Arrival rates for the acute stroke unit (ASU) by patient type.
These are the average time intervals (in days) between new admissions.
For example, a value of 1.2 means a new admission every 1.2 days.
"""
def __init__(self, stroke=1.2, tia=9.3, neuro=3.6, other=3.2):
"""
Parameters
----------
stroke: float
Stroke patient.
tia: float
Transient ischaemic attack (TIA) patient.
neuro: float
Complex neurological patient.
other: float
Other patient types (including medical outliers).
"""
self.stroke = stroke
self.tia = tia
self.neuro = neuro
self.other = other
class RehabArrivals(RestrictAttributes):
"""
Arrival rates for the rehabiliation unit by patient type.
These are the average time intervals (in days) between new admissions.
For example, a value of 21.8 means a new admission every 21.8 days.
"""
def __init__(self, stroke=21.8, neuro=31.7, other=28.6):
"""
Parameters
----------
stroke: float
Stroke patient.
neuro: float
Complex neurological patient.
other: float
Other patient types.
"""
self.stroke = stroke
self.neuro = neuro
self.other = other
class ASULOS(RestrictAttributes):
"""
Mean and standard deviation (SD) of length of stay (LOS) in days in the
acute stroke unit (ASU) by patient type.
Attributes
----------
stroke_noesd: dict
Mean and SD of LOS for stroke patients without early support discharge.
stroke_esd: dict
Mean and SD of LOS for stroke patients with early support discharge.
tia: dict
Mean and SD of LOS for transient ischemic attack (TIA) patients.
neuro: dict
Mean and SD of LOS for complex neurological patients.
other: dict
Mean and SD of LOS for other patients.
"""
def __init__(
self,
=7.4, stroke_no_esd_sd=8.61,
stroke_no_esd_mean=4.6, stroke_esd_sd=4.8,
stroke_esd_mean=7.0, stroke_mortality_sd=8.7,
stroke_mortality_mean=1.8, tia_sd=2.3,
tia_mean=4.0, neuro_sd=5.0,
neuro_mean=3.8, other_sd=5.2
other_mean
):"""
Parameters
----------
stroke_no_esd_mean: float
Mean LOS for stroke patients without early support discharge (ESD)
services.
stroke_no_esd_sd: float
SD of LOS for stroke patients without ESD.
stroke_esd_mean: float
Mean LOS for stroke patients with ESD.
stroke_esd_sd: float
SD of LOS for stroke patients with ESD.
stroke_mortality_mean: float
Mean LOS for stroke patients who pass away.
stroke_mortality_sd: float
SD of LOS for stroke patients who pass away.
tia_mean: float
Mean LOS for TIA patients.
tia_sd: float
SD of LOS for TIA patients.
neuro_mean: float
Mean LOS for complex neurological patients.
neuro_sd: float
SD of LOS for complex neurological patients.
other_mean: float
Mean LOS for other patient types.
other_sd: float
SD of LOS for other patient types.
"""
self.stroke_noesd = {
"mean": stroke_no_esd_mean,
"sd": stroke_no_esd_sd
}self.stroke_esd = {
"mean": stroke_esd_mean,
"sd": stroke_esd_sd
}self.stroke_mortality = {
"mean": stroke_mortality_mean,
"sd": stroke_mortality_sd
}self.tia = {
"mean": tia_mean,
"sd": tia_sd
}self.neuro = {
"mean": neuro_mean,
"sd": neuro_sd
}self.other = {
"mean": other_mean,
"sd": other_sd
}
class RehabLOS(RestrictAttributes):
"""
Mean and standard deviation (SD) of length of stay (LOS) in days in the
rehabilitation unit by patient type.
Attributes
----------
stroke_noesd: dict
Mean and SD of LOS for stroke patients without early support discharge.
stroke_esd: dict
Mean and SD of LOS for stroke patients with early support discharge.
tia: dict
Mean and SD of LOS for transient ischemic attack (TIA) patients.
neuro: dict
Mean and SD of LOS for complex neurological patients.
other: dict
Mean and SD of LOS for other patients.
"""
def __init__(
self,
=28.4, stroke_no_esd_sd=27.2,
stroke_no_esd_mean=30.3, stroke_esd_sd=23.1,
stroke_esd_mean=18.7, tia_sd=23.5,
tia_mean=27.6, neuro_sd=28.4,
neuro_mean=16.1, other_sd=14.1
other_mean
):"""
Parameters
----------
stroke_no_esd_mean: float
Mean LOS for stroke patients without early support discharge (ESD)
services.
stroke_no_esd_sd: float
SD of LOS for stroke patients without ESD.
stroke_esd_mean: float
Mean LOS for stroke patients with ESD.
stroke_esd_sd: float
SD of LOS for stroke patients with ESD.
tia_mean: float
Mean LOS for TIA patients.
tia_sd: float
SD of LOS for TIA patients.
neuro_mean: float
Mean LOS for complex neurological patients.
neuro_sd: float
SD of LOS for complex neurological patients.
other_mean: float
Mean LOS for other patient types.
other_sd: float
SD of LOS for other patient types.
"""
self.stroke_noesd = {
"mean": stroke_no_esd_mean,
"sd": stroke_no_esd_sd
}self.stroke_esd = {
"mean": stroke_esd_mean,
"sd": stroke_esd_sd
}self.tia = {
"mean": tia_mean,
"sd": tia_sd
}self.neuro = {
"mean": neuro_mean,
"sd": neuro_sd
}self.other = {
"mean": other_mean,
"sd": other_sd
}
class ASURouting(RestrictAttributes):
"""
Probabilities of each patient type being transferred from the acute
stroke unit (ASU) to other destinations.
Attributes
----------
stroke: dict
Routing probabilities for stroke patients.
tia: dict
Routing probabilities for transient ischemic attack (TIA) patients.
neuro: dict
Routing probabilities for complex neurological patients.
other: dict
Routing probabilities for other patients.
"""
def __init__(
self,
# Stroke patients
=0.24, stroke_esd=0.13, stroke_other=0.63,
stroke_rehab# TIA patients
=0.01, tia_esd=0.01, tia_other=0.98,
tia_rehab# Complex neurological patients
=0.11, neuro_esd=0.05, neuro_other=0.84,
neuro_rehab# Other patients
=0.05, other_esd=0.10, other_other=0.85
other_rehab
):"""
Parameters
----------
stroke_rehab: float
Stroke patient to rehabilitation unit.
stroke_esd: float
Stroke patient to early support discharge (ESD) services.
stroke_other: float
Stroke patient to other destinations (e.g., own home, care
home, mortality).
tia_rehab: float
TIA patient to rehabilitation unit.
tia_esd: float
TIA patient to ESD.
tia_other: float
TIA patient to other destinations.
neuro_rehab: float
Complex neurological patient to rehabilitation unit.
neuro_esd: float
Complex neurological patient to ESD.
neuro_other: float
Complex neurological patient to other destinations.
other_rehab: float
Other patient type to rehabilitation unit.
other_esd: float
Other patient type to ESD.
other_other: float
Other patient type to other destinations.
"""
self.stroke = {
"rehab": stroke_rehab,
"esd": stroke_esd,
"other": stroke_other
}self.tia = {
"rehab": tia_rehab,
"esd": tia_esd,
"other": tia_other
}self.neuro = {
"rehab": neuro_rehab,
"esd": neuro_esd,
"other": neuro_other
}self.other = {
"rehab": other_rehab,
"esd": other_esd,
"other": other_other
}
class RehabRouting(RestrictAttributes):
"""
Probabilities of each patient type being transferred from the rehabiliation
unit to other destinations.
Attributes
----------
stroke: dict
Routing probabilities for stroke patients.
tia: dict
Routing probabilities for transient ischemic attack (TIA) patients.
neuro: dict
Routing probabilities for complex neurological patients.
other: dict
Routing probabilities for other patients.
"""
def __init__(
self,
# Stroke patients
=0.40, stroke_other=0.60,
stroke_esd# TIA patients
=0, tia_other=1,
tia_esd# Complex neurological patients
=0.09, neuro_other=0.91,
neuro_esd# Other patients
=0.13, other_other=0.88
other_esd
):"""
Parameters
----------
stroke_esd: float
Stroke patient to early support discharge (ESD) services.
stroke_other: float
Stroke patient to other destinations (e.g., own home, care home,
mortality).
tia_esd: float
TIA patient to ESD.
tia_other: float
TIA patient to other destinations.
neuro_esd: float
Complex neurological patient to ESD.
neuro_other: float
Complex neurological patient to other destinations.
other_esd: float
Other patient type to ESD.
other_other: float
Other patient type to other destinations.
"""
self.stroke = {
"esd": stroke_esd,
"other": stroke_other
}self.tia = {
"esd": tia_esd,
"other": tia_other
}self.neuro = {
"esd": neuro_esd,
"other": neuro_other
}self.other = {
"esd": other_esd,
"other": other_other
}
class Param(RestrictAttributes):
"""
Default parameters for simulation.
"""
def __init__(
self,
=ASUArrivals(),
asu_arrivals=RehabArrivals(),
rehab_arrivals=ASULOS(),
asu_los=RehabLOS(),
rehab_los=ASURouting(),
asu_routing=RehabRouting(),
rehab_routing=365*3, # 3 years
warm_up_period=365*5, # 5 years
data_collection_period=150,
number_of_runs=1,
audit_interval=1,
cores=False,
log_to_console=False,
log_to_file=("../outputs/logs/" +
log_file_pathf"{time.strftime("%Y-%m-%d_%H-%M-%S")}.log")
):"""
Initialise a parameter set for the simulation.
Parameters
----------
asu_arrivals: ASUArrivals
Arrival rates to the acute stroke unit (ASU) in days.
rehab_arrivals: RehabArrivals
Arrival rates to the rehabilitation unit in days.
asu_los: ASULOS
Length of stay (LOS) distributions for patients in the ASU in days.
rehab_los: RehabLOS
LOS distributions for patients in the rehabilitation unit in days.
asu_routing: ASURouting
Transfer probabilities from the ASU to other destinations.
rehab_routing: RehabRouting
Transfer probabilities from the rehabilitation unit to other
destinations.
warm_up_period: int
Length of the warm-up period.
data_collection_period: int
Length of the data collection period.
number_of_runs: int
The number of runs (i.e. replications), defining how many times to
re-run the simulation (with different random numbers).
audit_interval: float
Frequency of simulation audits in days.
cores: int
Number of CPU cores to use for parallel execution. Set to desired
number, or to -1 to use all available cores. For sequential
execution, set to 1.
log_to_console: boolean
Whether to print log messages to the console.
log_to_file: boolean
Whether to save log to a file.
log_file_path: str
Path to save log to file. Note, if you use an existing .log
file name, it will append to that log.
"""
# Set parameters
self.asu_arrivals = asu_arrivals
self.rehab_arrivals = rehab_arrivals
self.asu_los = asu_los
self.rehab_los = rehab_los
self.asu_routing = asu_routing
self.rehab_routing = rehab_routing
self.warm_up_period = warm_up_period
self.data_collection_period = data_collection_period
self.number_of_runs = number_of_runs
self.audit_interval = audit_interval
self.cores = cores
# Set up logger
self.logger = SimLogger(log_to_console=log_to_console,
=log_to_file,
log_to_file=log_file_path)
file_path
def check_param_validity(self):
"""
Check the validity of the provided parameters.
Validates all simulation parameters to ensure they meet requirements:
- Warm-up period and data collection period must be >= 0
- Number of runs and audit interval must be > 0
- Arrival rates must be >= 0
- Length of stay parameters must be >= 0
- Routing probabilities must sum to 1 and be between 0 and 1
Raises
------
ValueError
If any parameter fails validation with a descriptive error message.
"""
# Validate parameters that must be >= 0
for param in ["warm_up_period", "data_collection_period"]:
self.validate_param(
lambda x: x >= 0,
param, "must be greater than or equal to 0")
# Validate parameters that must be > 0
for param in ["number_of_runs", "audit_interval"]:
self.validate_param(
lambda x: x > 0,
param, "must be greater than 0")
# Validate arrival parameters
for param in ["asu_arrivals", "rehab_arrivals"]:
self.validate_nested_param(
lambda x: x >= 0,
param, "must be greater than 0")
# Validate length of stay parameters
for param in ["asu_los", "rehab_los"]:
self.validate_nested_param(
lambda x: x >= 0,
param, "must be greater than 0", nested=True)
# Validate routing parameters
for param in ["asu_routing", "rehab_routing"]:
self.validate_routing(param)
def validate_param(self, param_name, condition, error_msg):
"""
Validate a single parameter against a condition.
Parameters
----------
param_name: str
Name of the parameter being validated.
condition: callable
A function that returns True if the value is valid.
error_msg: str
Error message to display if validation fails.
Raises
------
ValueError:
If the parameter fails the validation condition.
"""
= getattr(self, param_name)
value if not condition(value):
raise ValueError(
f"Parameter '{param_name}' {error_msg}, but is: {value}")
def validate_nested_param(
self, obj_name, condition, error_msg, nested=False
):"""
Validate parameters within a nested object structure.
Parameters
----------
obj_name: str
Name of the object containing parameters.
condition: callable
A function that returns True if the value is valid.
error_msg: str
Error message to display if validation fails.
nested: bool, optional
If True, validates parameters in a doubly-nested structure. If
False, validates parameters in a singly-nested structure.
Raises
------
ValueError:
If any nested parameter fails the validation condition.
"""
= getattr(self, obj_name)
obj for key, value in vars(obj).items():
if key == "_initialised":
continue
if nested:
for sub_key, sub_value in value.items():
if not condition(sub_value):
raise ValueError(
f"Parameter '{sub_key}' for '{key}' in " +
f"'{obj_name}' {error_msg}, but is: {sub_value}")
else:
if not condition(value):
raise ValueError(
f"Parameter '{key}' from '{obj_name}' {error_msg}, " +
f"but is: {value}")
def validate_routing(self, obj_name):
"""
Validate routing probability parameters.
Performs two validations:
1. Checks that all probabilities for each routing option sum to 1.
2. Checks that individual probabilities are between 0 and 1 inclusive.
Parameters
----------
obj_name: str
Name of the routing object.
Raises
------
ValueError:
If the probabilities don't sum to 1, or if any probability is
outside [0,1].
"""
= getattr(self, obj_name)
obj for key, value in vars(obj).items():
if key == "_initialised":
continue
# Check that probabilities sum to 1
# Note: In the article, rehab other is 88% and 13%, so have
# allowed deviation of 1%
= sum(value.values())
total_prob if total_prob < 0.99 or total_prob > 1.01:
raise ValueError(
f"Routing probabilities for '{key}' in '{obj_name}' " +
f"should sum to apx. 1 but sum to: {total_prob}")
# Check that probabilities are between 0 and 1
for sub_key, sub_value in value.items():
if sub_value < 0 or sub_value > 1:
raise ValueError(
f"Parameter '{sub_key}' for '{key}' in '{obj_name}'" +
f"must be between 0 and 1, but is: {sub_value}")
TODO.