codextwo-point-transformation-monoid.py
two-point-transformation-monoid.py
"""Render all self-maps of a two-point set and their composition table to cwd."""
from itertools import product
from pathlib import Path
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
import numpy as np
def compose(f, g):
return tuple(f[g[x]] for x in range(2))
def main():
names = ['i', 's', 'c₀', 'c₁']
maps = [(0, 1), (1, 0), (0, 0), (1, 1)]
index = {f: k for k, f in enumerate(maps)}
table = np.array([[index[compose(f, g)] for g in maps] for f in maps])
identity, swap, zero, one = maps
assert set(maps) == set(product(range(2), repeat=2))
assert all(compose(identity, f) == compose(f, identity) == f for f in maps)
assert all(compose(compose(f, g), h) == compose(f, compose(g, h))
for f, g, h in product(maps, repeat=3))
invertible = [f for f in maps if any(compose(f, g) == compose(g, f) == identity
for g in maps)]
assert invertible == [identity, swap]
assert compose(zero, swap) == zero and compose(swap, zero) == one
assert compose(zero, zero) == zero and compose(one, one) == one
assert np.array_equal(table[0], np.arange(4))
assert np.array_equal(table[:, 0], np.arange(4))
plt.rcParams.update({'font.size': 12, 'axes.titlesize': 14})
fig = plt.figure(figsize=(12.2, 7.2), dpi=120, facecolor='white')
outer = fig.add_gridspec(1, 2, width_ratios=[1.1, 1], wspace=.25)
diagrams = outer[0].subgridspec(2, 2, hspace=.35, wspace=.25)
for k, (f, name) in enumerate(zip(maps, names)):
ax = fig.add_subplot(diagrams[k//2, k % 2])
for x in range(2):
y = 1-x
ax.scatter([0, 1], [y, y], s=330, facecolor='#e9f0f6',
edgecolor='#4c6d86', zorder=3)
ax.text(0, y, str(x), ha='center', va='center', fontsize=12, zorder=4)
ax.text(1, y, str(x), ha='center', va='center', fontsize=12, zorder=4)
ax.annotate('', xy=(.91, 1-f[x]), xytext=(.09, y),
arrowprops={'arrowstyle': '->', 'lw': 1.7,
'color': '#246ea0', 'shrinkA': 4, 'shrinkB': 4})
desc = ['identity', 'swap', 'constant 0', 'constant 1'][k]
ax.set_title(f'{name}: {desc}', pad=10)
ax.text(.5, -.35, 'has an inverse' if k < 2 else 'has no inverse',
ha='center', fontsize=11,
color='#277c54' if k < 2 else '#965225')
ax.set(xlim=(-.22, 1.22), ylim=(-.45, 1.35)); ax.axis('off')
ax = fig.add_subplot(outer[1])
palette = ListedColormap(['#d8eddc', '#cbdcf0', '#f8e5c9', '#e7d7ed'])
ax.imshow(table, cmap=palette, vmin=0, vmax=3)
for row, col in product(range(4), repeat=2):
ax.text(col, row, names[table[row, col]], ha='center', va='center', fontsize=22)
ax.set_xticks(range(4), names); ax.set_yticks(range(4), names)
ax.set_xticks(np.arange(-.5, 4), minor=True)
ax.set_yticks(np.arange(-.5, 4), minor=True)
ax.grid(which='minor', color='white', lw=3)
ax.tick_params(which='minor', bottom=False, left=False)
ax.xaxis.tick_top(); ax.xaxis.set_label_position('top')
ax.set(xlabel='g: applied first (column)', ylabel='f: applied second (row)')
ax.set_title('Cell = f ∘ g', pad=55)
ax.text(.5, -.15, 'c₀ ∘ s = c₀ but s ∘ c₀ = c₁',
transform=ax.transAxes, ha='center', fontsize=13)
fig.suptitle('Composition is associative even when transformations lose information',
fontsize=16, y=.97)
fig.text(.5, .06, 'Only the identity and swap are reversible; the four maps together form a monoid.',
ha='center', fontsize=12)
fig.subplots_adjust(left=.045, right=.975, bottom=.20, top=.78)
fig.savefig(Path.cwd()/Path(__file__).with_suffix('.png').name,
facecolor='white', transparent=False)
plt.close(fig)
if __name__ == '__main__':
main()