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