Back to blog
pysic-rs — mathematical physics engine

De Dirac à Wheeler–DeWitt : six équations, un seul moteur Rust

From Dirac to Wheeler–DeWitt: Six Equations, One Rust Engine

📖 Abstract

This article follows a single idea — quantise, then make it relativistic, then make it geometric — from Schrödinger's equation (1926) to Wheeler–DeWitt (1967), by way of Dirac, Maxwell, Aharonov–Bohm and the ADM decomposition. Each equation is solved by the compiled Rust core of pysic-rs and then graded against an independent reference: a closed form wherever one exists.

The result is a reproducible walk-through: a fully executed companion notebook in which no number is asserted without being compared. Along the way that discipline surfaced four incorrect solvers in our own library. We document them, because how a bug survives is more instructive than the bug itself.

1. Schrödinger: quantising

In 1926 Schrödinger went looking for a wave equation whose stationary solutions would reproduce Bohr's energy levels. He found one, and it worked remarkably well. But it carries a structural flaw: it is first order in time and second order in space.

$$i\hbar\,\frac{\partial\psi}{\partial t} = \left[-\frac{\hbar^2}{2m}\nabla^2 + V(\mathbf r)\right]\psi$$

That asymmetry is fatal in special relativity, where space and time must enter on the same footing. It is precisely this flaw that motivated Dirac two years later.

`pysic-rs` evolves it with a split-step Fourier scheme (Strang splitting): the kinetic operator is diagonal in $k$, the potential diagonal in $x$, and you alternate. Each factor is unitary, so the norm is conserved by construction.

import numpy as np, pysicrs as ps

N, L = 1024, 80.0
dx = L / N
x = (np.arange(N) - N/2) * dx
psi0 = (1/(2*np.sqrt(np.pi)))**0.5 * np.exp(-x**2/8) * np.exp(1j*x)

re, im = ps.schrodinger_split_step_1d(
    psi0.real.tolist(), psi0.imag.tolist(),
    [0.0]*N, dx, dt=0.005, n_steps=200,
)
Measured: for a free Gaussian packet the closed form $\sigma(t)=\sigma_0\sqrt{1+(\hbar t/m\sigma_0^2)^2}$ is reproduced to 1.6×10−14 — machine precision — with the norm conserved to 2×10−14.

For bound states we diagonalise the finite-difference Hamiltonian directly. The harmonic oscillator returns $E_n=(n+\tfrac12)\hbar\omega$ to $10^{-5}$, and the eigenvectors overlap the exact Hermite functions to $10^{-9}$. The $n$-th state carries exactly $n$ nodes — provided you don't count round-off noise in the classically forbidden tails, where $|\psi|\sim10^{-300}$.

2. Dirac: relativity forces antimatter

Dirac wanted an equation first order in both time and space, so that the probability density stays positive. He wrote $i\hbar\partial_t\psi = (c\,\boldsymbol\alpha\cdot\mathbf p + \beta mc^2)\psi$ and demanded that iterating it recover the relativistic relation $E^2=p^2c^2+m^2c^4$. That forces:

$$\{\gamma^\mu,\gamma^\nu\} = 2\eta^{\mu\nu}\mathbb{1}, \qquad \alpha^2=\beta^2=\mathbb{1}, \qquad \{\alpha,\beta\}=0$$

Numbers cannot anticommute. So the coefficients must be matrices, and $\psi$ a spinor. Two consequences fall out without being postulated: spin 1/2 appears in the structure of the equation, and the spectrum contains a negative-energy branch. Dirac interpreted it in 1931 as a new particle; Anderson photographed the positron in 1932.

p E gap 2mc² E₊ = +√(p²c² + m²c⁴) — matière E₋ = −√(p²c² + m²c⁴) — antimatière +mc² −mc²
The Dirac spectrum has two branches separated by a $2mc^2$ gap. The lower branch is not a mathematical artefact you can discard: it is forced by the algebra, and it is antimatter.
Verified: the Pauli matrices returned by `pysic-rs` are Hermitian, square to the identity, and their cross anticommutators vanish to machine precision. The Dirac propagator conserves the norm to 1.4×10−12 over 5000 steps.
That last sentence could not have been written a week ago. The same call took the norm from 1.77 to 3.6×1057. See section 7.

3. Zitterbewegung: watching antimatter beat

