diff --git a/papers/medial_tire_decompositions_of_plane_triangulations/experiments/kempe_valid_colorings.py b/papers/medial_tire_decompositions_of_plane_triangulations/experiments/kempe_valid_colorings.py new file mode 100644 index 0000000..f6f73c6 --- /dev/null +++ b/papers/medial_tire_decompositions_of_plane_triangulations/experiments/kempe_valid_colorings.py @@ -0,0 +1,361 @@ +"""Enumerate proper 3-colourings of a full medial tire graph and label them +valid / invalid by a Kempe-parity rule. + +For a proper 3-colouring of M(T) and a colour pair P = {a, b}, the subgraph +induced by the vertices coloured a or b splits into connected components, the +P-Kempe chains. Every vertex coloured a or b lies on exactly one P-Kempe +chain, so "lies on some P-Kempe chain" is the same as "coloured a or b". + +A *valid face* is the single outer (unbounded) face, or an inner face that is +not a tooth -- equivalently the faces of B(T) other than tooth triangles: the +root face plus one inner-gap face per bite (Remark 3.8). A colouring is VALID +when, for every valid face F and every colour pair P = {a, b}, the number of +non-bite *apex* vertices incident to F that lie on a P-Kempe chain (i.e. are +coloured a or b) is even. The count is over the whole pair P on the face, not +per individual chain. + +The non-bite apex vertices on the boundary of each valid face are: + + * outer face: every up-tooth apex; + * an inner non-tooth face F: the singleton down-tooth apexes whose edge lies + on F's arc. + +A singleton down apex on edge m lies on the inner face of the innermost bite +(i, j) with i < m < j, else the root face. Annular vertices and bite apexes are +never counted. + +The rule is invariant under permuting the three colours, so colourings may be +reduced modulo the six colour permutations without affecting validity. +""" + +from __future__ import annotations + +import argparse +import math +import os +from dataclasses import dataclass +from typing import Iterator + +from full_medial_tire_generator import ( + FullMedialTireGraph, + face_singleton_counts, + generate, + innermost_bite, +) + +HERE = os.path.dirname(os.path.abspath(__file__)) + +Coloring = dict[str, int] +COLOR_PAIRS = ((0, 1), (0, 2), (1, 2)) + + +def adjacency(graph: FullMedialTireGraph) -> dict[str, set[str]]: + adj: dict[str, set[str]] = {v: set() for v in graph.vertices()} + for u, v in graph.edges(): + adj[u].add(v) + adj[v].add(u) + return adj + + +def proper_3_colorings(graph: FullMedialTireGraph) -> Iterator[Coloring]: + """Every proper 3-colouring of M(T), by backtracking (annular cycle first).""" + adj = adjacency(graph) + # colour the heavily-constrained annular cycle first, then the apexes + order = [f"a{k}" for k in range(graph.n)] + order += [v for v in graph.vertices() if not v.startswith("a")] + coloring: Coloring = {} + + def rec(idx: int) -> Iterator[Coloring]: + if idx == len(order): + yield dict(coloring) + return + v = order[idx] + used = {coloring[w] for w in adj[v] if w in coloring} + for c in (0, 1, 2): + if c in used: + continue + coloring[v] = c + yield from rec(idx + 1) + coloring.pop(v, None) + + yield from rec(0) + + +def kempe_components( + adj: dict[str, set[str]], coloring: Coloring, pair: tuple[int, int] +) -> list[set[str]]: + """Connected components of the subgraph induced by the two colours in pair.""" + members = {v for v, c in coloring.items() if c in pair} + seen: set[str] = set() + components: list[set[str]] = [] + for start in members: + if start in seen: + continue + stack = [start] + comp: set[str] = set() + seen.add(start) + while stack: + v = stack.pop() + comp.add(v) + for w in adj[v]: + if w in members and w not in seen: + seen.add(w) + stack.append(w) + components.append(comp) + return components + + +def valid_faces(graph: FullMedialTireGraph) -> dict[str, frozenset[str]]: + """Map each valid face to the set of non-bite *apex* vertices on it. + + Keys: "outer", "root", or "bite(i,j)". The outer face carries the up-tooth + apexes; each inner non-tooth face carries the singleton down-tooth apexes on + its arc. Annular vertices and bite apexes are never included. + """ + faces: dict[str, set[str]] = { + "outer": {f"u{i}" for i in graph.up_edges}, + "root": set(), + } + for i, j in graph.bites: + faces[f"bite({i},{j})"] = set() + + def inner_key(face) -> str: + return "root" if face is None else f"bite({face[0]},{face[1]})" + + for m in graph.singleton_down_edges: + faces[inner_key(innermost_bite(m, graph.bites))].add(f"d{m}") + + return {name: frozenset(verts) for name, verts in faces.items()} + + +@dataclass(frozen=True) +class Verdict: + valid: bool + reason: str = "" # "" when valid + pair: tuple | None = None # the offending colour pair {a,b} + apexes: frozenset = frozenset() # the non-bite apexes counted (odd) + face: str = "" # name of the offending valid face + + +def classify(graph: FullMedialTireGraph, coloring: Coloring) -> Verdict: + """Label a single proper 3-colouring valid / invalid by the Kempe rule. + + For each valid face and each colour pair {a,b}, the non-bite apex vertices + on that face coloured a or b must be even in number. On a violation the + Verdict records the offending face, pair, and that odd apex set. + """ + faces = valid_faces(graph) + + for face_name, face_apexes in faces.items(): + for pair in COLOR_PAIRS: + on_pair = frozenset(v for v in face_apexes if coloring[v] in pair) + if len(on_pair) % 2 != 0: + return Verdict( + False, + f"{len(on_pair)} non-bite apex vertices on the {face_name} " + f"lie on an {set(pair)}-Kempe chain (odd)", + pair, on_pair, face_name, + ) + return Verdict(True) + + +def canonical_coloring(graph: FullMedialTireGraph, coloring: Coloring) -> tuple: + """Representative modulo the six colour permutations (first-seen relabel).""" + order = graph.vertices() + remap: dict[int, int] = {} + out = [] + for v in order: + c = coloring[v] + if c not in remap: + remap[c] = len(remap) + out.append(remap[c]) + return tuple(out) + + +def classify_colorings( + graph: FullMedialTireGraph, dedup_colors: bool = True +) -> list[tuple[Coloring, Verdict]]: + """Enumerate all proper 3-colourings of ``graph`` and label each valid/invalid.""" + results: list[tuple[Coloring, Verdict]] = [] + seen: set[tuple] = set() + for coloring in proper_3_colorings(graph): + if dedup_colors: + key = canonical_coloring(graph, coloring) + if key in seen: + continue + seen.add(key) + results.append((coloring, classify(graph, coloring))) + return results + + +# --------------------------------------------------------------------------- +# Plotting. +# --------------------------------------------------------------------------- + +def _draw_colored(ax, graph: FullMedialTireGraph, coloring: Coloring, verdict: Verdict): + import matplotlib.pyplot as plt # noqa: F401 (backend already chosen in run) + + n = graph.n + palette = {0: "#e6550d", 1: "#3182bd", 2: "#31a354"} # three colour classes + + def ann_xy(k): + ang = math.pi / 2 - 2 * math.pi * k / n + return math.cos(ang), math.sin(ang) + + def mid_ang(i): + return math.pi / 2 - 2 * math.pi * (i + 0.5) / n + + pos: dict[str, tuple[float, float]] = {f"a{k}": ann_xy(k) for k in range(n)} + matched = graph.bite_edges + for i, tooth in enumerate(graph.tooth_word): + if tooth == "U": + r = 1.42 + pos[f"u{i}"] = (r * math.cos(mid_ang(i)), r * math.sin(mid_ang(i))) + elif i not in matched: + r = 0.58 + pos[f"d{i}"] = (r * math.cos(mid_ang(i)), r * math.sin(mid_ang(i))) + for i, j in sorted(graph.bites): + corners = [ann_xy(i), ann_xy((i + 1) % n), ann_xy(j), ann_xy((j + 1) % n)] + cx = sum(p[0] for p in corners) / 4.0 + cy = sum(p[1] for p in corners) / 4.0 + pos[f"p{i}_{j}"] = (cx * 0.82, cy * 0.82) + + for u, v in graph.edges(): + ax.plot([pos[u][0], pos[v][0]], [pos[u][1], pos[v][1]], + color="#bbbbbb", lw=0.5, zorder=1) + # annular cycle a little heavier + for k in range(n): + a, b = f"a{k}", f"a{(k + 1) % n}" + ax.plot([pos[a][0], pos[b][0]], [pos[a][1], pos[b][1]], + color="#666666", lw=1.0, zorder=2) + + # highlight the offending pair's Kempe chains (those carrying the counted + # apexes) on invalid colourings + if not verdict.valid and verdict.pair is not None: + pair = verdict.pair + highlight: set[str] = set() + for comp in kempe_components(adjacency(graph), coloring, pair): + if comp & verdict.apexes: + highlight |= comp + for u, v in graph.edges(): + if u in highlight and v in highlight and coloring[u] in pair and coloring[v] in pair: + ax.plot([pos[u][0], pos[v][0]], [pos[u][1], pos[v][1]], + color="#f5b800", lw=2.4, zorder=2.5, solid_capstyle="round") + + for v, (x, y) in pos.items(): + is_bite = v.startswith("p") + ax.scatter([x], [y], s=34 if is_bite else 24, color=palette[coloring[v]], + edgecolors="black", linewidths=0.5 if is_bite else 0.3, zorder=3) + + # ring the apex vertices whose odd count caused the violation + if not verdict.valid and verdict.apexes: + xs = [pos[v][0] for v in verdict.apexes] + ys = [pos[v][1] for v in verdict.apexes] + ax.scatter(xs, ys, s=120, facecolors="none", edgecolors="#d62728", + linewidths=1.8, zorder=4) + + ax.set_xlim(-1.65, 1.65) + ax.set_ylim(-1.85, 1.65) + ax.set_aspect("equal") + ax.axis("off") + color = "#2ca02c" if verdict.valid else "#d62728" + label = "VALID" if verdict.valid else "INVALID" + ax.set_title(label, fontsize=7, color=color, pad=1.5) + if not verdict.valid: + ax.text(0.5, -0.04, verdict.reason, transform=ax.transAxes, + ha="center", va="top", fontsize=4.6, color="#d62728") + + +def plot_all(graph: FullMedialTireGraph, results, path_png: str, path_pdf: str): + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + cols = 10 + rows = math.ceil(len(results) / cols) + fig, axes = plt.subplots(rows, cols, figsize=(cols * 1.5, rows * 1.65)) + axes = axes.reshape(rows, cols) + for idx in range(rows * cols): + ax = axes[idx // cols][idx % cols] + if idx < len(results): + coloring, verdict = results[idx] + _draw_colored(ax, graph, coloring, verdict) + else: + ax.axis("off") + + n_valid = sum(1 for _, v in results if v.valid) + bites = ",".join(f"({i},{j})" for i, j in sorted(graph.bites)) or "-" + fig.suptitle( + f"Proper 3-colourings of M(T): word={graph.tooth_word}, bites={bites}\n" + f"{len(results)} colourings (mod colour permutation) — " + f"{n_valid} valid, {len(results) - n_valid} invalid\n" + f"invalid panels: gold = offending pair's Kempe chains, " + f"red ring = the odd set of non-bite apexes on the offending face", + fontsize=11, y=0.998, + ) + fig.tight_layout(rect=(0, 0, 1, 0.97)) + fig.savefig(path_png, dpi=200) + fig.savefig(path_pdf) + print(f"wrote {path_png}") + print(f"wrote {path_pdf}") + + +def pick_demo_graph() -> FullMedialTireGraph: + """A small n=9 class that has a bite, at least one VALID colouring, and + (preferably) singleton down teeth sitting inside a bite's inner face, so the + plot shows both valid and invalid colourings and exercises a bite face.""" + best = None + fallback = None + for g in generate(9, dedup=True): + if not g.bites: + continue + fallback = fallback or g + n_valid = sum(1 for _, v in classify_colorings(g, dedup_colors=True) if v.valid) + if n_valid == 0: + continue + has_bite_face = any( + face is not None for face in face_singleton_counts(g.tooth_word, g.bites) + ) + key = (not has_bite_face, -n_valid, len(g.singleton_down_edges)) + if best is None or key < best[0]: + best = (key, g) + return best[1] if best else fallback + + +def run(args: argparse.Namespace) -> None: + graph = pick_demo_graph() + bites = ",".join(f"({i},{j})" for i, j in sorted(graph.bites)) or "-" + print(f"demo graph: n={graph.n} word={graph.tooth_word} bites={bites}") + print(f" up teeth: {graph.up_edges}") + print(f" singleton down teeth: {graph.singleton_down_edges}") + print(f" face singleton counts: {face_singleton_counts(graph.tooth_word, graph.bites)}") + + all_results = classify_colorings(graph, dedup_colors=False) + results = classify_colorings(graph, dedup_colors=True) + n_valid = sum(1 for _, v in results if v.valid) + print(f"proper 3-colourings: {len(all_results)} total, " + f"{len(results)} modulo colour permutation") + print(f" valid: {n_valid} invalid: {len(results) - n_valid}") + + # show a couple of invalid reasons for transparency + shown = 0 + for _, v in results: + if not v.valid and shown < 3: + print(f" invalid example: {v.reason}") + shown += 1 + + # valid first, then invalid, for a readable grid + results.sort(key=lambda cv: (not cv[1].valid,)) + png = os.path.join(HERE, "kempe_valid_colorings_demo.png") + pdf = os.path.join(HERE, "kempe_valid_colorings_demo.pdf") + plot_all(graph, results, png, pdf) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.parse_args() + run(parser.parse_args()) + + +if __name__ == "__main__": + main() diff --git a/papers/medial_tire_decompositions_of_plane_triangulations/experiments/kempe_valid_colorings_demo.pdf b/papers/medial_tire_decompositions_of_plane_triangulations/experiments/kempe_valid_colorings_demo.pdf new file mode 100644 index 0000000..5a09cc0 Binary files /dev/null and b/papers/medial_tire_decompositions_of_plane_triangulations/experiments/kempe_valid_colorings_demo.pdf differ diff --git a/papers/medial_tire_decompositions_of_plane_triangulations/experiments/kempe_valid_colorings_demo.png b/papers/medial_tire_decompositions_of_plane_triangulations/experiments/kempe_valid_colorings_demo.png new file mode 100644 index 0000000..7067d44 Binary files /dev/null and b/papers/medial_tire_decompositions_of_plane_triangulations/experiments/kempe_valid_colorings_demo.png differ diff --git a/papers/medial_tire_decompositions_of_plane_triangulations/paper.aux b/papers/medial_tire_decompositions_of_plane_triangulations/paper.aux index e845bb1..2a1025b 100644 --- a/papers/medial_tire_decompositions_of_plane_triangulations/paper.aux +++ b/papers/medial_tire_decompositions_of_plane_triangulations/paper.aux @@ -35,6 +35,10 @@ \newlabel{conj:medial-chain-pigeonhole}{{5.2}{7}} \newlabel{conj:medial-route-fct}{{5.3}{7}} \@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{5.1}{Kempe-cycle conservation across medial tires}}{7}{}\protected@file@percent } +\newlabel{lem:kempe-cycles}{{5.5}{7}} +\newlabel{lem:kempe-conservation}{{5.6}{8}} +\newlabel{def:kempe-balanced}{{5.7}{8}} +\newlabel{rem:kempe-balance-necessary}{{5.8}{8}} \bibcite{bauerfeld-nested-tire-decompositions}{1} \bibcite{tait-original}{2} \newlabel{tocindent-1}{0pt} diff --git a/papers/medial_tire_decompositions_of_plane_triangulations/paper.fdb_latexmk b/papers/medial_tire_decompositions_of_plane_triangulations/paper.fdb_latexmk index 6898b6e..8230c63 100644 --- a/papers/medial_tire_decompositions_of_plane_triangulations/paper.fdb_latexmk +++ b/papers/medial_tire_decompositions_of_plane_triangulations/paper.fdb_latexmk @@ -1,5 +1,5 @@ # Fdb version 3 -["pdflatex"] 1781194762 "paper.tex" "paper.pdf" "paper" 1781194763 +["pdflatex"] 1781207945 "paper.tex" "paper.pdf" "paper" 1781207946 "/usr/local/texlive/2022/texmf-dist/fonts/map/fontname/texfonts.map" 1577235249 3524 cb3e574dea2d1052e39280babc910dc8 "" "/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/cmextra/cmex7.tfm" 1246382020 1004 54797486969f23fa377b128694d548df "" "/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/cmextra/cmex8.tfm" 1246382020 988 bdf658c3bfc2d96d3c8b02cfc1c94c20 "" @@ -132,8 +132,8 @@ "/usr/local/texlive/2022/texmf-var/fonts/map/pdftex/updmap/pdftex.map" 1647878959 4410336 7d30a02e9fa9a16d7d1f8d037ba69641 "" "/usr/local/texlive/2022/texmf-var/web2c/pdftex/pdflatex.fmt" 1665017617 2826443 7e98410c533054b636c6470db83a27bc "" "/usr/local/texlive/2022/texmf.cnf" 1647878952 577 209b46be99c9075fd74d4c0369380e8c "" - "paper.aux" 1781194763 4035 146533a306519cb8688d4e85db1d3f80 "pdflatex" - "paper.tex" 1781194559 37363 b7b005cfaefc0e3a582f756b993a034e "" + "paper.aux" 1781207946 4206 a817291c83280f23be785ea9b9789717 "pdflatex" + "paper.tex" 1781207918 39941 f80d8bca5b99e67d65ad8e4bb2d30152 "" (generated) "paper.aux" "paper.log" diff --git a/papers/medial_tire_decompositions_of_plane_triangulations/paper.log b/papers/medial_tire_decompositions_of_plane_triangulations/paper.log index 4fc5435..bdc8dc2 100644 --- a/papers/medial_tire_decompositions_of_plane_triangulations/paper.log +++ b/papers/medial_tire_decompositions_of_plane_triangulations/paper.log @@ -1,4 +1,4 @@ -This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 11 JUN 2026 12:19 +This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 11 JUN 2026 15:59 entering extended mode restricted \write18 enabled. %&-line parsing enabled. @@ -508,10 +508,10 @@ LaTeX Warning: `h' float specifier changed to `ht'. [4] [5] [6] [7] [8] [9] [10] (./paper.aux) ) Here is how much of TeX's memory you used: - 14415 strings out of 478268 - 283664 string characters out of 5846347 - 609309 words of memory out of 5000000 - 32244 multiletter control sequences out of 15000+600000 + 14419 strings out of 478268 + 283755 string characters out of 5846347 + 609349 words of memory out of 5000000 + 32248 multiletter control sequences out of 15000+600000 477048 words of font info for 58 fonts, out of 8000000 for 9000 1302 hyphenation exceptions out of 8191 84i,8n,89p,736b,838s stack positions out of 10000i,1000n,20000p,200000b,200000s @@ -535,7 +535,7 @@ ts/cm/cmti10.pfb> -Output written on paper.pdf (10 pages, 272876 bytes). +Output written on paper.pdf (10 pages, 276272 bytes). PDF statistics: 135 PDF objects out of 1000 (max. 8388607) 84 compressed objects within 1 object stream diff --git a/papers/medial_tire_decompositions_of_plane_triangulations/paper.pdf b/papers/medial_tire_decompositions_of_plane_triangulations/paper.pdf index 4d69bcd..058fc07 100644 Binary files a/papers/medial_tire_decompositions_of_plane_triangulations/paper.pdf and b/papers/medial_tire_decompositions_of_plane_triangulations/paper.pdf differ diff --git a/papers/medial_tire_decompositions_of_plane_triangulations/paper.tex b/papers/medial_tire_decompositions_of_plane_triangulations/paper.tex index 65d0dfb..42a9ca8 100644 --- a/papers/medial_tire_decompositions_of_plane_triangulations/paper.tex +++ b/papers/medial_tire_decompositions_of_plane_triangulations/paper.tex @@ -727,7 +727,8 @@ Since $M$ is $4$-regular and $\varphi$ is proper, every vertex of $M_P$ has degree $2$ in $M_P$. Hence every component of $M_P$ is a cycle. We call these components the $P$-Kempe cycles of $\varphi$. -\begin{lemma}[Kempe cycles are cycles] +\begin{lemma}[Kempe chains are cycles] +\label{lem:kempe-cycles} Let $G$ be a plane triangulation, let $M=M(G)$, and let $\varphi$ be a proper $3$-colouring of $M$. For each $P\in\{\{1,2\},\{2,3\},\{3,1\}\}$, every component of $M_P$ is a cycle. @@ -763,6 +764,7 @@ in the tire tree. The following lemma is the basic conservation principle. \begin{lemma}[Kempe-cycle conservation across level cycles] +\label{lem:kempe-conservation} Let $C$ be a level cycle of $M$ separating a parent side from a child side. Let $K$ be a $P$-Kempe cycle for some $P\in\{\{1,2\},\{2,3\},\{3,1\}\}$. Then $K$ cannot enter the child side @@ -784,6 +786,56 @@ $C$. Thus every entrance through $C$ is paired with an exit through $C$. \end{proof} +We now use these Kempe cycles to single out the colourings of a full +medial tire graph that respect the annular tooth structure. + +\begin{definition}[Kempe-balanced colouring] +\label{def:kempe-balanced} +Let $\varphi$ be a proper $3$-colouring of the full medial tire graph +$\mathsf{M}(T)$. For a colour pair $P=\{a,b\}$, let $\mathsf{M}(T)_P$ be +the subgraph induced by the vertices of colours $a$ and $b$. Since +$\mathsf{M}(T)$ need not be $4$-regular, the components of +$\mathsf{M}(T)_P$ are paths or cycles; we call them the $P$-\emph{Kempe +chains} of $\varphi$. Every vertex of colour $a$ or $b$ lies on exactly +one $P$-Kempe chain. + +A \emph{valid face} is the outer face of $\mathsf{M}(T)$, or an interior +face of $B(T)$ that is not a tooth---namely the root face or a bite +inner-gap face of Remark~\ref{rem:bite-face-count}. The \emph{tooth +apexes incident to} a valid face $F$ are: +\begin{itemize} + \item the up-tooth apexes (Definition~\ref{def:annular-teeth}), when + $F$ is the outer face; + \item the singleton down-tooth apexes whose annular edge lies on $F$, + when $F$ is interior---the apex on annular edge $m$ being incident to + the innermost bite $(i,j)$ with $i