codexcylinder-unrolled-geodesic.py
cylinder-unrolled-geodesic.py
"""Unroll a one-turn helical geodesic; write basename.png to the cwd.
Tested with Python 3.14, NumPy 2.3.5, and Matplotlib 3.10.7.
"""
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
def main():
plt.rcParams.update({"font.size": 12, "mathtext.fontset": "dejavusans"})
fig = plt.figure(figsize=(10.2, 5.3), facecolor="white")
ax = fig.add_axes([.015, .20, .42, .64], projection="3d", computed_zorder=False)
ax.set_facecolor("white")
ax.set_proj_type("ortho")
ax.set_box_aspect((2, 2, 2.3), zoom=1.15)
azimuth = -55
ax.view_init(elev=20, azim=azimuth)
ax.set(xlim=(-1.2, 1.2), ylim=(-1.2, 1.2), zlim=(-.15, 2.2))
ax.set_axis_off()
ax.set_title("On the cylinder (R = 1)", fontsize=15, pad=10)
phi, z = np.meshgrid(np.linspace(0, 2 * np.pi, 81), np.linspace(0, 2, 31))
ax.plot_wireframe(np.cos(phi), np.sin(phi), z, rstride=6, cstride=10,
color="#a7bec6", lw=.75, alpha=.65, zorder=1)
t = np.linspace(0, 1, 1001)
theta = 2 * np.pi * t
curve = np.array([np.cos(theta), np.sin(theta), 2 * t])
visible = np.cos(theta - np.deg2rad(azimuth)) >= 0
for mask, style, opacity in [(visible, "-", 1), (~visible, "--", .65)]:
segment = curve.copy()
segment[:, ~mask] = np.nan
ax.plot(*segment, color="#b54d21", lw=3, linestyle=style, alpha=opacity, zorder=4)
ax.plot(np.ones_like(t), np.zeros_like(t), 2 * t,
color="#19788a", lw=2.5, zorder=3)
for point, label in [(curve[:, 0], "A"), (curve[:, -1], "B")]:
ax.scatter(*point, color="#262d35", s=24, depthshade=False, zorder=5)
ax.text(point[0] + .10, point[1], point[2] + .08, label, fontsize=13, zorder=6)
flat = fig.add_axes([.52, .38, .445, .36])
flat.set_facecolor("white")
flat.set_title("On the unrolled sheet", fontsize=15, pad=14)
flat.set_aspect("equal")
flat.set(xlim=(-.2, 2 * np.pi + .2), ylim=(-.18, 2.18))
flat.set_xticks([0, np.pi, 2 * np.pi], ["0", r"$\pi$", r"$2\pi$"])
flat.set_yticks([0, 1, 2])
flat.set_xlabel(r"Circumferential arc length $u=\theta$", labelpad=10)
flat.set_ylabel("Height z")
flat.grid(color="#dce3e6", lw=.8)
flat.plot(2 * np.pi * t, 2 * t, color="#b54d21", lw=3)
flat.plot([0, 0], [0, 2], color="#19788a", lw=2.5)
flat.plot([2 * np.pi, 2 * np.pi], [0, 2], color="#19788a", lw=1.7, linestyle=":")
flat.scatter([0, 0, 2 * np.pi], [0, 2, 2], color="#262d35", s=24, zorder=5)
flat.annotate("A", (0, 0), xytext=(9, 7), textcoords="offset points")
flat.annotate(r"$B_0$", (0, 2), xytext=(9, -16), textcoords="offset points")
flat.annotate(r"$B_1$", (2 * np.pi, 2), xytext=(-27, -19), textcoords="offset points")
flat.text(np.pi, .27, r"$I=du^2+dz^2$", fontsize=14, ha="center",
bbox={"facecolor": "white", "edgecolor": "none", "pad": 3})
fig.suptitle("A helical geodesic is a straight line after unrolling", fontsize=17, y=.96)
fig.text(.73, .24, r"$B_0$ and $B_1$ are the same point on the cylinder.", ha="center", fontsize=11)
fig.text(.5, .14, r"One-turn helix: $L=\sqrt{4\pi^2+4}\approx6.59$", ha="center", color="#b54d21", fontsize=14)
fig.text(.5, .075, "Vertical generator: L = 2. The helix minimizes within its winding class, but is longer overall.",
ha="center", color="#19788a", fontsize=11)
fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name,
dpi=120, facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()