codexfree-actions-two-orbits-and-fixed-points.py
free-actions-two-orbits-and-fixed-points.py
"""Compare a free C2 action on four points with a trivial C2 action.
Run from _media. Tested with Python 3.14, matplotlib 3.10.7, numpy 2.3.5.
C2's two abstract elements remain distinct even in the trivial action.
"""
from pathlib import Path
import matplotlib.pyplot as plt
from matplotlib.patches import Circle, Ellipse, FancyArrowPatch
import numpy as np
def check_c2_action(action):
assert action[0] == tuple(range(4))
for g in range(2):
for h in range(2):
for x in range(4):
assert action[g][action[h][x]] == action[(g + h) % 2][x]
orbits = sorted({tuple(sorted({action[g][x] for g in range(2)})) for x in range(4)})
stabilizers = [[g for g in range(2) if action[g][x] == x] for x in range(4)]
transports = [[sum(action[g][x] == y for g in range(2)) for y in range(4)] for x in range(4)]
assert all(len({action[g][x] for g in range(2)}) * len(stabilizers[x]) == 2 for x in range(4))
return orbits, stabilizers, transports
def loop(ax, point, color, side):
x, y = point
angles = np.linspace(-2.5, 2.5, 80) if side == 1 else np.linspace(-0.6, 5.4, 80)
center = np.array([x + side * 0.23, y + 0.22])
curve = center[:, None] + 0.18 * np.array([np.cos(angles), np.sin(angles)])
ax.plot(*curve, color=color, lw=1.8)
ax.add_patch(FancyArrowPatch(curve[:, -5], curve[:, -1], arrowstyle="-|>",
color=color, mutation_scale=12, lw=1.8))
def main():
free = [(0, 1, 2, 3), (1, 0, 3, 2)]
trivial = [(0, 1, 2, 3), (0, 1, 2, 3)]
fo, fs, ft = check_c2_action(free)
to, ts, tt = check_c2_action(trivial)
assert fo == [(0, 1), (2, 3)] and to == [(0,), (1,), (2,), (3,)]
assert fs == [[0]] * 4 and ts == [[0, 1]] * 4
assert ft == [[1, 1, 0, 0], [1, 1, 0, 0], [0, 0, 1, 1], [0, 0, 1, 1]]
assert tt == [[2 if i == j else 0 for j in range(4)] for i in range(4)]
plt.rcParams.update({"font.size": 12, "font.family": "DejaVu Sans"})
fig, axes = plt.subplots(1, 2, figsize=(10.4, 5.5), dpi=120, facecolor="white")
fig.subplots_adjust(left=0.025, right=0.975, bottom=0.17, top=0.85, wspace=0.1)
blue, gray = "#2166ac", "#737c86"
points = np.array([[-0.72, 0.54], [0.72, 0.54], [-0.72, -0.55], [0.72, -0.55]])
for ax, title in zip(axes, ["Two pairs exchanged", "Every point fixed"]):
ax.set(xlim=(-1.55, 1.55), ylim=(-2.0, 1.65), aspect="equal")
ax.axis("off")
ax.text(0, 1.48, title, ha="center", fontsize=16, weight="bold")
for y in (0.54, -0.55):
axes[0].add_patch(Ellipse((0, y), 2.34, 0.94, edgecolor="#a9c7a1", facecolor="#f5faf3", lw=1.5))
for i, p in enumerate(points):
axes[1].add_patch(Ellipse(p, 1.02, 0.90, edgecolor="#a9c7a1", facecolor="#f5faf3", lw=1.5))
for ax in axes:
loop(ax, p, gray, side=1)
ax.add_patch(Circle(p, 0.15, facecolor="white", edgecolor="#243444", lw=2, zorder=5))
ax.text(*p, str(i + 1), ha="center", va="center", fontsize=15, zorder=6)
loop(axes[1], p, blue, side=-1)
for i, j in [(0, 1), (2, 3)]:
axes[0].add_patch(FancyArrowPatch(points[i], points[j], arrowstyle="<|-|>",
shrinkA=18, shrinkB=18, color=blue,
mutation_scale=17, lw=2.3))
axes[0].text(0, points[i, 1] - 0.22, r"$a$", color=blue, ha="center", fontsize=15)
axes[0].text(0, -1.50, "Two orbits of size 2\nFree, not transitive", ha="center", fontsize=13)
axes[1].text(0, -1.50, "Four singleton orbits\nNeither free nor transitive", ha="center", fontsize=13)
fig.suptitle(r"Two different actions of $C_2=\{e,a\}$ on four points", y=0.96, fontsize=17)
fig.text(0.5, 0.085, "Blue arrows: a. Gray loops: e. Green outlines: orbits.", ha="center")
fig.text(0.5, 0.035, r"Left: $G_x=\{e\}$ for all $x$. Right: $G_x=\{e,a\}$ for all $x$.", ha="center", fontsize=13)
fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()