codex/queue-utilization-delay.py

queue-utilization-delay.py
"""Draw exact M/M/1 congestion curves; write the PNG to the CWD.

Tested with Python 3.14.4, matplotlib 3.10.7, and numpy 2.3.5.
Run from _media: MPLBACKEND=Agg python3 ../queue-utilization-delay.py
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    mu = 5.  # customers per hour
    rho = np.linspace(0, .98, 801)
    population = rho / (1 - rho)
    waiting_population = rho**2 / (1 - rho)
    duration = 1 / (mu * (1 - rho))
    waiting_duration = rho / (mu * (1 - rho))
    assert np.allclose(population - waiting_population, rho)
    assert np.allclose(duration - waiting_duration, 1 / mu)
    assert np.allclose(mu * rho * duration, population)

    plt.rcParams.update({'font.size': 12, 'axes.titlesize': 14})
    blue, orange = '#246da4', '#d0802a'
    fig, (count, delay) = plt.subplots(1, 2, figsize=(10, 5.3), dpi=120,
                                     facecolor='white')
    count.plot(rho, population, color=blue, linewidth=2.5, label='L: whole system')
    count.plot(rho, waiting_population, color=orange, linestyle='--',
               linewidth=2, label='Lq: waiting only')
    delay.plot(rho, duration, color=blue, linewidth=2.5, label='W: whole system')
    delay.plot(rho, waiting_duration, color=orange, linestyle='--',
               linewidth=2, label='Wq: waiting only')
    for load, customer_count, hours in [(0.8, 4, 1), (0.9, 9, 2), (0.98, 49, 10)]:
        for ax, y in [(count, customer_count), (delay, hours)]:
            ax.scatter(load, y, color=blue, s=34, zorder=4)
            suffix = ' customers' if ax is count else ' h'
            offset = (-108, 1) if load == .98 else (-100, 5)
            ax.annotate(f'ρ = {load:g}: {y:g}{suffix}', (load, y),
                        xytext=offset, textcoords='offset points', fontsize=10,
                        color='#16496f',
                        arrowprops={'arrowstyle': '-', 'color': '#768b9b'})
    count.set(ylabel='Mean customers', title='Population', ylim=(0, 55))
    delay.set(ylabel='Mean duration (hours)', title='Time per customer', ylim=(0, 11.3))
    for ax in (count, delay):
        ax.axvline(1, color='#8f5353', linestyle=':', linewidth=1.5)
        ax.set(xlim=(0, 1.025), xlabel='Server utilization ρ = λ / μ',
               xticks=[0, .2, .4, .6, .8, 1])
        ax.legend(loc='upper left', frameon=False, fontsize=10)
        ax.grid(color='#e4e8ec', linewidth=.8)
        ax.spines[['top', 'right']].set_visible(False)
    fig.suptitle('M/M/1 congestion: a small capacity margin means a long wait',
                 fontsize=16, y=.97)
    fig.text(.5, .045, 'μ = 5 customers/hour; mean service time = 12 min.  '
             'Curves stop at ρ = 0.98; no stationary law at ρ ≥ 1.',
             ha='center', fontsize=10.5)
    fig.subplots_adjust(left=.075, right=.975, top=.82, bottom=.19, wspace=.30)
    fig.savefig(Path.cwd() / Path(__file__).with_suffix('.png').name,
                facecolor='white', transparent=False)
    plt.close(fig)


if __name__ == '__main__':
    main()