These examples are adapted from the Python package ‘SimPy’, here. Users familiar with SimPy may find these examples helpful for transitioning to simmer
. Some very basic material is not covered. Beginners should first read The Bank Tutorial: Part I & Part II.
Covers:
From here:
A carwash has a limited number of washing machines and defines a washing process that takes some time. Cars arrive at the carwash at a random time. If one washing machine is available, they start the washing process and wait for it to finish. If not, they wait until they an use one.
Parameters and setup:
library(simmer)
<- 2 # Number of machines in the carwash
NUM_MACHINES <- 5 # Minutes it takes to clean a car
WASHTIME <- 7 # Create a car every ~7 minutes
T_INTER <- 20 # Simulation time in minutes
SIM_TIME
# setup
set.seed(42)
<- simmer() env
The implementation with simmer
is as simple as defining the trajectory each arriving car
will share and follow. This trajectory comprises seizing a wash machine, spending some time and releasing it. The process takes WASHTIME
and removes some random percentage of dirt between 50 and 90%. The log_()
messages are merely illustrative.
<- trajectory() %>%
car log_("arrives at the carwash") %>%
seize("wash", 1) %>%
log_("enters the carwash") %>%
timeout(WASHTIME) %>%
set_attribute("dirt_removed", function() sample(50:99, 1)) %>%
log_(function()
paste0(get_attribute(env, "dirt_removed"), "% of dirt was removed")) %>%
release("wash", 1) %>%
log_("leaves the carwash")
Finally, we setup the resources and feed the trajectory with a couple of generators. The result is shown below:
%>%
env add_resource("wash", NUM_MACHINES) %>%
# feed the trajectory with 4 initial cars
add_generator("car_initial", car, at(rep(0, 4))) %>%
# new cars approx. every T_INTER minutes
add_generator("car", car, function() sample((T_INTER-2):(T_INTER+2), 1)) %>%
# start the simulation
run(SIM_TIME)
#> 0: car_initial0: arrives at the carwash
#> 0: car_initial0: enters the carwash
#> 0: car_initial1: arrives at the carwash
#> 0: car_initial1: enters the carwash
#> 0: car_initial2: arrives at the carwash
#> 0: car_initial3: arrives at the carwash
#> 5: car_initial0: 86% of dirt was removed
#> 5: car_initial1: 50% of dirt was removed
#> 5: car_initial0: leaves the carwash
#> 5: car_initial1: leaves the carwash
#> 5: car0: arrives at the carwash
#> 5: car_initial2: enters the carwash
#> 5: car_initial3: enters the carwash
#> 10: car_initial2: 59% of dirt was removed
#> 10: car_initial3: 85% of dirt was removed
#> 10: car_initial2: leaves the carwash
#> 10: car_initial3: leaves the carwash
#> 10: car0: enters the carwash
#> 10: car1: arrives at the carwash
#> 10: car1: enters the carwash
#> 15: car0: 98% of dirt was removed
#> 15: car1: 96% of dirt was removed
#> 15: car0: leaves the carwash
#> 15: car1: leaves the carwash
#> 16: car2: arrives at the carwash
#> 16: car2: enters the carwash
#> simmer environment: anonymous | now: 20 | next: 21
#> { Monitor: in memory }
#> { Resource: wash | monitored: TRUE | server status: 1(2) | queue status: 0(Inf) }
#> { Source: car_initial | monitored: 1 | n_generated: 4 }
#> { Source: car | monitored: 1 | n_generated: 4 }
Covers:
From here:
This example comprises a workshop with
n
identical machines. A stream of jobs (enough to keep the machines busy) arrives. Each machine breaks down periodically. Repairs are carried out by one repairman. The repairman has other, less important tasks to perform, too. Broken machines preempt theses tasks. The repairman continues them when he is done with the machine repair. The workshop works continuously.
First of all, we load the libraries and prepare the environment.
library(simmer)
<- 10.0 # Avg. processing time in minutes
PT_MEAN <- 2.0 # Sigma of processing time
PT_SIGMA <- 300.0 # Mean time to failure in minutes
MTTF <- 1 / MTTF # Param. for exponential distribution
BREAK_MEAN <- 30.0 # Time it takes to repair a machine in minutes
REPAIR_TIME <- 30.0 # Duration of other jobs in minutes
JOB_DURATION <- 10 # Number of machines in the machine shop
NUM_MACHINES <- 4 # Simulation time in weeks
WEEKS <- WEEKS * 7 * 24 * 60 # Simulation time in minutes
SIM_TIME
# setup
set.seed(42)
<- simmer() env
The make_parts
trajectory defines a machine’s operating loop. A worker seizes the machine and starts manufacturing and counting parts in an infinite loop. Provided we want more than one machine, we parametrise the trajectory as a function of the machine:
<- function(machine)
make_parts trajectory() %>%
seize(machine, 1) %>%
timeout(function() rnorm(1, PT_MEAN, PT_SIGMA)) %>%
set_attribute("parts", 1, mod="+") %>%
rollback(2, Inf) # go to 'timeout' over and over
Repairman’s unimportant jobs may be modelled in the same way (without the accounting part):
<- trajectory() %>%
other_jobs seize("repairman", 1) %>%
timeout(JOB_DURATION) %>%
rollback(1, Inf)
Failures are high-priority arrivals, both for the machines and for the repairman. Each random generated failure will randomly select and seize (break) a machine, and will seize (call) the repairman. After the machine is repaired, both resources are released and the corresponding workers begin where they left.
<- paste0("machine", 1:NUM_MACHINES-1)
machines
<- trajectory() %>%
failure select(machines, policy = "random") %>%
seize_selected(1) %>%
seize("repairman", 1) %>%
timeout(REPAIR_TIME) %>%
release("repairman", 1) %>%
release_selected(1)
The machines and their workers are appended to the simulation environment. Note that each machine, which is defined as preemptive, has space for one worker (or a failure) and no space in queue.
for (i in machines) env %>%
add_resource(i, 1, 0, preemptive = TRUE) %>%
add_generator(paste0(i, "_worker"), make_parts(i), at(0), mon = 2)
The same for the repairman, but this time the queue is infinite, since there could be any number of machines at any time waiting for repairments.
%>%
env add_resource("repairman", 1, Inf, preemptive = TRUE) %>%
add_generator("repairman_worker", other_jobs, at(0)) %>%
invisible
Finally, the failure generator is defined with priority=1
(default: 0), and the simulation begins:
%>%
env add_generator("failure", failure,
function() rexp(1, BREAK_MEAN * NUM_MACHINES),
priority = 1) %>%
run(SIM_TIME) %>% invisible
The last value per worker from the attributes table reports the number of parts made:
aggregate(value ~ name, get_mon_attributes(env), max)
#> name value
#> 1 machine0_worker0 3250
#> 2 machine1_worker0 3303
#> 3 machine2_worker0 3241
#> 4 machine3_worker0 3336
#> 5 machine4_worker0 3399
#> 6 machine5_worker0 3423
#> 7 machine6_worker0 3304
#> 8 machine7_worker0 3181
#> 9 machine8_worker0 3253
#> 10 machine9_worker0 3169
Covers:
From here:
This example models a movie theater with one ticket counter selling tickets for three movies (next show only). People arrive at random times and try to buy a random number (1-6) of tickets for a random movie. When a movie is sold out, all people waiting to buy a ticket for that movie renege (leave the queue).
First of all, we load the libraries and prepare the environment.
library(simmer)
<- 50 # Number of tickets per movie
TICKETS <- 120 # Simulate until
SIM_TIME <- c("R Unchained", "Kill Process", "Pulp Implementation")
movies
# setup
set.seed(42)
<- simmer() env
The main actor of this simulation is the moviegoer, a process that will try to buy a number of tickets for a certain movie. The logic is as follows:
This recipe is directly translated into a simmer
trajectory as follows:
<- function() movies[get_attribute(env, "movie")]
get_movie <- function() paste0(get_movie(), " sold out")
soldout_signal <- function() get_capacity(env, get_movie()) == 0
check_soldout <- function()
check_tickets_available get_server_count(env, get_movie()) > (TICKETS - 2)
<- trajectory() %>%
moviegoer # select a movie
set_attribute("movie", function() sample(3, 1)) %>%
select(get_movie) %>%
# set reneging condition
renege_if(soldout_signal) %>%
# leave immediately if the movie was already sold out
leave(check_soldout) %>%
# wait for my turn
seize("counter", 1) %>%
# buy tickets
seize_selected(
function() sample(6, 1), continue = FALSE,
reject = trajectory() %>%
timeout(0.5) %>%
release("counter", 1)
%>%
) # abort reneging condition
renege_abort() %>%
# check the tickets available
branch(
continue = TRUE,
check_tickets_available, trajectory() %>%
set_capacity_selected(0) %>%
send(soldout_signal)
%>%
) timeout(1) %>%
release("counter", 1) %>%
# watch the movie
wait()
which actually constitutes a quite interesting subset of simmer
’s capabilities. The next step is to add the required resources to the environment env
: the three movies and the ticket counter.
# add movies as resources with capacity TICKETS and no queue
for (i in movies) env %>%
add_resource(i, TICKETS, 0)
# add ticket counter with capacity 1 and infinite queue
%>% add_resource("counter", 1, Inf)
env #> simmer environment: anonymous | now: 0 | next:
#> { Monitor: in memory }
#> { Resource: R Unchained | monitored: TRUE | server status: 0(50) | queue status: 0(0) }
#> { Resource: Kill Process | monitored: TRUE | server status: 0(50) | queue status: 0(0) }
#> { Resource: Pulp Implementation | monitored: TRUE | server status: 0(50) | queue status: 0(0) }
#> { Resource: counter | monitored: TRUE | server status: 0(1) | queue status: 0(Inf) }
And finally, we attach an exponential moviegoer generator to the moviegoer
trajectory and the simulation starts:
# add a moviegoer generator and start simulation
%>%
env add_generator("moviegoer", moviegoer, function() rexp(1, 1 / 0.5), mon=2) %>%
run(SIM_TIME)
#> simmer environment: anonymous | now: 120 | next: 120.027892152333
#> { Monitor: in memory }
#> { Resource: R Unchained | monitored: TRUE | server status: 50(0) | queue status: 0(0) }
#> { Resource: Kill Process | monitored: TRUE | server status: 50(0) | queue status: 0(0) }
#> { Resource: Pulp Implementation | monitored: TRUE | server status: 49(0) | queue status: 0(0) }
#> { Resource: counter | monitored: TRUE | server status: 0(1) | queue status: 0(Inf) }
#> { Source: moviegoer | monitored: 2 | n_generated: 223 }
The analysis is performed with standard R tools:
# get the three rows with the sold out instants
<- get_mon_resources(env) %>%
sold_time subset(resource != "counter" & capacity == 0)
# get the arrivals that left at the sold out instants
# count the number of arrivals per movie
<- get_mon_arrivals(env) %>%
n_reneges subset(finished == FALSE & end_time %in% sold_time$time) %>%
merge(get_mon_attributes(env)) %>%
transform(resource = movies[value]) %>%
aggregate(value ~ resource, data=., length)
# merge the info and print
invisible(apply(merge(sold_time, n_reneges), 1, function(i) {
cat("Movie '", i["resource"], "' was sold out in ", i["time"], " minutes.\n",
" Number of people that left the queue: ", i["value"], "\n", sep="")
}))#> Movie 'Kill Process' was sold out in 53.09917 minutes.
#> Number of people that left the queue: 9
#> Movie 'Pulp Implementation' was sold out in 51.59917 minutes.
#> Number of people that left the queue: 9
#> Movie 'R Unchained' was sold out in 33.09917 minutes.
#> Number of people that left the queue: 8
Covers:
From here:
This example models a gas station and cars that arrive for refuelling. The gas station has a limited number of fuel pumps and a fuel tank that is shared between the fuel pumps.
Vehicles arriving at the gas station first request a fuel pump from the station. Once they acquire one, they try to take the desired amount of fuel from the fuel pump. They leave when they are done.
The fuel level is reqularly monitored by a controller. When the level drops below a certain threshold, a tank truck is called for refilling the tank.
Parameters and setup:
library(simmer)
<- 200 # liters
GAS_STATION_SIZE <- 10 # Threshold for calling the tank truck (in %)
THRESHOLD <- 50 # liters
FUEL_TANK_SIZE <- c(5, 25) # Min/max levels of fuel tanks (in liters)
FUEL_TANK_LEVEL <- 2 # liters / second
REFUELING_SPEED <- 300 # Seconds it takes the tank truck to arrive
TANK_TRUCK_TIME <- c(30, 100) # Create a car every [min, max] seconds
T_INTER <- 1000 # Simulation time in seconds
SIM_TIME
# setup
set.seed(42)
<- simmer() env
SimPy solves this problem using a special kind of resource called container, which is not present in simmer
so far. However, a container is nothing more than an embellished counter. Therefore, we can implement this in simmer
with a global counter (GAS_STATION_LEVEL
here), some signaling and some care checking the counter bounds.
Let us consider just the refuelling process in the first place. A car needs to check whether there is enough fuel available to fill its tank. If not, it needs to block until the gas station is refilled.
<- GAS_STATION_SIZE
GAS_STATION_LEVEL <- "gas station refilled"
signal
<- trajectory() %>%
refuelling # check if there is enough fuel available
branch(function() FUEL_TANK_SIZE - get_attribute(env, "level") > GAS_STATION_LEVEL,
continue = TRUE,
# if not, block until the signal "gas station refilled" is received
trajectory() %>%
trap(signal) %>%
wait() %>%
untrap(signal)
%>%
) # refuel
timeout(function() {
<- FUEL_TANK_SIZE - get_attribute(env, "level")
liters_required <<- GAS_STATION_LEVEL - liters_required
GAS_STATION_LEVEL return(liters_required / REFUELING_SPEED)
})
Now a car’s trajectory is straightforward. First, the starting time is saved as well as the tank level. Then, the car seizes a pump, refuels and leaves.
<- trajectory() %>%
car set_attribute(c("start", "level"), function()
c(now(env), sample(FUEL_TANK_LEVEL[1]:FUEL_TANK_LEVEL[2], 1))) %>%
log_("arriving at gas station") %>%
seize("pump", 1) %>%
# 'join()' concatenates the refuelling trajectory here
join(refuelling) %>%
release("pump", 1) %>%
log_(function()
paste0("finished refuelling in ", now(env) - get_attribute(env, "start"), " seconds"))
The tank truck takes some time to arrive. Then, the gas station is completely refilled and the signal “gas station refilled” is sent to the blocked cars.
<- trajectory() %>%
tank_truck timeout(TANK_TRUCK_TIME) %>%
log_("tank truck arriving at gas station") %>%
log_(function() {
<- GAS_STATION_SIZE - GAS_STATION_LEVEL
refill <<- GAS_STATION_SIZE
GAS_STATION_LEVEL paste0("tank truck refilling ", refill, " liters")
%>%
}) send(signal)
The controller periodically checks the GAS_STATION_LEVEL
and calls the tank truck when needed.
<- trajectory() %>%
controller branch(function() GAS_STATION_LEVEL / GAS_STATION_SIZE * 100 < THRESHOLD,
continue = TRUE,
trajectory() %>%
log_("calling the tank truck") %>%
join(tank_truck)
%>%
) timeout(10) %>%
rollback(2, Inf)
Finally, we need to add a couple of pumps, a controller worker and a car generator.
%>%
env add_resource("pump", 2) %>%
add_generator("controller", controller, at(0)) %>%
add_generator("car", car, function() sample(T_INTER[1]:T_INTER[2], 1)) %>%
run(SIM_TIME)
#> 78: car0: arriving at gas station
#> 98.5: car0: finished refuelling in 20.5 seconds
#> 172: car1: arriving at gas station
#> 190: car1: finished refuelling in 18 seconds
#> 219: car2: arriving at gas station
#> 233.5: car2: finished refuelling in 14.5 seconds
#> 295: car3: arriving at gas station
#> 314.5: car3: finished refuelling in 19.5 seconds
#> 361: car4: arriving at gas station
#> 377: car4: finished refuelling in 16 seconds
#> 410: car5: arriving at gas station
#> 442: car6: arriving at gas station
#> 498: car7: arriving at gas station
#> 532: car8: arriving at gas station
#> 595: car9: arriving at gas station
#> 627: car10: arriving at gas station
#> 698: car11: arriving at gas station
#> 742: car12: arriving at gas station
#> 807: car13: arriving at gas station
#> 854: car14: arriving at gas station
#> 952: car15: arriving at gas station
#> simmer environment: anonymous | now: 1000 | next: 1000
#> { Monitor: in memory }
#> { Resource: pump | monitored: TRUE | server status: 2(2) | queue status: 9(Inf) }
#> { Source: controller | monitored: 1 | n_generated: 1 }
#> { Source: car | monitored: 1 | n_generated: 17 }
Note though that this model has an important flaw, both in the original and in this adaptation, as discussed in #239: when two cars require more fuel than available, but the level is still above the threshold, these cars completely block the queue, and the refilling truck is never called. It could be solved by detecting this situation in the controller.