A localised state is not an energy eigenstate: it superposes both branches. Interference between them produces Zitterbewegung — the “trembling motion” identified by Schrödinger in 1930. At zero momentum the Hamiltonian reduces to $mc^2\sigma_x$, and the evolution of a state initially in the upper component is exactly solvable:

$$\psi(t) = \begin{pmatrix}\cos(mc^2t/\hbar)\\[2pt] -i\sin(mc^2t/\hbar)\end{pmatrix}\quad\Longrightarrow\quad |\psi_R|^2 = \frac{1-\cos(2mc^2t/\hbar)}{2}$$

The lower-component population therefore oscillates at the Compton frequency $\omega_{\rm zb}=2mc^2/\hbar$ — about $1.6\times10^{21}$ rad/s for an electron. It is a closed form, so it is a grade rather than an illustration.

Measured: the FFT peak lands 0.44 bins from $2mc^2/\hbar$ — that is, below the spectral resolution, hence consistent with the prediction.
The trap we nearly published. A first version integrated to $t=1.2$ when the period is $\pi$ — less than half a cycle. The FFT returned a frequency 160% wrong, with no error and no warning. You cannot measure a frequency over a fraction of a period, and a correct solver is no defence against a wrong measurement protocol.

4. Maxwell & Aharonov–Bohm: phase becomes physical

Maxwell unified electricity and magnetism in 1865. For nearly a century the potentials $(\phi,\mathbf A)$ were taken for calculational conveniences: only $\mathbf E$ and $\mathbf B$ were physically real, since only they are gauge invariant.

Aharonov and Bohm showed in 1959 that this is wrong. An electron going around a solenoid — through a region where $\mathbf B=0$ everywhere along its path — picks up a measurable phase. Chambers observed it in 1960.

$$\Delta\varphi_{\rm AB} = \frac{q}{\hbar}\oint \mathbf A\cdot d\boldsymbol\ell = \frac{q\Phi}{\hbar}$$

The potential carries information the local fields do not. Electromagnetism is a gauge theory, and the observable quantity is a geometric phase — an early special case of what Berry would generalise in 1984.

Φ B ≠ 0 source écran chemin 1 (B = 0 partout) chemin 2 (B = 0 partout) Δφ = qΦ/ħ l'électron ne rencontre jamais le champ — et la phase se décale quand même
The electron never passes through the region where $\mathbf B\neq0$, yet the interference pattern shifts with the enclosed flux. Only the potential, integrated along the path, accounts for it.
Measured: the Berry phase of an equatorial spinor loop is exactly $-\pi$ ($\gamma=-3.1415926536$, zero deviation), and the winding numbers of $e^{in\theta}$ are exact integers for $n=\pm1,\pm2,\pm3$. Maxwell returns the canonical log-log slopes: $-2$ for the Coulomb field, $+2$ for Larmor, $+4$ for the dipole.

The quantisation of winding is not cosmetic: it is what makes the AB effect robust to disorder, and it is the same topological invariant that reappears as the Chern number in the quantum Hall effect — which `pysic-rs` also computes, through the Harper equation that draws the Hofstadter butterfly in the logo.

5. ADM: gravity as a constraint

In 1959 Arnowitt, Deser and Misner recast general relativity by slicing spacetime into spatial hypersurfaces. The four-dimensional metric splits into a spatial metric $\gamma_{ij}$, a lapse function $N$ and a shift vector $N^i$.

The result is striking: Einstein's equations split into six evolution equations and four constraints containing no time derivative at all.

$$\mathcal{H} = {}^{(3)}\!R + K^2 - K_{ij}K^{ij} - 16\pi\rho = 0,\qquad \mathcal{M}^i = D_j\big(K^{ij} - \gamma^{ij}K\big) - 8\pi j^i = 0$$

In other words: the total Hamiltonian of general relativity is a constraint, $\mathcal H\approx0$. It does not generate evolution in an external time — there is no external time. That innocuous-looking fact is what produces the problem of time in quantum gravity.

g = np.eye(3).tolist()          # tranche plate
K = np.zeros((3,3)).tolist()     # courbure extrinseque nulle

ps.hamiltonian_constraint(g, K, rho=1.0)   # -> -50.26548246  ==  -16*pi
ps.momentum_constraint(g, K, [0,0,0])      # -> [0.0, 0.0, 0.0]

