codexinclusion-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()