Skip to content

Commit 1067bea

Browse files
authored
Merge pull request #109 from pythonhealthdatascience/dev
Dev
2 parents 25d1159 + 661e63e commit 1067bea

77 files changed

Lines changed: 1215 additions & 881 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CITATION.cff

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,5 @@ repository-code: >-
2424
abstract: >-
2525
Reproducible analytical pipeline (RAP) for R discrete-event simulation (DES)
2626
implementing a simple M/M/s queueing model.
27-
version: 0.5.0
28-
date-released: '2025-11-07'
27+
version: 1.0.0
28+
date-released: '2026-03-06'

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ If you are a maintainer and need to publish a new release:
113113

114114
1. Update `NEWS.md`.
115115

116-
2. Update the version number in `DESCRIPTION`, `CITATION.cff` and `CITATION`, and update the date in `CITATION.cff`.
116+
2. Update the version number in `DESCRIPTION` and `CITATION.cff`, and update the date in `CITATION.cff`.
117117

118118
3. Create a release on GitHub, which will automatically archive to Zenodo.
119119

DESCRIPTION

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Package: simulation
22
Type: Package
33
Title: Simulation
4-
Version: 0.5.0
4+
Version: 1.0.0
55
Authors@R: c(
66
person(
77
"Amy", "Heather",
@@ -32,7 +32,8 @@ Imports:
3232
ggplot2,
3333
tibble,
3434
gridExtra,
35-
R6
35+
R6,
36+
checkmate
3637
Suggests:
3738
testthat (>= 3.0.0),
3839
patrick,

NAMESPACE

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,17 @@ export(filter_warmup)
1717
export(get_run_results)
1818
export(model)
1919
export(parameters)
20+
export(run_replications_algorithm)
2021
export(run_scenarios)
2122
export(runner)
2223
export(time_series_inspection)
2324
export(valid_inputs)
2425
importFrom(R6,R6Class)
26+
importFrom(checkmate,assert_flag)
27+
importFrom(checkmate,assert_int)
28+
importFrom(checkmate,assert_list)
29+
importFrom(checkmate,assert_number)
30+
importFrom(checkmate,assert_string)
2531
importFrom(dplyr,across)
2632
importFrom(dplyr,arrange)
2733
importFrom(dplyr,bind_rows)

NEWS.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
1+
# Simple M/M/s queuing model: R DES RAP v1.0.0
2+
3+
Some of the main changes in this update include moving away from R6, using checkmate in validation, correcting warm-up analysis, expanding replications analysis to include all metrics, and switching to package-level imports.
4+
5+
## New features
6+
7+
* Add retrospective QA summary.
8+
* Now run choosing replications analysis on all metrics.
9+
10+
## Bug fixes
11+
12+
* Correct determination of warm-up period to use intervals, and re-run analysis with new parameters.
13+
14+
## Other changes
15+
16+
* Switched to package-level importfrom statements to avoid duplication.
17+
* Changed replications R6 classes to functions.
18+
* Use checkmate in validation.
19+
* Changed some parameters.
20+
* Updated STRESS.
21+
* Minor updates to some docstrings.
22+
123
# Simple M/M/s queuing model: R DES RAP v0.5.0
224

325
This update adds new performance measures and tests, improves the GitHub actions, implements some fixes and documentation improvements, among other changes.

R/choose_replications.R

Lines changed: 87 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,9 @@
99
#'
1010
#' This class is based on the Python class `OnlineStatistics` from Tom Monks
1111
#' (2021) sim-tools: fundamental tools to support the simulation process in
12-
#' python (https://github.com/TomMonks/sim-tools) (MIT Licence).
12+
#' python (https://github.com/sim-tools/sim-tools) (MIT Licence).
1313
#'
1414
#' @docType class
15-
#' @importFrom R6 R6Class
1615
#'
1716
#' @return Object of `R6Class` with methods for running mean and variance
1817
#' calculation.
@@ -128,10 +127,9 @@ WelfordStats <- R6Class("WelfordStats", list( # nolint: object_name_linter
128127
#'
129128
#' This class is based on the Python class `ReplicationTabulizer` from Tom
130129
#' Monks (2021) sim-tools: fundamental tools to support the simulation process
131-
#' in python (https://github.com/TomMonks/sim-tools) (MIT Licence).
130+
#' in python (https://github.com/sim-tools/sim-tools) (MIT Licence).
132131
#'
133132
#' @docType class
134-
#' @importFrom R6 R6Class
135133
#'
136134
#' @return Object of `R6Class` with methods for storing and tabulising results.
137135
#' @export
@@ -205,10 +203,9 @@ ReplicationTabuliser <- R6Class("ReplicationTabuliser", list( # nolint: object_n
205203
#'
206204
#' This class is based on the Python class `ReplicationsAlgorithm` from Tom
207205
#' Monks (2021) sim-tools: fundamental tools to support the simulation process
208-
#' in python (https://github.com/TomMonks/sim-tools) (MIT Licence).
206+
#' in python (https://github.com/sim-tools/sim-tools) (MIT Licence).
209207
#'
210208
#' @docType class
211-
#' @importFrom R6 R6Class
212209
#'
213210
#' @return Object of `ReplicationsAlgorithm` with methods for determining the
214211
#' appropriate number of replications to use.
@@ -304,7 +301,7 @@ ReplicationsAlgorithm <- R6Class("ReplicationsAlgorithm", list( # nolint: object
304301
stop("desired_precision must be greater than 0.", call. = FALSE)
305302
}
306303
if (self$replication_budget < self$initial_replications) {
307-
stop("replication_budget must be less than initial_replications.",
304+
stop("replication_budget must be greater than initial_replications.",
308305
call. = FALSE)
309306
}
310307
},
@@ -333,8 +330,13 @@ ReplicationsAlgorithm <- R6Class("ReplicationsAlgorithm", list( # nolint: object
333330
call. = FALSE)
334331
}
335332

336-
# Check if list is empty or no values below threshold
337-
if (length(lst) == 0L || all(is.na(lst)) || !any(unlist(lst) < 0.5)) {
333+
# Check if list is empty or all NA
334+
if (length(lst) == 0L || all(is.na(lst))) {
335+
return(NULL)
336+
}
337+
338+
# Check that there are no values below threshold
339+
if (!any(unlist(lst) < self$desired_precision, na.rm = TRUE)) {
338340
return(NULL)
339341
}
340342

@@ -504,6 +506,82 @@ ReplicationsAlgorithm <- R6Class("ReplicationsAlgorithm", list( # nolint: object
504506
))
505507

506508

509+
#' Automated replications selection using Welford‑based online statistics.
510+
#'
511+
#' @description
512+
#' A user‑friendly wrapper around the ReplicationsAlgorithm class that:
513+
#' - Runs the replications sequence
514+
#' - Returns minimum required replications for each metric
515+
#' - Returns summary tables for each metric (replication‑by‑replication)
516+
#'
517+
#' Based on Hoad, Robinson, & Davies (2010) "Automated selection of the number
518+
#' of replications for a discrete-event simulation".
519+
#' You just call this as a function, without exposing the R6 internals.
520+
#'
521+
#' @param param List, model configuration (passed to your `runner` or `model`).
522+
#' @param metrics Character vector of metric names (columns in `run_results`).
523+
#' @param desired_precision Target deviation of CI half‑width as proportion
524+
#' of the mean, e.g. `0.1` for 10%.
525+
#' @param initial_replications Number of initial replications.
526+
#' @param look_ahead Minimum extra replications to “look ahead”.
527+
#' @param replication_budget Maximum allowed replications.
528+
#' @param verbose Logical; whether to print startup messages.
529+
#'
530+
#' @return A list with:
531+
#' \itemize{
532+
#' \item `nreps` named list of min replications per metric (or `NA` if not
533+
#' met).
534+
#' \item `summary_table` dataframe with per‑replication statistics for all
535+
#' metrics.
536+
#' \item `status` character vector: metrics for which desired precision was
537+
#' not reached.
538+
#' }
539+
#' @export
540+
run_replications_algorithm <- function(
541+
param,
542+
metrics = c("mean_waiting_time_nurse",
543+
"mean_serve_time_nurse",
544+
"utilisation_nurse"),
545+
desired_precision = 0.1,
546+
initial_replications = 3L,
547+
look_ahead = 5L,
548+
replication_budget = 1000L,
549+
verbose = TRUE
550+
) {
551+
# Construct the R6 algorithm object
552+
alg <- ReplicationsAlgorithm$new(
553+
param = param,
554+
metrics = metrics,
555+
desired_precision = desired_precision,
556+
initial_replications = initial_replications,
557+
look_ahead = look_ahead,
558+
replication_budget = replication_budget,
559+
verbose = verbose
560+
)
561+
562+
# Run the algorithm
563+
alg$select()
564+
565+
# Extract results in a plain‑list form
566+
nreps <- as.list(alg$nreps) # numeric/NA for each metric
567+
568+
# Identify which metrics didn’t converge
569+
unsolved <- names(nreps)[vapply(nreps, is.na, logical(1L))]
570+
# nolint start: keyword_quote_linter
571+
status <- if (length(unsolved) > 0L) {
572+
c("not_converged" = unsolved)
573+
} else {
574+
c("converged" = NA_character_)
575+
} # nolint end: keyword_quote_linter
576+
577+
list(
578+
nreps = nreps,
579+
summary_table = alg$summary_table,
580+
status = status
581+
)
582+
}
583+
584+
507585
#' Use the confidence interval method to select the number of replications.
508586
#'
509587
#' This could be altered to use WelfordStats and ReplicationTabuliser if
@@ -514,10 +592,6 @@ ReplicationsAlgorithm <- R6Class("ReplicationsAlgorithm", list( # nolint: object
514592
#' @param metric Name of performance metric to assess.
515593
#' @param verbose Boolean, whether to print messages about parameters.
516594
#'
517-
#' @importFrom dplyr filter pull select slice_head
518-
#' @importFrom stats sd t.test
519-
#' @importFrom utils tail
520-
#'
521595
#' @return Dataframe with results from each replication.
522596
#' @export
523597

@@ -609,10 +683,6 @@ confidence_interval_method <- function(replications, desired_precision,
609683
#' @param file_path Path and filename to save the plot to.
610684
#' @param min_rep The number of replications required to meet the desired
611685
#' precision.
612-
#'
613-
#' @importFrom ggplot2 aes geom_line geom_ribbon geom_vline ggplot ggsave labs
614-
#' @importFrom ggplot2 theme_minimal
615-
#' @importFrom rlang .data
616686

617687
plot_replication_ci <- function(
618688
conf_ints, yaxis_title, file_path = NULL, min_rep = NULL

R/choose_warmup.R

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,19 @@
33
#' Find the cumulative mean results and plot over time (overall and per run).
44
#'
55
#' @param result Named list with `arrivals` containing output from
6-
#' `get_mon_arrivals()` and `resources` containing output from
7-
#' `get_mon_resources()` (`per_resource = TRUE` and `ongoing = TRUE`).
6+
#' `get_mon_arrivals()`, `resources` containing output from
7+
#' `get_mon_resources()` (`per_resource = TRUE` and `ongoing = TRUE`), and
8+
#' `patients_in_service` containing counts of patients in system over time.
89
#' @param simulation_end_time Time at end of simulation run.
910
#' @param file_path Path to save figure to.
11+
#' @param interval Time interval in minutes for calculating cumulative means.
1012
#' @param warm_up Location on X axis to plot vertical red line indicating the
1113
#' chosen warm-up period. Defaults to NULL, which will not plot a line.
1214
#'
13-
#' @importFrom dplyr arrange group_by mutate rename select ungroup
14-
#' @importFrom ggplot2 aes_string annotate geom_line geom_vline ggsave labs
15-
#' @importFrom ggplot2 theme_minimal ggplot
16-
#' @importFrom gridExtra marrangeGrob
17-
#' @importFrom rlang .data
18-
#' @importFrom tidyselect all_of
19-
#'
2015
#' @export
2116

2217
time_series_inspection <- function(
23-
result, simulation_end_time, file_path, warm_up = NULL
18+
result, simulation_end_time, file_path, interval = 120L, warm_up = NULL
2419
) {
2520

2621
plot_list <- list()
@@ -57,25 +52,47 @@ time_series_inspection <- function(
5752
metrics[[5L]] <- rename(result[["patients_in_service"]],
5853
patients_in_system = .data[["count"]])
5954

55+
# Create sequence of time intervals
56+
time_breaks <- seq(0L, simulation_end_time + interval, by = interval)
57+
6058
# Loop through all the dataframes in df_list
6159
for (i in seq_along(metrics)) {
6260

6361
# Get name of the metric
6462
metric <- setdiff(names(metrics[[i]]), c("time", "replication"))
6563

66-
# Calculate cumulative mean for the current metric
67-
cumulative <- metrics[[i]] |>
64+
# Aggregate data to time intervals (calculate mean within each interval)
65+
aggregated <- metrics[[i]] |>
66+
mutate(time_bin = as.numeric(as.character(
67+
cut(time, breaks = time_breaks, labels = time_breaks[-1L])
68+
))) |>
69+
group_by(.data[["replication"]], .data[["time_bin"]]) |>
70+
summarise(metric_mean = mean(.data[[metric]])) |>
71+
ungroup() |>
72+
rename(time = .data[["time_bin"]])
73+
74+
# Calculate cumulative mean for the current metric per replication
75+
cumulative <- aggregated |>
6876
arrange(.data[["replication"]], .data[["time"]]) |>
6977
group_by(.data[["replication"]]) |>
70-
mutate(cumulative_mean = cumsum(.data[[metric]]) /
71-
seq_along(.data[[metric]])) |>
78+
mutate(cumulative_mean = (cumsum(.data[["metric_mean"]]) /
79+
seq_along(.data[["metric_mean"]]))) |>
7280
ungroup()
7381

7482
# Repeat calculation, but including all replications in one
75-
overall_cumulative <- metrics[[i]] |>
83+
overall_aggregated <- metrics[[i]] |>
84+
mutate(time_bin = as.numeric(as.character(
85+
cut(time, breaks = time_breaks, labels = time_breaks[-1L])
86+
))) |>
87+
group_by(.data[["time_bin"]]) |>
88+
summarise(metric_mean = mean(.data[[metric]])) |>
89+
ungroup() |>
90+
rename(time = .data[["time_bin"]])
91+
92+
overall_cumulative <- overall_aggregated |>
7693
arrange(.data[["time"]]) |>
77-
mutate(cumulative_mean = cumsum(.data[[metric]]) /
78-
seq_along(.data[[metric]])) |>
94+
mutate(cumulative_mean = cumsum(.data[["metric_mean"]]) /
95+
seq_along(.data[["metric_mean"]])) |>
7996
ungroup()
8097

8198
# Create plot

0 commit comments

Comments
 (0)