codexlabelled-trees-on-four-vertices.py
labelled-trees-on-four-vertices.py
"""All sixteen fixed-label trees, independently enumerated; Python 3.14.
Write the matching opaque PNG into the current working directory.
"""
from itertools import combinations
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
def adjacency_of(edges):
adjacency = {v: set() for v in range(1, 5)}
for a, b in edges:
adjacency[a].add(b)
adjacency[b].add(a)
return adjacency
def is_connected(edges):
adjacency = adjacency_of(edges)
visited, pending = set(), [1]
while pending:
v = pending.pop()
if v not in visited:
visited.add(v)
pending.extend(adjacency[v] - visited)
return len(visited) == 4
def encode(edges):
adjacency, code = adjacency_of(edges), []
for _ in range(2):
leaf = min(v for v in adjacency if len(adjacency[v]) == 1)
neighbour = next(iter(adjacency[leaf]))
code.append(neighbour)
adjacency[neighbour].remove(leaf)
del adjacency[leaf]
return tuple(code)
def layout(edges):
adjacency = adjacency_of(edges)
centre = next((v for v in adjacency if len(adjacency[v]) == 3), None)
if centre is not None:
leaves = sorted(set(adjacency) - {centre})
return dict(zip([centre] + leaves, [(0, 0), (0, 0.9), (-0.8, -0.65), (0.8, -0.65)]))
start = min(v for v in adjacency if len(adjacency[v]) == 1)
order, previous, current = [], None, start
while current is not None:
order.append(current)
following = adjacency[current] - ({previous} if previous is not None else set())
previous, current = current, next(iter(following), None)
return {v: (x, 0) for v, x in zip(order, [-1.15, -0.38, 0.38, 1.15])}
def main():
trees = [edges for edges in combinations(list(combinations(range(1, 5), 2)), 3)
if is_connected(edges)]
stars = sorted((edges for edges in trees if max(map(len, adjacency_of(edges).values())) == 3),
key=encode)
paths = sorted((edges for edges in trees if max(map(len, adjacency_of(edges).values())) == 2),
key=encode)
assert len(trees) == 16 and len(stars) == 4 and len(paths) == 12
assert {encode(edges) for edges in trees} == {(a, b) for a in range(1, 5) for b in range(1, 5)}
fig, axes = plt.subplots(4, 4, figsize=(10, 8.2), dpi=120, facecolor="white")
for i, (ax, edges) in enumerate(zip(axes.flat, stars + paths)):
positions = layout(edges)
colour = "#b55c1c" if i < 4 else "#226b96"
for a, b in edges:
ax.plot([positions[a][0], positions[b][0]], [positions[a][1], positions[b][1]],
color=colour, lw=2.3, zorder=1)
for v in range(1, 5):
ax.scatter(*positions[v], s=390, facecolor="white", edgecolor=colour, lw=1.6, zorder=2)
ax.text(*positions[v], str(v), ha="center", va="center", fontsize=12, zorder=3)
ax.text(0, -1.13, f"Code {encode(edges)}", ha="center", fontsize=11, color=colour)
ax.set_xlim(-1.55, 1.55)
ax.set_ylim(-1.45, 1.2)
ax.set_aspect("equal")
ax.axis("off")
fig.suptitle("Four fixed labels: 16 trees, only 2 shapes", y=0.975, fontsize=17, color="#243746")
fig.text(0.5, 0.927, "Top row: 4 stars (choose the centre). Lower rows: 12 paths (4! / 2).",
ha="center", fontsize=12)
fig.text(0.5, 0.035, "Equal-label codes give stars; unequal-label codes give paths. Each ordered pair occurs once.",
ha="center", fontsize=11)
fig.subplots_adjust(left=0.035, right=0.965, top=0.885, bottom=0.085, hspace=0.15, wspace=0.12)
fig.savefig(Path.cwd() / Path(__file__).with_suffix(".png").name, facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()