codex/little-law-sample-path.py

little-law-sample-path.py
"""Draw a deterministic Little's-law example; 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 ../little-law-sample-path.py
"""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Patch


def main():
    arrivals = np.array([0., 1., 4.])
    starts = np.array([0., 2., 4.])
    departures = np.array([2., 3., 5.])
    times = np.array([0., 1., 2., 3., 4., 5., 6.])
    midpoints = (times[:-1] + times[1:]) / 2
    population = np.sum((arrivals[:, None] <= midpoints)
                        & (midpoints < departures[:, None]), axis=0)
    area = np.dot(np.diff(times), population)
    assert area == 5 == np.sum(departures - arrivals)
    assert np.sum(starts - arrivals) == 1
    assert np.sum(departures - starts) == 4

    plt.rcParams.update({'font.size': 12, 'axes.titlesize': 14})
    blue, orange = '#246da4', '#d0802a'
    fig, (occupancy, visits) = plt.subplots(
        2, 1, figsize=(8.4, 6.5), dpi=120, sharex=True,
        gridspec_kw={'height_ratios': [1, 1.2]}, facecolor='white')
    occupancy.stairs(population, times, fill=True, color=blue, alpha=.2)
    occupancy.stairs(population, times, color=blue, linewidth=2.4)
    for left, n in zip(times[:-1], population):
        if n:
            occupancy.text(left + .5, n / 2, str(n), ha='center', va='center',
                           color='#16496f', fontsize=14, weight='bold')
    occupancy.text(3.2, 1.9, 'Area = 5 customer-minutes\nL = 5/6 customer',
                   fontsize=13, color='#16496f', va='center',
                   bbox={'facecolor': 'white', 'edgecolor': 'none'})
    occupancy.set(ylim=(0, 2.6), yticks=[0, 1, 2], ylabel='Customers N(t)',
                  title='Count by time: height × duration')

    for row, name, arrival, start, departure in zip(
            [3, 2, 1], ['A', 'B', 'C'], arrivals, starts, departures):
        if start > arrival:
            visits.broken_barh([(arrival, start - arrival)], (row - .22, .44),
                              facecolors=orange)
        visits.broken_barh([(start, departure - start)], (row - .22, .44),
                          facecolors=blue)
        visits.text((arrival + departure) / 2, row + .3,
                    f'{departure - arrival:g} min in system',
                    ha='center', fontsize=11)
        visits.plot([arrival, departure], [row, row], 'o', color='#18364d',
                    markersize=4)
    visits.text(5.9, 2.65, '2 + 2 + 1 = 5\ncustomer-minutes', ha='right',
                color='#16496f', fontsize=12)
    visits.set(yticks=[1, 2, 3], yticklabels=['C', 'B', 'A'], ylim=(.3, 3.8),
               xlim=(-.1, 6.1), xticks=times, xlabel='Time (minutes)',
               ylabel='Customer', title='Count by customer: duration of each visit')
    visits.legend(handles=[Patch(color=blue, label='Service: total 4 min'),
                           Patch(color=orange, label='Waiting: total 1 min')],
                  loc='lower left', frameon=False, fontsize=10)
    for ax in (occupancy, visits):
        ax.grid(axis='x', color='#dde3e8', linewidth=.8)
        ax.set_axisbelow(True)
        ax.spines[['top', 'right']].set_visible(False)
    fig.suptitle("Little's law without random arrivals", fontsize=17, y=.98)
    fig.text(.5, .035, '3 customers / 6 minutes × 5/3 minutes per customer = 5/6 customer',
             ha='center', fontsize=11)
    fig.subplots_adjust(left=.105, right=.975, top=.87, bottom=.14, hspace=.42)
    fig.savefig(Path.cwd() / Path(__file__).with_suffix('.png').name,
                facecolor='white', transparent=False)
    plt.close(fig)


if __name__ == '__main__':
    main()