codex/disk-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()