Skip to the plate
FORMA PUBLIC DOMAIN GENERATIVE ATLAS / ED. 0.28
Plate 122, Hopf Fibration: a still of the fibration / stereographic projection plate as the atlas renders it, in the curves accent.

PL. 122  ·  CURVES / FIBRATION / STEREOGRAPHIC PROJECTION

Hopf Fibration

Heinz Hopf, 1931 · the circles on the torus: Yvon Villarceau, 1848

OPEN THE LIVE PLATE ▸

DEFINITION

fibre over (θ, φ):  z₁ = cos(θ/2)·exp(i(ψ + φ)),  z₂ = sin(θ/2)·exp(iψ),  ψ ∈ [0, 2π)
h(z₁, z₂) = (2 z₁ z̄₂, |z₁|² − |z₂|²) ∈ S²,  the Hopf map
stereographic from (0, 0, 0, 1):  P = (x₁, x₂, x₃)/(1 − x₄)

NOTES

The three-sphere is the set of points one unit from the origin in four-dimensional space. It is a three-dimensional object with no room to sit in three-dimensional space, which makes it the first thing in this atlas that has to be projected before it can be drawn at all rather than merely turned to face the reader. Hopf published a map from it onto the ordinary two-sphere in 1931, and the surprise is what sits above each point of the target: not a point but an entire great circle. So the three-sphere is filled by circles, one for every point of an ordinary globe, no two of them meeting, and every pair of them linked exactly once. This plate takes a ring of points around a circle of latitude on that globe and draws the circle sitting above each one. Stereographic projection carries the ring of circles out of four dimensions into three, where they are still circles, because that projection sends circles to circles, and where all the circles above one latitude fill a torus of revolution. On it they are the Villarceau circles, the two families of slanted circles a torus carries beyond the obvious meridians and parallels; the fibres give one family and the mirror fibration gives the other. Several latitudes give several tori, nested. What the projection costs is size. Up in four dimensions every fibre is a great circle and they all have the same length; down here they run from a small ring to an enormous one, and the one fibre passing through the point the projection is taken from is not a circle in the picture at all but a straight line out to infinity. That fibre sits over one particular point of the globe, the pole opposite the ring, and this plate stays away from it by construction rather than by clipping the result: the runaway fibre sits at a colatitude of 180 degrees and no constant here can drive a ring past 126. Length is a casualty of the projection and linking is not, so the single circle drawn in white is genuinely linked once with every other circle in the frame, and no two circles anywhere in the picture can be pulled apart.

PROVENANCE

Origin
H. Hopf, "Über die Abbildungen der dreidimensionalen Sphäre auf die Kugelfläche", Mathematische Annalen 104, 1931, 637–665. Verified on Crossref against title, author, year and venue, and against the volume and page range the same record carries (DOI 10.1007/BF01457962). Crossref also holds two later reprints of this paper — Selecta Heinz Hopf, 1964, and the Springer collected works, 2001 — and neither is what is cited here
The circles on the torus
Nouvelles annales de mathématiques 7, 1848, 345–347, where the result is recorded under the title "Théorème sur le tore de M. Villarceau (Yvon)". The write-up is by the journal editor Terquem and the theorem is Villarceau — that is how the source itself apportions it, and it is repeated here rather than tidied into a single name. Crossref holds no record of the item, as expected of an 1848 French journal; it is at numdam.org/item/NAM_1848_1_7__345_1, and no identifier is printed on the plate because none exists that this atlas would trust
Standing
Public domain. A map between spheres published in 1931 and a theorem about the torus published in 1848; no patent is possible on either
Checked, not just plotted
The three lines this plate runs are a collapsed form of the literal construction, and the collapse was verified numerically before any drawing code existed, over 40 colatitudes by 7 longitudes by 720 sample angles. Each sampled point lies over its own base point under the Hopf map to 1.0e-15; the collapsed form agrees with the literal ψ parametrisation of the same fibre to 3.4e-14; the projected fibre is a circle of radius sec(θ/2) centred tan(θ/2) from the origin to 2.2e-15, which is the claim the plate is named for and is therefore the one worth measuring rather than assuming; the sampling is uniform in arc length to 2.6e-13; and the frame is fitted from an exact bound, tan(π/4 + θ/4), matched to 5.3e-15
The pole is avoided, not clipped
Stereographic projection is a bijection everywhere except at the pole it is taken from, and exactly one fibre passes through that pole — the fibre over the south pole of the target sphere, where the colatitude reaches 180 degrees. Its image is a straight line through the origin, unbounded, and any plate that let a base ring reach it would have to clamp or discard something and would be showing a lie at the clamp. Instead the declared ranges cannot get there: the colatitude of a ring is the slider plus at most half the ring spacing across the whole nest plus the ten degrees of sway, which at the widest declared corner is 86 + 30 + 10 = 126 degrees against the 180 degrees where the fibre runs away. The 1/(1 − x₄) factor is therefore bounded by 9.2 everywhere in the box, and the outermost torus by tan(π/4 + 126°/4) = 4.17 frame radii. The lower end is bounded too and for a different reason: at colatitude 0 the whole family collapses onto the single circle over the north pole, which renders one ring rather than a nest, so the floor sits at 54 − 30 − 10 = 14 degrees
Constants
lat is the colatitude of the ring of base points and spread the colatitude between one ring and the next, so together they set how large the outermost torus is and how tightly the nest packs. rings and fibres are locked from regenerate because they are cost and only cost: another fibre on a torus is another Villarceau circle of that same torus, and another ring is another torus of the same nest, so neither can find a shape jitter would not otherwise reach
Source
doi:10.1007/BF01457962

