codex/great-circle-versus-parallel-distance.py

great-circle-versus-parallel-distance.py
"""Compare spherical great-circle and constant-latitude paths; PNG to CWD.

Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
Run from _media to regenerate the published image. This is idealized geometry.
"""

from pathlib import Path

import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np


def main():
    latitude = np.pi / 6
    longitude = np.linspace(-np.pi / 3, np.pi / 3, 501)
    parallel = np.column_stack((np.cos(latitude) * np.cos(longitude),
                                np.cos(latitude) * np.sin(longitude),
                                np.full_like(longitude, np.sin(latitude))))
    p, q = parallel[0], parallel[-1]
    delta = np.arccos(np.clip(p @ q, -1, 1))
    t = np.linspace(0, 1, 501)
    great = (np.sin((1 - t) * delta)[:, None] * p
             + np.sin(t * delta)[:, None] * q) / np.sin(delta)
    great_lon = np.arctan2(great[:, 1], great[:, 0])
    great_lat = np.arctan2(great[:, 2], np.hypot(great[:, 0], great[:, 1]))
    apex = great[len(great) // 2]
    parallel_length = np.cos(latitude) * (2 * np.pi / 3)
    assert np.isclose(p @ q, -1 / 8)
    assert np.allclose(np.linalg.norm(great, axis=1), 1)
    assert np.allclose(great @ np.cross(p, q), 0)
    assert np.isclose(great_lat.max(), np.arctan(2 / np.sqrt(3)))
    assert delta < parallel_length

    plt.rcParams.update({'font.size': 12})
    fig = plt.figure(figsize=(9.6, 6.5), facecolor='white')
    ax = fig.add_axes([-.03, .17, .58, .69], projection='3d', computed_zorder=False)
    ax.set_facecolor('white')
    ax.set_proj_type('ortho')
    ax.view_init(elev=22, azim=0)
    ax.set_box_aspect((1, 1, 1), zoom=1.18)
    ax.set(xlim=(-1.1, 1.1), ylim=(-1.1, 1.1), zlim=(-1.1, 1.1))
    ax.set_axis_off()
    phi, theta = np.meshgrid(np.linspace(0, 2 * np.pi, 85), np.linspace(0, np.pi, 43))
    ax.plot_wireframe(np.sin(theta) * np.cos(phi), np.sin(theta) * np.sin(phi),
                      np.cos(theta), rstride=4, cstride=7, color='#acbdca',
                      alpha=.35, lw=.65, zorder=1)
    equator = np.column_stack((np.cos(phi[0]), np.sin(phi[0]), np.zeros_like(phi[0])))
    ax.plot(*equator.T, color='#8a9aa7', lw=1, alpha=.6, zorder=2)
    ax.plot(*parallel.T, color='#c05d1d', lw=3, ls='--', zorder=3)
    ax.plot(*great.T, color='#147789', lw=3.2, zorder=4)
    for vertex, label, align in [(p, r'$P:\ -60^\circ$', 'right'),
                                  (q, r'$Q:\ +60^\circ$', 'left')]:
        ax.scatter(*vertex, color='#253c48', s=25, depthshade=False, zorder=5)
        ax.text(*(1.13 * vertex), label, ha=align, va='center', fontsize=12, zorder=6)
    ax.scatter(*apex, color='#147789', s=22, depthshade=False, zorder=5)
    ax.text(*(apex + [0, 0, .13]), r'$49.1066^\circ$', ha='center', fontsize=12,
            color='#147789', zorder=6)
    fig.text(.25, .23, 'Ideal sphere of radius R', ha='center', color='#465865', fontsize=12)

    chart = fig.add_axes([.60, .35, .36, .43])
    chart.set_facecolor('white')
    chart.plot(np.degrees(great_lon), np.degrees(great_lat), color='#147789', lw=3,
               label='Great circle')
    chart.plot(np.degrees(longitude), np.full_like(longitude, 30), color='#c05d1d',
               lw=2.5, ls='--', label='Constant latitude')
    chart.scatter([-60, 60], [30, 30], color='#253c48', s=23, zorder=5)
    chart.set(xlim=(-65, 65), ylim=(25, 56), xticks=[-60, -30, 0, 30, 60],
               yticks=[30, 40, 50], xlabel='Longitude (degrees)', ylabel='Latitude (degrees)')
    chart.spines[['top', 'right']].set_visible(False)
    chart.grid(alpha=.2)
    chart.legend(loc='upper center', fontsize=10, frameon=False)
    chart.annotate('49.1066°', xy=(0, np.degrees(great_lat.max())), xytext=(20, 46),
                   fontsize=10, color='#147789',
                   arrowprops={'arrowstyle': '-', 'color': '#147789'})
    fig.suptitle('Same latitude, different surface paths', fontsize=18, y=.96)
    fig.text(.5, .87, 'Endpoints: latitude 30°, longitudes −60° and +60°', ha='center', fontsize=13)
    fig.text(.60, .20, r'$d_{\mathrm{great}}=R\arccos(-1/8)\approx1.696124R$',
             color='#147789', fontsize=13)
    fig.text(.60, .12, r'$\ell_{\mathrm{parallel}}=R\cos30^\circ(2\pi/3)\approx1.813799R$',
             color='#c05d1d', fontsize=12)
    fig.text(.5, .035, 'The latitude-longitude chart shows positions; its plotted line lengths are not surface distances.',
             ha='center', fontsize=11)
    fig.savefig(Path.cwd() / Path(__file__).with_suffix('.png').name,
                dpi=130, facecolor='white', transparent=False)
    plt.close(fig)


if __name__ == '__main__':
    main()