codex/inclusion-exclusion-counting.py

inclusion-exclusion-counting.py
"""Exact overlaps of divisibility filters on 1..30; PNG goes to cwd."""

from pathlib import Path

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, Rectangle


def main():
    universe = set(range(1, 31))
    a = {x for x in universe if x % 2 == 0}
    b = {x for x in universe if x % 3 == 0}
    c = {x for x in universe if x % 5 == 0}
    assert (len(a - b), len(a & b), len(b - a), len(universe - (a | b))) == (10, 5, 5, 10)
    assert len(a | b) == len(a) + len(b) - len(a & b) == 20
    sizes = [len(a), len(b), len(c), len(a & b), len(a & c), len(b & c), len(a & b & c)]
    assert sizes == [15, 10, 6, 5, 3, 2, 1]
    assert len(a | b | c) == sum(sizes[:3]) - sum(sizes[3:6]) + sizes[6] == 22
    fig, axes = plt.subplots(1, 2, figsize=(10.8, 5.3), dpi=120)
    blue, orange = "#176997", "#b55d20"
    ax = axes[0]
    ax.add_patch(Rectangle((-0.5, -0.1), 5, 3.05, fill=False, ec="#aeb8c0", lw=1.2))
    ax.add_patch(Circle((1.3, 1.45), 1.18, facecolor=blue, alpha=0.13, edgecolor=blue, lw=2))
    ax.add_patch(Circle((2.7, 1.45), 1.18, facecolor=orange, alpha=0.13, edgecolor=orange, lw=2))
    for x, count, label in [(0.75, 10, "A only"), (2, 5, "both"), (3.25, 5, "B only")]:
        ax.text(x, 1.45, str(count), ha="center", fontsize=21, color="#273746")
        ax.text(x, 1.12, label, ha="center", fontsize=10, color="#536371")
    ax.text(1.05, 2.75, "A: multiples of 2", ha="center", fontsize=11, color=blue)
    ax.text(3.05, 2.75, "B: multiples of 3", ha="center", fontsize=11, color=orange)
    ax.text(2, 0.05, "neither: 10", ha="center", fontsize=12)
    ax.text(2, -0.57, "15 + 10 − 5 = 20 selected", ha="center", fontsize=14)
    ax.text(2, -0.92, "Region areas are schematic, not proportional.", ha="center", fontsize=10)
    ax.set_xlim(-0.6, 4.6)
    ax.set_ylim(-0.3, 3.1)
    ax.set_aspect("equal")
    ax.axis("off")
    ax.set_title("Two overlapping filters", fontsize=13, pad=15)
    ax = axes[1]
    colors = {0: "#f2f4f6", 1: "#d9eaf4", 2: "#ffd6b7", 3: "#b55d20"}
    for x in range(1, 31):
        column, row = (x - 1) % 6, (x - 1) // 6
        membership = sum(x in s for s in [a, b, c])
        ax.add_patch(Rectangle((column, 4 - row), 0.86, 0.78,
                              facecolor=colors[membership], ec="#b5bfc6", lw=0.8))
        ax.text(column + 0.43, 4 - row + 0.39, str(x), ha="center", va="center",
                fontsize=12, color="white" if membership == 3 else "#263746")
    for i, text in enumerate(["0", "1", "2", "3"]):
        ax.add_patch(Rectangle((i * 1.45, -0.75), 0.28, 0.28,
                              facecolor=colors[i], ec="#b5bfc6", clip_on=False))
        ax.text(i * 1.45 + 0.38, -0.62, text, va="center", fontsize=10)
    ax.text(2.95, -1.15, "Number of filters matched (2, 3, and 5)", ha="center", fontsize=10)
    ax.text(2.95, -1.8, "15 + 10 + 6 − 5 − 3 − 2 + 1 = 22", ha="center", fontsize=13)
    ax.set_xlim(-0.2, 6.0)
    ax.set_ylim(-0.2, 5.05)
    ax.set_aspect("equal")
    ax.axis("off")
    ax.set_title("Add a third filter: multiples of 5", fontsize=13, pad=15)
    fig.suptitle("Inclusion–exclusion counts each integer in the union once",
                 fontsize=15, y=0.97)
    fig.subplots_adjust(left=0.04, right=0.97, top=0.80, bottom=0.25, wspace=0.23)
    fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name,
                facecolor="white")
    plt.close(fig)


if __name__ == "__main__":
    main()