codex/cyclic-group-endomorphism-maps.py

cyclic-group-endomorphism-maps.py
"""Draw two additive self-maps of C6; write an opaque 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
from matplotlib.patches import Circle, FancyArrowPatch
import numpy as np


def main():
    plt.rcParams.update({"font.size": 12, "figure.facecolor": "white"})
    fig, axes = plt.subplots(1, 2, figsize=(9.6, 5.4), dpi=100)
    blue, orange, gray = "#2166a5", "#b65b13", "#6b7280"
    rows = np.arange(5, -1, -1)
    for ax, multiplier in zip(axes, (2, 5), strict=True):
        outputs = multiplier * np.arange(6) % 6
        kernel = set(np.flatnonzero(outputs == 0).tolist())
        image = set(outputs.tolist())
        for k, output in enumerate(outputs):
            ax.add_patch(FancyArrowPatch(
                (0, rows[k]), (2.1, rows[output]), shrinkA=13, shrinkB=13,
                arrowstyle="-|>", mutation_scale=14, linewidth=1.7,
                color=orange if k in kernel else blue, alpha=0.9, zorder=1,
            ))
        for k in range(6):
            for x, color in ((0, orange if k in kernel else blue),
                             (2.1, blue if k in image else gray)):
                ax.add_patch(Circle((x, rows[k]), 0.16, edgecolor=color,
                                    facecolor="white", linewidth=2, zorder=2))
                ax.text(x, rows[k], str(k), ha="center", va="center",
                        color=color, weight="bold", zorder=3)
        ax.text(0, 5.55, "Input", ha="center", weight="bold")
        ax.text(2.1, 5.55, "Output", ha="center", weight="bold")
        ax.set_title(rf"$f_{multiplier}(k)={multiplier}k\;\mathrm{{mod}}\;6$",
                     fontsize=17, pad=12)
        summary = ("Outputs: 0, 2, 4, 0, 2, 4\nKernel: {0, 3}; image: {0, 2, 4}\n"
                   "Pairs collapse: no inverse" if multiplier == 2 else
                   "Outputs: 0, 5, 4, 3, 2, 1\nKernel: {0}; image: all six elements\n"
                   "A permutation; its own inverse")
        ax.text(1.05, -0.65, summary, ha="center", va="top", fontsize=11,
                linespacing=1.5)
        ax.set(xlim=(-0.45, 2.55), ylim=(-2, 5.9))
        ax.axis("off")
    fig.suptitle("Both maps preserve addition; only one is reversible",
                 fontsize=16, weight="bold", y=0.965)
    fig.subplots_adjust(left=0.04, right=0.96, bottom=0.05, top=0.80, wspace=0.25)
    fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()