codexcylinder-geodesics-unrolled.py
cylinder-geodesics-unrolled.py
"""Compare winding classes using the Euclidean covering plane; PNG to cwd."""
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
def main():
plt.rcParams.update({"font.size": 12, "figure.facecolor": "white"})
fig = plt.figure(figsize=(12, 6), layout="constrained")
ax = fig.add_subplot(121, projection="3d")
flat = fig.add_subplot(122)
p = np.array([1., 0., 0.])
q = np.array([0., 1., 3.])
t = np.linspace(0, 1, 2001)
phi, height = np.meshgrid(np.linspace(0, 2 * np.pi, 65), np.linspace(0, 3, 33))
ax.plot_wireframe(np.cos(phi), np.sin(phi), height, rstride=6, cstride=8,
color="#aabfc7", lw=.7, alpha=.65)
azimuth = -60
colors = ["#087e8b", "#c85b28"]
for k, color in zip([0, -1], colors):
end_x = np.pi / 2 + 2 * np.pi * k
length = np.hypot(end_x, 3)
curve = np.array([np.cos(end_x * t), np.sin(end_x * t), 3 * t])
assert np.allclose(curve[:, 0], p)
assert np.allclose(curve[:, -1], q)
polygon_length = np.linalg.norm(np.diff(curve, axis=1), axis=0).sum()
assert np.isclose(polygon_length, length, rtol=1e-6)
visible = np.cos(end_x * t - np.deg2rad(azimuth)) >= 0
for mask, style, alpha in [(visible, "-", 1), (~visible, "--", .65)]:
part = curve.copy()
part[:, ~mask] = np.nan
ax.plot(*part, color=color, lw=3, linestyle=style, alpha=alpha)
flat.plot(end_x * t, 3 * t, color=color, lw=3,
label=f"k = {k}: length {length:.3f}")
flat.scatter(end_x, 3, color=color, s=40)
flat.annotate(rf"$q_{{{k}}}$", (end_x, 3), xytext=(5, 7),
textcoords="offset points", fontsize=14)
candidates = np.hypot(np.pi / 2 + 2 * np.pi * np.arange(-8, 9), 3)
assert np.argmin(candidates) == 8 # k = 0 minimizes among every nearby winding.
for point, label in [(p, "p"), (q, "q")]:
ax.scatter(*point, color="#25313b", s=35, depthshade=False)
ax.text(*(point + np.array([.08, .08, .10])), label, fontsize=14)
ax.view_init(elev=23, azim=azimuth)
ax.set_box_aspect((2, 2, 3))
ax.set(xlim=(-1.15, 1.15), ylim=(-1.15, 1.15), zlim=(-.1, 3.2))
ax.set_axis_off()
ax.set_title("Same endpoints; different winding", pad=15)
flat.scatter(0, 0, color="#25313b", s=40, zorder=5)
flat.annotate("p", (0, 0), xytext=(6, -18), textcoords="offset points", fontsize=14)
flat.plot([-3 * np.pi / 2, np.pi / 2], [3, 3], ":", color="#8796a3", lw=1.4)
flat.text(-np.pi / 2, 3.38, r"Equivalent lifts are $2\pi$ apart",
ha="center", fontsize=11)
flat.set(xlim=(-5.2, 2.2), ylim=(-.5, 3.9), xlabel=r"Unrolled coordinate $x=\theta$",
ylabel="Height z")
flat.set_aspect("equal")
flat.set_xticks([-3 * np.pi / 2, 0, np.pi / 2], [r"$-3\pi/2$", "0", r"$\pi/2$"])
flat.set_yticks([0, 1, 2, 3])
flat.grid(alpha=.18)
flat.spines[["top", "right"]].set_visible(False)
flat.set_title("Straight segments on the covering plane", pad=16)
flat.legend(loc="upper center", bbox_to_anchor=(.5, -.14), frameon=False, fontsize=11)
fig.suptitle("Cylinder distance: choose the shortest lifted segment", fontsize=17)
fig.savefig(Path(__file__).with_suffix(".png").name, dpi=100, facecolor="white")
plt.close(fig)
if __name__ == "__main__":
main()