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)
shop %>% run(until = 45)
## 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)
shop %>% run(until = 45)
## 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)
shop %>% run(until = 45)
## 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")

door_schedule <- schedule(c(1,7,9,13), c(Inf,0,Inf,0), period = 13)

shop <-
  simmer("shop") %>%
  add_resource("door", capacity = door_schedule) %>%
  add_resource("counter") %>%
  add_generator("Customer", customer, function() rexp(1, 1))

shop %>% run(26)
## 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.")

shop <- simmer("shop")
shop %>%
  add_resource("door") %>%
  add_resource("counter") %>%
  add_generator("Customer",
                customer, function() rexp(1, 1/20)) 
## 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 }
shop %>% run(65)
## 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 every timeout time units even if the batch size has not been fulfilled.