File size: 12,887 Bytes
aaef24a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
# /// script
# requires-python = ">=3.13"
# dependencies = [
#     "altair",
#     "asimpy",
#     "marimo",
#     "polars==1.24.0",
# ]
# ///

import marimo

__generated_with = "0.20.4"
app = marimo.App(width="medium")


@app.cell(hide_code=True)
def _():
    import marimo as mo
    import random
    import statistics
    import altair as alt
    import polars as pl
    from asimpy import Environment, Process, Resource

    return Environment, Process, Resource, alt, mo, pl, random, statistics


@app.cell(hide_code=True)
def _(mo):
    mo.md(r"""
    # Little's Law

    ## *The Universal Conservation Law of Queues*

    Little's Law states that in a stable system, L = λW, where:

    - L = mean number of customers in the system
    - λ = mean arrival rate
    - W = mean time a customer spends in the system

    The law holds regardless of arrival or service distributions, number of
    servers, or scheduling discipline.
    """)
    return


@app.cell(hide_code=True)
def _(mo):
    mo.md(r"""
    ## Why It Is Surprising

    Little's Law holds without any assumptions about the distribution of arrival rates or service times. It does not matter whether arrivals are Poisson, deterministic, or correlated, whether service times are exponential, constant, or heavy-tailed, whether there is one server or a hundred, or what scheduling discipline is used (FIFO, LIFO, random, or priority). As long as the system is stable and stationary, $L = \lambda W$. This universality is remarkable because almost every other formula in queueing theory *does* depend on distributional assumptions.
    """)
    return


@app.cell(hide_code=True)
def _(mo):
    mo.md(r"""
    ## Practical Use

    Because $L = \lambda W$ is universal, it can be used to measure hard-to-observe quantities from easy-to-observe ones. For example, the mean number of requests in a web server ($L$) and the observed request rate ($\lambda$) immediately give the mean response time ($W = L/\lambda$) without needing to instrument individual request latencies.
    """)
    return


@app.cell(hide_code=True)
def _(mo):
    mo.md(r"""
    ## Proof Sketch

    Consider a flow diagram where time runs horizontally and each customer traces a horizontal line from arrival to departure. The area under all lines equals both:

    - $\sum_i W_i$ (sum of individual sojourn times), and
    - $\int_0^T L(t)\,dt$ (integral of instantaneous queue length).

    Dividing both sides by $T$ and taking $T \to \infty$:

    $$\bar{L} = \lambda \bar{W}$$

    The argument is purely combinatorial: no probability is needed.
    """)
    return


@app.cell(hide_code=True)
def _(mo):
    mo.md(r"""
    ## Simulation Design

    The simulation verifies Little's Law across three configurations:

    - M/M/1 (Poisson arrivals, exponential service, one server)
    - M/D/1 (Poisson arrivals, deterministic service, one server)
    - M/M/3 (Poisson arrivals, exponential service, three servers)

    For each configuration, $L$ is measured two ways: by direct sampling of the
    queue length, and by computing $\lambda W$ from observed throughput and mean
    sojourn time.

    The scenarios below sweep arrival rate $\lambda$ over (0.5, 1.0, 1.5, 2.0, 2.5)
    at server capacities 2, 3, and 4. The `error_%` column shows how closely
    $L_{\text{Little}} = \lambda W$ matches the directly sampled $L_{\text{direct}}$.
    """)
    return


@app.cell
def _():
    SEED = 192           # random seed for reproducibility
    SIM_TIME = 1000      # simulated time units per scenario
    SAMPLE_INTERVAL = 1  # sim-time units between Monitor samples
    SERVICE_RATE = 1.0   # exponential service rate (mu) for random service

    return SAMPLE_INTERVAL, SEED, SERVICE_RATE, SIM_TIME


@app.cell(hide_code=True)
def _(mo):
    mo.md(r"""
    ## Random Processes
    """)
    return


@app.cell
def _(Process, SERVICE_RATE, random):
    class RandomCustomer(Process):
        def init(self, server, in_system, sojourn_times):
            self.server = server
            self.in_system = in_system
            self.sojourn_times = sojourn_times

        async def run(self):
            arrival = self.now
            self.in_system[0] += 1
            async with self.server:
                await self.timeout(random.expovariate(SERVICE_RATE))
            self.in_system[0] -= 1
            self.sojourn_times.append(self.now - arrival)

    return (RandomCustomer,)


