Case study 02 · Simulation · JavaScript

QuantumSim

How can you make quantum probability something you can explore?

Status
Working prototype
Stack
ES modules, Three.js r180, WebGL2
Size
About 6,600 lines plus a 1,600-line guide
Last updated
QuantumSim's orbital builder: a 3d z-squared orbital drawn as a dense blue and white point cloud, with quantum number controls on the right.
The orbital builder showing 3dz². Each dot is one sample drawn from |ψ|². Screenshot of the app running locally in Chrome, November 6, 2026.
Overview

What it is

In quantum mechanics you cannot say where an electron is, only how likely it is to be found in each place. That probability comes from the wave function ψ: the chance of finding the particle near a point is proportional to |ψ|² there. This is the Born rule.

QuantumSim turns that into three things you can play with in a browser: atomic orbitals as clouds of sampled points, a particle trapped in a square box, and a wave packet hitting a barrier and partly tunnelling through. A fourth page, the field guide, explains the physics and lists what the app does not model.

There is no server. Every number on screen is computed on the visitor's machine.

Model

The maths

Orbitals. For a hydrogen-like atom, ψ separates into a radial part and an angular part. To draw a cloud, QuantumSim builds the cumulative distribution of the radial probability r²|R(r)|², picks a radius by inverting it with a binary search, then picks a direction by rejection sampling the angular part. Dense regions of dots are regions of high probability.

Tunnelling. The time-dependent Schrödinger equation (in units where ħ = m = 1) is

i ∂ψ/∂t = −½ ∂²ψ/∂x² + V(x) ψ

The potential term is simple in position space and the kinetic term is simple in momentum space. The split-step Fourier method takes advantage of that: half a step of potential, a Fourier transform, a full step of kinetic energy, an inverse transform, then the other half step of potential.

ψ(t+dt) ≈ e^(−iV dt/2) · F⁻¹[ e^(−ik² dt/2) · F[ e^(−iV dt/2) ψ(t) ] ]
Diagram of one split-step update: half potential step, FFT, kinetic step, inverse FFT, half potential step.
One time step, from the QuantumSim field guide. The figure was generated from the same modules the app uses.
Implementation

How the code is laid out

  • src/physics/orbitals.js: exact radial functions (generalised Laguerre polynomials), real angular shapes, the radial CDF and the sampler.
  • src/physics/tdse.js and fft.js: the split-step solver and a radix-2 FFT, on a 1,024-point grid.
  • src/views/*.js: one view per route. Each owns its Three.js scene and disposes it on exit.
  • docs/EXPLAINER.md: the field guide, with worked numbers taken from the code.

The kinetic step, from simulate() in tdse.js. ψ is stored as separate real and imaginary arrays, so multiplying by a phase is written out by hand:

// Full kinetic step in momentum space
fft(psiR, psiI);
for (let i = 0; i < N; i++) {
  const r = psiR[i];
  const m = psiI[i];
  const pr = kineticHalfR[i];
  const pi = kineticHalfI[i];
  psiR[i] = r * pr - m * pi;
  psiI[i] = r * pi + m * pr;
}
ifft(psiR, psiI);

src/physics/tdse.js, lines 226 to 236

The tunnelling page: a striped blue wave packet to the left of a thin barrier, with a probability density trace along the bottom and controls on the right.
The tunnelling experiment before the packet reaches the barrier.
Design decision

Physics with no DOM

The physics modules never touch the page. They take numbers and return arrays. The views do the drawing.

That separation is what makes the next section possible: the solver can be imported straight into Node and checked against a known answer, with no browser involved. It also means the field guide's figures come from the same functions the app runs, so the article cannot quietly drift away from the code.

Observation

Checking the solver

The README includes a sanity check. On November 6, 2026 I ran it again in Node with the default settings (packet energy 2, barrier height 4, width 1.2):

{ norm: '1.0000', packetT: '0.0404', planeWaveT: '0.0324', grid: 1024, frames: 121 }
  • Norm 1.0000: total probability is conserved, as it must be.
  • Transmission 0.0404 vs 0.0324: about 4% of the packet gets through. The analytic number is for a single plane wave at exactly that energy. A real packet contains a spread of energies, and the higher-energy part tunnels more easily, so the packet's transmission should come out a little higher, and it does.
Four snapshots of probability density over time: a packet approaching a barrier, splitting into a large reflected part and a small transmitted part.
Probability density at four moments during a tunnelling run. From the field guide, exported from tdse.js.
A grid of orbital point clouds for several combinations of n, l and m.
Orbital clouds sampled by orbitals.js for several quantum numbers.
Limits and next question

What it does not model

  • The orbitals use a one-electron, hydrogen-like model with screened charge. They get the shapes and trends right, not real many-electron energies.
  • Tunnelling is one-dimensional, single-particle, and uses an ideal rectangular barrier. The narrowest barrier preset is only marginally resolved by the grid.
  • The 3D cloud on the tunnelling page is a visual aid; the simulation itself is 1D.
  • There is no public hosted build yet. To try it, clone the repository and run python3 -m http.server.

Next question: what does tunnelling look like in two dimensions, where a packet can go around an obstacle as well as through it?

Related writing

Schrödinger's Equation for Dummies

Before building this I wrote a two-part explainer on where the equation comes from: Part I, the history, and Part II, decoding the equation.

Credits

Dependencies

Three.js r180 (MIT licence), vendored in the repository. Atomic reference data are standard tabulated values.