HOUDINI · VEX

The same published mathematics as a Detail Wrangle body. Paste it into a Wrangle with Run Over set to Detail; every constant is the published value plus a tweak channel, so Create Spare Parameters gives a slider that starts where the paper does.

// FORMA — PL. 122 · HOPF FIBRATION — Heinz Hopf, 1931 · the circles on the torus: Yvon Villarceau, 1848
//   fibre over (θ, φ):  z₁ = cos(θ/2)·exp(i(ψ + φ)),  z₂ = sin(θ/2)·exp(iψ),  ψ ∈ [0, 2π)
//   h(z₁, z₂) = (2 z₁ z̄₂, |z₁|² − |z₂|²) ∈ S²,  the Hopf map
//   stereographic from (0, 0, 0, 1):  P = (x₁, x₂, x₃)/(1 − x₄)
// Paste into a Detail Wrangle (Run Over: Detail), no inputs needed.
// Written from the published mathematics, not adapted from any code.
// Constants arrive at their published values. Press the node's Create
// Spare Parameters button and every tweak becomes a slider — starting
// at 0, the published figure, and moving in the constant's own units.
// https://forma-gen.com/#plate=hopf

float p_lat    = 68 + chf('lat_tweak');         // θ — base colatitude (deg) · live 54 .. 86
float p_spread = 14 + chf('spread_tweak');      // colatitude between rings (deg) · live 8 .. 20
float p_rings  = 3 + chf('rings_tweak');        // nested tori · live 1 .. 4
float p_fibres = 12 + chf('fibres_tweak');      // fibres per torus · live 6 .. 20

// The plate's own colour: FORMA's CURVES accent as a cosine ramp,
// brightest near t = 0 and t = 1, near-black around t = 0.5.
vector forma_ramp(float t){
  return set(
    0.11 + 0.1196 * cos(6.28318530718 * (t + 0)),
    0.46 + 0.5 * cos(6.28318530718 * (t + 0.05)),
    0.2453 + 0.2667 * cos(6.28318530718 * (t + 0.1)));
}

// One fibre of the Hopf fibration for each of p_fibres base points spaced
// around a circle of latitude on the target sphere, repeated for each of
// p_rings nested latitudes. Each fibre is a great circle of the three-sphere
// and comes out of the stereographic projection as a genuine circle, so the
// family over one latitude fills a torus of revolution with major radius
// sec(theta/2) and minor radius tan(theta/2) and the fibres are that torus's
// Villarceau circles. Nothing is waived: all four published constants reach
// the body.
//
// The plate drifts -- its base ring sways in colatitude and turns in
// longitude against the page clock. A Houdini cook has no clock, so both are
// frozen at t = 0 using PHASE's actual computed value for the id "hopf",
// forma_phase below, with the same formulae the draw function uses rather
// than an independently chosen offset. This therefore cooks the frame the
// live plate shows at its own time origin.
//
// The camera tilt the plate applies is deliberately not ported: that is a
// view, and Houdini has a camera of its own. What is emitted here is the raw
// three-dimensional projection, with the axis all the tori share mapped onto
// Houdini's y (up) -- the z-up convention every 3-D curve in this corpus
// follows, catenoid.vex and lissajous3d.vex included.
//
// Every snippet-scope name here is forma_-prefixed, TAU included: two ports
// pasted into one wrangle share a namespace, which is the rule the
// unprefixed-helper gate exists for, and it costs nothing to extend it to
// the constants as well.
float forma_tau   = 6.28318530718;
float forma_deg   = 0.01745329252;
float forma_phase = 0.527311282698065;
float forma_sway  = 10.0 * forma_deg;
int   forma_seg   = 96;

