library(simmer)
library(ggplot2)
When a generator creates an arrival, it couples the arrival to a given trajectory. A trajectory is defined as an interlinkage of activities which together form the arrivals’ lifetime in the system. Once an arrival is coupled to the trajectory, it will (in general) start processing the activities in the trajectory in the specified order and, eventually, leave the system. Consider the following:
patient_traj <- create_trajectory(name = "patient_trajectory") %>%
seize(resource = "doctor", amount = 1) %>%
timeout(task = 3) %>%
release(resource = "doctor", amount = 1)
Here we create a trajectory where a patient seizes a doctor for 3 minutes and then releases him again.
This is a very straightforward example, however, most of the trajectory-related functions allow for more advanced usage. The different functions are introduced below.
set_attribute
The set_attribute(., key, value)
function set the value
of an arrival’s attribute key
. Be aware that value
can only be numeric.
patient_traj <- create_trajectory(name = "patient_trajectory") %>%
set_attribute(key = "my_key", value = 123) %>%
timeout(5) %>%
set_attribute(key = "my_key", value = 456)
env <- simmer() %>%
add_generator("patient", patient_traj, at(0), mon = 2) %>%
run()
get_mon_attributes(env)
#> time name key value replication
#> 1 0 patient0 my_key 123 1
#> 2 5 patient0 my_key 456 1
Above, a trajectory which only sets attribute my_key
to value 123
is launched once by an arrival generated at time 0 (check ?at
). The mon=2
of add_generator
makes the simulation environment monitor the attributes’ evolution (disabled by default). Using get_mon_attributes
, we can look at the evolution of the value of my_key
.
If you want to set an attribute that depends on another attribute, or on the current value of the attribute to be set, this is also possible. In fact, if, instead of a numeric value, you supply a function with one parameter, the current set of attributes is passed as a list to that function. Whatever (numeric value) your function returns is set as the value of the specified attribute key. If the supplied function has no parameters, it is evaluated in the same way, but the attribute list is not accesible in the function body. This means that, if you supply a function to the value
parameter, it has to be in the form of either function(attrs){}
(first case) or function(){}
(second case). Below, you can see an example of this in practice.
patient_traj <- create_trajectory(name = "patient_trajectory") %>%
set_attribute("my_key", 123) %>%
timeout(5) %>%
set_attribute("my_key", function(attrs) attrs[["my_key"]] + 1) %>%
timeout(5) %>%
set_attribute("dependent_key", function(attrs) ifelse(attrs[["my_key"]]<=123, 1, 0)) %>%
timeout(5) %>%
set_attribute("independent_key", function() runif(1))
env<- simmer() %>%
add_generator("patient", patient_traj, at(0), mon = 2) %>%
run()
get_mon_attributes(env)
#> time name key value replication
#> 1 0 patient0 my_key 123.0000000 1
#> 2 5 patient0 my_key 124.0000000 1
#> 3 10 patient0 dependent_key 0.0000000 1
#> 4 15 patient0 independent_key 0.9547723 1
In general, whenever an activity accepts a function as a parameter, the rule above applies, and you can obtain the current set of attributes as the first argument of that function.
timeout
At its simplest, the timeout(., task)
function delays the arrival’s advance through the trajectory for a specified amount of time. Consider the following minimal example where we simply supply a static value to the timeout’s task
parameter.
patient_traj <- create_trajectory(name = "patient_trajectory") %>%
timeout(task = 3)
env <- simmer() %>%
add_generator("patient", patient_traj, at(0)) %>%
run()
get_mon_arrivals(env)
#> name start_time end_time activity_time finished replication
#> 1 patient0 0 3 3 TRUE 1
Often, however, you want a timeout to be dependent on a distribution or, for example, an earlier set attribute. This is achieved by passing a function in to form of either function(){}
or function(attrs){}
to the task
parameter, as we explained before. In the following example, this functionality is demonstrated:
patient_traj <- create_trajectory(name = "patient_trajectory") %>%
set_attribute("health", function() sample(20:80, 1)) %>%
# distribution-based timeout
timeout(function() rexp(1, 10)) %>%
# attribute-dependent timeout
timeout(function(attrs) (100 - attrs[["health"]]) * 2)
env <- simmer() %>%
add_generator("patient", patient_traj, at(0), mon = 2) %>%
run()
get_mon_arrivals(env)
#> name start_time end_time activity_time finished replication
#> 1 patient0 0 124.2508 124.2508 TRUE 1
get_mon_attributes(env)
#> time name key value replication
#> 1 0 patient0 health 38 1
Be aware that if you want the timeout
’s task
parameter to be evaluated dynamically, you should supply a callable function. For example in timeout(function() rexp(1, 10))
, rexp(1, 10)
will be evaluated every time the timeout activity is executed. However, if you supply it in the form of timeout(rexp(1, 10))
, it will only be evaluated at the initalization and will remain static after that.
Of course, this task
, supplied as a function, may be as complex as you need and, for instance, it may check a resource’s status, interact with other entities in your simulation model… The same applies to all the activities when they accept a function as a parameter.
seize
& release
The seize(., resource, amount)
function seizes a specified amount
of resources of type resource
. Conversely, the release(., resource, amount)
function releases a specified amount
of resource of type resource
. Be aware that, in order to use these functions in relation to a specific resource type, you have to create that resource type in your definition of the simulation environment (check ?add_resource
).
Consider the following example:
patient_traj <- create_trajectory(name = "patient_trajectory") %>%
seize(resource = "doctor", amount = 1) %>%
timeout(3) %>%
release(resource = "doctor", amount = 1)
env <- simmer() %>%
add_resource("doctor", capacity=1, mon = 1) %>%
add_generator("patient", patient_traj, at(0)) %>%
run()
get_mon_resources(env)
#> resource time server queue capacity queue_size system limit replication
#> 1 doctor 0 1 0 1 Inf 1 Inf 1
#> 2 doctor 3 0 0 1 Inf 0 Inf 1
Here the mon=1
argument (=default) of add_resource
makes the simulation environment monitor the resource usage. Using the get_mon_resources(env)
function you can get access to the log of the usage evolution of resources.
There are situations where you want to let the amount of resources seized/released be dependent on a specific function or on a previously set attribute. To achieve this, you can pass a function in the form of either function(){}
or function(attrs){}
to the amount
parameter instead of a numeric value. If going for the latter, the current state of the arrival’s attributes will be passed to attrs
as a list which you can inspect. This allows for the following:
patient_traj <- create_trajectory(name = "patient_trajectory") %>%
set_attribute("health", function() sample(20:80, 1)) %>%
set_attribute("docs_to_seize", function(attrs) ifelse(attrs[["health"]]<50, 1, 2)) %>%
seize("doctor", function(attrs) attrs[["docs_to_seize"]]) %>%
timeout(3) %>%
release("doctor", function(attrs) attrs[["docs_to_seize"]])
env <- simmer() %>%
add_resource("doctor", capacity = 2, mon = 1) %>%
add_generator("patient", patient_traj, at(0), mon = 2) %>%
run()
get_mon_resources(env)
#> resource time server queue capacity queue_size system limit replication
#> 1 doctor 0 1 0 2 Inf 1 Inf 1
#> 2 doctor 3 0 0 2 Inf 0 Inf 1
get_mon_attributes(env)
#> time name key value replication
#> 1 0 patient0 health 40 1
#> 2 0 patient0 docs_to_seize 1 1
By default, an unsuccessful seize
results in the rejection of the arrival. In the following example, the second patient tries to seize the only doctor while the first patient is being attended. There is no waiting room available, therefore it is rejected:
patient_traj <- create_trajectory(name = "patient_trajectory") %>%
seize("doctor", 1) %>%
# the second patient won't reach this point
timeout(5) %>%
release("doctor", 1)
env <- simmer() %>%
add_resource("doctor", capacity = 1, queue_size = 0) %>%
add_generator("patient", patient_traj, at(0, 1)) %>%
run()
get_mon_arrivals(env)
#> name start_time end_time activity_time finished replication
#> 1 patient1 1 1 0 FALSE 1
#> 2 patient0 0 5 5 TRUE 1
get_mon_resources(env)
#> resource time server queue capacity queue_size system limit replication
#> 1 doctor 0 1 0 1 0 1 1 1
#> 2 doctor 5 0 0 1 0 0 1 1
Sometimes, you don’t want to reject an unsuccessful seize
, but to follow another path. Let’s modify the example above to enable the second patient to visit a nurse instead:
patient_traj <- create_trajectory(name = "patient_trajectory") %>%
seize("doctor", 1, continue = FALSE,
reject = create_trajectory("rejected patient") %>%
seize("nurse", 1) %>%
timeout(2) %>%
release("nurse", 1)) %>%
# the second patient won't reach this point
timeout(5) %>%
release("doctor", 1)
env <- simmer() %>%
add_resource("doctor", capacity = 1, queue_size = 0) %>%
add_resource("nurse", capacity = 10, queue_size = 0) %>%
add_generator("patient", patient_traj, at(0, 1)) %>%
run()
get_mon_arrivals(env)
#> name start_time end_time activity_time finished replication
#> 1 patient1 1 3 2 TRUE 1
#> 2 patient0 0 5 5 TRUE 1
get_mon_resources(env)
#> resource time server queue capacity queue_size system limit replication
#> 1 doctor 0 1 0 1 0 1 1 1
#> 2 nurse 1 1 0 10 0 1 10 1
#> 3 nurse 3 0 0 10 0 0 10 1
#> 4 doctor 5 0 0 1 0 0 1 1
The flag continue
indicates whether the reject
sub-trajectory should be connected to the main trajectory or not. In this case, with continue=FALSE
, the rejected arrival seizes the nurse and its lifetime ends after releasing him/her. Otherwise, it would keep executing activities in the main trajectory.
Note that the second patient may also keep trying if he/she must see the doctor:
patient_traj <- create_trajectory(name = "patient_trajectory") %>%
seize("doctor", 1, continue = FALSE,
reject = create_trajectory("rejected patient") %>%
# go for a walk and try again
timeout(2) %>%
rollback(amount = 2, times = Inf)) %>%
# the second patient will reach this point after a couple of walks
timeout(5) %>%
release("doctor", 1)
env <- simmer() %>%
add_resource("doctor", capacity = 1, queue_size = 0) %>%
add_generator("patient", patient_traj, at(0, 1)) %>%
run()
get_mon_arrivals(env)
#> name start_time end_time activity_time finished replication
#> 1 patient0 0 5 5 TRUE 1
#> 2 patient1 1 10 9 TRUE 1
get_mon_resources(env)
#> resource time server queue capacity queue_size system limit replication
#> 1 doctor 0 1 0 1 0 1 1 1
#> 2 doctor 5 0 0 1 0 0 1 1
#> 3 doctor 5 1 0 1 0 1 1 1
#> 4 doctor 10 0 0 1 0 0 1 1
There is another optional sub-trajectory called post.seize
and, as its name suggests, it is executed after a successful seize
. Thus, you can do the following:
patient_traj <- create_trajectory(name = "patient_trajectory") %>%
seize("doctor", 1, continue = c(TRUE, TRUE),
post.seize = create_trajectory("admitted patient") %>%
timeout(5) %>%
release("doctor", 1),
reject = create_trajectory("rejected patient") %>%
seize("nurse", 1) %>%
timeout(2) %>%
release("nurse", 1)) %>%
# both patients will reach this point, as continue = c(TRUE, TRUE)
timeout(10)
env <- simmer() %>%
add_resource("doctor", capacity = 1, queue_size = 0) %>%
add_resource("nurse", capacity = 10, queue_size = 0) %>%
add_generator("patient", patient_traj, at(0, 1)) %>%
run()
get_mon_arrivals(env)
#> name start_time end_time activity_time finished replication
#> 1 patient1 1 13 12 TRUE 1
#> 2 patient0 0 15 15 TRUE 1
get_mon_resources(env)
#> resource time server queue capacity queue_size system limit replication
#> 1 doctor 0 1 0 1 0 1 1 1
#> 2 nurse 1 1 0 10 0 1 10 1
#> 3 nurse 3 0 0 10 0 0 10 1
#> 4 doctor 5 0 0 1 0 0 1 1
select
, seize_selected
& release_selected
seize
and release
work well when you know the resources implied beforehand. But sometimes the resource to choose may depend on a certain policy. For these situations, the select(., resources, policy, id)
method offers the possibility of selecting a resource at any point, and this choice will be observed by seize_selected
and release_selected
:
patient_traj <- create_trajectory(name = "patient_trajectory") %>%
select(resources = c("doctor1", "doctor2", "doctor3"), policy = "round-robin") %>%
seize_selected(amount = 1) %>%
timeout(5) %>%
release_selected(amount = 1)
env <- simmer() %>%
add_resource("doctor1", capacity = 1) %>%
add_resource("doctor2", capacity = 1) %>%
add_resource("doctor3", capacity = 1) %>%
add_generator("patient", patient_traj, at(0, 1, 2)) %>%
run()
get_mon_arrivals(env)
#> name start_time end_time activity_time finished replication
#> 1 patient0 0 5 5 TRUE 1
#> 2 patient1 1 6 5 TRUE 1
#> 3 patient2 2 7 5 TRUE 1
get_mon_resources(env)
#> resource time server queue capacity queue_size system limit replication
#> 1 doctor1 0 1 0 1 Inf 1 Inf 1
#> 2 doctor2 1 1 0 1 Inf 1 Inf 1
#> 3 doctor3 2 1 0 1 Inf 1 Inf 1
#> 4 doctor1 5 0 0 1 Inf 0 Inf 1
#> 5 doctor2 6 0 0 1 Inf 0 Inf 1
#> 6 doctor3 7 0 0 1 Inf 0 Inf 1
If you provide select
with resources
as a vector of names, you can use one of the predefined policies (see ?select
). If you need some custom policy, you can define it and supply it as a function. For instance, let’s pick a resource based on a previously set attribute:
patient_traj <- create_trajectory(name = "patient_trajectory") %>%
set_attribute("resource", function() sample(1:3, 1)) %>%
select(resources = function(attrs) paste0("doctor", attrs["resource"])) %>%
seize_selected(amount = 1) %>%
timeout(5) %>%
release_selected(amount = 1)
env <- simmer() %>%
add_resource("doctor1", capacity = 1) %>%
add_resource("doctor2", capacity = 1) %>%
add_resource("doctor3", capacity = 1) %>%
add_generator("patient", patient_traj, at(0, 1, 2), mon = 2) %>%
run()
get_mon_attributes(env)
#> time name key value replication
#> 1 0 patient0 resource 1 1
#> 2 1 patient1 resource 1 1
#> 3 2 patient2 resource 1 1
get_mon_arrivals(env)
#> name start_time end_time activity_time finished replication
#> 1 patient0 0 5 5 TRUE 1
#> 2 patient1 1 10 5 TRUE 1
#> 3 patient2 2 15 5 TRUE 1
get_mon_resources(env)
#> resource time server queue capacity queue_size system limit replication
#> 1 doctor1 0 1 0 1 Inf 1 Inf 1
#> 2 doctor1 1 1 1 1 Inf 2 Inf 1
#> 3 doctor1 2 1 2 1 Inf 3 Inf 1
#> 4 doctor1 5 1 1 1 Inf 2 Inf 1
#> 5 doctor1 10 1 0 1 Inf 1 Inf 1
#> 6 doctor1 15 0 0 1 Inf 0 Inf 1
And, of course, everything learned for seize
and release
applies to seize_selected
and release_selected
.
set_prioritization
The add_generator
method assigns a set of prioritization values to each generated arrival: by default, priority=0
, preemptible=priority
, restart=FALSE
(see ?add_generator
for more details). The set_prioritization(., values)
method can change those values with more granularity at any point in the trajectory:
patient_traj <- create_trajectory(name = "patient_trajectory") %>%
set_attribute("priority", 3) %>%
# static values
set_prioritization(values = c(3, 7, TRUE)) %>%
# dynamically with a function
set_prioritization(values = function(attrs) c(attrs["priority"], 7, TRUE))
More details on prioritization in the Advanced Resource Usage vignette (pending).
branch
The branch(., option, continue, ...)
method offers the possibility of adding alternative paths in the trajectory. The following example shows how a trajectory can be built with a 50-50 chance for an arrival to pass through each path of a two-path branch.
t1 <- create_trajectory("trajectory with a branch") %>%
seize("server", 1) %>%
branch(option = function() sample(1:2, 1), continue = c(T, F),
create_trajectory("branch1") %>%
timeout(function() 1),
create_trajectory("branch2") %>%
timeout(function() rexp(1, 3)) %>%
release("server", 1)
) %>%
release("server", 1)
When an arrival gets to the branch, the first argument is evaluated to select a specific path to follow, so it must be callable and must return a numeric value between 1 and n
, where n
is the number of paths defined. The second argument, continue
, indicates whether the arrival must continue executing the activities after the selected path or not. In the example above, only the first path continues to the last release.
Sometimes you may need to count how many times a certain trajectory in a certain branch is entered, or how much time arrivals spend inside that trajectory. For these situations, it is handy to use resources with infinite capacity just for accounting purposes, like in the example below.
t0 <- create_trajectory() %>%
branch(function() sample(c(1, 2), 1), continue = c(T, T),
create_trajectory() %>%
seize("branch1", 1) %>%
# do stuff here
timeout(function() rexp(1, 1)) %>%
release("branch1", 1),
create_trajectory() %>%
seize("branch2", 1) %>%
# do stuff here
timeout(function() rexp(1, 1/2)) %>%
release("branch2", 1))
env <- simmer() %>%
add_generator("dummy", t0, at(rep(0, 1000))) %>%
# Resources with infinite capacity, just for accounting purposes
add_resource("branch1", Inf) %>%
add_resource("branch2", Inf) %>%
run()
arrivals <- get_mon_arrivals(env, per_resource = T)
# Times that each branch was entered
table(arrivals$resource)
#>
#> branch1 branch2
#> 510 490
# The `activity_time` is the total time inside each branch for each arrival
# Let's see the distributions
ggplot(arrivals) + geom_histogram(aes(x=activity_time)) + facet_wrap(~resource)
rollback
The rollback(., amount, times, check)
function allows an arrival to rollback the trajectory an amount
number of steps.
Consider the following where a string is printed in the timeout function. After the first run, the trajectory is rolled back 3 times.
t0 <- create_trajectory() %>%
timeout(function() { print("Hello!"); return(0) }) %>%
rollback(amount = 1, times = 3)
simmer() %>%
add_generator("hello_sayer", t0, at(0)) %>%
run()
#> [1] "Hello!"
#> [1] "Hello!"
#> [1] "Hello!"
#> [1] "Hello!"
#> simmer environment: anonymous | now: 0 | next:
#> { Generator: hello_sayer | monitored: 1 | n_generated: 1 }
The rollback
function also accepts an optional check
parameter which overrides the default amount-based behaviour. This parameter must be a function that returns a logical value. Each time an arrival reaches the activity, this check
is evaluated to determine whether the rollback
with amount
steps must be performed or not. Consider the following example:
t0 <- create_trajectory() %>%
set_attribute("happiness", 0) %>%
# the timeout function is used simply to print something and returns 0,
# hence it is a dummy timeout
timeout(function(attrs){
cat(">> Happiness level is at: ", attrs[["happiness"]], " -- ")
cat(ifelse(attrs[["happiness"]]<25,"PETE: I'm feeling crappy...",
ifelse(attrs[["happiness"]]<50,"PETE: Feelin' a bit moody",
ifelse(attrs[["happiness"]]<75,"PETE: Just had a good espresso",
"PETE: Let's do this! (and stop this loop...)")))
, "\n")
return(0)
}) %>%
set_attribute("happiness", function(attrs) attrs[["happiness"]] + 25) %>%
rollback(amount = 2, check = function(attrs) attrs[["happiness"]] < 100)
simmer() %>%
add_generator("mood_swinger", t0, at(0)) %>%
run()
#> >> Happiness level is at: 0 -- PETE: I'm feeling crappy...
#> >> Happiness level is at: 25 -- PETE: Feelin' a bit moody
#> >> Happiness level is at: 50 -- PETE: Just had a good espresso
#> >> Happiness level is at: 75 -- PETE: Let's do this! (and stop this loop...)
#> simmer environment: anonymous | now: 0 | next:
#> { Generator: mood_swinger | monitored: 1 | n_generated: 1 }
leave
The leave(., prob)
method allows an arrival to leave the trajectory with some probability:
patient_traj <- create_trajectory(name = "patient_trajectory") %>%
seize("nurse", 1) %>%
timeout(3) %>%
release("nurse", 1) %>%
leave(prob = 1) %>%
# patients will never seize the doctor
seize("doctor", 1) %>%
timeout(3) %>%
release("doctor", 1)
env <- simmer() %>%
add_resource("nurse", capacity=1) %>%
add_resource("doctor", capacity=1) %>%
add_generator("patient", patient_traj, at(0)) %>%
run()
get_mon_resources(env)
#> resource time server queue capacity queue_size system limit replication
#> 1 nurse 0 1 0 1 Inf 1 Inf 1
#> 2 nurse 3 0 0 1 Inf 0 Inf 1
And of course, this probability may be evaluated dynamically also:
set.seed(1234)
patient_traj <- create_trajectory(name = "patient_trajectory") %>%
seize("nurse", 1) %>%
timeout(3) %>%
release("nurse", 1) %>%
leave(prob = function() runif(1) < 0.5) %>%
# some patients will seize the doctor
seize("doctor", 1) %>%
timeout(3) %>%
release("doctor", 1)
env <- simmer() %>%
add_resource("nurse", capacity=1) %>%
add_resource("doctor", capacity=1) %>%
add_generator("patient", patient_traj, at(0, 1)) %>%
run()
get_mon_arrivals(env)
#> name start_time end_time activity_time finished replication
#> 1 patient0 0 3 3 FALSE 1
#> 2 patient1 1 9 6 TRUE 1
get_mon_resources(env)
#> resource time server queue capacity queue_size system limit replication
#> 1 nurse 0 1 0 1 Inf 1 Inf 1
#> 2 nurse 1 1 1 1 Inf 2 Inf 1
#> 3 nurse 3 1 0 1 Inf 1 Inf 1
#> 4 nurse 6 0 0 1 Inf 0 Inf 1
#> 5 doctor 6 1 0 1 Inf 1 Inf 1
#> 6 doctor 9 0 0 1 Inf 0 Inf 1
renege_in
& renege_abort
The renege_in(., t, out)
method offers the possibility of setting a timeout after which the arrival will abandon the trajectory. After reneging, the arrival can follow an optional sub-trajectory out
. The renege_abort(.)
method cancels a previously established timeout. Together, they allows us, for instance, to model arrivals with limited patience. In the example below, customer1
arrives at the bank, where there is only one busy clerk. He/she waits in the queue for 5 minutes and then leaves.
t <- create_trajectory(name = "bank") %>%
timeout(function() { print("Here I am"); 0 } ) %>%
# renege in 5 minutes
renege_in(5,
out = create_trajectory() %>%
timeout(function() { print("Lost my patience. Reneging..."); 0 } )) %>%
seize("clerk", 1) %>%
# stay if I'm being attended within 5 minutes
renege_abort() %>%
timeout(function() { print("I'm being attended"); 0 } ) %>%
timeout(10) %>%
release("clerk", 1) %>%
timeout(function() { print("Finished"); 0 } )
env <- simmer(verbose = TRUE) %>%
add_resource("clerk", 1) %>%
add_generator("customer", t, at(0, 1)) %>%
run()
#> 0 | generator: customer | new: customer0 | 0
#> 0 | generator: customer | new: customer1 | 1
#> 0 | arrival: customer0 | activity: Timeout | 0x55d6232e5750
#> [1] "Here I am"
#> 0 | arrival: customer0 | activity: RenegeIn | 5, 1 paths
#> 0 | arrival: customer0 | activity: Seize | clerk, 1, 0 paths
#> 0 | resource: clerk | arrival: customer0 | SERVE
#> 0 | arrival: customer0 | activity: RenegeAbort |
#> 0 | arrival: customer0 | activity: Timeout | 0x55d623176358
#> [1] "I'm being attended"
#> 0 | arrival: customer0 | activity: Timeout | 10
#> 1 | arrival: customer1 | activity: Timeout | 0x55d6232e5750
#> [1] "Here I am"
#> 1 | arrival: customer1 | activity: RenegeIn | 5, 1 paths
#> 1 | arrival: customer1 | activity: Seize | clerk, 1, 0 paths
#> 1 | resource: clerk | arrival: customer1 | ENQUEUE
#> 6 | task: Renege-Timer | |
#> 6 | resource: clerk | arrival: customer1 | DEPART
#> 6 | arrival: customer1 | activity: Timeout | 0x55d6231a9980
#> [1] "Lost my patience. Reneging..."
#> 10 | arrival: customer0 | activity: Release | clerk, 1
#> 10 | resource: clerk | arrival: customer0 | DEPART
#> 10 | task: Post-Release | |
#> 10 | arrival: customer0 | activity: Timeout | 0x55d623c03b58
#> [1] "Finished"
clone
& synchronize
The clone(., n, ...)
method offers the possibility of replicating an arrival n-1
times to be processed through up to n
sub-trajectories in parallel. Then, the synchronize(., wait, mon_all)
method synchronizes and removes replicas. By default, synchronize
waits for all of the replicas to arrive and allows the last one to continue:
t <- create_trajectory() %>%
clone(n = 3,
create_trajectory("original") %>%
timeout(1),
create_trajectory("clone 1") %>%
timeout(2),
create_trajectory("clone 2") %>%
timeout(3)) %>%
synchronize(wait = TRUE) %>%
timeout(0.5)
env <- simmer(verbose = TRUE) %>%
add_generator("arrival", t, at(0)) %>%
run()
#> 0 | generator: arrival | new: arrival0 | 0
#> 0 | arrival: arrival0 | activity: Clone | 3, 3 paths
#> 0 | arrival: arrival0 | activity: Timeout | 2
#> 0 | arrival: arrival0 | activity: Timeout | 3
#> 0 | arrival: arrival0 | activity: Timeout | 1
#> 1 | arrival: arrival0 | activity: Synchronize | 1
#> 2 | arrival: arrival0 | activity: Synchronize | 1
#> 3 | arrival: arrival0 | activity: Synchronize | 1
#> 3 | arrival: arrival0 | activity: Timeout | 0.5
get_mon_arrivals(env)
#> name start_time end_time activity_time finished replication
#> 1 arrival0 0 3.5 3.5 TRUE 1
Note that the parameter n
may also be a function. If there are more sub-trajectories than clones, the extra ones are ignored. If there are less sub-trajectories than clones, some clones will continue to the next activity directly:
t <- create_trajectory() %>%
clone(n = 3,
create_trajectory("original") %>%
timeout(1),
create_trajectory("clone 1") %>%
timeout(2)) %>%
synchronize(wait = TRUE) %>%
timeout(0.5)
env <- simmer(verbose = TRUE) %>%
add_generator("arrival", t, at(0)) %>%
run()
#> 0 | generator: arrival | new: arrival0 | 0
#> 0 | arrival: arrival0 | activity: Clone | 3, 2 paths
#> 0 | arrival: arrival0 | activity: Timeout | 2
#> 0 | arrival: arrival0 | activity: Synchronize | 1
#> 0 | arrival: arrival0 | activity: Timeout | 1
#> 1 | arrival: arrival0 | activity: Synchronize | 1
#> 2 | arrival: arrival0 | activity: Synchronize | 1
#> 2 | arrival: arrival0 | activity: Timeout | 0.5
get_mon_arrivals(env)
#> name start_time end_time activity_time finished replication
#> 1 arrival0 0 2.5 2.5 TRUE 1
The behaviour of synchronize
can be modified in order to let the first clone pass and remove the others by setting wait=FALSE
:
t <- create_trajectory() %>%
clone(n = 3,
create_trajectory("original") %>%
timeout(1),
create_trajectory("clone 1") %>%
timeout(2),
create_trajectory("clone 2") %>%
timeout(3)) %>%
synchronize(wait = FALSE) %>%
timeout(0.5)
env <- simmer(verbose = TRUE) %>%
add_generator("arrival", t, at(0)) %>%
run()
#> 0 | generator: arrival | new: arrival0 | 0
#> 0 | arrival: arrival0 | activity: Clone | 3, 3 paths
#> 0 | arrival: arrival0 | activity: Timeout | 2
#> 0 | arrival: arrival0 | activity: Timeout | 3
#> 0 | arrival: arrival0 | activity: Timeout | 1
#> 1 | arrival: arrival0 | activity: Synchronize | 0
#> 1 | arrival: arrival0 | activity: Timeout | 0.5
#> 2 | arrival: arrival0 | activity: Synchronize | 0
#> 3 | arrival: arrival0 | activity: Synchronize | 0
get_mon_arrivals(env)
#> name start_time end_time activity_time finished replication
#> 1 arrival0 0 1.5 1.5 TRUE 1
By default, synchronize
does not record information about the clones removed (mon_all=FALSE
). However, if it is required, you can get it by setting mon_all=TRUE
:
t <- create_trajectory() %>%
clone(n = 3,
create_trajectory("original") %>%
timeout(1),
create_trajectory("clone 1") %>%
timeout(2),
create_trajectory("clone 2") %>%
timeout(3)) %>%
synchronize(wait = FALSE, mon_all = TRUE) %>%
timeout(0.5)
env <- simmer(verbose = TRUE) %>%
add_generator("arrival", t, at(0)) %>%
run()
#> 0 | generator: arrival | new: arrival0 | 0
#> 0 | arrival: arrival0 | activity: Clone | 3, 3 paths
#> 0 | arrival: arrival0 | activity: Timeout | 2
#> 0 | arrival: arrival0 | activity: Timeout | 3
#> 0 | arrival: arrival0 | activity: Timeout | 1
#> 1 | arrival: arrival0 | activity: Synchronize | 0
#> 1 | arrival: arrival0 | activity: Timeout | 0.5
#> 2 | arrival: arrival0 | activity: Synchronize | 0
#> 3 | arrival: arrival0 | activity: Synchronize | 0
get_mon_arrivals(env)
#> name start_time end_time activity_time finished replication
#> 1 arrival0 0 1.5 1.5 TRUE 1
#> 2 arrival0 0 2.0 2.0 TRUE 1
#> 3 arrival0 0 3.0 3.0 TRUE 1
batch
& separate
The batch(., n, timeout, permanent, name, rule)
method offers the possibility of collecting a number of arrivals before they can continue processing as a block. Then, the separate(.)
method splits a previously established non-permanent batch. This allows us to implement a rollercoaster process, for instance.
Let us consider a rollercoaster, with up to 10 places and a queue of 20 people, that lasts 5 minutes. We can model this problem as follows:
set.seed(1234)
t <- create_trajectory() %>%
batch(10, timeout = 5, permanent = FALSE) %>%
seize("rollercoaster", 1) %>%
timeout(5) %>%
release("rollercoaster", 1) %>%
separate()
env <- simmer() %>%
# capacity and queue_size are defined in batches of 10
add_resource("rollercoaster", capacity = 1, queue_size = 2) %>%
add_generator("person", t, function() rexp(1, 2)) %>%
run(15)
get_mon_arrivals(env, per_resource = TRUE)
#> name start_time end_time activity_time resource replication
#> 1 person0 3.800074 8.800074 5 rollercoaster 1
#> 2 person1 3.800074 8.800074 5 rollercoaster 1
#> 3 person2 3.800074 8.800074 5 rollercoaster 1
#> 4 person3 3.800074 8.800074 5 rollercoaster 1
#> 5 person4 3.800074 8.800074 5 rollercoaster 1
#> 6 person5 3.800074 8.800074 5 rollercoaster 1
#> 7 person6 3.800074 8.800074 5 rollercoaster 1
#> 8 person7 3.800074 8.800074 5 rollercoaster 1
#> 9 person8 3.800074 8.800074 5 rollercoaster 1
#> 10 person9 3.800074 8.800074 5 rollercoaster 1
#> 11 person10 8.800074 13.800074 5 rollercoaster 1
#> 12 person11 8.800074 13.800074 5 rollercoaster 1
#> 13 person12 8.800074 13.800074 5 rollercoaster 1
#> 14 person13 8.800074 13.800074 5 rollercoaster 1
#> 15 person14 8.800074 13.800074 5 rollercoaster 1
#> 16 person15 8.800074 13.800074 5 rollercoaster 1
We can see above that 3 batches have been created. The first 10 people arrive within 3.8 minutes and goes into the rollercoaster. When the ride ends, at 8.8, there are only 6 people waiting, but the batch
timer (timeout=5
) has run out, and another ride starts with them. These batches are non-permanent (permanent=FALSE
), so that separate
can split them and people can go their separate ways.
The optional argument rule
accepts a function to perform a fine-grained selection of which arrivals should be batched. For each particular arrival, it is batched if the function returns TRUE
, or it simply continues otherwise. For instance, in the example above, we can prevent batching by returning always FALSE
:
t_batch <- create_trajectory() %>%
batch(10, timeout = 5, permanent = FALSE, rule = function() FALSE) %>%
seize("rollercoaster", 1) %>%
timeout(5) %>%
release("rollercoaster", 1) %>%
separate()
t_nobatch <- create_trajectory() %>%
seize("rollercoaster", 1) %>%
timeout(5) %>%
release("rollercoaster", 1)
set.seed(1234)
env_batch <- simmer() %>%
# capacity and queue_size are defined in batches of 10
add_resource("rollercoaster", capacity = 1, queue_size = 2) %>%
add_generator("person", t_batch, function() rexp(1, 2)) %>%
run(15)
set.seed(1234)
env_nobatch <- simmer() %>%
# capacity and queue_size are defined in batches of 10
add_resource("rollercoaster", capacity = 1, queue_size = 2) %>%
add_generator("person", t_nobatch, function() rexp(1, 2)) %>%
run(15)
get_mon_arrivals(env_batch, per_resource = TRUE)
#> name start_time end_time activity_time resource replication
#> 1 person0 1.250879 6.250879 5 rollercoaster 1
#> 2 person1 1.374259 11.250879 5 rollercoaster 1
get_mon_arrivals(env_nobatch, per_resource = TRUE)
#> name start_time end_time activity_time resource replication
#> 1 person0 1.250879 6.250879 5 rollercoaster 1
#> 2 person1 1.374259 11.250879 5 rollercoaster 1
By default, batches are unnamed (name=""
), which makes them independent of one another. However, it may be interesting to feed a common batch from different trajectories. For instance, we can try this:
t0 <- create_trajectory() %>%
batch(2) %>%
timeout(2) %>%
separate()
t1 <- create_trajectory() %>%
timeout(1) %>%
join(t0)
env <- simmer(verbose = TRUE) %>%
add_generator("t0_", t0, at(0)) %>%
add_generator("t1_", t1, at(0)) %>%
run()
#> 0 | generator: t0_ | new: t0_0 | 0
#> 0 | generator: t1_ | new: t1_0 | 0
#> 0 | arrival: t0_0 | activity: Batch | 2, 0, 0,
#> 0 | arrival: t1_0 | activity: Timeout | 1
#> 1 | arrival: t1_0 | activity: Batch | 2, 0, 0,
get_mon_arrivals(env)
#> [1] name start_time end_time activity_time finished
#> <0 rows> (or 0-length row.names)
But we don’t get the expected output because the arrivals are feeding two different batches. The arrival following t1
join t0
after the timeout, but effectively this is a clone of t0
, which means that the above definition is equivalent to the following:
t0 <- create_trajectory() %>%
batch(2) %>%
timeout(2) %>%
separate()
t1 <- create_trajectory() %>%
timeout(1) %>%
batch(2) %>%
timeout(2) %>%
separate()
Thus, arrivals following a different trajectory will end up in a different batch in general. Nonetheless, there is one way to share a common batch across batch
activities. This can be done by using a common name:
t0 <- create_trajectory() %>%
batch(2, name = "mybatch") %>%
timeout(2) %>%
separate()
t1 <- create_trajectory() %>%
timeout(1) %>%
batch(2, name = "mybatch") %>%
timeout(2) %>%
separate()
env <- simmer(verbose = TRUE) %>%
add_generator("t0_", t0, at(0)) %>%
add_generator("t1_", t1, at(0)) %>%
run()
#> 0 | generator: t0_ | new: t0_0 | 0
#> 0 | generator: t1_ | new: t1_0 | 0
#> 0 | arrival: t0_0 | activity: Batch | 2, 0, 0, mybatch
#> 0 | arrival: t1_0 | activity: Timeout | 1
#> 1 | arrival: t1_0 | activity: Batch | 2, 0, 0, mybatch
#> 1 | arrival: batch_mybatch | activity: Timeout | 2
#> 3 | arrival: batch_mybatch | activity: Separate |
get_mon_arrivals(env)
#> name start_time end_time activity_time finished replication
#> 1 t0_0 0 3 2 TRUE 1
#> 2 t1_0 0 3 3 TRUE 1
Or, equivalently,
t0 <- create_trajectory() %>%
batch(2, name = "mybatch") %>%
timeout(2) %>%
separate()
t1 <- create_trajectory() %>%
timeout(1) %>%
join(t0)
env <- simmer(verbose = TRUE) %>%
add_generator("t0_", t0, at(0)) %>%
add_generator("t1_", t1, at(0)) %>%
run()
#> 0 | generator: t0_ | new: t0_0 | 0
#> 0 | generator: t1_ | new: t1_0 | 0
#> 0 | arrival: t0_0 | activity: Batch | 2, 0, 0, mybatch
#> 0 | arrival: t1_0 | activity: Timeout | 1
#> 1 | arrival: t1_0 | activity: Batch | 2, 0, 0, mybatch
#> 1 | arrival: batch_mybatch | activity: Timeout | 2
#> 3 | arrival: batch_mybatch | activity: Separate |
get_mon_arrivals(env)
#> name start_time end_time activity_time finished replication
#> 1 t0_0 0 3 2 TRUE 1
#> 2 t1_0 0 3 3 TRUE 1
It is possible to concatenate together any number of trajectories using the join(...)
verb. It may be used as a standalone function as follows:
t1 <- create_trajectory() %>% seize("dummy", 1)
t2 <- create_trajectory() %>% timeout(1)
t3 <- create_trajectory() %>% release("dummy", 1)
t0 <- join(t1, t2, t3)
t0
#> simmer trajectory: anonymous, 3 activities
#> { Activity: Seize | resource: dummy | amount: 1 }
#> { Activity: Timeout | delay: 1 }
#> { Activity: Release | resource: dummy | amount: 1 }
Or it may operate inline, like another activity:
t0 <- create_trajectory() %>%
join(t1) %>%
timeout(1) %>%
join(t3)
t0
#> simmer trajectory: anonymous, 3 activities
#> { Activity: Seize | resource: dummy | amount: 1 }
#> { Activity: Timeout | delay: 1 }
#> { Activity: Release | resource: dummy | amount: 1 }
It is possible to interact with the simulation environment in order to extract parameters of interest such as the current simulation time (now()
), status of resources (get_capacity
, get_queue_size
, get_server_count
, get_queue_count
), status of generators (get_n_generated
) or directly to gather the history of monitored values (get_mon_*
). You may also want (or in other words, your model may need) to check and use all this information to take decisions inside a given trajectory.
For instance, let’s suppose we just want to print the simulation time at a given point in a trajectory. The only requirement is that you must define the simulation environment before running the simulation. This won’t work:
remove(env)
t <- create_trajectory() %>%
timeout(function() print(env %>% now()))
env <- simmer() %>%
add_generator("dummy", t, function() 1) %>%
run(4)
#> Error in eval(expr, envir, enclos): objeto 'env' no encontrado
Because the global env
is not available at runtime: the simulation runs and then the resulting object is assigned to env
. We need to assign first, then run. So this will work:
t <- create_trajectory() %>%
timeout(function() print(env %>% now()))
env <- simmer() %>%
add_generator("dummy", t, function() 1)
env %>% run(4)
#> [1] 1
#> [1] 2
#> [1] 3
#> simmer environment: anonymous | now: 4 | next: 4
#> { Generator: dummy | monitored: 1 | n_generated: 5 }
And we get the expected output. However, as a general rule of good practice, it is recommended to instantiate the environment always in the first place, to avoid possible mistakes and because the code becomes more readable:
# First, instantiate the environment
env <- simmer()
# Here I'm using it
t <- create_trajectory() %>%
timeout(function() print(env %>% now()))
# And finally, run it
env %>%
add_generator("dummy", t, function() 1) %>%
run(4)
#> [1] 1
#> [1] 2
#> [1] 3
#> simmer environment: anonymous | now: 4 | next: 4
#> { Generator: dummy | monitored: 1 | n_generated: 5 }