@app.cell
def _(Process, RandomCustomer, random):
    class RandomArrivals(Process):
        def init(self, rate, server, in_system, sojourn_times):
            self.rate = rate
            self.server = server
            self.in_system = in_system
            self.sojourn_times = sojourn_times

        async def run(self):
            while True:
                await self.timeout(random.expovariate(self.rate))
                RandomCustomer(self._env, self.server, self.in_system, self.sojourn_times)

    return (RandomArrivals,)


@app.cell(hide_code=True)
def _(mo):
    mo.md(r"""
    ## Deterministic Processes
    """)
    return


@app.cell
def _(Process):
    DETERMINISTIC_SERVICE = 1.0  # fixed service time for M/D/1 scenarios

    class DeterministicCustomer(Process):
        def init(self, server, in_system, sojourn_times):
            self.server = server
            self.in_system = in_system
            self.sojourn_times = sojourn_times

        async def run(self):
            arrival = self.now
            self.in_system[0] += 1
            async with self.server:
                await self.timeout(DETERMINISTIC_SERVICE)
            self.in_system[0] -= 1
            self.sojourn_times.append(self.now - arrival)

    return DETERMINISTIC_SERVICE, DeterministicCustomer


@app.cell
def _(DeterministicCustomer, Process, random):
    class DeterministicArrivals(Process):
        def init(self, rate, server, in_system, sojourn_times):
            self.rate = rate
            self.server = server
            self.in_system = in_system
            self.sojourn_times = sojourn_times

        async def run(self):
            while True:
                await self.timeout(random.expovariate(self.rate))
                DeterministicCustomer(self._env, self.server, self.in_system, self.sojourn_times)

    return (DeterministicArrivals,)


@app.cell
def _(Process, SAMPLE_INTERVAL):
    class Monitor(Process):
        def init(self, in_system, samples):
            self.in_system = in_system
            self.samples = samples

        async def run(self):
            while True:
                self.samples.append(self.in_system[0])
                await self.timeout(SAMPLE_INTERVAL)

    return (Monitor,)


@app.cell
def _(Environment, Monitor, Resource, SIM_TIME, statistics):
    def run_scenario(lam, capacity, arrivals_cls):
        in_system = [0]
        sojourns = []
        samples = []
        env = Environment()
        server = Resource(env, capacity=capacity)
        arrivals_cls(env, lam, server, in_system, sojourns)
        Monitor(env, in_system, samples)
        env.run(until=SIM_TIME)
        L_direct = statistics.mean(samples)
        W = statistics.mean(sojourns)
        lam_obs = len(sojourns) / SIM_TIME
        L_little = lam_obs * W
        error = 100.0 * (L_little - L_direct) / L_direct
        return {
            "lambda": round(lam_obs, 3),
            "capacity": capacity,
            "W": round(W, 3),
            "L_direct": round(L_direct, 3),
            "L_little": round(L_little, 3),
            "error_%": round(error, 2),
        }

    return (run_scenario,)


@app.cell(hide_code=True)
def _(mo):
    mo.md(r"""
    ## Verification: L = λW
    """)
    return


@app.cell
def _(RandomArrivals, SEED, pl, random, run_scenario):
    random.seed(SEED)
    rows = []
    for lam in (0.5, 1.0, 1.5, 2.0, 2.5):
        for capacity in (2, 3, 4):
            rows.append(run_scenario(lam, capacity, RandomArrivals))

    df = pl.DataFrame(rows)
    df
    return (df,)


@app.cell
def _(alt, df, pl):
    points = (
        alt.Chart(df)
        .mark_point(size=100, filled=True)
        .encode(
            x=alt.X("L_direct:Q", title="L (direct sample)"),
            y=alt.Y("L_little:Q", title="L = λW (Little's Law)"),
            color=alt.Color("capacity:O", title="Capacity"),
            tooltip=["lambda:Q", "capacity:O", "L_direct:Q", "L_little:Q", "error_%:Q"],
        )
    )
    max_val = max(df["L_direct"].to_list()) * 1.1
    diagonal = (
        alt.Chart(pl.DataFrame({"x": [0.0, max_val], "y": [0.0, max_val]}))
        .mark_line(color="gray", strokeDash=[4, 4])
        .encode(x="x:Q", y="y:Q")
    )
    (diagonal + points).properties(title="Little's Law: Direct Sample vs. λW")
    return


