codexdisk-circle-loop-obstruction.py
disk-circle-loop-obstruction.py
"""Draw disk contraction and circle winding; write the PNG basename to cwd.
Tested with Python 3.14.4, NumPy 2.3.5 and Matplotlib 3.10.7.
Run from _media: MPLCONFIGDIR=/tmp/codex-wiki-mpl python ../disk-circle-loop-obstruction.py
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
import numpy as np
def main():
plt.rcParams.update({"font.size": 11, "axes.titlesize": 13})
blue, orange, purple = "#16679c", "#b34e16", "#76529c"
fig = plt.figure(figsize=(9.6, 6.4), facecolor="white")
grid = fig.add_gridspec(2, 2, height_ratios=[1.05, 1], hspace=0.72)
disk, circle = fig.add_subplot(grid[0, 0]), fig.add_subplot(grid[0, 1])
winding = fig.add_subplot(grid[1, :])
fig.subplots_adjust(left=0.09, right=0.96, top=0.89, bottom=0.10)
fig.suptitle("A filling permits contraction; a circle retains winding", fontsize=16)
theta = np.linspace(0, 2 * np.pi, 801)
for ax in [disk, circle]:
ax.set_aspect("equal")
ax.set(xlim=(-1.2, 1.2), ylim=(-1.16, 1.2))
ax.axis("off")
disk.add_patch(Circle((0, 0), 1, facecolor="#eaf3f9", edgecolor=blue, lw=2))
for radius, color in [(1, blue), (0.5, orange), (0.25, purple)]:
disk.plot(radius * np.cos(theta), radius * np.sin(theta), color=color, lw=2)
start = np.array([0.6, 0.8])
disk.annotate("", xy=(0, 0), xytext=start,
arrowprops={"arrowstyle": "->", "color": "#333333"})
for t in [0, 0.5, 1]:
point = (1 - t) * start
disk.scatter(*point, s=28, color="#333333", zorder=5)
disk.text(0.04, -0.16, r"$t=1$", fontsize=10)
disk.text(0.38, 0.36, r"$t=1/2$", fontsize=10)
disk.text(0.66, 0.83, r"$t=0$", fontsize=10)
disk.set_title(r"Filled disk $D^2$: $H(x,t)=(1-t)x$")
disk.text(0.5, -0.11, r"All intermediate loops stay in $D^2$; $\pi_1(D^2)=0$",
transform=disk.transAxes, ha="center", fontsize=10)
circle.plot(np.cos(theta), np.sin(theta), color=blue, lw=3)
circle.plot(0.5 * np.cos(theta), 0.5 * np.sin(theta), color=orange, lw=2, ls="--")
circle.scatter([0], [0], s=30, marker="x", color=orange)
circle.text(0.5, -0.04, r"Candidate at $t=1/2$: radius $1/2$ is outside $S^1$",
transform=circle.transAxes, ha="center", color=orange, fontsize=10)
circle.set_title(r"Circle $S^1$: only the blue boundary belongs")
circle.text(0.5, -0.11, r"Winding prevents contraction in $S^1$; $\pi_1(S^1)=\mathbb{Z}$",
transform=circle.transAxes, ha="center", fontsize=10)
s = np.linspace(0, 1, 801)
for n, color in [(2, blue), (1, purple), (0, "#777777"), (-1, orange)]:
winding.plot(s, n * s, color=color, lw=2, label=rf"$\gamma_{{{n}}}$: {n} turns")
turns = np.where(s <= 0.5, 4 * s, 3 - 2 * s)
winding.plot(s, turns, color="#202020", lw=2, ls="--",
label=r"$\gamma_2 * \gamma_{-1}$: net 1 turn")
winding.scatter([1], [1], color="#202020", zorder=5)
winding.set(xlim=(0, 1.02), ylim=(-1.12, 2.18), xlabel=r"Loop parameter $s$",
ylabel=r"Unwrapped angle / $2\pi$",
title=r"$\gamma_n(s)=e^{2\pi i n s}$: nonzero windings share the same circular image")
winding.set_yticks([-1, 0, 1, 2])
winding.grid(alpha=0.2)
winding.legend(loc="upper left", fontsize=9, ncol=2, framealpha=1)
fig.savefig(Path(__file__).stem + ".png", dpi=120, facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()