Lecture 32: Single-Server Queueing Systems in Python

Lecture 32: Single-Server Queueing Systems in Python#

Note

This lecture details how to simulate a single-server queueing system developing the simulation logic along with its implemenation in Python


Simulation Logic#

  1. Procedure \(\text{SSQ}(n,A,S)\)

  2. // SYSTEM STATUS

  3. \(t \ \leftarrow 0\)   // simulation clock

  4. \(\varphi \leftarrow 0\)   // server idle/busy

  5. \(Q \leftarrow \varnothing\) // queue

  6. // SYSTEM DYNAMICS

  7. \(T_a \leftarrow \{\}\)// customer arrival time

  8. \(T_s \leftarrow \{\}\)// customer service initiation time

  9. \(T_d \leftarrow \{\}\)// customer departure time

  10. // SYSTEM LOG

  11. \(T_o \leftarrow \{\}\)// event time (arrival/service initiation/departure)

  12. \(\Phi \leftarrow \{\}\) // server status at event time

  13. \(J \leftarrow \{\}\) // queue length at event time

  14. // INITIALIZATION

  15. \(i \ \leftarrow 0\)   // index of last customer serviced

  16. \(j \ \leftarrow 0\)  // queue length

  17. \(k \leftarrow 0\)   // customers serviced

  18. \(t_a \leftarrow A_i\)     // next arrival event time

  19. \(t_d \leftarrow A_i + S_i\) // next departure event time

  20. while \(k < n\) do

  21. if \(t_a \leq t_d\) then    // time jump to next customer arrival event

  22.    \(e \leftarrow 1\)

  23.    \(t \leftarrow t_a\)

  24. else         // time jump to next customer departure event

  25.    \(e \leftarrow -1\)

  26.    \(t \leftarrow t_d\)

  27. end if

  28. if \(e = 1\) then     // update system status for customer arrival

  29.    \(T_o \leftarrow T_o \cup \{t\}\)

  30.    \(T_a \leftarrow T_a \cup \{t\}\)

  31.    if \(\varphi = 0\) then

  32.     \(e \leftarrow 0\)

  33.     \(i \leftarrow i + 1\)

  34.    else

  35.     \(Q \leftarrow Q \cup \{i+j+1\}\)

  36.     \(j \leftarrow j + 1\)

  37.    end if

  38.    \(t_a \leftarrow (i+j+1 \leq n) \ ? \ t + A_{i+j+1} : \infty\)

  39.    \(\Phi \leftarrow \Phi \cup \{\varphi\}\)

  40.    \(J \leftarrow J \cup \{j\}\)

  41. else if \(e = -1\) then// update system status for customer departure

  42.    \(T_o \leftarrow T_o \cup \{t\}\)

  43.    \(T_d \leftarrow T_d \cup \{t\}\)

  44.    if \(j = 0\) then

  45.     \(\varphi \leftarrow 0\)

  46.     \(t_d \leftarrow= \infty\)

  47.    else

  48.     \(e \leftarrow 0\)

  49.     \(i \leftarrow \text{pop}(Q)\)

  50.     \(j \leftarrow j - 1\)

  51.    end if

  52.    \(\Phi \leftarrow \Phi \cup \{\varphi\}\)

  53.    \(J \leftarrow J \cup \{j\}\)

  54.    \(k \leftarrow k + 1\)

  55. end if

  56. if \(e = 0\) then     // update system status for customer service intiation

  57.    \(T_o \leftarrow T_o \cup \{t\}\)

  58.    \(T_s \leftarrow T_s \cup \{t\}\)

  59.    \(\varphi \leftarrow 1\)

  60.    \(t_d \leftarrow t + S_i\)

  61.    \(\Phi \leftarrow \Phi \cup \{\varphi\}\)

  62.    \(J \leftarrow J \cup \{j\}\)

  63. end if

  64. end while

  65. \(d \leftarrow \sum_{i=1}^{n}(T_{s_i} - T_{a_i}) / n\)// average delay

  66. \(q \leftarrow \)// average queue length

  67. \(u \leftarrow \Phi \cdot T_o / t\)        // average utilization

  68. return d, q, u

Implementation#

def ssq(n, A, S, silent=False):
    """
    This function simulates a single-server queueing system for n customers given their inter-arrival times - A, and service times - S

    Parameters
    ---
    n: int
        number of customers
    A: list[float]
        customer inter-arrival times
    S: list[float]
        customer service times
    silent=False: bool
        run in silence if True else display system
    
    Returns
    ---
    d: float
        average delay
    q: float
        average queue length
    u: float
        average utilization
    """
    # System Status
    t  = 0              # simulation clock
    z  = 0              # server status
    Q  = []             # queue
    # System Dynamics
    Ta = []             # customer arrival time
    Ts = []             # customer service initiation time
    Td = []             # customer depature time
    # System Log
    To = [0]            # event time (customer arrival/service initiation/departure time)
    Z  = [0]            # server status at event time
    J  = [0]            # queue length at event time
    # Initialization
    i  = -1             # index of last customer serviced
    j  = 0              # queue length
    k  = 0              # customers serviced
    ta = A[0]           # next arrival event time
    td = A[0] + S[0]    # next departure event time
    # Loop
    while k < n:
        if ta <= td:    # time jump to next customer arrival event
            e = 1
            t = ta
        else:           # time jump to next customer departure event
            e = -1
            t = td
        if e == 1:      # update system status for customer arrival
            To.append(t)
            Ta.append(t)
            if z == 0:
                e = 0
                i = i + 1
            else:
                Q.append(i+j+1)
                j = j + 1
            ta = round(ta + A[i+j+1], 1) if i+j+1 < len(A) else float('inf')
            Z.append(z)
            J.append(j)
            if not silent: print("Arrival      :     ", " ", "X", i , Q)
        elif e == -1:   # update system status for customer departure
            if not silent: print("Departure    :   ", i, "X", " ", Q)
            To.append(t)
            Td.append(t)
            if j == 0:
                z  = 0
                td = float('inf')
            else:
                e  = 0
                i  = Q.pop(0) 
                j  = j - 1
            Z.append(z)
            J.append(j)
            k = k + 1
        if e == 0:      # update system status for customer service initiation
            To.append(t)
            Ts.append(t)
            z  = 1
            td = t + S[i]
            Z.append(z)
            J.append(j)
            if not silent: print("Service      :     ", " ", "X", i, Q)
    T = [To[i]-To[i-1] for i in range(1,len(To))] + [0.0]
    d = round(sum([Ts[i] - Ta[i] for i in range(n)]) / n, 3)        # average delay
    q = round(sum([J[i] * T[i] for i in range(len(T))]) / t, 3)     # average queue length
    u = round(sum([Z[i] * T[i] for i in range(len(T))]) / t, 3)     # average utilization
    return d, q, u