int   forma_rings = int(rint(p_rings));
int   forma_per   = int(rint(p_fibres));
float forma_th0   = p_lat * forma_deg + forma_sway * sin(forma_tau * forma_phase);
float forma_gap   = p_spread * forma_deg;
float forma_phi0  = forma_tau * forma_phase;

for (int k = 0; k < forma_rings; k++){
    // rings run smallest colatitude first, which is innermost torus first
    float th = forma_th0 + (float(k) - float(forma_rings - 1) / 2.0) * forma_gap;
    float ct = cos(th / 2.0), st = sin(th / 2.0);

    // the nest walks 0.82 to 1.12 of the ramp, inside its bright lobe at both
    // ends, exactly as the plate colours it
    float tone = 0.82;
    if (forma_rings > 1) tone = 0.82 + 0.30 * float(k) / float(forma_rings - 1);
    vector col = forma_ramp(tone);

    for (int j = 0; j < forma_per; j++){
        float phi = forma_phi0 + float(j) / float(forma_per) * forma_tau;
        float cf = cos(phi), sf = sin(phi);
        int prim = addprim(0, "polyline");
        for (int i = 0; i <= forma_seg; i++){
            // The fibre over the base point at colatitude th and longitude phi
            // is psi -> (cos(th/2) exp(i(psi+phi)), sin(th/2) exp(i psi)) in
            // C^2, whose stereographic image from the pole (0,0,0,1) is
            // (x1, x2, x3)/(1 - x4). Substituting
            //   sin psi = (sin a + sin(th/2)) / (1 + sin(th/2) sin a),
            // a Moebius map of the circle onto itself, cancels that denominator
            // and walks the projected circle at constant speed. The last
            // sample repeats the first, which is how a closed curve is emitted
            // here (packing.vex does the same).
            float a  = float(i) / float(forma_seg) * forma_tau;
            float ca = cos(a);
            float q  = (sin(a) + st) / ct;
            float X = ca * cf - q * sf;
            float Y = ca * sf + q * cf;
            float Z = ca * st / ct;
            // z-up: the axis the tori share maps onto Houdini's y
            int pt = addpoint(0, set(X, Z, Y));
            setpointattrib(0, "Cd", pt, col);
            addvertex(0, prim, pt);
        }
    }
}

AFTER EFFECTS · EXPRESSION

The same published mathematics as a Shape Layer path expression. Paste it onto a Path property; every constant is the published value plus a Slider Control named _tweak, so a bare paste already draws the figure and each slider moves one constant in its own units. Trim Paths is the comet.

// FORMA — PL. 122 · HOPF FIBRATION — Heinz Hopf, 1931 · the circles on the torus: Yvon Villarceau, 1848
//   fibre over (θ, φ):  z₁ = cos(θ/2)·exp(i(ψ + φ)),  z₂ = sin(θ/2)·exp(iψ),  ψ ∈ [0, 2π)
//   h(z₁, z₂) = (2 z₁ z̄₂, |z₁|² − |z₂|²) ∈ S²,  the Hopf map
//   stereographic from (0, 0, 0, 1):  P = (x₁, x₂, x₃)/(1 − x₄)
// After Effects port — paste onto a Shape Layer's Path property
// (Contents › Shape › Path). Written from the published mathematics, not
// adapted from any code. Constants arrive at their published values; add a
// Slider Control (Effect › Expression Controls) named <k>_tweak and that
// constant moves in its own units, starting at 0 — the published figure.
// The plate's comet and its reveal are Trim Paths; the stroke colour is
// FORMA's CURVES accent, #3DFF88. Animation runs on time.
// This plate draws 36 separate paths at its published constants:
// duplicate the group (Contents › Group) that many times and each copy draws
// its own part, read from its position in the layer. A Slider Control named
// "part" on the layer pins one instead.
// https://forma-gen.com/#plate=hopf

