codex/square-group-abelianization.py

square-group-abelianization.py
"""Illustrate and exhaustively check the square group of order 8 (D8).

Tested with Python 3.14.4. Products act rightmost first and
[x,y] = x^-1 y^-1 x y. Write the PNG to cwd for the Makefile.
"""

from pathlib import Path

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


def compose(p, q):
    return tuple(p[q[i]] for i in range(4))


def inverse(p):
    return tuple(p.index(i) for i in range(4))


def power(p, n):
    result = tuple(range(4))
    for _ in range(n):
        result = compose(result, p)
    return result


def verify():
    e, r, s = tuple(range(4)), (1, 2, 3, 0), (0, 3, 2, 1)
    elements = {(i, j): compose(power(r, i), power(s, j))
                for i in range(4) for j in range(2)}
    coordinates = {p: ij for ij, p in elements.items()}
    group = set(coordinates)
    assert len(group) == 8
    assert power(r, 4) == power(s, 2) == e
    assert compose(compose(s, r), s) == inverse(r)
    comm = lambda p, q: compose(compose(compose(inverse(p), inverse(q)), p), q)
    normal = {e, power(r, 2)}
    assert comm(r, s) == power(r, 2)
    assert {comm(p, q) for p in group for q in group} == normal
    assert {p for p in group if all(compose(p, q) == compose(q, p)
                                  for q in group)} == normal
    bits = lambda p: (coordinates[p][0] % 2, coordinates[p][1])
    assert {bits(p) for p in group} == {(0, 0), (1, 0), (0, 1), (1, 1)}
    assert {p for p in group if bits(p) == (0, 0)} == normal
    cosets = {frozenset(compose(p, n) for n in normal) for p in group}
    assert len(cosets) == 4 and all(len(c) == 2 for c in cosets)
    for p in group:
        assert compose(p, inverse(p)) == e == compose(inverse(p), p)
        assert {compose(p, n) for n in normal} == {q for q in group if bits(q) == bits(p)}
        for q in group:
            i, j = coordinates[p]
            k, ell = coordinates[q]
            assert compose(p, q) == elements[((i + (-1) ** j * k) % 4, (j + ell) % 2)]
            assert bits(compose(p, q)) == tuple((a + b) % 2 for a, b in zip(bits(p), bits(q)))
            for t in group:
                assert compose(compose(p, q), t) == compose(p, compose(q, t))
    classes = {frozenset(compose(compose(q, p), inverse(q)) for q in group)
               for p in group}
    assert classes == {frozenset({e}), frozenset({power(r, 2)}),
                       frozenset({r, power(r, 3)}),
                       frozenset({s, elements[(2, 1)]}),
                       frozenset({elements[(1, 1)], elements[(3, 1)]})}
    print('D8 (order 8): checked all 64 products and commutators, 512 associative triples; '
          'derived=center={e,r²}, four cosets, quotient=C2×C2, conjugacy sizes=1,1,2,2,2.')
    return elements


def square(ax, p, title):
    positions = np.array([[1, 1], [-1, 1], [-1, -1], [1, -1]])
    closed = np.vstack([positions, positions[0]])
    ax.plot(closed[:, 0], closed[:, 1], color='#8395a2', lw=1.5)
    colors = ['#c57012', '#326ca3', '#367f62', '#945482']
    for original, destination in enumerate(p):
        point = positions[destination]
        ax.scatter(*point, s=350, color=colors[original], zorder=3)
        ax.text(*point, 'ABCD'[original], ha='center', va='center',
                fontsize=12, weight='bold', color='white', zorder=4)
    ax.set(xlim=(-1.5, 1.5), ylim=(-1.5, 1.5), aspect='equal')
    ax.set_title(title, fontsize=14, pad=0)
    ax.axis('off')


def main():
    elements = verify()
    fig = plt.figure(figsize=(10, 6.7), dpi=120, facecolor='white')
    fig.text(.5, .96, 'The square group of order 8: four classes survive',
             ha='center', fontsize=18, weight='bold')
    fig.text(.5, .91, r'$r$: counterclockwise quarter-turn; $s$: reflection in diagonal AC',
             ha='center', fontsize=12)
    columns = [((0, 0), [r'$e$', r'$r^2$'], '#edf5ed'),
               ((1, 0), [r'$r$', r'$r^3$'], '#edf2f9'),
               ((0, 1), [r'$s$', r'$r^2s$'], '#faf1e7'),
               ((1, 1), [r'$rs$', r'$r^3s$'], '#f6edf6')]
    for col, ((i, j), labels, color) in enumerate(columns):
        left = .03 + col * .24
        fig.add_artist(FancyBboxPatch((left, .40), .215, .44,
                       boxstyle='round,pad=.006', facecolor=color,
                       edgecolor='#c6d2da', transform=fig.transFigure, zorder=0))
        fig.text(left + .108, .805, f'bit pair ({i}, {j})', ha='center', fontsize=13)
        for row, label in enumerate(labels):
            square(fig.add_axes([left + .033, .615 - row * .185, .15, .15]),
                   elements[(i + 2 * row, j)], label)
    fig.text(.5, .365, r"$N=D_8'=Z(D_8)=\{e,r^2\}$; each column is one coset of $N$",
             ha='center', fontsize=14)
    ax = fig.add_axes([.055, .065, .39, .25])
    ax.axis('off')
    bit_pairs = [(0, 0), (1, 0), (0, 1), (1, 1)]
    labels = ['00', '10', '01', '11']
    entries = [[f'{(a + c) % 2}{(b + d) % 2}' for c, d in bit_pairs]
               for a, b in bit_pairs]
    table = ax.table(cellText=entries, rowLabels=labels, colLabels=labels,
                     cellLoc='center', loc='center', bbox=[.12, .03, .83, .88])
    table.auto_set_font_size(False)
    table.set_fontsize(11)
    for (row, col), cell in table.get_celld().items():
        cell.set_edgecolor('#c6d2da')
        cell.set_facecolor('#edf2f9' if row == 0 or col == -1 else 'white')
    ax.set_title('Quotient operation: add each bit mod 2', fontsize=12, pad=5)
    fig.text(.68, .23, 'First bit: even or odd quarter-turn count\n'
             'Second bit: rotation or reflection\n'
             'A half-turn is forgotten.', ha='center', va='center',
             fontsize=12, linespacing=1.7)
    fig.text(.68, .095, r'$D_8/N\cong C_2\times C_2$', ha='center', fontsize=17)
    fig.text(.5, .023, 'Products act rightmost first. Vertex labels follow the original vertices.',
             ha='center', fontsize=10)
    fig.savefig(Path.cwd() / Path(__file__).with_suffix('.png').name,
                facecolor='white', transparent=False)
    plt.close(fig)


if __name__ == '__main__':
    main()