codex/gravitational-escape-energy.py

gravitational-escape-energy.py
"""Generate gravitational-escape-energy.png in the current directory.

Analytic radial point-mass motion with a fixed source; tested with
Python 3.14.4, NumPy 2.3.5 and Matplotlib 3.10.7.
Energy unit GMm/r0 and speed unit sqrt(GM/r0).
"""

from pathlib import Path

import matplotlib

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


def radial_speed(radius, launch_speed):
    """Outward speed on the accessible branch, in normalized units."""
    speed_squared = launch_speed**2 - 2 + 2 / np.asarray(radius)
    return np.sqrt(np.maximum(speed_squared, 0))


def main():
    radius = np.linspace(1, 8, 501)
    fig, axes = plt.subplots(1, 2, figsize=(9.5, 4.5), dpi=120,
                             facecolor="white", layout="constrained")
    fig.suptitle("Radial escape from a fixed point mass", fontsize=15)
    axes[0].plot(radius, 1 / radius, color="#236b9c", lw=2.4,
                 label=r"Kinetic $K=1/z$")
    axes[0].plot(radius, -1 / radius, color="#b3502a", lw=2.4,
                 label=r"Potential $U=-1/z$")
    axes[0].axhline(0, color="#397a58", ls="--", lw=1.8,
                    label=r"Total $E=K+U=0$")
    axes[0].set(title=r"At escape: $v_0=\sqrt{2}$",
                ylabel=r"Energy / $(GMm/r_0)$", ylim=(-1.16, 1.16))
    axes[0].legend(loc="upper right", fontsize=10)
    axes[0].text(.46, .12, "K and |U| shrink together;\nthe total stays zero.",
                 transform=axes[0].transAxes, fontsize=10,
                 bbox={"facecolor": "white", "edgecolor": "none"})

    bound_radius = np.linspace(1, 2, 161)
    axes[1].plot(bound_radius, radial_speed(bound_radius, 1),
                 color="#b3502a", lw=2.4, label=r"$v_0=1$: turns at $z=2$")
    axes[1].plot(radius, radial_speed(radius, np.sqrt(2)),
                 color="#397a58", lw=2.4, label=r"$v_0=\sqrt{2}$: escape threshold")
    axes[1].plot(radius, radial_speed(radius, 1.6),
                 color="#236b9c", lw=2.4, label=r"$v_0=1.6$: excess energy")
    limiting_speed = np.sqrt(1.6**2 - 2)
    axes[1].axhline(limiting_speed, color="#236b9c", ls=":", lw=1.4)
    axes[1].scatter(2, 0, s=40, color="#b3502a", zorder=4)
    axes[1].annotate("turn and return", (2, 0), xytext=(3.0, .19),
                     arrowprops={"arrowstyle": "->", "color": "#b3502a"},
                     fontsize=10)
    axes[1].text(5.0, .24, "Blue dotted limit:\n" +
                 rf"$v_\infty={limiting_speed:.3f}$", fontsize=11, color="#236b9c")
    axes[1].set(title="Outward speed; all launches are radial",
                ylabel=r"Speed / $\sqrt{GM/r_0}$", ylim=(-.08, 2.1))
    axes[1].legend(loc="upper right", fontsize=9)
    for ax in axes:
        ax.set(xlabel=r"Radius $z=r/r_0$", xlim=(.9, 8.1))
        ax.set_xticks([1, 2, 4, 6, 8])
        ax.set_facecolor("white")
        ax.grid(alpha=.18)
        ax.spines[["top", "right"]].set_visible(False)
    fig.savefig(Path.cwd() / "gravitational-escape-energy.png",
                facecolor="white", transparent=False)
    plt.close(fig)
    print(f"Escape speed: {np.sqrt(2):.10f}; v0=1 turns at z=2")
    print(f"For v0=1.6: E=0.28 and limiting speed={limiting_speed:.10f}")


if __name__ == "__main__":
    main()