CoolFace
Apppublic

marimo-team/marimo-learn

sourceHugging Faceupdated 5mo agoView on Hugging Face
3likes
02_queue_formation.py308 linesDownload Raw Back to queueing
1# /// script2# requires-python = ">=3.13"3# dependencies = [4#     "altair",5#     "asimpy",6#     "marimo",7#     "polars==1.24.0",8# ]9# ///10 11import marimo12 13__generated_with = "0.20.4"14app = marimo.App(width="medium")15 16 17@app.cell(hide_code=True)18def _():19    import marimo as mo20    import random21    import statistics22 23    import altair as alt24    import polars as pl25 26    from asimpy import Environment, Process, Resource27 28    return Environment, Process, Resource, alt, mo, pl, random, statistics29 30 31@app.cell(hide_code=True)32def _(mo):33    mo.md(r"""34    # Queue Formation35 36    ## *Randomness Creates Waiting Even with Spare Capacity*37 38    We now combine arrivals (Poisson at rate $\lambda$) with a server (exponential service at rate $\mu$) into a complete queue.  The system is stable because $\rho = \lambda/\mu < 1$, which means that on average, the server handles more work than arrives.39 40    Our first question is, how long is the queue? The surprising answer is that even when the server has plenty of spare capacity, customers wait.  The mean number of customers in the system (both waiting and being served) is:41 42    $$L = \frac{\rho}{1 - \rho}$$43 44    The table below gives some representative values:45 46    | $\rho$ | $L$ |47    |:---:|:---:|48    | 0.1 | 0.11 |49    | 0.5 | 1.00 |50    | 0.8 | 4.00 |51    | 0.9 | 9.00 |52 53    When $\rho = 0.5$, half the server's capacity is idle, but there is on average one customer in the system at any moment.  That customer either had to wait for a previous customer, or is currently being served.  The queue is *never* consistently empty, even at moderate load.54 55    The formula also explains why simple queues are so sensitive to utilization: $L$ blows up as $\rho \to 1$. One way to think about this is that the denominator $(1 - \rho)$ is the spare capacity. As spare capacity vanishes, queue length increases.56    """)57    return58 59 60@app.cell61def _(mo):62    mo.md(r"""63    ## Why Queues Form at All64 65    With deterministic arrivals and service (every customer arrives exactly $1/\lambda$ apart and takes exactly $1/\mu$), a server with $\rho < 1$ would never form a queue: each customer would depart before the next arrived. Randomness changes this. Sometimes three customers arrive close together before the server finishes even one, so the server falls briefly behind. While it recovers, customers wait.  These temporary pileups are unavoidable whenever inter-arrival or service times have any variance.66 67    The probability that exactly $n$ customers are in an M/M/1 system at steady state is:68 69    $$P(N = n) = (1 - \rho)\,\rho^n \qquad n = 0, 1, 2, \ldots$$70 71    This is a *geometric distribution* with success probability $1 - \rho$. The formula says the server is idle (i.e., n=0) with probability $1 - \rho$, which is consistent with the utilization result from the previous scenario. Each additional customer in the system is $\rho$ times less likely than the previous count.72 73    This formula $L = \rho/(1-\rho)$ is the foundation of the later M/M/1 nonlinearity scenario, which shows the practical consequences of the $(1-\rho)$ denominator. Every queue-length formula in queueing theory has a similar structure: a traffic factor $\rho$ divided by a spare-capacity factor $(1 - \rho)$, possibly multiplied by a variability correction.74    """)75    return76 77 78@app.cell79def _(mo):80    mo.md(r"""81    ## Implementation82 83    A `Customer` process increments a shared `in_system` counter on arrival and decrements it on departure.  A `Monitor` process samples `in_system[0]` every `SAMPLE_INTERVAL` time units.  After the simulation, the mean of the samples estimates $L$.  The theoretical value $\rho/(1-\rho)$ is computed and compared. By the law of large numbers, this converges to the true steady-state mean as the simulation time grows.84 85    The simulation sweeps $\rho$ from 0.1 to 0.9, confirming the formula at each load level.86    """)87    return88 89 90@app.cell(hide_code=True)91def _(mo):92    sim_time_slider = mo.ui.slider(93        start=0,94        stop=100_000,95        step=1_000,96        value=20_000,97        label="Simulation time",98    )99 100    service_rate_slider = mo.ui.slider(101        start=1.0,102        stop=5.0,103        step=0.01,104        value=2.0,105        label="Service rate",106    )107 108    sample_interval_slider = mo.ui.slider(109        start=1.0,110        stop=5.0,111        step=1.0,112        value=1.0,113        label="Sample interval",114    )115 116    seed_input = mo.ui.number(117        value=192,118        step=1,119        label="Random seed",120    )121 122    run_button = mo.ui.run_button(label="Run simulation")123 124    mo.vstack([125        sim_time_slider,126        service_rate_slider,127        sample_interval_slider,128        seed_input,129        run_button,130    ])131    return (132        sample_interval_slider,133        seed_input,134        service_rate_slider,135        sim_time_slider,136    )137 138 139@app.cell140def _(141    sample_interval_slider,142    seed_input,143    service_rate_slider,144    sim_time_slider,145):146    SIM_TIME = int(sim_time_slider.value)147    SERVICE_RATE = float(service_rate_slider.value)148    SAMPLE_INTERVAL = float(sample_interval_slider.value)149    SEED = int(seed_input.value)150    return SAMPLE_INTERVAL, SEED, SERVICE_RATE, SIM_TIME151 152 153@app.cell154def _(Process, SERVICE_RATE, random):155    class Customer(Process):156        def init(self, server, in_system):157            self.server = server158            self.in_system = in_system159 160        async def run(self):161            self.in_system[0] += 1162            async with self.server:163                await self.timeout(random.expovariate(SERVICE_RATE))164            self.in_system[0] -= 1165 166    return (Customer,)167 168 169@app.cell170def _(Customer, Process, random):171    class Arrivals(Process):172        def init(self, rate, server, in_system):173            self.rate = rate174            self.server = server175            self.in_system = in_system176 177        async def run(self):178            while True:179                await self.timeout(random.expovariate(self.rate))180                Customer(self._env, self.server, self.in_system)181 182    return (Arrivals,)183 184 185@app.cell186def _(Process, SAMPLE_INTERVAL):187    class Monitor(Process):188        """Samples total customers in system at regular intervals."""189 190        def init(self, in_system, samples):191            self.in_system = in_system192            self.samples = samples193 194        async def run(self):195            while True:196                self.samples.append(self.in_system[0])197                await self.timeout(SAMPLE_INTERVAL)198 199    return (Monitor,)200 201 202@app.cell203def _(204    Arrivals,205    Environment,206    Monitor,207    Resource,208    SERVICE_RATE,209    SIM_TIME,210    statistics,211):212    def simulate(rho):213        arrival_rate = rho * SERVICE_RATE214        env = Environment()215        server = Resource(env, capacity=1)216        in_system = [0]217        samples = []218        Arrivals(env, arrival_rate, server, in_system)219        Monitor(env, in_system, samples)220        env.run(until=SIM_TIME)221        sim_L = statistics.mean(samples)222        theory_L = rho / (1.0 - rho)223        return {224            "rho": rho,225            "sim_L": round(sim_L, 4),226            "theory_L": round(theory_L, 4),227            "error_pct": round(100.0 * (sim_L - theory_L) / theory_L, 2),228        }229 230    return (simulate,)231 232 233@app.cell234def _(SEED, pl, random, simulate):235    def sweep():236        rows = [simulate(rho) for rho in [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]]237        return pl.DataFrame(rows)238 239    random.seed(SEED)240    df = sweep()241    df242    return (df,)243 244 245@app.cell246def _(alt, df):247    df_plot = df.unpivot(248        on=["sim_L", "theory_L"],249        index="rho",250        variable_name="source",251        value_name="L",252    )253    chart = (254        alt.Chart(df_plot)255        .mark_line(point=True)256        .encode(257            x=alt.X("rho:Q", title="Utilization (ρ)"),258            y=alt.Y("L:Q", title="Mean customers in system (L)"),259            color=alt.Color("source:N", title="Source"),260            tooltip=["rho:Q", "source:N", "L:Q"],261        )262        .properties(title="Queue Formation: Simulated vs. Theoretical L = ρ/(1−ρ)")263    )264    chart265    return266 267 268@app.cell269def _(mo):270    mo.md(r"""271    ## Understanding the Math272 273    ### Why is the queue length geometric?274 275    The M/M/1 queue can be analyzed as a random walk on the non-negative integers.  When the server is busy, the queue grows by 1 with each arrival (with rate $\lambda$) and shrinks by 1 with each service completion (with rate $\mu$).  The ratio $\lambda/\mu = \rho$ is the probability that the queue grows rather than shrinks at the next event. Under steady state, the probability of being at level $n$ is proportional to $\rho^n$ — because reaching level $n$ requires $n$ consecutive "up" steps. Normalizing so the probabilities sum to 1 gives $(1-\rho)\rho^n$.276 277    ### Deriving $L = \rho/(1-\rho)$ from the geometric distribution278 279    Given $P(N = n) = (1 - \rho)\rho^n$, the mean is:280 281    $$L = E[N] = \sum_{n=0}^{\infty} n \cdot (1-\rho)\rho^n = (1-\rho) \sum_{n=0}^{\infty} n\rho^n$$282 283    A result from basic calculus is that the geometric series $\sum_{n=0}^{\infty} \rho^n = 1/(1-\rho)$.  Differentiating both sides with respect to $\rho$:284 285    $$\sum_{n=0}^{\infty} n\rho^{n-1} = \frac{1}{(1-\rho)^2}$$286 287    Multiply both sides by $\rho$:288 289    $$\sum_{n=0}^{\infty} n\rho^n = \frac{\rho}{(1-\rho)^2}$$290 291    Substituting back:292 293    $$L = (1-\rho) \cdot \frac{\rho}{(1-\rho)^2} = \frac{\rho}{1-\rho}$$294 295    ### Checking the formula at the boundaries296 297    When $\rho \to 0$: there are almost no arrivals, so $L \to 0$, i.e., the server is nearly always idle.298 299    When $\rho \to 1$: the spare capacity is $(1-\rho) \to 0$, so $L \to \infty$, i.e., the queue grows without bound.300 301    Both limits match physical intuition.  Note that the formula is exact (not an approximation) for an M/M/1 queues in steady state.302    """)303    return304 305 306if __name__ == "__main__":307    app.run()308