Skip to content
The Lumenary

Math laboratory

Light bending and Mercury's perihelion from geodesics

Integrates the Schwarzschild orbit equations u'' + u = 3Mu^2 (light) and u'' + u = M/L^2 + 3Mu^2 (planets) with RK4 and compares the deflection angle with 4M/b and the perihelion advance with 6 pi M/p for several impact parameters and orbits; the ratios tend to 1 as the field weakens. Then evaluates 1.75 arcseconds for starlight grazing the Sun and 43 arcseconds per century for Mercury. Floating-point evidence.

Back to the labNumerical approximation
Light bending and Mercury's perihelion from geodesicsPythonNumerical approximationComputationally tested
"""Light bending and perihelion precession from Schwarzschild geodesics.

With u = 1/r and units G = c = 1, geodesics in the equatorial plane of the
Schwarzschild metric satisfy u'' + u = 3 M u^2 for light and
u'' + u = M / L^2 + 3 M u^2 for planets (' = d/dphi). We integrate both by
the Runge-Kutta method (RK4), locate the end point by bisection, and compare
the light deflection with 4M/b (plus the second-order term 15 pi M^2/(4 b^2))
and the perihelion advance per orbit with 6 pi M/p, p = L^2/M. Finally we
evaluate both leading-order formulas for the Sun and Mercury in SI units.
"""

import math

def rk4_step(f, phi, y, h):
    """One classical Runge-Kutta step for y' = f(phi, y), y = (u, w)."""
    k1 = f(phi, y)
    k2 = f(phi + h / 2, [y[i] + h / 2 * k1[i] for i in range(2)])
    k3 = f(phi + h / 2, [y[i] + h / 2 * k2[i] for i in range(2)])
    k4 = f(phi + h, [y[i] + h * k3[i] for i in range(2)])
    return [y[i] + h / 6 * (k1[i] + 2 * k2[i] + 2 * k3[i] + k4[i])
            for i in range(2)]

def first_crossing(f, y0, index, sign, h, phi_min):
    """Smallest phi > phi_min where component `index` crosses 0 with the
    given sign of the derivative (+1 upward, -1 downward), by RK4 + bisection.
    """
    phi, y = 0.0, list(y0)
    while True:
        y_next = rk4_step(f, phi, y, h)
        crosses = (y[index] * sign < 0 <= y_next[index] * sign)
        if phi + h > phi_min and crosses:
            lo, hi = 0.0, h
            for _ in range(60):
                mid = (lo + hi) / 2
                if rk4_step(f, phi, y, mid)[index] * sign < 0:
                    lo = mid
                else:
                    hi = mid
            return phi + (lo + hi) / 2
        phi, y = phi + h, y_next

def light_deflection(mass, b, steps=6000):
    """Total deflection angle of a light ray with impact parameter b."""
    def f(_phi, y):
        return [y[1], 3 * mass * y[0] ** 2 - y[0]]
    end = first_crossing(f, [0.0, 1.0 / b], 0, -1, math.pi / steps, 1.0)
    return end - math.pi

def perihelion_advance(mass, p, e, steps=6000):
    """Advance of the perihelion per orbit for an orbit with u_max = (1+e)/p."""
    def f(_phi, y):
        return [y[1], mass / (mass * p) + 3 * mass * y[0] ** 2 - y[0]]
    end = first_crossing(f, [(1 + e) / p, 0.0], 1, -1, 2 * math.pi / steps,
                         math.pi)
    return end - 2 * math.pi

def main():
    print("Light ray past mass M = 1, impact parameter b:")
    print("       b    deflection     4M/b   + 15 pi M^2/(4 b^2)   ratio")
    for b in [10.0, 30.0, 100.0, 1000.0]:
        d = light_deflection(1.0, b)
        lead = 4 / b
        second = lead + 15 * math.pi / (4 * b * b)
        print(f"  {b:>6.0f}   {d:.7f}   {lead:.7f}   {second:.7f}"
              f"          {d / lead:.5f}")
    print()
    print("Bound orbit, eccentricity 0.2, semi-latus rectum p (M = 1):")
    print("       p    advance/orbit   6 pi M/p     ratio")
    for p in [30.0, 100.0, 1000.0, 10000.0]:
        adv = perihelion_advance(1.0, p, 0.2)
        lead = 6 * math.pi / p
        print(f"  {p:>6.0f}   {adv:.8f}      {lead:.8f}   {adv / lead:.5f}")
    print()
    gm, c = 1.32712440018e20, 299792458.0
    r_sun = 6.957e8
    arcsec = 180 / math.pi * 3600
    bend = 4 * gm / (c * c * r_sun) * arcsec
    a, e, period_days = 5.7909050e10, 0.205630, 87.9691
    per_orbit = 6 * math.pi * gm / (c * c * a * (1 - e * e))
    per_century = per_orbit * 36525 / period_days * arcsec
    print(f"Sun: starlight grazing the limb is bent by {bend:.3f} arcsec")
    print(f"Mercury: perihelion advance {per_century:.2f} arcsec per century")
    print()
    print("Epistemic status: RK4 integration in floating point (step errors")
    print("far below the digits shown) of the exact geodesic equations, for")
    print("finitely many cases. The 4M/b and 6 pi M/p laws are leading-order")
    print("theorems derived in the lesson; observations confirm the numbers.")

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

Used in