codexcircular-orbit-continuous-free-fall.py
circular-orbit-continuous-free-fall.py
"""Exact circular free fall and the tangent trajectory if gravity is removed."""
from pathlib import Path
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
import numpy as np
def main():
plt.rcParams.update({"font.size": 11, "figure.facecolor": "white"})
fig, (orbit, detail) = plt.subplots(1, 2, figsize=(11.4, 6),
layout="constrained")
angle = np.linspace(0, 2 * np.pi, 801)
points = np.c_[np.cos(angle), np.sin(angle)]
assert np.allclose(np.linalg.norm(points, axis=1), 1)
# In units a=mu=1, velocity=(-sin t,cos t), acceleration=-position.
velocity = np.c_[-np.sin(angle), np.cos(angle)]
assert np.allclose((velocity * points).sum(axis=1), 0)
assert np.allclose((velocity ** 2).sum(axis=1) / 2 - 1, -0.5)
orbit.add_patch(Circle((0, 0), 0.48, facecolor="#e1e9ed",
edgecolor="#788d99", lw=1.5))
orbit.text(0, 0, "Spherical\nsource", ha="center", va="center",
color="#364f5d")
orbit.plot(points[:, 0], points[:, 1], color="#167e8d", lw=2.3)
for theta in (0, np.pi / 2, np.pi, 3 * np.pi / 2):
start = np.array([np.cos(theta), np.sin(theta)])
tangent = np.array([-np.sin(theta), np.cos(theta)])
orbit.scatter(*start, color="#167e8d", s=35, zorder=4)
for vector, color in ((0.3 * tangent, "#526b92"),
(-0.27 * start, "#c26030")):
orbit.annotate("", xy=start + vector, xytext=start,
arrowprops={"arrowstyle": "->", "color": color,
"lw": 2.2})
orbit.plot([1, 1], [0, 1.2], "--", color="#788793", lw=1.6)
orbit.text(1.06, 0.65, "No-gravity\npath", fontsize=10)
orbit.text(-1.18, 1.25, "Blue: tangential velocity\nOrange: inward acceleration",
fontsize=11)
orbit.set_title("Constant speed, continually changing velocity", fontsize=13)
orbit.set(xlabel="x / orbital radius a", ylabel="y / orbital radius a",
xlim=(-1.38, 1.55), ylim=(-1.35, 1.55))
orbit.set_aspect("equal")
orbit.spines[["top", "right"]].set_visible(False)
orbit.grid(alpha=0.13)
theta = np.linspace(0, 0.65, 201)
detail.plot(np.cos(theta), np.sin(theta), color="#167e8d", lw=2.5,
label="With gravity: circular orbit")
detail.plot(np.ones_like(theta), theta, "--", color="#788793", lw=2,
label="Without gravity: same initial velocity")
for value in (0.2, 0.4, 0.6):
circular = np.array([np.cos(value), np.sin(value)])
inertial = np.array([1.0, value])
detail.scatter(*circular, color="#167e8d", s=35)
detail.scatter(*inertial, color="#788793", s=35)
detail.plot([circular[0], inertial[0]], [circular[1], inertial[1]],
color="#c26030", lw=1.5)
detail.text(1.015, value, f"omega t = {value:g}", fontsize=10,
va="center")
detail.scatter([1], [0], color="#24343e", s=45, zorder=4)
detail.annotate("Same starting position\nand initial velocity", (1, 0),
xytext=(0.76, -0.16),
arrowprops={"arrowstyle": "->", "color": "#405660"},
fontsize=10)
detail.text(0.67, 0.74, r"Initial inward departure: $\frac{1}{2}(\omega t)^2$",
fontsize=11, color="#a54e28")
detail.set_title("Equal-time positions over a short interval", fontsize=13)
detail.set(xlabel="x / a", ylabel="y / a", xlim=(0.63, 1.28),
ylim=(-0.24, 0.85))
detail.set_aspect("equal")
detail.spines[["top", "right"]].set_visible(False)
detail.grid(alpha=0.13)
detail.legend(loc="lower left", bbox_to_anchor=(-0.25, -0.25),
fontsize=10, frameon=False)
fig.suptitle("A circular orbit is continuous free fall", fontsize=18)
fig.get_layout_engine().set(rect=(0, 0.095, 1, 0.88))
fig.text(0.5, 0.018,
"No outward physical force balances gravity in this inertial frame; gravity supplies the curvature.",
ha="center", fontsize=10)
fig.savefig(Path(__file__).with_suffix(".png").name, dpi=100,
facecolor="white")
plt.close(fig)
if __name__ == "__main__":
main()