codex/dirichlet-convolution-factor-pairs.py

dirichlet-convolution-factor-pairs.py
"""Ordered factor pairs and their convolution weights; write basename PNG to cwd.

Tested with Python 3.14.4, NumPy 2.3.5, and Matplotlib 3.10.7.
Reproduce: make _media/dirichlet-convolution-factor-pairs.png
"""

from pathlib import Path

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np


def main():
    n = 12
    divisors = np.array([d for d in range(1, n + 1) if n % d == 0])
    complements = n // divisors
    tau_weights = np.ones_like(divisors)
    sigma_weights = divisors.copy()
    assert np.all(divisors * complements == n)
    assert tau_weights.sum() == 6 and sigma_weights.sum() == 28
    assert sum(1 for a in range(1, 13) for b in range(1, 13)
               for c in range(1, 13) if a * b * c == n) == 18

    fig, (ax, tab_ax) = plt.subplots(1, 2, figsize=(11, 5), dpi=120,
                                   gridspec_kw={"width_ratios": [1, 1.24]},
                                   facecolor="white")
    blue, orange, ink = "#176997", "#b55d20", "#243746"
    x = np.linspace(1, n, 300)
    ax.plot(x, n / x, color="#a8bdcb", lw=1.4, zorder=1)
    ax.scatter(divisors, complements, s=65, color=blue, zorder=3)
    offsets = [(7, 0), (8, 8), (-16, 14), (8, 8), (8, 4), (-58, 10)]
    for d, e, offset in zip(divisors, complements, offsets):
        ax.annotate(f"({d}, {e})", (d, e), xytext=offset,
                    textcoords="offset points", fontsize=11, color=ink)
    ax.set(xlim=(0, 14), ylim=(0, 14), xlabel=r"First factor $d$",
           ylabel=r"Second factor $12/d$")
    ax.set_xticks([1, 2, 3, 4, 6, 12])
    ax.set_yticks([1, 2, 3, 4, 6, 12])
    ax.set_aspect("equal")
    ax.spines[["top", "right"]].set_visible(False)
    ax.grid(alpha=0.14)
    ax.set_title("Six ordered positive pairs", fontsize=14, pad=13)
    ax.text(0.5, -0.22, "(3, 4) and (4, 3) are separate pairs.",
            transform=ax.transAxes, ha="center", fontsize=11, color=ink)

    rows = [[str(d), str(e), str(t), str(s)]
            for d, e, t, s in zip(divisors, complements, tau_weights, sigma_weights)]
    rows.append(["Sum", "", str(tau_weights.sum()), str(sigma_weights.sum())])
    table = tab_ax.table(cellText=rows,
                         colLabels=[r"$d$", r"$12/d$", r"$\mathbf{1}(d)\mathbf{1}(12/d)$",
                                    r"$\mathrm{id}(d)\mathbf{1}(12/d)$"],
                         colWidths=[0.13, 0.17, 0.34, 0.36],
                         cellLoc="center", bbox=[0, 0.08, 1, 0.86])
    table.auto_set_font_size(False)
    table.set_fontsize(12)
    for (row, col), cell in table.get_celld().items():
        cell.set_edgecolor("#d9e2e8")
        cell.set_linewidth(0.7)
        cell.set_text_props(color=ink)
        if row == 0:
            cell.set_facecolor("#edf2f6")
        elif col == 2:
            cell.set_facecolor("#edf5fa")
            cell.set_text_props(color=blue)
        elif col == 3:
            cell.set_facecolor("#fff1e4")
            cell.set_text_props(color=orange)
        if row == len(rows):
            cell.set_text_props(weight="bold", fontsize=14)
    tab_ax.axis("off")
    tab_ax.set_title("Add one contribution from each pair", fontsize=14, pad=13)
    tab_ax.text(0.5, -0.05, r"$(\mathbf{1}*\mathbf{1})(12)=6\qquad(\mathrm{id}*\mathbf{1})(12)=28$",
                transform=tab_ax.transAxes, ha="center", fontsize=14)
    fig.suptitle("Dirichlet convolution sums over factor pairs of 12", fontsize=17, y=0.97)
    fig.text(0.52, 0.035,
             r"Pointwise products use only the input 12: $(\mathbf{1}\cdot\mathbf{1})(12)=1$, $(\mathrm{id}\cdot\mathbf{1})(12)=12$.",
             ha="center", fontsize=12, color=ink)
    fig.subplots_adjust(left=0.065, right=0.98, bottom=0.24, top=0.82, wspace=0.26)
    fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()