codexvirial-kepler-ellipse-energy-time-averages.py
virial-kepler-ellipse-energy-time-averages.py
"""Exact two-body Kepler energies; Python 3.14, NumPy 2.3.5, Matplotlib 3.10.7."""
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
def main():
# Units: reduced mass m = semimajor axis a = coupling kappa = 1.
eccentricity = 0.6
phase = np.linspace(0.0, 1.0, 2401)
mean_anomaly = 2 * np.pi * phase
anomaly = mean_anomaly.copy()
for _ in range(12):
anomaly -= (anomaly - eccentricity * np.sin(anomaly) - mean_anomaly) / (
1 - eccentricity * np.cos(anomaly)
)
radius = 1 - eccentricity * np.cos(anomaly)
kinetic = 1 / radius - 0.5
potential = -1 / radius
# Integrate analytically using dt = (1 - e cos eta) d eta.
residual = np.empty_like(phase)
residual[0] = eccentricity / (1 - eccentricity)
residual[1:] = eccentricity * np.sin(anomaly[1:]) / mean_anomaly[1:]
boundary = np.empty_like(phase)
boundary[0] = residual[0]
boundary[1:] = (eccentricity * np.sin(anomaly[1:])) / (2 * np.pi * phase[1:])
plt.rcParams.update({"font.size": 11, "axes.spines.top": False, "axes.spines.right": False})
fig, (energy_ax, residual_ax) = plt.subplots(
2, 1, figsize=(9, 6.8), sharex=True, gridspec_kw={"height_ratios": [1.5, 1]}
)
fig.patch.set_facecolor("white")
energy_ax.plot(phase, kinetic, color="#1769aa", lw=2.2, label=r"$K(t)$")
energy_ax.plot(phase, potential, color="#d35f16", lw=2.2, label=r"$U(t)$")
energy_ax.axhline(-0.5, color="#333333", lw=1.7, label=r"$E=K+U=-0.5$")
energy_ax.axhline(0.5, color="#1769aa", ls="--", lw=1.5, label=r"$\langle K\rangle_P=0.5$")
energy_ax.axhline(-1, color="#d35f16", ls="--", lw=1.5, label=r"$\langle U\rangle_P=-1$")
energy_ax.set_ylabel(r"Energy / $(\kappa/a)$")
energy_ax.set_ylim(-2.8, 2.35)
energy_ax.legend(loc="lower center", ncol=3, fontsize=10, framealpha=0.96)
energy_ax.set_title("Kepler ellipse: varying energies, exact period averages", pad=12)
residual_ax.plot(phase, residual, color="#1769aa", lw=3,
label=r"$2\langle K\rangle_T+\langle U\rangle_T$")
residual_ax.plot(phase, boundary, color="#d35f16", ls=(0, (5, 4)), lw=1.8,
label=r"$[G(T)-G(0)]/T$")
residual_ax.axhline(0, color="#444444", lw=0.8)
quarter = len(phase) // 4
residual_ax.scatter([0.25], [residual[quarter]], s=35, color="#222222", zorder=5)
residual_ax.annotate(f"Quarter-period residual: {residual[quarter]:.4f}",
xy=(0.25, residual[quarter]), xytext=(0.39, 0.95),
arrowprops={"arrowstyle": "->", "color": "#444444"}, fontsize=10)
residual_ax.set_ylabel(r"Virial residual / $(\kappa/a)$")
residual_ax.set_xlabel(r"Elapsed time / orbital period, $T/P$")
residual_ax.set_ylim(-0.23, 1.7)
residual_ax.legend(loc="upper right", fontsize=10)
for ax in (energy_ax, residual_ax):
ax.set_facecolor("white")
ax.axvspan(0, 0.25, color="#f4dfa5", alpha=0.32, zorder=0)
ax.set_xlim(0, 1)
ax.set_xticks(np.arange(0, 1.01, 0.125))
ax.grid(alpha=0.18)
fig.text(0.5, 0.015, "Original exact model: m = a = κ = 1, e = 0.6; shaded window T = P/4",
ha="center", fontsize=10)
fig.tight_layout(rect=(0, 0.045, 1, 1))
fig.savefig(Path(__file__).stem + ".png", dpi=120, facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()