// A missing slider reads 0, so a bare paste already draws the figure.
function forma_tweak(n){ try { return effect(n)("Slider"); } catch (e){ return 0; } }
var p_lat    = 68 + forma_tweak("lat_tweak");       // θ — base colatitude (deg) · live 54 .. 86
var p_spread = 14 + forma_tweak("spread_tweak");    // colatitude between rings (deg) · live 8 .. 20
var p_rings  = 3 + forma_tweak("rings_tweak");      // nested tori · live 1 .. 4
var p_fibres = 12 + forma_tweak("fibres_tweak");    // fibres per torus · live 6 .. 20

// The frame: the plate's W × H canvas is this comp, with the origin at the
// layer's anchor; canvas y already runs down, as After Effects' does.
var forma_W = thisComp.width, forma_H = thisComp.height, forma_t = time;
var forma_phase = 0.527311282698065;   // this plate's own fixed phase, as the page has it
function forma_pt(x, y){ return [x - forma_W / 2, y - forma_H / 2]; }
function forma_partIndex(){
  try { return Math.round(effect("part")("Slider")); } catch (e){}
  try { return thisProperty.propertyGroup(3).propertyIndex - 1; } catch (e){ return 0; }
}
var forma_part = forma_partIndex();

// The Hopf fibration: over each base point (colatitude θ, longitude φ) of the
// 2-sphere the fibre ψ ↦ (cos(θ/2)·e^{i(ψ+φ)}, sin(θ/2)·e^{iψ}) in ℂ², seen
// through stereographic projection from (0, 0, 0, 1) — a circle, and every
// pair of fibres linked once. A ring of base points at one colatitude gives
// the fibres of one torus; `rings` tori at `spread` degrees apart, `fibres`
// per ring, the ring of base points swaying 10° about `lat` and turning in
// longitude from this plate's phase, viewed from 60° elevation — the page's
// own construction, with its exact fit (the tori stay inside tan(π/4 + θ/4)
// whatever the sway). The substitution sin ψ = (sin α + sin(θ/2)) /
// (1 + sin(θ/2) sin α) walks the projected circle at constant speed and
// never divides per point. Fibres never touch — that is the theorem — so
// each is its own path: rings × fibres parts, part k·fibres + j the j-th
// fibre of the k-th torus, innermost torus first.
// parts: 36
var TAU = 6.283185307179586, DEG = Math.PI / 180;
var rings = Math.round(p_rings), perRing = Math.round(p_fibres);
var SWAY = 10 * DEG;
var theta0 = p_lat * DEG + SWAY * Math.sin(TAU * (forma_t * 0.031 + forma_phase));
var gap = p_spread * DEG;
var phi0 = TAU * (forma_t * 0.055 + forma_phase);
var thetaHi = p_lat * DEG + gap * (rings - 1) / 2 + SWAY;
var outer = Math.tan(Math.PI / 4 + thetaHi / 4);
var EL = 60 * DEG, ce = Math.cos(EL), se = Math.sin(EL);
var hHi = Math.tan(thetaHi / 2) + se / Math.cos(thetaHi / 2);
var sc = Math.min(forma_W * 0.46 / outer, forma_H * 0.46 / hHi);
var cx = forma_W / 2, cy = forma_H / 2;
var k = Math.floor(forma_part / perRing) % rings, j = forma_part % perRing;
var th = theta0 + (k - (rings - 1) / 2) * gap;
var c = Math.cos(th / 2), sn = Math.sin(th / 2);
var Rpx = Math.max(1, sc / c);
var N = Math.max(48, Math.min(192, Math.ceil(TAU / Math.sqrt(2 / Rpx))));
var phi = phi0 + j / perRing * TAU, cf = Math.cos(phi), sf = Math.sin(phi);
var pts = [];
for (var i = 0; i < N; i++){
  var a = i / N * TAU, ca = Math.cos(a), q = (Math.sin(a) + sn) / c;
  var X = ca * cf - q * sf, Y = ca * sf + q * cf, Z = ca * sn / c;
  pts.push(forma_pt(cx + X * sc, cy - (Z * ce - Y * se) * sc));
}
createPath(pts, [], [], true);