# K_ij = c delta_ij  =>  H = K^2 - K_ij K^ij = 9c^2 - 3c^2 = 6c^2
ps.hamiltonian_constraint(g, (2*np.eye(3)).tolist(), 0.0)   # -> 24.0 == 6*2^2
Measured: all three closed forms ($0$, $-16\pi\rho$, $6c^2$) are reproduced with exactly zero deviation. And Schwarzschild, a vacuum solution, gives a Ricci scalar of $\sim10^{-8}$ — the finite-difference residual — at every radius and every polar angle.

6. Wheeler–DeWitt: when time disappears

If the Hamiltonian is a constraint, canonical quantisation — replacing observables by operators acting on a wave functional of geometry — immediately gives $\hat{\mathcal H}\Psi=0$. This is the Wheeler–DeWitt equation (1967).

It contains no time derivative. The state of the universe does not evolve: it is. This is the problem of time — and it remains unresolved.

In minisuperspace you freeze every degree of freedom except the scale factor $a$. The equation becomes a second-order ODE, and therefore gradable:

$$\frac{d^2\Psi}{da^2} + \frac{p}{a}\frac{d\Psi}{da} - U(a)\,\Psi = 0,\qquad U(a) = a^2 - \frac{\Lambda}{3}a^4, \qquad a_t=\sqrt{3/\Lambda}$$

The potential vanishes at the turning point $a_t$, which separates the classically forbidden regime from the expanding universe. Tunnelling through it is the universe-creation mechanism of Vilenkin and of Hartle–Hawking. Linearising about $a_t$ gives $U\simeq-2\sqrt{3/\Lambda}\,(a-a_t)$, and the equation reduces to $\Psi_{zz}=z\Psi$: the Airy equation.

Measured: integration by the adaptive RK45 of `pysic-rs` (48 steps) matches scipy's Airy function — an independent reference — to 9.2×10−3 within the window where the linearisation holds. The deviation grows as you move away from $a_t$, which is the expected signature: it is the linearisation degrading, not the integrator.

7. Four bugs, and how they survived

Writing this article broke our own library. Grading each solver against a closed form exposed four of them as wrong. Here is what they were, and more importantly why nobody had noticed.

Solver Symptom Cause
dirac_split_step_1d norm 1.77 → 3.6×1057 mass rotation written exp(i·sinθ/2) (modulus 1) instead of i·sinθ/2: the step matrix became [[1,-1],[-1,1]], singular values 2 and 0
schrodinger_eigen_1d 32771 instead of 4.93 Jacobi sweep with a malformed angle and self-referential updates that never tracked tridiagonal fill-in; it returned the diagonal
rk45_solve returned None Ok(py.None()) behind a // TODO; the error estimator also used wrong coefficients
ricci_scalar $R=-2/r^2$ in vacuum metric frozen at $\theta=\pi/2$, so $\partial_\theta g_{\varphi\varphi}=0$: the angular terms stopped cancelling

The common thread

None of these four solvers had a test comparing it to a known value. Dirac's only test checked a different function. The Ricci test did exist — but it handed the solver a constant metric that ignored its coordinates: every derivative was zero and $R$ vanished trivially. Its tolerance of $0.1$ would have admitted the $-0.02$ artefact anyway.

The lesson. A test that cannot fail is no better than no test — it is worse, because it inspires confidence. A useful test compares against a quantity you know independently, with a tolerance scaled to the phenomenon rather than to a convenient constant.

All four are fixed and covered by closed-form tests: the infinite well against the exact discrete spectrum, the harmonic oscillator against $(n+\tfrac12)\hbar\omega$, Dirac norm conservation, exponential decay for RK45, and Schwarzschild Ricci flatness at arbitrary $r$ and $\theta$ — plus a regression test pinning the $-2/r^2$ artefact. 54 tests pass.

8. Reproduce it

Everything above fits in one end-to-end executed notebook: 11 code cells, 10 figures, each preceded by its theorem and followed by its reading.

pip install pysic-rs

git clone https://github.com/ThotDjehuty/pysic-rs
jupyter lab pysic-rs/notebooks/14_dirac_adm_wheeler_dewitt.ipynb

Go further

The companion notebook, the documentation and the repository.

Companion notebook Getting started GitHub
M

Mel & HFThot Research

Mathematical physics, numerical methods and Rust. We publish the negative results and the bugs we find in our own code — that is what makes the positive results credible.