# Single Simulation Outcomes
n = 6
A = [0.4, 1.2, 0.5, 1.7, 0.2, 1.6, 0.2, 1.4, 1.9]
S = [2.0, 0.7, 0.2, 1.1, 3.7, 0.6, 1.0, 1.0, 1.0]
ssq(n, A, S)
Arrival      :        X 0 []
Service      :        X 0 []
Arrival      :        X 0 [1]
Arrival      :        X 0 [1, 2]
Departure    :    0 X   [1, 2]
Service      :        X 1 [2]
Departure    :    1 X   [2]
Service      :        X 2 []
Departure    :    2 X   []
Arrival      :        X 3 []
Service      :        X 3 []
Arrival      :        X 3 [4]
Departure    :    3 X   [4]
Service      :        X 4 []
Arrival      :        X 4 [5]
Arrival      :        X 4 [5, 6]
Arrival      :        X 4 [5, 6, 7]
Departure    :    4 X   [5, 6, 7]
Service      :        X 5 [6, 7]
Arrival      :        X 5 [6, 7, 8]
Departure    :    5 X   [6, 7, 8]
Service      :        X 6 [7, 8]
(0.95, 1.217, 0.902)
import random
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

def simulate(sims=1000, n=100, lam=0.8, mu=1.0):
    """
    Run Monte Carlo simulation of SSQ using exponential inter-arrivals and service times.
    
    Parameters
    ----------
    sims : int
        number of simulations (default=1000)
    n : int
        number of customers per run (default=100)
    lam : float
        arrival rate λ
    mu : float
        service rate μ
    
    Returns
    -------
    df : pandas.DataFrame
        Results with columns ['d','q','u']
    """
    # Random simulations
    results = []
    for k in range(sims):
        rng = random.Random(k)
        # Generate n+5 to avoid indexing issues in ssq
        A = [rng.expovariate(lam) for _ in range(n+5)]
        S = [rng.expovariate(mu) for _ in range(n+5)]
        d, q, u = ssq(n, A, S, silent=True)
        results.append((d, q, u))
    return pd.DataFrame(results, columns=["d","q","u"])

# Descriptive statistics
def describe_series(x: pd.Series):
    arr = np.asarray(x, dtype=float)
    mean = float(np.mean(arr))
    median = float(np.median(arr))
    rng = float(np.max(arr) - np.min(arr))
    sd = float(np.std(arr, ddof=1))
    iqr = np.percentile(arr, 75) - np.percentile(arr, 25)
    skew = np.mean((arr - mean)**3) / (sd**3) if sd > 0 else np.nan
    kurt = np.mean((arr - mean)**4) / (sd**4) - 3 if sd > 0 else np.nan
    return {
        "mean": mean, "median": median, "range": rng,
        "sd": sd, "iqr": iqr, "skewness": skew, "kurtosis": kurt
    }

def summarize_df(df: pd.DataFrame):
    return pd.DataFrame({col: describe_series(df[col]) for col in df.columns}).T

# Run and visualize
if __name__ == "__main__":
    df = simulate(sims=1000, n=1000, lam=0.8, mu=1.0)
    summary = summarize_df(df).round(4)
    print("\nSummary Statistics (1000 runs, n=100)\n")
    print(summary.to_string())
    print("\nNote: kurtosis is excess kurtosis.\n")
    
    # Histograms
    for metric in ["d","q","u"]:
        plt.figure()
        plt.hist(df[metric], bins=40)
        plt.title(f"Histogram of {metric}")
        plt.xlabel(metric)
        plt.ylabel("Frequency")
        plt.show()
Summary Statistics (1000 runs, n=100)

     mean  median   range      sd     iqr  skewness  kurtosis
d  4.0092  3.6670  17.552  1.6074  1.4693    3.6866   25.0216
q  3.2290  2.9295  15.411  1.3720  1.2185    3.8767   27.4364
u  0.7985  0.7980   0.252  0.0352  0.0470    0.1558    0.2841

Note: kurtosis is excess kurtosis.
../_images/50c11f006afc2ff421c7eebdc6422ba754160f5892a7cf1713f26af1f69df0de.png ../_images/f7253bc456b74eb1c146e1fe5c8911b20391b7460e01cecab256e1f08c12bc78.png ../_images/7463961ed59f0b0c079b2082dd223ebce860da222c0931f9c3e7233d2470ceb5.png