codex/cayley-faithful-and-nonfaithful-actions.py

cayley-faithful-and-nonfaithful-actions.py
"""Compare faithful and nonfaithful C4 actions; write matching PNG to CWD.

Tested with Python 3.14, matplotlib 3.10.7, and numpy 2.3.5.
"""

from pathlib import Path

import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch
import numpy as np


def action(k, degree):
    return tuple((k + x) % degree for x in range(degree))


def main():
    regular = [action(k, 4) for k in range(4)]
    reduced = [action(k, 2) for k in range(4)]
    for degree in [4, 2]:
        for g in range(4):
            for h in range(4):
                assert tuple(action(g, degree)[x] for x in action(h, degree)) == action((g + h) % 4, degree)
    assert len(set(regular)) == 4
    assert len(set(reduced)) == 2
    assert [k for k in range(4) if action(k, 2) == (0, 1)] == [0, 2]

    plt.rcParams.update({"font.family": "DejaVu Sans", "font.size": 13})
    fig = plt.figure(figsize=(10, 6.8), facecolor="white")
    fig.suptitle("The same group, two actions: which elements remain distinguishable?", fontsize=17, y=.97)
    for j, (degree, title, color) in enumerate([
        (4, "Faithful: four labels", "#2369a0"),
        (2, "Nonfaithful: two labels", "#ad4b24"),
    ]):
        left = .05 + j * .5
        ax = fig.add_axes([left, .52, .4, .31])
        angles = np.pi / 2 - np.arange(degree) * 2 * np.pi / degree
        points = np.column_stack((.72 * np.cos(angles), .72 * np.sin(angles)))
        for i, p in enumerate(points):
            ax.add_patch(FancyArrowPatch(p, points[(i + 1) % degree], arrowstyle="-|>",
                                        connectionstyle="arc3,rad=.24", shrinkA=17, shrinkB=17,
                                        mutation_scale=16, color=color, lw=2))
            ax.scatter(*p, s=650, facecolor="white", edgecolor=color, lw=2, zorder=2)
            ax.text(*p, str(i), ha="center", va="center", fontsize=15, zorder=3)
        ax.set(xlim=(-1.2, 1.2), ylim=(-1, 1), aspect="equal")
        ax.set_title(title, fontsize=16, color=color, pad=8)
        ax.axis("off")
        fig.text(left + .2, .48, rf"Generator $1$: $x\mapsto 1+x$ (mod {degree})", ha="center", fontsize=13)
        tax = fig.add_axes([left, .17, .4, .26])
        tax.axis("off")
        notation = ["id", "(0 1 2 3)", "(0 2)(1 3)", "(0 3 2 1)"] if degree == 4 else ["id", "(0 1)", "id", "(0 1)"]
        rows = [[str(k), notation[k]] for k in range(4)]
        table = tax.table(cellText=rows, colLabels=[r"$k\in C_4$", "Permutation on the labels"],
                          cellLoc="center", bbox=[0, 0, 1, 1], colWidths=[.22, .78])
        table.auto_set_font_size(False)
        table.set_fontsize(12)
        for (row, col), cell in table.get_celld().items():
            cell.set_edgecolor("#c5ccd2")
            if row == 0:
                cell.set_facecolor("#edf2f6")
            elif degree == 2:
                cell.set_facecolor("#fff0df" if row in [1, 3] else "#f0e9f5")
        summary = r"Kernel $=\{0\}$; image has 4 elements" if degree == 4 else r"Kernel $=\{0,2\}$; image has 2 elements"
        fig.text(left + .2, .115, summary, ha="center", color=color, fontsize=13)
    fig.text(.5, .04, "Both actions reach every point. Only the left action distinguishes every group element.",
             ha="center", fontsize=12)
    fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=100, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()