Skip to content

Numerical Quadrature

Rectangles, trapezoids, parabola tops.

Intermediate / integration / error-analysis

Three quadrature rules approximate the same definite integral against its exact value: left Riemann sums err like 1/n, trapezoids like 1/n-squared, Simpson like 1/n-to-the-fourth. Halve the panels and watch the error drop by the predicted factor - convergence orders you can measure on the screen.

Tagged: Riemann sum, trapezoid rule, Simpson, convergence order

Live simulation

Double the panels: Riemann error quarters, trapezoid /4, Simpson /16.

Speed

Double the panels: Riemann error quarters, trapezoid /4, Simpson /16.

Mathematics

Definite integrals can be approximated to any accuracy by summing simple shapes: left Riemann rectangles (error O(n^-1)), trapezoids (O(n^-2)), and Simpson's parabola-topped panels (O(n^-4)). The exact value is known for each test integrand, so the error is a measurement.

left Riemann: sum f(x_i) h

trapezoid: h (f(a)/2 + sum_{i=1}^{n-1} f(x_i) + f(b)/2)

Simpson: h/3 (f(a) + 4 sum odd + 2 sum even + f(b))

The error column is the lesson: double the panels and the Riemann error quarters, the trapezoid error divides by four, the Simpson error by sixteen - the convergence orders of the three rules, measured rather than recited. For exp(-x^2) the 'exact' value is the erf-based constant quoted to 15 digits.

How it works

