codextidal-field-difference-and-stretching.py
tidal-field-difference-and-stretching.py
"""Draw point-mass gravity and its linear tidal residual.
Tested with Python 3.14, Matplotlib 3.10.7 and NumPy 2.3.5.
Run from _media; the matching opaque PNG is written to the current directory.
"""
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Circle
def main():
plt.rcParams.update({"font.size": 11, "figure.facecolor": "white"})
mu = 9.0
center = np.array([3.0, 0.0])
radius = 0.36
angles = np.arange(8) * np.pi / 4
offsets = radius * np.column_stack((np.cos(angles), np.sin(angles)))
points = center + offsets
gravity = -mu * points / np.linalg.norm(points, axis=1)[:, None] ** 3
tensor = mu / np.linalg.norm(center) ** 3 * np.diag([2.0, -1.0])
residual = offsets @ tensor.T
fig, axes = plt.subplots(1, 2, figsize=(10.8, 4.1), dpi=120)
labels = [
("Gravity at each position", "Exact field: all arrows point toward the source", gravity, 0.60, "#24699c"),
("After subtracting common acceleration", "Linear residual: radial stretch, transverse squeeze", residual, 2.60, "#c45c1b"),
]
for ax, (title, subtitle, vectors, scale, color) in zip(axes, labels):
ax.set_aspect("equal")
ax.set_xlim(-0.45, 4.15)
ax.set_ylim(-1.40, 1.45)
ax.set_axis_off()
ax.set_title(title, fontsize=12, pad=8, fontweight="bold")
ax.plot([-0.05, 3.95], [0, 0], color="#d4d7db", linewidth=1, zorder=0)
ax.add_patch(Circle((0, 0), 0.12, color="#30343b"))
ax.text(0, -0.32, "Source M", ha="center", fontsize=11)
ax.add_patch(Circle(center, radius, fill=False, linestyle="--", color="#b2b7bc", linewidth=1))
ax.scatter(*center, marker="+", s=80, linewidth=1.5, color="#30343b", zorder=4)
ax.scatter(points[:, 0], points[:, 1], s=22, color="#30343b", zorder=4)
for point, vector in zip(points, vectors):
ax.annotate("", xy=point + scale * vector, xytext=point,
arrowprops={"arrowstyle": "-|>", "color": color, "lw": 1.8, "mutation_scale": 13}, zorder=3)
ax.text(3, -0.94, "Reference point R", ha="center", fontsize=11)
ax.annotate("", xy=(2.98, -0.65), xytext=(0.10, -0.65),
arrowprops={"arrowstyle": "<->", "lw": 1, "color": "#767d85"})
ax.text(1.5, -0.57, "R", ha="center", color="#555c64")
ax.text(1.8, -1.30, subtitle, ha="center", fontsize=9.5, color="#555c64")
axes[0].text(1.5, 1.1, r"$\mathbf{g}(\mathbf{R}+\mathbf{x})$", ha="center", color="#24699c", fontsize=13)
axes[1].text(1.5, 1.1, r"$\mathbf{g}(\mathbf{R}+\mathbf{x})-\mathbf{g}(\mathbf{R})\simeq T\mathbf{x}$", ha="center", color="#c45c1b", fontsize=12)
fig.suptitle("Point-mass tides reveal differences in the pull", fontsize=15, y=0.99)
fig.text(0.5, 0.025, "Arrows use separate length scales in the two panels. Dots mark positions, not a time sequence.",
ha="center", fontsize=10, color="#555c64")
fig.subplots_adjust(left=0.035, right=0.985, top=0.84, bottom=0.12, wspace=0.18)
fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()