codex/foucault-pendulum-exact-slow-trajectories.py

foucault-pendulum-exact-slow-trajectories.py
"""Exact trajectories of the stated truncated linear ODE, not a full Earth model.
Python 3.14, NumPy 2.3.5, Matplotlib 3.10.7. Run from _media.
Equal-amplitude release from rest, length 20 m, g=9.81 m/s²; physical Earth rate.
Each coloured segment spans one fast period, starting at the stated elapsed time.
"""
from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt

OMEGA = 7.292115e-5
LENGTH = 20.0
G = 9.81

def trajectory(t, latitude):
    alpha = OMEGA * np.sin(np.deg2rad(latitude))
    beta = np.sqrt(G / LENGTH + alpha**2)
    return np.exp(-1j * alpha * t) * (np.cos(beta * t) + 1j * alpha / beta * np.sin(beta * t))

if __name__ == '__main__':
    plt.rcParams.update({'font.size': 11, 'figure.facecolor': 'white', 'axes.facecolor': 'white'})
    fig, axes = plt.subplots(1, 3, figsize=(11.4, 4.8), layout='constrained')
    for ax, latitude, name in zip(axes, (45, 0, -45), ('45° north: clockwise', 'Equator: no slow turning', '45° south: anticlockwise')):
        alpha = OMEGA * np.sin(np.deg2rad(latitude))
        beta = np.sqrt(G / LENGTH + alpha**2)
        for hours, color, style in ((0, '#235b91', '-'), (3, '#cf7a18', '--'), (6, '#9b365e', ':')):
            t = hours * 3600 + np.linspace(0, 2 * np.pi / beta, 1500)
            z = trajectory(t, latitude)
            ax.plot(z.real, z.imag, color=color, ls=style, lw=2.2, label=f'{hours} h')
        ax.axhline(0, color='.8', lw=.8)
        ax.axvline(0, color='.8', lw=.8)
        ax.set(xlim=(-1.13, 1.13), ylim=(-1.13, 1.13), aspect='equal', xlabel='East displacement / A', ylabel='North displacement / A', title=name)
        ax.set_xticks([-1, 0, 1])
        ax.set_yticks([-1, 0, 1])
        ax.legend(loc='upper right', fontsize=9)
    fig.suptitle('Rapid swings, slowly rotating direction\nExact linear solution at the real Earth rotation rate', weight='bold', fontsize=15)
    fig.supxlabel('20 m illustrative pendulum: fast period ≈ 8.97 s; snapshots start 3 hours apart.\nEach segment is one full swing; the tiny transverse width is below this drawing’s resolution.', fontsize=10)
    fig.savefig(Path(__file__).stem + '.png', dpi=100, facecolor='white')