chrisphan.com

Happy (belated) π Day!

An analog clock reading 4:15 

Categories: computers, fun, programming, math

Happy belated π Day! π Day is the 14th of March, i.e. 03-14 both the U.S. or ISO 8601 date formats. Two times in the past, I have posted some kind of method (not necessarily efficient) to approximate π as a π Day celebration:

This year's entry, admittedly a little late, involves WebAssembly and differential equations.

A system of differential equations

This year, I decided to estimate π by approximating \(\sin t\) for values of \(t > 0\), using differential equations and Euler's method. Specifically, note that \(x = \sin t, y = \cos t\) is the only solution to the system of differential equations \[\begin{aligned} \frac{dx}{dt} &= y, \\ \frac{dy}{dt} &= -x \end{aligned}\] that satisfies the initial conditions \(x(0) = 0,\; y(0) = 1\).

Euler's method

Suppose we have a system of differential equations of the form \[\begin{aligned} \frac{dx}{dt} &= a x + b y,\\ \frac{dy}{dt} &= c x + d y. \end{aligned}\] We can use Euler's method to find numerical solutions (that is to say, approximations of the solutions) to this system. For any real number \(t_0\), and small real number \(\Delta t\), we can estimate the quantities \[\begin{aligned} \Delta x &= x(t_0 + \Delta t) - x(t_0),\\ \Delta y &= y(t_0 + \Delta t) - y(t_0), \end{aligned}\] with the approximations \[\begin{aligned} \Delta x &\approx \left(a x(t_0) + b y(t_0)\right) \Delta t,\\ \Delta y &\approx \left(c x(t_0) + d y(t_0)\right) \Delta t. \end{aligned}\]

Given initial values \(x(0) = x_0\) and \(y(0) = y_0\), we can come up with approximations \(x_k\) and \(y_k\) for \(x(k \Delta t)\) and \(y(k \Delta t)\), respectively, by recursively defining \[\begin{aligned} x_{k + 1} &= x_k + \left(a x_k + b y_k\right) \Delta t,\\ y_{k + 1} &= y_k + \left(c x_k + d y_k\right) \Delta t. \end{aligned}\]

Enter WASM

Lately, I have been learning WebAssembly (WASM), and thought it would be fun to implement this year's π Day demo in WASM. While traditional assembly language is used to specify processor instructions in order to program a computer directly, WebAssembly instructions are run on a stack-based virtual machine inside the web browser.2 WASM in created by writing the instructions in a human-readable text format, WebAssembly text or WAT.

For example, here is some WAT code for my WASM implementation of Euler's method:

pi_day_2026.wat WAT

        ;; set delta_x = (a*x + b*y) * delta_t
        local.get $a
        local.get $x
        f64.mul
        local.get $b
        local.get $y
        f64.mul
        f64.add
        local.tee $dxdt
        local.get $delta_t
        f64.mul
        local.set $delta_x

Lines starting with ;; are comments. The virtual machine that executes WASM is a stack machine. In the above snippet3:

Line Stack before WAT Instruction Description Stack after
44 (empty) local.get $a Copy the value stored in the variable $a and push it on top of the stack $a
45 $a local.get $x Copy the value stored in the variable $x and push it on top of the stack $a, $x
46 $a, $x f64.mul Pop the top two values off the stack, multiply them, and push the result on the stack $a × $x
47 $a × $x local.get $b Copy the value stored in the variable $b and push it on top of the stack $a × $x, $b
48 $a × $x, $b local.get $y Copy the value stored in the variable $y and push it on top of the stack $a × $x, $b, $y
49 $a × $x, $b, $y f64.mul Pop the top two values off the stack, multiply them, and push the result on the stack $a × $x, $b × $y
50 $a × $x, $b × $y f64.add Pop the top two values off the stack, add them, and push the result on the stack $a × $x + $b × $y
51 $a × $x + $b × $y local.tee $dxdt Copy the top value from the stack (without removing it) and store in the variable $dxdt $a × $x + $b × $y
52 $a × $x + $b × $y local.get $delta_t Copy the value stored in the variable $delta_t and push it on top of the stack $a × $x + $b × $y, $delta_t
53 $a × $x + $b × $y, $delta_t f64.mul Pop the top two values off the stack, multiply them, and push the result on the stack ($a × $x + $b × $y) × $delta_t
54 ($a × $x + $b × $y) × $delta_t local.set $delta_x Pop the top value off the stack and store in the variable $delta_x (empty)

