codex/mycielski-single-edge-five-cycle-colouring.py

mycielski-single-edge-five-cycle-colouring.py
"""Draw K2 -> mu(K2)=C5; Python 3.14, matplotlib 3.10.7, numpy 2.3.5.

Run from _media; saves the matching PNG to the current working directory.
"""

from itertools import combinations, product
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.lines import Line2D


PALETTE = ["#79b7e8", "#f2ae60", "#87cf9d"]
VERTICES = ["v1", "v2", "u1", "w", "u2"]
EDGES = [("v1", "v2"), ("v2", "u1"), ("u1", "w"), ("w", "u2"), ("u2", "v1")]
COLOURS = {"v1": 1, "v2": 2, "u1": 1, "u2": 2, "w": 3}


def verify():
    """Enumerate all labelled two- and three-colour assignments."""
    counts = {}
    for k in (2, 3):
        counts[k] = sum(
            all(c[VERTICES.index(a)] != c[VERTICES.index(b)] for a, b in EDGES)
            for c in product(range(k), repeat=5)
        )
    edge_set = {frozenset(e) for e in EDGES}
    triangles = sum(
        all(frozenset(e) in edge_set for e in combinations(t, 2))
        for t in combinations(VERTICES, 3)
    )
    assert counts == {2: 0, 3: 30}
    assert triangles == 0 and len(edge_set) == 5
    assert all(COLOURS[a] != COLOURS[b] for a, b in EDGES)
    return {"vertices": 5, "edges": 5, "triangles": triangles, "colouring_counts": counts}


def edge_style(a, b):
    if "w" in (a, b):
        return "#3c7b51", ":", 2.5
    if a.startswith("u") or b.startswith("u"):
        return "#68717d", "--", 2.2
    return "#283544", "-", 2.8


def draw(ax, positions, edges, colours):
    for a, b in edges:
        colour, style, width = edge_style(a, b)
        ax.plot(*zip(positions[a], positions[b]), color=colour, ls=style, lw=width, zorder=1)
    for name, p in positions.items():
        ax.scatter(*p, s=1050, color=PALETTE[colours[name] - 1], edgecolor="#283544", lw=1.5, zorder=3)
        ax.text(*p, str(colours[name]), ha="center", va="center", fontsize=14, weight="bold", zorder=4)
        offset = np.asarray(p) / (np.linalg.norm(p) or 1) * 0.44
        label = "$w$" if name == "w" else "$" + name[0] + "_" + name[1] + "$"
        ax.text(*(np.asarray(p) + offset), label, ha="center", va="center", fontsize=15)
    ax.set(xlim=(-1.55, 1.55), ylim=(-1.6, 1.6), aspect="equal")
    ax.axis("off")


def main():
    print(verify())
    plt.rcParams.update({"font.family": "DejaVu Sans", "font.size": 12})
    fig, axes = plt.subplots(1, 2, figsize=(8.4, 4.5), facecolor="white")
    draw(axes[0], {"v1": (-0.7, 0), "v2": (0.7, 0)}, [("v1", "v2")], COLOURS)
    axes[0].set_title(r"$K_2$: two colours", fontsize=15, pad=12)
    angles = np.pi / 2 - np.arange(5) * 2 * np.pi / 5
    positions = {v: (np.cos(t), np.sin(t)) for v, t in zip(VERTICES, angles)}
    draw(axes[1], positions, EDGES, COLOURS)
    axes[1].set_title(r"$\mu(K_2)=C_5$: three colours", fontsize=15, pad=12)
    fig.text(0.50, 0.54, r"$\mu$" + "\n" + r"$\longrightarrow$", ha="center", fontsize=21)
    legend = [Line2D([0], [0], color=c, ls=s, lw=w, label=l) for c, s, w, l in (
        ("#283544", "-", 2.8, "Original edge"),
        ("#68717d", "--", 2.2, "Shadow to original"),
        ("#3c7b51", ":", 2.5, "Apex to shadow"),
    )]
    fig.legend(handles=legend, loc="lower center", ncol=3, frameon=False, fontsize=11)
    fig.subplots_adjust(left=0.03, right=0.97, top=0.86, bottom=0.16, wspace=0.28)
    fig.savefig(Path.cwd() / (Path(__file__).stem + ".png"), dpi=140, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()