codexsynchronous-rotation-facing-companion.py
synchronous-rotation-facing-companion.py
"""Generate synchronous-rotation-facing-companion.png in the current directory.
Tested with Python 3.14.4, numpy 2.3.5 and matplotlib 3.10.7.
Circular-orbit geometry cartoon, not an ephemeris or observed lunar states.
The colored hemispheres and marker indicate material orientation, not light.
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, Wedge
import numpy as np
def marked_body(ax, centre, theta, radius=0.32):
"""Draw a body with a marked material direction at inertial angle theta."""
angle = np.degrees(theta)
ax.add_patch(Circle(centre, radius, facecolor="#d5e7f2",
edgecolor="none", zorder=3))
ax.add_patch(Wedge(centre, radius, angle - 90, angle + 90,
facecolor="#ffe1bf", edgecolor="none", zorder=4))
ax.add_patch(Circle(centre, radius, fill=False, edgecolor="#4b5964",
linewidth=1.3, zorder=5))
tip = centre + radius * np.array([np.cos(theta), np.sin(theta)])
ax.annotate("", xy=tip, xytext=centre,
arrowprops={"arrowstyle": "->", "color": "#b95012", "lw": 1.8},
zorder=6)
ax.scatter(*tip, s=30, color="#b95012", edgecolor="white",
linewidth=0.6, zorder=7)
def main():
plt.rcParams.update({"font.size": 11, "figure.facecolor": "white",
"axes.facecolor": "white"})
fig, axes = plt.subplots(1, 2, figsize=(10.4, 6.0), dpi=110)
fig.subplots_adjust(left=0.025, right=0.975, bottom=0.18, top=0.80, wspace=0.12)
phases = np.arange(4) * np.pi / 2
labels = ["0", "T/4", "T/2", "3T/4"]
orbit_radius = 1.65
samples = np.linspace(0, 2 * np.pi, 400)
for ax, synchronous in zip(axes, [True, False]):
ax.set_aspect("equal")
ax.axis("off")
ax.set(xlim=(-2.45, 2.45), ylim=(-2.35, 2.35))
ax.plot(orbit_radius * np.cos(samples), orbit_radius * np.sin(samples),
color="#a9b4bc", linestyle="--", lw=1)
ax.add_patch(Circle((0, 0), 0.38, facecolor="#687d91", zorder=3))
ax.text(0, -0.62, "Companion", ha="center", color="#34495b")
for f, label in zip(phases, labels):
direction = np.array([np.cos(f), np.sin(f)])
centre = orbit_radius * direction
theta = f + np.pi if synchronous else np.pi
marked_body(ax, centre, theta)
ax.text(*(2.20 * direction), label, ha="center", va="center")
a0, a1 = -np.pi / 3, -np.pi / 3 + 0.25
ax.annotate("", xy=orbit_radius * np.array([np.cos(a1), np.sin(a1)]),
xytext=orbit_radius * np.array([np.cos(a0), np.sin(a0)]),
arrowprops={"arrowstyle": "->", "color": "#65727d", "lw": 1.7})
ax.annotate("", xy=(-1.35, -2.02), xytext=(-2.10, -2.02),
arrowprops={"arrowstyle": "->", "color": "#65727d", "lw": 1.2})
ax.text(-1.73, -2.25, "Fixed direction in space", ha="center", fontsize=9)
if synchronous:
ax.set_title("Synchronous: one spin per orbit", fontsize=13, pad=14)
ax.text(0.5, -0.07, r"$\theta=f+\pi$: same marked face points inward",
transform=ax.transAxes, ha="center", fontsize=11)
else:
ax.set_title("No spin: fixed orientation in space", fontsize=13, pad=14)
ax.text(0.5, -0.07, r"$\theta=\pi$: different faces point inward",
transform=ax.transAxes, ha="center", fontsize=11)
fig.suptitle("Facing a companion requires rotation in space", fontsize=16, y=0.97)
fig.text(0.5, 0.885, "Four snapshots of each circular orbit; orange dot = the same surface patch",
ha="center", fontsize=11)
fig.text(0.5, 0.055, "Material colors do not show sunlight. Spin axes point out of the page; sizes are arbitrary.",
ha="center", fontsize=10)
fig.text(0.5, 0.023, "Geometry cartoons, not photographs or measured lunar states.",
ha="center", fontsize=10)
fig.savefig(Path(__file__).stem + ".png", facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()