codexpast-exam-of-the-topology-course-of-the-university-of-oxford2021-polygon-boundaries.py
past-exam-of-the-topology-course-of-the-university-of-oxford/2021-polygon-boundaries.py
"""Generate the 2021 topology polygon explanation in the current directory.
Tested with Python 3.14, NumPy 2.3.5 and Matplotlib 3.10.7.
Run from _media/past-exam-of-the-topology-course-of-the-university-of-oxford.
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
import numpy as np
def arrow(ax, start, end, label, offset):
start, end = np.asarray(start), np.asarray(end)
mid = (start + end) / 2
direction = (end - start) / np.linalg.norm(end - start)
ax.annotate(
"", xy=mid + 0.16 * direction, xytext=mid - 0.16 * direction,
arrowprops={"arrowstyle": "->", "color": "#26364b", "lw": 1.7},
)
ax.text(*(mid + np.asarray(offset)), label, ha="center", va="center", fontsize=13)
def draw_panel(ax, points, classes, paired, free, title, count):
points = np.asarray(points, dtype=float)
ax.add_patch(Polygon(points, closed=True, facecolor="#f0f3f7", edgecolor="#26364b", lw=1.8))
for start, end, name, offset in paired:
arrow(ax, points[start], points[end], name, offset)
for start, end, name in free:
p, q = points[start], points[end]
ax.plot([p[0], q[0]], [p[1], q[1]], color="#c26a10", lw=4, solid_capstyle="round")
mid = (p + q) / 2
ax.text(mid[0] - 0.18, mid[1] - 0.18, name, color="#8b4908", ha="center", va="center", fontsize=12)
centroid = points.mean(axis=0)
for index, (point, name) in enumerate(zip(points, classes)):
color = "#165eaa" if name == "P" else "#973b8b"
ax.scatter(*point, c=color, s=38, zorder=4)
outward = point - centroid
outward /= np.linalg.norm(outward)
ax.text(*(point + 0.23 * outward), f"$v_{index}$: {name}", fontsize=10, color=color, ha="center", va="center")
ax.set_title(title, fontsize=14, pad=12)
ax.text(1, -0.68, count, ha="center", va="top", fontsize=11, linespacing=1.6)
ax.set(xlim=(-0.6, 2.6), ylim=(-1.25, 2.55), aspect="equal")
ax.axis("off")
def main():
fig, axes = plt.subplots(1, 2, figsize=(9.4, 5.0), facecolor="white")
draw_panel(
axes[0], [(0, 2), (2, 2), (2, 0), (1, 0), (0, 1)], ["P"] * 5,
[(4, 0, "$a$", (-0.18, 0)), (2, 1, "$a$", (0.18, 0)),
(0, 1, "$b$", (0, 0.2)), (3, 2, "$b$", (0, -0.2))],
[(3, 4, "$e$")], "3(b)(ii): pentagon",
r"$V=1,\ E=3,\ F=1,\ \chi=-1$" "\nBoundary: one loop $e$ at $P$",
)
draw_panel(
axes[1], [(0, 2), (1, 2), (2, 1), (2, 0), (1, 0), (0, 1)], ["P", "Q", "P", "Q", "P", "Q"],
[(5, 0, "$a$", (-0.18, 0)), (3, 2, "$a$", (0.18, 0)),
(0, 1, "$b$", (0, 0.2)), (4, 3, "$b$", (0, -0.2))],
[(1, 2, "$e$"), (4, 5, "$f$")], "3(b)(iii): hexagon",
r"$V=2,\ E=4,\ F=1,\ \chi=-1$" "\nBoundary: $P$ to $Q$ along $e$, back along $f$",
)
fig.suptitle("The orange free edges form one boundary circle in each quotient", fontsize=14, y=0.98)
fig.tight_layout(rect=(0, 0, 1, 0.93))
fig.savefig(Path("2021-polygon-boundaries.png"), dpi=150, facecolor="white")
plt.close(fig)
if __name__ == "__main__":
main()