codexsquare-symmetry-orbits.py
square-symmetry-orbits.py
"""Verify D4 using exact coordinate permutations, then draw all eight symmetries.
Run from _media to write square-symmetry-orbits.png there. Tested with Python
3.14, matplotlib 3.10.7 and numpy 2.3.5 as specified by the root pyproject.toml.
"""
from itertools import permutations, product
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
COORDS = ((1, 1), (-1, 1), (-1, -1), (1, -1))
LETTERS = "ABCD"
def compose(g, h):
"""Tuples record vertex images; apply h first, then g."""
return tuple(g[h[i]] for i in range(4))
def square_model():
"""All signed coordinate permutations preserving the square, exactly."""
coordinate_maps = {}
for axes in permutations(range(2)):
for signs in product((-1, 1), repeat=2):
images = tuple(
tuple(signs[j] * point[axes[j]] for j in range(2))
for point in COORDS
)
permutation = tuple(COORDS.index(point) for point in images)
coordinate_maps[permutation] = (axes, signs)
identity = tuple(range(4))
r = tuple(COORDS.index((-y, x)) for x, y in COORDS)
s = tuple(COORDS.index((y, x)) for x, y in COORDS)
powers = [identity]
for _ in range(3):
powers.append(compose(r, powers[-1]))
labels = ("e", "r", "r²", "r³", "s", "rs", "r²s", "r³s")
group = powers + [compose(p, s) for p in powers]
assert len(coordinate_maps) == len(set(group)) == 8
assert set(group) == set(coordinate_maps)
actions = {
"Vertices": (tuple(range(4)), lambda g, x: g[x]),
"Edges": (
tuple(frozenset((i, (i + 1) % 4)) for i in range(4)),
lambda g, x: frozenset(g[i] for i in x),
),
"Diagonals": (
(frozenset((0, 2)), frozenset((1, 3))),
lambda g, x: frozenset(g[i] for i in x),
),
"Center": (
((0, 0),),
lambda g, x: tuple(
coordinate_maps[g][1][j] * x[coordinate_maps[g][0][j]]
for j in range(2)
),
),
}
return group, dict(zip(group, labels)), actions
def verify_square():
group, labels, actions = square_model()
identity, r, s = group[0], group[1], group[4]
assert compose(r, group[3]) == identity
assert compose(s, s) == identity
assert compose(compose(s, r), s) == group[3]
for g in group:
assert set(g) == set(range(4))
assert compose(g, identity) == compose(identity, g) == g
inverse = tuple(g.index(i) for i in range(4))
assert inverse in group
assert compose(g, inverse) == compose(inverse, g) == identity
for h in group:
assert compose(g, h) in group
for k in group:
assert compose(compose(g, h), k) == compose(g, compose(h, k))
expected_sizes = {"Vertices": (4, 2), "Edges": (4, 2),
"Diagonals": (2, 4), "Center": (1, 8)}
expected_stabilizers = {
"Vertices": {"e", "s"}, "Edges": {"e", "rs"},
"Diagonals": {"e", "r²", "s", "r²s"}, "Center": set(labels.values()),
}
cases = 0
for name, (objects, act) in actions.items():
for x in objects:
assert act(identity, x) == x
orbit = {act(g, x) for g in group}
stabilizer = {g for g in group if act(g, x) == x}
assert orbit == set(objects)
assert (len(orbit), len(stabilizer)) == expected_sizes[name]
assert len(orbit) * len(stabilizer) == len(group)
for g in stabilizer:
assert tuple(g.index(i) for i in range(4)) in stabilizer
for h in stabilizer:
assert compose(g, h) in stabilizer
for g, h in product(group, repeat=2):
assert act(g, x) in objects
assert act(compose(g, h), x) == act(g, act(h, x))
cases += 1
stabilizer = {labels[g] for g in group if act(g, objects[0]) == objects[0]}
assert stabilizer == expected_stabilizers[name]
print("D4: 8 signed coordinate permutations; 64 closure and 512 associativity cases.")
print(f"All identities/inverses, {cases} action-law cases and 11 orbit/stabilizer products checked.")
print("Vertices 4×2; edges 4×2; diagonals 2×4; center 1×8; every product equals 8.")
return group, labels, actions
def main():
group, labels, _ = verify_square()
fig, axes = plt.subplots(2, 4, figsize=(10, 6.3), dpi=120, facecolor="white")
outline = np.array(COORDS + (COORDS[0],))
colors = ("#126b91", "#44833f", "#8b58a2", "#bc6721")
for row in range(2):
for column in range(4):
ax = axes[row, column]
g = group[column + 4 * row]
color = colors[column]
ax.plot(outline[:, 0], outline[:, 1], color="#697780", lw=1.6)
ax.scatter(outline[:4, 0], outline[:4, 1], s=25, color="#697780")
for original, image in enumerate(g):
x, y = COORDS[image]
ax.text(1.25 * x, 1.25 * y, LETTERS[original], fontsize=13,
ha="center", va="center",
color=color if original == 0 else "#27343d",
fontweight="bold" if original == 0 else "normal")
target = COORDS[g[0]]
ax.scatter(*target, s=190, facecolor="white", edgecolor=color,
linewidth=2.5, zorder=3)
ax.scatter(*target, s=32, color=color, zorder=4)
ax.set_title(labels[g], fontsize=16, pad=9, color=color)
ax.set(xlim=(-1.55, 1.55), ylim=(-1.55, 1.55), aspect="equal")
ax.axis("off")
if row == 0:
ax.text(0.5, 1.29, f"A goes to {LETTERS[column]}",
transform=ax.transAxes, ha="center", fontsize=13,
color=color, fontweight="bold")
fig.suptitle("Eight square symmetries, four images of A", fontsize=19, y=0.97)
fig.text(0.5, 0.055, "Each column: two transformations give the same image of A.\n"
"Letters travel with their original vertices; the ring marks A's image.",
ha="center", va="center", fontsize=12, color="#27343d")
fig.subplots_adjust(left=0.04, right=0.96, bottom=0.14, top=0.77,
wspace=0.2, hspace=0.45)
output = Path.cwd() / "square-symmetry-orbits.png"
fig.savefig(output, facecolor="white", transparent=False)
plt.close(fig)
print(f"Wrote {output.name}: 1200×756 pixels, opaque white background.")
if __name__ == "__main__":
main()