codex/fermat-prime-residue-short-cycles.py

fermat-prime-residue-short-cycles.py
"""Draw the two cycles of multiplication by two modulo seven.

Run from _media; 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 Circle, FancyArrowPatch
import numpy as np


def main():
    p, a = 7, 2
    remaining = set(range(1, p))
    cycles = []
    while remaining:
        start = min(remaining)
        cycle, x = [], start
        while x in remaining:
            cycle.append(x)
            remaining.remove(x)
            x = a * x % p
        assert x == start
        cycles.append(cycle)
    assert cycles == [[1, 2, 4], [3, 6, 5]]

    plt.rcParams.update({"font.size": 14, "font.family": "DejaVu Sans"})
    fig, ax = plt.subplots(figsize=(8.6, 5.2), dpi=100, facecolor="white")
    ax.set(xlim=(0, 8.6), ylim=(0, 5.2), aspect="equal")
    ax.axis("off")
    ax.text(4.3, 4.93, "Multiplication by 2 modulo 7: two shorter cycles",
            ha="center", va="center", fontsize=16, weight="bold")
    ax.text(4.3, 4.47, "Every arrow means multiply by 2, then reduce modulo 7.",
            ha="center", fontsize=13)
    for j, cycle in enumerate(cycles):
        center = np.array([2.12 + 4.35 * j, 2.70])
        angles = np.pi / 2 - np.arange(3) * 2 * np.pi / 3
        points = [center + 1.05 * np.array([np.cos(t), np.sin(t)]) for t in angles]
        color = ["#236a9f", "#35814c"][j]
        for k, (value, pos) in enumerate(zip(cycle, points)):
            nxt = points[(k + 1) % 3]
            ax.add_patch(FancyArrowPatch(pos, nxt, arrowstyle="-|>", mutation_scale=19,
                                        shrinkA=21, shrinkB=21, lw=2, color=color,
                                        connectionstyle="arc3,rad=0.12"))
            ax.add_patch(Circle(pos, 0.29, fc="white", ec=color, lw=2))
            ax.text(*pos, str(value), ha="center", va="center", fontsize=19, color=color)
        ax.text(center[0], 1.27, ["Powers of 2: 1, 2, 4", "Other residues: 3, 6, 5"][j],
                ha="center", fontsize=13, color=color)
    ax.text(4.3, 0.76, "$2^3 \\equiv 1 \\;(\\mathrm{mod}\\;7)$: shortest return is 3 steps", ha="center")
    ax.text(4.3, 0.29, "$2^6 \\equiv (2^3)^2 \\equiv 1 \\;(\\mathrm{mod}\\;7)$: Fermat's 6-step return still holds",
            ha="center", fontsize=13)
    fig.subplots_adjust(left=0.02, right=0.98, bottom=0.02, top=0.98)
    fig.savefig(Path(__file__).with_suffix(".png").name, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()