codex/equatorial-coordinates-reference-geometry.py

equatorial-coordinates-reference-geometry.py
"""Original equatorial-coordinate geometry; run from _media to write the PNG.

Tested with Python 3.14, NumPy 2.3.5 and Matplotlib 3.10.7.
All coordinates are analytical examples, not a star catalogue.
"""
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def project(points):
    """Linear schematic projection onto a readable two-dimensional drawing."""
    points = np.asarray(points)
    return np.column_stack((0.9 * points[..., 0] + 0.4 * points[..., 1],
                            -0.25 * points[..., 0] + 0.5 * points[..., 1]
                            + 0.85 * points[..., 2]))


def curve(ax, points, **kwargs):
    xy = project(points)
    ax.plot(xy[:, 0], xy[:, 1], **kwargs)


def label(ax, point, text, offset=(5, 5), **kwargs):
    ax.annotate(text, project([point])[0], xytext=offset,
                textcoords="offset points", **kwargs)


def main():
    plt.rcParams.update({"font.size": 11, "font.family": "DejaVu Sans"})
    fig, (ax, bx) = plt.subplots(1, 2, figsize=(10, 5.6),
                                gridspec_kw={"width_ratios": [1.25, 1]})
    fig.patch.set_facecolor("white")
    fig.suptitle("Equatorial coordinates: directions on a reference sphere", fontsize=15)
    blue, orange, green = "#1966a3", "#be510b", "#22805b"
    t = np.linspace(0, 2 * np.pi, 500)
    equator = np.column_stack((np.cos(t), np.sin(t), np.zeros_like(t)))
    curve(ax, equator, color=blue, lw=2)
    alpha, delta = np.deg2rad([60, 30])
    d = np.linspace(-np.pi / 2, np.pi / 2, 300)
    hour_half = np.column_stack((np.cos(d) * np.cos(alpha),
                                 np.cos(d) * np.sin(alpha), np.sin(d)))
    curve(ax, hour_half, color="#969696", lw=1.3)
    back = hour_half.copy()
    back[:, :2] *= -1
    curve(ax, back, color="#b6b6b6", lw=1, ls="--")
    for endpoint in ([1, 0, 0], [0, 1, 0], [0, 0, 1.2], [0, 0, -1.12]):
        p = project([endpoint])[0]
        ax.annotate("", p, xytext=(0, 0), arrowprops={"arrowstyle": "->", "color": "#666666"})
    n = [np.cos(delta) * np.cos(alpha), np.cos(delta) * np.sin(alpha), np.sin(delta)]
    curve(ax, [[0, 0, 0], n], color=green, lw=2)
    xy = project([n])[0]
    ax.scatter(*xy, s=85, marker="*", color=green, zorder=5)
    a = np.linspace(0, alpha, 100)
    curve(ax, np.column_stack((np.cos(a), np.sin(a), a * 0)), color=orange, lw=4)
    dd = np.linspace(0, delta, 100)
    curve(ax, np.column_stack((np.cos(dd) * np.cos(alpha),
                               np.cos(dd) * np.sin(alpha), np.sin(dd))), color=green, lw=4)
    label(ax, [1, 0, 0], "reference equinox\nα = 0", offset=(4, -33), ha="left", color=orange)
    label(ax, [0, 0, 1.2], "north reference pole\nδ = +90°", ha="center", offset=(0, 8))
    label(ax, n, "synthetic direction\nα = 4 h, δ = 30°", offset=(10, 12), ha="left", color=green)
    label(ax, [0, 1, 0], "y", offset=(-10, -12))
    label(ax, [0.86, 0.5, 0], "α: eastward", offset=(9, -14), color=orange)
    label(ax, [np.cos(.18) * .5, np.cos(.18) * np.sqrt(3) / 2, np.sin(.18)],
          "δ: northward", offset=(12, 0), color=green)
    ax.text(-1.35, -1.24, "Blue: reference celestial equator\nGrey: hour circle through the direction\nCentre: origin of viewing directions", fontsize=10, va="top")
    ax.set(xlim=(-1.48, 1.85), ylim=(-1.8, 1.7), aspect="equal")
    ax.axis("off")

    bx.set_title("A parallel shrinks toward a pole", fontsize=12)
    for cy, dec, color in [(0.88, 0, blue), (-.15, 60, green)]:
        radius = np.cos(np.deg2rad(dec))
        u = np.linspace(0, 2 * np.pi, 360)
        bx.plot(radius * np.cos(u), cy + .30 * radius * np.sin(u), color="#aeb8bf", lw=1.4)
        u = np.deg2rad(np.linspace(55, 70, 100))
        bx.plot(radius * np.cos(u), cy + .30 * radius * np.sin(u), color=color, lw=5)
        bx.plot([0, radius * np.cos(u[0])], [cy, cy + .30 * radius * np.sin(u[0])],
                color=color, lw=1)
        bx.plot([0, radius * np.cos(u[-1])], [cy, cy + .30 * radius * np.sin(u[-1])],
                color=color, lw=1)
        bx.text(-1.05, cy - .40, f"δ = {dec}°: parallel radius = {radius:g}", color=color, fontsize=11)
    bx.text(0, 1.46, "Same RA increment: Δα = 1 h = 15°", ha="center", fontsize=11)
    bx.text(0, .30, "Equator: arc length = 15°", ha="center", color=blue)
    bx.text(0, -.72, "δ = 60°: arc length = 7.5°", ha="center", color=green)
    bx.text(0, -1.13, "At δ = ±90°, the parallel is one point.\nAll α values describe the same direction.",
            ha="center", fontsize=10, bbox={"boxstyle": "round,pad=.5", "fc": "#f3f5f7", "ec": "none"})
    bx.text(0, -1.56, "Arc lengths follow the parallel, not a great circle.\nAnalytical illustration; no measured star positions.",
            ha="center", fontsize=9, color="#555555")
    bx.set(xlim=(-1.35, 1.35), ylim=(-1.8, 1.8), aspect="equal")
    bx.axis("off")
    fig.subplots_adjust(left=.025, right=.98, bottom=.04, top=.86, wspace=.05)
    fig.savefig(Path(__file__).with_suffix(".png").name, dpi=120, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()