codex/modular-arithmetic-clock-and-classes.py

modular-arithmetic-clock-and-classes.py
"""Render clock addition and an integer congruence class into the current directory."""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def clock_point(residue, radius=1.0):
    angle = np.pi / 2 - 2 * np.pi * np.asarray(residue) / 12
    return radius * np.cos(angle), radius * np.sin(angle)


def main():
    assert (10 + 7) % 12 == 5
    representatives = np.array([-8, -3, 2, 7, 12])
    assert np.all(representatives % 5 == 2)
    plt.rcParams.update({"font.size": 12, "axes.titlesize": 14})
    fig, axes = plt.subplots(1, 2, figsize=(11.0, 5.0), dpi=120)
    fig.patch.set_facecolor("white")
    ax = axes[0]
    theta = np.linspace(0, 2 * np.pi, 400)
    ax.plot(np.cos(theta), np.sin(theta), color="#a9b7c4", lw=2)
    for residue in range(12):
        x, y = clock_point(residue)
        ax.plot(x, y, "o", color="#607080", ms=5)
        tx, ty = clock_point(residue, 1.18)
        ax.text(tx, ty, str(residue), ha="center", va="center")
    path = np.linspace(10, 17, 300)
    px, py = clock_point(path, 0.86)
    ax.plot(px, py, color="#1a77b9", lw=3)
    for residue in range(11, 18):
        start = clock_point(residue - 0.19, 0.86)
        end = clock_point(residue, 0.86)
        ax.annotate("", xy=end, xytext=start,
                    arrowprops={"arrowstyle": "->", "color": "#1a77b9", "lw": 2})
    for residue, color in [(10, "#d57816"), (5, "#168556")]:
        x, y = clock_point(residue)
        ax.scatter(x, y, s=140, color=color, edgecolor="white", zorder=5)
    ax.text(0, 0.14, r"$10+7\equiv5\;(\mathrm{mod}\;12)$", ha="center", fontsize=15)
    ax.text(0, -0.14, "Seven clockwise steps", ha="center", fontsize=11)
    ax.text(0, -1.46, "0 is the clock position usually labelled 12", ha="center", fontsize=10)
    ax.set_title("A position survives; complete turns do not", pad=15)
    ax.set_aspect("equal")
    ax.set_xlim(-1.45, 1.45)
    ax.set_ylim(-1.62, 1.42)
    ax.axis("off")

    ax = axes[1]
    integers = np.arange(-9, 14)
    ax.axhline(0, color="#607080", lw=1.2)
    ax.scatter(integers, np.zeros_like(integers), color="#c4ccd3", s=24, zorder=3)
    ax.scatter(representatives, np.zeros_like(representatives), color="#168556", s=100, zorder=4)
    for residue in representatives:
        ax.text(residue, -0.22, str(residue), ha="center", color="#12683f", fontweight="bold")
    for start, stop in zip(representatives[:-1], representatives[1:]):
        ax.annotate("", xy=(stop, 0.33), xytext=(start, 0.33),
                    arrowprops={"arrowstyle": "<->", "color": "#1a77b9", "lw": 1.6})
        ax.text((start + stop) / 2, 0.45, "+5", ha="center", color="#1a77b9")
    ax.text(2, 1.08, r"$[2]_5=\{2+5k:k\in\mathbb{Z}\}$", ha="center", fontsize=16)
    ax.text(2, -0.78, "Different integers, one congruence class", ha="center")
    ax.text(2, -1.12, "The least nonnegative representative is 2", ha="center", fontsize=11)
    ax.set_title("Modulo five identifies positions five apart", pad=15)
    ax.set_xlim(-9.5, 13.5)
    ax.set_ylim(-1.5, 1.55)
    ax.axis("off")
    fig.subplots_adjust(left=0.04, right=0.98, top=0.88, bottom=0.08, wspace=0.25)
    output = Path.cwd() / Path(__file__).with_suffix(".png").name
    fig.savefig(output, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()