codexmodule-lattice-and-torsion.py
module-lattice-and-torsion.py
"""Draw integer-module examples; tested with Python 3.14 and Matplotlib 3.10.
Uses the repository's existing NumPy and Matplotlib dependencies. Writes the
opaque, white-background basename PNG in the current working directory.
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch
import numpy as np
def main():
plt.rcParams.update({"font.size": 12, "font.family": "DejaVu Sans"})
blue, orange, gray = "#176b9d", "#c45c19", "#697780"
fig, (grid, cycle) = plt.subplots(1, 2, figsize=(10.4, 5.2), dpi=110)
fig.patch.set_facecolor("white")
fig.subplots_adjust(left=0.06, right=0.96, top=0.81, bottom=0.23, wspace=0.3)
fig.suptitle("Two modules with integer scalars", fontsize=18, y=0.98)
grid.set_title(r"Free: $\mathbb{Z}^2$", fontsize=16, pad=18)
points = np.array([(a, b) for a in range(-1, 5) for b in range(-1, 4)])
grid.scatter(*points.T, s=20, color="#a8b1b6", zorder=3)
grid.set(xlim=(-1.4, 4.4), ylim=(-1.35, 3.35))
grid.set_xticks(range(-1, 5))
grid.set_yticks(range(-1, 4))
grid.grid(color="#e4e8eb", linewidth=0.8)
grid.axhline(0, color=gray, linewidth=1)
grid.axvline(0, color=gray, linewidth=1)
grid.set_aspect("equal")
for end in [(1, 0), (0, 1)]:
grid.annotate("", xy=end, xytext=(0, 0),
arrowprops={"arrowstyle": "-|>", "color": blue, "lw": 2.4},
zorder=5)
grid.text(1.03, -0.48, r"$e_1=(1,0)$", color=blue, fontsize=11)
grid.text(-1.28, 1.28, r"$e_2=(0,1)$", color=blue, fontsize=11)
grid.plot([0, 3, 3], [0, 0, 2], color=orange, linestyle="--", linewidth=2)
grid.scatter([3], [2], s=70, color=orange, zorder=5)
grid.text(2.0, 2.5, r"$(3,2)=3e_1+2e_2$", color=orange)
grid.text(1.6, 0.3, r"$3e_1$", color=orange)
grid.text(3.15, 0.9, r"$2e_2$", color=orange)
grid.set_xlabel("First integer coordinate")
grid.set_ylabel("Second integer coordinate")
grid.spines[["top", "right"]].set_visible(False)
cycle.set_title(r"Cyclic and torsion: $\mathbb{Z}/6\mathbb{Z}$", fontsize=16, pad=18)
angles = np.pi / 2 - np.arange(6) * np.pi / 3
positions = np.column_stack((np.cos(angles), np.sin(angles)))
for j, start in enumerate(positions):
end = positions[(j + 1) % 6]
cycle.add_patch(FancyArrowPatch(start, end, arrowstyle="-|>",
connectionstyle="arc3,rad=-0.22", shrinkA=16,
shrinkB=16, mutation_scale=15, color=gray, lw=1.7))
cycle.scatter(*positions.T, s=350,
c=[blue if j % 2 == 0 else orange for j in range(6)], zorder=4)
for j, (x, y) in enumerate(positions):
cycle.text(x, y, rf"$\overline{{{j}}}$", ha="center", va="center",
color="white", fontsize=13, zorder=5)
cycle.text(0, 0.18, r"Each arrow: $+\overline{1}$", ha="center", fontsize=12)
cycle.text(0, -0.17, r"$6\overline{1}=\overline{0}$", ha="center", fontsize=16)
cycle.set(xlim=(-1.5, 1.5), ylim=(-1.45, 1.45))
cycle.set_aspect("equal")
cycle.axis("off")
fig.text(0.275, 0.095, "Unique integer coordinates; no nonzero torsion",
ha="center", fontsize=11, color=blue)
fig.text(0.765, 0.095, r"Blue points: $\mathbb{Z}\overline{2}=\{\overline{0},\overline{2},\overline{4}\}$",
ha="center", fontsize=12, color=blue)
fig.text(0.765, 0.04, r"$3\overline{2}=\overline{0}$; every element is torsion",
ha="center", fontsize=11, color=gray)
fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), facecolor="white",
transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()