Post Office

Post Office

There is a small post office with one clerk serving the arriving customers. Customers have differing wishes leading to different serving times, from 1 - 5 minutes. We have a little variation in serving times due to variation in customer habits and clerk performance. The arrival rate of customers is about 18 per hour, every 3.33 minutes or 3 minutes, 20 seconds on average. Our post office is small and customer patience is limited, so queue length is limited to 5 customers.

We have provided 10% extra capacity, so our expectation is that there should not be too many customers discouraged for long waiting times or for full queues.

post office

Let's do a process-based simulation using Simulate. We need

  1. a source: all the people, providing an unlimited supply for customers,
  2. customers with their demands and their limited patience,
  3. a queue and
  4. our good old clerk.

First we must load the needed modules, describe a customer and define some helper functions:

using Simulate, Random, Distributions, DataFrames

mutable struct Customer
    id::Int64
    arrival::Float64
    request::Int64

    Customer(n::Int64, arrival::Float64) = new(n, arrival, rand(DiscreteUniform(1, 5)))
end

full(q::Channel) = length(q.data) >= q.sz_max
logevent(nr, queue::Channel, info::AbstractString, wt::Number) =
    push!(df, (round(τ(), digits=2), nr, length(queue.data), info, wt))

logevent (generic function with 1 method)

Then we define functions for our processes: people and clerk.

function people(output::Channel, β::Float64)
    i = 1
    while true
        Δt = rand(Exponential(β))
        delay!(Δt)
        if !full(output)
            put!(output, Customer(i, τ()))
            logevent(i, output, "enqueues", 0)
         else
            logevent(i, output, "leaves - queue is full!", -1)
        end
        i += 1
    end
end

function clerk(input::Channel)
    cust = take!(input)
    Δt = cust.request + randn()*0.2
    logevent(cust.id, input, "now being served", τ() - cust.arrival)
    delay!(Δt)
    logevent(cust.id, input, "leaves", τ() - cust.arrival)
end

clerk (generic function with 1 method)

Then we have to create a logging table, register and startup the processes:

reset!(𝐶)  # for repeated runs it is easier if we reset our central clock here
Random.seed!(2019)  # seed random number generator for reproducibility
queue = Channel(5)  # thus we determine the max size of the queue

df = DataFrame(time=Float64[], cust=Int[], qlen=Int64[], status=String[], wtime=Float64[])

process!(𝐶, SimProcess(1, people, queue, 3.333)) # register the functions as processes
process!(𝐶, SimProcess(2, clerk, queue))

2

Then we can simply run the simulation. We assume our time unit being minutes, so we run for 600 units:

println(run!(𝐶, 600))
println("``(length(queue.data)) customers yet in queue")

run! finished with 338 clock events, 0 sample steps, simulation time: 600.0
0 customers yet in queue

Our table has registered it all:

df
notimecustqlenstatuswtime
11.211enqueues0.0
21.210now being served0.0
35.4621enqueues0.0
45.532enqueues0.0
56.1912leaves4.99532
66.1921now being served0.737497
77.9942enqueues0.0
88.8122leaves3.35581
98.8131now being served3.30971
1012.3352enqueues0.0
1112.9832leaves7.4733
1212.9841now being served4.98585
1313.7341leaves5.74268
1413.7350now being served1.39837
1515.7261enqueues0.0
1617.1272enqueues0.0
1717.7352leaves5.3967
1817.7361now being served2.00988
1920.082enqueues0.0
2020.7693enqueues0.0
2123.2663leaves7.53774
2223.2672now being served6.13554
2325.43103enqueues0.0
2426.0114enqueues0.0
2526.3574leaves9.22525
2626.3583now being served6.34474
2727.49124enqueues0.0
2827.6484leaves7.63665
2927.6493now being served6.88549
3029.06134enqueues0.0
last(df, 5)
notimecustqlenstatuswtime
1589.11711enqueues0.0
2589.11710now being served0.0
3593.771710leaves4.67801
4598.011721enqueues0.0
5598.011720now being served0.0
describe(df[df[!, :wtime] .> 0, :wtime])
Summary Stats:
Length:         302
Missing Count:  0
Mean:           7.486712
Minimum:        0.009196
1st Quartile:   3.866847
Median:         6.409644
3rd Quartile:   10.541481
Maximum:        23.268310
Type:           Float64

In $600$ minutes simulation time, we registered $172$ customers and $505$ status changes. The mean and median waiting times were around $7$ minutes.

by(df, :status, df -> size(df, 1))
nostatusx1
1enqueues167
2now being served167
3leaves166
4leaves - queue is full!5

Of the $172$ customers, $167$ of them participated in the whole process and were served, but $5$ left beforehand because the queue was full:

df[df.wtime .< 0,:]
notimecustqlenstatuswtime
145.32195leaves - queue is full!-1.0
2249.11665leaves - queue is full!-1.0
3270.04745leaves - queue is full!-1.0
4380.391065leaves - queue is full!-1.0
5382.021075leaves - queue is full!-1.0
using PyPlot
step(df.time, df.wtime)
step(df.time, df.qlen)
axhline(y=0, color="k")
grid()
xlabel("time [min]")
ylabel("wait time [min], queue length")
title("Waiting Time in the Post Office")
legend(["wait_time", "queue_len"]);

png

Many customers had waiting times of more than 10, 15 up to even more than 20 minutes. The negative waiting times were the 5 customers, which left because the queue was full.

So many customers will remain angry. If this is the situation all days, our post office will have an evil reputation. What should we do?

Conclusion

Even if our process runs within predetermined bounds (queue length, customer wishes …), it seems to fluctuate wildly and to produce unpredicted effects. This is due to variation in arrivals, in demands and in serving time on system performance. In this case 10% extra capacity is not enough to provide enough buffer for variation and for customer service – even if our post clerk is the most willing person.

Even for such a simple everyday system, we cannot say beforehand – without reality check – which throughput, waiting times, mean queue length, capacity utilization or customer satisfaction will emerge. Even more so for more complicated systems in production, service, projects and supply chains with multiple dependencies.

If we had known the situation beforehand, we could have provided standby for our clerk or install an automatic stamp dispenser for cutting shorter tasks … We should have done a simulation before …