codex/stars-and-bars-three-bin-allocation.py

stars-and-bars-three-bin-allocation.py
"""Draw labelled-bin encodings; tested with Python 3.14.4, matplotlib 3.10.7.

Write stars-and-bars-three-bin-allocation.png in the current directory.
"""

from itertools import combinations, product
from math import comb
from pathlib import Path

import matplotlib

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


def encode(counts):
    return "|".join("•" * count for count in counts)


def main():
    weak = {counts for counts in product(range(6), repeat=3) if sum(counts) == 5}
    positive = {counts for counts in weak if min(counts) >= 1}
    strings = {"".join("|" if i in bars else "•" for i in range(7))
               for bars in combinations(range(7), 2)}
    assert len(weak) == comb(7, 2) == 21
    assert len(positive) == comb(4, 2) == 6
    assert {encode(counts) for counts in weak} == strings
    assert {tuple(x - 1 for x in counts) for counts in positive} == {
        counts for counts in product(range(3), repeat=3) if sum(counts) == 2}
    rows = [((2, 1, 2), "Ordinary split"),
            ((2, 0, 3), "Adjacent bars: second bin empty"),
            ((0, 2, 3), "Left end bar: first bin empty"),
            ((2, 3, 0), "Right end bar: third bin empty")]
    colors = ["#186c9c", "#b85b19", "#34815c"]
    fig, ax = plt.subplots(figsize=(9, 5.9), dpi=120, facecolor="white")
    ax.set(xlim=(-0.6, 13), ylim=(-0.95, 7.4))
    ax.axis("off")
    for row, (counts, description) in enumerate(rows):
        y = 6 - 1.7 * row
        ax.text(-0.3, y + 0.65, description, fontsize=11, color="#253746")
        bin_index = 0
        for pos, symbol in enumerate(encode(counts)):
            if symbol == "|":
                ax.plot([pos, pos], [y - 0.25, y + 0.25],
                        color="#253746", linewidth=2.3)
                bin_index += 1
            else:
                ax.scatter(pos, y, s=110, color=colors[bin_index], zorder=3)
        ax.text(7.15, y, "→", fontsize=20, va="center", color="#253746")
        for bin_index, count in enumerate(counts):
            ax.text(8.3 + 1.7 * bin_index, y, str(count), ha="center", va="center",
                    fontsize=18, color=colors[bin_index])
    for bin_index, label in enumerate(["First bin", "Second bin", "Third bin"]):
        ax.text(8.3 + 1.7 * bin_index, 7.1, label, ha="center",
                fontsize=10, color=colors[bin_index])
    fig.suptitle("Five identical objects, three labelled bins", fontsize=16, y=0.98)
    fig.text(0.5, 0.025, "21 nonnegative count tuples; 6 positive count tuples. Two bars delimit three bins.",
             ha="center", fontsize=11, color="#253746")
    fig.subplots_adjust(left=0.04, right=0.99, bottom=0.09, top=0.90)
    fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name,
                facecolor="white", transparent=False)
    plt.close(fig)
    print(f"Verified {len(weak)} nonnegative and {len(positive)} positive tuples; "
          "bar-position enumeration and positive shifts agree.")


if __name__ == "__main__":
    main()