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