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.
# Load required packages
library(R6) # nolint: undesirable_function_linter


2 What not to do: hardcoding parameters

Hardcoding means writing parameter values directly into your code. For example:

def model():
    # Hardcoded parameter values
    interarrival_time = 5
    consultation_time = 20
    transfer_prob = 0.3
    # ...rest of the model...
model <- function() {
    # Hardcoded parameter values
    interarrival_time <- 5
    consultation_time <- 20
    transfer_prob <- 0.3
    # ...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
INTERARRIVAL_TIME = 5
CONSULTATION_TIME = 20
TRANSFER_PROB = 0.3

def model():
    # Use the global parameters
    # ...
# Parameters for base case
interarrival_time <- 5
consultation_time <- 20
transfer_prob <- 0.3

model <- function() {
  # Use the global parameters
  # ...
}

For scenarios, you would define the same global variables with alternative values:

# Scenario 1
INTERARRIVAL_TIME = 6
CONSULTATION_TIME = 20
TRANSFER_PROB = 0.3
# Scenario 2
INTERARRIVAL_TIME = 5
CONSULTATION_TIME = 20
TRANSFER_PROB = 0.4
# Scenario 1
interarrival_time <- 6
consultation_time <- 20
transfer_prob <- 0.3
# Scenario 2
interarrival_time <- 5
consultation_time <- 20
transfer_prob <- 0.4

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:

  1. Group parameters into a dedicated object.
  2. 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
global_params = Parameters()

def simulate():
    # Uses global_params.interarrival_time... 😬
    ...
# BAD: Parameters are grouped but still global
global_params <- list()

simulate <- function() {
  # 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
simulate <- function(interarrival_time, consultation_time, transfer_prob, ...) {
  # 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
scenario1 = base_params.copy()
scenario1["interarrival_time"] = 6
print(scenario1)
{'interarrival_time': 6, 'consultation_time': 20, 'transfer_prob': 0.3}


Using a function

def create_params(
  interarrival_time=5, consultation_time=20, transfer_prob=0.3
):
    """
    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
base_params = create_params()
print(base_params)
{'interarrival_time': 5, 'consultation_time': 20, 'transfer_prob': 0.3}
# Create a scenario with altered inter-arrival time
scenario1 = create_params(interarrival_time=6)
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
base_params = Parameters()
print(base_params.__dict__)
{'interarrival_time': 5, 'consultation_time': 20, 'transfer_prob': 0.3}
# Create a scenario with altered inter-arrival time
scenario1 = Parameters(interarrival_time=6)
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
base_params <- list(
  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
scenario1 <- base_params
scenario1$interarrival_time <- 6L
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.

create_params <- function(interarrival_time = 5L,
                          consultation_time = 20L,
                          transfer_prob = 0.3) {
  list(
    interarrival_time = interarrival_time,
    consultation_time = consultation_time,
    transfer_prob     = transfer_prob
  )
}

# Base case
base_params <- create_params()
base_params
$interarrival_time
[1] 5

$consultation_time
[1] 20

$transfer_prob
[1] 0.3
# Create a scenario with altered inter-arrival time
scenario1 <- create_params(interarrival_time = 6L)
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).

Parameters <- R6Class( # nolint: object_name_linter
  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
    ) {
      self$interarrival_time <- interarrival_time
      self$consultation_time <- consultation_time
      self$transfer_prob <- transfer_prob
    },

    #' @description
    #' Returns parameters as a named list.

    get_params = function() {
      # Get all non-function fields
      all_names <- ls(self)
      is_not_function <- vapply(
        all_names,
        function(x) !is.function(self[[x]]),
        FUN.VALUE = logical(1L)
      )
      param_names <- all_names[is_not_function]
      mget(param_names, envir = self)
    }
  )
)

# Base case
base_params <- Parameters$new()
base_params$get_params()
$consultation_time
[1] 20

$interarrival_time
[1] 5

$transfer_prob
[1] 0.3
# Create a scenario with altered inter-arrival time
scenario1 <- Parameters$new(interarrival_time = 6L)
scenario1$get_params()
$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
    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
):
    """
    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
base_params = create_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,
        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
    ):
        """
        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
base_params = Parameters()
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.

create_params <- 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
) {
  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
base_params <- create_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).

Parameters <- R6Class( # nolint: object_name_linter
  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
      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
    },

    #' @description
    #' Returns parameters as a named list.
    #'
    #' @return A named list containing all parameter values.

    get_params = function() {
      # Get all non-function fields
      all_names <- ls(self)
      is_not_function <- vapply(
        all_names,
        function(x) !is.function(self[[x]]),
        FUN.VALUE = logical(1L)
      )
      param_names <- all_names[is_not_function]
      mget(param_names, envir = self)
    }
  )
)


# Example usage
base_params <- Parameters$new()
base_params$get_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(
    arrivals=None,
    consultations=None,
    transfers=None
):
    """
    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
    arrivals = create_arrivals() if arrivals is None else arrivals
    consultations = (
      create_consultations() if consultations is None else consultations)
    transfers = create_transfers() if transfers is None else transfers
    # Create dictionary
    return {
        "arrivals": arrivals,
        "consultations": consultations,
        "transfers": transfers
    }


# Example usage
base_params = create_parameters()
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
base_params = Parameters()
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.
create_arrivals <- function(adult = 5L, child = 7L, elderly = 10L) {
  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.
create_consultations <- function(adult = 20L, child = 15L, elderly = 30L) {
  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.
create_transfers <- function(adult = 0.3, child = 0.2, elderly = 0.5) {
  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.
create_parameters <- function(
  arrivals = create_arrivals(),
  consultations = create_consultations(),
  transfers = create_transfers()
) {
  list(
    arrivals = arrivals,
    consultations = consultations,
    transfers = transfers
  )
}


# Example usage
base_params <- create_parameters()
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).

Arrivals <- R6Class( # nolint: object_name_linter
  classname = "Arrivals",
  public = list(
    adult = NULL,
    child = NULL,
    elderly = NULL,

    #' @description
    #' Initialise Arrivals instance.

    initialize = function(adult = 5L, child = 7L, elderly = 10L) {
      self$adult <- adult
      self$child <- child
      self$elderly <- elderly
    }
  )
)

#' @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).

Consultations <- R6Class( # nolint: object_name_linter
  classname = "Consultations",
  public = list(
    adult = NULL,
    child = NULL,
    elderly = NULL,

    #' @description
    #' Initialise Consultations instance.

    initialize = function(adult = 20L, child = 15L, elderly = 30L) {
      self$adult <- adult
      self$child <- child
      self$elderly <- elderly
    }
  )
)

#' @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).

Transfers <- R6Class( # nolint: object_name_linter
  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) {
      self$adult <- adult
      self$child <- child
      self$elderly <- elderly
    }
  )
)

#' @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.

Parameters <- R6Class( # nolint: object_name_linter
  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()
    ) {
      self$arrivals <- arrivals
      self$consultations <- consultations
      self$transfers <- transfers
    },

    #' @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
      all_names <- ls(self)
      is_not_function <- vapply(
        all_names,
        function(x) !is.function(self[[x]]),
        FUN.VALUE = logical(1L)
      )
      instance_names <- all_names[is_not_function]

      # Get the instances
      instances <- mget(instance_names, envir = self)

      # Apply parameter extraction to each instance using lapply
      lapply(instances, function(instance) {
        if (inherits(instance, "R6")) {
          # Extract non-function fields from R6 instance
          all_fields <- ls(instance)
          is_not_function <- vapply(
            all_fields,
            function(x) !is.function(instance[[x]]),
            FUN.VALUE = logical(1L)
          )
          field_names <- all_fields[is_not_function]
          mget(field_names, envir = instance)
        } else {
          # Return non-R6 objects as-is
          instance
        }
      })
    }
  )
)