Panels are drawn as filled shapes under the curve (trapezoid draws quads, Simpson uses the fitted parabola value at each panel's midpoint), so the approximation is visually auditable against the curve.

Simpson silently rounds odd panel counts up - the parity requirement is part of the rule.

Implementation

src/engine/experiments/quadrature.ts · 171 lines · the module running above

TypeScript · fixed-timestep engine

1import { BaseExperiment } from "../core/base-experiment";
2import type { ParameterDef, ThemeColors } from "../core/types";
3
4type Fn = { label: string; f: (x: number) => number; exact: string; exactValue: (a: number, b: number) => number; domain: [number, number] };
5
6const FUNCTIONS: Record<string, Fn> = {
7 sin: {
8 label: "sin(x)",
9 f: Math.sin,
10 exact: "2",
11 exactValue: (a, b) => Math.cos(a) - Math.cos(b),
12 domain: [0, Math.PI],
13 },
14 parabola: {
15 label: "x^2",
16 f: (x) => x * x,
17 exact: "1/3",
18 exactValue: (a, b) => (b ** 3 - a ** 3) / 3,
19 domain: [0, 1],
20 },
21 gaussian: {
22 label: "exp(-x^2)",
23 f: (x) => Math.exp(-x * x),
24 exact: "sqrt(pi)/2 erf(1)",
25 exactValue: () => 0.746824132812427,
26 domain: [0, 1],
27 },
28};
29
30/**
31 * Numerical quadrature three ways - left Riemann, trapezoid, Simpson -
32 * against the exact value. The error metric exposes the convergence orders:
33 * Riemann O(n^-1), trapezoid O(n^-2), Simpson O(n^-4).
34 */
35export class QuadratureExperiment extends BaseExperiment {
36 readonly id = "quadrature";
37
38 protected params(): ParameterDef[] {
39 return [
40 {
41 key: "fn",
42 label: "Integrand",
43 options: [
44 { value: "sin", label: "sin(x) on [0, pi]" },
45 { value: "parabola", label: "x^2 on [0, 1]" },
46 { value: "gaussian", label: "exp(-x^2) on [0, 1]" },
47 ],
48 defaultValue: "sin",
49 },
50 {
51 key: "method",
52 label: "Rule",
53 options: [
54 { value: "left", label: "Left Riemann" },
55 { value: "trapezoid", label: "Trapezoid" },
56 { value: "simpson", label: "Simpson" },
57 ],
58 defaultValue: "trapezoid",
59 },
60 { key: "n", label: "Panels n", min: 2, max: 200, step: 2, defaultValue: 12 },
61 ];
62 }
63
64 protected onReset(): void {}
65
66 protected onUpdate(): void {}
67
68 private fn(): Fn {
69 return FUNCTIONS[this.str("fn")];
70 }
71
72 private approximate(): number {
73 const f = this.fn().f;
74 const [a, b] = this.fn().domain;
75 const n = this.num("n");
76 const h = (b - a) / n;
77 const method = this.str("method");
78 if (method === "left") {
79 let sum = 0;
80 for (let i = 0; i < n; i++) sum += f(a + i * h);
81 return sum * h;
82 }
83 if (method === "trapezoid") {
84 let sum = (f(a) + f(b)) / 2;
85 for (let i = 1; i < n; i++) sum += f(a + i * h);
86 return sum * h;
87 }
88 // Simpson needs an even n.
89 const m = n % 2 === 0 ? n : n + 1;
90 const hh = (b - a) / m;
91 let sum = f(a) + f(b);
92 for (let i = 1; i < m; i++) sum += f(a + i * hh) * (i % 2 === 1 ? 4 : 2);
93 return (sum * hh) / 3;
94 }
95
96 render(ctx: CanvasRenderingContext2D, theme: ThemeColors): void {
97 ctx.fillStyle = theme.bg;
98 ctx.fillRect(0, 0, this.width, this.height);
99 const f = this.fn();
100 const [a, b] = f.domain;
101 const toX = (x: number) => ((x - a) / (b - a)) * this.width;
102 const valueSpan = 2.4;
103 const toY = (y: number) => this.height - 24 - (y / valueSpan) * (this.height - 40);
104 const n = this.num("n");
105 const h = (b - a) / n;
106 const method = this.str("method");
107 // Panels
108 ctx.fillStyle = theme.raised;
109 for (let i = 0; i < n; i++) {
110 const x0 = a + i * h;
111 const x1 = x0 + h;
112 const yv = method === "left" ? f.f(x0) : method === "trapezoid" ? NaN : (f.f(x0) + 4 * f.f((x0 + x1) / 2) + f.f(x1)) / 6;
113 if (Number.isNaN(yv)) {
114 // Trapezoid panel: draw as a quad.
115 ctx.beginPath();
116 ctx.moveTo(toX(x0), toY(0));
117 ctx.lineTo(toX(x0), toY(f.f(x0)));
118 ctx.lineTo(toX(x1), toY(f.f(x1)));
119 ctx.lineTo(toX(x1), toY(0));
120 ctx.closePath();
121 ctx.fill();
122 continue;
123 }
124 const px = toX(x0);
125 const pw = toX(x1) - toX(x0);
126 const py = toY(Math.max(0, yv));
127 ctx.fillRect(px, py, pw, toY(0) - py);
128 }
129 // Curve
130 ctx.strokeStyle = theme.fg;
131 ctx.lineWidth = 1.8;
132 ctx.beginPath();
133 for (let px = 0; px <= this.width; px += 1) {
134 const xv = a + (px / this.width) * (b - a);
135 const py = toY(f.f(xv));
136 if (px === 0) ctx.moveTo(px, py);
137 else ctx.lineTo(px, py);
138 }
139 ctx.stroke();
140 ctx.strokeStyle = theme.line;
141 ctx.beginPath();
142 ctx.moveTo(0, toY(0));
143 ctx.lineTo(this.width, toY(0));
144 ctx.stroke();
145 ctx.lineWidth = 1;
146 }
147
148 getMetrics() {
149 const f = this.fn();
150 const [a, b] = f.domain;
151 const approx = this.approximate();
152 const exact = f.exactValue(a, b);
153 return {
154 method: this.str("method"),
155 panels: this.num("n"),
156 approximation: approx.toFixed(8),
157 exact: f.exact,
158 error: Math.abs(approx - exact).toExponential(3),
159 };
160 }
161
162 describe(): string {
163 const m = this.getMetrics();
164 return `${m.method} with ${m.panels} panels approximates the integral as ${m.approximation} (exact ${m.exact}); error ${m.error}.`;
165 }
166
167 entities(): number {
168 return this.num("n");
169 }
170}
171

Performance

Time per tick
O(n) evaluations per approximation
Space
None
Notes
Simpson with n = 200 on sin over [0, pi] matches 2 to ten decimals: try to catch it being wrong.

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.