Skip to content
The Lumenary

Math laboratory

Counting preimages with and without signs

For a degree-3 map of the circle that folds back, finds all preimages of several regular values: their number varies (5 or 7) but the count with signs of the derivative is always 3; also lists the critical values. Then counts preimages for the holomorphic map z^3 - 3z (degree 3, critical values -2 and 2) and for conj(z)^3 (degree -3). Numerical evidence.

Back to the labNumerical approximation
Counting preimages with and without signsPythonNumerical approximationComputationally tested
"""Counting preimages: the unsigned count changes, the signed count does not.

Part 1 takes a wiggly map of the circle to itself, of degree 3, whose
derivative changes sign (it folds back). For several regular values y it
finds every preimage numerically and adds up the signs of the derivative.
The number of preimages depends on y; the signed count is always 3. The few
values where the count jumps are critical values (Sard: a finite set here).

Part 2 does the same for maps of the Riemann sphere: z -> z^3 - 3z, which is
holomorphic (every preimage counts +1), and z -> conj(z)^3, which reverses
orientation (every preimage counts -1). Roots are found with numpy.
This is numerical evidence for the theorem that the degree is well defined.
"""

import math

import numpy as np

TWO_PI = 2 * math.pi
WIGGLE = 4.0
SAMPLE_VALUES = [0.2, 1.0, 1.8, 2.6, 3.1, 4.0, 5.0, 6.1]

def lift(t):
    """A lift F of the circle map: F(t + 2 pi) = F(t) + 3 * 2 pi (degree 3)."""
    return 3 * t + WIGGLE * math.sin(2 * t)

def lift_derivative(t):
    """F'(t) = 3 + 2 * WIGGLE * cos(2t); negative near t = pi/2 and 3 pi/2."""
    return 3 + 2 * WIGGLE * math.cos(2 * t)

def preimages(y, samples=4000):
    """All t in [0, 2 pi) with F(t) = y mod 2 pi, by sign changes + bisection."""
    roots = []
    ts = [TWO_PI * k / samples for k in range(samples + 1)]
    lo = math.floor((lift(0) - 10 - y) / TWO_PI)
    hi = math.ceil((lift(TWO_PI) + 10 - y) / TWO_PI)
    for k in range(lo, hi + 1):
        target = y + TWO_PI * k
        values = [lift(t) - target for t in ts]
        for i in range(samples):
            a, b = ts[i], ts[i + 1]
            fa, fb = values[i], values[i + 1]
            if fa == 0 or fa * fb < 0:
                for _ in range(60):
                    m = (a + b) / 2
                    if (lift(a) - target) * (lift(m) - target) <= 0:
                        b = m
                    else:
                        a = m
                roots.append((a + b) / 2)
    return sorted(roots)

def signed_count(roots):
    """Sum of the signs of F' at the preimages: the degree at a regular value."""
    return sum(1 if lift_derivative(t) > 0 else -1 for t in roots)

def critical_values():
    """The critical values F(t) mod 2 pi, where F'(t) = 0 (cos 2t = -3/(2W))."""
    s = math.acos(-3 / (2 * WIGGLE)) / 2
    points = [s, math.pi - s, math.pi + s, TWO_PI - s]
    return sorted(lift(t) % TWO_PI for t in points)

def complex_preimages(coeffs, w, tol=1e-6):
    """Distinct roots of p(z) = w, p given by numpy coefficients (highest first)."""
    c = list(coeffs)
    c[-1] -= w
    distinct = []
    for r in np.roots(c):
        if all(abs(r - d) > tol for d in distinct):
            distinct.append(r)
    return distinct

def critical_values_complex(coeffs):
    """Images p(c) of the critical points c (roots of p')."""
    p = np.poly1d(coeffs)
    return [complex(p(c)) for c in np.roots(np.polyder(p).coeffs)]

def main():
    print("1. The circle map F(t) = 3t + 4 sin 2t (mod 2 pi), degree 3")
    crit = ", ".join(f"{v:.3f}" for v in critical_values())
    print(f"   critical values (mod 2 pi): {crit}")
    print("   y        preimages   signed count")
    for y in SAMPLE_VALUES:
        roots = preimages(y)
        print(f"   {y:5.3f}    {len(roots):5d}       {signed_count(roots):+d}")
    print()
    print("2. Maps of the Riemann sphere")
    cubic = [1, 0, -3, 0]
    crit = critical_values_complex(cubic)
    for name, w in [("0", 0), ("i", 1j), ("2", 2), ("-2", -2), ("5-3i", 5 - 3j)]:
        roots = complex_preimages(cubic, w)
        critical = any(abs(w - c) < 1e-9 for c in crit)
        label = "critical value" if critical else "regular value"
        print(f"   z^3 - 3z = {name:<5}: {len(roots)} preimages ({label})")
    roots = complex_preimages(cubic, 1j)
    conj_roots = [r.conjugate() for r in roots]
    print(f"   conj(z)^3 = conj(i) has {len(conj_roots)} preimages, each with")
    print("   Jacobian -|3z^2|^2 < 0: signed count -3, degree -3.")
    print()
    print("Epistemic status: numerical root-finding on a grid with bisection, for")
    print("finitely many values. The constant signed counts are evidence for the")
    print("theorem that the degree does not depend on the regular value; the")
    print("proof uses Sard's theorem and the classification of compact 1-manifolds.")

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

Used in