codexsphere-geodesic-short-and-long.py
sphere-geodesic-short-and-long.py
"""Compare two affinely parametrized great-circle arcs; emit PNG to cwd."""
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
def main():
plt.rcParams.update({"font.size": 12, "figure.facecolor": "white"})
fig = plt.figure(figsize=(11, 5.4), layout="constrained")
ax = fig.add_subplot(121, projection="3d")
flat = fig.add_subplot(122)
short_color, long_color = "#087e8b", "#c85b28"
t = np.linspace(0, 1, 2001)
theta_short, theta_long = np.pi * t / 3, -5 * np.pi * t / 3
p = np.array([1., 0., 0.])
q = np.array([.5, np.sqrt(3) / 2, 0.])
assert np.allclose([np.cos(theta_long[-1]), np.sin(theta_long[-1]), 0], q)
assert np.isclose(np.linalg.norm(p - q), 1)
for theta, length in [(theta_short, np.pi / 3), (theta_long, 5 * np.pi / 3)]:
chord_sum = np.hypot(np.diff(np.cos(theta)), np.diff(np.sin(theta))).sum()
assert np.isclose(chord_sum, length, rtol=1e-6)
phi, latitude = np.meshgrid(np.linspace(0, 2 * np.pi, 61), np.linspace(-np.pi / 2, np.pi / 2, 31))
ax.plot_wireframe(np.cos(latitude) * np.cos(phi), np.cos(latitude) * np.sin(phi),
np.sin(latitude), rstride=5, cstride=8, color="#bdc9d2", lw=.65, alpha=.65)
azimuth = -65
for theta, color in [(theta_long, long_color), (theta_short, short_color)]:
curve = np.array([np.cos(theta), np.sin(theta), np.zeros_like(theta)])
visible = np.cos(theta - np.deg2rad(azimuth)) >= 0
for mask, style, alpha in [(visible, "-", 1), (~visible, "--", .7)]:
part = curve.copy()
part[:, ~mask] = np.nan
ax.plot(*part, color=color, lw=3, linestyle=style, alpha=alpha)
for point, label in [(p, "p"), (q, "q")]:
ax.scatter(*point, color="#25313b", s=30, depthshade=False)
ax.text(*(point + np.array([.06, .06, .07])), label, fontsize=14)
ax.view_init(elev=26, azim=azimuth)
ax.set_box_aspect((1, 1, 1), zoom=1.35)
ax.set(xlim=(-1.15, 1.15), ylim=(-1.15, 1.15), zlim=(-1.15, 1.15))
ax.set_axis_off()
ax.set_title("Both arcs lie on the unit sphere", pad=12)
for theta, color, label in [
(theta_short, short_color, r"Short arc: $\pi/3 \approx 1.047$"),
(theta_long, long_color, r"Long arc: $5\pi/3 \approx 5.236$"),
]:
flat.plot(np.cos(theta), np.sin(theta), color=color, lw=3.5, label=label)
n = len(theta) // 2
flat.annotate("", xy=(np.cos(theta[n + 60]), np.sin(theta[n + 60])),
xytext=(np.cos(theta[n]), np.sin(theta[n])),
arrowprops={"arrowstyle": "->", "color": color, "lw": 2})
flat.plot([p[0], q[0]], [p[1], q[1]], "--", color="#85909a", lw=1.5,
label="Chord: 1 (leaves the surface)")
for point, label in [(p, "p"), (q, "q")]:
flat.scatter(*point[:2], color="#25313b", s=30, zorder=5)
flat.annotate(label, point[:2], xytext=(8, 5), textcoords="offset points", fontsize=14)
flat.set_aspect("equal")
flat.set(xlim=(-1.25, 1.28), ylim=(-1.24, 1.24))
flat.set_axis_off()
flat.set_title("The equatorial plane shows the two routes")
flat.legend(loc="upper center", bbox_to_anchor=(.5, -.02), frameon=False, fontsize=11)
fig.suptitle("A geodesic need not be the globally shortest route", fontsize=17)
fig.savefig(Path(__file__).with_suffix(".png").name, dpi=100, facecolor="white")
plt.close(fig)
if __name__ == "__main__":
main()