@app.cell(hide_code=True)
def _(mo):
    mo.md(r"""
    ## The Error Column

    The large error at $\lambda = 2.5$, capacity $= 2$ is a stability problem, not a simulation bug.
    Little's Law only holds in steady state. For an M/M/c queue, steady state requires
    that the arrival rate $\lambda$ be less than capacity times service rate $\mu$.
    With `SERVICE_RATE = 1.0` and `capacity = 2`,
    the maximum sustainable throughput is $2 \times 1.0 = 2.0$.
    At $\lambda = 2.5$, the load exceeds service capacity, so the queue grows without bound.
    By the time the simulation ends,
    hundreds of customers are waiting in queue, and their sojourns are never recorded.

    ## Key Points

    1. `Monitor` samples `in_system[0]` every `SAMPLE_INTERVAL` time units to
       estimate $L$ directly without any queueing formula.

    2. The `error_%` column shows that $L_{\text{direct}}$ and $\lambda W$ agree to within
       less than 1% for all stable configurations, even though the service-time
       distributions are completely different.

    3. `DeterministicCustomer` uses the fixed `DETERMINISTIC_SERVICE` constant
       rather than a random draw; everything else in the simulation is unchanged.
       The law still holds.

    4. `Resource(env, capacity=3)` creates a three-slot server for M/M/3.
       Setting the arrival rate to 2.4 gives utilization 0.8 per server.
    """)
    return


@app.cell(hide_code=True)
def _(mo):
    mo.md(r"""
    ## Understanding the Math

    ### The area argument made concrete

    Draw a horizontal time axis from $t = 0$ to $t = T$. Each customer gets a horizontal bar starting at their arrival time and ending at their departure time. The length of their bar is exactly their sojourn time $W_i$ — the total time they spend in the system. At any moment $t$, the number of bars that cross that vertical slice is exactly $L(t)$, the instantaneous number of customers in the system.

    Now compute the total area under all the bars in two different ways. First, add up the lengths of all the bars: total area $= \sum_i W_i$. Second, integrate the height of the stack over time: total area $= \int_0^T L(t)\,dt$. These are the same area, so $\sum_i W_i = \int_0^T L(t)\,dt$.

    Divide both sides by $T$. The right side becomes the time-average $\bar{L}$. The left side becomes $(n/T) \cdot \bar{W}$, where $n$ is the total number of customers and $\bar{W}$ is their mean sojourn time. As $T \to \infty$, $n/T \to \lambda$ (the long-run arrival rate). That gives $\bar{L} = \lambda \bar{W}$, which is Little's Law.

    ### No distribution required

    The argument above uses only geometry. There is no probability distribution, no exponential assumption, no Poisson process. The shape of each bar (i.e., how long each customer takes) can be anything. This is why the law applies to M/M/1, M/D/1, M/M/3, and every other configuration equally.

    ### Using it in practice

    Suppose you run a web service. Your monitoring dashboard shows $\lambda = 500$ requests per second and your server logs show a mean response time of $W = 20$ milliseconds. Little's Law immediately tells you that the mean number of active requests in the system is $L = \lambda W = 500 \times 0.02 = 10$ requests. Alternatively, if you observe $L$ and $\lambda$ but not individual response times, you get $W = L/\lambda$ without any per-request timing instrumentation.

    ### Units check

    $\lambda$ has units of customers per unit time; $W$ has units of time; so $L = \lambda W$ is dimensionless — a pure count of customers. Always verify units when applying Little's Law to a new problem: if your units do not cancel correctly, you have applied the law incorrectly.

    ### Stability condition

    Little's Law requires the system to reach steady state: over the long run, arrivals and departures must balance. If $\lambda > \mu$ (the arrival rate exceeds the service rate), the queue grows without bound. $L = \infty$ and $W = \infty$; the law still holds, but it tells you the system is broken, not that it is well-behaved.
    """)
    return


if __name__ == "__main__":
    app.run()