codex/set-partitions-stirling-insert-four.py

set-partitions-stirling-insert-four.py
"""Construct the seven two-block partitions of four elements, exactly.

Python 3.14 / Matplotlib 3.10. Run from _media. Gold marks element 4;
block colours and left-to-right positions are drawing aids, not labels.
"""

from pathlib import Path

import matplotlib.pyplot as plt
from matplotlib.patches import Circle, FancyBboxPatch


def canonical(blocks):
    return tuple(sorted(tuple(sorted(block)) for block in blocks))


def insert(partition, element, block_index):
    return canonical([block + (element,) if i == block_index else block
                      for i, block in enumerate(partition)])


def draw_partition(ax, partition, x, y):
    colours = ["#1b6d97", "#975725"]
    widths = [0.39 * len(block) + 0.14 for block in partition]
    total = sum(widths) + 0.12 * (len(widths) - 1)
    cursor = x - total / 2
    for block, width, colour in zip(partition, widths, colours):
        ax.add_patch(FancyBboxPatch(
            (cursor, y - 0.27), width, 0.54,
            boxstyle="round,pad=0.02,rounding_size=0.10",
            linewidth=1.8, edgecolor=colour, facecolor="white",
        ))
        for j, element in enumerate(block):
            px = cursor + 0.265 + 0.39 * j
            ax.add_patch(Circle((px, y), 0.155,
                               facecolor="#ffcf5c" if element == 4 else "#edf3f7",
                               edgecolor="#9c6900" if element == 4 else "none"))
            ax.text(px, y, str(element), ha="center", va="center", fontsize=13)
        cursor += width + 0.12


def main():
    sources = [((1, 2), (3,)), ((1, 3), (2,)), ((1,), (2, 3))]
    outputs = [insert(p, 4, j) for p in sources for j in range(2)]
    singleton_case = ((1, 2, 3), (4,))
    assert len(set(outputs + [singleton_case])) == 7
    assert all(any(4 in block and len(block) > 1 for block in p) for p in outputs)
    assert all(sorted(e for block in p for e in block) == [1, 2, 3, 4]
               for p in outputs + [singleton_case])
    assert sum(sorted(map(len, p)) == [1, 3] for p in outputs + [singleton_case]) == 4
    fig, ax = plt.subplots(figsize=(11.2, 7.6), dpi=100, facecolor="white")
    fig.subplots_adjust(left=0.02, right=0.98, bottom=0.02, top=0.98)
    ax.set(xlim=(0, 11), ylim=(0, 7.2))
    ax.axis("off")
    ax.text(5.5, 6.9, "Build every two-block partition of {1, 2, 3, 4}", ha="center", fontsize=19, weight="bold")
    ax.text(5.5, 6.5, "Exact construction: S(4, 2) = 2 S(3, 2) + S(3, 1) = 6 + 1", ha="center", fontsize=14)
    ax.text(1.9, 5.93, "Start with 2 blocks", ha="center", fontsize=13, weight="bold")
    ax.text(6, 5.93, "Insert 4 in first block", ha="center", fontsize=13, weight="bold")
    ax.text(9.2, 5.93, "Insert 4 in second block", ha="center", fontsize=13, weight="bold")
    for i, (p, y) in enumerate(zip(sources, [5.3, 4.3, 3.3])):
        draw_partition(ax, p, 1.9, y)
        draw_partition(ax, outputs[2 * i], 6, y)
        draw_partition(ax, outputs[2 * i + 1], 9.2, y)
        ax.annotate("", xy=(4.64, y), xytext=(3.15, y),
                    arrowprops={"arrowstyle": "->", "color": "#56616a", "lw": 1.5})
    ax.plot([0.35, 10.65], [2.65, 2.65], color="#bbbbbb", lw=1)
    ax.text(5.5, 2.24, "The remaining case: give 4 its own singleton block", ha="center", fontsize=14, weight="bold")
    draw_partition(ax, ((1, 2, 3),), 1.9, 1.65)
    ax.annotate("", xy=(5.15, 1.65), xytext=(3.15, 1.65),
                arrowprops={"arrowstyle": "->", "color": "#56616a", "lw": 1.5})
    draw_partition(ax, singleton_case, 6.5, 1.65)
    ax.text(1.9, 1.06, "S(3, 1) = 1", ha="center", fontsize=13)
    ax.text(6.5, 1.06, "{{1,2,3}, {4}}", ha="center", fontsize=13)
    ax.text(5.5, 0.53, "Gold marks the distinguished element 4. Each outcome appears exactly once.", ha="center", fontsize=12)
    ax.text(5.5, 0.16, "Block order and colours are display conventions; the blocks remain unnamed.", ha="center", fontsize=12)
    fig.savefig(Path(__file__).with_suffix('.png').name, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()