Parameters from script

Learning objectives:

  • Recognise the drawbacks of hardcoding parameters in simulation models.
  • Learn about methods and best practices for parameter management.
  • Note ways to keep models organised when working with many parameters .

Relevant reproducibility guidelines:

  • STARS Reproducibility Recommendations: Avoid hard-coded parameters.

Required packages:

This should be available from environment setup in the “🧪 Test yourself” section of Environments.

library(R6)


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 explains best practices for managing model parameters directly within scripts.

🚫 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).

📄 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.

✅ Recommendation for managing parameters in DES

Two critical practices

To manage parameters effectively, you need to:

  1. Group parameters into a dedicated object.
  2. Pass this object 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!
  ...
}

Three implementation options

There are three implementation options: dictionary, function or class.

Using a dictionary

# 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:
    """
    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

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 - although it is ultimately possible to perform similar validation regardless of whether you use dictionaries, functions and classes - 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.

🧮 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…

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(
    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:
    """
    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

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
    if arrivals is None:
        arrivals = create_arrivals()
    if consultations is None:
        consultations = create_consultations()
    if transfers is None:
        transfers = create_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:
    """
    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:
    """
    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:
    """
    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():
    """
    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

🔍 Explore the example models

🩺 Nurse visit simulation

GitHub Click to visit pydesrap_mms repository

Key file simulation/param.py
What to look for? Notice how all parameters are grouped into a single Param class that gets passed to the simulation class. This demonstrates the “dedicated object” approach we recommend.
Why it matters? This simulation manages 11 different parameters. Imagine trying to pass all these individually to every function!

GitHub Click to visit rdesrap_mms repository

Key file R/parameters.R
What to look for? Notice how all parameters are grouped into a single parameters function that gets passed to the simulation functions. This demonstrates the “dedicated object” approach we recommend.
Why it matters? This simulation manages 11 different parameters. Imagine trying to pass all these individually to every function!

🧠 Stroke pathway simulation

Not applicable as the stroke example stores parameters in a file.

🧪 Test yourself

When creating multiple scenarios that differ by only one parameter, what is a robust approach?
Why is it important to pass parameters explicitly to the model instead of relying on global variables?


If you haven’t already, let’s have a go at managing parameters inside scripts.

Task:

  • Create a new Jupyter notebook in your project (e.g. in a notebooks/ directory).

  • Try out at least two different ways of grouping and passing parameters, using examples from above (e.g. dictionary, function, or class).

  • Create a new Rmarkdown file in your project (e.g. in a rmarkdown/ directory).

  • Try out at least two different ways of grouping and passing parameters, using examples from above (e.g. list, function, or R6 class).

  • For each option, create at least one variant scenario by overriding one or more parameters.