codexinverse-square-escape-energy-balance.py
inverse-square-escape-energy-balance.py
"""Plot the isolated inverse-square force's escape energy budget.
Run from _media; writes the matching PNG basename to the working directory.
Tested with Python 3.14, Matplotlib 3.10.7, NumPy 2.3.5 (root pyproject.toml).
All values are synthetic: mu = 8 m^3/s^2, r0 = 2 m, test mass = 1 kg.
"""
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
def main():
mu, r0 = 8.0, 2.0
energy_unit = mu / r0
energies = (-0.5, 0.0, 0.5)
colors = ("#2765a4", "#c66c0d", "#21825b")
x = np.linspace(1.0, 8.0, 900)
plt.rcParams.update({"font.size": 12, "axes.titlesize": 14})
fig, axes = plt.subplots(1, 2, figsize=(10.0, 4.8), layout="constrained")
fig.set_facecolor("white")
fig.suptitle("Radial launch: energy determines escape", fontsize=17)
ax, kinetic = axes
ax.plot(x, -1 / x, color="#282d34", lw=2.6, label=r"Potential $U/(m\mu/r_0)=-1/x$")
for e, color in zip(energies, colors, strict=True):
label = rf"$\mathcal{{E}}={e:+g}$" if e else r"$\mathcal{E}=0$ (threshold)"
ax.axhline(e, color=color, lw=2, label=label)
allowed_x = np.linspace(1, -1 / e if e < 0 else 8, 700)
k = e + 1 / allowed_x
assert np.min(k) >= -1e-14
np.testing.assert_allclose(k - 1 / allowed_x, e, atol=1e-14)
kinetic.plot(allowed_x, k, color=color, lw=2.4, label=label)
ax.plot(1, e, "o", color=color, ms=6)
kinetic.plot(1, e + 1, "o", color=color, ms=6)
ax.plot(2, -0.5, "o", color=colors[0], ms=7)
ax.annotate("Bound launch turns\nat $r=2r_0$", xy=(2, -0.5), xytext=(3.5, -0.77),
arrowprops={"arrowstyle": "->", "color": colors[0]}, color=colors[0])
kinetic.plot(2, 0, "o", color=colors[0], ms=7)
kinetic.axhline(0.5, color=colors[2], ls=":", lw=1.3)
kinetic.annotate(r"Positive energy: $K\to E$", xy=(7.2, 0.5), xytext=(3.1, 0.31),
arrowprops={"arrowstyle": "->", "color": colors[2]}, color=colors[2])
kinetic.annotate(r"Threshold: $K\to0$", xy=(7.6, 1 / 7.6), xytext=(3.6, 0.04),
arrowprops={"arrowstyle": "->", "color": colors[1]}, color=colors[1])
ax.set(title="Potential and constant total energies", ylim=(-1.1, 0.76),
ylabel=r"Energy / $(m\mu/r_0)$")
kinetic.set(title="Remaining kinetic energy",
ylim=(-0.08, 1.62), ylabel=r"Kinetic energy / $(m\mu/r_0)$")
for axis in axes:
axis.set(xlim=(0.85, 8.15), xlabel=r"Distance $x=r/r_0$")
axis.grid(alpha=0.18)
axis.spines[["top", "right"]].set_visible(False)
ax.legend(loc="upper right", fontsize=10, framealpha=1)
speeds = np.sqrt(2 * energy_unit * (np.array(energies) + 1))
np.testing.assert_allclose(speeds**2, [4, 8, 12], atol=1e-13)
assert mu / r0 == 4 and -mu / (energy_unit * energies[0]) == 4
fig.savefig(Path(__file__).stem + ".png", dpi=120, facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()