codex/linear-congruence-solution-classes.py

linear-congruence-solution-classes.py
"""Illustrate all solution classes of two linear congruences modulo fifteen."""

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np


def main():
    x = np.arange(15)
    residues = 6 * x % 15
    solutions = x[residues == 9]
    assert solutions.tolist() == [4, 9, 14]
    assert not np.any(residues == 10)
    assert sorted(set(residues)) == [0, 3, 6, 9, 12]
    assert np.all((2 * solutions) % 5 == 3)
    plt.rcParams.update({"font.size": 12, "axes.titlesize": 16})
    fig, ax = plt.subplots(figsize=(10, 4.7), dpi=120)
    fig.patch.set_facecolor("white")
    ax.vlines(x, 0, residues, color="#bbcad7", lw=2)
    ax.scatter(x, residues, s=70, color="#246b9e", zorder=4, label=r"$6x\;\mathrm{mod}\;15$")
    ax.scatter(solutions, np.full(3, 9), s=150, color="#18844f", edgecolor="white",
               linewidth=1.2, zorder=5, label="Solutions: 4, 9, 14")
    ax.axhline(9, color="#18844f", lw=1.8, linestyle="--")
    ax.axhline(10, color="#d57816", lw=1.8, linestyle=":", label="Residue 10 is never reached")
    for solution in solutions:
        ax.annotate(str(solution), (solution, 9), xytext=(0, -27), textcoords="offset points",
                    ha="center", color="#12683f", fontweight="bold")
    ax.set_xticks(x)
    ax.set_yticks([0, 3, 6, 9, 10, 12])
    ax.set_xlim(-0.6, 14.6)
    ax.set_ylim(-0.8, 13.5)
    ax.set_xlabel(r"Representative $x$ of a class modulo 15")
    ax.set_ylabel("Least nonnegative residue")
    ax.set_title("One congruence has three classes; the other has none", pad=16)
    ax.grid(axis="y", color="#dfe5e8", linewidth=0.8)
    ax.spines[["top", "right"]].set_visible(False)
    ax.legend(loc="upper center", bbox_to_anchor=(0.5, -0.18), ncol=3, frameon=False, fontsize=10)
    fig.text(0.5, 0.92, r"$6x\equiv9\;(\mathrm{mod}\;15)\ \Longleftrightarrow\ x\equiv4\;(\mathrm{mod}\;5)$",
             ha="center", fontsize=14)
    fig.subplots_adjust(left=0.09, right=0.98, top=0.79, bottom=0.23)
    output = Path.cwd() / Path(__file__).with_suffix(".png").name
    fig.savefig(output, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()