codex/cayley-left-translation-cycles.py

cayley-left-translation-cycles.py
"""Draw left multiplication in C4 and S3; write the 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 compose(p, q):
    """Apply q first, then p."""
    return tuple(p[q[i]] for i in range(len(p)))


def cycles(permutation):
    remaining = set(range(len(permutation)))
    result = []
    while remaining:
        first = min(remaining)
        cycle, current = [], first
        while current in remaining:
            cycle.append(current)
            remaining.remove(current)
            current = permutation[current]
        result.append(cycle)
    return result


def draw(ax, permutation, labels, title, notation, color):
    parts = cycles(permutation)
    positions = {}
    for j, part in enumerate(parts):
        center = (j - (len(parts) - 1) / 2) * 2.35
        if len(part) == 2:
            points = [(center, .65), (center, -.65)]
        else:
            angles = np.pi / 2 - np.arange(len(part)) * 2 * np.pi / len(part)
            points = [(center + .78 * np.cos(a), .78 * np.sin(a)) for a in angles]
        positions.update(zip(part, points))
    for start, end in enumerate(permutation):
        ax.add_patch(FancyArrowPatch(
            positions[start], positions[end], arrowstyle="-|>",
            connectionstyle="arc3,rad=.22", shrinkA=22, shrinkB=22,
            mutation_scale=15, lw=2, color=color, zorder=1))
    for i, position in positions.items():
        ax.scatter(*position, s=1300, facecolor="white", edgecolor=color, lw=2, zorder=2)
        ax.text(*position, labels[i], ha="center", va="center", fontsize=14, zorder=3)
    extent = max(1.4, (len(parts) - 1) * 2.35 / 2 + 1.2)
    ax.set(xlim=(-extent, extent), ylim=(-1.25, 1.3), aspect="equal")
    ax.set_title(title, fontsize=15, pad=8)
    ax.text(.5, -.10, notation, ha="center", va="top", transform=ax.transAxes, fontsize=13)
    ax.axis("off")


def main():
    e, r, s = (0, 1, 2), (1, 2, 0), (1, 0, 2)
    r2 = compose(r, r)
    elements = [e, r, r2, s, compose(r, s), compose(r2, s)]
    assert len(set(elements)) == 6
    translations = [tuple(elements.index(compose(g, x)) for x in elements) for g in elements]
    for i, g in enumerate(elements):
        for j, h in enumerate(elements):
            assert compose(translations[i], translations[j]) == translations[elements.index(compose(g, h))]
    assert translations[1] == (1, 2, 0, 4, 5, 3)
    assert translations[3] == (3, 5, 4, 0, 2, 1)
    assert compose(translations[1], translations[3])[0] == 4
    assert compose(translations[3], translations[1])[0] == 5

    plt.rcParams.update({"font.family": "DejaVu Sans", "font.size": 12})
    fig, axes = plt.subplots(2, 2, figsize=(11, 7.3), facecolor="white")
    fig.suptitle("Cayley's model: each group element permutes all the labels", fontsize=18, y=.98)
    c4labels = [str(i) for i in range(4)]
    draw(axes[0, 0], (1, 2, 3, 0), c4labels, r"$C_4$: add 1", r"$L_1=(0\ 1\ 2\ 3)$", "#2369a0")
    draw(axes[0, 1], (2, 3, 0, 1), c4labels, r"$C_4$: add 2", r"$L_2=(0\ 2)(1\ 3)$", "#2369a0")
    labels = [r"$e$", r"$r$", r"$r^2$", r"$s$", r"$rs$", r"$r^2s$"]
    draw(axes[1, 0], translations[1], labels, r"$S_3$: multiply on the left by $r=(123)$",
         r"$L_r=(e\ r\ r^2)(s\ rs\ r^2s)$", "#ad4b24")
    draw(axes[1, 1], translations[3], labels, r"$S_3$: multiply on the left by $s=(12)$",
         r"$L_s=(e\ s)(r\ r^2s)(r^2\ rs)$", "#ad4b24")
    fig.subplots_adjust(left=.04, right=.98, top=.89, bottom=.12, hspace=.55, wspace=.25)
    fig.text(.5, .025, r"An arrow means $x\mapsto gx$.  Products act rightmost first: $L_gL_h=L_{gh}$.",
             ha="center", fontsize=13)
    fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=100, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()