codex/quadratic-residue-square-map-fibers.py

quadratic-residue-square-map-fibers.py
"""Draw the fibers of squaring modulo two odd primes; Python 3.14/matplotlib 3.10."""

from pathlib import Path

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


def chip(ax, x, y, label, color, width=0.105):
    ax.add_patch(FancyBboxPatch(
        (x - width / 2, y - 0.031), width, 0.062,
        boxstyle="round,pad=0.008,rounding_size=0.012",
        facecolor=color, edgecolor="#384957", linewidth=1.1,
    ))
    ax.text(x, y, str(label), ha="center", va="center", fontsize=13)


def draw_panel(ax, p):
    ax.set(xlim=(0, 1), ylim=(0, 1))
    ax.axis("off")
    ax.text(0.5, 0.95, f"Modulo {p}", ha="center", fontsize=18, weight="bold")
    ax.text(0.21, 0.865, "Inputs: x and −x", ha="center", fontsize=12)
    ax.text(0.77, 0.865, "Output: x² mod p", ha="center", fontsize=12)
    colors = ["#dcebf7", "#e6efcd", "#fbe5cb", "#eadff5", "#d9efeb"]
    roots = range(1, (p + 1) // 2)
    spacing = 0.114 if p == 11 else 0.185
    for i, x in enumerate(roots):
        y = 0.775 - i * spacing
        value = x * x % p
        color = colors[i]
        chip(ax, 0.13, y + 0.027, x, color)
        chip(ax, 0.30, y - 0.027, p - x, color)
        chip(ax, 0.77, y, value, color)
        for start in [(0.195, y + 0.027), (0.365, y - 0.027)]:
            ax.add_patch(FancyArrowPatch(
                start, (0.705, y), arrowstyle="-|>",
                mutation_scale=13, linewidth=1.3, color="#435565",
            ))
    ax.plot([0.04, 0.96], [0.24, 0.24], color="#b9c2ca", lw=1)
    squares = sorted({x * x % p for x in range(1, p)})
    nonsquares = sorted(set(range(1, p)) - set(squares))
    ax.text(0.5, 0.195, f"Nonzero squares: {', '.join(map(str, squares))}",
            ha="center", fontsize=11.5, color="#23434b")
    ax.text(0.5, 0.135, f"Empty fibers: {', '.join(map(str, nonsquares))}",
            ha="center", fontsize=11.5)
    ax.text(0.5, 0.065, "Zero separately: 0 → 0 (one input)",
            ha="center", fontsize=11.5)


def main():
    fig, axes = plt.subplots(1, 2, figsize=(9.8, 6.2), dpi=100, facecolor="white")
    fig.suptitle("Squaring forgets exactly one sign", fontsize=21, weight="bold", y=0.975)
    for ax, prime in zip(axes, (7, 11), strict=True):
        draw_panel(ax, prime)
    fig.text(0.5, 0.027, "Each nonzero square has two roots; each nonsquare has none.",
             ha="center", fontsize=13)
    fig.subplots_adjust(left=0.025, right=0.975, top=0.88, bottom=0.09, wspace=0.12)
    fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()