6.3 The donut shop - advanced features
In many situations there is a system of priority service. Those customers with high priority are served first, those with low priority must wait. In some cases, preemptive priority will even allow a high-priority customer to interrupt the service of one with a lower priority.
Simmer
implements priority requests with an extra integer priority argument to add_generator()
. By default, priority is zero; higher integers have higher priority.
6.3.1 Priority customers without preemption
Suppose the donut shop have priority customers that when arrive at the shop, are served as soon as possible. We make the assumption that they arrive
<-
customer trajectory("Customer's path") %>%
log_("Here I am") %>%
seize("counter") %>%
timeout(function() rnorm(1,10,2)) %>%
release("counter") %>%
log_("Finished")
<-
shop simmer("shop") %>%
add_resource("counter") %>%
add_generator("Customer", customer, function() rexp(1, 1/5)) %>%
add_generator("Priority_Customer", customer, function() rexp(1, 1/15), priority = 1)
set.seed(2021)
%>% run(until = 45) shop
## 5.92531: Customer0: Here I am
## 7.28854: Customer1: Here I am
## 11.0879: Customer2: Here I am
## 14.2421: Customer3: Here I am
## 14.3366: Customer4: Here I am
## 15.0111: Customer5: Here I am
## 16.9819: Customer0: Finished
## 18.2117: Customer6: Here I am
## 18.3019: Priority_Customer0: Here I am
## 20.1029: Customer7: Here I am
## 21.0334: Customer8: Here I am
## 21.7555: Customer9: Here I am
## 24.807: Customer10: Here I am
## 24.9206: Customer11: Here I am
## 25.6428: Customer1: Finished
## 28.047: Customer12: Here I am
## 28.879: Customer13: Here I am
## 32.9443: Customer14: Here I am
## 35.9055: Priority_Customer0: Finished
## 36.8793: Customer15: Here I am
## 40.846: Priority_Customer1: Here I am
## simmer environment: shop | now: 45 | next: 47.5452877648588
## { Monitor: in memory }
## { Resource: counter | monitored: TRUE | server status: 1(1) | queue status: 14(Inf) }
## { Source: Customer | monitored: 1 | n_generated: 17 }
## { Source: Priority_Customer | monitored: 1 | n_generated: 3 }
From the output we can see that whenever a priority customer joins the queue he is sold donuts as soon as the employee becomes available.
6.3.2 Priority customers with preemption
Now we allow priority customers to have preemptive priority. They will displace any customer in service when they arrive. That customer will resume when they finish (unless higher priority customers intervene). This requires only a change to one line of the program, adding the argument, preemptive = TRUE
to the add_resource
function call.
<-
shop simmer("shop") %>%
add_resource("counter", preemptive = TRUE) %>%
add_generator("Customer", customer, function() rexp(1, 1/5)) %>%
add_generator("Priority_Customer", customer, function() rexp(1, 1/15), priority = 1)
set.seed(2021)
%>% run(until = 45) shop
## 5.92531: Customer0: Here I am
## 7.28854: Customer1: Here I am
## 11.0879: Customer2: Here I am
## 14.2421: Customer3: Here I am
## 14.3366: Customer4: Here I am
## 15.0111: Customer5: Here I am
## 16.9819: Customer0: Finished
## 18.2117: Customer6: Here I am
## 18.3019: Priority_Customer0: Here I am
## 20.1029: Customer7: Here I am
## 23.9383: Customer8: Here I am
## 24.6604: Customer9: Here I am
## 27.7119: Customer10: Here I am
## 27.8255: Customer11: Here I am
## 30.9519: Customer12: Here I am
## 31.4746: Customer13: Here I am
## 31.6998: Priority_Customer0: Finished
## 39.0407: Customer1: Finished
## 39.2381: Customer14: Here I am
## 40.846: Priority_Customer1: Here I am
## simmer environment: shop | now: 45 | next: 45.2787752879948
## { Monitor: in memory }
## { Resource: counter | monitored: TRUE | server status: 1(1) | queue status: 13(Inf) }
## { Source: Customer | monitored: 1 | n_generated: 16 }
## { Source: Priority_Customer | monitored: 1 | n_generated: 3 }
In this other case, priority customers are served straight away. The customer that was served when the priority customer arrived resumes is service as soon as the priority customer finishes.
6.3.3 Balking customers
Balking occurs when a customer refuses to join a queue if it is too long. Suppose that if there is one customer queuing in our shop then customers do not join the queue and leave. We can implement this by setting the queue_size
option of add_resource
and by adding some options of the seize
function. Let’s consider the following code.
<-
customer trajectory("Customer's path") %>%
log_("Here I am") %>%
seize("counter", continue = FALSE, reject =
trajectory("Balked customer") %>% log_("Balking") ) %>%
timeout(function() rnorm(1,10,2)) %>%
release("counter") %>%
log_("Finished")
<-
shop simmer("shop") %>%
add_resource("counter", queue_size = 1) %>%
add_generator("Customer", customer,
function() rexp(1, 1/5))
The input queue_size
is self-explanatory and simply sets how many people can queue for the counter. In the seize
function we set the inputs continue
and reject
. With continue = FALSE
we are saying that a rejected customer does not follow the rest of the trajectory. With reject
we are specifying what trajectory the rejected customer will follow.
Let’s run the simulation.
set.seed(2021)
%>% run(until = 45) shop
## 5.92531: Customer0: Here I am
## 12.0259: Customer1: Here I am
## 13.4303: Customer2: Here I am
## 13.4303: Customer2: Balking
## 16.6226: Customer0: Finished
## 17.2296: Customer3: Here I am
## 28.4187: Customer1: Finished
## 36.649: Customer4: Here I am
## 38.7585: Customer3: Finished
## 40.1462: Customer5: Here I am
## 40.6299: Customer6: Here I am
## 40.6299: Customer6: Balking
## 41.5604: Customer7: Here I am
## 41.5604: Customer7: Balking
## 42.2825: Customer8: Here I am
## 42.2825: Customer8: Balking
## simmer environment: shop | now: 45 | next: 45.3340111165536
## { Monitor: in memory }
## { Resource: counter | monitored: TRUE | server status: 1(1) | queue status: 1(1) }
## { Source: Customer | monitored: 1 | n_generated: 10 }
So now we see that often customers just leave the shop because they decide not to queue. We can count how many of them left for balking using:
sum(get_mon_arrivals(shop)$activity_time == 0)
## [1] 4
and the hourly rate at which they leave
sum(get_mon_arrivals(shop)$activity_time == 0)/now(shop)*60
## [1] 5.333333
6.3.4 Reneging (or abandoning) customers
Often in practice an impatient customer will leave the queue before being served. Simmer can model this reneging behaviour using the renege_in()
function in a trajectory. This defines the maximum time that a customer will wait before reneging, as well as an ‘out’ trajectory for them to follow when they renege.
If the customer reaches the server before reneging, then their impatience must be cancelled with the renege_abort()
function.
<-
customer trajectory("Customer's path") %>%
log_("Here I am") %>%
renege_in(function() rnorm(1,5,1),
out = trajectory("Reneging customer") %>%
log_("I am off")) %>%
seize("counter") %>%
renege_abort() %>%
timeout(function() rnorm(1,10,2)) %>%
release("counter") %>%
log_("Finished")
<-
shop simmer("shop") %>%
add_resource("counter") %>%
add_generator("Customer", customer, function() rexp(1, 1/5))
run(shop, until = 45)
## 0.113593: Customer0: Here I am
## 0.892989: Customer1: Here I am
## 7.40631: Customer1: I am off
## 9.74378: Customer2: Here I am
## 10.2131: Customer3: Here I am
## 13.0758: Customer0: Finished
## 16.4212: Customer3: I am off
## 24.9321: Customer2: Finished
## 27.4101: Customer4: Here I am
## 27.7221: Customer5: Here I am
## 27.808: Customer6: Here I am
## 32.6284: Customer5: I am off
## 32.8674: Customer6: I am off
## 34.1951: Customer7: Here I am
## 36.7021: Customer4: Finished
## simmer environment: shop | now: 45 | next: 45.8000847848882
## { Monitor: in memory }
## { Resource: counter | monitored: TRUE | server status: 1(1) | queue status: 0(Inf) }
## { Source: Customer | monitored: 1 | n_generated: 9 }
6.3.5 Several counters with individual queues
Each counter is now assumed to have its own queue. The programming is more complicated because the customer has to decide which queue to join. The obvious technique is to make each counter a separate resource.
In practice, a customer might join the shortest queue. We implement this behaviour by first selecting the shortest queue, using the select
function. Then we use seize_selected
to enter the chosen queue, and later release_selected
.
The rest of the program is the same as before.
set.seed(2021)
<-
customer trajectory("Customer's path") %>%
log_("Here I am") %>%
select(c("counter1", "counter2"), policy = "shortest-queue") %>%
seize_selected() %>%
timeout(function() rnorm(1,10,2)) %>%
release_selected() %>%
log_("Finished")
<-
shop simmer("shop") %>%
add_resource("counter1", 1) %>%
add_resource("counter2", 1) %>%
add_generator("Customer", customer, function() rexp(1, 1/5))
run(shop, until = 45)
## 5.92531: Customer0: Here I am
## 12.0259: Customer1: Here I am
## 13.4303: Customer2: Here I am
## 13.5248: Customer3: Here I am
## 14.1994: Customer4: Here I am
## 16.6226: Customer0: Finished
## 17.3999: Customer5: Here I am
## 19.2911: Customer6: Here I am
## 20.7802: Customer1: Finished
## 25.2835: Customer2: Finished
## 26.8058: Customer7: Here I am
## 29.8574: Customer8: Here I am
## 29.971: Customer9: Here I am
## 33.0974: Customer10: Here I am
## 33.62: Customer11: Here I am
## 34.0487: Customer4: Finished
## 34.1781: Customer3: Finished
## 41.3835: Customer12: Here I am
## 42.2932: Customer6: Finished
## simmer environment: shop | now: 45 | next: 45.3185680974989
## { Monitor: in memory }
## { Resource: counter1 | monitored: TRUE | server status: 1(1) | queue status: 3(Inf) }
## { Resource: counter2 | monitored: TRUE | server status: 1(1) | queue status: 2(Inf) }
## { Source: Customer | monitored: 1 | n_generated: 14 }
There are several policies implemented internally that can be accessed by name:
shortest-queue
: The resource with the shortest queue is selected.round-robin
: Resources will be selected in a cyclical nature.first-available
: The first available resource is selected.random
A resource is randomly selected.
6.3.6 Opening times
Customers arrive at random, some of them getting to the shop before the door is opened by a doorman. They wait for the door to be opened and then rush in and queue to be served.
This model defines the door as a resource, just like the counter. The capacity of the door is defined according to the schedule
function, so that it has zero capacity when it is shut, and infinite capacity when it is open. Customers ‘seize’ the door and must then wait until it has capacity to ‘serve’ them. Once it is available, all waiting customers are ‘served’ immediately (i.e. they pass through the door). There is no timeout between ‘seizing’ and ‘releasing’ the door.
<-
customer trajectory("Customer's path") %>%
log_(function()
if (get_capacity(shop, "door") == 0)
"Here I am but the door is shut."
else "Here I am and the door is open."
%>%
) seize("door") %>%
log_("I can go in!") %>%
release("door") %>%
seize("counter") %>%
timeout(function() {rexp(1, 10)}) %>%
release("counter")
<- schedule(c(1,7,9,13), c(Inf,0,Inf,0), period = 13)
door_schedule
<-
shop simmer("shop") %>%
add_resource("door", capacity = door_schedule) %>%
add_resource("counter") %>%
add_generator("Customer", customer, function() rexp(1, 1))
%>% run(26) shop
## 3.53354: Customer0: Here I am and the door is open.
## 3.53354: Customer0: I can go in!
## 6.97294: Customer1: Here I am and the door is open.
## 6.97294: Customer1: I can go in!
## 7.05686: Customer2: Here I am but the door is shut.
## 9: Customer2: I can go in!
## 9.30068: Customer3: Here I am and the door is open.
## 9.30068: Customer3: I can go in!
## 10.4405: Customer4: Here I am and the door is open.
## 10.4405: Customer4: I can go in!
## 11.3978: Customer5: Here I am and the door is open.
## 11.3978: Customer5: I can go in!
## 15.4491: Customer6: Here I am and the door is open.
## 15.4491: Customer6: I can go in!
## 17.7096: Customer7: Here I am and the door is open.
## 17.7096: Customer7: I can go in!
## 18.4016: Customer8: Here I am and the door is open.
## 18.4016: Customer8: I can go in!
## 19.1858: Customer9: Here I am and the door is open.
## 19.1858: Customer9: I can go in!
## 22.0271: Customer10: Here I am and the door is open.
## 22.0271: Customer10: I can go in!
## 22.9206: Customer11: Here I am and the door is open.
## 22.9206: Customer11: I can go in!
## 24.0738: Customer12: Here I am and the door is open.
## 24.0738: Customer12: I can go in!
## 24.9154: Customer13: Here I am and the door is open.
## 24.9154: Customer13: I can go in!
## 25.3469: Customer14: Here I am and the door is open.
## 25.3469: Customer14: I can go in!
## 25.7375: Customer15: Here I am and the door is open.
## 25.7375: Customer15: I can go in!
## simmer environment: shop | now: 26 | next: 26
## { Monitor: in memory }
## { Resource: door | monitored: TRUE | server status: 0(Inf) | queue status: 0(Inf) }
## { Resource: counter | monitored: TRUE | server status: 1(1) | queue status: 0(Inf) }
## { Source: Customer | monitored: 1 | n_generated: 17 }
6.3.7 Batching clients
Customers arrive at random, some of them getting to the shop before the door is open. This is controlled by an automatic machine called the doorman which opens the door only at intervals of 30 minutes (it is a very secure shop). The customers wait for the door to be opened and all those waiting enter and proceed to the counter. The door is closed behind them.
One possible solution is using batching. Customers can be collected into batches of a given size, or for a given time, or whichever occurs first. Here, they are collected for periods of 30, and the number of customers in each batch is unrestricted.
After the batch is created with batch
it is then separated with separate
.
set.seed(2021)
<-
customer trajectory("Customer's path") %>%
log_("Here I am, but the door is shut.") %>%
batch(n = Inf, timeout = 30) %>%
separate() %>%
log_("The door is open!") %>%
seize("counter") %>%
timeout(function() {rexp(1, 1/2)}) %>%
release("counter") %>%
log_("Finished.")
<- simmer("shop")
shop %>%
shop add_resource("door") %>%
add_resource("counter") %>%
add_generator("Customer",
function() rexp(1, 1/20)) customer,
## simmer environment: shop | now: 0 | next: 0
## { Monitor: in memory }
## { Resource: door | monitored: TRUE | server status: 0(1) | queue status: 0(Inf) }
## { Resource: counter | monitored: TRUE | server status: 0(1) | queue status: 0(Inf) }
## { Source: Customer | monitored: 1 | n_generated: 0 }
%>% run(65) shop
## 23.7012: Customer0: Here I am, but the door is shut.
## 48.1037: Customer1: Here I am, but the door is shut.
## 53.5567: Customer2: Here I am, but the door is shut.
## 53.7012: Customer0: The door is open!
## 53.7012: Customer1: The door is open!
## 53.7012: Customer2: The door is open!
## 54.263: Customer0: Finished.
## 55.7827: Customer1: Finished.
## 57.0444: Customer2: Finished.
## 61.6105: Customer3: Here I am, but the door is shut.
## 61.9885: Customer4: Here I am, but the door is shut.
## 64.6866: Customer5: Here I am, but the door is shut.
## simmer environment: shop | now: 65 | next: 77.4887534806737
## { Monitor: in memory }
## { Resource: door | monitored: TRUE | server status: 0(1) | queue status: 0(Inf) }
## { Resource: counter | monitored: TRUE | server status: 0(1) | queue status: 0(Inf) }
## { Source: Customer | monitored: 1 | n_generated: 7 }
The function balk
takes two inputs:
n
: the batch size;timeout
: set an optional timer which triggers batches everytimeout
time units even if the batch size has not been fulfilled.