codex/function-injection-surjection-bijection.py

function-injection-surjection-bijection.py
"""Render finite functions that separate injectivity and surjectivity to cwd."""

from pathlib import Path

import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse
import numpy as np


def properties(mapping, source_count, target_count):
    assert len(mapping) == source_count
    assert all(0 <= target < target_count for target in mapping)
    return len(set(mapping)) == source_count, len(set(mapping)) == target_count


def main():
    cases = [
        (3, 4, [0, 1, 2], "Injective, not surjective", "Distinct inputs stay distinct;\ntarget 4 is unused."),
        (4, 3, [0, 1, 1, 2], "Surjective, not injective", "Every target is reached;\nB and C share target 2."),
        (3, 3, [0, 1, 2], "Bijective", "Each target has one predecessor;\nall arrows reverse uniquely."),
    ]
    assert [properties(m, n, k) for n, k, m, _, _ in cases] == [(True, False), (False, True), (True, True)]
    inverse = {target: source for source, target in enumerate(cases[-1][2])}
    assert all(inverse[target] == source for source, target in enumerate(cases[-1][2]))
    plt.rcParams.update({"font.size": 12, "axes.titlesize": 14})
    fig, axes = plt.subplots(1, 3, figsize=(11.4, 5.6), dpi=120, facecolor="white")
    colors = ["#236c9f", "#bc7624", "#2c8467"]
    for ax, (source_count, target_count, mapping, title, explanation), color in zip(axes, cases, colors):
        ax.add_patch(Ellipse((.21, .5), .33, .87, facecolor="#edf3f7", edgecolor="#9eb2c1", lw=1.2))
        ax.add_patch(Ellipse((.79, .5), .33, .87, facecolor="#f3f5f1", edgecolor="#adb8a7", lw=1.2))
        source_y = np.linspace(.79, .21, source_count)
        target_y = np.linspace(.79, .21, target_count)
        for i, ordinate in enumerate(source_y):
            ax.scatter([.21], [ordinate], s=70, color=color, zorder=4)
            ax.text(.12, ordinate, chr(ord("A")+i), ha="center", va="center", fontsize=13)
        for i, ordinate in enumerate(target_y):
            used = i in mapping
            ax.scatter([.79], [ordinate], s=75, facecolors=color if used else "white",
                       edgecolors=color if used else "#b43a47", linewidths=1.5, zorder=4)
            ax.text(.89, ordinate, str(i+1), ha="center", va="center", fontsize=13)
        for source, target in enumerate(mapping):
            offset = .015 if source == 1 and source_count == 4 else (-.015 if source == 2 and source_count == 4 else 0)
            ax.annotate("", xy=(.755, target_y[target]+offset), xytext=(.25, source_y[source]),
                        arrowprops={"arrowstyle": "->", "color": color, "lw": 1.9})
        ax.text(.21, 1.005, "Domain X", ha="center", fontsize=12)
        ax.text(.79, 1.005, "Codomain Y", ha="center", fontsize=12)
        ax.set(xlim=(0, 1), ylim=(-.11, 1.14), title=title)
        ax.text(.5, -.08, explanation, ha="center", va="center", fontsize=11, color="#334550")
        ax.set_axis_off()
    fig.suptitle("A function assigns exactly one output to every input", fontsize=16, y=.985)
    fig.text(.5, .027, "The output pattern determines injectivity, surjectivity and invertibility",
             ha="center", fontsize=12)
    fig.subplots_adjust(left=.025, right=.98, top=.85, bottom=.105, wspace=.13)
    fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name,
                facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()