codex/euler-unit-powers-and-exponent-periods.py

euler-unit-powers-and-exponent-periods.py
"""Generate original modular-power and exponent-period diagrams.

Run from _media with Python 3.14 and matplotlib 3.10.7. Writes its own
basename as an opaque white-background PNG in the current directory.
"""

from pathlib import Path

import matplotlib.pyplot as plt
from matplotlib.patches import FancyBboxPatch


def main():
    fig, ax = plt.subplots(figsize=(13.5, 7.2), dpi=80)
    fig.patch.set_facecolor("white")
    ax.set(xlim=(-2.0, 10.0), ylim=(-2.6, 5.5))
    ax.axis("off")
    ax.text(4, 5.22, "A guaranteed return can exceed the shortest period",
            ha="center", fontsize=18)
    ax.text(-1.85, 4.48, "Exponent k", fontsize=13)
    for k in range(9):
        ax.text(k + 0.4, 4.48, str(k), ha="center", fontsize=14)
    rows = [(5, 12, 2, 2, 4), (7, 15, 4, 4, 8), (4, 15, 2, 4, 8), (3, 15, None, 4, 8)]
    colors = ["#e8f2f7", "#ede6f4", "#eaf3e7", "#fcf0de"]
    for row, (a, n, order, carmichael, phi) in enumerate(rows):
        y = 3.63 - 1.32 * row
        ax.text(-1.85, y + 0.15, rf"${a}^k$ mod {n}", va="center", fontsize=16)
        for k in range(9):
            value = pow(a, k, n)
            first = order is not None and k == order
            ax.add_patch(FancyBboxPatch((k + 0.08, y - 0.16), 0.64, 0.64,
                                        boxstyle="round,pad=0.04,rounding_size=0.08",
                                        facecolor=colors[row], edgecolor="#323d48" if first else "#cbd0d5",
                                        lw=2.8 if first else 1))
            ax.text(k + 0.4, y + 0.16, str(value), ha="center", va="center", fontsize=16)
        if order:
            label = rf"first return: $d={order}$     |     common return: $\lambda({n})={carmichael}$     |     Euler return: $\varphi({n})={phi}$"
            assert pow(a, order, n) == 1
            assert all(pow(a, j, n) != 1 for j in range(1, order))
        else:
            label = "nonunit: no positive-exponent return to 1; exponent reduction by 8 fails"
        ax.text(0.05, y - 0.55, label, fontsize=12, color="#323d48")
    ax.text(4.0, -1.98, r"$2029=253\cdot8+5\quad\Rightarrow\quad 7^{2029}\equiv7^5\equiv7\; (\mathrm{mod}\;15)$",
            ha="center", fontsize=16)
    ax.text(4.0, -2.50, r"$2029=507\cdot4+1\quad\Rightarrow\quad 7^{2029}\equiv7^1\equiv7\; (\mathrm{mod}\;15)$",
            ha="center", fontsize=16, color="#744391")
    fig.subplots_adjust(left=0.02, right=0.99, bottom=0.055, top=0.99)
    fig.savefig(Path(__file__).stem + ".png", facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()