simmer.bricks
The simmer
package provides a rich and flexible API to
build discrete-event simulations. However, there are certain recurring
patterns that are typed over and over again. The most common example is
probably to spend some time holding a resource. Let us consider the
basic example from the Introduction
to simmer
:
library(simmer)
patient.1 <- 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)
These seize
> timeout
>
release
blocks can be substituted by the visit
verb, included in simmer.bricks
:
library(simmer.bricks)
patient.2 <- trajectory("patients' path") %>%
## add an intake activity
visit("nurse", function() rnorm(1, 15)) %>%
## add a consultation activity
visit("doctor", function() rnorm(1, 20)) %>%
## add a planning activity
visit("administration", function() rnorm(1, 5))
Internally, simmer.bricks
just uses simmer
verbs, so both trajectories are equivalent:
patient.1
#> trajectory: patients' path, 9 activities
#> { Activity: Seize | resource: nurse, amount: 1 }
#> { Activity: Timeout | delay: function() }
#> { Activity: Release | resource: nurse, amount: 1 }
#> { Activity: Seize | resource: doctor, amount: 1 }
#> { Activity: Timeout | delay: function() }
#> { Activity: Release | resource: doctor, amount: 1 }
#> { Activity: Seize | resource: administration, amount: 1 }
#> { Activity: Timeout | delay: function() }
#> { Activity: Release | resource: administration, amount: 1 }
patient.2
#> trajectory: patients' path, 9 activities
#> { Activity: Seize | resource: nurse, amount: 1 }
#> { Activity: Timeout | delay: function() }
#> { Activity: Release | resource: nurse, amount: 1 }
#> { Activity: Seize | resource: doctor, amount: 1 }
#> { Activity: Timeout | delay: function() }
#> { Activity: Release | resource: doctor, amount: 1 }
#> { Activity: Seize | resource: administration, amount: 1 }
#> { Activity: Timeout | delay: function() }
#> { Activity: Release | resource: administration, amount: 1 }
which means that you must have this in mind if you want to use a
rollback()
to loop over some part of the trajectory.
In summary, the simmer.bricks
package is a repository of
simmer
activity patterns like this one. See
help(package="simmer.bricks")
for a comprehensive list.
Some simulations require a resource to become inoperative for some
time after a release. It is possible to simulate this with
simmer
using a technique that we call delayed
release. Basically, while an arrival releases the resource and
continues the trajectory, a clone of the latter keeps the resource busy
for the time required; finally, the clone is removed. The main problem
is that this keeping the resource busy must be implemented in
different ways depending on the resource type, i.e., whether it is
preemptive or not.
This package encapsulates all this logic in a very easy-to-use brick
called delayed_release()
:
env <- simmer() %>%
add_resource("res1") %>%
add_resource("res2", preemptive=TRUE)
t <- trajectory() %>%
seize("res1") %>%
log_("res1 seized") %>%
seize("res2") %>%
log_("res2 seized") %>%
# inoperative for 2 units of time
delayed_release("res1", 2) %>%
log_("res1 released") %>%
# inoperative for 5 units of time
delayed_release("res2", 5, preemptive=TRUE) %>%
log_("res2 released")
env %>%
add_generator("dummy", t, at(0, 1)) %>%
run() %>% invisible
#> 0: dummy0: res1 seized
#> 0: dummy0: res2 seized
#> 0: dummy0: res1 released
#> 0: dummy0: res2 released
#> 2: dummy1: res1 seized
#> 5: dummy1: res2 seized
#> 5: dummy1: res1 released
#> 5: dummy1: res2 released
If you are curious, you can print the trajectory above to see what happens behind the scenes.
Another common pattern is to set up a number of parallel tasks with
clone()
. This could be challenging if the original arrival
had resources seized. Let us consider the following case, in which a
doctor and a nurse are visiting patients in a hospital room:
t <- trajectory() %>%
seize("room") %>%
clone(
n = 2,
trajectory("doctor") %>%
timeout(1),
trajectory("nurse") %>%
timeout(2)) %>%
synchronize(wait = TRUE) %>%
timeout(0.5) %>%
release("room",1)
simmer() %>%
add_resource("room") %>%
add_generator("visit", t, at(0)) %>%
run()
#> Error: 'visit0' at 2.50 in [Timeout]->Release->[]:
#> 'room' not previously seized
This simulation fails. This is because the original arrival, which
seized the room and follows the first path (doctor), finishes its duty
in the first place. Given that wait = TRUE
for the
synchronize()
activity, it means that the last clone to
arrive there (the nurse in this case) continues, while the others are
removed.
Solving this requires ensuring that the original arrival reaches the
synchronize()
activity in the last place (or in the first
place if wait = FALSE
), which can be tricky, as some
asynchronous programming must be used. However,
simmer.bricks
provides the do_parallel()
brick:
env <- simmer()
t <- trajectory() %>%
seize("room") %>%
log_("room seized") %>%
do_parallel(
trajectory("doctor") %>%
timeout(1) %>%
log_("doctor path done"),
trajectory("nurse") %>%
timeout(2) %>%
log_("nurse path done"),
.env = env
) %>%
timeout(0.5) %>%
release("room",1) %>%
log_("room released")
env %>%
add_resource("room") %>%
add_generator("visit", t, at(0)) %>%
run() %>% invisible
#> 0: visit0: room seized
#> 1: visit0: doctor path done
#> 2: visit0: nurse path done
#> 2.5: visit0: room released
And everything just works.
Assembly lines are chains of limited resources in which the current resource cannot be released until the next one is available. This class of problems can be solved with a pattern called interleaved resources. Such pattern uses auxiliary resources to guard the access to the second and subsequent resources in the chain, serving as a token to the guarded resource. As a consequence, if a resource is blocked for some reason, its tokens will exhaust eventually, and thus the blockage will propagate backwards.
Let us consider a chain of two machines, A and B, whose service times are 1 and 2 respectively. Then, the chain of resources can be set up as follows:
t <- trajectory() %>%
interleave(c("A", "B"), c(1, 2))
t
#> trajectory: anonymous, 8 activities
#> { Activity: Seize | resource: A, amount: 1 }
#> { Activity: Timeout | delay: 1 }
#> { Activity: Seize | resource: B_token, amount: 1 }
#> { Activity: Release | resource: A, amount: 1 }
#> { Activity: Seize | resource: B, amount: 1 }
#> { Activity: Timeout | delay: 2 }
#> { Activity: Release | resource: B, amount: 1 }
#> { Activity: Release | resource: B_token, amount: 1 }
As can be seen, the interleave
brick uses an auxiliary
resource called "B_token"
that must be defined too. If
machine B has capacity=1
and queue_size=1
,
then "B_token"
must have capacity=2
(B’s
capacity + queue size) and queue_size=Inf
, to avoid
dropping arrivals.
simmer() %>%
add_resource("A", 3, 1) %>%
add_resource("B_token", 2, Inf) %>%
add_resource("B", 1, 1) %>%
add_generator("dummy", t, at(rep(0, 3))) %>%
run(4) %>%
get_mon_arrivals(per_resource = TRUE)
#> name start_time end_time activity_time resource replication
#> 1 dummy0 0 1 1 A 1
#> 2 dummy1 0 1 1 A 1
#> 3 dummy0 1 3 2 B 1
#> 4 dummy0 1 3 2 B_token 1
#> 5 dummy2 0 3 1 A 1
In the simuation above, three arrivals are processed in machine A during 1 unit of time. Then the first two successfully seize a token to B, but the last arrival has to wait until one of them leave B before releasing A.
If you know about more patterns that you would like to see included
in simmer.bricks
, please, open an issue or a pull request
on GitHub.