codex/sumset-pair-sums.py

sumset-pair-sums.py
"""Draw ordered pair sums and their counts; tested with Python 3.14.

Write sumset-pair-sums.png to the current working directory.
"""

from pathlib import Path

import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
import numpy as np


def main():
    examples = [np.array([0, 1, 2, 3]), np.array([0, 1, 4, 10])]
    expected_sums = [np.arange(7), np.array([0, 1, 2, 4, 5, 8, 10, 11, 14, 20])]
    expected_counts = [[1, 2, 3, 4, 3, 2, 1], [1, 2, 1, 2, 2, 1, 2, 2, 2, 1]]
    all_sums = np.unique(np.concatenate(expected_sums))
    palette = plt.colormaps["tab20"](np.arange(len(all_sums)))
    palette[:, :3] = 0.65 * palette[:, :3] + 0.35
    colors = dict(zip(all_sums, palette))
    plt.rcParams.update({"font.size": 11, "axes.titlesize": 13})
    fig, axes = plt.subplots(2, 2, figsize=(9, 6.6), dpi=120, facecolor="white")
    fig.suptitle("Sixteen ordered pairs, different numbers of sums", fontsize=16, y=0.975)
    fig.text(0.5, 0.915, "Each cell shows row + column. The same sum has the same color.",
             ha="center", fontsize=11)
    for col, (values, name) in enumerate(zip(examples, ["A", "B"])):
        pair_sums = values[:, None] + values[None, :]
        sums, counts = np.unique(pair_sums, return_counts=True)
        assert np.array_equal(sums, expected_sums[col])
        assert counts.tolist() == expected_counts[col]
        assert int(counts.sum()) == len(values) ** 2 == 16
        ax = axes[0, col]
        indices = np.searchsorted(all_sums, pair_sums)
        ax.imshow(indices, cmap=ListedColormap(palette), vmin=-0.5,
                  vmax=len(palette) - 0.5, interpolation="nearest")
        for row in range(4):
            for column in range(4):
                ax.text(column, row, str(pair_sums[row, column]), ha="center",
                        va="center", fontsize=15, fontweight="bold")
        ax.set_xticks(range(4), labels=values)
        ax.set_yticks(range(4), labels=values)
        ax.set_xticks(np.arange(-0.5, 4, 1), minor=True)
        ax.set_yticks(np.arange(-0.5, 4, 1), minor=True)
        ax.grid(which="minor", color="white", linewidth=2)
        ax.tick_params(which="both", length=0)
        ax.set_xlabel("Second member")
        ax.set_ylabel("First member")
        ax.set_title(f"{name} = {{{', '.join(map(str, values))}}}", pad=10)
        ax.spines[:].set_visible(False)

        ax = axes[1, col]
        ax.bar(sums, counts, color=[colors[s] for s in sums], width=0.72,
               edgecolor="#344454", linewidth=0.5)
        for total, count in zip(sums, counts):
            ax.text(total, count + 0.1, str(count), ha="center", fontsize=11)
        ax.set_title(f"{len(sums)} distinct sums; counts total 16", pad=10)
        ax.set_xlabel("Sum s (ordinary integer addition)")
        ax.set_ylabel(r"Ordered-pair count $r_{" + name + "+" + name + r"}(s)$")
        ax.set_xticks(sums)
        if col == 1:
            plt.setp(ax.get_xticklabels(), rotation=40, ha="right", fontsize=10)
        ax.set_yticks(range(5))
        ax.set_ylim(0, 4.7)
        ax.set_xlim(-0.85, sums[-1] + 0.85)
        ax.spines[["top", "right"]].set_visible(False)
        ax.set_axisbelow(True)
        ax.grid(axis="y", color="#e3e8eb")
    fig.subplots_adjust(left=0.09, right=0.98, top=0.83, bottom=0.105,
                        wspace=0.34, hspace=0.65)
    fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()