codex/open-and-closed-sets-on-line.py

open-and-closed-sets-on-line.py
"""Draw membership for four open/closed cases in the usual real line.

Tested with Python 3.14, NumPy 2.3.5, and Matplotlib 3.10.7.
Run via make _media/open-and-closed-sets-on-line.png; save to cwd.
"""

from pathlib import Path

import matplotlib

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


BLUE = "#176b9c"
GREY = "#bdc6ce"


def number_line(ax, y):
    ax.annotate("", xy=(1.42, y), xytext=(-0.42, y), zorder=1,
                arrowprops={"arrowstyle": "<->", "color": GREY, "lw": 1.4})
    for x in (0, 1):
        ax.plot([x, x], [y - 0.045, y + 0.045], color=GREY, lw=1)
        ax.text(x, y - 0.21, str(x), ha="center", va="top", fontsize=11)


def interval(ax, y, closed_left, closed_right):
    ax.plot([0, 1], [y, y], color=BLUE, lw=5, solid_capstyle="butt")
    for x, included in ((0, closed_left), (1, closed_right)):
        ax.scatter([x], [y], s=95, facecolors=BLUE if included else "white",
                   edgecolors=BLUE, linewidths=2, zorder=4)


def main():
    fig, ax = plt.subplots(figsize=(9.6, 5.7), facecolor="white")
    fig.subplots_adjust(left=0.04, right=0.98, top=0.88, bottom=0.09)
    ax.set(xlim=(-1.52, 1.65), ylim=(-0.46, 4.92))
    ax.axis("off")
    fig.suptitle("Open and closed: four possibilities", fontsize=19, y=0.97)
    ax.text(0.5, 4.77, "Ambient space: the real line with its usual topology",
            ha="center", fontsize=12)
    rows = np.array([4.08, 3.06, 2.04, 0.98])
    for y, label, expression, endpoints in zip(
        rows,
        ("Open only", "Closed only", "Neither", "Both (clopen)"),
        (r"$(0,1)$", r"$[0,1]$", r"$[0,1)$", r"$\varnothing$ and $\mathbb{R}$"),
        ((False, False), (True, True), (True, False), None),
    ):
        ax.text(-1.45, y + 0.07, label, fontsize=14, va="bottom")
        ax.text(-1.45, y - 0.22, expression, fontsize=14, va="top")
        if endpoints is not None:
            number_line(ax, y)
            interval(ax, y, *endpoints)
        else:
            number_line(ax, y + 0.22)
            ax.text(1.49, y + 0.22, r"$\varnothing$", fontsize=14, va="center")
            ax.text(0.5, y + 0.44, "No points included", ha="center", fontsize=11)
            number_line(ax, y - 0.40)
            ax.annotate("", xy=(1.42, y - 0.40), xytext=(-0.42, y - 0.40),
                        arrowprops={"arrowstyle": "<->", "color": BLUE,
                                    "lw": 4, "mutation_scale": 17})
            ax.text(1.49, y - 0.40, r"$\mathbb{R}$", fontsize=14, va="center")
    fig.text(0.5, 0.025, "Filled circle: included endpoint   |   Hollow circle: excluded endpoint",
             ha="center", fontsize=11)
    fig.savefig(Path.cwd() / f"{Path(__file__).stem}.png", dpi=125,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()