codexsurface-intrinsic-curvature.py
surface-intrinsic-curvature.py
"""Compare plane, cylinder, and sphere; 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.8, 4.8), facecolor="white")
titles = ["Plane", "Circular cylinder", "Sphere"]
colors = ["#d9e9ed", "#d9e9ed", "#f0dfc3"]
grid_colors = ["#347c91", "#347c91", "#ac742e"]
axes = []
for i, title in enumerate(titles):
ax = fig.add_axes([.015 + .33 * i, .30, .31, .53], projection="3d", computed_zorder=False)
ax.set_proj_type("ortho")
ax.set_facecolor("white")
ax.set_box_aspect((1, 1, 1), zoom=1.35)
ax.view_init(elev=24, azim=-55)
ax.set(xlim=(-1.25, 1.25), ylim=(-1.25, 1.25), zlim=(-1.25, 1.25))
ax.set_axis_off()
ax.set_title(title, fontsize=15, pad=0)
axes.append(ax)
# Same equally spaced arc-length coordinates on the plane and cylinder.
u, v = np.meshgrid(np.linspace(-1.1, 1.1, 81), np.linspace(-.9, .9, 61))
plane = (u, v, np.zeros_like(u))
cylinder = (np.sin(u), -np.cos(u), v)
phi, theta = np.meshgrid(np.linspace(0, 2 * np.pi, 97), np.linspace(0, np.pi, 65))
sphere = (np.sin(theta) * np.cos(phi), np.sin(theta) * np.sin(phi), np.cos(theta))
for ax, coords, color in zip(axes, [plane, cylinder, sphere], colors):
ax.plot_surface(*coords, color=color, linewidth=0, alpha=1,
shade=False, antialiased=False, zorder=1)
t = np.linspace(-1.1, 1.1, 151)
s = np.linspace(-.9, .9, 151)
for fixed_u in np.linspace(-1.1, 1.1, 9):
axes[0].plot(np.full_like(s, fixed_u), s, np.full_like(s, .006),
color=grid_colors[0], lw=1, zorder=3)
# Tiny outward displacement keeps grid curves visible on the surface.
axes[1].plot(np.full_like(s, 1.006 * np.sin(fixed_u)),
np.full_like(s, -1.006 * np.cos(fixed_u)), s,
color=grid_colors[1], lw=1, zorder=3)
for fixed_v in np.linspace(-.9, .9, 7):
axes[0].plot(t, np.full_like(t, fixed_v), np.full_like(t, .006),
color=grid_colors[0], lw=1, zorder=3)
axes[1].plot(1.006 * np.sin(t), -1.006 * np.cos(t), np.full_like(t, fixed_v),
color=grid_colors[1], lw=1, zorder=3)
# Only draw the front half of each spherical grid curve to avoid false
# impressions of transparency; hidden curves are occluded by the sphere.
camera = np.array([np.cos(np.deg2rad(24)) * np.cos(np.deg2rad(-55)),
np.cos(np.deg2rad(24)) * np.sin(np.deg2rad(-55)),
np.sin(np.deg2rad(24))])
def front_grid(x, y, z):
coords = 1.006 * np.array([x, y, z])
coords[:, camera @ coords < 0] = np.nan
axes[2].plot(*coords, color=grid_colors[2], lw=1, zorder=3)
ph = np.linspace(0, 2 * np.pi, 361)
th = np.linspace(0, np.pi, 181)
for latitude in np.linspace(np.pi / 6, 5 * np.pi / 6, 5):
front_grid(np.sin(latitude) * np.cos(ph), np.sin(latitude) * np.sin(ph),
np.full_like(ph, np.cos(latitude)))
for longitude in np.linspace(0, 2 * np.pi, 12, endpoint=False):
front_grid(np.sin(th) * np.cos(longitude), np.sin(th) * np.sin(longitude), np.cos(th))
metrics = [r"$I=du^2+dv^2$", r"$I=du^2+dv^2$", r"$I=R^2(d\theta^2+\sin^2\theta\,d\phi^2)$"]
curvatures = [r"$K=0$", r"$K=0$", r"$K=1/R^2$"]
notes = ["No bending", "Bends in one direction", "Bends in both principal directions"]
for i in range(3):
x = .17 + .33 * i
fig.text(x, .255, metrics[i], ha="center", fontsize=13)
fig.text(x, .185, curvatures[i], ha="center", fontsize=16, weight="bold")
fig.text(x, .125, notes[i], ha="center", fontsize=11)
fig.suptitle("Visible bending does not determine intrinsic curvature", fontsize=17, y=.97)
fig.text(.5, .035, "Plane and cylinder grids preserve local lengths and angles. A spherical patch cannot flatten isometrically.",
ha="center", 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()