Skip to content

Commit a80d9a4

Browse files
committed
feat(runner): add runner for replications to run in parallel, and set to full sim_length
1 parent 8a31743 commit a80d9a4

9 files changed

Lines changed: 404 additions & 277 deletions

File tree

DESCRIPTION

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ Encoding: UTF-8
1818
LazyData: true
1919
RoxygenNote: 7.3.2
2020
Imports:
21+
dplyr,
22+
future,
23+
future.apply,
2124
simmer
2225
Suggests:
23-
devtools,
24-
dplyr
26+
devtools
2527
Config/testthat/edition: 3

NAMESPACE

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,20 @@ export(create_rehab_arrivals)
1010
export(create_rehab_los)
1111
export(create_rehab_routing)
1212
export(create_rehab_trajectory)
13+
export(get_occupancy_stats)
1314
export(model)
15+
export(runner)
1416
export(sample_routing)
1517
export(transform_to_lnorm)
18+
importFrom(dplyr,bind_rows)
1619
importFrom(dplyr,filter)
20+
importFrom(dplyr,mutate)
21+
importFrom(dplyr,rowwise)
22+
importFrom(dplyr,ungroup)
23+
importFrom(future,multisession)
24+
importFrom(future,plan)
25+
importFrom(future,sequential)
26+
importFrom(future.apply,future_lapply)
1727
importFrom(simmer,add_generator)
1828
importFrom(simmer,add_resource)
1929
importFrom(simmer,branch)

R/get_occupancy_stats.R

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#'
1818
#' @return A list of data frames, one per resource, each containing occupancy
1919
#' statistics.
20+
#' @export
2021

