First, load the package and instantiate a new simulation environment.
library(simmer)
env <- simmer("SuperDuperSim")
env
#> simmer environment: SuperDuperSim | now: 0 | next:
Set-up a simple trajectory. Let’s say we want to simulate an ambulatory consultation where a patient is first seen by a nurse for an intake, next by a doctor for the consultation and finally by administrative staff to schedule a follow-up appointment.
patient <- create_trajectory("patients' path") %>%
## add an intake activity
seize("nurse", 1) %>%
timeout(function() rnorm(1, 15)) %>%
release("nurse", 1) %>%
## add a consultation activity
seize("doctor", 1) %>%
timeout(function() rnorm(1, 20)) %>%
release("doctor", 1) %>%
## add a planning activity
seize("administration", 1) %>%
timeout(function() rnorm(1, 5)) %>%
release("administration", 1)
In this case, the argument of the timeout
activity is a function, which is evaluated dynamically to produce an stochastic waiting time, but it could be a constant too. Apart from that, this function may be as complex as you need and may do whatever you want: interact with entities in your simulation model, get resources’ status, make decisions according to the latter…
Once the trajectory is known, you may attach arrivals to it and define the resources needed. In the example below, three types of resources are added: the nurse and administration resources, each one with a capacity of 1, and the doctor resource, with a capacity of 2. The last method adds a generator of arrivals (patients) following the trajectory t0
. The time between patients is about 10 minutes (a Gaussian of mean=10
and sd=2
). (Note: returning a negative interarrival time at some point would stop the generator).
env %>%
add_resource("nurse", 1) %>%
add_resource("doctor", 2) %>%
add_resource("administration", 1) %>%
add_generator("patient", patient, function() rnorm(1, 10, 2))
#> simmer environment: SuperDuperSim | now: 0 | next: 7.33531526552597
#> { Resource: nurse | monitored: 1 | server status: 0(1) | queue status: 0(Inf) }
#> { Resource: doctor | monitored: 1 | server status: 0(2) | queue status: 0(Inf) }
#> { Resource: administration | monitored: 1 | server status: 0(1) | queue status: 0(Inf) }
#> { Generator: patient | monitored: 1 | n_generated: 1 }
The simulation is now ready for a test run; just let it simmer for a bit. Below, we specify that we want to limit the runtime to 80 time units using the until
argument. After that, we verify the current simulation time (now
) and when will be the next 3 events (peek
).
env %>% run(until=80)
#> simmer environment: SuperDuperSim | now: 84.0022534994433 | next: 84.0022534994433
#> { Resource: nurse | monitored: 1 | server status: 0(1) | queue status: 3(Inf) }
#> { Resource: doctor | monitored: 1 | server status: 1(2) | queue status: 0(Inf) }
#> { Resource: administration | monitored: 1 | server status: 0(1) | queue status: 0(Inf) }
#> { Generator: patient | monitored: 1 | n_generated: 9 }
env %>% now()
#> [1] 84.00225
env %>% peek(3)
#> [1] 84.00225 84.00225 84.79306
It is possible to run the simulation step by step, and such a method is chainable too.
env %>% onestep()
#> simmer environment: SuperDuperSim | now: 84.0022534994433 | next: 84.0022534994433
#> { Resource: nurse | monitored: 1 | server status: 1(1) | queue status: 2(Inf) }
#> { Resource: doctor | monitored: 1 | server status: 1(2) | queue status: 0(Inf) }
#> { Resource: administration | monitored: 1 | server status: 0(1) | queue status: 0(Inf) }
#> { Generator: patient | monitored: 1 | n_generated: 9 }
env %>% onestep() %>% onestep() %>% onestep()
#> simmer environment: SuperDuperSim | now: 84.0022534994433 | next: 84.7930634235397
#> { Resource: nurse | monitored: 1 | server status: 1(1) | queue status: 2(Inf) }
#> { Resource: doctor | monitored: 1 | server status: 2(2) | queue status: 0(Inf) }
#> { Resource: administration | monitored: 1 | server status: 0(1) | queue status: 0(Inf) }
#> { Generator: patient | monitored: 1 | n_generated: 9 }
env %>% now()
#> [1] 84.00225
env %>% peek(Inf, verbose=TRUE)
#> time process
#> 1 84.79306 patient
#> 2 84.79306 patient8
#> 3 88.18559 patient3
#> 4 99.58803 patient5
#> 5 104.41267 patient4
Also, it is possible to resume the automatic execution simply by specifying a longer runtime. Below, we continue the execution until 120 time units.
env %>%
run(until=120) %>%
now()
#> [1] 123.8842
Finally, you can reset the simulation, flush all results, resources and generators, and restart from the beginning.
env %>%
reset() %>%
run(until=80) %>%
now()
#> [1] 80.41575
It is very easy to replicate a simulation multiple times using standard R functions.
envs <- lapply(1:100, function(i) {
simmer("SuperDuperSim") %>%
add_resource("nurse", 1) %>%
add_resource("doctor", 2) %>%
add_resource("administration", 1) %>%
add_generator("patient", patient, function() rnorm(1, 10, 2)) %>%
run(80)
})
The advantage of the latter approach is that, if the individual replicas are heavy, it is straightforward to parallelize their execution (for instance, in the next example we use the function mclapply
from the package parallel). Nevertheless, the external pointers to the C++ simmer core are no longer valid when the parallelized execution ends. Thus, it is necessary to extract the results for each thread at the end of the execution. This can be done with the helper function wrap
as follows.
library(parallel)
envs <- mclapply(1:100, function(i) {
simmer("SuperDuperSim") %>%
add_resource("nurse", 1) %>%
add_resource("doctor", 2) %>%
add_resource("administration", 1) %>%
add_generator("patient", patient, function() rnorm(1, 10, 2)) %>%
run(80) %>%
wrap()
})
This helper function brings the simulation data back to R and makes it accessible through the same methods that a simmer
environment.
envs[[1]] %>% get_n_generated("patient")
#> [1] 8
envs[[1]] %>% get_capacity("doctor")
#> [1] 2
envs[[1]] %>% get_queue_size("doctor")
#> [1] Inf
head(
envs %>% get_mon_resources()
)
#> time server queue capacity queue_size system limit resource
#> 1 9.576992 1 0 1 Inf 1 Inf nurse
#> 2 20.064301 1 1 1 Inf 2 Inf nurse
#> 3 22.275208 1 0 1 Inf 1 Inf nurse
#> 4 29.278749 1 1 1 Inf 2 Inf nurse
#> 5 35.022816 1 0 1 Inf 1 Inf nurse
#> 6 40.993901 1 1 1 Inf 2 Inf nurse
#> replication
#> 1 1
#> 2 1
#> 3 1
#> 4 1
#> 5 1
#> 6 1
head(
envs %>% get_mon_arrivals()
)
#> name start_time end_time activity_time finished replication
#> 1 patient0 9.576992 47.00613 37.42914 TRUE 1
#> 2 patient1 20.064301 58.08972 35.81452 TRUE 1
#> 3 patient2 29.278749 77.97465 42.95184 TRUE 1
#> 4 patient0 10.174132 51.32508 41.15095 TRUE 2
#> 5 patient1 16.527268 61.82785 36.21454 TRUE 2
#> 6 patient2 25.024117 78.49536 39.15364 TRUE 2
Unfortunately, as the C++ simulation cores are destroyed, parallelization does not allow to resume the execution of replicas.
This package provides some basic visualization tools to help you take a glance at your simulations quickly, but of course you probably may want to extract the RAW data with the functions shown above and develop your own data analysis.
For instance, after you’ve left it simmering for a bit (pun intended), we can have a look at the overall resource utilization. The top and bottom of the error bars show respectively the 25th and 75th percentile of the utilization across all the replications. The top of the bar shows the median utilization.
plot_resource_utilization(envs, c("nurse", "doctor","administration"))
#> Warning: failed to assign NativeSymbolInfo for env since env is already
#> defined in the 'lazyeval' namespace
It is also possible to have a look at a specific resource and its activity during the simulation.
plot_resource_usage(envs, "doctor", items="server", steps=T)
In the above graph, the individual lines are all separate replications. The step lines are instantaneous utilization and the smooth line is a running average. Let’s take a look now at a specific replication. In the example below the 6th replication is shown.
plot_resource_usage(envs[[6]], "doctor", items="server", steps=T)
Next we can have a look at the evolution of the arrivals’ flow time during the simulation. In the below plot, each individual line represents a replication. A smooth line is drawn over them. All arrivals that didn’t finish their entire trajectory are excluded from the plot.
plot_evolution_arrival_times(envs, type = "flow_time")
Similarly one can have a look at the evolution of the activity times with type = "activity_time"
and waiting times with type = "waiting_time"
.