Skip to content
The Lumenary

Math laboratory

Total curvature of closed curves: Hopf, Fenchel, Fary-Milnor

Integrates the curvature of closed curves numerically. Plane curves (circle, ellipse, limacon, figure eight) give 2 pi times their turning numbers 1, 1, 2, 0; space curves give total curvature 2 pi for a convex plane curve and more than 4 pi for a trefoil knot, as the theorems of Fenchel and Fary-Milnor predict; a helix's curvature matches a/(a^2+b^2). Numerical evidence for these curves only.

Back to the labNumerical approximation
Total curvature of closed curves: Hopf, Fenchel, Fary-MilnorPythonNumerical approximationComputationally tested
"""Total curvature of closed curves: Hopf, Fenchel and Fary-Milnor.

For a closed plane curve, the signed curvature integrated along the curve,
the integral of kappa ds, is 2 pi times the turning number (Hopf's
Umlaufsatz: +-1 for a simple closed curve). For a closed space curve the
total curvature, the integral of |kappa| ds, is at least 2 pi (Fenchel), and
more than 4 pi if the curve is knotted (Fary-Milnor). We compute these
integrals numerically: derivatives by central differences, integrals by the
midpoint rule. We also check the curvature of a helix against the exact value
a / (a^2 + b^2). Evidence: numerical, for these curves only.
"""

import math

H = 1e-4
N = 4000

def derivatives(curve, t):
    """First and second derivatives of a curve R -> R^n by central differences."""
    p_minus, p, p_plus = curve(t - H), curve(t), curve(t + H)
    d1 = [(b - a) / (2 * H) for a, b in zip(p_minus, p_plus)]
    d2 = [(a - 2 * c + b) / H ** 2 for a, c, b in zip(p_minus, p, p_plus)]
    return d1, d2

def cross(u, v):
    """Cross product in R^3."""
    return (u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2],
            u[0] * v[1] - u[1] * v[0])

def norm(u):
    """Euclidean length."""
    return math.sqrt(sum(x * x for x in u))

def signed_curvature(curve, t):
    """kappa = (x'y'' - y'x'') / |gamma'|^3 for a plane curve."""
    (x1, y1), (x2, y2) = derivatives(curve, t)
    return (x1 * y2 - y1 * x2) / (x1 * x1 + y1 * y1) ** 1.5

def curvature(curve, t):
    """kappa = |gamma' x gamma''| / |gamma'|^3 for a space curve."""
    d1, d2 = derivatives(curve, t)
    return norm(cross(d1, d2)) / norm(d1) ** 3

def integrate_over_period(f, curve, period=2 * math.pi, n=N):
    """Midpoint rule for the integral of f(curve, t) |gamma'(t)| dt."""
    dt = period / n
    total = 0.0
    for k in range(n):
        t = (k + 0.5) * dt
        speed = norm(derivatives(curve, t)[0])
        total += f(curve, t) * speed * dt
    return total

PLANE = {
    "circle": lambda t: (math.cos(t), math.sin(t)),
    "ellipse 3 x 1": lambda t: (3 * math.cos(t), math.sin(t)),
    "limacon r=1+2cos": lambda t: ((1 + 2 * math.cos(t)) * math.cos(t),
                                   (1 + 2 * math.cos(t)) * math.sin(t)),
    "figure eight": lambda t: (math.sin(t), math.sin(t) * math.cos(t)),
}

SPACE = {
    "circle in a plane": lambda t: (math.cos(t), math.sin(t), 0.0),
    "tilted ellipse": lambda t: (2 * math.cos(t), math.sin(t),
                                 math.sin(t)),
    "bent loop (unknot)": lambda t: (math.cos(t), math.sin(t),
                                     0.8 * math.cos(2 * t)),
    "trefoil knot": lambda t: (math.sin(t) + 2 * math.sin(2 * t),
                               math.cos(t) - 2 * math.cos(2 * t),
                               -math.sin(3 * t)),
}

def helix(a, b):
    """The helix t -> (a cos t, a sin t, b t)."""
    return lambda t: (a * math.cos(t), a * math.sin(t), b * t)

def main():
    print("Plane curves: integral of signed curvature / 2 pi = turning number")
    for name, c in PLANE.items():
        total = integrate_over_period(signed_curvature, c)
        print(f"  {name:<18} {round(total / (2 * math.pi), 5) + 0.0:9.5f}")
    print()
    print("Space curves: total curvature / pi (Fenchel: >= 2; knotted: > 4)")
    for name, c in SPACE.items():
        total = integrate_over_period(curvature, c)
        print(f"  {name:<18} {total / math.pi:9.5f}")
    print()
    a, b = 2.0, 1.0
    k = curvature(helix(a, b), 0.7)
    print(f"Helix a = {a}, b = {b}: kappa = {k:.6f}, exact a/(a^2+b^2) = "
          f"{a / (a * a + b * b):.6f}")
    print()
    print("The figure eight turns 0 times (it is not simple); the limacon with")
    print("its inner loop turns twice. Only the planar convex curves reach")
    print("Fenchel's bound 2 pi exactly, and the knotted curve exceeds 4 pi.")
    print()
    print("Epistemic status: numerical (central differences with h = 1e-4 and")
    print("a 4000-point midpoint rule), for these particular curves. The")
    print("theorems of Hopf, Fenchel and Fary-Milnor are proved in the")
    print("literature cited in the lesson, not here.")

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

Used in