16.5 Simulation in software
Software 16.5.1 (Random numbers and Monte Carlo). The Park–Miller generator, checked for mean and variance; exponential variates by the inverse transform; and a Monte Carlo estimate of \(\int _0^1e^{-v^2}\,dv\) with its \(95\%\) half-width, against the exact value \(\frac {\sqrt \pi }2\operatorname {erf}(1)\):
>>> import numpy as np >>> def lcg(seed, n, a=16807, m=2**31 - 1): # Park and Miller's generator ... x, out = seed, [] ... for _ in range(n): ... x = (a * x) % m; out.append(x / m) ... return np.array(out) ... >>> u = lcg(12345, 100000) >>> round(u.mean(), 4), round(u.var(), 4) # uniform: 1/2 and 1/12 = 0.0833 (0.5, 0.0834) >>> x = -np.log(1 - u) / 2.0 # inverse transform: exponential, rate 2 >>> round(x.mean(), 4), round(x.std(), 4) # both 1/2 (0.5008, 0.502) >>> f = lambda v: np.exp(-v**2) # Monte Carlo: int_0^1 exp(-v^2) dv >>> fx = f(u); est = fx.mean(); half = 1.96 * fx.std(ddof=1) / np.sqrt(len(u)) >>> round(est, 5), round(half, 5) # estimate and 95% half-width (0.74684, 0.00125) >>> from scipy.special import erf >>> round(np.sqrt(np.pi) / 2 * erf(1), 5) # the exact value 0.74682
Reading the output. The \(100\,000\) uniforms have mean \(0.5000\) and variance \(0.0834\) (exact \(\frac 1{12} = 0.0833\)); the exponentials with rate \(2\) have mean and standard deviation \(0.50\), as they should. The integral is estimated as \(0.74684 \pm 0.00125\), and the exact \(0.74682\) lies well inside.
MATLAB/Octave: rand, -log(1-u)/2, mean and std.
Software 16.5.2 (A discrete-event simulation). An event-driven simulation of the bank of Example 14.2.2 with \(3\) tellers, the event list kept as a heap; \(30\) independent replications of \(2000\) hours, and a \(95\%\) confidence interval for the mean wait in minutes, using \(t_{0.975, 29} = 2.045\):
>>> import heapq >>> def bank(c, lam, mu, T, rng): # event-driven M/M/c: mean wait ... events = [(rng.exponential(1 / lam), 'arrive')] ... free, queue, waits = c, [], [] ... while events: ... t, kind = heapq.heappop(events) ... if t > T: break ... if kind == 'arrive': ... nxt = t + rng.exponential(1 / lam) ... heapq.heappush(events, (nxt, 'arrive')) ... if free: # a teller is free: serve at once ... free -= 1; waits.append(0.0) ... done = t + rng.exponential(1 / mu) ... heapq.heappush(events, (done, 'end')) ... else: queue.append(t) ... elif queue: # a service ends: take the next ... waits.append(t - queue.pop(0)) ... heapq.heappush(events, (t + rng.exponential(1 / mu), 'end')) ... else: free += 1 ... return np.mean(waits) ... >>> rng = np.random.default_rng(7) >>> reps = np.array([bank(3, 24, 10, 2000, rng) for _ in range(30)]) * 60 >>> m, h = reps.mean(), 2.045 * reps.std(ddof=1) / np.sqrt(len(reps)) >>> round(m, 3), round(h, 3) # minutes; Erlang C gives 6.47 (6.417, 0.127)
Reading the output. The mean wait is \(6.42 \pm 0.13\) minutes, consistent with the Erlang C value \(6.47\). With the program verified, it could now be changed in ways no formula covers: tellers taking breaks, customers giving up after a while, or a separate counter for business accounts.
MATLAB/Octave: a priority queue of events, or the SimEvents toolbox.
Software 16.5.3 (Variance reduction). Estimating \(\mathbb E[e^U]\) from \(2n\) independent uniforms against \(n\) antithetic pairs (the same number of function evaluations), with the variances of the two estimates; then the difference in expected newsvendor profit between orders of \(195\) and \(210\), estimated with independent and with common demands:
>>> rng = np.random.default_rng(3) >>> g = lambda v: np.exp(v) # estimate E exp(U) = e - 1 >>> n = 10000; U = rng.uniform(size=n) >>> plain = g(rng.uniform(size=2 * n)) # 2n independent draws >>> anti = (g(U) + g(1 - U)) / 2 # n antithetic pairs, same effort >>> np.e - 1, round(plain.mean(), 5), round(anti.mean(), 5) (1.718281828459045, 1.72028, 1.7194) >>> round(plain.var() / (2 * n), 9), round(anti.var() / n, 9) # variances (1.2057e-05, 4.01e-07) >>> d1 = rng.normal(200, 40, 5000); d2 = rng.normal(200, 40, 5000) >>> def prof(q, d): # newsvendor profit of Section 13 ... return 10 * np.minimum(d, q) + np.maximum(q - d, 0) - 6 * q ... >>> indep = prof(195, d1) - prof(210, d2) # compare two orders, independently >>> crn = prof(195, d1) - prof(210, d1) # ... and with the same demands >>> round(indep.mean(), 2), round(indep.std() / np.sqrt(5000), 3) (6.0, 4.448) >>> round(crn.mean(), 2), round(crn.std() / np.sqrt(5000), 3) (11.88, 0.905)
Reading the output. Both estimates of \(e - 1 = 1.71828\) are close, but the antithetic one has variance \(4.0 \times 10^{-7}\) against \(1.2 \times 10^{-5}\), a factor of \(30\). For the policy comparison, independent samples give a difference of \(6.0\) with standard error \(4.4\), which cannot even tell which order is better; common random numbers give \(11.9\) with standard error \(0.9\), clearly favouring the order of \(195\), the one near the critical fractile.
MATLAB/Octave: the same vector operations.
Questions on this section
Stuck on something here? Ask below and it stays attached to this topic.