codex/set-union-intersection-regions.py

set-union-intersection-regions.py
"""Generate set-union-intersection-regions.png in the current directory.

Tested with Python 3.14.4, numpy 2.3.5, matplotlib 3.10.7.
Positions encode membership; circle areas do not encode cardinality.
"""

from pathlib import Path

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


A = {1, 2, 3}
B = {3, 4}
U = set(range(1, 7))
POSITIONS = {1: (-1.2, .35), 2: (-1.2, -.35), 3: (0, 0),
             4: (1.5, 0), 5: (-2.1, -1.25), 6: (2.1, -1.25)}
RADIUS = 1.25
BOUNDS = (-2.65, 2.65, -1.65, 1.65)


def draw_panel(ax, region, selected, title, answer):
    x = np.linspace(BOUNDS[0], BOUNDS[1], 801)
    y = np.linspace(BOUNDS[2], BOUNDS[3], 501)
    xx, yy = np.meshgrid(x, y)
    in_a = (xx + .8) ** 2 + yy ** 2 <= RADIUS ** 2
    in_b = (xx - .8) ** 2 + yy ** 2 <= RADIUS ** 2
    ax.contourf(xx, yy, region(in_a, in_b).astype(int),
                levels=[-.5, .5, 1.5], colors=["white", "#b9e0d4"])
    ax.add_patch(Rectangle((BOUNDS[0], BOUNDS[2]), BOUNDS[1] - BOUNDS[0],
                           BOUNDS[3] - BOUNDS[2], fill=False, lw=1.3,
                           edgecolor="#68777e", clip_on=False))
    for cx in [-.8, .8]:
        ax.add_patch(Circle((cx, 0), RADIUS, fill=False, lw=1.7,
                            edgecolor="#263c48"))
    ax.text(-1.05, 1.03, "$A$", ha="center", fontsize=13)
    ax.text(1.05, 1.03, "$B$", ha="center", fontsize=13)
    ax.text(-2.45, 1.32, "$U$", fontsize=13)
    for n, (px, py) in POSITIONS.items():
        assert (((px + .8) ** 2 + py ** 2 <= RADIUS ** 2) == (n in A))
        assert (((px - .8) ** 2 + py ** 2 <= RADIUS ** 2) == (n in B))
        assert bool(region(n in A, n in B)) == (n in selected)
        ax.text(px, py, str(n), ha="center", va="center", fontsize=14,
                color="#123e32" if n in selected else "#6a747a",
                fontweight="bold" if n in selected else "normal",
                bbox={"boxstyle": "circle,pad=.17", "facecolor": "white",
                      "edgecolor": "#28735d" if n in selected else "#c6cdd1"})
    ax.set(xlim=(BOUNDS[0] - .08, BOUNDS[1] + .08),
           ylim=(BOUNDS[2] - .08, BOUNDS[3] + .08), aspect="equal")
    ax.set_title(title, fontsize=14, pad=12)
    ax.text(.5, -.08, answer, transform=ax.transAxes, ha="center", fontsize=12)
    ax.axis("off")


def main():
    fig, axes = plt.subplots(2, 2, figsize=(8.8, 6.9), dpi=120,
                             facecolor="white")
    panels = [
        (lambda a, b: a | b, A | B, r"Union: $A\cup B$", r"$\{1,2,3,4\}$"),
        (lambda a, b: a & b, A & B, r"Intersection: $A\cap B$", r"$\{3\}$"),
        (lambda a, b: a & ~np.asarray(b), A - B, r"Difference: $A\setminus B$", r"$\{1,2\}$"),
        (lambda a, b: b & ~np.asarray(a), B - A, r"Difference: $B\setminus A$", r"$\{4\}$"),
    ]
    for ax, panel in zip(axes.flat, panels):
        draw_panel(ax, *panel)
    fig.suptitle("One membership picture, four selections", fontsize=16, y=.97)
    fig.text(.5, .914, r"$A=\{1,2,3\},\quad B=\{3,4\},\quad U=\{1,2,3,4,5,6\}$",
             ha="center", fontsize=12)
    fig.text(.5, .025, "Green regions are selected. Numbers show membership.\n"
             "Circle areas do not represent the number of elements.",
             ha="center", fontsize=10)
    fig.subplots_adjust(left=.045, right=.955, top=.84, bottom=.14,
                        hspace=.48, wspace=.13)
    fig.savefig(Path.cwd() / "set-union-intersection-regions.png",
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()