codex/nonnegative-positive-integer-injection-chain.py

nonnegative-positive-integer-injection-chain.py
"""Draw the sparse-shift bijection; emit the matching PNG in the CWD."""

from pathlib import Path

import matplotlib

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


BLUE = "#2069a5"
GRAY = "#75818a"
ORANGE = "#b65b18"


def node(ax, x, y, label, in_x, color=BLUE):
    ax.add_patch(Circle((x, y), 0.30, facecolor="#e8f2fa" if in_x else "white",
                        edgecolor=color, linewidth=1.8, zorder=3))
    ax.text(x, y, str(label), fontsize=15, ha="center", va="center", zorder=4)
    ax.text(x, y - 0.48, "$X$" if in_x else "$Y$", ha="center", fontsize=12,
            color=GRAY)


def arrow(ax, x1, x2, y, label, color, dashed=False):
    ax.add_patch(FancyArrowPatch((x1, y), (x2, y), arrowstyle="-|>",
                                mutation_scale=15, shrinkA=24, shrinkB=24,
                                linewidth=1.7 if dashed else 2.5,
                                linestyle="--" if dashed else "-", color=color,
                                zorder=2))
    ax.text((x1 + x2) / 2, y + 0.35, label, ha="center", fontsize=12, color=color)


def main():
    fig, ax = plt.subplots(figsize=(10.4, 5.3), dpi=120, facecolor="white")
    ax.set(xlim=(-0.7, 10.7), ylim=(-0.65, 4.5))
    ax.axis("off")
    fig.suptitle("Repair the missing zero along one chain", fontsize=18, y=0.96)
    ax.text(5, 4.03, r"$f(n)=2n+2,\quad g(m)=m,\quad A=\{0,2,6,14,30,\ldots\}$",
            fontsize=15, ha="center")

    xs = [0.05 + 1.35 * i for i in range(8)]
    values = [0, 2, 2, 6, 6, 14, 14, 30]
    ax.text(-0.45, 3.53, r"On $A$: follow $f$", fontsize=13, color=BLUE)
    for i, (x, n) in enumerate(zip(xs, values)):
        node(ax, x, 2.9, n, i % 2 == 0)
        if i < 7:
            chosen = i % 2 == 0
            arrow(ax, x, xs[i + 1], 2.9, "$h=f$" if chosen else "$g$",
                  BLUE if chosen else GRAY, dashed=not chosen)
    ax.text(10.20, 2.9, r"$\cdots$", fontsize=20, va="center")
    ax.text(5, 1.95, r"$h(0)=2,\quad h(2)=6,\quad h(6)=14,\quad h(14)=30$",
            ha="center", fontsize=14, color=BLUE)
    ax.plot([-0.45, 10.45], [1.65, 1.65], color="#d8dee3", linewidth=1)

    ax.text(-0.45, 1.23, r"Outside $A$: reverse $g$, keeping the number", fontsize=13,
            color=ORANGE)
    for j, n in enumerate([1, 3, 4, 5]):
        x1 = 0.05 + 2.75 * j
        x2 = x1 + 1.35
        node(ax, x1, 0.55, n, True, ORANGE)
        node(ax, x2, 0.55, n, False, ORANGE)
        arrow(ax, x1, x2, 0.55, r"$h=g^{-1}$", ORANGE)
    fig.text(0.5, 0.045, "Chain outputs: A without 0.   Fixed outputs: all positive integers outside A.",
             ha="center", fontsize=12)
    fig.subplots_adjust(left=0.04, right=0.99, top=0.87, bottom=0.10)
    fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), facecolor="white",
                transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()