🧬 The Monodromy Loom

Take a polynomial whose coefficients depend on a parameter, and walk that parameter slowly around a closed loop. The roots move as it moves — and when the loop encircles the places where two roots collide, the roots come back permuted. That permutation is the family’s monodromy: its Galois action, made visible as a literal braid of tracked solution paths.

five solution strands of x^5-5x-c braiding into a 5-cycle as c loops four branch points

Every strand above is one root of \(f(x;c) = x^5 - 5x - c\), plotted in 3-D as \((\operatorname{Re} x,\ \operatorname{Im} x,\ \theta)\) where \(\theta\) is the loop angle running \(0 \to 2\pi\). As \(c\) circles once around all four branch points, the five roots weave through one another and land in a single 5-cycle — the generator of the family’s Galois group. The four white star-bursts are the near-collisions: each is where the loop grazes a branch point and a different pair of strands very nearly touches.

The texture is the tracker’s heartbeat

The strands are not uniform ribbons. Their brightness and width are the adaptive-precision tracker’s own diagnostics, streamed step-by-step from a PathDataCollector. Right at a pinch the Jacobian is nearly singular, so the tracker’s step size collapses and it piles up tiny steps to inch past — and exactly there the strand flares and swells. You are watching the solver work hardest precisely where the mathematics is hardest.

Start simple: one loop, one swap

Before the five-strand braid, the whole idea fits in a cubic. \(f(x;c) = x^3 - 3x - c\) has two branch points, at \(c = \pm 2\). Loop one of them and exactly two roots swap — a single transposition — while the third rides straight through, untouched:

three strands of x^3-3x-c; two swap around one branch point while the third rides straight

The two swapping strands (magenta and gold) cross at the glowing branch point at \(\theta=\pi\) — note how they brighten and thicken there as the tracker fights the near-collision — and their start rings at the base map to each other’s end stars at the top. The calm strand keeps its place. That is monodromy in one picture; the quintic is the same lesson, louder.

How it is built

The trick is to bake the entire loop into a single homotopy by making the coefficient a function of the path variable, \(c(t) = \text{center} + \text{radius}\cdot e^{\,i(\theta + \varphi)}\) with \(\theta = 2\pi(1-t)\). Then one continuous track per strand — from \(t=1\) (the start configuration) to \(t=0\) (the loop closed) — traces the whole loop, and the path variable maps straight onto the loop angle:

def loom_homotopy(degree, center, radius, phi_over_pi):
    """H(x, t) = x^d - d*x - c(t),  c(t) = center + radius*exp(i*(theta + phi)),
    theta = 2pi(1-t),  phi = phi_over_pi * pi.

    At t=1 theta=phi (the start configuration); at t=0 the loop has closed.  Tracking a root of the
    start configuration from t=1 to t=0 carries it once around the loop.
    """
    x = bertini.Variable('x')
    t = bertini.Variable('t')
    theta = 2 * bertini.Pi * (1 - t) + bertini.coefficient(phi_over_pi) * bertini.Pi
    sys = bertini.System()
    sys.add_function(x**degree - degree * x - _c_of_theta(theta, center, radius))
    sys.add_path_variable(t)
    sys.add_variable_group(bertini.VariableGroup([x]))
    return sys

Tracking every strand and capturing its trajectory is then just an AMPTracker with a PathDataCollector attached, once per root:

def track_loop(degree, center, radius, phi_over_pi, tol=1e-10):
    """Track every strand once around the loop.  Returns a list of per-strand dicts with the loop
    angle ``theta``, the complex position ``x``, and the tracker diagnostics along the path."""
    H = loom_homotopy(degree, center, radius, phi_over_pi)
    roots = start_configuration(degree, center, radius, phi_over_pi)

    tracker = bertini.AMPTracker(H)
    tracker.setup(bertini.tracking.Predictor.RK4, tol, 1e6,
                  tracking.SteppingConfig(), tracking.NewtonConfig())
    tracker.precision_setup(tracking.amp_config_from(H))

    strands = []
    for r in roots:
        collector = amp_observers.PathDataCollector()
        tracker.add_observer(collector)
        end = np.zeros(H.num_variables(), dtype=complex_mp)
        tracker.track_path(end, complex_mp(1.0, 0.0), complex_mp(0.0, 0.0),
                           np.array([complex_mp(r.real, r.imag)]))
        tracker.remove_observer(collector)

        times = collector.times()               # complex path-variable value per step
        pts = collector.points()[:, 0]           # affine x (one variable)
        dgn = collector.diagnostics()            # (n, 4): |t|, condition, precision, stepsize
        strands.append(dict(start=r, end=complex(end[0]),
                            theta=2 * math.pi * (1 - times.real), x=pts,
                            precision=dgn[:, 2], stepsize=dgn[:, 3]))
    return strands, roots

The rest — resampling the strands, colouring by step-size stress, and finding the pinches — is plotting; see monodromy_loom.py in full.

Why it matters

The braid is not decoration: it is the monodromy representation of the family. Reading the permutation off the strands recovers the Galois group of \(f(x;c)\) over \(\mathbb{C}(c)\), and the same loop-tracking is the engine behind monodromy methods in numerical algebraic geometry (decomposing solution sets, certifying trace tests). What makes it a showpiece here is that the picture carries three stories at once — the algebra (the braid), the geometry (the branch points the strands must wind around), and the numerics (the adaptive tracker sweating at every collision).

Note

This is a raster showpiece: PNG only (a 3-D render has no meaningful SVG), regenerated through tools/refresh_doc_artifacts.py. It is not a doctest — the code above is shown from the real generator, which you can also run directly with python monodromy_loom.py.