Monte Carlo Estimation
Random darts, converging on pi.
Beginner / probability / sampling / convergence
Throw uniform darts at a unit square; the fraction inside the quarter circle estimates pi over four. The estimate error falls like one over root N - agonizingly slow, which is the Monte Carlo lesson. The running absolute error against true pi is displayed, exponent and all.
Tagged: pi estimate, variance, inverse sqrt, uniform sampling
Live simulation
More darts, smaller error - falling like one over root N. Keyboard: Space runs or pauses, . steps, r resets.
Speed
More darts, smaller error - falling like one over root N.
Mathematics
Monte Carlo estimation turns a deterministic quantity into a sampling problem: the fraction of uniform darts inside a quarter circle estimates pi/4, with standard error falling like 1/sqrt(N). The price of generality is that slow convergence rate - four times the darts buys half the error.
estimate: pi_hat = 4 * (hits / N)
standard error: sigma / sqrt(N) with sigma ~ 1.64 for this estimator
running error: |pi_hat - pi|
The displayed absolute error against true pi fluctuates - sometimes getting worse for a stretch - because the convergence rate is statistical, not monotone. That honesty matters: Monte Carlo methods buy generality with variance, and the only cure is more samples.
How it works
Each tick throws a fixed number of seeded darts; the display buffer keeps the most recent ones so the point cloud stays readable while the statistics accumulate over every draw ever made. The quarter-circle arc, the square, and the in/out coloring make the estimator's geometry literal.
At 400 darts per tick, a hundred ticks is 40,000 darts - error around the 1% mark, exactly as 1/sqrt(N) predicts.
Implementation
src/engine/experiments/monte-carlo.ts · 111 lines · the module running above
TypeScript · fixed-timestep engine
| 1 | import { BaseExperiment } from "../core/base-experiment"; |
| 2 | import type { ParameterDef, ThemeColors } from "../core/types"; |
| 3 | |
| 4 | const CAPACITY = 20000; |
| 5 | |
| 6 | /** |
| 7 | * Monte Carlo estimation of pi: uniform darts on the unit square, the ratio |
| 8 | * inside the quarter circle converges to pi/4. The error against the true |
| 9 | * pi should fall like 1/sqrt(N) - the metric shows the actual exponent. |
| 10 | */ |
| 11 | export class MonteCarloExperiment extends BaseExperiment { |
| 12 | readonly id = "monte-carlo"; |
| 13 | |
| 14 | private xs = new Float32Array(CAPACITY); |
| 15 | private ys = new Float32Array(CAPACITY); |
| 16 | private inside = new Uint8Array(CAPACITY); |
| 17 | private darts = 0; |
| 18 | private totalHits = 0; |
| 19 | private estimate = 0; |
| 20 | private error = 0; |
| 21 | |
| 22 | protected params(): ParameterDef[] { |
| 23 | return [ |
| 24 | { key: "rate", label: "Darts per tick", min: 20, max: 3000, step: 20, defaultValue: 400 }, |
| 25 | { |
| 26 | key: "show", |
| 27 | label: "Show darts", |
| 28 | options: [ |
| 29 | { value: "recent", label: "Recent" }, |
| 30 | { value: "none", label: "None" }, |
| 31 | ], |
| 32 | defaultValue: "recent", |
| 33 | }, |
| 34 | ]; |
| 35 | } |
| 36 | |
| 37 | protected onReset(): void { |
| 38 | this.darts = 0; |
| 39 | this.totalHits = 0; |
| 40 | this.estimate = 0; |
| 41 | this.error = 0; |
| 42 | } |
| 43 | |
| 44 | protected onUpdate(): void { |
| 45 | const rate = this.num("rate"); |
| 46 | const slots = Math.min(CAPACITY, rate); |
| 47 | // Fill the display buffer round-robin; statistics use all draws ever. |
| 48 | for (let i = 0; i < rate; i++) { |
| 49 | const x = this.rng(); |
| 50 | const y = this.rng(); |
| 51 | const hit = x * x + y * y <= 1 ? 1 : 0; |
| 52 | const slot = this.darts % CAPACITY; |
| 53 | this.xs[slot] = x; |
| 54 | this.ys[slot] = y; |
| 55 | this.inside[slot] = hit; |
| 56 | this.totalHits += hit; |
| 57 | this.darts += 1; |
| 58 | } |
| 59 | void slots; |
| 60 | this.estimate = this.darts > 0 ? (4 * this.totalHits) / this.darts : 0; |
| 61 | this.error = Math.abs(this.estimate - Math.PI); |
| 62 | } |
| 63 | |
| 64 | render(ctx: CanvasRenderingContext2D, theme: ThemeColors): void { |
| 65 | ctx.fillStyle = theme.bg; |
| 66 | ctx.fillRect(0, 0, this.width, this.height); |
| 67 | const size = Math.min(this.width, this.height) - 32; |
| 68 | const ox = (this.width - size) / 2; |
| 69 | const oy = (this.height - size) / 2; |
| 70 | // Quarter circle arc (unit circle from the corner) |
| 71 | ctx.strokeStyle = theme.accent; |
| 72 | ctx.lineWidth = 1.6; |
| 73 | ctx.beginPath(); |
| 74 | ctx.arc(ox, oy + size, size, 1.5 * Math.PI, 2 * Math.PI); |
| 75 | ctx.stroke(); |
| 76 | ctx.strokeStyle = theme.line; |
| 77 | ctx.strokeRect(ox, oy, size, size); |
| 78 | // Recent darts |
| 79 | if (this.str("show") === "recent") { |
| 80 | const shown = Math.min(this.darts, 2600); |
| 81 | for (let s = 0; s < shown; s++) { |
| 82 | const slot = (this.darts - 1 - s + CAPACITY * 4) % CAPACITY; |
| 83 | const px = ox + this.xs[slot] * size; |
| 84 | const py = oy + (1 - this.ys[slot]) * size; |
| 85 | ctx.fillStyle = this.inside[slot] ? theme.accent : theme.viz[1]; |
| 86 | ctx.globalAlpha = 0.7; |
| 87 | ctx.fillRect(px - 1, py - 1, 2, 2); |
| 88 | } |
| 89 | ctx.globalAlpha = 1; |
| 90 | } |
| 91 | ctx.lineWidth = 1; |
| 92 | } |
| 93 | |
| 94 | getMetrics() { |
| 95 | return { |
| 96 | darts: this.darts.toLocaleString("en-US"), |
| 97 | inside: this.totalHits.toLocaleString("en-US"), |
| 98 | estimate: this.estimate.toFixed(5), |
| 99 | absError: this.error.toExponential(2), |
| 100 | }; |
| 101 | } |
| 102 | |
| 103 | describe(): string { |
| 104 | return `${this.darts.toLocaleString("en-US")} darts thrown; ${this.totalHits.toLocaleString("en-US")} landed inside the quarter circle, estimating pi as ${this.estimate.toFixed(4)} (error ${this.error.toExponential(1)}).`; |
| 105 | } |
| 106 | |
| 107 | entities(): number { |
| 108 | return this.darts; |
| 109 | } |
| 110 | } |
| 111 | |
Performance
- Time per tick
- O(1) per dart
- Space
- A 20,000-dart display ring buffer
- Notes
- The estimate is unbiased from the very first dart - it just takes a while to become precise.
Live numbers (FPS, frame time, steps per second, microseconds per step, entity count) are in the performance overlay on the simulation above: the pulse icon in its top-right corner.