Skip to content
The Lumenary

Math laboratory

Hopf fibres are linked circles

Samples fibres of the Hopf map S^3 -> S^2, checks numerically that the map is constant on each fibre, projects the fibres stereographically to R^3 and evaluates the Gauss linking integral for several pairs; it is 1 to about ten digits. Numerical evidence for the linking of Hopf fibres, which the lesson proves; it is not itself a proof.

Back to the labNumerical approximation
Hopf fibres are linked circlesPythonNumerical approximationComputationally tested
"""The Hopf fibration numerically: fibres are circles, and any two of them link.

The Hopf map h(z, w) = (2 z conj(w), |z|^2 - |w|^2) sends the unit sphere S^3
in C^2 to the unit sphere S^2. The fibre over a point p is the circle
{(lz, lw) : |l| = 1}. We sample fibres, check that h is constant on them, map
them into R^3 by stereographic projection, and evaluate Gauss's linking
integral for pairs of fibres. The answer is 1 every time: the Hopf invariant of
h, which is why h generates pi_3(S^2) = Z. Numerical evidence only.
"""

import math

import numpy as np

def hopf(z, w):
    """The Hopf map, returning a point (x, y, t) of R^3."""
    v = 2 * z * np.conj(w)
    return np.stack([v.real, v.imag, abs(z) ** 2 - abs(w) ** 2], axis=-1)

def fibre(p, n=400):
    """n points (z, w) on the fibre over the unit vector p = (a, b, c)."""
    a, b, c = p
    if c >= 0:
        z0 = math.sqrt((1 + c) / 2)
        w0 = complex(a, -b) / (2 * z0)
    else:
        w0 = math.sqrt((1 - c) / 2)
        z0 = complex(a, b) / (2 * w0)
    lam = np.exp(2j * np.pi * np.arange(n) / n)
    return lam * z0, lam * w0

def stereographic(z, w):
    """Project S^3 minus the point (0, i) to R^3."""
    return np.stack([z.real, z.imag, w.real], axis=-1) / (1 - w.imag)[:, None]

def spectral_derivative(c):
    """Derivative d/dtheta of a closed curve sampled at n equally spaced angles."""
    n = len(c)
    k = np.fft.fftfreq(n, d=1.0 / n)
    k[n // 2] = 0 if n % 2 == 0 else k[n // 2]
    return np.real(np.fft.ifft(1j * k[:, None] * np.fft.fft(c, axis=0), axis=0))

def linking_number(c1, c2):
    """Gauss linking integral of two closed curves sampled uniformly in angle.

    Uses the periodic trapezoidal rule with spectrally exact tangent vectors,
    which converges faster than any power of 1/n for smooth curves.
    """
    t1, t2 = spectral_derivative(c1), spectral_derivative(c2)
    r = c1[:, None, :] - c2[None, :, :]
    cross = np.cross(t1[:, None, :], t2[None, :, :])
    integrand = np.einsum("ijk,ijk->ij", r, cross)
    integrand /= np.linalg.norm(r, axis=-1) ** 3
    h = 2 * math.pi / len(c1) * 2 * math.pi / len(c2)
    return integrand.sum() * h / (4 * math.pi)

def round_circle(center, n=400, vertical=False):
    """A unit circle around center, horizontal or in a vertical (x, z) plane."""
    t = 2 * np.pi * np.arange(n) / n
    if vertical:
        pts = np.stack([np.cos(t), np.zeros(n), np.sin(t)], axis=-1)
    else:
        pts = np.stack([np.cos(t), np.sin(t), np.zeros(n)], axis=-1)
    return pts + np.asarray(center, dtype=float)

def unit(v):
    """Normalise a vector to a point of S^2."""
    v = np.asarray(v, dtype=float)
    return v / np.linalg.norm(v)

POINTS = {
    "north pole": unit([0, 0, 1]),
    "equator, x": unit([1, 0, 0]),
    "equator, y": unit([0, 1, 0]),
    "(1, 1, 1)": unit([1, 1, 1]),
    "(-2, 1, -1)": unit([-2, 1, -1]),
}

def main():
    print("Hopf map h : S^3 -> S^2, h(z, w) = (2 z conj(w), |z|^2 - |w|^2)")
    print()
    curves = {}
    worst = 0.0
    for name, p in POINTS.items():
        z, w = fibre(p)
        worst = max(worst, float(np.abs(hopf(z, w) - p).max()))
        curves[name] = stereographic(z, w)
    print(f"max |h(x) - p| over 400 points on each of 5 fibres: {worst:.1e}")
    print()
    print("Gauss linking numbers of the projected fibres (400 points each):")
    names = list(POINTS)
    for i in range(len(names)):
        for j in range(i + 1, len(names)):
            lk = linking_number(curves[names[i]], curves[names[j]])
            print(f"  {names[i]:<12} with {names[j]:<12} Lk = {lk:.10f}")
    flat = round_circle([0, 0, 0])
    apart = linking_number(flat, round_circle([3, 0, 0], vertical=True))
    print(f"  control: two unlinked round circles      Lk = {abs(apart):.10f}")
    print()
    print("Every pair of fibres links once. The linking number of two fibres")
    print("is the Hopf invariant H(h) = 1; a map S^3 -> S^2 homotopic to a")
    print("constant would have H = 0, so h is not null-homotopic.")
    print()
    print("Epistemic status: numerical evidence. The trapezoidal rule for")
    print("smooth closed curves converges very fast and the values agree with")
    print("the integer 1 to many digits, but floating-point quadrature proves")
    print("nothing; the lesson proves that h generates pi_3(S^2) = Z.")

if __name__ == "__main__":
    main()
Runs entirely in your browser, in an isolated worker. Nothing is sent to a server.

Used in