codexquadratic-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()