# Example usage
base_params <- Parameters$new()
base_params$get_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,
        patient_inter=4,
        mean_n_consult_time=10,
        number_of_nurses=5,
        warm_up_period=1440*27,  # 27 days
        data_collection_period=1440*30,  # 30 days
        number_of_runs=31,
        audit_interval=120,  # Every 2 hours
        scenario_name=0,
        cores=-1,
        logger=SimLogger(log_to_console=False, log_to_file=False)
    ):
        """
        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

parameters <- function(
  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
        instance = super().__call__(*args, **kwargs)
        # Set the "_initialised" flag to True, marking end of initialisation
        instance.__dict__["_initialised"] = True
        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
            existing = ", ".join(self.__dict__.keys())
            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,
        stroke_no_esd_mean=7.4, stroke_no_esd_sd=8.61,
        stroke_esd_mean=4.6, stroke_esd_sd=4.8,
        stroke_mortality_mean=7.0, stroke_mortality_sd=8.7,
        tia_mean=1.8, tia_sd=2.3,
        neuro_mean=4.0, neuro_sd=5.0,
        other_mean=3.8, other_sd=5.2
    ):
        """
        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,
        stroke_no_esd_mean=28.4, stroke_no_esd_sd=27.2,
        stroke_esd_mean=30.3, stroke_esd_sd=23.1,
        tia_mean=18.7, tia_sd=23.5,
        neuro_mean=27.6, neuro_sd=28.4,
        other_mean=16.1, other_sd=14.1
    ):
        """
        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
        stroke_rehab=0.24, stroke_esd=0.13, stroke_other=0.63,
        # TIA patients
        tia_rehab=0.01, tia_esd=0.01, tia_other=0.98,
        # Complex neurological patients
        neuro_rehab=0.11, neuro_esd=0.05, neuro_other=0.84,
        # Other patients
        other_rehab=0.05, other_esd=0.10, other_other=0.85
    ):
        """
        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
        stroke_esd=0.40, stroke_other=0.60,
        # TIA patients
        tia_esd=0, tia_other=1,
        # Complex neurological patients
        neuro_esd=0.09, neuro_other=0.91,
        # Other patients
        other_esd=0.13, other_other=0.88
    ):
        """
        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,
        asu_arrivals=ASUArrivals(),
        rehab_arrivals=RehabArrivals(),
        asu_los=ASULOS(),
        rehab_los=RehabLOS(),
        asu_routing=ASURouting(),
        rehab_routing=RehabRouting(),
        warm_up_period=365*3,  # 3 years
        data_collection_period=365*5,  # 5 years
        number_of_runs=150,
        audit_interval=1,
        cores=1,
        log_to_console=False,
        log_to_file=False,
        log_file_path=("../outputs/logs/" +
                       f"{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,
                                file_path=log_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(
                param, lambda x: x >= 0,
                "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(
                param, lambda x: x > 0,
                "must be greater than 0")

        # Validate arrival parameters
        for param in ["asu_arrivals", "rehab_arrivals"]:
            self.validate_nested_param(
                param, lambda x: x >= 0,
                "must be greater than 0")

        # Validate length of stay parameters
        for param in ["asu_los", "rehab_los"]:
            self.validate_nested_param(
                param, lambda x: x >= 0,
                "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.
        """
        value = getattr(self, param_name)
        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.
        """
        obj = getattr(self, obj_name)
        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].
        """
        obj = getattr(self, obj_name)
        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%
            total_prob = sum(value.values())
            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.