We've seen the stack data type, which is essentially a list where we can only add and remove elements at the top of the stack (the end of the list in our implementation)

Another common data structure is the queue, where I can only enqueue (at the back) and dequeue (at the front). So, like waiting in line: you can only get in line at the end of the line, and people only get off the line at the front.

Many real world processes involve queues: waiting in line to buy a theater ticket, waiting in line to check in at the airport, waiting to get your car serviced, waiting to vote at a polling station, etc. These processes usually differ in the following aspects:

  • The number of "servers". We use the term in the sense of "something that provides a service". For example, if there's only one ticket booth at the theater, there's only one server. If there's three mechanics who can service cars, there's three servers, etc.
  • The service time: When someone reaches the front of the line, the time it takes for the service to be provided.
  • The rate of arrival of customers

We can model real world queues by using a queue data structure and modeling the behavior of the above three aspects:

  • We add customers to the queue according to a rate of arrival. This rate of arrival could be deterministic (e.g., a customer arrives every 5 minutes) or it could behave according to a probability distribution.
  • When a customer gets to the front of the queue, they can only be serviced if there is an available server.
  • Once a custome is assigned to a server, their service time can be deterministic or it can behave according to a probability distribution.

In this lecture we are going to see an example of an M/D/1 queue:

  • Arrivals follow a Markov process (M). The rate of arrival is specified via a parameter λ, where 1/λ is the mean time between arrivals. We use this as a parameter to an exponential distribution.
  • The time to service each customer is deterministic (D). The rate of service is specified via a parameter μ, where 1/μ is the mean service time. Since the service time is deterministic, that means that it will be exactly 1/μ for every customer (except of being drawn from a probability distribution.
  • There is only one server (1)

Note: An "M/D/1" queue isn't a special type of queue. We use a regular queue; M/D/1 specifies the way in which customers will enter/leave the queue.

For simplicity, let's assume we count time in minutes starting at zero. Let's say our probability distribution generates the following arrival times:

2, 2, 6

Note that these are actually "times since the last arrival", so we have three customers arriving at times 2, 4, and 10. We will also assume that the service time is exactly 3. Our M/D/1 queue would progress as follows:

  • Time 0: nothing happens. Queue: []
  • Time 1: nothing happens. Queue: []
  • Time 2: Customer 1 arrives. Queue: [1]. They are at the front of the queue, so they start receiving service.
  • Time 3: nothing happens. Queue: [1]
  • Time 4: Customer 2 arrives. Queue: [2,1].
  • Time 5: Customer 1 departs. Queue: [2]. Customer 2 starts receiving service.
  • Time 6: nothing happens. Queue: [2]
  • Time 7: nothing happens. Queue: [2]
  • Time 8: Customer 2 departs. Queue: [].
  • Time 9: nothing happens. Queue: []
  • Time 10: Customer 3 arrives. Queue: [3]. Customer 3 starts receiving service.

While there are ways to prove things analytically about M/D/1 queues, we will do so via simulation, effectively reproducing the process shown above.

We will use a queue:

In [2]:
from mytypes import Queue

And we need to model a Customer. We will add Customer objects to the queue.

In [3]:
# A class to represent a single customer in an M/D/1 queue simulation.
# Each customer has three attributes:
#
#  - cid: A customer identifier (can be anything, but we will use consecutive integers)
#  - arrival_time: The time at which the customer arrived at the queue
#  - departure_time: The time at which the customer departed the queue
class Customer(object):
    def __init__(self, cid, arrival_time):
        self.cid = cid
        self.arrival_time = arrival_time
        self.departure_time = None
        
    def get_wait(self):
        if self.departure_time is None:
            return None
        else:
            return self.departure_time - self.arrival_time
        
    def __str__(self):
        return "Customer({}, {})".format(self.cid, self.arrival_time)
    
    def __repr__(self):
        return str(self)

Let's add some customers to the queue:

In [6]:
q = Queue()
In [7]:
q.enqueue(Customer(1, 10))
Out[7]:
True
In [8]:
q.enqueue(Customer(2, 15))
Out[8]:
True
In [9]:
q.enqueue(Customer(3, 18))
Out[9]:
True
In [11]:
q
Out[11]:
 --> Customer(3, 18) --> Customer(2, 15) --> Customer(1, 10) -->

Below is our simulation code. The comments explain what is happening at each step. If you want to go through the code step by step, I suggest following the explanation in the comments.

In [12]:
import random

# simulate_md1: Simulates an M/D/1 queue.
#
# In an M/D/1 queue que have:
#   
# - Arrivals follow a Markov process (M)
# - The time to service each customer is deterministic (D)
# - There is only one server (1)
#
# The function takes three parameters (plus one optional parameter)
#
# - lambd: The simulation uses an exponential distribution to determine
#          the arrival time of the next customer. This parameters is the
#          lambda parameter to an exponential distribution (specifically,
#          Python's random.expovariate)
# - mu: The rate at which customers are serviced. The larger this value is,
#       the more customers will be serviced per unit of time
# - max_time: The maximum time of the simulation
# - verbosity (optional): Can be 0 (no output), 1 (print state of the queue
#                         at each time), or 2 (same as 1, but also print when
#                         each customer arrives and departs)
#
# The function returns two lists: one with all the customers that were served
# during the simulation, and one with all the customers that were yet to be
# served when the simulation ended.
#
def simulate_md1(lambd, mu, max_time, verbosity = 0):
    md1 = Queue()

    # Our return values: the list of customers that have been
    # served, and the list of customers that haven't been served
    served_customers = []
    unserved_customers = []
    
    # The type of simulation we have implemented in this function
    # is known as a "discrete event simulation"
    # (https://en.wikipedia.org/wiki/Discrete_event_simulation), where
    # we simulate a discrete sequence of events: customer arrivals
    # and customer departures. So, we only need to keep track of when 
    # the next arrival and the next departure will take place (because 
    # nothing interesting happens between those two types of events). 
    # Then, in each step of the simulation, we simply advance the 
    # simulation clock to earliest next event. Note that, because
    # we have a single server, this can be easily done with just
    # two variables.

    next_arrival = random.expovariate(lambd)
    next_service = next_arrival + 1/mu
        
    # We initialize the simulation's time to the earliest event:
    # the next arrival time
    t = next_arrival
    
    # We will number customers starting from 1
    cid = 1
    
    while t < max_time:

        # Process a new arrival
        if t == next_arrival:
            customer = Customer(cid, arrival_time = t)
            cid += 1
            md1.enqueue(customer)

            if verbosity >= 2:
                print("{:10.2f}: Customer {} arrives".format(t, customer.cid))

            next_arrival = t + random.expovariate(lambd)
            
        # The customer at the head of the queue has been served
        if t == next_service:
            done_customer = md1.dequeue()
            done_customer.departure_time = t
            
            served_customers.append(done_customer)

            if verbosity >= 2:
                print("{:10.2f}: Customer {} departs".format(t, done_customer.cid))            
            
            if md1.is_empty():
                # The next service time will be 1/mu after the next arrival
                next_service = next_arrival + 1/mu
            else:
                # We start serving the next customer, so the next service time
                # will be 1/mu after the current time.
                next_service = t + 1/mu
            
        if verbosity >= 1:
            print("{:10.2f}: {}".format(t, "#"*md1.length))
            
        # Advance the simulation clock to the next event
        t = min(next_arrival, next_service)
        
    # Any remaining customers in the queue haven't been served
    while not md1.is_empty():
        unserved_customers.append(md1.dequeue())
        
    return served_customers, unserved_customers
In [13]:
simulate_md1(0.167, 0.15, 100, verbosity=2)
      0.10: Customer 1 arrives
      0.10: #
      3.49: Customer 2 arrives
      3.49: ##
      6.77: Customer 1 departs
      6.77: #
     13.44: Customer 2 departs
     13.44: 
     19.31: Customer 3 arrives
     19.31: #
     23.59: Customer 4 arrives
     23.59: ##
     25.98: Customer 3 departs
     25.98: #
     26.40: Customer 5 arrives
     26.40: ##
     28.07: Customer 6 arrives
     28.07: ###
     32.65: Customer 4 departs
     32.65: ##
     39.31: Customer 5 departs
     39.31: #
     45.98: Customer 6 departs
     45.98: 
     53.44: Customer 7 arrives
     53.44: #
     54.26: Customer 8 arrives
     54.26: ##
     60.11: Customer 7 departs
     60.11: #
     66.77: Customer 8 departs
     66.77: 
     70.43: Customer 9 arrives
     70.43: #
     74.88: Customer 10 arrives
     74.88: ##
     77.10: Customer 9 departs
     77.10: #
     82.19: Customer 11 arrives
     82.19: ##
     83.77: Customer 10 departs
     83.77: #
     90.43: Customer 11 departs
     90.43: 
Out[13]:
([Customer(1, 0.1036267965248641),
  Customer(2, 3.488284952674854),
  Customer(3, 19.313239255083378),
  Customer(4, 23.591705913242922),
  Customer(5, 26.403869696849995),
  Customer(6, 28.072379745541365),
  Customer(7, 53.43959663857426),
  Customer(8, 54.26090858431276),
  Customer(9, 70.43300741049345),
  Customer(10, 74.87835785798578),
  Customer(11, 82.18901165222695)],
 [])