codex/set-partitions-stirling-three-elements.py

set-partitions-stirling-three-elements.py
"""Draw an exact enumeration, not measured data. Python 3.14 / Matplotlib 3.10.

Run from _media; outlines and colours separate blocks but do not label them.
"""

from itertools import product
from pathlib import Path

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


def enumerate_partitions(n):
    """Enumerate canonical restricted-growth words independently of S(n,k)."""
    if n == 0:
        return [()]
    partitions = []
    for word in product(range(n), repeat=n):
        if word[0] != 0:
            continue
        if any(word[i] > 1 + max(word[:i]) for i in range(1, n)):
            continue
        partitions.append(tuple(
            tuple(i + 1 for i, label in enumerate(word) if label == j)
            for j in range(max(word) + 1)
        ))
    return partitions


def draw_partition(ax, partition, x, y):
    colours = ["#1b6d97", "#975725", "#6f519b"]
    widths = [0.43 * len(block) + 0.17 for block in partition]
    total = sum(widths) + 0.13 * (len(widths) - 1)
    cursor = x - total / 2
    for block, width, colour in zip(partition, widths, colours):
        ax.add_patch(FancyBboxPatch(
            (cursor, y - 0.29), width, 0.58,
            boxstyle="round,pad=0.025,rounding_size=0.12",
            linewidth=2, edgecolor=colour, facecolor="white",
        ))
        for j, element in enumerate(block):
            px = cursor + 0.30 + 0.43 * j
            ax.add_patch(Circle((px, y), 0.17, facecolor="#f2f5f7", edgecolor="none"))
            ax.text(px, y, str(element), ha="center", va="center", fontsize=14)
        cursor += width + 0.13
    notation = "{" + ", ".join("{" + ",".join(map(str, b)) + "}" for b in partition) + "}"
    ax.text(x, y - 0.53, notation, ha="center", va="center", fontsize=13)


def main():
    partitions = enumerate_partitions(3)
    assert len(partitions) == 5
    assert [sum(len(p) == k for p in partitions) for k in (1, 2, 3)] == [1, 3, 1]
    fig, ax = plt.subplots(figsize=(10.4, 6.4), dpi=100, facecolor="white")
    fig.subplots_adjust(left=0.025, right=0.975, bottom=0.03, top=0.98)
    ax.set(xlim=(0, 10), ylim=(0, 6.1))
    ax.axis("off")
    ax.text(5, 5.78, "All five set partitions of {1, 2, 3}", ha="center", fontsize=20, weight="bold")
    ax.text(5, 5.37, "Exact enumeration • elements are distinct; blocks are unnamed", ha="center", fontsize=13)
    for k, y in [(1, 4.4), (2, 2.8), (3, 1.2)]:
        row = [p for p in partitions if len(p) == k]
        label = (f"{k} block" + ("s" if k > 1 else "") + f"   →   {len(row)} "
                 + ("possibility" if len(row) == 1 else "possibilities"))
        ax.text(0.08, y + 0.53, label, fontsize=12, color="#333333")
        positions = [5] if len(row) == 1 else [1.7, 5, 8.3]
        for p, x in zip(row, positions):
            draw_partition(ax, p, x, y)
    ax.text(5, 0.12, "Reordering blocks or elements within a block gives the same partition.", ha="center", fontsize=12)
    fig.savefig(Path(__file__).with_suffix('.png').name, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()