2122
get_occupancy_stats <- function(occupancy) {
2223

R/model.R

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
#' may not wish to do if being set elsewhere - such as done in \code{runner()}).
77
#' Default is TRUE.
88
#'
9-
#' @importFrom dplyr filter
9+
#' @importFrom dplyr filter mutate rowwise ungroup
1010
#' @importFrom simmer add_resource get_mon_arrivals get_mon_resources simmer
1111
#' @importFrom simmer wrap
1212
#' @importFrom utils capture.output
@@ -62,8 +62,7 @@ model <- function(run_number, param, set_seed = TRUE) {
6262
}
6363

6464
# Run the model
65-
sim_length <- 20L
66-
# sim_length <- param[["data_collection_period"]] + param[["warm_up_period"]]
65+
sim_length <- param[["data_collection_period"]] + param[["warm_up_period"]]
6766
sim_log <- capture.output(
6867
env <- env |> # nolint
6968
simmer::run(sim_length) |>
@@ -97,7 +96,7 @@ model <- function(run_number, param, set_seed = TRUE) {
9796
# Calculate occupancy at end of each day (i.e. at time 1, 2, 3, 4...)
9897
# Make dataframe with one row per resource per day to count patients
9998
occupancy <- expand.grid(
100-
resource = unique(arrivals$resource),
99+
resource = unique(arrivals[["resource"]]),
101100
time = days
102101
) |>
103102
rowwise() |>
@@ -107,12 +106,17 @@ model <- function(run_number, param, set_seed = TRUE) {
107106
# - Have not yet left by this day (end_time > time), or have NA end_time
108107
# (still present at simulation end)
109108
occupancy = sum(
110-
arrivals$resource == .data[["resource"]] &
111-
arrivals$start_time <= time &
112-
(is.na(arrivals$end_time) | arrivals$end_time > time)
109+
arrivals[["resource"]] == .data[["resource"]] &
110+
arrivals[["start_time"]] <= .data[["time"]] &
111+
(is.na(arrivals[["end_time"]]) |
112+
arrivals[["end_time"]] > .data[["time"]])
113113
)
114114
) |>
115115
ungroup()
116116

117+
# Set replication
118+
arrivals <- mutate(arrivals, replication = run_number)
119+
occupancy <- mutate(occupancy, replication = run_number)
120+
117121
return(list(arrivals = arrivals, occupancy = occupancy))
118122
}

R/runner.R

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#' Run simulation for multiple replications, sequentially or in parallel.
2+
#'
3+
#' @param param Named list of model parameters.
4+
#' @param use_future_seeding Logical. If TRUE, the function will use the
5+
#' seeding mechanism provided by `future.seed = seed`, which is generally
6+
#' recommended and ensures reproducibility across parallel executions. However,
7+
#' this will not align exactly with the seeding approach used in `model()`. If
8+
#' FALSE, the function will override future's default seeding and instead
9+
#' generate a list of run numbers to use as seeds,similar to `model()`. Be
10+
#' aware that this approach is not recommended according to `future_lapply`
11+
#' documentation, which states: "Note that as.list(seq_along(x)) is not a valid
12+
#' set of such .Random.seed values."
13+
#'
14+
#' @importFrom future plan multisession sequential
15+
#' @importFrom future.apply future_lapply
16+
#' @importFrom dplyr bind_rows
17+
#'
18+
#' @return Named list with three tables: monitored arrivals, monitored
19+
#' resources, and the processed results from each run.
20+
#' @export
21+
22+
runner <- function(param, use_future_seeding = TRUE) {
23+
# Determine the parallel execution plan
24+
if (param[["cores"]] == 1L) {
25+
plan(sequential) # Sequential execution
26+
} else {
27+
if (param[["cores"]] == -1L) {
28+
cores <- future::availableCores() - 1L
29+
} else {
30+
cores <- param[["cores"]]
31+
}
32+
plan(multisession, workers = cores) # Parallel execution
33+
}
34+
35+
# Set seed for future.seed
36+
if (isTRUE(use_future_seeding)) {
37+
# Recommended option - base seed used when generating others by future.seed
38+
custom_seed <- 123456L
39+
} else {
40+
# Not recommended (but will allow match to model())
41+
# Generates list of pre-generated seeds set to the run numbers
42+
create_seeds <- function(seed) {
43+
set.seed(seed)
44+
.Random.seed
45+
}
46+
custom_seed <- lapply(1L:param[["number_of_runs"]], create_seeds)
47+
}
48+
49+
# Run simulations (sequentially or in parallel)
50+
# Mark set_seed as FALSE as we handle this using future.seed(), rather than
51+
# within the function, and we don't want to override future.seed
52+
results <- future_lapply(
53+
1L:param[["number_of_runs"]],
54+
function(i) {
55+
simulation::model(run_number = i,
56+
param = param,
57+
set_seed = FALSE)
58+
},
59+
future.seed = custom_seed
60+
)
61+
62+
# Combine the results from multiple replications into just two dataframes
63+
if (param[["number_of_runs"]] == 1L) {
64+
results <- results[[1L]]
65+
} else {
66+
all_arrivals <- do.call(
67+
rbind, lapply(results, function(x) x[["arrivals"]])
68+
)
69+
all_occupancy <- do.call(
70+
rbind, lapply(results, function(x) x[["occupancy"]])
71+
)
72+
results <- list(arrivals = all_arrivals,
73+
occupancy = all_occupancy)
74+
}
75+
76+
results[["occupancy_stats"]] <- get_occupancy_stats(results[["occupancy"]])
77+
78+
results
79+
}

man/runner.Rd

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

renv.lock

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,36 @@
376376
],
377377
"Hash": "15aeb8c27f5ea5161f9f6a641fafd93a"
378378
},
379+
"future": {
380+
"Package": "future",
381+
"Version": "1.58.0",
382+
"Source": "Repository",
383+
"Repository": "CRAN",
384+
"Requirements": [
385+
"R",
386+
"digest",
387+
"globals",
388+
"listenv",
389+
"parallel",
390+
"parallelly",
391+
"utils"
392+
],
393+
"Hash": "b096716c11b90643e3cecdcbef564259"
394+
},
395+
"future.apply": {
396+
"Package": "future.apply",
397+
"Version": "1.20.0",
398+
"Source": "Repository",
399+
"Repository": "CRAN",
400+
"Requirements": [
401+
"R",
402+
"future",
403+
"globals",
404+
"parallel",
405+
"utils"
406+
],
407+
"Hash": "42eb18487138fa2683ff92149e4bd01a"
408+
},
379409
"generics": {
380410
"Package": "generics",
381411
"Version": "0.1.4",
@@ -430,6 +460,17 @@
430460
],
431461
"Hash": "ab08ac61f3e1be454ae21911eb8bc2fe"
432462
},
463+
"globals": {
464+
"Package": "globals",
465+
"Version": "0.18.0",
466+
"Source": "Repository",
467+
"Repository": "CRAN",
468+
"Requirements": [
469+
"R",
470+
"codetools"
471+
],
472+
"Hash": "0e0c37bd3108b8835c99eaa4d83cf6f5"
473+
},
433474
"glue": {
434475
"Package": "glue",
435476
"Version": "1.7.0",
@@ -586,6 +627,16 @@
586627
],
587628
"Hash": "b8552d117e1b808b09a832f589b79035"
588629
},
630+
"listenv": {
631+
"Package": "listenv",
632+
"Version": "0.9.1",
633+
"Source": "Repository",
634+
"Repository": "CRAN",
635+
"Requirements": [
636+
"R"
637+
],
638+
"Hash": "e2fca3e12e4db979dccc6e519b10a7ee"
639+
},
589640
"magrittr": {
590641
"Package": "magrittr",
591642
"Version": "2.0.3",
@@ -639,6 +690,18 @@
639690
],
640691
"Hash": "05ce1ed077e8c97fbb3ec1cb078f1159"
641692
},
693+
"parallelly": {
694+
"Package": "parallelly",
695+
"Version": "1.45.0",
696+
"Source": "Repository",
697+
"Repository": "CRAN",
698+
"Requirements": [
699+
"parallel",
700+
"tools",
701+
"utils"
702+
],
703+
"Hash": "eec07caa14285c8a9f9de8276473e7a1"
704+
},
642705
"pillar": {
643706
"Package": "pillar",
644707
"Version": "1.10.2",

rmarkdown/analysis.Rmd

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
title: "analysis"
2+
title: "Analysis"
33
author: "Amy Heather"
44
date: "`r Sys.Date()`"
55
output:
@@ -8,25 +8,74 @@ output:
88
html_preview: false
99
---
1010

11+
This analysis reproduces the analysis performed in:
12+
13+
> Monks T, Worthington D, Allen M, Pitt M, Stein K, James MA. A modelling tool for capacity planning in acute and community stroke services. BMC Health Serv Res. 2016 Sep 29;16(1):530. doi: 10.1186/s12913-016-1789-4. PMID: 27688152; PMCID: PMC5043535.
14+
15+
It is organised into:
16+
17+
* Set-up
18+
* Base case
19+
* Run the model
20+
* Figure 1
21+
* Theory: probability of delay
22+
* Figure 3
23+
* Scenario analysis: altering arrivals
24+
* Scenario 1
25+
* Table 2
26+
* Scenario 4
27+
* Supplementary table 1
28+
* Scenario analysis: pooling beds
29+
* Theory: pooling beds
30+
* Scenario 2
31+
32+
## Set-up
33+
34+
Install the latest version of the local simulation package. If running sequentially, `devtools::load_all()` is sufficient. If running in parallel, you must use `devtools::install()`.
35+
1136
```{r}
12-
# Load the package from the local directory
13-
devtools::load_all()
37+
devtools::install(upgrade = "never")
1438
```
1539

1640
```{r}
1741
# nolint start: undesirable_function_linter
18-
# Load the package
19-
library(dplyr)
42+
# Import required packages.
43+
library(dplyr, warn.conflicts = FALSE)
2044
library(simulation)
2145
# nolint end
2246
```
2347

2448
```{r}
25-
param <- create_parameters(log_to_console = TRUE)
26-
result <- model(run_number = 1L, param = param, set_seed = TRUE)
27-
result
49+
start_time <- Sys.time()
50+
```
51+
52+
```{r}
53+
output_dir <- file.path("..", "outputs")
54+
```
55+
56+
## Base case
57+
58+
### Run the model
59+
60+
```{r}
61+
# Run 150 replications in parallel with nine cores
62+
param <- create_parameters(cores = 9)
63+
results <- runner(param = param)
2864
```
2965

3066
```{r}
31-
get_occupancy_stats(result[["occupancy"]])
67+
get_occupancy_stats(results[["occupancy"]])
68+
```
69+
70+
## Calculate run time
71+
72+
```{r end_timer}
73+
# Get run time in seconds
74+
end_time <- Sys.time()
75+
runtime <- as.numeric(end_time - start_time, units = "secs")
76+
77+
# Display converted to minutes and seconds
78+
minutes <- as.integer(runtime / 60L)
79+
seconds <- as.integer(runtime %% 60L)
80+
cat(sprintf("Notebook run time: %dm %ds", minutes, seconds))
3281
```

0 commit comments

Comments
 (0)