The overall effect of lines 44-54 is to:

This illustrates the downside to WASM: it takes 11 lines of code to accomplish what most programming languages can do in two.

Euler step demo

The total function to do one step of Euler's method takes about 40 lines of WAT code. This JavaScript on this page is able to access the Euler step function. To the browser, this function is called eulerStep. It takes 7 numbers as arguments (\(a, b, c, d, x(t), y(t),\) and \(\Delta T\)) and returns four numbers (estimates for \(x(t), y(t), x'(t),\) and \(y'(t)\). You can play with this function by opening your browser's JavaScript console and evaluating the function there, once the WASM is done loading:

JavaScript (console)
eulerStep(1, 2, 3, 4, 1, 1, 0.0625)
[1.1875, 1.4375, 3, 7]

On the other hand, if mucking around in your browser's JavaScript console isn't your thing, you can also access eulerStep via the following demo:

Demo is not ready yet. Loading WASM...

a =
b =
c =
d =
System:
dx/dt = a x + b y
dy/dt = c x + d y
x(0) =
y(0) =
Δ t =
t x y dx/dt dy/dt

Approximating π

One definition of π is that \(t = \pi\) smallest positive real solution to the equation \(\sin t = 0\). Earlier I mentioned that \(x(t) = \sin t, y(t) = \cos t\) is the unique solution to the system of differential equations \[\begin{aligned} \frac{dx}{dt} & = y, \\ \frac{dy}{dt} &= -x \end{aligned}\] that satisfies the initial conditions \(x(0) = 0, y(0) = 1\). My plan is to use Euler's method to approximate a solution to this system (with initial conditions), which means approximating \(\sin t\) and \(\cos t\), and looking for the first positive value of \(t\) for which \(\cos t \leq 0\), which will be our approximation of π. The smaller \(\Delta t\) used, the better approximation of π we get.

Once the WASM has loaded successfully, click on the Run button to come up with different estimates for π using smaller and smaller values of \(\Delta t\).

Our estimates are produced as IEEE 754 double-precision floating-point numbers. This 64-bit format represents numbers4 in the form \[(-1)^s \times \left(1 + f/2^{52}\right)\times 2^{e - 1023},\] using one bit for \(s\), 11 bits for \(e\), and 52 bits for \(f\). For example, the number 3.25 is represented as \[\begin{aligned} (-1)^0 \times \left(1 + 2814749767106560/2^{52}\right) \times 2^{1024 - 1023} &= \left(1 + \frac{10 \times 2^{48}}{2^{52}} \right) \times 2 \\ &= \left(1 + 10/16\right) \times 2 \\ &= 3.25. \end{aligned}\]

For each approximation produced, click on the Show me the IEEE! to see the approximation as it's represented internally.

Loading the WASM...

kΔt = 2-k Approx. for π Error from Math.PI Time (ms) Num. values Plot
  1. Real and Complex Analysis, 3rd Edition by Walter Rudin (McGraw Hill 1987)

  2. Or other runtimes, but the browser is my focus here.

  3. In this table, the stacks are written from bottom to top. For example, the stack after line 45 is executed, written as $a, $x, has $a on the bottom and $x on the top.

  4. I'm ignoring values such as NaN (not a number) and \(\pm\infty\), which can also be represented as IEEE double-precision floating-point numbers.