Compare commits

..

1 Commits

Author SHA1 Message Date
didericis d3fc4bfc4c Split medial pigeonhole programme into its own paper
Move Section 5 of "Medial Tire Decompositions of Plane Triangulations"
into a new standalone paper, "The Medial Pigeonhole Programme", which
cites the medial tire paper for its terminology and notation. Convert
the three cross-references that pointed into earlier sections (annular
teeth, bite-face-count, boundary medial vertices) into citations.

Remove Section 5 from the medial tire paper and update its abstract to
drop the moved chain-pigeonhole claim, pointing to the follow-up paper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 21:08:06 -04:00
86 changed files with 1697 additions and 9655 deletions
@@ -1,103 +0,0 @@
"""Experiment: is a given maximal planar graph in its own flip neighborhood?
The flip neighborhood N(G) of a maximal planar graph G (Definition
"flip-neighborhood" in paper.tex) is the set of maximal planar graphs obtainable
from G by a single admissible edge flip: pick an edge uv whose two incident
triangular faces are uvw and uvx, delete uv, and insert wx, provided wx is not
already an edge.
We call G a *self-flip-neighbor* iff some admissible flip G^flip(uv) is
isomorphic to G --- equivalently, G in N(G). A flip always changes the labelled
edge set (it removes uv and adds a non-edge wx), so this is a genuine question
about the isomorphism type: it asks whether a single diagonal flip can map the
triangulation back onto a copy of itself.
This module enumerates every admissible flip of G, tests each resulting
triangulation for isomorphism to G, and reports the witnessing edges. It can run
on a single graph (given as a graph6 string, or the icosahedron by default) or
survey every min-degree-5 maximal planar graph over a range of orders.
"""
import os, sys
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))) # repo root for `lib`
from typing import Iterator, Any, cast
from sage.all import Graph, graphs # type: ignore[attr-defined] # pylint: disable=no-name-in-module
# Reuse the flip machinery from the survey in this same directory.
from colored_edge_flip_class_survey import face_thirds, canonical_g6 # type: ignore[import]
def admissible_flips(g: Graph) -> Iterator[tuple[Any, Any, Any, Any, Graph]]:
"""Yield (u, v, w, x, H) for every admissible edge flip of g: uv is the
flipped edge, wx the inserted diagonal (a non-edge), and H = g^flip(uv)."""
pairs = face_thirds(g)
for u, v in list(g.edges(labels=False)):
thirds = pairs.get(frozenset((u, v)))
if thirds is None or len(thirds) != 2:
continue
w, x = thirds
if g.has_edge(w, x):
continue
h = g.copy()
h.delete_edge(u, v)
h.add_edge(w, x)
yield u, v, w, x, h
def self_flip_witnesses(g: Graph) -> list[tuple[Any, Any, Any, Any]]:
"""Return every (u, v, w, x) such that the admissible flip uv -> wx yields a
graph isomorphic to g. Non-empty iff g lies in its own flip neighborhood."""
target = canonical_g6(g)
witnesses: list[tuple[Any, Any, Any, Any]] = []
for u, v, w, x, h in admissible_flips(g):
if canonical_g6(h) == target:
witnesses.append((u, v, w, x))
return witnesses
def is_self_flip_neighbor(g: Graph) -> bool:
"""True iff g in N(g): some admissible flip of g is isomorphic to g."""
target = canonical_g6(g)
return any(canonical_g6(h) == target for *_e, h in admissible_flips(g))
def report_single(g: Graph) -> bool:
"""Print a per-edge flip report for g and return whether it self-flips."""
flips = list(admissible_flips(g))
witnesses = self_flip_witnesses(g)
print(f"graph6={g.graph6_string()} |V|={g.order()} |E|={g.size()} "
f"admissible flips={len(flips)} self-flip witnesses={len(witnesses)}")
for u, v, w, x in witnesses:
print(f" flip edge ({u},{v}) -> ({w},{x}) gives a graph isomorphic to G")
print(f" => G {'IS' if witnesses else 'is NOT'} in its own flip neighborhood")
return bool(witnesses)
def survey(min_order: int, max_order: int) -> None:
"""For each order in [min_order, max_order], count how many min-degree-5
maximal planar graphs are self-flip-neighbors."""
for n in range(min_order, max_order + 1):
gen = graphs.planar_graphs(
n, minimum_connectivity=3, maximum_face_size=3, minimum_degree=5
)
checked = 0
self_flip = 0
for g in gen:
checked += 1
if is_self_flip_neighbor(cast(Graph, g)):
self_flip += 1
print(f"order {n}: {checked} min-degree-5 maximal planar graphs, "
f"{self_flip} are self-flip-neighbors")
if __name__ == "__main__":
if len(sys.argv) >= 2 and sys.argv[1].lstrip("-").isdigit():
lo = int(sys.argv[1])
hi = int(sys.argv[2]) if len(sys.argv) > 2 else lo
survey(lo, hi)
else:
if len(sys.argv) >= 2:
graph = Graph(sys.argv[1])
else:
graph = graphs.IcosahedralGraph()
print("(no graph given; using the icosahedron)")
report_single(cast(Graph, graph))
+4 -8
View File
@@ -10,16 +10,12 @@
\newlabel{lem:edge-deletion-4colorable}{{4.2}{2}}
\newlabel{lem:edge-deletion-coloring-structure}{{4.3}{3}}
\newlabel{thm:flip-neighborhood-4colorable}{{4.4}{3}}
\@writefile{lof}{\contentsline {figure}{\numberline {2}{\ignorespaces Case\nonbreakingspace 2 of the proof of Theorem\nonbreakingspace 4.4\hbox {}: $u, v$ share color $a$ and $w, x$ share color $c$. The $\{a, b\}$-Kempe path $P$ from $u$ to $v$ separates $w$ from $x$ in the plane, so no $\{c, d\}$-path between $w$ and $x$ can avoid crossing $P$; since the color sets $\{a, b\}$ and $\{c, d\}$ are disjoint, no such path exists.}}{4}{}\protected@file@percent }
\newlabel{fig:flip-proof-case-two}{{2}{4}}
\newlabel{thm:no-colored-class-contains-G}{{4.5}{4}}
\@writefile{toc}{\contentsline {section}{\tocsection {}{5}{A computational aside: self-flip neighbors}}{4}{}\protected@file@percent }
\newlabel{def:self-flip-neighbor}{{5.1}{4}}
\newlabel{tocindent-1}{0pt}
\newlabel{tocindent0}{0pt}
\newlabel{tocindent1}{17.77782pt}
\newlabel{tocindent2}{0pt}
\newlabel{tocindent3}{0pt}
\@writefile{lot}{\contentsline {table}{\numberline {1}{\ignorespaces Self-flip neighbors among min-degree-$5$ maximal planar graphs. No graph on $n \leq 15$ vertices is a self-flip neighbor; beyond the first occurrences the fraction declines steadily.}}{5}{}\protected@file@percent }
\newlabel{tab:self-flip}{{1}{5}}
\gdef \@abspage@last{5}
\@writefile{lof}{\contentsline {figure}{\numberline {2}{\ignorespaces Case\nonbreakingspace 2 of the proof of Theorem\nonbreakingspace 4.4\hbox {}: $u, v$ share color $a$ and $w, x$ share color $c$. The $\{a, b\}$-Kempe path $P$ from $u$ to $v$ separates $w$ from $x$ in the plane, so no $\{c, d\}$-path between $w$ and $x$ can avoid crossing $P$; since the color sets $\{a, b\}$ and $\{c, d\}$ are disjoint, no such path exists.}}{4}{}\protected@file@percent }
\newlabel{fig:flip-proof-case-two}{{2}{4}}
\newlabel{thm:no-colored-class-contains-G}{{4.5}{4}}
\gdef \@abspage@last{4}
@@ -1,5 +1,5 @@
# Fdb version 3
["pdflatex"] 1781844089 "paper.tex" "paper.pdf" "paper" 1781844090
["pdflatex"] 1778743331 "paper.tex" "paper.pdf" "paper" 1778743331
"/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 ""
@@ -131,8 +131,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" 1781844090 2204 9c1b0b970c8aeef1caf450ea82c0c00d "pdflatex"
"paper.tex" 1781844079 17938 6757143b0e59891047a9dd2db3f626cf ""
"paper.aux" 1778743331 1709 057e58fcb5472314b0a7029f2c0f7505 "pdflatex"
"paper.tex" 1778743323 14730 0431b5dd1f68c135b8365d9286869b8f ""
(generated)
"paper.aux"
"paper.log"
+29 -33
View File
@@ -1,4 +1,4 @@
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 19 JUN 2026 00:41
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 14 MAY 2026 03:22
entering extended mode
restricted \write18 enabled.
%&-line parsing enabled.
@@ -486,43 +486,39 @@ File: epstopdf-sys.cfg 2010/07/13 v1.3 Configuration of (r)epstopdf for TeX Liv
e
))
[1{/usr/local/texlive/2022/texmf-var/fonts/map/pdftex/updmap/pdftex.map}]
[2] [3]
LaTeX Warning: `h' float specifier changed to `ht'.
[4] [5] (./paper.aux) )
[2] [3] [4] (./paper.aux) )
Here is how much of TeX's memory you used:
13208 strings out of 478268
266448 string characters out of 5846347
541829 words of memory out of 5000000
31043 multiletter control sequences out of 15000+600000
13206 strings out of 478268
266409 string characters out of 5846347
540812 words of memory out of 5000000
31041 multiletter control sequences out of 15000+600000
477211 words of font info for 59 fonts, out of 8000000 for 9000
1302 hyphenation exceptions out of 8191
100i,9n,104p,495b,794s stack positions out of 10000i,1000n,20000p,200000b,200000s
</usr/local/texlive/2022/texmf-dist/fonts/type1/public/a
msfonts/cm/cmbx10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/am
sfonts/cm/cmcsc10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/am
sfonts/cm/cmex10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/ams
fonts/cm/cmmi10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsf
onts/cm/cmmi5.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfon
ts/cm/cmmi7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts
/cm/cmmi8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/c
m/cmmi9.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/
cmr10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cm
r6.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr7.
pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr8.pfb
></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr9.pfb></
usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy10.pfb></u
sr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy7.pfb></usr
/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy8.pfb></usr/l
ocal/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy9.pfb></usr/loc
al/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti10.pfb></usr/loca
l/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti8.pfb></usr/local/
texlive/2022/texmf-dist/fonts/type1/public/amsfonts/symbols/msam10.pfb>
Output written on paper.pdf (5 pages, 251916 bytes).
</usr/local/texlive/2022/texmf-dist/fonts/type1/publ
ic/amsfonts/cm/cmbx10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/publi
c/amsfonts/cm/cmcsc10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/publi
c/amsfonts/cm/cmex10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public
/amsfonts/cm/cmmi10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/
amsfonts/cm/cmmi5.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/am
sfonts/cm/cmmi7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsf
onts/cm/cmmi8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfon
ts/cm/cmmi9.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts
/cm/cmr10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/c
m/cmr6.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/c
mr7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr8
.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr9.pf
b></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy10.pfb
></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy7.pfb><
/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy8.pfb></u
sr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy9.pfb></usr
/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti10.pfb></usr/
local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti8.pfb></usr/lo
cal/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/symbols/msam10.pfb>
Output written on paper.pdf (4 pages, 246274 bytes).
PDF statistics:
123 PDF objects out of 1000 (max. 8388607)
75 compressed objects within 1 object stream
120 PDF objects out of 1000 (max. 8388607)
73 compressed objects within 1 object stream
0 named destinations out of 1000 (max. 500000)
13 words of extra memory for PDF output out of 10000 (max. 10000000)
Binary file not shown.
@@ -371,79 +371,6 @@ in particular, $\varphi$ is a proper $4$-coloring of $H_k = G$. But
$\chi(G) \geq 5$ admits no such coloring, a contradiction.
\end{proof}
\section{A computational aside: self-flip neighbors}
Since $\mathcal{N}(G)$ is defined up to isomorphism, it is natural to
ask whether $G$ can be one of its own flip neighbors.
\begin{definition}[Self-flip neighbor]\label{def:self-flip-neighbor}
A maximal planar graph $G$ is a \emph{self-flip neighbor} if
$G \in \mathcal{N}(G)$; that is, if some admissible edge flip
$G^{\mathrm{flip}(uv)}$ is isomorphic to $G$.
\end{definition}
A single flip always changes the labelled edge set --- it deletes
$uv$ and inserts a non-edge $wx$ --- so the property is genuinely one
of the isomorphism type: it asks whether a diagonal flip can carry the
triangulation back onto a copy of itself. Equivalently, the degree
changes induced by the flip ($-1$ at $u$ and $v$, $+1$ at $w$ and $x$)
must be undone by an automorphism of the resulting graph.
We surveyed every maximal planar graph of minimum degree $5$ on
$n \leq 25$ vertices, counting those that are self-flip neighbors.
The results are recorded in Table~\ref{tab:self-flip}.
\begin{table}[h]
\centering
\begin{tabular}{rrrr}
\hline
$n$ & min-degree-$5$ & self-flip & fraction \\
& triangulations & neighbors & \\
\hline
$12$ & $1$ & $0$ & $0\%$ \\
$13$ & $0$ & $0$ & --- \\
$14$ & $1$ & $0$ & $0\%$ \\
$15$ & $1$ & $0$ & $0\%$ \\
$16$ & $3$ & $1$ & $33.3\%$ \\
$17$ & $4$ & $1$ & $25.0\%$ \\
$18$ & $12$ & $2$ & $16.7\%$ \\
$19$ & $23$ & $5$ & $21.7\%$ \\
$20$ & $73$ & $12$ & $16.4\%$ \\
$21$ & $192$ & $27$ & $14.1\%$ \\
$22$ & $651$ & $51$ & $7.8\%$ \\
$23$ & $2070$ & $120$ & $5.8\%$ \\
$24$ & $7290$ & $273$ & $3.7\%$ \\
$25$ & $25381$ & $598$ & $2.4\%$ \\
\hline
\end{tabular}
\caption{Self-flip neighbors among min-degree-$5$ maximal planar
graphs. No graph on $n \leq 15$ vertices is a self-flip neighbor;
beyond the first occurrences the fraction declines steadily.}
\label{tab:self-flip}
\end{table}
\begin{remark}
The proportion of self-flip neighbors declines as $n$ grows --- from a
peak near a third at $n = 16$ to roughly $2\%$ at $n = 25$ --- and the
trend is the one to expect. A self-flip neighbor requires a flip whose
induced degree changes are reversed by an automorphism of the flipped
graph, but min-degree-$5$ triangulations are asymptotically rigid: the
share with a nontrivial automorphism group, let alone one of the
required form, tends to $0$. The absolute count of self-flip neighbors
continues to grow, but ever more slowly than the census itself, so the
fraction appears to vanish in the limit.
This observation is not, by itself, useful for narrowing down the
minimal criminal $G_0$. Whether or not $G_0$ is a self-flip neighbor,
Theorem~\ref{thm:flip-neighborhood-4colorable} already shows every
graph in $\mathcal{N}(G_0)$ is $4$-colorable; self-flip neighborness
is a generic structural feature of the surrounding census rather than a
property forced on, or forbidden of, a minimum counterexample. It
neither includes nor excludes any candidate, and so contributes nothing
to the elimination program beyond confirming that the flip operation
behaves on min-degree-$5$ triangulations as one would anticipate.
\end{remark}
\end{document}
%-----------------------------------------------------------------------
@@ -1,182 +0,0 @@
"""
Does the richness invariant survive BRANCHING?
For a separating cycle C bounding a disk G_C (away from the source), the achievable
outer-interface set is
Phi(C) = { (lambda*(v))_{v in C} : lambda in {+1,-1}^{F(G_C)},
sum_{f ∋ w} lambda(f) ≡ 0 for every
truly-interior vertex w of G_C }.
This is the exact value the recursive transfer operator produces at C (interior
consistency = all the descendant gluings already performed; seam/boundary vertices
are deferred, exactly as in the recursion). We compute Phi(C) by constrained
enumeration over real triangulations and test the candidate self-similar invariant
non-empty & closed under sign flip & full single-position marginals
separately at BRANCH nodes (region encloses >=2 disjoint deeper sub-tires) and at
LINEAR nodes (one child), to see whether branching breaks it.
"""
import sys
from collections import defaultdict, deque
from itertools import product
import numpy as np
from scipy.spatial import Delaunay
def delaunay(n, rng):
pts = rng.random((n, 2))
tri = Delaunay(pts)
faces = [tuple(int(x) for x in s) for s in tri.simplices]
hull = set(int(v) for e in tri.convex_hull for v in e)
return faces, hull
def build(faces):
adj = defaultdict(set)
efaces = defaultdict(list)
vfaces = defaultdict(list)
for fi, (a, b, c) in enumerate(faces):
adj[a] |= {b, c}; adj[b] |= {a, c}; adj[c] |= {a, b}
for e in ((a, b), (b, c), (a, c)):
efaces[frozenset(e)].append(fi)
for v in (a, b, c):
vfaces[v].append(fi)
fadj = [set() for _ in faces]
for fl in efaces.values():
for i in fl:
for j in fl:
if i != j:
fadj[i].add(j)
return adj, fadj, vfaces
def bfs(adj, src):
lev = {src: 0}; q = deque([src])
while q:
u = q.popleft()
for w in adj[u]:
if w not in lev:
lev[w] = lev[u] + 1; q.append(w)
return lev
def components(face_ids, fadj):
idset = set(face_ids)
seen = set(); comps = []
for s in face_ids:
if s in seen:
continue
comp = []; stack = [s]; seen.add(s)
while stack:
u = stack.pop(); comp.append(u)
for w in fadj[u]:
if w in idset and w not in seen:
seen.add(w); stack.append(w)
comps.append(comp)
return comps
def sign_closed(S):
return all(tuple((3 - x) % 3 for x in s) in S for s in S)
def marginals_full(S, k):
return all({s[i] for s in S} == {0, 1, 2} for i in range(k))
def phi_of_region(comp_faces, faces, vfaces, lev, d, cap):
"""Constrained-enumeration Phi on the outer (level-d) cycle of a region."""
Gc = comp_faces
if not (1 <= len(Gc) <= cap):
return None
Gcset = set(Gc)
verts = sorted(set(v for fi in Gc for v in faces[fi]))
# truly-interior: every global incident face is inside G_C (=> level > d)
interior = [v for v in verts if all(f in Gcset for f in vfaces[v])]
boundary_C = [v for v in verts if lev[v] == d and v not in interior]
if not boundary_C:
return None
F = len(Gc)
fidx = {fi: j for j, fi in enumerate(Gc)}
# incidence rows
Bint = np.zeros((len(interior), F), dtype=np.int64)
for r, w in enumerate(interior):
for fi in vfaces[w]:
if fi in Gcset:
Bint[r, fidx[fi]] = 1
Cinc = np.zeros((len(boundary_C), F), dtype=np.int64)
for r, v in enumerate(boundary_C):
for fi in vfaces[v]:
if fi in Gcset:
Cinc[r, fidx[fi]] = 1
labs = np.array(list(product((1, 2), repeat=F)), dtype=np.int64)
if len(interior):
ok = np.all((labs @ Bint.T) % 3 == 0, axis=1)
labs = labs[ok]
if labs.shape[0] == 0:
return set(), len(boundary_C)
outer = (labs @ Cinc.T) % 3
return set(map(tuple, np.unique(outer, axis=0))), len(boundary_C)
def main():
seed = int(sys.argv[1]) if len(sys.argv) > 1 else 0
nprng = np.random.default_rng(seed)
CAP = 18
stats = {True: [0, 0, 0, 0], False: [0, 0, 0, 0]} # branch: [n, nonempty, sign, marg]
examples_fail = []
for _ in range(300):
faces, hull = delaunay(int(nprng.integers(14, 34)), nprng)
adj, fadj, vfaces = build(faces)
lev = bfs(adj, min(hull))
if len(lev) != len(adj):
continue
depth = [min(lev[v] for v in faces[fi]) for fi in range(len(faces))]
maxd = max(depth)
for d in range(1, maxd + 1):
fge = [fi for fi in range(len(faces)) if depth[fi] >= d]
for comp in components(fge, fadj):
if not (1 <= len(comp) <= CAP):
continue
deeper = [fi for fi in comp if depth[fi] >= d + 1]
n_children = len(components(deeper, fadj))
is_branch = n_children >= 2
res = phi_of_region(comp, faces, vfaces, lev, d, CAP)
if res is None:
continue
S, k = res
rec = stats[is_branch]
rec[0] += 1
rec[1] += bool(S)
rec[2] += (bool(S) and sign_closed(S))
rec[3] += (bool(S) and marginals_full(S, k))
if S and not marginals_full(S, k) and len(examples_fail) < 6:
examples_fail.append((is_branch, n_children, len(comp), k,
len(S)))
for branch in (False, True):
n, ne, sg, mg = stats[branch]
tag = "BRANCH (>=2 children)" if branch else "LINEAR (1 child)"
if n:
print(f"{tag}: {n} regions")
print(f" non-empty : {ne}/{n} ({100*ne/n:.1f}%)")
print(f" sign-closed : {sg}/{n} ({100*sg/n:.1f}%)")
print(f" marginals-full : {mg}/{n} ({100*mg/n:.1f}%)")
else:
print(f"{tag}: 0 regions")
if examples_fail:
print("\n marginals-NOT-full examples (branch?,n_children,|G_C|,|C|,|Phi|):")
for e in examples_fail:
print(f" {e}")
else:
print("\n richness (incl. full marginals) held on every region tested.")
if __name__ == "__main__":
main()
@@ -1,94 +0,0 @@
"""
Robustness check for the 2^(n-2) constraint floor over DIVERSE (not just stacked)
triangulated disks.
maximally_constrain.py searches Apollonian-stacked disks only, which miss
non-stacked triangulations (e.g. a wheel with a high-degree center). Here we
generate disks from random interior points + Delaunay, with boundary points in
convex but NON-cocircular position (cocircular boundary points are a Delaunay
degeneracy that yields INVALID disks -- missing a boundary edge -- and spuriously
report sub-floor |Phi|). Every disk is validated (2k+n-2 faces, all n boundary
edges present) before Phi is computed.
Finding: min |Phi| over validated diverse disks is exactly 2^(n-2), attained by
the interior-free triangulation; deeper structure never goes below it (and the
central-apex wheel actually ENLARGES Phi: 5 vs the fan's 4 on the 4-cycle).
"""
import sys
from collections import Counter
from itertools import product
import numpy as np
from scipy.spatial import Delaunay
np.seterr(all="ignore")
def disk(n, k, rng):
ang = 2 * np.pi * np.arange(n) / n
rad = 1.0 + 0.15 * rng.random(n) # convex but not cocircular
bpts = np.c_[rad * np.cos(ang), rad * np.sin(ang)]
if k:
r = 0.75 * np.sqrt(rng.random(k)); t = 2 * np.pi * rng.random(k)
ipts = np.c_[r * np.cos(t), r * np.sin(t)]
pts = np.vstack([bpts, ipts])
else:
pts = bpts
tri = Delaunay(pts)
return [tuple(int(x) for x in s) for s in tri.simplices]
def valid(faces, n, k):
if len(faces) != 2 * k + n - 2:
return False
ec = Counter()
for a, b, c in faces:
for e in ((a, b), (b, c), (a, c)):
ec[frozenset(e)] += 1
return all(ec[frozenset((i, (i + 1) % n))] == 1 for i in range(n))
def phi(faces, n, k):
F = len(faces)
interior = list(range(n, n + k))
Bint = np.zeros((len(interior), F), dtype=np.int64)
Cinc = np.zeros((n, F), dtype=np.int64)
for j, (a, b, c) in enumerate(faces):
for v in (a, b, c):
if v >= n:
Bint[interior.index(v), j] = 1
else:
Cinc[v, j] = 1
labs = np.array(list(product((1, 2), repeat=F)), dtype=np.int64)
if interior:
labs = labs[np.all((labs @ Bint.T) % 3 == 0, axis=1)]
if labs.shape[0] == 0:
return set()
return set(map(tuple, np.unique((labs @ Cinc.T) % 3, axis=0)))
def main():
ns = [int(x) for x in sys.argv[1:]] or [4, 5, 6]
rng = np.random.default_rng(1)
print("Min |Phi| over validated diverse (Delaunay) disks\n")
for n in ns:
best = 10 ** 9; bk = None; nval = 0; max_seen = 0
for k in range(0, 7):
for _ in range(250):
faces = disk(n, k, rng)
if not valid(faces, n, k) or len(faces) > 20:
continue
nval += 1
P = phi(faces, n, k)
if P:
max_seen = max(max_seen, len(P))
if len(P) < best:
best = len(P); bk = k
print(f" n={n}: {nval} valid disks min|Phi|={best} (k={bk}) "
f"max|Phi|={max_seen} 2^(n-2)={2**(n-2)} "
f"below-floor={best < 2**(n-2)}")
if __name__ == "__main__":
main()
@@ -1,134 +0,0 @@
"""
The open case for the 2^(n-2) lower bound is the IRREDUCIBLE disk: k>=1 interior
vertices, all of degree >=4 (un-stacking already settles everything reducible to
a degree-3 vertex). This probe isolates irreducible disks and asks:
(a) does the floor |Phi| >= 2^(n-2) hold there (it must, but it's the open case);
(b) is it STRICT (|Phi| > 2^(n-2)) -- if irreducible disks never sit ON the floor,
the proof only needs ">= floor" via "any nontrivial slack";
(c) how many UNIVERSAL toggles each has -- a "boundary-only" face (all 3 vertices
on C) can be flipped feasibly regardless of the labelling, giving a guaranteed
doubling. n-2 independent ones would prove the bound outright. We count them
to see whether universal toggles alone can carry the irreducible case (a wheel
has zero, so we expect NOT).
"""
import sys
from collections import Counter
from itertools import product
import numpy as np
from scipy.spatial import Delaunay
np.seterr(all="ignore")
def disk(n, k, rng):
ang = 2 * np.pi * np.arange(n) / n
rad = 1.0 + 0.18 * rng.random(n)
bpts = np.c_[rad * np.cos(ang), rad * np.sin(ang)]
r = 0.78 * np.sqrt(rng.random(k)); t = 2 * np.pi * rng.random(k)
pts = np.vstack([bpts, np.c_[r * np.cos(t), r * np.sin(t)]])
return [tuple(int(x) for x in s) for s in Delaunay(pts).simplices]
def valid(faces, n, k):
if len(faces) != 2 * k + n - 2:
return False
ec = Counter()
for a, b, c in faces:
for e in ((a, b), (b, c), (a, c)):
ec[frozenset(e)] += 1
return all(ec[frozenset((i, (i + 1) % n))] == 1 for i in range(n))
def interior_degrees(faces, n):
deg = Counter()
for f in faces:
for v in f:
if v >= n:
deg[v] += 1
return deg
def phi(faces, n):
interior = sorted(v for f in faces for v in f if v >= n)
interior = sorted(set(interior))
F = len(faces)
if F > 18:
return None
Bint = np.zeros((len(interior), F), dtype=np.int64)
Cinc = np.zeros((n, F), dtype=np.int64)
iidx = {w: r for r, w in enumerate(interior)}
for j, (a, b, c) in enumerate(faces):
for v in (a, b, c):
if v >= n:
Bint[iidx[v], j] = 1
else:
Cinc[v, j] = 1
labs = np.array(list(product((1, 2), repeat=F)), dtype=np.int64)
if interior:
labs = labs[np.all((labs @ Bint.T) % 3 == 0, axis=1)]
if labs.shape[0] == 0:
return set()
return set(map(tuple, np.unique((labs @ Cinc.T) % 3, axis=0)))
def boundary_only_faces(faces, n):
return sum(1 for (a, b, c) in faces if a < n and b < n and c < n)
def main():
seed = int(sys.argv[1]) if len(sys.argv) > 1 else 0
rng = np.random.default_rng(seed)
by_n = {}
n_irred = 0
floor_fail = 0
on_floor = 0 # irreducible AND exactly at 2^(n-2)
bof_ge = 0 # irreducible with >= n-2 boundary-only faces
bof_zero = 0
min_bof = 99
for _ in range(6000):
n = int(rng.integers(4, 7)); k = int(rng.integers(1, 4))
faces = disk(n, k, rng)
if not valid(faces, n, k) or len(faces) > 16:
continue
deg = interior_degrees(faces, n)
if any(deg[v] < 4 for v in deg) or len(deg) < k:
continue # reducible (has a degree-3 vertex)
P = phi(faces, n)
if P is None or not P:
continue
n_irred += 1
floor = 2 ** (n - 2)
if len(P) < floor:
floor_fail += 1
if len(P) == floor:
on_floor += 1
bof = boundary_only_faces(faces, n)
min_bof = min(min_bof, bof)
bof_ge += (bof >= n - 2)
bof_zero += (bof == 0)
d = by_n.setdefault(n, {"cnt": 0, "min": 10**9, "floor": floor})
d["cnt"] += 1
d["min"] = min(d["min"], len(P))
print(f"irreducible disks (k>=1, all interior degree >=4): {n_irred}\n")
for n in sorted(by_n):
d = by_n[n]
print(f" n={n}: {d['cnt']:5d} disks min|Phi|={d['min']} "
f"2^(n-2)={d['floor']} floor-held={d['min']>=d['floor']}")
print(f"\n floor violations (|Phi| < 2^(n-2)) : {floor_fail}")
print(f" irreducible disks sitting ON the floor: {on_floor} "
f"(if 0, irreducible => strictly above floor)")
print(f" universal toggles (boundary-only faces):")
print(f" >= n-2 such faces : {bof_ge}/{n_irred}")
print(f" exactly 0 : {bof_zero}/{n_irred} (min over all = {min_bof})")
print(" => universal toggles alone "
+ ("CAN" if bof_zero == 0 else "CANNOT")
+ " carry the irreducible case.")
if __name__ == "__main__":
main()
@@ -1,112 +0,0 @@
"""
Local heart of the size-reduction lemma.
Removing a degree-d interior vertex v swaps its star (d faces, contribution
mu_{j-1}+mu_j at link vertex u_j, plus the v-constraint sum mu ≡ 0) for a fan
(d-2 faces). Outside is shared, entering only as prescribed residues t_j at the
INTERIOR link vertices. The boundary-link contribution sets are
Star(t) = {(mu_{j-1}+mu_j)_{u_j boundary} : mu in {+-1}^d, sum mu ≡ 0,
mu_{j-1}+mu_j ≡ t_j for interior u_j}
Fan_r(t)= {(fan_j)_{u_j boundary} : nu in {+-1}^{d-2},
fan_j ≡ t_j for interior u_j}
(fan rooted at link vertex r; fan_j = contribution of the fan to u_j)
The reduction may CHOOSE the fan root, so it wants min_r |Fan_r(t)|. For the
induction |Phi(D-v)| <= |Phi(D)| to be locally supported we need, for every
interior-mask and every t:
|Star(t)| >= min_r |Fan_r(t)| (and Star(t) nonempty whenever some Fan is)
We check this exhaustively for small d. (+-1 encoded as 1,2 mod 3.)
"""
import sys
from itertools import product
def star_contrib(d, interior, t):
"""interior: set of link indices that are interior; t: dict j->residue for
interior j. Returns set of boundary contribution tuples."""
bnd = [j for j in range(d) if j not in interior]
out = set()
for mu in product((1, 2), repeat=d): # +-1 as 1,2
if sum(mu) % 3 != 0: # v-constraint
continue
contrib = [(mu[(j - 1) % d] + mu[j]) % 3 for j in range(d)]
if any(contrib[j] != t[j] for j in interior):
continue
out.add(tuple(contrib[j] for j in bnd))
return out
def fan_contrib(d, root, interior, t):
"""Fan of the link d-gon rooted at vertex `root`. Reindex so root=0."""
bnd = [j for j in range(d) if j not in interior]
# contribution of a fan (root r) to vertex u_j, for nu over the d-2 fan faces.
# In root-0 coordinates faces are (0,j,j+1) for j=1..d-2 with labels nu[1..d-2].
# contribution: u0 -> sum(nu); u1 -> nu1; u_{d-1} -> nu_{d-2};
# u_j (1<j<d-1) -> nu_{j-1}+nu_j.
out = set()
for nu in product((1, 2), repeat=d - 2): # nu indexed 1..d-2 -> 0..d-3
nuf = {j: nu[j - 1] for j in range(1, d - 1)}
contrib = [0] * d
# in root coordinates u'_i = u_{(root+i) mod d}
c = [0] * d
c[0] = sum(nu) % 3
c[1] = nuf[1] % 3
c[d - 1] = nuf[d - 2] % 3
for j in range(2, d - 1):
c[j] = (nuf[j - 1] + nuf[j]) % 3
# map back: actual vertex = (root+i) mod d gets c[i]
for i in range(d):
contrib[(root + i) % d] = c[i]
if any(contrib[j] != t[j] for j in interior):
continue
out.add(tuple(contrib[j] for j in bnd))
return out
def main():
print("Local claim: |Star(t)| >= min_root |Fan_root(t)| for all interior-masks, t\n")
fails = 0
checked = 0
worst = None
for d in range(3, 8):
for im in range(0, 1 << d):
interior = {j for j in range(d) if im >> j & 1}
if len(interior) == d:
continue # need a boundary vertex to see
int_list = sorted(interior)
for tvals in product((0, 1, 2), repeat=len(interior)):
t = dict(zip(int_list, tvals))
S = star_contrib(d, interior, t)
fans = [fan_contrib(d, r, interior, t) for r in range(d)]
# the reduction needs Star nonempty whenever it removes v; and
# it picks the fan, so compare to the SMALLEST fan that is itself
# achievable (nonempty) -- an empty fan means that root is invalid.
nonempty_fans = [len(f) for f in fans if f]
if not S:
# star infeasible: then v cannot carry this context. Only a
# problem if some fan IS feasible (then D-v has a seq D lacks).
if nonempty_fans:
fails += 1
continue
checked += 1
if nonempty_fans:
mn = min(nonempty_fans)
if len(S) < mn:
fails += 1
if worst is None or len(S) - mn < worst[0]:
worst = (len(S) - mn, d, sorted(interior), t,
len(S), mn)
print(f" configurations checked: {checked}")
print(f" violations of |Star| >= min nonempty |Fan|: {fails}")
if worst:
print(f" worst gap (|Star|-minFan, d, interior, t, |Star|, minFan): {worst}")
if fails == 0:
print(" => LOCAL CLAIM HOLDS: the star always dominates the best fan.")
if __name__ == "__main__":
main()
@@ -1,167 +0,0 @@
"""
Construct the triangulated disk (= nested tire substructure) that MAXIMALLY
constrains its outer cycle.
For a triangulated disk D with boundary cycle C = (0..n-1), the achievable outer
Heawood set is
Phi(D) = { (lambda*(v))_{v in C} : lambda in {+1,-1}^{faces},
sum_{f ∋ w} lambda(f) ≡ 0 for every interior vertex w } .
Phi depends only on the disk triangulation (no BFS/tree needed). We want the disk
minimising |Phi| -- the worst case for the pigeonhole. Note Phi is always
sign-closed and non-empty, so |Phi| >= 1, and |Phi| = 1 forces Phi = { all-zeros }.
Key local fact: a degree-3 interior vertex (one Apollonian stack) has incident
faces f1,f2,f3 with lambda(f1)+lambda(f2)+lambda(f3) ≡ 0 mod 3 over +/-1 values,
which forces f1=f2=f3. So stacking chains equalities and collapses Phi.
We (a) randomly search disks built by Apollonian stacking, and (b) try a
deterministic deep-stack construction, reporting the smallest Phi found.
"""
import random
import sys
from itertools import product
import numpy as np
def fan_triangulation(n):
"""n-gon (0..n-1) triangulated as a fan from vertex 0. No interior vertex."""
return [(0, i, i + 1) for i in range(1, n - 1)]
def stack(faces, idx, v):
a, b, c = faces[idx]
faces[idx] = (a, b, v)
faces.append((b, c, v))
faces.append((a, c, v))
def phi(faces, n, cap):
"""Phi on boundary 0..n-1; interior = vertices >= n."""
verts = set(v for f in faces for v in f)
interior = sorted(v for v in verts if v >= n)
F = len(faces)
if F > cap:
return None
# incidence
Bint = np.zeros((len(interior), F), dtype=np.int64)
iindex = {w: r for r, w in enumerate(interior)}
Cinc = np.zeros((n, F), dtype=np.int64)
for j, (a, b, c) in enumerate(faces):
for v in (a, b, c):
if v >= n:
Bint[iindex[v], j] = 1
else:
Cinc[v, j] = 1
labs = np.array(list(product((1, 2), repeat=F)), dtype=np.int64)
if len(interior):
keep = np.all((labs @ Bint.T) % 3 == 0, axis=1)
labs = labs[keep]
if labs.shape[0] == 0:
return set()
outer = (labs @ Cinc.T) % 3
return set(map(tuple, np.unique(outer, axis=0)))
def disp(s):
return tuple(-1 if int(x) == 2 else int(x) for x in s)
def gf3_rank(rows):
M = [[int(x) % 3 for x in r] for r in rows]
if not M:
return 0
nc = len(M[0]); r = 0
for c in range(nc):
piv = next((i for i in range(r, len(M)) if M[i][c] % 3), None)
if piv is None:
continue
M[r], M[piv] = M[piv], M[r]
inv = M[r][c] % 3
M[r] = [(x * inv) % 3 for x in M[r]]
for i in range(len(M)):
if i != r and M[i][c] % 3:
fct = M[i][c] % 3
M[i] = [(M[i][k] - fct * M[r][k]) % 3 for k in range(nc)]
r += 1
if r == len(M):
break
return r
def describe(P):
P = list(P)
sign_closed = all(tuple((3 - x) % 3 for x in s) in set(P) for s in P)
s0 = P[0]
D = [tuple((np.array(s) - np.array(s0)) % 3) for s in P]
rank = gf3_rank(D)
affine = (len(P) == 3 ** rank)
pow2 = (len(P) & (len(P) - 1)) == 0
return (f"sign-closed={sign_closed} affine-GF3={affine} "
f"|Phi|={len(P)} (power-of-2={pow2}) hull-dim={rank}")
def random_disk(n, n_stacks, rng):
faces = fan_triangulation(n)
nxt = n
for _ in range(n_stacks):
stack(faces, rng.randrange(len(faces)), nxt)
nxt += 1
return faces
def deep_stack_disk(n, n_stacks):
"""Always stack into the most-recently created face -> deep equality chain."""
faces = fan_triangulation(n)
nxt = n
for _ in range(n_stacks):
stack(faces, len(faces) - 1, nxt)
nxt += 1
return faces
def search(n, cap=18, trials=400, seed=0):
rng = random.Random(seed)
best = (10 ** 9, None, None)
max_stacks = (cap - (n - 2)) // 2
# random search
for _ in range(trials):
k = rng.randint(0, max_stacks)
faces = random_disk(n, k, rng)
P = phi(faces, n, cap)
if P is None:
continue
if len(P) < best[0]:
best = (len(P), k, P)
# deterministic deep stack at max depth
for k in range(max_stacks + 1):
faces = deep_stack_disk(n, k)
P = phi(faces, n, cap)
if P is not None and len(P) < best[0]:
best = (len(P), k, P)
size, k, P = best
print(f"n={n}: min |Phi| = {size} (= 2^(n-2) = {2**(n-2)}?) "
f"interior vertices = {k}, max stacks at cap {cap} = {max_stacks}")
print(f" {describe(P)}")
for s in sorted(P)[:6]:
print(f" {disp(s)}")
if len(P) > 6:
print(f" ... (+{len(P)-6} more)")
return size
def main():
ns = [int(x) for x in sys.argv[1:]] or [4, 5, 6, 7]
print("Searching for maximally-constraining disks (min |Phi|)\n")
for n in ns:
# bigger cap for small n
cap = 18 if n <= 6 else 16
search(n, cap=cap)
print()
if __name__ == "__main__":
main()
@@ -1,170 +0,0 @@
"""
Test the MONOTONICITY LEMMA behind the 2^(n-2) lower bound:
adding an interior vertex never DECREASES |Phi|
(equivalently Phi(D') subset Phi(D) when D = D' + one interior vertex).
If true, every disk reduces to the k=0 base case without increasing Phi, so
|Phi(D)| >= |Phi(D_0)| = 2^(n-2). A single insertion that shrinks Phi (or breaks
the inclusion) would refute the proof strategy.
We build a validated base disk D' (Delaunay, convex non-cocircular boundary), then
insert a vertex two ways:
* degree-3 stack into a face (proved: should give equality)
* degree-4 open of an internal edge (a,b)|(c,d) (the first genuinely open case)
and compare Phi(D') to Phi(D).
"""
import sys
from collections import Counter, defaultdict
from itertools import product
import numpy as np
from scipy.spatial import Delaunay
np.seterr(all="ignore")
def base_disk(n, k, rng):
ang = 2 * np.pi * np.arange(n) / n
rad = 1.0 + 0.15 * rng.random(n)
bpts = np.c_[rad * np.cos(ang), rad * np.sin(ang)]
if k:
r = 0.7 * np.sqrt(rng.random(k)); t = 2 * np.pi * rng.random(k)
pts = np.vstack([bpts, np.c_[r * np.cos(t), r * np.sin(t)]])
else:
pts = bpts
faces = [tuple(int(x) for x in s) for s in Delaunay(pts).simplices]
return faces
def valid(faces, n, k):
if len(faces) != 2 * k + n - 2:
return False
ec = Counter()
for a, b, c in faces:
for e in ((a, b), (b, c), (a, c)):
ec[frozenset(e)] += 1
return all(ec[frozenset((i, (i + 1) % n))] == 1 for i in range(n))
def phi(faces, n):
verts = set(v for f in faces for v in f)
interior = sorted(v for v in verts if v >= n)
F = len(faces)
if F > 18:
return None
Bint = np.zeros((len(interior), F), dtype=np.int64)
Cinc = np.zeros((n, F), dtype=np.int64)
iidx = {w: r for r, w in enumerate(interior)}
for j, (a, b, c) in enumerate(faces):
for v in (a, b, c):
if v >= n:
Bint[iidx[v], j] = 1
else:
Cinc[v, j] = 1
labs = np.array(list(product((1, 2), repeat=F)), dtype=np.int64)
if interior:
labs = labs[np.all((labs @ Bint.T) % 3 == 0, axis=1)]
if labs.shape[0] == 0:
return set()
return set(map(tuple, np.unique((labs @ Cinc.T) % 3, axis=0)))
def insert_deg3(faces, fi, v):
a, b, c = faces[fi]
out = [f for i, f in enumerate(faces) if i != fi]
out += [(a, b, v), (b, c, v), (a, c, v)]
return out
def insert_deg4(faces, n):
"""Open an internal edge (a,b) shared by faces (a,b,c),(a,b,d): replace those
two faces with the 4-star of a new center over the quad a-c-b-d."""
ef = defaultdict(list)
for i, (a, b, c) in enumerate(faces):
for e in ((a, b), (b, c), (a, c)):
ef[frozenset(e)].append(i)
for e, fl in ef.items():
if len(fl) != 2:
continue
a, b = tuple(e)
if (a < n and b < n and (b - a) % n in (1, n - 1)):
continue # boundary edge
f1, f2 = faces[fl[0]], faces[fl[1]]
c = next(x for x in f1 if x not in (a, b))
d = next(x for x in f2 if x not in (a, b))
if c == d:
continue
v = max(max(f) for f in faces) + 1
out = [f for i, f in enumerate(faces) if i not in fl]
out += [(a, c, v), (c, b, v), (b, d, v), (d, a, v)]
return out, v
return None, None
def main():
seed = int(sys.argv[1]) if len(sys.argv) > 1 else 0
rng = np.random.default_rng(seed)
viol_size = 0 # |Phi(D)| < |Phi(D')| (monotonicity broken)
viol_incl = 0 # Phi(D') not subset Phi(D)
tested = 0
deg3_equal = 0; deg3_tot = 0
deg4_strict = 0; deg4_tot = 0
examples = []
for _ in range(500):
n = int(rng.integers(4, 7)); k = int(rng.integers(0, 4))
faces = base_disk(n, k, rng)
if not valid(faces, n, k) or len(faces) > 14:
continue
Pp = phi(faces, n)
if Pp is None:
continue
# degree-3 insertions: every face
for fi in range(len(faces)):
D = insert_deg3(faces, fi, max(max(f) for f in faces) + 1)
P = phi(D, n)
if P is None:
continue
tested += 1; deg3_tot += 1
if not Pp <= P:
viol_incl += 1
if len(P) < len(Pp):
viol_size += 1
if len(examples) < 5:
examples.append(("deg3", n, len(Pp), len(P)))
if len(P) == len(Pp):
deg3_equal += 1
# one degree-4 insertion
D4, v = insert_deg4(faces, n)
if D4 is not None:
P = phi(D4, n)
if P is not None:
tested += 1; deg4_tot += 1
if not Pp <= P:
viol_incl += 1
if len(P) < len(Pp):
viol_size += 1
if len(examples) < 5:
examples.append(("deg4", n, len(Pp), len(P)))
if len(P) > len(Pp):
deg4_strict += 1
print(f"insertions tested: {tested}")
print(f" monotonicity violations (|Phi(D)| < |Phi(D')|): {viol_size}")
print(f" inclusion violations (Phi(D') not subset Phi(D)): {viol_incl}")
print(f" degree-3: {deg3_equal}/{deg3_tot} gave EXACT equality "
f"(proved un-stacking => should be all)")
print(f" degree-4: {deg4_strict}/{deg4_tot} strictly ENLARGED Phi")
if examples:
print(" VIOLATION examples (type,n,|Phi(D')|,|Phi(D)|):")
for e in examples:
print(f" {e}")
else:
print(" no violation: every insertion preserved or enlarged Phi, "
"and Phi(D') subset Phi(D) throughout.")
if __name__ == "__main__":
main()
@@ -1,168 +0,0 @@
"""
Proof strategy A for the irreducible lemma (|Phi| >= 2^(n-2)):
induction on k via a Phi-NON-INCREASING vertex removal. Monotonicity is false
(some removals raise Phi), but we only need ONE good removal per disk: if every
disk with k>=1 has an interior vertex v and a link-retriangulation with
|Phi(D - v)| <= |Phi(D)|, then chaining down to k=0 gives |Phi(D)| >= 2^(n-2).
This probe: for each disk, try removing each interior vertex (retriangulating its
link by a fan from every link vertex, keeping only valid retriangulations), and
record whether the BEST removal satisfies |Phi(D-v)| <= |Phi(D)|. Reports the
fraction of disks where such a removal EXISTS (strategy viable) vs disks where
EVERY removal strictly raises Phi (strategy fails) -- and dumps the failures.
"""
import sys
from collections import Counter, defaultdict
from itertools import product
import numpy as np
from scipy.spatial import Delaunay
np.seterr(all="ignore")
def disk(n, k, rng):
ang = 2 * np.pi * np.arange(n) / n
rad = 1.0 + 0.18 * rng.random(n)
bpts = np.c_[rad * np.cos(ang), rad * np.sin(ang)]
r = 0.8 * np.sqrt(rng.random(k)); t = 2 * np.pi * rng.random(k)
pts = np.vstack([bpts, np.c_[r * np.cos(t), r * np.sin(t)]])
return [tuple(int(x) for x in s) for s in Delaunay(pts).simplices]
def valid(faces, n, k):
if len(faces) != 2 * k + n - 2:
return False
ec = Counter()
for a, b, c in faces:
for e in ((a, b), (b, c), (a, c)):
ec[frozenset(e)] += 1
return all(ec[frozenset((i, (i + 1) % n))] == 1 for i in range(n))
def phi_size(faces, n):
interior = sorted(set(v for f in faces for v in f if v >= n))
F = len(faces)
if F > 16:
return None
Bint = np.zeros((len(interior), F), dtype=np.int64)
Cinc = np.zeros((n, F), dtype=np.int64)
iidx = {w: r for r, w in enumerate(interior)}
for j, (a, b, c) in enumerate(faces):
for v in (a, b, c):
if v >= n:
Bint[iidx[v], j] = 1
else:
Cinc[v, j] = 1
labs = np.array(list(product((1, 2), repeat=F)), dtype=np.int64)
if interior:
labs = labs[np.all((labs @ Bint.T) % 3 == 0, axis=1)]
if labs.shape[0] == 0:
return 0
return len(set(map(tuple, np.unique((labs @ Cinc.T) % 3, axis=0))))
def edges_of(faces):
E = set()
for a, b, c in faces:
for e in ((a, b), (b, c), (a, c)):
E.add(frozenset(e))
return E
def link_cycle(faces, v):
opp = [tuple(x for x in f if x != v) for f in faces if v in f]
adj = defaultdict(list)
for a, b in opp:
adj[a].append(b); adj[b].append(a)
if any(len(adj[u]) != 2 for u in adj):
return None # link not a simple cycle
start = opp[0][0]; cyc = [start]; prev = None; cur = start
while True:
nxt = [x for x in adj[cur] if x != prev]
if not nxt:
return None
nxt = nxt[0]
if nxt == start:
break
cyc.append(nxt); prev, cur = cur, nxt
if len(cyc) > len(adj):
return None
return cyc if len(cyc) == len(adj) else None
def removals(faces, v, n):
"""Yield valid (faces of D - v) over fan retriangulations of v's link."""
cyc = link_cycle(faces, v)
if cyc is None:
return
d = len(cyc)
rest = [f for f in faces if v not in f]
Erest = edges_of(rest)
for s in range(d):
order = cyc[s:] + cyc[:s]
diags = [frozenset((order[0], order[j])) for j in range(2, d - 1)]
if any(dg in Erest for dg in diags):
continue # duplicate-edge: invalid retriangulation
fan = [(order[0], order[j], order[j + 1]) for j in range(1, d - 1)]
yield rest + fan
def main():
seed = int(sys.argv[1]) if len(sys.argv) > 1 else 0
rng = np.random.default_rng(seed)
irreducible_only = "--irr" in sys.argv
tested = 0
has_good = 0
fail_examples = []
for _ in range(4000):
n = int(rng.integers(4, 7)); k = int(rng.integers(1, 4))
faces = disk(n, k, rng)
if not valid(faces, n, k) or len(faces) > 14:
continue
deg = Counter()
for f in faces:
for x in f:
if x >= n:
deg[x] += 1
if irreducible_only and (len(deg) < k or any(deg[x] < 4 for x in deg)):
continue
base = phi_size(faces, n)
if base is None:
continue
best = None
for v in [x for x in deg]:
for D2 in removals(faces, v, n):
s = phi_size(D2, n)
if s is None:
continue
if best is None or s < best:
best = s
if best is None:
continue
tested += 1
if best <= base:
has_good += 1
elif len(fail_examples) < 6:
fail_examples.append((n, k, base, best, sorted(deg.values())))
tag = "irreducible" if irreducible_only else "all"
print(f"disks tested ({tag}, k>=1): {tested}")
print(f" have a Phi-non-increasing removal: {has_good}/{tested} "
f"({100*has_good/max(tested,1):.1f}%)")
if fail_examples:
print(" FAILURES (every removal raised Phi) "
"(n,k,|Phi(D)|,best|Phi(D-v)|,int-degs):")
for e in fail_examples:
print(f" {e}")
else:
print(" no failure: every disk had a non-increasing removal "
"=> induction strategy A is viable.")
if __name__ == "__main__":
main()
@@ -1,129 +0,0 @@
"""
Search for a UNIVERSAL Heawood boundary sequence for a tire graph.
Fix an outer boundary cycle B_out of length n (the interface at which a tire
glues to its parent). Each way of filling the annulus -- an inner boundary of
size m together with a spoke triangulation ("inner graph") -- gives a tire whose
annular faces induce a set of realisable outer Heawood sequences
R_out(tire) = { (lambda*(v0), ..., lambda*(v_{n-1})) : lambda in {+1,-1}^F }
{0,1,-1}^n .
A *universal sequence* for B_out is one realisable for EVERY inner graph, i.e. a
member of the intersection ∩_tire R_out(tire). If a universal sequence existed,
a parent could always present its negation and glue to any child regardless of
the child's interior.
Note: chords of the inner outerplanar graph O lie inside B_in and bound no
annular face, so they do not change R_out -- only (n, m, spoke-path) do. And
intersecting over a SUBFAMILY of inner graphs can only OVERestimate the true
intersection, so finding the intersection empty over simple-cycle inner fills is
already conclusive that NO universal sequence exists.
"""
import sys
from itertools import combinations, product
import numpy as np
def lattice_paths(n_outer, m_inner):
"""All spoke triangulations: strings with n_outer 'O' moves, m_inner 'I'."""
N = n_outer + m_inner
for opos in combinations(range(N), n_outer):
opos = set(opos)
yield "".join("O" if i in opos else "I" for i in range(N))
def annular_faces(n, m, path):
"""Faces (triangles) of the annulus between outer n-cycle (0..n-1) and inner
m-cycle (n..n+m-1) under the spoke path. Starts at spoke (outer0, inner0)."""
faces = []
i = j = 0
for mv in path:
if mv == "O":
faces.append((i % n, (i + 1) % n, n + (j % m)))
i += 1
else:
faces.append((i % n, n + (j % m), n + ((j + 1) % m)))
j += 1
return faces
def fan_faces(n):
"""m = 1 degenerate inner boundary: a wheel/fan, center = vertex n."""
return [(i, (i + 1) % n, n) for i in range(n)]
def realisable_outer(n, faces):
"""Set of outer Heawood sequences over all +/-1 face labellings."""
F = len(faces)
A = np.zeros((n, F), dtype=np.int64) # outer-vertex x face incidence
for f, tri in enumerate(faces):
for v in tri:
if v < n:
A[v, f] = 1
labs = np.array(list(product((1, 2), repeat=F)), dtype=np.int64)
vals = (labs @ A.T) % 3
# display residues in {0, 1, -1}: 2 -> -1
vals = np.where(vals == 2, -1, vals)
return set(tuple(int(x) for x in row) for row in np.unique(vals, axis=0))
def tires_for(n, m_max, fcap):
"""Yield (label, faces) for inner fills of an n-outer tire."""
yield (f"m=1 fan", fan_faces(n))
for m in range(2, m_max + 1):
if n + m > fcap:
continue
for path in lattice_paths(n, m):
yield (f"m={m} {path}", annular_faces(n, m, path))
def run(n, m_max=7, fcap=13):
inter = None
n_tires = 0
min_set = (10**9, None)
shrink_trace = []
for label, faces in tires_for(n, m_max, fcap):
R = realisable_outer(n, faces)
n_tires += 1
if len(R) < min_set[0]:
min_set = (len(R), label)
if inter is None:
inter = set(R)
else:
before = len(inter)
inter &= R
if len(inter) < before:
shrink_trace.append((n_tires, label, len(inter)))
if not inter:
break
print(f"n={n}: {n_tires} tires tried, "
f"smallest single R_out = {min_set[0]} ({min_set[1]})")
if inter:
print(f" UNIVERSAL sequences found: {len(inter)}")
for s in sorted(inter)[:12]:
print(f" {s}")
else:
print(f" NO universal sequence: intersection emptied after "
f"{n_tires} tires")
print(" intersection size as tires were added (last few shrinks):")
for t in shrink_trace[-6:]:
print(f" after tire {t[0]:4d} ({t[1]}): |∩| = {t[2]}")
return bool(inter)
def main():
if len(sys.argv) > 1:
ns = [int(sys.argv[1])]
else:
ns = [3, 4, 5, 6]
print("Searching for universal Heawood boundary sequences\n")
for n in ns:
run(n)
print()
if __name__ == "__main__":
main()
@@ -1,172 +0,0 @@
"""
Transfer operator for the Heawood program, in the cleanest self-similar setting:
a chain of annular tires with n_out = n_in = n. Each tire's labelling map sends
+/-1 face labels to (outer sequence, inner sequence). Gluing a child below means
the parent's inner sequence must negate (mod 3) the child's achievable outer
sequence. So the achievable outer-interface set propagates UP the chain by
Phi(parent) = { outer(lambda) : lambda in {+-1}^F,
inner(lambda) in -Phi(child) }.
This is a monotone set-operator on subsets of (Z/3)^n. Iterating it models a
deepening nested chain; we look for a FIXED POINT (absorbing set) and test which
candidate self-similar invariants the limit satisfies:
* non-empty
* closed under the global sign flip s -> -s
* local marginals: does every position attain all of {0,1,-1}?
* is it an affine GF(3) subspace? (we expect NO -- R_T is a zonotope)
* does a linear/parity constraint cut it out?
Sequences are stored mod 3 in {0,1,2}; printed in {0,1,-1} (2 -> -1).
"""
import sys
from itertools import product
import numpy as np
def annular_tire(n_out, n_in, path):
"""Faces between outer cycle 0..n_out-1 and inner cycle n_out..n_out+n_in-1."""
faces = []
i = j = 0
for mv in path:
if mv == "O":
faces.append((i % n_out, (i + 1) % n_out, n_out + (j % n_in)))
i += 1
else:
faces.append((i % n_out, n_out + (j % n_in), n_out + ((j + 1) % n_in)))
j += 1
return faces
def labelling_pairs(n_out, n_in, faces):
"""All (outer_seq, inner_seq) over lambda in {+1,-1}^F, as Z/3 tuples."""
F = len(faces)
Ao = np.zeros((n_out, F), dtype=np.int64)
Ai = np.zeros((n_in, F), dtype=np.int64)
for f, (a, b, c) in enumerate(faces):
for v in (a, b, c):
if v < n_out:
Ao[v, f] = 1
else:
Ai[v - n_out, f] = 1
labs = np.array(list(product((1, 2), repeat=F)), dtype=np.int64)
outer = (labs @ Ao.T) % 3
inner = (labs @ Ai.T) % 3
return [(tuple(o), tuple(i)) for o, i in zip(outer.tolist(), inner.tolist())]
def make_operator(pairs):
def op(phi_child):
neg = {tuple((3 - x) % 3 for x in s) for s in phi_child}
return {o for (o, inn) in pairs if inn in neg}
return op
def iterate_to_fixed(op, start, max_iter=50):
phi = frozenset(start)
seen = [phi]
for _ in range(max_iter):
nxt = frozenset(op(phi))
if nxt == phi:
return phi, "fixed", len(seen)
if nxt in seen:
return nxt, "cycle", len(seen)
phi = nxt
seen.append(phi)
return phi, "no-converge", len(seen)
# ----------------- invariant tests -------------------------------------------
def disp(s):
return tuple(-1 if x == 2 else x for x in s)
def gf3_rank(rows):
M = [[x % 3 for x in r] for r in rows]
if not M:
return 0
nc = len(M[0]); r = 0
for c in range(nc):
piv = next((i for i in range(r, len(M)) if M[i][c] % 3), None)
if piv is None:
continue
M[r], M[piv] = M[piv], M[r]
inv = M[r][c] % 3 # 1->1, 2->2 are self-inverse mod 3
M[r] = [(x * inv) % 3 for x in M[r]]
for i in range(len(M)):
if i != r and M[i][c] % 3:
f = M[i][c] % 3
M[i] = [(M[i][k] - f * M[r][k]) % 3 for k in range(nc)]
r += 1
if r == len(M):
break
return r
def is_affine(S):
S = list(S)
if len(S) <= 1:
return True
s0 = S[0]
D = [tuple((np.array(s) - np.array(s0)) % 3) for s in S]
return len(S) == 3 ** gf3_rank(D)
def marginals_full(S, n):
return all({s[i] for s in S} == {0, 1, 2} for i in range(n))
def sign_closed(S):
return all(tuple((3 - x) % 3 for x in s) in S for s in S)
def linear_constraints(S, n):
"""Dimension of the space of linear forms vanishing on S-s0 (codim of hull)."""
S = list(S)
if len(S) <= 1:
return n
s0 = S[0]
D = [tuple((np.array(s) - np.array(s0)) % 3) for s in S]
return n - gf3_rank(D)
def analyse(tag, S, n):
print(f" [{tag}] |Phi|={len(S)} of 3^{n}={3**n} "
f"sign-closed={sign_closed(S)} marginals-full={marginals_full(S,n)} "
f"affine={is_affine(S)} hull-codim={linear_constraints(S,n)}")
def run(n, paths=None):
if paths is None:
# a few distinct same-n annular triangulations
paths = ["OI" * n, "O" * n + "I" * n, ("OOI" * n)[:2 * n]]
paths = [p for p in paths if p.count("O") == n and p.count("I") == n]
print(f"=== n={n} ===")
full = set(product((0, 1, 2), repeat=n))
for path in paths:
faces = annular_tire(n, n, path)
pairs = labelling_pairs(n, n, faces)
op = make_operator(pairs)
single = set(o for (o, _) in pairs) # leaf: full single-tire outer set
fixed, how, steps = iterate_to_fixed(op, single)
# also iterate from the universal start (all sequences allowed below)
fixed2, how2, _ = iterate_to_fixed(op, full)
print(f" path={path}: single-tire |outer|={len(single)}; "
f"iterate->{how} in {steps} steps; "
f"same-limit-from-full={fixed==fixed2}")
analyse("limit", fixed, n)
sample = sorted(disp(s) for s in fixed)[:8]
print(f" sample of limit set: {sample}")
print()
def main():
ns = [int(x) for x in sys.argv[1:]] or [4, 5, 6]
print("Transfer-operator fixed points on same-n annular tire chains\n")
for n in ns:
run(n)
if __name__ == "__main__":
main()
@@ -1,123 +0,0 @@
"""
Option 2: a direct "strong transversal" for |Phi(D)| >= 2^(n-2).
In the Boolean reformulation, feasible x in {0,1}^F satisfy |x|_w ≡ -deg(w) at
interior w, and the boundary sequence is (deg(v)+|x|_v mod 3)_{v in C}. A STRONG
TRANSVERSAL is a set A of n-2 faces such that
(i) every assignment x_A in {0,1}^A extends to a feasible x,
(ii) the boundary sequence is single-valued in x_A (all feasible completions of
a given x_A give the same boundary), and
(iii) the map x_A -> boundary is injective.
If such A exists, the 2^(n-2) assignments give 2^(n-2) distinct boundary sequences,
proving the bound CONSTRUCTIVELY. We test whether a strong transversal exists.
Heuristic worry: over GF(3) the internal completion freedom (dim k) exceeds the
boundary-invisible freedom (dim k-1), so (ii) may fail. Test settles it.
"""
import sys
from collections import defaultdict
from itertools import combinations, product
import numpy as np
from scipy.spatial import Delaunay
np.seterr(all="ignore")
def disk(n, k, rng):
ang = 2 * np.pi * np.arange(n) / n
rad = 1.0 + 0.18 * rng.random(n)
bpts = np.c_[rad * np.cos(ang), rad * np.sin(ang)]
if k:
r = 0.8 * np.sqrt(rng.random(k)); t = 2 * np.pi * rng.random(k)
pts = np.vstack([bpts, np.c_[r * np.cos(t), r * np.sin(t)]])
else:
pts = bpts
return [tuple(int(x) for x in s) for s in Delaunay(pts).simplices]
def valid(faces, n, k):
if len(faces) != 2 * k + n - 2:
return False
from collections import Counter
ec = Counter()
for a, b, c in faces:
for e in ((a, b), (b, c), (a, c)):
ec[frozenset(e)] += 1
return all(ec[frozenset((i, (i + 1) % n))] == 1 for i in range(n))
def feasible_table(faces, n):
"""Return list of (x tuple, boundary tuple) over feasible x in {0,1}^F."""
interior = sorted(set(v for f in faces for v in f if v >= n))
F = len(faces)
if F > 14:
return None
Bint = np.zeros((len(interior), F), dtype=np.int64)
Cinc = np.zeros((n, F), dtype=np.int64)
deg = np.zeros(n, dtype=np.int64)
iidx = {w: r for r, w in enumerate(interior)}
degw = np.zeros(len(interior), dtype=np.int64)
for j, (a, b, c) in enumerate(faces):
for v in (a, b, c):
if v >= n:
Bint[iidx[v], j] = 1; degw[iidx[v]] += 1
else:
Cinc[v, j] = 1; deg[v] += 1
xs = np.array(list(product((0, 1), repeat=F)), dtype=np.int64)
if len(interior):
ok = np.all((xs @ Bint.T - (-degw)) % 3 == 0, axis=1)
xs = xs[ok]
if xs.shape[0] == 0:
return []
bnd = (deg + xs @ Cinc.T) % 3
return list(zip(map(tuple, xs.tolist()), map(tuple, bnd.tolist())))
def has_strong_transversal(table, F, n):
target = 2 ** (n - 2)
for A in combinations(range(F), n - 2):
groups = defaultdict(set)
for x, b in table:
groups[tuple(x[i] for i in A)].add(b)
if len(groups) != target:
continue # not every x_A feasible
if any(len(bs) != 1 for bs in groups.values()):
continue # not single-valued (cond ii)
reps = [next(iter(bs)) for bs in groups.values()]
if len(set(reps)) == target: # injective (cond iii)
return True
return False
def main():
seed = int(sys.argv[1]) if len(sys.argv) > 1 else 0
rng = np.random.default_rng(seed)
tested = 0; have = 0; fails = []
for _ in range(3000):
n = int(rng.integers(4, 6)); k = int(rng.integers(1, 3))
faces = disk(n, k, rng)
if not valid(faces, n, k) or len(faces) > 12:
continue
tbl = feasible_table(faces, n)
if not tbl:
continue
tested += 1
if has_strong_transversal(tbl, len(faces), n):
have += 1
elif len(fails) < 5:
fails.append((n, k, len(faces)))
print(f"disks tested (k>=1): {tested}")
print(f" have a STRONG transversal (size n-2, single-valued, injective): "
f"{have}/{tested} ({100*have/max(tested,1):.1f}%)")
if fails:
print(f" failures (n,k,F): {fails}")
if have == tested and tested:
print(" => strong transversals always exist: constructive proof viable.")
elif have == 0:
print(" => strong transversals NEVER exist: this clean form of option 2 is dead.")
if __name__ == "__main__":
main()
@@ -1,154 +0,0 @@
"""
Pin the extremal irreducible disk.
The wheel W_n: boundary n-cycle + one center, faces (i,i+1,c). The center is the
only interior vertex (degree n), constraint sum_i lambda_i ≡ 0 (mod 3), and the
boundary value is the cyclic adjacent sum sigma_i = lambda_{i-1}+lambda_i (mod 3).
So
|Phi(W_n)| = #{ (lambda_{i-1}+lambda_i)_i mod 3 : lambda in {+-1}^n, sum ≡ 0 } .
We (1) compute |Phi(W_n)| exactly for a range of n and look for a formula, and
(2) run a thorough irreducible-disk search to check whether the wheel is actually
the MINIMISER over irreducible disks (and dump the minimiser's structure).
"""
import sys
from collections import Counter
from itertools import product
import numpy as np
from scipy.spatial import Delaunay
np.seterr(all="ignore")
# ---------------- exact wheel value ------------------------------------------
def wheel_phi_size(n):
S = set()
cnt = 0
for lam in product((1, -1), repeat=n):
if sum(lam) % 3 != 0:
continue
cnt += 1
sig = tuple((lam[i - 1] + lam[i]) % 3 for i in range(n))
S.add(sig)
return len(S), cnt # distinct boundary seqs, feasible labellings
# ---------------- general disk Phi -------------------------------------------
def disk(n, k, rng):
ang = 2 * np.pi * np.arange(n) / n
rad = 1.0 + 0.18 * rng.random(n)
bpts = np.c_[rad * np.cos(ang), rad * np.sin(ang)]
r = 0.8 * np.sqrt(rng.random(k)); t = 2 * np.pi * rng.random(k)
pts = np.vstack([bpts, np.c_[r * np.cos(t), r * np.sin(t)]])
return [tuple(int(x) for x in s) for s in Delaunay(pts).simplices]
def valid(faces, n, k):
if len(faces) != 2 * k + n - 2:
return False
ec = Counter()
for a, b, c in faces:
for e in ((a, b), (b, c), (a, c)):
ec[frozenset(e)] += 1
return all(ec[frozenset((i, (i + 1) % n))] == 1 for i in range(n))
def phi_size(faces, n):
interior = sorted(set(v for f in faces for v in f if v >= n))
F = len(faces)
if F > 18:
return None
Bint = np.zeros((len(interior), F), dtype=np.int64)
Cinc = np.zeros((n, F), dtype=np.int64)
iidx = {w: r for r, w in enumerate(interior)}
for j, (a, b, c) in enumerate(faces):
for v in (a, b, c):
if v >= n:
Bint[iidx[v], j] = 1
else:
Cinc[v, j] = 1
labs = np.array(list(product((1, 2), repeat=F)), dtype=np.int64)
if interior:
labs = labs[np.all((labs @ Bint.T) % 3 == 0, axis=1)]
if labs.shape[0] == 0:
return 0
return len(set(map(tuple, np.unique((labs @ Cinc.T) % 3, axis=0))))
def min_degree_ok(faces, n, k):
deg = Counter()
for f in faces:
for v in f:
if v >= n:
deg[v] += 1
return len(deg) == k and all(deg[v] >= 4 for v in deg)
def single_deg_disk(n, d):
"""One interior vertex v=n of degree d: fan over boundary 0..d-1, the rest of
the n-gon polygon-triangulated from vertex 0 (so v stays degree d, k=1)."""
v = n
faces = [(i, i + 1, v) for i in range(d - 1)]
poly = [0] + list(range(n - 1, d - 2, -1)) + [v]
for j in range(1, len(poly) - 1):
faces.append((0, poly[j], poly[j + 1]))
return faces
def degree_sweep():
print("\n|Phi| of a single degree-d interior vertex (rest at the floor):\n")
print(" ratio |Phi|/2^(n-2) by center degree d -- MINIMUM marks the extremal disk")
for n in (6, 7, 8):
row = []
for d in range(4, n + 1):
f = single_deg_disk(n, d)
row.append(f"d={d}:{phi_size(f, n)/2**(n-2):.4f}")
print(f" n={n} (floor {2**(n-2)}): " + " ".join(row))
print(" => min ratio 5/4 at d=4,5 (extremal); rises with d to ~4/3 at the wheel.")
def main():
print("Exact |Phi(W_n)| and candidate formula:\n")
print(" n |Phi(W_n)| feasible-labellings ratio-to-2^(n-2)")
vals = {}
for n in range(3, 15):
s, cnt = wheel_phi_size(n)
vals[n] = s
print(f" {n:2d} {s:8d} {cnt:14d} {s / 2**(n-2):.4f}")
# differences / ratios to spot a pattern
print("\n consecutive ratios |Phi(W_{n+1})| / |Phi(W_n)|:")
for n in range(3, 14):
print(f" {n}->{n+1}: {vals[n+1]/vals[n]:.4f}")
print("\nThe actual irreducible minimiser (Delaunay search, dumping structure)\n")
rng = np.random.default_rng(0)
for n in (4, 5, 6, 7):
best = 10**9; bestdeg = None; bestk = None
for _ in range(12000):
k = int(rng.integers(1, 4))
faces = disk(n, k, rng)
if not valid(faces, n, k) or len(faces) > 16:
continue
if not min_degree_ok(faces, n, k):
continue
s = phi_size(faces, n)
if s and s < best:
best = s
deg = Counter()
for f in faces:
for v in f:
if v >= n:
deg[v] += 1
bestdeg = sorted(deg.values()); bestk = k
floor = 2 ** (n - 2)
print(f" n={n}: min irreducible |Phi|={best} (k={bestk}, interior degrees "
f"{bestdeg}) ratio to floor = {best/floor:.4f} "
f"5*2^(n-4)={5*2**(n-4)} wheel={vals[n]}")
degree_sweep()
if __name__ == "__main__":
main()
@@ -1,6 +0,0 @@
\relax
\newlabel{lem:unstack}{{}{2}}
\newlabel{lem:base}{{}{3}}
\newlabel{prop:reduction}{{}{3}}
\newlabel{conj:irreducible}{{}{3}}
\gdef \@abspage@last{3}
@@ -1,300 +0,0 @@
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 17 JUN 2026 21:32
entering extended mode
restricted \write18 enabled.
%&-line parsing enabled.
**boundary_restriction_structure.tex
(./boundary_restriction_structure.tex
LaTeX2e <2021-11-15> patch level 1
L3 programming layer <2022-02-24>
(/usr/local/texlive/2022/texmf-dist/tex/latex/base/article.cls
Document Class: article 2021/10/04 v1.4n Standard LaTeX document class
(/usr/local/texlive/2022/texmf-dist/tex/latex/base/size11.clo
File: size11.clo 2021/10/04 v1.4n Standard LaTeX file (size option)
)
\c@part=\count185
\c@section=\count186
\c@subsection=\count187
\c@subsubsection=\count188
\c@paragraph=\count189
\c@subparagraph=\count190
\c@figure=\count191
\c@table=\count192
\abovecaptionskip=\skip47
\belowcaptionskip=\skip48
\bibindent=\dimen138
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsmath.sty
Package: amsmath 2021/10/15 v2.17l AMS math features
\@mathmargin=\skip49
For additional information on amsmath, use the `?' option.
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amstext.sty
Package: amstext 2021/08/26 v2.01 AMS text
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsgen.sty
File: amsgen.sty 1999/11/30 v2.0 generic functions
\@emptytoks=\toks16
\ex@=\dimen139
))
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsbsy.sty
Package: amsbsy 1999/11/29 v1.2d Bold Symbols
\pmbraise@=\dimen140
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsopn.sty
Package: amsopn 2021/08/26 v2.02 operator names
)
\inf@bad=\count193
LaTeX Info: Redefining \frac on input line 234.
\uproot@=\count194
\leftroot@=\count195
LaTeX Info: Redefining \overline on input line 399.
\classnum@=\count196
\DOTSCASE@=\count197
LaTeX Info: Redefining \ldots on input line 496.
LaTeX Info: Redefining \dots on input line 499.
LaTeX Info: Redefining \cdots on input line 620.
\Mathstrutbox@=\box50
\strutbox@=\box51
\big@size=\dimen141
LaTeX Font Info: Redeclaring font encoding OML on input line 743.
LaTeX Font Info: Redeclaring font encoding OMS on input line 744.
\macc@depth=\count198
\c@MaxMatrixCols=\count199
\dotsspace@=\muskip16
\c@parentequation=\count266
\dspbrk@lvl=\count267
\tag@help=\toks17
\row@=\count268
\column@=\count269
\maxfields@=\count270
\andhelp@=\toks18
\eqnshift@=\dimen142
\alignsep@=\dimen143
\tagshift@=\dimen144
\tagwidth@=\dimen145
\totwidth@=\dimen146
\lineht@=\dimen147
\@envbody=\toks19
\multlinegap=\skip50
\multlinetaggap=\skip51
\mathdisplay@stack=\toks20
LaTeX Info: Redefining \[ on input line 2938.
LaTeX Info: Redefining \] on input line 2939.
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amssymb.sty
Package: amssymb 2013/01/14 v3.01 AMS font symbols
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amsfonts.sty
Package: amsfonts 2013/01/14 v3.01 Basic AMSFonts support
\symAMSa=\mathgroup4
\symAMSb=\mathgroup5
LaTeX Font Info: Redeclaring math symbol \hbar on input line 98.
LaTeX Font Info: Overwriting math alphabet `\mathfrak' in version `bold'
(Font) U/euf/m/n --> U/euf/b/n on input line 106.
))
(/usr/local/texlive/2022/texmf-dist/tex/latex/amscls/amsthm.sty
Package: amsthm 2020/05/29 v2.20.6
\thm@style=\toks21
\thm@bodyfont=\toks22
\thm@headfont=\toks23
\thm@notefont=\toks24
\thm@headpunct=\toks25
\thm@preskip=\skip52
\thm@postskip=\skip53
\thm@headsep=\skip54
\dth@everypar=\toks26
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphicx.sty
Package: graphicx 2021/09/16 v1.2d Enhanced LaTeX Graphics (DPC,SPQR)
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/keyval.sty
Package: keyval 2014/10/28 v1.15 key=value parser (DPC)
\KV@toks@=\toks27
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphics.sty
Package: graphics 2021/03/04 v1.4d Standard LaTeX Graphics (DPC,SPQR)
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/trig.sty
Package: trig 2021/08/11 v1.11 sin cos tan (DPC)
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics-cfg/graphics.cfg
File: graphics.cfg 2016/06/04 v1.11 sample graphics configuration
)
Package graphics Info: Driver file: pdftex.def on input line 107.
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics-def/pdftex.def
File: pdftex.def 2020/10/05 v1.2a Graphics/color driver for pdftex
))
\Gin@req@height=\dimen148
\Gin@req@width=\dimen149
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/geometry/geometry.sty
Package: geometry 2020/01/02 v5.9 Page Geometry
(/usr/local/texlive/2022/texmf-dist/tex/generic/iftex/ifvtex.sty
Package: ifvtex 2019/10/25 v1.7 ifvtex legacy package. Use iftex instead.
(/usr/local/texlive/2022/texmf-dist/tex/generic/iftex/iftex.sty
Package: iftex 2022/02/03 v1.0f TeX engine tests
))
\Gm@cnth=\count271
\Gm@cntv=\count272
\c@Gm@tempcnt=\count273
\Gm@bindingoffset=\dimen150
\Gm@wd@mp=\dimen151
\Gm@odd@mp=\dimen152
\Gm@even@mp=\dimen153
\Gm@layoutwidth=\dimen154
\Gm@layoutheight=\dimen155
\Gm@layouthoffset=\dimen156
\Gm@layoutvoffset=\dimen157
\Gm@dimlist=\toks28
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/booktabs/booktabs.sty
Package: booktabs 2020/01/12 v1.61803398 Publication quality tables
\heavyrulewidth=\dimen158
\lightrulewidth=\dimen159
\cmidrulewidth=\dimen160
\belowrulesep=\dimen161
\belowbottomsep=\dimen162
\aboverulesep=\dimen163
\abovetopsep=\dimen164
\cmidrulesep=\dimen165
\cmidrulekern=\dimen166
\defaultaddspace=\dimen167
\@cmidla=\count274
\@cmidlb=\count275
\@aboverulesep=\dimen168
\@belowrulesep=\dimen169
\@thisruleclass=\count276
\@lastruleclass=\count277
\@thisrulewidth=\dimen170
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/l3backend/l3backend-pdftex.def
File: l3backend-pdftex.def 2022-02-07 L3 backend support: PDF output (pdfTeX)
\l__color_backend_stack_int=\count278
\l__pdf_internal_box=\box52
)
(./boundary_restriction_structure.aux)
\openout1 = `boundary_restriction_structure.aux'.
LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 19.
LaTeX Font Info: ... okay on input line 19.
LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 19.
LaTeX Font Info: ... okay on input line 19.
LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 19.
LaTeX Font Info: ... okay on input line 19.
LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 19.
LaTeX Font Info: ... okay on input line 19.
LaTeX Font Info: Checking defaults for TS1/cmr/m/n on input line 19.
LaTeX Font Info: ... okay on input line 19.
LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 19.
LaTeX Font Info: ... okay on input line 19.
LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 19.
LaTeX Font Info: ... okay on input line 19.
(/usr/local/texlive/2022/texmf-dist/tex/context/base/mkii/supp-pdf.mkii
[Loading MPS to PDF converter (version 2006.09.02).]
\scratchcounter=\count279
\scratchdimen=\dimen171
\scratchbox=\box53
\nofMPsegments=\count280
\nofMParguments=\count281
\everyMPshowfont=\toks29
\MPscratchCnt=\count282
\MPscratchDim=\dimen172
\MPnumerator=\count283
\makeMPintoPDFobject=\count284
\everyMPtoPDFconversion=\toks30
) (/usr/local/texlive/2022/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty
Package: epstopdf-base 2020-01-24 v2.11 Base part for package epstopdf
Package epstopdf-base Info: Redefining graphics rule for `.eps' on input line 4
85.
(/usr/local/texlive/2022/texmf-dist/tex/latex/latexconfig/epstopdf-sys.cfg
File: epstopdf-sys.cfg 2010/07/13 v1.3 Configuration of (r)epstopdf for TeX Liv
e
))
*geometry* driver: auto-detecting
*geometry* detected driver: pdftex
*geometry* verbose mode - [ preamble ] result:
* driver: pdftex
* paper: <default>
* layout: <same size as paper>
* layoutoffset:(h,v)=(0.0pt,0.0pt)
* modes:
* h-part:(L,W,R)=(72.26999pt, 469.75502pt, 72.26999pt)
* v-part:(T,H,B)=(72.26999pt, 650.43001pt, 72.26999pt)
* \paperwidth=614.295pt
* \paperheight=794.96999pt
* \textwidth=469.75502pt
* \textheight=650.43001pt
* \oddsidemargin=0.0pt
* \evensidemargin=0.0pt
* \topmargin=-37.0pt
* \headheight=12.0pt
* \headsep=25.0pt
* \topskip=11.0pt
* \footskip=30.0pt
* \marginparwidth=59.0pt
* \marginparsep=10.0pt
* \columnsep=10.0pt
* \skip\footins=10.0pt plus 4.0pt minus 2.0pt
* \hoffset=0.0pt
* \voffset=0.0pt
* \mag=1000
* \@twocolumnfalse
* \@twosidefalse
* \@mparswitchfalse
* \@reversemarginfalse
* (1in=72.27pt=25.4mm, 1cm=28.453pt)
LaTeX Font Info: Trying to load font information for U+msa on input line 20.
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsa.fd
File: umsa.fd 2013/01/14 v3.01 AMS symbols A
)
LaTeX Font Info: Trying to load font information for U+msb on input line 20.
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsb.fd
File: umsb.fd 2013/01/14 v3.01 AMS symbols B
) [1
{/usr/local/texlive/2022/texmf-var/fonts/map/pdftex/updmap/pdftex.map}] [2] [3]
(./boundary_restriction_structure.aux) )
Here is how much of TeX's memory you used:
3268 strings out of 478268
48713 string characters out of 5846347
350712 words of memory out of 5000000
21451 multiletter control sequences out of 15000+600000
481419 words of font info for 73 fonts, out of 8000000 for 9000
1141 hyphenation exceptions out of 8191
55i,8n,62p,247b,208s stack positions out of 10000i,1000n,20000p,200000b,200000s
</usr/local/texlive/2022/texmf-dist/fon
ts/type1/public/amsfonts/cm/cmbx10.pfb></usr/local/texlive/2022/texmf-dist/font
s/type1/public/amsfonts/cm/cmbx12.pfb></usr/local/texlive/2022/texmf-dist/fonts
/type1/public/amsfonts/cm/cmbxti10.pfb></usr/local/texlive/2022/texmf-dist/font
s/type1/public/amsfonts/cm/cmex10.pfb></usr/local/texlive/2022/texmf-dist/fonts
/type1/public/amsfonts/cm/cmmi10.pfb></usr/local/texlive/2022/texmf-dist/fonts/
type1/public/amsfonts/cm/cmmi12.pfb></usr/local/texlive/2022/texmf-dist/fonts/t
ype1/public/amsfonts/cm/cmmi6.pfb></usr/local/texlive/2022/texmf-dist/fonts/typ
e1/public/amsfonts/cm/cmmi8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1
/public/amsfonts/cm/cmr10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/p
ublic/amsfonts/cm/cmr12.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/pub
lic/amsfonts/cm/cmr17.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/publi
c/amsfonts/cm/cmr8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/a
msfonts/cm/cmss8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/ams
fonts/cm/cmsy10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsf
onts/cm/cmsy8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfon
ts/cm/cmti10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfont
s/cm/cmtt10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts
/symbols/msbm10.pfb>
Output written on boundary_restriction_structure.pdf (3 pages, 218332 bytes).
PDF statistics:
104 PDF objects out of 1000 (max. 8388607)
62 compressed objects within 1 object stream
0 named destinations out of 1000 (max. 500000)
1 words of extra memory for PDF output out of 10000 (max. 10000000)
@@ -1,231 +0,0 @@
\documentclass[11pt]{article}
\usepackage{amsmath,amssymb,amsthm}
\usepackage{graphicx}
\usepackage{geometry}
\usepackage{booktabs}
\geometry{margin=1in}
\title{Heawood boundary restriction sets:\\
zonotope structure and the $2^{n-2}$ constraint floor}
\author{}
\date{}
\newtheorem*{obs}{Observation}
\newtheorem*{prop}{Proposition}
\newtheorem*{conj}{Conjecture}
\newtheorem*{lem}{Lemma}
\newtheorem*{verify}{Empirical check}
\begin{document}
\maketitle
This note records the empirical structure of the Heawood boundary
restriction sets studied in \texttt{paper.tex}, and a clean
\emph{maximal-constraint} result. All claims below are backed by the
experiments in \texttt{experiments/} (filenames given inline). Sequences
live in $(\mathbb{Z}/3)^{\,\cdot}$, displayed in $\{0,1,-1\}$.
\section*{Setup}
Fix a triangulated disk $D$ with boundary cycle $C = (v_0,\dots,v_{n-1})$.
A Heawood face-labelling is $\lambda : \{\text{faces of }D\} \to \{+1,-1\}$,
with induced vertex value $\lambda^{*}(v) = \sum_{f \ni v}\lambda(f) \bmod 3$.
The achievable outer set is
\[
\Phi(D) \;=\; \bigl\{\, (\lambda^{*}(v_0),\dots,\lambda^{*}(v_{n-1}))
\;:\; \lambda \in \{+1,-1\}^{F(D)},\;
\lambda^{*}(w) \equiv 0 \ \forall\ \text{interior } w \,\bigr\}.
\]
This is exactly the value the recursive transfer operator produces at
$C$ (interior consistency $=$ all descendant gluings performed; boundary
deferred). Crucially $\Phi(D)$ depends \emph{only} on the disk
triangulation, not on any BFS/tire-tree labelling.
\section*{1. The restriction sets are zonotopes, not subspaces}
(\texttt{probe\_RK\_structure.py}.) Writing $\lambda = \mathbf{1}+b$ with
$b \in \{0,1\}^F$, the labelling map is $\lambda \mapsto M\mathbf{1}+Mb
\pmod 3$, a linear image of the Boolean cube ($M$ the face/vertex
incidence matrix). Over $3655$ cluster restriction sets $R_{\mathsf K}$:
none was an affine $\mathrm{GF}(3)$ subspace; the map is usually
injective, so $|R_{\mathsf K}| = 2^{|F|}$ (a power of $2$ inside the
column space of size $3^{\operatorname{rank} M}$); the nowhere-zero
constraint $\lambda \neq 0$ shrank the set below the full linear image in
\emph{every} case. The only surviving linear structure is
$R_{\mathsf K} \subseteq \operatorname{col}(M)$ (cokernel relations such
as $\sum_v \lambda^{*}(v) \equiv 0$). So $\Phi$ is a $\mathbb{Z}/3$
zonotope: a projected cube, sign-closed but not closed under addition.
\section*{2. ``Richness'' is not a self-similar invariant}
(\texttt{transfer\_operator.py}, \texttt{branch\_invariant.py}.) In a
homogeneous same-$n$ spoke-only chain the operator saturates: $\Phi$ has
full single-position marginals (every interface vertex independently
attains all of $\{0,1,-1\}$), and the alternating tire reaches the
\emph{entire} space $3^n$. This is an artifact of non-shrinking annuli
with no interior constraints. On genuine triangulations the marginal
fullness holds for only ${\sim}8\%$ of regions: depth (not branching)
shrinks $\Phi$, e.g.\ a region with $|C|=10$ realised only $|\Phi|=400$
of $3^{10}\approx 59000$. Only non-emptiness and sign-closure survive,
both of which are automatic / equivalent to $4$CT. Hence no abundance
(counting) pigeonhole: a working invariant must tolerate \emph{small}
$\Phi$.
\section*{3. The maximal-constraint floor}
(\texttt{maximally\_constrain.py}.) Minimising $|\Phi(D)|$ over disks with
a fixed boundary $n$-cycle:
\begin{center}
\begin{tabular}{ccccc}
\toprule
$n$ & $4$ & $5$ & $6$ & $7$\\
\midrule
$\min |\Phi|$ (search) & $4$ & $8$ & $16$ & $32$\\
fan, $0$ interior vertices & $4$ & $8$ & $16$ & $32$\\
$2^{\,n-2}$ & $4$ & $8$ & $16$ & $32$\\
\bottomrule
\end{tabular}
\end{center}
A search over $1700{+}$ \emph{validated} triangulated disks per $n$
(boundary points in convex but non-cocircular position, random interior
points, Delaunay; each checked to have $2k+n-2$ faces and all $n$
boundary edges present), together with deep-stacked single-apex chains up
to $8$ interior vertices, never beat $2^{n-2}$, and the interior-free
triangulation already attains it. (Note: cocircular boundary points
produce degenerate Delaunay outputs --- invalid disks missing a boundary
edge --- which spuriously report sub-floor values; these are excluded by
the validity check.) Counterintuitively, adding interior structure tends
to \emph{enlarge} $\Phi$: e.g.\ on the $4$-cycle the central-apex wheel
realises $5$ sequences against the fan's $4$, since each interior vertex
contributes two faces but only one constraint. Thus:
\begin{obs}
The interior-free triangulation already attains $2^{n-2}$, no search disk
beats it, and deep nesting only approaches this value from above ---
suggesting it is a floor, with a single trivial tire already maximally
constraining. Whether $2^{n-2}$ is a genuine lower bound for \emph{all}
disks is the Conjecture below; it is \emph{not} a proven theorem.
\end{obs}
The achievability is transparent: in a fan from $v_0$,
\[
\sigma_1 = \lambda_1,\quad
\sigma_i = \lambda_{i-1}+\lambda_i \ (1<i<n-1),\quad
\sigma_{n-1} = \lambda_{n-2},\quad
\sigma_0 = \textstyle\sum_j \lambda_j ,
\]
so $(\lambda_1,\dots,\lambda_{n-2})$ is recoverable from $\sigma$ and the
map is injective onto $2^{n-2}$ sequences. The lower bound over
\emph{all} disks is the substance:
\begin{conj}[Boundary degrees of freedom]
For every triangulated disk $D$ with boundary $n$-cycle,
$|\Phi(D)| \ge 2^{n-2}$. Equivalently, the $n-2$ binary degrees of
freedom carried by the boundary-incident faces survive every interior
Heawood constraint (which relates only interior-incident faces).
\end{conj}
The minimal set is itself a sign-closed zonotope of size $2^{n-2}$, hull
dimension $n-2$, not a $\mathrm{GF}(3)$ subspace --- the same fingerprint
as $\S1$.
\section*{4. A proof programme for the lower bound}
The lower bound $|\Phi(D)| \ge 2^{n-2}$ reduces, by an exact
$\Phi$-preserving reduction, to a single lemma about ``irreducible''
disks. Two dead ends bound the search first: \emph{monotonicity is false}
--- inserting a degree-$4$ interior vertex can shrink $|\Phi|$ ($6\to5$,
$30\to28$; \texttt{monotonicity\_test.py}), so there is no reduce-to-base
proof by ``adding vertices only grows $\Phi$''; and \emph{universal
toggles are insufficient} --- a flip preserves feasibility for every
labelling only if it touches no interior vertex (a \emph{boundary-only}
face), and an irreducible disk can have none (the wheel has zero). What
does work:
\begin{lem}[Un-stacking; degree-$3$ removal preserves $\Phi$]
\label{lem:unstack}
Let $v$ be a degree-$3$ interior vertex of $D$, with link triangle
$abc$ and incident faces $(vab),(vbc),(vca)$. Its constraint
$\lambda_{vab}+\lambda_{vbc}+\lambda_{vca}\equiv 0 \pmod 3$ over
$\{+1,-1\}$ forces the three to a common value $s$, so each of $a,b,c$
receives $2s\equiv -s$ from $v$'s star. Let $D'$ delete $v$ and restore
$abc$ as one face. Then setting that face to $-s$ reproduces the
contribution $-s$ at $a,b,c$, and $s\mapsto -s$ is a bijection on
$\{+1,-1\}$. Hence the map is a bijection between feasible labellings of
$D$ and of $D'$ preserving every boundary value and interior constraint,
so
\[
\Phi(D) = \Phi(D'), \qquad k(D') = k(D)-1 .
\]
\end{lem}
\begin{verify}\textnormal{(\texttt{monotonicity\_test.py})} Degree-$3$
insertion gave exact equality in $8884/8884$ trials.\end{verify}
\begin{lem}[Base case; ear-peeling]
\label{lem:base}
If $D$ has no interior vertices ($k=0$) then $|\Phi(D)| = 2^{n-2}$. A
polygon triangulation has an \emph{ear} $(v_{i-1},v_i,v_{i+1})$ with
$v_i$ of face-degree $1$, so $\sigma_{v_i}=\lambda_{\mathrm{ear}}$ reads
the ear label directly; remove it and induct on the $(n-1)$-gon. The
boundary map is injective, giving $2^{n-2}$.
\end{lem}
\begin{prop}[Reduction to the irreducible case]
\label{prop:reduction}
Iterating Lemma~\ref{lem:unstack} terminates ($k$ strictly decreases) at
a residue $D^{\ast}$ with no degree-$3$ interior vertex and the same
$n$, and $\Phi(D)=\Phi(D^{\ast})$. The residue is either $k=0$, where
$|\Phi|=2^{n-2}$ by Lemma~\ref{lem:base}, or \emph{irreducible}: $k\ge1$
with every interior vertex of degree $\ge 4$. Hence
\[
|\Phi(D)| \ge 2^{n-2}
\quad\Longleftarrow\quad
|\Phi(D^{\ast})| \ge 2^{n-2}\ \text{for every irreducible } D^{\ast}.
\]
\end{prop}
\begin{conj}[Irreducible lemma --- the remaining content]
\label{conj:irreducible}
Every irreducible disk satisfies $|\Phi| \ge 2^{n-2}$; in fact
$|\Phi| \ge \tfrac54\cdot 2^{n-2} = 5\cdot 2^{n-4}$.
\end{conj}
\begin{verify}\textnormal{(\texttt{irreducible\_floor.py},
\texttt{wheel\_extremal.py})} Over $10^4{+}$ irreducible disks
($n=4,5,6$) there were $0$ floor violations and none sat on the floor.
The bound $\tfrac54\cdot 2^{n-2}$ is \emph{tight}, attained by a single
\textbf{minimal-degree} interior vertex (degree $4$ or $5$, which tie):
the ratio $|\Phi|/2^{n-2}$ rises monotonically with the interior vertex's
degree, $\tfrac54$ at $d\in\{4,5\}$, $\tfrac{21}{16}$ at $d\in\{6,7\}$,
$\ldots$, up to $\tfrac43$ at the wheel $d=n$, where
$|\Phi(W_n)|=\lfloor 2^n/3\rfloor$ exactly. So a proof of
Conjecture~\ref{conj:irreducible} should be stress-tested against the
degree-$4$ patch, the tight case --- \emph{not} the wheel.\end{verify}
\noindent
\emph{Status.} Lemmas~\ref{lem:unstack}--\ref{lem:base} and
Proposition~\ref{prop:reduction} are proofs; they settle every disk that
un-stacks to $k=0$ (the entire Apollonian class). The whole open content
is Conjecture~\ref{conj:irreducible}, with guaranteed $25\%$ slack and a
single explicit extremal disk.
\section*{Consequence for the pigeonhole}
Even a maximally-constraining child still presents $2^{n-2}$ outer
options --- exponential in the interface length $n$. So the gluing
problem has the least slack at \emph{short} interfaces ($n=4$ leaves $4$
options, $n=3$ leaves $2$), and is easy at long ones. The crux of the
Heawood programme therefore lives entirely at short level cycles, exactly
where the medial programme's $N(k)$ bound concentrates.
\medskip
\noindent\emph{Meta-remark.} Because $4$CT holds, every actual
triangulation glues, so no experiment can exhibit an obstruction (pair or
chain). The experiments measure \emph{structure} (zonotope type,
constraint floor), not proof difficulty; the difficulty is localised, not
removed.
\end{document}
@@ -1,4 +0,0 @@
\relax
\@writefile{toc}{\contentsline {paragraph}{Inner.}{2}{}\protected@file@percent }
\@writefile{toc}{\contentsline {paragraph}{Outer.}{2}{}\protected@file@percent }
\gdef \@abspage@last{2}
@@ -1,296 +0,0 @@
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 18 JUN 2026 23:27
entering extended mode
restricted \write18 enabled.
%&-line parsing enabled.
**double_contraction_reductio.tex
(./double_contraction_reductio.tex
LaTeX2e <2021-11-15> patch level 1
L3 programming layer <2022-02-24>
(/usr/local/texlive/2022/texmf-dist/tex/latex/base/article.cls
Document Class: article 2021/10/04 v1.4n Standard LaTeX document class
(/usr/local/texlive/2022/texmf-dist/tex/latex/base/size11.clo
File: size11.clo 2021/10/04 v1.4n Standard LaTeX file (size option)
)
\c@part=\count185
\c@section=\count186
\c@subsection=\count187
\c@subsubsection=\count188
\c@paragraph=\count189
\c@subparagraph=\count190
\c@figure=\count191
\c@table=\count192
\abovecaptionskip=\skip47
\belowcaptionskip=\skip48
\bibindent=\dimen138
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsmath.sty
Package: amsmath 2021/10/15 v2.17l AMS math features
\@mathmargin=\skip49
For additional information on amsmath, use the `?' option.
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amstext.sty
Package: amstext 2021/08/26 v2.01 AMS text
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsgen.sty
File: amsgen.sty 1999/11/30 v2.0 generic functions
\@emptytoks=\toks16
\ex@=\dimen139
))
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsbsy.sty
Package: amsbsy 1999/11/29 v1.2d Bold Symbols
\pmbraise@=\dimen140
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsopn.sty
Package: amsopn 2021/08/26 v2.02 operator names
)
\inf@bad=\count193
LaTeX Info: Redefining \frac on input line 234.
\uproot@=\count194
\leftroot@=\count195
LaTeX Info: Redefining \overline on input line 399.
\classnum@=\count196
\DOTSCASE@=\count197
LaTeX Info: Redefining \ldots on input line 496.
LaTeX Info: Redefining \dots on input line 499.
LaTeX Info: Redefining \cdots on input line 620.
\Mathstrutbox@=\box50
\strutbox@=\box51
\big@size=\dimen141
LaTeX Font Info: Redeclaring font encoding OML on input line 743.
LaTeX Font Info: Redeclaring font encoding OMS on input line 744.
\macc@depth=\count198
\c@MaxMatrixCols=\count199
\dotsspace@=\muskip16
\c@parentequation=\count266
\dspbrk@lvl=\count267
\tag@help=\toks17
\row@=\count268
\column@=\count269
\maxfields@=\count270
\andhelp@=\toks18
\eqnshift@=\dimen142
\alignsep@=\dimen143
\tagshift@=\dimen144
\tagwidth@=\dimen145
\totwidth@=\dimen146
\lineht@=\dimen147
\@envbody=\toks19
\multlinegap=\skip50
\multlinetaggap=\skip51
\mathdisplay@stack=\toks20
LaTeX Info: Redefining \[ on input line 2938.
LaTeX Info: Redefining \] on input line 2939.
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amssymb.sty
Package: amssymb 2013/01/14 v3.01 AMS font symbols
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amsfonts.sty
Package: amsfonts 2013/01/14 v3.01 Basic AMSFonts support
\symAMSa=\mathgroup4
\symAMSb=\mathgroup5
LaTeX Font Info: Redeclaring math symbol \hbar on input line 98.
LaTeX Font Info: Overwriting math alphabet `\mathfrak' in version `bold'
(Font) U/euf/m/n --> U/euf/b/n on input line 106.
))
(/usr/local/texlive/2022/texmf-dist/tex/latex/amscls/amsthm.sty
Package: amsthm 2020/05/29 v2.20.6
\thm@style=\toks21
\thm@bodyfont=\toks22
\thm@headfont=\toks23
\thm@notefont=\toks24
\thm@headpunct=\toks25
\thm@preskip=\skip52
\thm@postskip=\skip53
\thm@headsep=\skip54
\dth@everypar=\toks26
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphicx.sty
Package: graphicx 2021/09/16 v1.2d Enhanced LaTeX Graphics (DPC,SPQR)
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/keyval.sty
Package: keyval 2014/10/28 v1.15 key=value parser (DPC)
\KV@toks@=\toks27
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphics.sty
Package: graphics 2021/03/04 v1.4d Standard LaTeX Graphics (DPC,SPQR)
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/trig.sty
Package: trig 2021/08/11 v1.11 sin cos tan (DPC)
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics-cfg/graphics.cfg
File: graphics.cfg 2016/06/04 v1.11 sample graphics configuration
)
Package graphics Info: Driver file: pdftex.def on input line 107.
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics-def/pdftex.def
File: pdftex.def 2020/10/05 v1.2a Graphics/color driver for pdftex
))
\Gin@req@height=\dimen148
\Gin@req@width=\dimen149
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/geometry/geometry.sty
Package: geometry 2020/01/02 v5.9 Page Geometry
(/usr/local/texlive/2022/texmf-dist/tex/generic/iftex/ifvtex.sty
Package: ifvtex 2019/10/25 v1.7 ifvtex legacy package. Use iftex instead.
(/usr/local/texlive/2022/texmf-dist/tex/generic/iftex/iftex.sty
Package: iftex 2022/02/03 v1.0f TeX engine tests
))
\Gm@cnth=\count271
\Gm@cntv=\count272
\c@Gm@tempcnt=\count273
\Gm@bindingoffset=\dimen150
\Gm@wd@mp=\dimen151
\Gm@odd@mp=\dimen152
\Gm@even@mp=\dimen153
\Gm@layoutwidth=\dimen154
\Gm@layoutheight=\dimen155
\Gm@layouthoffset=\dimen156
\Gm@layoutvoffset=\dimen157
\Gm@dimlist=\toks28
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/booktabs/booktabs.sty
Package: booktabs 2020/01/12 v1.61803398 Publication quality tables
\heavyrulewidth=\dimen158
\lightrulewidth=\dimen159
\cmidrulewidth=\dimen160
\belowrulesep=\dimen161
\belowbottomsep=\dimen162
\aboverulesep=\dimen163
\abovetopsep=\dimen164
\cmidrulesep=\dimen165
\cmidrulekern=\dimen166
\defaultaddspace=\dimen167
\@cmidla=\count274
\@cmidlb=\count275
\@aboverulesep=\dimen168
\@belowrulesep=\dimen169
\@thisruleclass=\count276
\@lastruleclass=\count277
\@thisrulewidth=\dimen170
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/l3backend/l3backend-pdftex.def
File: l3backend-pdftex.def 2022-02-07 L3 backend support: PDF output (pdfTeX)
\l__color_backend_stack_int=\count278
\l__pdf_internal_box=\box52
)
No file double_contraction_reductio.aux.
\openout1 = `double_contraction_reductio.aux'.
LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 20.
LaTeX Font Info: ... okay on input line 20.
LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 20.
LaTeX Font Info: ... okay on input line 20.
LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 20.
LaTeX Font Info: ... okay on input line 20.
LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 20.
LaTeX Font Info: ... okay on input line 20.
LaTeX Font Info: Checking defaults for TS1/cmr/m/n on input line 20.
LaTeX Font Info: ... okay on input line 20.
LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 20.
LaTeX Font Info: ... okay on input line 20.
LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 20.
LaTeX Font Info: ... okay on input line 20.
(/usr/local/texlive/2022/texmf-dist/tex/context/base/mkii/supp-pdf.mkii
[Loading MPS to PDF converter (version 2006.09.02).]
\scratchcounter=\count279
\scratchdimen=\dimen171
\scratchbox=\box53
\nofMPsegments=\count280
\nofMParguments=\count281
\everyMPshowfont=\toks29
\MPscratchCnt=\count282
\MPscratchDim=\dimen172
\MPnumerator=\count283
\makeMPintoPDFobject=\count284
\everyMPtoPDFconversion=\toks30
) (/usr/local/texlive/2022/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty
Package: epstopdf-base 2020-01-24 v2.11 Base part for package epstopdf
Package epstopdf-base Info: Redefining graphics rule for `.eps' on input line 4
85.
(/usr/local/texlive/2022/texmf-dist/tex/latex/latexconfig/epstopdf-sys.cfg
File: epstopdf-sys.cfg 2010/07/13 v1.3 Configuration of (r)epstopdf for TeX Liv
e
))
*geometry* driver: auto-detecting
*geometry* detected driver: pdftex
*geometry* verbose mode - [ preamble ] result:
* driver: pdftex
* paper: <default>
* layout: <same size as paper>
* layoutoffset:(h,v)=(0.0pt,0.0pt)
* modes:
* h-part:(L,W,R)=(72.26999pt, 469.75502pt, 72.26999pt)
* v-part:(T,H,B)=(72.26999pt, 650.43001pt, 72.26999pt)
* \paperwidth=614.295pt
* \paperheight=794.96999pt
* \textwidth=469.75502pt
* \textheight=650.43001pt
* \oddsidemargin=0.0pt
* \evensidemargin=0.0pt
* \topmargin=-37.0pt
* \headheight=12.0pt
* \headsep=25.0pt
* \topskip=11.0pt
* \footskip=30.0pt
* \marginparwidth=59.0pt
* \marginparsep=10.0pt
* \columnsep=10.0pt
* \skip\footins=10.0pt plus 4.0pt minus 2.0pt
* \hoffset=0.0pt
* \voffset=0.0pt
* \mag=1000
* \@twocolumnfalse
* \@twosidefalse
* \@mparswitchfalse
* \@reversemarginfalse
* (1in=72.27pt=25.4mm, 1cm=28.453pt)
LaTeX Font Info: Trying to load font information for U+msa on input line 21.
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsa.fd
File: umsa.fd 2013/01/14 v3.01 AMS symbols A
)
LaTeX Font Info: Trying to load font information for U+msb on input line 21.
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsb.fd
File: umsb.fd 2013/01/14 v3.01 AMS symbols B
) [1
{/usr/local/texlive/2022/texmf-var/fonts/map/pdftex/updmap/pdftex.map}] [2]
(./double_contraction_reductio.aux) )
Here is how much of TeX's memory you used:
3256 strings out of 478268
48463 string characters out of 5846347
347695 words of memory out of 5000000
21442 multiletter control sequences out of 15000+600000
479484 words of font info for 65 fonts, out of 8000000 for 9000
1141 hyphenation exceptions out of 8191
55i,5n,62p,244b,218s stack positions out of 10000i,1000n,20000p,200000b,200000s
{/usr/local/texlive/2022/texmf-dist/fonts/
enc/dvips/cm-super/cm-super-ts1.enc}</usr/local/texlive/2022/texmf-dist/fonts/t
ype1/public/amsfonts/cm/cmbx10.pfb></usr/local/texlive/2022/texmf-dist/fonts/ty
pe1/public/amsfonts/cm/cmbx12.pfb></usr/local/texlive/2022/texmf-dist/fonts/typ
e1/public/amsfonts/cm/cmbxti10.pfb></usr/local/texlive/2022/texmf-dist/fonts/ty
pe1/public/amsfonts/cm/cmitt10.pfb></usr/local/texlive/2022/texmf-dist/fonts/ty
pe1/public/amsfonts/cm/cmmi10.pfb></usr/local/texlive/2022/texmf-dist/fonts/typ
e1/public/amsfonts/cm/cmmi8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1
/public/amsfonts/cm/cmr10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/p
ublic/amsfonts/cm/cmr17.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/pub
lic/amsfonts/cm/cmr8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public
/amsfonts/cm/cmss8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/a
msfonts/cm/cmsy10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/am
sfonts/cm/cmsy8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsf
onts/cm/cmti10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfo
nts/symbols/msam10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/c
m-super/sfrm1095.pfb>
Output written on double_contraction_reductio.pdf (2 pages, 176278 bytes).
PDF statistics:
87 PDF objects out of 1000 (max. 8388607)
52 compressed objects within 1 object stream
0 named destinations out of 1000 (max. 500000)
1 words of extra memory for PDF output out of 10000 (max. 10000000)
@@ -1,138 +0,0 @@
\documentclass[11pt]{article}
\usepackage{amsmath,amssymb,amsthm}
\usepackage{graphicx}
\usepackage{geometry}
\usepackage{booktabs}
\geometry{margin=1in}
\title{A double-contraction reductio at a degree-5 vertex\\
via Kempe chains as Heawood face-chains}
\author{}
\date{}
\newtheorem*{obs}{Observation}
\newtheorem*{prop}{Proposition}
\newtheorem*{conj}{Conjecture}
\newtheorem*{lem}{Lemma}
\newtheorem*{claim}{Claim}
\newtheorem*{remk}{Remark}
\begin{document}
\maketitle
\emph{Status: exploratory strategy note. Records a proof skeleton and the
single lemma it reduces to; nothing here is proved. Companion to
\texttt{boundary\_restriction\_structure.tex} and \texttt{paper.tex}.}
\section*{Setup: the move}
Let $G$ be a minimal counterexample to the Four Colour Theorem (a smallest
triangulation with no proper $4$-vertex-colouring). By Euler $G$ has a
vertex $v$ of degree $\le 5$; degrees $\le 4$ are classically reducible, so
take $\deg(v)=5$. The link of $v$ is a pentagon $a,x,b,y,z$ (consecutive
neighbours adjacent). For a non-adjacent (diagonal) pair, say $a,b$, the
\emph{double contraction} contracts both edges $(a,v)$ and $(b,v)$,
identifying $a=v=b$ into a single vertex. There are $5$ diagonal pairs.
The double contraction is a proper minor with two fewer vertices, hence a
smaller planar graph $G'$; by minimality $G'$ is $4$-colourable. It stays a
simple triangulation iff each contracted edge is non-separating (its
endpoints have exactly two common neighbours).
\section*{Kempe chains as Heawood face-chains}
Work in the cubic dual $G'$ (vertices $=$ faces of $G$, so the Heawood
$\pm1$ labels sit on dual vertices). Tait-colour the edges with the three
nonzero Klein-4 elements $\{a,b,c\}$ (edge colour $=$ colour-difference
across the primal edge). Then:
\begin{obs}[Kempe chain $=$ Tait cycle $=$ flip-set]
A primal Kempe chain (component of two vertex-colour classes of $G$)
corresponds to a connected component of the two-edge-colour subgraph
$\{a,b\}$ of the dual. That subgraph is $2$-regular, so it is a disjoint
union of cycles; each such cycle alternates $a,b,a,b,\dots$ and is therefore
\textbf{always even}. The cycle is literally a cyclic chain of faces of $G$.
A Kempe swap reverses the rotational order $(a,b,c)$ at exactly the vertices
on the cycle, so the set of faces whose Heawood sign \emph{flips} under the
swap is precisely the Kempe chain.
\end{obs}
\begin{obs}[Sign alternation $=$ same-side rule]
Even-ness does \emph{not} force the Heawood signs to alternate along the
cycle. At each cycle vertex the third (off-cycle, colour-$c$) edge points
either inside or outside the region bounded by the cycle. For consecutive
vertices $A,B$:
\[
\text{same side (both in / both out)} \Rightarrow \text{signs flip;}
\qquad
\text{opposite side} \Rightarrow \text{signs repeat.}
\]
(Local chirality computation: the four in/out cases give
$+,-$ / $-,+$ / $+,+$ / $-,-$.) Hence the Heawood sign sequence around a
Tait cycle is determined, up to one global sign, by the in/out pattern of
the third edges; it is fully alternating iff there are zero side-switches,
i.e.\ all third edges lie on one side.
\end{obs}
\section*{The transport hypothesis (L1)}
\begin{conj}[Chain transport, claimed for all triangulations]
The double contraction preserves the relevant (crossing) Kempe/Heawood
chain structure as it propagates up the nested tire decomposition: the two
chains anchored at the pentagon survive, consistently, through every tire
interface above $v$.
\end{conj}
\section*{The reductio (two nested contradictions)}
We are \emph{not} claiming $G'$ is uncolourable, and we are not claiming
intertwined Kempe chains are impossible (they occur, e.g.\ in the Errera
graph). The reducible object is \emph{forced} intertwining.
\paragraph{Inner.} Suppose intertwining is the \emph{only} way to colour
$G'$ (every colouring is intertwined at $v$). By transport the two crossing
chains pass through every interface above; if uncrossing is excluded at each
interface, the tire restriction relations $R_{\mathsf K}$ collapse to their
``crossed-only'' sub-relations. Contract each interface \textbf{along the
chain} to obtain a strictly smaller triangulation $H''$. If the crossed-only
sub-relations admit no compatible gluing on $H''$, then $H''$ is
uncolourable and smaller than $G$ --- contradicting minimality.
\paragraph{Outer.} Therefore $G'$ admits a non-intertwined colouring; it
uncrosses at $v$, frees a colour for $v$, and lifts to a colouring of $G$ ---
contradicting that $G$ is a counterexample. \hfill$\square$ (modulo the
lemma below)
\section*{What it all reduces to}
\begin{claim}[the only open content]
Under the crossed-only collapse, the surviving Heawood boundary sequences at
a (forced-short) tire interface have empty pointwise-negation gluing on
$H''$ --- not merely small, genuinely empty.
\end{claim}
This is where the $2^{n-2}$ constraint floor and the mod-3 side-pattern must
do real work: the floor guarantees that \emph{smallness alone} never empties
a relation, so the emptiness must come from the chains pinning the
sub-relation down, not from short interface length.
\section*{Open points / to pin next}
\begin{itemize}
\item \textbf{Define $H''$ precisely.} Current candidate: contract each tire
interface along the transported chain (the global analogue of the local
double contraction). Confirm this is a definite, strictly smaller
triangulation.
\item \textbf{Lift is not automatic.} ``Non-intertwined'' must be defined as
``admits a swap collapsing the pentagon $a,x,b,y,z$ to $\le 3$ colours,''
or freeing $v$ fails even after uncrossing (with $c(a)=c(b)$ the pentagon
can still show $4$ colours).
\item \textbf{Errera oracle.} Run the whole construction against the Errera
graph (and Fritsch / Kittell). These are colourable but Kempe-intertwined;
the hypothesis ``every colouring intertwined'' must \emph{fail} for them,
so the construction must decline. \emph{Why} it declines is exactly the
mechanism the Claim must deny in the counterexample case.
\item \textbf{Prove or break L1} as a statement about all triangulations.
\end{itemize}
\end{document}
@@ -1,56 +0,0 @@
\relax
\citation{bauerfeld-nested-tires}
\citation{bauerfeld-nested-tires}
\citation{bauerfeld-nested-tires}
\citation{bauerfeld-nested-tires}
\citation{bauerfeld-nested-tires}
\citation{bauerfeld-nested-tires}
\citation{Heawood1898}
\citation{bauerfeld-medial-tires}
\citation{bauerfeld-nested-tires}
\@writefile{toc}{\contentsline {section}{\tocsection {}{1}{Introduction}}{1}{}\protected@file@percent }
\citation{bauerfeld-nested-tires}
\citation{bauerfeld-nested-tires}
\@writefile{toc}{\contentsline {section}{\tocsection {}{2}{Connected tire clusters}}{2}{}\protected@file@percent }
\newlabel{sec:tire-clusters}{{2}{2}}
\newlabel{lem:same-depth-vertex-meet}{{2.1}{2}}
\newlabel{def:connected-tire-cluster}{{2.2}{2}}
\newlabel{rem:cluster-cut-vertices}{{2.3}{2}}
\newlabel{prop:two-clusters-per-vertex}{{2.4}{2}}
\citation{bauerfeld-nested-tires}
\citation{bauerfeld-nested-tires}
\@writefile{toc}{\contentsline {section}{\tocsection {}{3}{Heawood restrictions on the tire dual}}{3}{}\protected@file@percent }
\newlabel{sec:heawood-restrictions}{{3}{3}}
\newlabel{def:heawood-labelling}{{3.1}{3}}
\newlabel{rem:no-interior-constraint}{{3.2}{3}}
\newlabel{def:boundary-sequences}{{3.3}{3}}
\newlabel{def:heawood-compatible}{{3.4}{3}}
\citation{Heawood1898}
\newlabel{rem:compat-is-heawood}{{3.5}{4}}
\newlabel{eq:heawood-face-sum-dual}{{3.1}{4}}
\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{}{Why the programme runs between nested clusters}}{4}{}\protected@file@percent }
\newlabel{prop:two-sided-decomposition}{{3.6}{4}}
\citation{bauerfeld-nested-tires}
\newlabel{rem:why-clusters}{{3.7}{5}}
\newlabel{conj:heawood-chain-pigeonhole}{{3.8}{5}}
\newlabel{conj:heawood-route-fct}{{3.9}{5}}
\@writefile{toc}{\contentsline {section}{\tocsection {}{4}{The constraint floor}}{6}{}\protected@file@percent }
\newlabel{sec:constraint-floor}{{4}{6}}
\newlabel{def:achievable-boundary-set}{{4.1}{6}}
\newlabel{prop:attainment}{{4.2}{6}}
\newlabel{lem:unstack}{{4.3}{6}}
\newlabel{conj:constraint-floor}{{4.4}{6}}
\newlabel{rem:floor-status}{{4.5}{6}}
\bibcite{Heawood1898}{1}
\bibcite{bauerfeld-depth}{2}
\bibcite{bauerfeld-nested-tires}{3}
\bibcite{bauerfeld-medial-tires}{4}
\bibcite{bauerfeld-nested-tire-duals}{5}
\newlabel{tocindent-1}{0pt}
\newlabel{tocindent0}{14.69437pt}
\newlabel{tocindent1}{17.77782pt}
\newlabel{tocindent2}{0pt}
\newlabel{tocindent3}{0pt}
\newlabel{rem:floor-consequences}{{4.6}{7}}
\@writefile{toc}{\contentsline {section}{\tocsection {}{}{References}}{7}{}\protected@file@percent }
\gdef \@abspage@last{7}
@@ -1,64 +0,0 @@
# Fdb version 3
["pdflatex"] 1781668577 "paper.tex" "paper.pdf" "paper" 1781668578
"/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 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/symbols/msam10.tfm" 1246382020 916 f87d7c45f9c908e672703b83b72241a3 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/symbols/msam5.tfm" 1246382020 924 9904cf1d39e9767e7a3622f2a125a565 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/symbols/msam7.tfm" 1246382020 928 2dc8d444221b7a635bb58038579b861a ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/symbols/msbm10.tfm" 1246382020 908 2921f8a10601f252058503cc6570e581 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/symbols/msbm5.tfm" 1246382020 940 75ac932a52f80982a9f8ea75d03a34cf ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/symbols/msbm7.tfm" 1246382020 940 228d6584342e91276bf566bcf9716b83 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmbx10.tfm" 1136768653 1328 c834bbb027764024c09d3d2bf908b5f0 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmbx8.tfm" 1136768653 1332 1fde11373e221473104d6cc5993f046e ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmcsc10.tfm" 1136768653 1300 63a6111ee6274895728663cf4b4e7e81 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmmi6.tfm" 1136768653 1512 f21f83efb36853c0b70002322c1ab3ad ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmmi8.tfm" 1136768653 1520 eccf95517727cb11801f4f1aee3a21b4 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmr6.tfm" 1136768653 1300 b62933e007d01cfd073f79b963c01526 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmr8.tfm" 1136768653 1292 21c1c5bfeaebccffdb478fd231a0997d ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmsy6.tfm" 1136768653 1116 933a60c408fc0a863a92debe84b2d294 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmsy8.tfm" 1136768653 1120 8b7d695260f3cff42e636090a8002094 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmti10.tfm" 1136768653 1480 aa8e34af0eb6a2941b776984cf1dfdc4 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmti8.tfm" 1136768653 1504 1747189e0441d1c18f3ea56fafc1c480 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx10.pfb" 1248133631 34811 78b52f49e893bcba91bd7581cdc144c0 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx8.pfb" 1248133631 32166 b0c356b15f19587482a9217ce1d8fa67 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmcsc10.pfb" 1248133631 32001 6aeea3afe875097b1eb0da29acd61e28 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi10.pfb" 1248133631 36299 5f9df58c2139e7edcf37c8fca4bd384d ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi7.pfb" 1248133631 36281 c355509802a035cadc5f15869451dcee ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr10.pfb" 1248133631 35752 024fb6c41858982481f6968b5fc26508 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr7.pfb" 1248133631 32762 224316ccc9ad3ca0423a14971cfa7fc1 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr8.pfb" 1248133631 32726 0a1aea6fcd6468ee2cf64d891f5c43c8 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy10.pfb" 1248133631 32569 5e5ddc8df908dea60932f3c484a54c0d ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy7.pfb" 1248133631 32716 08e384dc442464e7285e891af9f45947 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti10.pfb" 1248133631 37944 359e864bd06cde3b1cf57bb20757fb06 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti8.pfb" 1248133631 35660 fb24af7afbadb71801619f1415838111 ""
"/usr/local/texlive/2022/texmf-dist/tex/context/base/mkii/supp-pdf.mkii" 1461363279 71627 94eb9990bed73c364d7f53f960cc8c5b ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/amscls/amsart.cls" 1591045760 61881 a7369c346c2922a758ae6283cc1ed014 ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amsfonts.sty" 1359763108 5949 3f3fd50a8cc94c3d4cbf4fc66cd3df1c ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amssymb.sty" 1359763108 13829 94730e64147574077f8ecfea9bb69af4 ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsa.fd" 1359763108 961 6518c6525a34feb5e8250ffa91731cff ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsb.fd" 1359763108 961 d02606146ba5601b5645f987c92e6193 ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsbsy.sty" 1622667781 2222 da905dc1db75412efd2d8f67739f0596 ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsgen.sty" 1622667781 4173 bc0410bcccdff806d6132d3c1ef35481 ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsmath.sty" 1636758526 87648 07fbb6e9169e00cb2a2f40b31b2dbf3c ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsopn.sty" 1636758526 4128 8eea906621b6639f7ba476a472036bbe ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amstext.sty" 1636758526 2444 926f379cc60fcf0c6e3fee2223b4370d ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty" 1579991033 13886 d1306dcf79a944f6988e688c1785f9ce ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/graphics-cfg/graphics.cfg" 1465944070 1224 978390e9c2234eab29404bc21b268d1e ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/graphics-def/pdftex.def" 1601931164 19103 48d29b6e2a64cb717117ef65f107b404 ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphics.sty" 1622581934 18399 7e40f80366dffb22c0e7b70517db5cb4 ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphicx.sty" 1636758526 7996 a8fb260d598dcaf305a7ae7b9c3e3229 ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/keyval.sty" 1622581934 2671 4de6781a30211fe0ea4c672e4a2a8166 ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/trig.sty" 1636758526 4009 187ea2dc3194cd5a76cd99a8d7a6c4d0 ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/l3backend/l3backend-pdftex.def" 1644269979 29921 d0acc05a38bd4aa3af2017f0b7c137ce ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/latexconfig/epstopdf-sys.cfg" 1279039959 678 4792914a8f45be57bb98413425e4c7af ""
"/usr/local/texlive/2022/texmf-dist/web2c/texmf.cnf" 1646502317 40171 cdab547de63d26590bebb3baff566530 ""
"/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" 1781668577 894 c07070c22299dc3a5b11f2d70e9e6864 "pdflatex"
"paper.tex" 1781668572 3984 c8e5ad80c1dfc803df154b6857fc59b0 ""
(generated)
"paper.aux"
"paper.log"
"paper.pdf"
@@ -1,231 +0,0 @@
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 17 JUN 2026 21:31
entering extended mode
restricted \write18 enabled.
%&-line parsing enabled.
**paper.tex
(./paper.tex
LaTeX2e <2021-11-15> patch level 1
L3 programming layer <2022-02-24>
(/usr/local/texlive/2022/texmf-dist/tex/latex/amscls/amsart.cls
Document Class: amsart 2020/05/29 v2.20.6
\linespacing=\dimen138
\normalparindent=\dimen139
\normaltopskip=\skip47
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsmath.sty
Package: amsmath 2021/10/15 v2.17l AMS math features
\@mathmargin=\skip48
For additional information on amsmath, use the `?' option.
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amstext.sty
Package: amstext 2021/08/26 v2.01 AMS text
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsgen.sty
File: amsgen.sty 1999/11/30 v2.0 generic functions
\@emptytoks=\toks16
\ex@=\dimen140
))
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsbsy.sty
Package: amsbsy 1999/11/29 v1.2d Bold Symbols
\pmbraise@=\dimen141
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsopn.sty
Package: amsopn 2021/08/26 v2.02 operator names
)
\inf@bad=\count185
LaTeX Info: Redefining \frac on input line 234.
\uproot@=\count186
\leftroot@=\count187
LaTeX Info: Redefining \overline on input line 399.
\classnum@=\count188
\DOTSCASE@=\count189
LaTeX Info: Redefining \ldots on input line 496.
LaTeX Info: Redefining \dots on input line 499.
LaTeX Info: Redefining \cdots on input line 620.
\Mathstrutbox@=\box50
\strutbox@=\box51
\big@size=\dimen142
LaTeX Font Info: Redeclaring font encoding OML on input line 743.
LaTeX Font Info: Redeclaring font encoding OMS on input line 744.
\macc@depth=\count190
\c@MaxMatrixCols=\count191
\dotsspace@=\muskip16
\c@parentequation=\count192
\dspbrk@lvl=\count193
\tag@help=\toks17
\row@=\count194
\column@=\count195
\maxfields@=\count196
\andhelp@=\toks18
\eqnshift@=\dimen143
\alignsep@=\dimen144
\tagshift@=\dimen145
\tagwidth@=\dimen146
\totwidth@=\dimen147
\lineht@=\dimen148
\@envbody=\toks19
\multlinegap=\skip49
\multlinetaggap=\skip50
\mathdisplay@stack=\toks20
LaTeX Info: Redefining \[ on input line 2938.
LaTeX Info: Redefining \] on input line 2939.
)
LaTeX Font Info: Trying to load font information for U+msa on input line 397
.
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsa.fd
File: umsa.fd 2013/01/14 v3.01 AMS symbols A
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amsfonts.sty
Package: amsfonts 2013/01/14 v3.01 Basic AMSFonts support
\symAMSa=\mathgroup4
\symAMSb=\mathgroup5
LaTeX Font Info: Redeclaring math symbol \hbar on input line 98.
LaTeX Font Info: Overwriting math alphabet `\mathfrak' in version `bold'
(Font) U/euf/m/n --> U/euf/b/n on input line 106.
)
\copyins=\insert199
\abstractbox=\box52
\listisep=\skip51
\c@part=\count197
\c@section=\count198
\c@subsection=\count266
\c@subsubsection=\count267
\c@paragraph=\count268
\c@subparagraph=\count269
\c@figure=\count270
\c@table=\count271
\abovecaptionskip=\skip52
\belowcaptionskip=\skip53
\captionindent=\dimen149
\thm@style=\toks21
\thm@bodyfont=\toks22
\thm@headfont=\toks23
\thm@notefont=\toks24
\thm@headpunct=\toks25
\thm@preskip=\skip54
\thm@postskip=\skip55
\thm@headsep=\skip56
\dth@everypar=\toks26
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amssymb.sty
Package: amssymb 2013/01/14 v3.01 AMS font symbols
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphicx.sty
Package: graphicx 2021/09/16 v1.2d Enhanced LaTeX Graphics (DPC,SPQR)
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/keyval.sty
Package: keyval 2014/10/28 v1.15 key=value parser (DPC)
\KV@toks@=\toks27
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphics.sty
Package: graphics 2021/03/04 v1.4d Standard LaTeX Graphics (DPC,SPQR)
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/trig.sty
Package: trig 2021/08/11 v1.11 sin cos tan (DPC)
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics-cfg/graphics.cfg
File: graphics.cfg 2016/06/04 v1.11 sample graphics configuration
)
Package graphics Info: Driver file: pdftex.def on input line 107.
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics-def/pdftex.def
File: pdftex.def 2020/10/05 v1.2a Graphics/color driver for pdftex
))
\Gin@req@height=\dimen150
\Gin@req@width=\dimen151
)
\c@theorem=\count272
(/usr/local/texlive/2022/texmf-dist/tex/latex/l3backend/l3backend-pdftex.def
File: l3backend-pdftex.def 2022-02-07 L3 backend support: PDF output (pdfTeX)
\l__color_backend_stack_int=\count273
\l__pdf_internal_box=\box53
)
(./paper.aux)
\openout1 = `paper.aux'.
LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 27.
LaTeX Font Info: ... okay on input line 27.
LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 27.
LaTeX Font Info: ... okay on input line 27.
LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 27.
LaTeX Font Info: ... okay on input line 27.
LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 27.
LaTeX Font Info: ... okay on input line 27.
LaTeX Font Info: Checking defaults for TS1/cmr/m/n on input line 27.
LaTeX Font Info: ... okay on input line 27.
LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 27.
LaTeX Font Info: ... okay on input line 27.
LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 27.
LaTeX Font Info: ... okay on input line 27.
LaTeX Font Info: Trying to load font information for U+msa on input line 27.
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsa.fd
File: umsa.fd 2013/01/14 v3.01 AMS symbols A
)
LaTeX Font Info: Trying to load font information for U+msb on input line 27.
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsb.fd
File: umsb.fd 2013/01/14 v3.01 AMS symbols B
)
(/usr/local/texlive/2022/texmf-dist/tex/context/base/mkii/supp-pdf.mkii
[Loading MPS to PDF converter (version 2006.09.02).]
\scratchcounter=\count274
\scratchdimen=\dimen152
\scratchbox=\box54
\nofMPsegments=\count275
\nofMParguments=\count276
\everyMPshowfont=\toks28
\MPscratchCnt=\count277
\MPscratchDim=\dimen153
\MPnumerator=\count278
\makeMPintoPDFobject=\count279
\everyMPtoPDFconversion=\toks29
) (/usr/local/texlive/2022/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty
Package: epstopdf-base 2020-01-24 v2.11 Base part for package epstopdf
Package epstopdf-base Info: Redefining graphics rule for `.eps' on input line 4
85.
(/usr/local/texlive/2022/texmf-dist/tex/latex/latexconfig/epstopdf-sys.cfg
File: epstopdf-sys.cfg 2010/07/13 v1.3 Configuration of (r)epstopdf for TeX Liv
e
))
[1{/usr/local/texlive/2022/texmf-var/fonts/map/pdftex/updmap/pdftex.map}]
[2] [3] [4] [5] [6] [7] (./paper.aux) )
Here is how much of TeX's memory you used:
3024 strings out of 478268
42307 string characters out of 5846347
342360 words of memory out of 5000000
21070 multiletter control sequences out of 15000+600000
477578 words of font info for 59 fonts, out of 8000000 for 9000
1302 hyphenation exceptions out of 8191
69i,7n,76p,242b,290s stack positions out of 10000i,1000n,20000p,200000b,200000s
</usr/local/texlive/2022/texmf-dist/font
s/type1/public/amsfonts/cm/cmbx10.pfb></usr/local/texlive/2022/texmf-dist/fonts
/type1/public/amsfonts/cm/cmbx8.pfb></usr/local/texlive/2022/texmf-dist/fonts/t
ype1/public/amsfonts/cm/cmcsc10.pfb></usr/local/texlive/2022/texmf-dist/fonts/t
ype1/public/amsfonts/cm/cmex10.pfb></usr/local/texlive/2022/texmf-dist/fonts/ty
pe1/public/amsfonts/cm/cmmi10.pfb></usr/local/texlive/2022/texmf-dist/fonts/typ
e1/public/amsfonts/cm/cmmi5.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1
/public/amsfonts/cm/cmmi7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/p
ublic/amsfonts/cm/cmr10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/pub
lic/amsfonts/cm/cmr5.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public
/amsfonts/cm/cmr7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/am
sfonts/cm/cmr8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfo
nts/cm/cmss10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfon
ts/cm/cmss8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts
/cm/cmsy10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/
cm/cmsy5.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm
/cmsy7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/c
mti10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cm
ti8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/symbols
/msam10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/sym
bols/msbm10.pfb>
Output written on paper.pdf (7 pages, 269140 bytes).
PDF statistics:
128 PDF objects out of 1000 (max. 8388607)
78 compressed objects within 1 object stream
0 named destinations out of 1000 (max. 500000)
1 words of extra memory for PDF output out of 10000 (max. 10000000)
@@ -1,575 +0,0 @@
%% filename: amsart-template.tex
%% American Mathematical Society
%% AMS-LaTeX v.2 template for use with amsart
%% ====================================================================
\documentclass{amsart}
\usepackage{amssymb}
\usepackage{graphicx}
\newtheorem{theorem}{Theorem}[section]
\newtheorem{lemma}[theorem]{Lemma}
\newtheorem{corollary}[theorem]{Corollary}
\newtheorem{proposition}[theorem]{Proposition}
\newtheorem{conjecture}[theorem]{Conjecture}
\theoremstyle{definition}
\newtheorem{definition}[theorem]{Definition}
\newtheorem{example}[theorem]{Example}
\newtheorem{xca}[theorem]{Exercise}
\theoremstyle{remark}
\newtheorem{remark}[theorem]{Remark}
\numberwithin{equation}{section}
\begin{document}
\title{Heawood Restrictions on Nested Tire Graph Duals}
% author one information
\author{Eric Bauerfeld}
\address{}
\curraddr{}
\email{}
\thanks{}
\subjclass[2010]{Primary }
\keywords{plane graph, triangulation, plane depth, level edge, dual graph,
tire graph, Heawood number}
\date{}
\dedicatory{}
\begin{abstract}
%% TODO: abstract. Following \cite{bauerfeld-nested-tires}, which establishes
%% the basic vocabulary of tire graphs and dual depth, we study the Heawood
%% (mod-3 / face-sum) restrictions imposed on the duals of nested tire graphs.
\end{abstract}
\maketitle
\section{Introduction}
A classical theorem of Tait recasts the Four Colour Theorem in dual,
edge-colouring terms: a plane triangulation $G$ is properly $4$-vertex-colourable
if and only if its dual cubic graph $G'$ is properly $3$-edge-colourable. Thus a
minimal counterexample to the Four Colour Theorem -- a smallest triangulation
admitting no proper $4$-colouring -- corresponds to a smallest cubic plane graph
admitting no proper $3$-edge-colouring.
This paper continues the series studying that structure through the
lens of \emph{nested level duals}. The foundational vocabulary ---
level sources, levels, the inner planar dual $G'$ and its dual depth,
and tire graphs --- is developed in the companion paper
\cite{bauerfeld-nested-tires}; we refer to that paper for those
definitions and rely on them throughout. In particular we use,
without restating, the notions of:
\begin{itemize}
\item \emph{level source} $S$ and $G$-vertex levels $\ell_G(v)$;
\item the inner planar dual $G'$
(\cite[Definition~1.3]{bauerfeld-nested-tires});
\item \emph{dual depth} $\delta_G(d_f)$
(\cite[Definition~1.4]{bauerfeld-nested-tires});
\item \emph{tire graph} $T = (B_{\mathrm{out}}, O, E_{\mathrm{ann}})$
with outer/inner boundaries and annular edges
(\cite[Definition~1.5]{bauerfeld-nested-tires});
\item the \emph{tire-component lemma}
(\cite[Lemma~1.8]{bauerfeld-nested-tires}); and
\item the \emph{tire-tread partition theorem}
(\cite[Theorem~1.9]{bauerfeld-nested-tires}).
\end{itemize}
Throughout, $G = (V, E)$ is a plane maximal planar graph (a triangulation)
with a fixed planar embedding $\Pi_G$. We write $|V| = n$, so $|E| = 3n - 6$
and $G$ has $2n - 4$ triangular faces.
The classical input is Heawood's face-sum identity \cite{Heawood1898}:
for any proper $3$-edge-colouring of a cubic plane graph $H$, assigning
each face of $H$ a number in $\{+1, -1\}$ can be done so that the labels
around every vertex of $H$ sum to $0 \pmod 3$. In the triangulation
$G$ dual to $H$ this becomes a $\{+1, -1\}$ labelling of the
\emph{faces} of $G$ whose incident-face sum at every vertex of $G$
vanishes mod $3$. Our aim is to record what this restriction forces
along the boundary cycles of a nested tire graph, and to formulate a
chain-pigeonhole programme in this Heawood labelling parallel to the
medial programme of \cite{bauerfeld-medial-tires}.
\section{Connected tire clusters}
\label{sec:tire-clusters}
The tire treads at a fixed depth partition the depth-$d$ faces of $G$
\cite{bauerfeld-nested-tires}, but distinct depth-$d$ tires need not be
vertex-disjoint: a single vertex of $G$ may lie on the source-side
boundary of several depth-$d$ tires at once (this occurs exactly when
the depth-$d$ faces around that vertex are split into more than one arc
by depth-$(d{-}1)$ faces). We organise the depth-$d$ tires by this
sharing.
\begin{lemma}[Same-depth tires meet only in vertices]
\label{lem:same-depth-vertex-meet}
Let $T \neq T'$ be two distinct tire treads at the same depth $d$ in
$\mathcal{T}(G, S)$, arising from connected components $C', C''$ of the
depth-$d$ dual subgraph $G'_d$. Then $T$ and $T'$ share no edge of $G$;
any intersection $V(T) \cap V(T')$ consists of isolated vertices.
\end{lemma}
\begin{proof}
An edge $e$ of $G$ shared by two depth-$d$ annular faces $f_1, f_2$ is,
by definition of the inner dual \cite[Definition~1.3]{bauerfeld-nested-tires},
a dual edge of $G'$ joining $d_{f_1}$ and $d_{f_2}$; since $\delta(d_{f_1})
= \delta(d_{f_2}) = d$, this edge lies in $G'_d$, so $d_{f_1}$ and
$d_{f_2}$ belong to the same component of $G'_d$. Hence no edge of $G$
is shared by annular faces of two \emph{different} components, and
distinct depth-$d$ tires share no edge. Their intersection is therefore
a set of isolated vertices.
\end{proof}
\begin{definition}[Connected tire cluster]
\label{def:connected-tire-cluster}
Fix a nested tire decomposition $\mathcal{T}(G, S)$ and a depth $d$. On
the set of depth-$d$ tire treads define the relation
\[
T \sim T' \quad\Longleftrightarrow\quad V(T) \cap V(T') \neq \varnothing .
\]
A \emph{connected tire cluster} at depth $d$ is the subgraph of $G$
\[
\mathsf{K} \;=\; \bigcup_{i} T_i \;\subseteq\; G
\]
obtained as the union (of underlying plane graphs) of the tires in a
single connected component $\{T_i\}$ of the transitive closure of
$\sim$. A cluster consisting of a single tire is \emph{trivial}; the
connected tire clusters at depth $d$ partition the depth-$d$ tires.
\end{definition}
\begin{remark}
\label{rem:cluster-cut-vertices}
By Lemma~\ref{lem:same-depth-vertex-meet} the constituent tires of a
connected tire cluster are joined only at shared vertices, each of which
is a cut vertex of $\mathsf{K}$; a connected tire cluster is thus a
``cactus of tires'' and is in general \emph{not} itself a tire graph,
since the annulus structure of \cite[Definition~1.5]{bauerfeld-nested-tires}
fails at each such pinch. The shared (cut) vertices are precisely the
vertices that belong to more than one depth-$d$ tire.
\end{remark}
A single vertex may belong to several tires at one depth --- the
high-degree case where its depth-$d$ faces split into many arcs --- so
the number of \emph{tires} through a vertex is unbounded. Clustering
collapses exactly this multiplicity: all tires through a vertex at a
fixed depth share that vertex, hence lie in one cluster. The cluster
count is therefore controlled.
\begin{proposition}[A vertex meets at most two clusters]
\label{prop:two-clusters-per-vertex}
Every vertex $v \in V(G)$ belongs to at most two connected tire
clusters, namely at most one at each of the two consecutive depths
$\ell_G(v) - 1$ and $\ell_G(v)$. In particular a source vertex
($\ell_G(v) = 0$) belongs to a single cluster.
\end{proposition}
\begin{proof}
Write $\ell = \ell_G(v)$.
\emph{Step 1: every bounded face incident to $v$ has dual depth
$\ell - 1$ or $\ell$.} Let $f$ be a bounded triangular face with
$v \in V(f)$. Then
$\delta_G(d_f) = \min_{u \in V(f)} \ell_G(u) \le \ell_G(v) = \ell$.
The other two vertices of $f$ are adjacent to $v$ in $G$, and the level
function $\ell_G(\cdot) = \mathrm{dist}_G(\cdot, S)$ is $1$-Lipschitz
along edges, so each has level at least $\ell - 1$; hence
$\delta_G(d_f) \ge \ell - 1$. Thus $\delta_G(d_f) \in \{\ell-1, \ell\}$
(only $\delta_G(d_f) = 0$ when $\ell = 0$), so $v$ bounds faces of, and
therefore belongs to tires of, no depth other than $\ell - 1$ or
$\ell$.
\emph{Step 2: at each depth, all tires through $v$ lie in one cluster.}
Fix $d \in \{\ell-1, \ell\}$ and let $T, T'$ be depth-$d$ tires with
$v \in V(T) \cap V(T')$. Then $V(T) \cap V(T') \ne \varnothing$, so
$T \sim T'$ in the sense of
Definition~\ref{def:connected-tire-cluster}, and all depth-$d$ tires
containing $v$ lie in a single connected component of $\sim$ --- one
connected tire cluster $\mathsf{K}_d$.
Combining the two steps, $v$ belongs to at most the clusters
$\mathsf{K}_{\ell-1}$ and $\mathsf{K}_{\ell}$, i.e.\ to at most two
connected tire clusters; when $\ell = 0$ only $\mathsf{K}_0$ occurs.
\end{proof}
\section{Heawood restrictions on the tire dual}
\label{sec:heawood-restrictions}
We work inside a fixed nested tire decomposition $\mathcal{T}(G, S)$ of
$G$ from a single-vertex level source $S$ \cite{bauerfeld-nested-tires},
and use the tire data $T = (B_{\mathrm{out}}, O, E_{\mathrm{ann}})$ with
annular faces $F_{\mathrm{ann}}$, outer boundary $B_{\mathrm{out}}$, and
inner boundary $B_{\mathrm{in}}$
(\cite[Definition~1.5]{bauerfeld-nested-tires}). Since $O$ is
outerplanar, every vertex of a tire lies on $B_{\mathrm{out}}$ or on the
inner-boundary walk $B_{\mathrm{in}}$; a tire has no interior vertices.
\begin{definition}[Heawood face-labelling of a tire]
\label{def:heawood-labelling}
A \emph{Heawood face-labelling} of a tire graph $T$ is a map
\[
\lambda : F_{\mathrm{ann}} \longrightarrow \{+1, -1\}
\]
assigning a sign to each annular face of $T$. For a vertex
$v \in V(T)$, write $F_{\mathrm{ann}}(v) \subseteq F_{\mathrm{ann}}$ for
the set of annular faces of $T$ incident to $v$, and define the
\emph{induced vertex value}
\[
\lambda^{\!*}(v) \;:=\; \sum_{f \in F_{\mathrm{ann}}(v)} \lambda(f)
\;\;\bmod 3 \;\in\; \{0, 1, -1\}.
\]
The value $\lambda^{\!*}(v)$ is the \emph{partial} face-sum at $v$ taken
over the annular faces of $T$ alone, not over all faces of $G$ incident
to $v$.
\end{definition}
\begin{remark}
\label{rem:no-interior-constraint}
Because a tire has no interior vertices, every annular face of $T$ is
incident to $B_{\mathrm{out}} \cup B_{\mathrm{in}}$, and a Heawood
face-labelling is subject to \emph{no} internal constraint: all
$2^{|F_{\mathrm{ann}}|}$ sign assignments are admissible. The Heawood
restriction is felt only on the two boundary cycles, through the induced
vertex values $\lambda^{\!*}$.
\end{remark}
\begin{definition}[Induced boundary sequences]
\label{def:boundary-sequences}
Let $\lambda$ be a Heawood face-labelling of $T$. Reading the vertices
of $B_{\mathrm{out}}$ in clockwise order $v_0, v_1, \dots, v_{p-1}$, the
\emph{outer Heawood sequence} of $(T, \lambda)$ is
\[
\sigma_{\mathrm{out}}(T, \lambda)
\;:=\; \bigl(\lambda^{\!*}(v_0), \dots, \lambda^{\!*}(v_{p-1})\bigr)
\;\in\; \{0, 1, -1\}^{p}.
\]
Reading the inner-boundary walk $B_{\mathrm{in}}$ in clockwise order
$w_0, \dots, w_{q-1}$ gives the \emph{inner Heawood sequence}
$\sigma_{\mathrm{in}}(T, \lambda) \in \{0, 1, -1\}^{q}$. The
\emph{Heawood restriction relation} of $T$ is the set
\[
R_T \;:=\; \bigl\{\,
\bigl(\sigma_{\mathrm{out}}(T, \lambda),\,
\sigma_{\mathrm{in}}(T, \lambda)\bigr)
\;:\; \lambda : F_{\mathrm{ann}} \to \{+1, -1\}
\,\bigr\}
\]
of all (outer, inner) sequence pairs realisable by a single
face-labelling, read up to rotation and the global sign-flip
$\lambda \mapsto -\lambda$ (equivalently
$\sigma \mapsto -\sigma$).
\end{definition}
\begin{definition}[Heawood compatibility across an interface]
\label{def:heawood-compatible}
Let $T$ be a tire and $T' \in \mathcal{T}(G, S)$ a child of $T$, so the
outer boundary cycle $B_{\mathrm{out}}^{(T')}$ coincides with a bounded
face of $O^{(T)}$; let $\gamma$ be this shared cycle, of length $L$, and
let $v$ range over its vertices. Heawood face-labellings $\lambda$ of
$T$ and $\lambda'$ of $T'$ are \emph{compatible along $\gamma$} if at
every shared vertex $v$,
\[
\lambda^{\!*}(v) + (\lambda')^{\!*}(v) \;\equiv\; 0 \pmod 3,
\]
i.e.\ $0$ is paired with $0$ and $+1$ with $-1$. Equivalently, the
inner Heawood sequence of $T$ on $\gamma$ is the pointwise negation
mod $3$ of the outer Heawood sequence of $T'$ on $\gamma$, after
reversing one of the two clockwise readings to account for the opposite
rotational senses in which $T$ and $T'$ traverse $\gamma$.
\end{definition}
\begin{remark}
\label{rem:compat-is-heawood}
Call $v$ \emph{interior} if it is not incident to the outer face of
$\Pi_G$. For an interior vertex every incident face is bounded, and
compatibility along $\gamma$ at $v$ is exactly the statement that the
incident-face sum at $v$ --- over the parent's annular faces together
with the child's --- vanishes mod $3$:
\begin{equation}
\label{eq:heawood-face-sum-dual}
\sum_{f \ni v} \lambda(f) \;\equiv\; 0 \pmod 3
\qquad\text{for every interior vertex } v \in V(G),
\end{equation}
the sum ranging over the bounded faces incident to $v$. The interfaces
of $\mathcal{T}(G, S)$ are interior level cycles, so cluster
compatibility only ever constrains interior vertices and is untouched by
the outer face.
To pass from \eqref{eq:heawood-face-sum-dual} to a colouring one must
account for the outer face: an outer-boundary vertex is incident to the
unbounded face $f_\infty$, whose label is omitted from the bounded sum.
Extend $\lambda$ by a single label $\lambda(f_\infty) \in \{+1, -1\}$ on
$f_\infty$. Then a family of Heawood face-labellings that is pairwise
compatible along every interface of $\mathcal{T}(G, S)$ assembles into a
$\{+1,-1\}$ labelling of \emph{all} faces of $G$ for which
$\sum_{f \ni v} \lambda(f) \equiv 0 \pmod 3$ holds at every vertex ---
the outer-boundary vertices now carrying $\lambda(f_\infty)$ in their
sum. This is Heawood's face-sum identity \cite{Heawood1898} for a
proper $3$-edge-colouring of the full cubic dual of $G$, hence (by Tait)
a proper $4$-vertex-colouring of $G$.
\end{remark}
\subsection*{Why the programme runs between nested clusters}
The vanishing condition \eqref{eq:heawood-face-sum-dual} at a vertex $v$
is a constraint on the \emph{full} face-star of $v$. To run a
pigeonhole between two objects --- a child and a parent --- we need that
full sum to split as exactly two one-sided contributions, so that each
vertex label is the combination of a single child value and a single
parent value. This is true at the level of connected tire clusters, and
\emph{false} at the level of individual tires. Extend a Heawood
face-labelling to a connected tire cluster $\mathsf{K}$ by labelling
every annular face of every tire of $\mathsf{K}$, and for $v \in
V(\mathsf{K})$ write
\[
\lambda^{\!*}_{\mathsf{K}}(v) \;:=\;
\sum_{f} \lambda(f) \;\bmod 3,
\]
the sum over the annular faces of $\mathsf{K}$ incident to $v$.
\begin{proposition}[Two-sided cluster decomposition at a vertex]
\label{prop:two-sided-decomposition}
Let $v \in V(G)$ have level $\ell = \ell_G(v)$, and let
$\mathsf{K}_{\ell}$ and $\mathsf{K}_{\ell-1}$ be the at most two
connected tire clusters containing $v$, of depths $\ell$ and $\ell-1$
respectively (Proposition~\ref{prop:two-clusters-per-vertex}). Then the
bounded faces of $G$ incident to $v$ partition into the annular faces of
$\mathsf{K}_{\ell}$ at $v$ and the annular faces of $\mathsf{K}_{\ell-1}$
at $v$, and
\[
\sum_{f \ni v} \lambda(f)
\;\equiv\;
\lambda^{\!*}_{\mathsf{K}_{\ell}}(v) +
\lambda^{\!*}_{\mathsf{K}_{\ell-1}}(v)
\pmod 3 .
\]
Each one-sided value $\lambda^{\!*}_{\mathsf{K}_d}(v)$ is the
\emph{complete} sum over all depth-$d$ faces at $v$, so the Heawood
condition \eqref{eq:heawood-face-sum-dual} at $v$ reads
\[
\lambda^{\!*}_{\mathsf{K}_{\ell}}(v) +
\lambda^{\!*}_{\mathsf{K}_{\ell-1}}(v) \;\equiv\; 0 \pmod 3 ,
\]
a pairing between the single child cluster $\mathsf{K}_{\ell}$ and the
single parent cluster $\mathsf{K}_{\ell-1}$. (When $\ell = 0$, or when
$v$ bounds no depth-$\ell$ face, only one term is present.)
\end{proposition}
\begin{proof}
By Proposition~\ref{prop:two-clusters-per-vertex} (Step~1) every bounded
face incident to $v$ has depth $\ell-1$ or $\ell$, partitioning the
incident faces by depth; by Step~2 all depth-$\ell$ faces at $v$ lie in
the single cluster $\mathsf{K}_{\ell}$ and all depth-$(\ell-1)$ faces at
$v$ in $\mathsf{K}_{\ell-1}$. Hence the depth-$\ell$ part is exactly the
annular faces of $\mathsf{K}_{\ell}$ at $v$, the depth-$(\ell-1)$ part
those of $\mathsf{K}_{\ell-1}$, and summing $\lambda$ over the two parts
gives the identity; \eqref{eq:heawood-face-sum-dual} is its vanishing.
\end{proof}
\begin{remark}[Failure at the tire level]
\label{rem:why-clusters}
Proposition~\ref{prop:two-sided-decomposition} is what makes the binary
parent/child pairing possible, and it requires the cluster. A vertex
$v$ may lie on many depth-$\ell$ tires --- the unbounded case of
Section~\ref{sec:tire-clusters} --- and the per-tire value
$\lambda^{\!*}(v)$ of Definition~\ref{def:heawood-labelling} then records
only the faces of \emph{one} tire at $v$, a fragment of $v$'s face-star.
No single child tire carries the complete depth-$\ell$ sum, so the label
$\sum_{f \ni v}\lambda(f)$ cannot be written as one child value plus one
parent value, and per-tire compatibility
(Definition~\ref{def:heawood-compatible}) fails to assemble to
\eqref{eq:heawood-face-sum-dual}. Clustering repairs this:
Proposition~\ref{prop:two-clusters-per-vertex} guarantees exactly one
cluster meets $v$ on each side, so $\lambda^{\!*}_{\mathsf{K}_{\ell}}(v)$
is the complete child contribution and
$\lambda^{\!*}_{\mathsf{K}_{\ell-1}}(v)$ the complete parent
contribution. Every vertex label is then realised as the combination of
a single child-cluster value with a single parent-cluster value, and the
pigeonhole programme below chains \emph{nested connected tire clusters}
rather than individual tires.
\end{remark}
We write $R_{\mathsf{K}}$ for the \emph{cluster Heawood restriction
relation}: the set of (outer, inner) boundary Heawood sequence pairs
realisable by a face-labelling of $\mathsf{K}$, defined as in
Definition~\ref{def:boundary-sequences} but with the outer and inner
boundaries of the cluster and the complete one-sided values
$\lambda^{\!*}_{\mathsf{K}}$ in place of a single tire's, read up to
rotation and global sign-flip. By
Proposition~\ref{prop:two-sided-decomposition} two nested clusters are
compatible along their shared interface exactly when the inner sequence
of the parent is the pointwise negation mod $3$ of the outer sequence of
the child (after the orientation reversal of
Definition~\ref{def:heawood-compatible}).
\begin{conjecture}[Heawood chain-pigeonhole principle]
\label{conj:heawood-chain-pigeonhole}
There is a function $N(k)$ such that the following holds. Let
\[
\mathsf{K}_0 \supset \mathsf{K}_1 \supset \cdots \supset
\mathsf{K}_{N(k)}
\]
be a nested chain of connected tire clusters in $\mathcal{T}(G, S)$ whose
shared interfaces have length at most $k$. Then two adjacent cluster
restriction relations $R_{\mathsf{K}_i}, R_{\mathsf{K}_{i+1}}$ in the
chain admit compatible face-labellings along their shared interface,
after rotation and global sign-flip. Equivalently, the chain contains a
local gluing step that cannot be obstructed by disjoint Heawood boundary
restrictions.
\end{conjecture}
\begin{conjecture}[Heawood cluster route to the Four Colour Theorem]
\label{conj:heawood-route-fct}
For every plane triangulation $G$ and every level source $S$, the
cluster Heawood restriction relations
$\{R_{\mathsf{K}} : \mathsf{K} \text{ a connected tire cluster}\}$ admit
a selection of face-labellings that is compatible along every cluster
interface. By Proposition~\ref{prop:two-sided-decomposition} and
Remark~\ref{rem:compat-is-heawood} this yields a $\{+1,-1\}$
face-labelling of $G$ satisfying \eqref{eq:heawood-face-sum-dual}, hence
$G$ is properly $4$-vertex-colourable.
\end{conjecture}
%% TODO: realisability of $R_{\mathsf{K}}$ per cluster; counting /
%% pigeonhole bound giving $N(k)$; orientation/reversal bookkeeping on
%% the shared interface.
\section{The constraint floor}
\label{sec:constraint-floor}
A nested substructure constrains its outer interface through the set of
Heawood boundary sequences it can realise. By the self-similarity of the
tire decomposition (\cite{bauerfeld-nested-tires}), the region $G_T$
enclosed by a tire's outer cycle, away from the source, is itself a
triangulated disk; we ask how tightly any such disk can constrain its
boundary. The achievable set below depends only on the disk
triangulation, not on a tire-tree labelling.
\begin{definition}[Achievable boundary set of a disk]
\label{def:achievable-boundary-set}
Let $D$ be a triangulated disk whose boundary is a simple $n$-cycle
$C = (v_0, \dots, v_{n-1})$. Call a Heawood face-labelling
$\lambda : F(D) \to \{+1,-1\}$ \emph{interior-valid} if
$\sum_{f \ni w} \lambda(f) \equiv 0 \pmod 3$ at every interior vertex $w$
of $D$ (no condition on $C$). The \emph{achievable boundary set} of $D$
is
\[
\Phi(D) \;:=\; \bigl\{\,
(\lambda^{*}(v_0), \dots, \lambda^{*}(v_{n-1}))
\;:\; \lambda \text{ interior-valid} \,\bigr\}
\;\subseteq\; \{0,1,-1\}^{n} .
\]
\end{definition}
\begin{proposition}[Interior-free disks attain $2^{n-2}$]
\label{prop:attainment}
If $D$ has no interior vertices then $|\Phi(D)| = 2^{\,n-2}$.
\end{proposition}
\begin{proof}
A triangulation of the $n$-gon has an \emph{ear}: a face
$(v_{i-1}, v_i, v_{i+1})$ whose middle vertex $v_i$ has face-degree $1$,
so $\lambda^{*}(v_i)$ equals that face's label and is read directly off
the boundary sequence. Deleting the ear leaves a triangulation of the
$(n-1)$-gon inducing the restricted boundary sequence; inducting, the
$n-2$ face labels are recovered injectively, so $\lambda \mapsto
\lambda^{*}|_C$ is a bijection onto a set of size $2^{\,n-2}$.
\end{proof}
\begin{lemma}[Un-stacking]
\label{lem:unstack}
If $v$ is a degree-$3$ interior vertex of $D$, deleting it and restoring
its link triangle as a single face yields a disk $D'$ with one fewer
interior vertex and $\Phi(D') = \Phi(D)$.
\end{lemma}
\begin{proof}
The constraint at $v$ forces its three faces to a common value $s$,
contributing $2s \equiv -s$ to each of the three link vertices; the
restored triangle, labelled $-s$, reproduces that contribution at each,
and $s \mapsto -s$ is a bijection on $\{+1,-1\}$. The resulting map is a
bijection between interior-valid labellings of $D$ and of $D'$ preserving
every boundary value, hence $\Phi(D) = \Phi(D')$.
\end{proof}
\begin{conjecture}[Constraint floor]
\label{conj:constraint-floor}
For every triangulated disk $D$ with boundary an $n$-cycle,
$|\Phi(D)| \ge 2^{\,n-2}$. Equivalently, no nested structure constrains
the outer cycle below $2^{\,n-2}$ achievable Heawood sequences.
\end{conjecture}
\begin{remark}[Status of Conjecture~\ref{conj:constraint-floor}]
\label{rem:floor-status}
Iterating Lemma~\ref{lem:unstack} reduces any disk, $\Phi$-faithfully and
at fixed $n$, to one with no degree-$3$ interior vertex: either
interior-free, where Proposition~\ref{prop:attainment} gives exactly
$2^{\,n-2}$, or \emph{irreducible} (every interior vertex of degree
$\ge 4$). Thus Conjecture~\ref{conj:constraint-floor} holds for the
entire stacked (Apollonian) class and reduces to the irreducible case.
Empirically it holds without exception over more than $10^4$ disks, and
\emph{strictly}: every irreducible disk satisfies $|\Phi(D)| \ge
\tfrac54 \cdot 2^{\,n-2}$, with equality at a single minimal-degree
interior vertex; the wheel $W_n$ gives $|\Phi(W_n)| = \lfloor 2^n/3
\rfloor$ and is \emph{not} the minimiser. A counting balance makes the
floor plausible --- a disk with $k$ interior vertices has $2k+n-2$ faces
(Euler) but only $k$ interior constraints, so the linear free dimension
$k+n-2$ grows with depth --- but this is only heuristic: $|\Phi(D)|$ is
\emph{not} monotone in $k$ (inserting a degree-$4$ vertex can shrink it),
it merely never drops below $2^{\,n-2}$. Two natural elementary proofs,
a $\Phi$-non-increasing vertex reduction and a direct $(n{-}2)$-face
transversal, both provably fail; a proof appears to need a global
argument on the Boolean / mod-$3$ structure of $\Phi$.
\end{remark}
\begin{remark}
\label{rem:floor-consequences}
Two consequences. First, $\Phi(D)$ is a $\mathbb{Z}/3$ zonotope --- a
projected cube, sign-closed but not a $\mathrm{GF}(3)$ subspace --- and at
the interior-free value it has size $2^{\,n-2}$ with affine hull of
dimension $n-2$. Second, granting Conjecture~\ref{conj:constraint-floor},
the floor is exponential in the interface length $n$, so a
maximally-constraining child still offers $2^{\,n-2}$ outer options, and
the gluing of Conjecture~\ref{conj:heawood-chain-pigeonhole} has the least
slack at \emph{short} interfaces (e.g.\ $n = 4$ leaves $4$ options) and is
easy at long ones; the difficulty of the programme is concentrated at
short level cycles.
\end{remark}
\begin{thebibliography}{9}
\bibitem{Heawood1898}
P.~J.~Heawood,
\emph{On the four-colour map theorem},
Quart. J.~Pure Appl. Math. \textbf{29} (1898), 270--285.
\bibitem{bauerfeld-depth}
E.~Bauerfeld,
\emph{Plane Depth},
manuscript (math-research repository), 2026.
\bibitem{bauerfeld-nested-tires}
E.~Bauerfeld,
\emph{Nested Tire Decompositions of Plane Triangulations},
manuscript (math-research repository), 2026.
\bibitem{bauerfeld-medial-tires}
E.~Bauerfeld,
\emph{Medial Tire Decompositions of Plane Triangulations},
manuscript (math-research repository), 2026.
\bibitem{bauerfeld-nested-tire-duals}
E.~Bauerfeld,
\emph{Coloring Nested Tire Dual Graphs},
manuscript (math-research repository), 2026.
\end{thebibliography}
\end{document}
@@ -0,0 +1,28 @@
\relax
\citation{bauerfeld-medial-tire}
\citation{bauerfeld-nested-tire-decompositions}
\citation{bauerfeld-medial-tire}
\citation{bauerfeld-medial-tire}
\@writefile{toc}{\contentsline {section}{\tocsection {}{1}{Introduction}}{1}{}\protected@file@percent }
\@writefile{toc}{\contentsline {section}{\tocsection {}{2}{A medial pigeonhole programme}}{1}{}\protected@file@percent }
\newlabel{def:medial-boundary-state}{{2.1}{2}}
\newlabel{conj:medial-chain-pigeonhole}{{2.2}{2}}
\newlabel{conj:medial-route-fct}{{2.3}{2}}
\@writefile{toc}{\contentsline {section}{\tocsection {}{3}{Kempe-cycle conservation across medial tires}}{2}{}\protected@file@percent }
\newlabel{lem:kempe-cycles}{{3.1}{2}}
\citation{bauerfeld-medial-tire}
\citation{bauerfeld-medial-tire}
\citation{bauerfeld-medial-tire}
\newlabel{lem:kempe-conservation}{{3.2}{3}}
\newlabel{def:kempe-balanced}{{3.3}{3}}
\newlabel{rem:kempe-balance-necessary}{{3.4}{3}}
\bibcite{bauerfeld-medial-tire}{1}
\bibcite{bauerfeld-nested-tire-decompositions}{2}
\bibcite{tait-original}{3}
\newlabel{tocindent-1}{0pt}
\newlabel{tocindent0}{12.7778pt}
\newlabel{tocindent1}{17.77782pt}
\newlabel{tocindent2}{0pt}
\newlabel{tocindent3}{0pt}
\@writefile{toc}{\contentsline {section}{\tocsection {}{}{References}}{6}{}\protected@file@percent }
\gdef \@abspage@last{6}
@@ -0,0 +1,135 @@
# Fdb version 3
["pdflatex"] 1781485473 "paper.tex" "paper.pdf" "paper" 1781485473
"/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 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/symbols/msam10.tfm" 1246382020 916 f87d7c45f9c908e672703b83b72241a3 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/symbols/msam5.tfm" 1246382020 924 9904cf1d39e9767e7a3622f2a125a565 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/symbols/msam7.tfm" 1246382020 928 2dc8d444221b7a635bb58038579b861a ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/symbols/msbm10.tfm" 1246382020 908 2921f8a10601f252058503cc6570e581 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/symbols/msbm5.tfm" 1246382020 940 75ac932a52f80982a9f8ea75d03a34cf ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/symbols/msbm7.tfm" 1246382020 940 228d6584342e91276bf566bcf9716b83 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmbx10.tfm" 1136768653 1328 c834bbb027764024c09d3d2bf908b5f0 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmbx8.tfm" 1136768653 1332 1fde11373e221473104d6cc5993f046e ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmcsc10.tfm" 1136768653 1300 63a6111ee6274895728663cf4b4e7e81 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmmi6.tfm" 1136768653 1512 f21f83efb36853c0b70002322c1ab3ad ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmmi8.tfm" 1136768653 1520 eccf95517727cb11801f4f1aee3a21b4 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmr6.tfm" 1136768653 1300 b62933e007d01cfd073f79b963c01526 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmr8.tfm" 1136768653 1292 21c1c5bfeaebccffdb478fd231a0997d ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmss10.tfm" 1136768653 1316 b636689f1933f24d1294acdf6041daaa ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmss8.tfm" 1136768653 1296 d77f431d10d47c8ea2cc18cf45346274 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmsy6.tfm" 1136768653 1116 933a60c408fc0a863a92debe84b2d294 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmsy8.tfm" 1136768653 1120 8b7d695260f3cff42e636090a8002094 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmti10.tfm" 1136768653 1480 aa8e34af0eb6a2941b776984cf1dfdc4 ""
"/usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmti8.tfm" 1136768653 1504 1747189e0441d1c18f3ea56fafc1c480 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx10.pfb" 1248133631 34811 78b52f49e893bcba91bd7581cdc144c0 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx8.pfb" 1248133631 32166 b0c356b15f19587482a9217ce1d8fa67 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmcsc10.pfb" 1248133631 32001 6aeea3afe875097b1eb0da29acd61e28 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi10.pfb" 1248133631 36299 5f9df58c2139e7edcf37c8fca4bd384d ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi5.pfb" 1248133631 37912 77d683123f92148345f3fc36a38d9ab1 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi7.pfb" 1248133631 36281 c355509802a035cadc5f15869451dcee ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr10.pfb" 1248133631 35752 024fb6c41858982481f6968b5fc26508 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr7.pfb" 1248133631 32762 224316ccc9ad3ca0423a14971cfa7fc1 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr8.pfb" 1248133631 32726 0a1aea6fcd6468ee2cf64d891f5c43c8 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmss10.pfb" 1248133631 24457 5cbb7bdf209d5d1ce9892a9b80a307cc ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy10.pfb" 1248133631 32569 5e5ddc8df908dea60932f3c484a54c0d ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy7.pfb" 1248133631 32716 08e384dc442464e7285e891af9f45947 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti10.pfb" 1248133631 37944 359e864bd06cde3b1cf57bb20757fb06 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti8.pfb" 1248133631 35660 fb24af7afbadb71801619f1415838111 ""
"/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/symbols/msam10.pfb" 1248133631 31764 459c573c03a4949a528c2cc7f557e217 ""
"/usr/local/texlive/2022/texmf-dist/tex/context/base/mkii/supp-pdf.mkii" 1461363279 71627 94eb9990bed73c364d7f53f960cc8c5b ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcore.code.tex" 1601326656 992 855ff26741653ab54814101ca36e153c ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorearrows.code.tex" 1601326656 43820 1fef971b75380574ab35a0d37fd92608 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreexternal.code.tex" 1601326656 19324 f4e4c6403dd0f1605fd20ed22fa79dea ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcoregraphicstate.code.tex" 1601326656 6038 ccb406740cc3f03bbfb58ad504fe8c27 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreimage.code.tex" 1601326656 6944 e12f8f7a7364ddf66f93ba30fb3a3742 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorelayers.code.tex" 1601326656 4883 42daaf41e27c3735286e23e48d2d7af9 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreobjects.code.tex" 1601326656 2544 8c06d2a7f0f469616ac9e13db6d2f842 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepathconstruct.code.tex" 1601326656 44195 5e390c414de027626ca5e2df888fa68d ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepathprocessing.code.tex" 1601326656 17311 2ef6b2e29e2fc6a2fc8d6d652176e257 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepathusage.code.tex" 1601326656 21302 788a79944eb22192a4929e46963a3067 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepatterns.code.tex" 1601326656 9690 01feb7cde25d4293ef36eef45123eb80 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepoints.code.tex" 1601326656 33335 dd1fa4814d4e51f18be97d88bf0da60c ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorequick.code.tex" 1601326656 2965 4c2b1f4e0826925746439038172e5d6f ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorerdf.code.tex" 1601326656 5196 2cc249e0ee7e03da5f5f6589257b1e5b ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorescopes.code.tex" 1601326656 20726 d4c8db1e2e53b72721d29916314a22ea ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreshade.code.tex" 1601326656 35249 abd4adf948f960299a4b3d27c5dddf46 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcoretransformations.code.tex" 1601326656 21989 fdc867d05d228316de137a9fc5ec3bbe ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcoretransparency.code.tex" 1601326656 8893 e851de2175338fdf7c17f3e091d94618 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibrarybackgrounds.code.tex" 1601326656 4572 4a19637ef65ce88ad2f2d5064b69541d ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibrarytopaths.code.tex" 1608933718 11518 738408f795261b70ce8dd47459171309 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/frontendlayer/tikz/tikz.code.tex" 1621110968 186007 6e7dfe0bd57520fd5f91641aa72dcac8 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/libraries/pgflibraryplothandlers.code.tex" 1601326656 32995 ac577023e12c0e4bd8aa420b2e852d1a ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfint.code.tex" 1557692582 3063 8c415c68a0f3394e45cfeca0b65f6ee6 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmath.code.tex" 1601326656 521 8e224a7af69b7fee4451d1bf76b46654 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathcalc.code.tex" 1601326656 13391 84d29568c13bdce4133ab4a214711112 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfloat.code.tex" 1601326656 104935 184ed87524e76d4957860df4ce0cd1c3 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.base.code.tex" 1601326656 10165 cec5fa73d49da442e56efc2d605ef154 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.basic.code.tex" 1601326656 28178 41c17713108e0795aac6fef3d275fbca ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.code.tex" 1601326656 9989 c55967bf45126ff9b061fa2ca0c4694f ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.comparison.code.tex" 1601326656 3865 ac538ab80c5cf82b345016e474786549 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.integerarithmetics.code.tex" 1557692582 3177 27d85c44fbfe09ff3b2cf2879e3ea434 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.misc.code.tex" 1621110968 11024 0179538121bc2dba172013a3ef89519f ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.random.code.tex" 1608933718 7854 4176998eeefd8745ac6d2d4bd9c98451 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.round.code.tex" 1601326656 3379 781797a101f647bab82741a99944a229 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.trigonometric.code.tex" 1601326656 92405 f515f31275db273f97b9d8f52e1b0736 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathparser.code.tex" 1601326656 37376 11cd75aac3da1c1b152b2848f30adc14 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathutil.code.tex" 1601326656 8471 c2883569d03f69e8e1cabfef4999cfd7 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/modules/pgfmodulematrix.code.tex" 1601326656 21201 08d231a2386e2b61d64641c50dc15abd ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/modules/pgfmoduleplot.code.tex" 1601326656 16121 346f9013d34804439f7436ff6786cef7 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/modules/pgfmoduleshapes.code.tex" 1621110968 44784 cedaa399d15f95e68e22906e2cc09ef8 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/pgf.revision.tex" 1621110968 465 d68603f8b820ea4a08cce534944db581 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgf.cfg" 1601326656 926 2963ea0dcf6cc6c0a770b69ec46a477b ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsys-common-pdf.def" 1601326656 5546 f3f24d7898386cb7daac70bdd2c4d6dc ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsys-pdftex.def" 1601326656 12601 4786e597516eddd82097506db7cfa098 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsys.code.tex" 1621110968 61163 9b2eefc24e021323e0fc140e9826d016 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsysprotocol.code.tex" 1601326656 1896 b8e0ca0ac371d74c0ca05583f6313c91 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsyssoftpath.code.tex" 1601326656 7778 53c8b5623d80238f6a20aa1df1868e63 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgffor.code.tex" 1606168878 23997 a4bed72405fa644418bea7eac2887006 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfkeys.code.tex" 1621110968 37060 797782f0eb50075c9bc952374d9a659a ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfkeysfiltered.code.tex" 1601326656 37431 9abe862035de1b29c7a677f3205e3d9f ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfrcs.code.tex" 1601326656 4494 af17fb7efeafe423710479858e42fa7e ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfutil-common-lists.tex" 1601326656 7251 fb18c67117e09c64de82267e12cd8aa4 ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfutil-common.tex" 1621110968 29274 e15c5b7157d21523bd9c9f1dfa146b8e ""
"/usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfutil-latex.def" 1621110968 6825 a2b0ea5b539dda0625e99dd15785ab59 ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/amscls/amsart.cls" 1591045760 61881 a7369c346c2922a758ae6283cc1ed014 ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amsfonts.sty" 1359763108 5949 3f3fd50a8cc94c3d4cbf4fc66cd3df1c ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amssymb.sty" 1359763108 13829 94730e64147574077f8ecfea9bb69af4 ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsa.fd" 1359763108 961 6518c6525a34feb5e8250ffa91731cff ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsb.fd" 1359763108 961 d02606146ba5601b5645f987c92e6193 ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsbsy.sty" 1622667781 2222 da905dc1db75412efd2d8f67739f0596 ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsgen.sty" 1622667781 4173 bc0410bcccdff806d6132d3c1ef35481 ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsmath.sty" 1636758526 87648 07fbb6e9169e00cb2a2f40b31b2dbf3c ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsopn.sty" 1636758526 4128 8eea906621b6639f7ba476a472036bbe ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amstext.sty" 1636758526 2444 926f379cc60fcf0c6e3fee2223b4370d ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty" 1579991033 13886 d1306dcf79a944f6988e688c1785f9ce ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/graphics-cfg/color.cfg" 1459978653 1213 620bba36b25224fa9b7e1ccb4ecb76fd ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/graphics-cfg/graphics.cfg" 1465944070 1224 978390e9c2234eab29404bc21b268d1e ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/graphics-def/pdftex.def" 1601931164 19103 48d29b6e2a64cb717117ef65f107b404 ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphics.sty" 1622581934 18399 7e40f80366dffb22c0e7b70517db5cb4 ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphicx.sty" 1636758526 7996 a8fb260d598dcaf305a7ae7b9c3e3229 ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/keyval.sty" 1622581934 2671 4de6781a30211fe0ea4c672e4a2a8166 ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/trig.sty" 1636758526 4009 187ea2dc3194cd5a76cd99a8d7a6c4d0 ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/l3backend/l3backend-pdftex.def" 1644269979 29921 d0acc05a38bd4aa3af2017f0b7c137ce ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/latexconfig/epstopdf-sys.cfg" 1279039959 678 4792914a8f45be57bb98413425e4c7af ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/pgf/basiclayer/pgf.sty" 1601326656 1090 bae35ef70b3168089ef166db3e66f5b2 ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/pgf/basiclayer/pgfcore.sty" 1601326656 410 615550c46f918fcbee37641b02a862d9 ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-0-65.sty" 1601326656 21013 f4ff83d25bb56552493b030f27c075ae ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-1-18.sty" 1601326656 989 c49c8ae06d96f8b15869da7428047b1e ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/pgf/frontendlayer/tikz.sty" 1601326656 339 c2e180022e3afdb99c7d0ea5ce469b7d ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/pgf/math/pgfmath.sty" 1601326656 306 c56a323ca5bf9242f54474ced10fca71 ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/pgf/systemlayer/pgfsys.sty" 1601326656 443 8c872229db56122037e86bcda49e14f3 ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgffor.sty" 1601326656 348 ee405e64380c11319f0e249fed57e6c5 ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfkeys.sty" 1601326656 274 5ae372b7df79135d240456a1c6f2cf9a ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfrcs.sty" 1601326656 325 f9f16d12354225b7dd52a3321f085955 ""
"/usr/local/texlive/2022/texmf-dist/tex/latex/xcolor/xcolor.sty" 1635798903 56029 3f7889dab51d620aa43177c391b7b190 ""
"/usr/local/texlive/2022/texmf-dist/web2c/texmf.cnf" 1646502317 40171 cdab547de63d26590bebb3baff566530 ""
"/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" 1781485473 1312 7ba44acc160ff1167e676415e34169c0 "pdflatex"
"paper.tex" 1781485463 17511 c613c597a654f07644b998ea69f242e5 ""
(generated)
"paper.aux"
"paper.log"
"paper.pdf"
@@ -0,0 +1,471 @@
PWD /Users/didericis/Code/math-research/papers/medial_pigeonhole_programme
INPUT /usr/local/texlive/2022/texmf.cnf
INPUT /usr/local/texlive/2022/texmf-dist/web2c/texmf.cnf
INPUT /usr/local/texlive/2022/texmf-var/web2c/pdftex/pdflatex.fmt
INPUT paper.tex
OUTPUT paper.log
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amscls/amsart.cls
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amscls/amsart.cls
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amscls/amsart.cls
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amscls/amsart.cls
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amscls/amsart.cls
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amscls/amsart.cls
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amscls/amsart.cls
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amscls/amsart.cls
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amscls/amsart.cls
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amscls/amsart.cls
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amscls/amsart.cls
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsmath.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsmath.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsmath.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsmath.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsmath.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsmath.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsmath.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsmath.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsmath.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsmath.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsmath.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsmath.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsmath.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsmath.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsmath.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsopn.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsopn.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amstext.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amstext.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amstext.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amstext.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amstext.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amstext.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amstext.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amstext.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amstext.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amstext.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amstext.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsgen.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsgen.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsgen.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsgen.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsgen.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsgen.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsgen.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsgen.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsgen.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsgen.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsgen.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsbsy.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsbsy.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsbsy.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsbsy.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsbsy.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsbsy.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsbsy.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsbsy.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsbsy.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsbsy.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsbsy.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsgen.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsopn.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsopn.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsopn.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsopn.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsopn.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsopn.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsopn.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsopn.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsopn.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsopn.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsopn.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsgen.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsa.fd
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsa.fd
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsa.fd
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsa.fd
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amsfonts.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amsfonts.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amsfonts.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amsfonts.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amsfonts.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amsfonts.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amsfonts.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amsfonts.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amsfonts.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amsfonts.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amsfonts.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amssymb.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amssymb.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amssymb.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amssymb.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amssymb.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amssymb.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amssymb.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amssymb.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amssymb.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amssymb.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amssymb.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amsfonts.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphicx.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphicx.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphicx.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphicx.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphicx.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphicx.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphicx.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphicx.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphicx.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphicx.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphicx.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/keyval.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/keyval.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/keyval.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/keyval.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/keyval.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/keyval.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/keyval.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/keyval.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/keyval.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/keyval.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/keyval.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphics.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphics.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphics.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphics.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphics.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphics.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphics.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphics.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphics.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphics.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphics.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/trig.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/trig.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/trig.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/trig.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/trig.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/trig.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/trig.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/trig.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/trig.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/trig.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/trig.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics-cfg/graphics.cfg
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics-cfg/graphics.cfg
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics-cfg/graphics.cfg
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics-cfg/graphics.cfg
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics-def/pdftex.def
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics-def/pdftex.def
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics-def/pdftex.def
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics-def/pdftex.def
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/frontendlayer/tikz.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/frontendlayer/tikz.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/frontendlayer/tikz.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/frontendlayer/tikz.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/frontendlayer/tikz.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/frontendlayer/tikz.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/frontendlayer/tikz.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/frontendlayer/tikz.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/frontendlayer/tikz.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/frontendlayer/tikz.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/frontendlayer/tikz.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/basiclayer/pgf.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/basiclayer/pgf.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/basiclayer/pgf.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/basiclayer/pgf.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/basiclayer/pgf.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/basiclayer/pgf.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/basiclayer/pgf.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/basiclayer/pgf.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/basiclayer/pgf.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/basiclayer/pgf.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/basiclayer/pgf.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfrcs.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfrcs.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfrcs.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfrcs.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfrcs.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfrcs.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfrcs.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfrcs.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfrcs.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfrcs.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfrcs.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfutil-common.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfutil-common-lists.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfutil-latex.def
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfrcs.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfrcs.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfrcs.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfrcs.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/pgf.revision.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/pgf.revision.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/basiclayer/pgfcore.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/basiclayer/pgfcore.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/basiclayer/pgfcore.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/basiclayer/pgfcore.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/basiclayer/pgfcore.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/basiclayer/pgfcore.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/basiclayer/pgfcore.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/basiclayer/pgfcore.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/basiclayer/pgfcore.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/basiclayer/pgfcore.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/basiclayer/pgfcore.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphicx.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/systemlayer/pgfsys.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/systemlayer/pgfsys.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/systemlayer/pgfsys.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/systemlayer/pgfsys.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/systemlayer/pgfsys.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/systemlayer/pgfsys.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/systemlayer/pgfsys.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/systemlayer/pgfsys.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/systemlayer/pgfsys.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/systemlayer/pgfsys.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/systemlayer/pgfsys.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfrcs.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsys.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsys.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsys.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsys.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfkeys.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfkeysfiltered.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgf.cfg
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsys-pdftex.def
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsys-pdftex.def
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsys-common-pdf.def
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsyssoftpath.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsyssoftpath.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsyssoftpath.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsyssoftpath.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsysprotocol.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsysprotocol.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsysprotocol.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/systemlayer/pgfsysprotocol.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics/keyval.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/xcolor/xcolor.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/xcolor/xcolor.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/xcolor/xcolor.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/xcolor/xcolor.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/xcolor/xcolor.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/xcolor/xcolor.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/xcolor/xcolor.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/xcolor/xcolor.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/xcolor/xcolor.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/xcolor/xcolor.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/xcolor/xcolor.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics-cfg/color.cfg
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics-cfg/color.cfg
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics-cfg/color.cfg
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/graphics-cfg/color.cfg
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcore.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcore.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcore.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcore.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmath.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathcalc.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathutil.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathparser.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.basic.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.trigonometric.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.random.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.comparison.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.base.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.round.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.misc.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfunctions.integerarithmetics.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmathfloat.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfint.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepoints.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepathconstruct.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepathusage.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorescopes.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcoregraphicstate.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcoretransformations.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorequick.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreobjects.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepathprocessing.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorearrows.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreshade.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreimage.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcoreexternal.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorelayers.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcoretransparency.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorepatterns.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/basiclayer/pgfcorerdf.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/modules/pgfmoduleshapes.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/modules/pgfmoduleplot.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-0-65.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-0-65.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-0-65.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-0-65.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-0-65.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-0-65.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-0-65.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-0-65.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-0-65.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-0-65.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-0-65.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfrcs.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-1-18.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-1-18.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-1-18.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-1-18.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-1-18.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-1-18.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-1-18.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-1-18.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-1-18.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-1-18.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/compatibility/pgfcomp-version-1-18.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfrcs.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgffor.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgffor.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgffor.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgffor.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgffor.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgffor.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgffor.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgffor.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgffor.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgffor.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgffor.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfrcs.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfkeys.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfkeys.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfkeys.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfkeys.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfkeys.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfkeys.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfkeys.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfkeys.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfkeys.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfkeys.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfkeys.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfkeys.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfkeys.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfkeys.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgfkeys.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/math/pgfmath.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/math/pgfmath.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/math/pgfmath.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/math/pgfmath.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/math/pgfmath.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/math/pgfmath.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/math/pgfmath.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/math/pgfmath.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/math/pgfmath.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/math/pgfmath.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/math/pgfmath.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfrcs.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/pgf/utilities/pgfkeys.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmath.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmath.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmath.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmath.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgffor.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgffor.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgffor.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/utilities/pgffor.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/math/pgfmath.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/frontendlayer/tikz/tikz.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/frontendlayer/tikz/tikz.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/frontendlayer/tikz/tikz.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/frontendlayer/tikz/tikz.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/libraries/pgflibraryplothandlers.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/libraries/pgflibraryplothandlers.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/modules/pgfmodulematrix.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibrarytopaths.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibrarytopaths.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibrarybackgrounds.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/generic/pgf/frontendlayer/tikz/libraries/tikzlibrarybackgrounds.code.tex
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/l3backend/l3backend-pdftex.def
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/l3backend/l3backend-pdftex.def
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/l3backend/l3backend-pdftex.def
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/l3backend/l3backend-pdftex.def
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/l3backend/l3backend-pdftex.def
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/l3backend/l3backend-pdftex.def
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/l3backend/l3backend-pdftex.def
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/l3backend/l3backend-pdftex.def
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/l3backend/l3backend-pdftex.def
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/l3backend/l3backend-pdftex.def
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/l3backend/l3backend-pdftex.def
INPUT ./paper.aux
INPUT paper.aux
INPUT paper.aux
OUTPUT paper.aux
INPUT /usr/local/texlive/2022/texmf-dist/fonts/map/fontname/texfonts.map
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmr8.tfm
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmr6.tfm
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmmi8.tfm
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmmi6.tfm
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmsy8.tfm
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmsy6.tfm
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/cmextra/cmex8.tfm
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/cmextra/cmex7.tfm
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/cmextra/cmex7.tfm
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsa.fd
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsa.fd
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsa.fd
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsa.fd
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/symbols/msam10.tfm
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/symbols/msam7.tfm
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/symbols/msam5.tfm
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsb.fd
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsb.fd
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsb.fd
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsb.fd
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/symbols/msbm10.tfm
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/symbols/msbm7.tfm
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/symbols/msbm5.tfm
INPUT /usr/local/texlive/2022/texmf-dist/tex/context/base/mkii/supp-pdf.mkii
INPUT /usr/local/texlive/2022/texmf-dist/tex/context/base/mkii/supp-pdf.mkii
INPUT /usr/local/texlive/2022/texmf-dist/tex/context/base/mkii/supp-pdf.mkii
INPUT /usr/local/texlive/2022/texmf-dist/tex/context/base/mkii/supp-pdf.mkii
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/latexconfig/epstopdf-sys.cfg
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/latexconfig/epstopdf-sys.cfg
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/latexconfig/epstopdf-sys.cfg
INPUT /usr/local/texlive/2022/texmf-dist/tex/latex/latexconfig/epstopdf-sys.cfg
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmcsc10.tfm
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmti8.tfm
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmbx10.tfm
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmcsc10.tfm
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/cmextra/cmex7.tfm
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/symbols/msam10.tfm
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/symbols/msam7.tfm
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/symbols/msbm10.tfm
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/amsfonts/symbols/msbm7.tfm
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmss10.tfm
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmss8.tfm
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmss8.tfm
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmti10.tfm
OUTPUT paper.pdf
INPUT /usr/local/texlive/2022/texmf-var/fonts/map/pdftex/updmap/pdftex.map
INPUT /usr/local/texlive/2022/texmf-dist/fonts/tfm/public/cm/cmbx8.tfm
INPUT paper.aux
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx10.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx8.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmcsc10.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi10.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi5.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi7.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr10.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr7.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr8.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmss10.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy10.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy7.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti10.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti8.pfb
INPUT /usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/symbols/msam10.pfb
@@ -1,4 +1,4 @@
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 15 JUN 2026 01:07
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 14 JUN 2026 21:05
entering extended mode
restricted \write18 enabled.
%&-line parsing enabled.
@@ -495,39 +495,35 @@ File: epstopdf-sys.cfg 2010/07/13 v1.3 Configuration of (r)epstopdf for TeX Liv
e
))
[1{/usr/local/texlive/2022/texmf-var/fonts/map/pdftex/updmap/pdftex.map}]
LaTeX Warning: `h' float specifier changed to `ht'.
[2] [3] (./whole_medial_seed59_min5.tikz) [4] [5] (./paper.aux) )
[2] [3] [4] [5] [6] (./paper.aux) )
Here is how much of TeX's memory you used:
13803 strings out of 478268
275582 string characters out of 5846347
655790 words of memory out of 5000000
31627 multiletter control sequences out of 15000+600000
477661 words of font info for 60 fonts, out of 8000000 for 9000
13536 strings out of 478268
270232 string characters out of 5846347
537626 words of memory out of 5000000
31366 multiletter control sequences out of 15000+600000
476880 words of font info for 57 fonts, out of 8000000 for 9000
1302 hyphenation exceptions out of 8191
84i,15n,89p,907b,804s stack positions out of 10000i,1000n,20000p,200000b,200000s
</usr/local/te
xlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx10.pfb></usr/local/tex
live/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx6.pfb></usr/local/texli
ve/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx7.pfb></usr/local/texlive
/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmcsc10.pfb></usr/local/texlive
/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi10.pfb></usr/local/texlive/
2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi7.pfb></usr/local/texlive/20
22/texmf-dist/fonts/type1/public/amsfonts/cm/cmr10.pfb></usr/local/texlive/2022
/texmf-dist/fonts/type1/public/amsfonts/cm/cmr6.pfb></usr/local/texlive/2022/te
xmf-dist/fonts/type1/public/amsfonts/cm/cmr7.pfb></usr/local/texlive/2022/texmf
-dist/fonts/type1/public/amsfonts/cm/cmr8.pfb></usr/local/texlive/2022/texmf-di
st/fonts/type1/public/amsfonts/cm/cmss10.pfb></usr/local/texlive/2022/texmf-dis
t/fonts/type1/public/amsfonts/cm/cmsy10.pfb></usr/local/texlive/2022/texmf-dist
/fonts/type1/public/amsfonts/cm/cmsy6.pfb></usr/local/texlive/2022/texmf-dist/f
onts/type1/public/amsfonts/cm/cmti10.pfb></usr/local/texlive/2022/texmf-dist/fo
nts/type1/public/amsfonts/cm/cmti8.pfb></usr/local/texlive/2022/texmf-dist/font
s/type1/public/amsfonts/cm/cmtt10.pfb>
Output written on paper.pdf (5 pages, 226751 bytes).
84i,7n,89p,414b,277s stack positions out of 10000i,1000n,20000p,200000b,200000s
</usr/local/texlive/2022/texmf-dist/fonts/ty
pe1/public/amsfonts/cm/cmbx10.pfb></usr/local/texlive/2022/texmf-dist/fonts/typ
e1/public/amsfonts/cm/cmbx8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1
/public/amsfonts/cm/cmcsc10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1
/public/amsfonts/cm/cmmi10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/
public/amsfonts/cm/cmmi5.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/pu
blic/amsfonts/cm/cmmi7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/publ
ic/amsfonts/cm/cmr10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public
/amsfonts/cm/cmr7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/am
sfonts/cm/cmr8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfo
nts/cm/cmss10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfon
ts/cm/cmsy10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfont
s/cm/cmsy7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/
cm/cmti10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/c
m/cmti8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/sym
bols/msam10.pfb>
Output written on paper.pdf (6 pages, 211901 bytes).
PDF statistics:
103 PDF objects out of 1000 (max. 8388607)
63 compressed objects within 1 object stream
101 PDF objects out of 1000 (max. 8388607)
62 compressed objects within 1 object stream
0 named destinations out of 1000 (max. 500000)
13 words of extra memory for PDF output out of 10000 (max. 10000000)
Binary file not shown.
@@ -0,0 +1,442 @@
%% filename: amsart-template.tex
%% American Mathematical Society
%% AMS-LaTeX v.2 template for use with amsart
%% ====================================================================
\documentclass{amsart}
\usepackage{amssymb}
\usepackage{graphicx}
\usepackage{tikz}
\usetikzlibrary{backgrounds}
\newtheorem{theorem}{Theorem}[section]
\newtheorem{lemma}[theorem]{Lemma}
\newtheorem{corollary}[theorem]{Corollary}
\newtheorem{proposition}[theorem]{Proposition}
\newtheorem{conjecture}[theorem]{Conjecture}
\theoremstyle{definition}
\newtheorem{definition}[theorem]{Definition}
\newtheorem{example}[theorem]{Example}
\newtheorem{xca}[theorem]{Exercise}
\theoremstyle{remark}
\newtheorem{remark}[theorem]{Remark}
\numberwithin{equation}{section}
\begin{document}
\title{The Medial Pigeonhole Programme}
% author one information
\author{Eric Bauerfeld}
\address{}
\curraddr{}
\email{}
\thanks{}
\subjclass[2010]{Primary }
\keywords{plane graph, triangulation, medial graph, tire graph, Tait coloring, Kempe chain, Four Colour Theorem}
\date{}
\dedicatory{}
\begin{abstract}
Building on the medial tire decomposition of a plane triangulation, we
formulate a pigeonhole programme for the Four Colour Theorem in medial
terms. Each tire carries a boundary-state restriction relation, and a
proper vertex $3$-colouring of the full medial graph is a compatible
selection of these boundary states across the tire tree. We state a
chain-pigeonhole conjecture asserting that the restriction relations
cannot remain mutually disjoint along every branch, and we refine the
boundary states by recording how two-colour Kempe cycles are routed
through each annular tire region. This yields a Kempe-enhanced
restriction relation and a notion of Kempe-compatible gluing along level
cycles.
\end{abstract}
\maketitle
\section{Introduction}
This paper continues the medial tire programme begun
in~\cite{bauerfeld-medial-tire}. We use freely the terminology and
notation introduced there. For a plane triangulation $G$ with fixed
embedding, $M(G)$ denotes the full medial graph, and the tire-tree
decomposition $\mathcal{T}(G,S)$ at a level source $S$
of~\cite{bauerfeld-nested-tire-decompositions} induces a decomposition
of $M(G)$ into full medial tire graphs $\mathsf{M}(T)$, one for each
tread $T$, glued along their boundary medial vertex sets
$\partial_{\mathrm{out}}\mathsf{M}(T)$ and
$\partial_{\mathrm{in}}\mathsf{M}(T)$. We also use the annular medial
cycle $A(T)$, its up and down teeth and their apexes, the bites and the
auxiliary plane graph $B(T)$, and the medial tire restriction relation
$R_T$ of~\cite{bauerfeld-medial-tire}.
By the Tait--medial correspondence of~\cite{bauerfeld-medial-tire},
proper vertex $3$-colourings of $M(G)$ are in natural bijection with
proper $3$-edge-colourings of the cubic planar dual $G^*$. Thus the
Four Colour Theorem is the assertion that the full medial graph of every
plane triangulation is properly vertex $3$-colourable, and the medial
tire decomposition turns this into a question about how local boundary
colourings compose across the tire tree.
\section{A medial pigeonhole programme}
The restriction relation $R_T$ records exactly the local information
needed to pass a medial $3$-colouring through a tire. In a nested
chain
\[
T_0 \supset T_1 \supset \cdots \supset T_k,
\]
the outer boundary state of $T_{i+1}$ must match an inner boundary
state allowed by $R_{T_i}$. Thus a proof of the Four Colour Theorem in
this framework would follow from a structural reason that these
restriction sets cannot remain mutually disjoint along every branch of
the tire tree.
\begin{definition}[Medial boundary state]
\label{def:medial-boundary-state}
A \emph{medial boundary state} on a boundary set
$\partial\mathsf{M}(T)$ is a proper vertex $3$-colouring of the
subgraph induced by that boundary set, considered up to permutation of
the three colours and the dihedral symmetries of the boundary walk
when that boundary is a cycle.
\end{definition}
\begin{conjecture}[Medial chain-pigeonhole principle]
\label{conj:medial-chain-pigeonhole}
There is a function $N(k)$ such that the following holds. Let
$T_0 \supset T_1 \supset \cdots \supset T_{N(k)}$ be a nested chain of
tire treads whose relevant boundary medial walks have length at most
$k$. Then two adjacent restriction relations in the chain have
compatible medial boundary states after colour permutation and boundary
symmetry. Equivalently, the chain contains a local gluing step that
cannot be obstructed by disjoint proper vertex $3$-colouring
restrictions.
\end{conjecture}
\begin{conjecture}[Medial tire route to the Four Colour Theorem]
\label{conj:medial-route-fct}
For every plane triangulation $G$ and every level source $S$, the
restriction relations $\{R_T : T \in V(\mathcal{T}(G,S))\}$ admit a
compatible selection of boundary states across the tire tree. Hence
$M(G)$ is properly vertex $3$-colourable, $G^*$ is properly
$3$-edge-colourable, and $G$ is properly $4$-vertex-colourable.
\end{conjecture}
\begin{remark}
Conjecture~\ref{conj:medial-route-fct} is equivalent in strength to
the Four Colour Theorem when combined with Tait's correspondence. The
point of the formulation is not to weaken the target theorem, but to
move the obstruction into finite boundary-state restrictions carried by
annular medial tire pieces.
\end{remark}
\section{Kempe-cycle conservation across medial tires}
We now record an additional structure carried by proper
$3$-colourings of medial graphs. This structure will be useful for
describing how colourings glue across level cycles.
Let $G$ be a plane triangulation and let $M=M(G)$ be its medial graph.
Let
\[
\varphi:V(M)\to\{1,2,3\}
\]
be a proper $3$-colouring of $M$. For a two-element colour set
$P=\{a,b\}\subseteq\{1,2,3\}$, let $M_P$ denote the subgraph of $M$
induced by the vertices of colours $a$ and $b$.
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 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.
\end{lemma}
\begin{proof}
Let $v\in V(M_P)$. In the medial graph $M$, the vertex $v$ has degree
$4$. Since $\varphi$ is a proper $3$-colouring, none of the neighbours
of $v$ has colour $\varphi(v)$. Thus all four neighbours of $v$ have
one of the two colours different from $\varphi(v)$.
In the medial graph of a plane triangulation, the neighbours of a
medial vertex occur in two opposite pairs corresponding to the two
faces incident with the corresponding edge of $G$. Around each such
triangular face, the three medial vertices receive all three colours.
Consequently, at $v$ there are exactly two neighbours of each colour
different from $\varphi(v)$. It follows that, in the subgraph induced
by any two colours $P$, every vertex has degree $2$. Hence each
component of $M_P$ is a cycle.
\end{proof}
Let $T$ be a medial tire region. We regard $T$ as an annular transition
region whose boundary consists of one outer level cycle and finitely
many inner level cycles:
\[
\partial T = C_0\sqcup C_1\sqcup\cdots\sqcup C_m.
\]
Here $C_0$ is the outer level cycle of $T$, and the cycles
$C_1,\ldots,C_m$ are the inner level cycles. Each inner level cycle
$C_i$ is also the outer level cycle of the corresponding child region
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
of $C$ without also leaving it.
Equivalently, the incidences of $K$ with $C$ are paired by the
components of $K$ lying on the child side of $C$, and also paired by the
components of $K$ lying on the parent side of $C$.
\end{lemma}
\begin{proof}
By the preceding lemma, $K$ is a cycle. The level cycle $C$ separates
the sphere into two closed regions, which we call the parent side and
the child side. Consider the intersection of $K$ with one of these
regions. Since $K$ is a cycle, no component of this intersection can
have exactly one boundary endpoint on $C$. Each component is either
closed within the region, or is a path with two boundary endpoints on
$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 in the sense of~\cite{bauerfeld-medial-tire}. The
\emph{tooth apexes incident to} a valid face $F$ are:
\begin{itemize}
\item the up-tooth apexes (\cite{bauerfeld-medial-tire}), 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<m<j$, or to the root face if there
is none.
\end{itemize}
Bite apexes are never incident to a valid face in this sense.
For a colour pair $P=\{a,b\}$ write $\nu_P(F)$ for the number of tooth
apexes incident to $F$ that are coloured $a$ or $b$---equivalently, that
lie on a $P$-Kempe chain. The colouring $\varphi$ is
\emph{Kempe-balanced} if $\nu_P(F)$ is even for every valid face $F$ and
every colour pair $P$.
\end{definition}
\begin{remark}[Necessity of Kempe-balance]
\label{rem:kempe-balance-necessary}
A proper $3$-colouring of $\mathsf{M}(T)$ can be part of a proper
$3$-colouring of the whole medial graph $M(G)$ only when it is
Kempe-balanced: if $\varphi$ is the restriction to $\mathsf{M}(T)$ of a
proper $3$-colouring of $M(G)$, then $\varphi$ is Kempe-balanced.
Equivalently, a colouring of $\mathsf{M}(T)$ that fails the parity
condition at some valid face and colour pair cannot extend to a proper
$3$-colouring of $M(G)$. This is an instance of Kempe-cycle
conservation (Lemma~\ref{lem:kempe-conservation}). The tooth apexes
incident to a valid face are boundary medial vertices
(\cite{bauerfeld-medial-tire}) lying on a single level
cycle of the tire decomposition: the up-tooth apexes lie on the outer
level cycle, and the singleton down-tooth apexes incident to an interior
non-tooth face lie on the inner level cycle bounding that face. In the
$4$-regular graph $M(G)$ each $P$-Kempe chain of $\mathsf{M}(T)$ closes
up into a $P$-Kempe cycle, which by Lemma~\ref{lem:kempe-conservation}
meets each level cycle in an even number of $P$-coloured incidences; for
a given valid face these incidences are exactly its incident tooth
apexes coloured $a$ or $b$, whence $\nu_P(F)$ is even.
This argument is verified computationally. For bite-free pieces---capped
triangulated annuli on annular cycles of length $6,8,10,12$---every proper
$3$-colouring of $M(G)$ restricts to a Kempe-balanced colouring. The same
holds for pieces carrying a bite, including the case where singleton down
teeth lie in the bite's inner-gap face: there the inner level cycle splits
into a child level cycle per gap, and conservation across each child cycle
supplies the parity (in the checked example the three singleton down apexes
of a bite gap are a rainbow in every restriction).
\end{remark}
More generally, let $T$ be a medial tire region with boundary
\[
\partial T = C_0\sqcup C_1\sqcup\cdots\sqcup C_m.
\]
For a $P$-Kempe cycle $K$, every component of $K\cap T$ is either a
cycle contained in $T$, or a path with two endpoints on
$\partial T$. Thus the $P$-Kempe arcs inside $T$ define a pairing of
the $P$-coloured boundary incidences of
\[
C_0\sqcup C_1\sqcup\cdots\sqcup C_m.
\]
This motivates the following refinement of boundary states.
\begin{definition}[Kempe-enhanced boundary state]
Let $T$ be a medial tire region with outer level cycle $C_0$ and inner
level cycles $C_1,\ldots,C_m$. Let
\[
\mathcal C(T)=C_0\sqcup C_1\sqcup\cdots\sqcup C_m.
\]
A \emph{Kempe-enhanced boundary state} on $T$ consists of the following
data:
\begin{enumerate}
\item a boundary colouring
\[
\alpha:V(\mathcal C(T))\to\{1,2,3\};
\]
\item for each colour pair
\[
P\in\{\{1,2\},\{2,3\},\{3,1\}\},
\]
a pairing $\pi_P$ of the $P$-coloured boundary incidences of
$\mathcal C(T)$ induced by the $P$-Kempe arcs lying inside $T$.
\end{enumerate}
We write such a state as
\[
\kappa=(\alpha,\pi_{12},\pi_{23},\pi_{31}).
\]
\end{definition}
Given a proper $3$-colouring $\varphi$ of the medial tire graph
$M(T)$, the restriction of $\varphi$ to the boundary level cycles gives
the boundary colouring $\alpha$, while the two-colour Kempe arcs inside
$T$ give the pairings $\pi_{12},\pi_{23},\pi_{31}$. Thus $\varphi$
determines a Kempe-enhanced boundary state, denoted
\[
\kappa_T(\varphi).
\]
\begin{definition}[Kempe-enhanced restriction relation]
The \emph{Kempe-enhanced restriction relation} of $T$ is
\[
\mathcal K_T
=
\left\{
\kappa_T(\varphi):
\varphi \text{ is a proper }3\text{-colouring of } M(T)
\right\}.
\]
This refines the ordinary boundary-colouring relation by recording not
only which boundary colourings extend across $T$, but also how the
two-colour Kempe cycles are routed through the annular tire region.
\end{definition}
The annular structure of a tire is useful in two distinct ways. First,
it gives a bounded transition region between level cycles: the colouring
of the annular medial cycle controls, and in many cases determines, the
colouring of the remaining medial tire vertices. Thus the number of
possible transition states is bounded in terms of the annular structure,
rather than the total size of the subtree below the tire. Second, it
describes how the outer level cycle and the inner level cycles are
related by Kempe arcs. The level cycles are the gluing interfaces, while
the annular tire is the transition operator between them.
\begin{definition}[Kempe-compatible gluing]
Let $T$ be a medial tire region and let $U$ be a child region glued to
$T$ along a common level cycle $C$. Thus $C$ is an inner level cycle of
$T$ and the outer level cycle of $U$.
Let
\[
\kappa_T=(\alpha_T,\pi^T_{12},\pi^T_{23},\pi^T_{31})
\in \mathcal K_T
\]
and
\[
\kappa_U=(\alpha_U,\pi^U_{12},\pi^U_{23},\pi^U_{31})
\in \mathcal K_U.
\]
We say that $\kappa_T$ and $\kappa_U$ are \emph{Kempe-compatible along
$C$} if:
\begin{enumerate}
\item the boundary colourings agree on $C$:
\[
\alpha_T|_{V(C)}=\alpha_U|_{V(C)};
\]
\item for each colour pair
\[
P\in\{\{1,2\},\{2,3\},\{3,1\}\},
\]
the pairings $\pi^T_P$ and $\pi^U_P$ compose along the
$P$-coloured incidences of $C$ without producing an unpaired endpoint.
\end{enumerate}
When these conditions hold, the composed pairings determine a
Kempe-enhanced boundary state on the exposed boundary of
$T\cup_C U$.
\end{definition}
In these terms, gluing local colourings is not merely a matter of
matching boundary colours. The colourings must also route their
two-colour Kempe arcs compatibly across every shared level cycle. The
ordinary restriction relation records whether a boundary colouring can
be extended locally; the Kempe-enhanced relation additionally records
the conservation of Kempe-cycle flow through the annular transition
region.
For a tire with one outer level cycle and several inner level cycles,
\[
\partial T=C_0\sqcup C_1\sqcup\cdots\sqcup C_m,
\]
the parent tire may correlate the boundary states on the different
inner cycles. The Kempe-enhanced relation records this correlation as
a system of pairings among the $P$-coloured incidences of all boundary
level cycles simultaneously. Thus one should view a medial tire as a
multi-output transition operator
\[
\mathcal K_T:
C_0 \leadsto (C_1,\ldots,C_m),
\]
rather than as an independent collection of binary transitions.
The guiding principle is therefore:
\begin{quote}
Level cycles are the interfaces used for gluing, while annular tire
regions are the bounded transition regions that route Kempe cycles
between those interfaces.
\end{quote}
\begin{thebibliography}{9}
\bibitem{bauerfeld-medial-tire}
E.~Bauerfeld,
\emph{Medial Tire Decompositions of Plane Triangulations},
manuscript (math-research repository), 2026.
\bibitem{bauerfeld-nested-tire-decompositions}
E.~Bauerfeld,
\emph{Nested Tire Decompositions of Plane Triangulations},
manuscript (math-research repository), 2026.
\bibitem{tait-original}
P.~G. Tait,
\emph{Remarks on the colourings of maps},
Proceedings of the Royal Society of Edinburgh \textbf{10} (1880),
729--729.
\end{thebibliography}
\end{document}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 KiB

@@ -1,289 +0,0 @@
"""Draw the walk-depth labelling and cut of a medial tire decomposition.
Paper-graphics companion to ``run_medial_tire_cut_experiment.py``: it imports
``run_experiment`` from there, runs the pipeline on a random maximal planar
graph, and emits TikZ. By default it draws one ``tikzpicture`` (walk-depth
labels + cut slits) per recognised full medial tire graph, using ``to_tikz``
from ``medial_tire_cut_labelling``. With ``--whole`` it instead draws a
two-panel Figure 3 graphic: the source graph with its source highlighted and
the whole medial graph M(G) drawn with every medial vertex at the midpoint of
its source edge and labelled by that source edge, with the full BFS-level chain
shown and the currently computed walk-depth labels and cuts marked.
This script only renders; the experiment itself draws nothing. Run with the
repo venv (networkx): ``.venv/bin/python``.
Examples:
.venv/bin/python draw_medial_tire_cut.py -n 20 --seed 59 > panels.tex
.venv/bin/python draw_medial_tire_cut.py -n 20 --seed 59 --whole > whole.tex
"""
from __future__ import annotations
import argparse
import math
import os
import sys
import networkx as nx
import numpy as np
_HERE = os.path.dirname(os.path.abspath(__file__))
_PAPER_DIR = os.path.dirname(_HERE)
_CUT_LIB = os.path.join(_PAPER_DIR, "lib")
sys.path.insert(0, _HERE)
sys.path.insert(0, _CUT_LIB)
from run_medial_tire_cut_experiment import run_experiment # noqa: E402
from medial_tire_cut_labelling import to_tikz # noqa: E402
from tire_realization_analysis import triangular_faces # noqa: E402
def tikz_panels(n: int, seed: int, scale: float = 1.6,
min_degree: int = 5, attempts: int = 1000) -> tuple[dict, list[str]]:
"""Run the experiment and return ``(result, panels)``, one TikZ panel per
recognised tread, each showing that tread's walk-depth labelling and cut."""
result = run_experiment(n=n, seed=seed, min_degree=min_degree, attempts=attempts)
panels = []
for d in sorted(result["results"]):
rec = result["results"][d]
panels.append(to_tikz(rec["g"], depth=rec["depth"], cuts=rec["cuts"],
entry_edge=rec["entry_edge"], scale=scale))
return result, panels
# --------------------------------------------------------------------------- #
# Figure 3: the source graph and midpoint drawing of the whole medial graph.
# --------------------------------------------------------------------------- #
def _source_layout(G: nx.Graph) -> dict[int, tuple[float, float]]:
"""Straight-line planar layout for the source graph, normalised to the unit
box and reused by the medial drawing."""
faces, _ = triangular_faces(G)
outer = list(faces[0])
outer_set = set(outer)
raw = {}
for i, v in enumerate(outer):
angle = math.radians(90.0 - i * 360.0 / len(outer))
raw[v] = np.array([math.cos(angle), math.sin(angle)], dtype=float)
inner = [v for v in sorted(G.nodes()) if v not in outer_set]
if inner:
idx = {v: i for i, v in enumerate(inner)}
n = len(inner)
A = np.zeros((n, n))
bx = np.zeros(n)
by = np.zeros(n)
for i, v in enumerate(inner):
nbrs = list(G.neighbors(v))
A[i, i] = 1.0
for w in nbrs:
if w in idx:
A[i, idx[w]] -= 1.0 / len(nbrs)
else:
bx[i] += raw[w][0] / len(nbrs)
by[i] += raw[w][1] / len(nbrs)
xs = np.linalg.solve(A, bx)
ys = np.linalg.solve(A, by)
for v in inner:
raw[v] = np.array([xs[idx[v]], ys[idx[v]]], dtype=float)
pts = np.array([raw[v] for v in G.nodes()], dtype=float)
center = 0.5 * (pts.max(axis=0) + pts.min(axis=0))
span = float(max(*(pts.max(axis=0) - pts.min(axis=0)), 1.0))
return {
v: tuple((raw[v] - center) / span)
for v in G.nodes()
}
def _edge_midpoint(pos: dict, edge) -> tuple[float, float]:
u, v = edge
ux, uy = pos[u]
vx, vy = pos[v]
return (0.5 * (ux + vx), 0.5 * (uy + vy))
def _edge_label(edge) -> str:
u, v = edge
return f"${u}\\!{{-}}\\!{v}$"
def _source_graph_tikz(result: dict, pos: dict, scale: float) -> str:
G, source = result["G"], result["source"]
L = []
A = L.append
A(f"\\begin{{tikzpicture}}[scale={scale},")
A(" sedge/.style={black!50, line width=0.35pt},")
A(" sv/.style={circle, draw=black!60, fill=white, inner sep=1.1pt},")
A(" srcv/.style={circle, draw=blue!75!black, fill=blue!18, line width=0.7pt, inner sep=1.8pt}]")
def pt(v):
x, y = pos[v]
return f"({x:.3f},{y:.3f})"
for u, v in sorted(G.edges()):
A(f"\\draw[sedge] {pt(u)}--{pt(v)};")
for v in sorted(G.nodes()):
style = "srcv" if v == source else "sv"
A(f"\\node[{style}] at {pt(v)} {{}};")
sx, sy = pos[source]
A(f"\\node[font=\\scriptsize, text=blue!70!black] at ({sx:.3f},{sy - 0.085:.3f}) {{source {source}}};")
A("\\end{tikzpicture}")
return "\n".join(L)
def _medial_midpoint_tikz(result: dict, pos: dict, scale: float) -> str:
"""Draw M(G) with each medial vertex at the midpoint of its source edge.
Every medial vertex is labelled by its source edge; same-level source edges
show the BFS level-chain tooth layers, and interlevel source edges show the
annular layers. Currently computed tire walk-depth labels and cut labels
are overlaid without moving the medial vertices away from their source
edges."""
G, M = result["G"], result["M"]
levels = nx.single_source_shortest_path_length(G, result["source"])
medial_pos = {edge: _edge_midpoint(pos, edge) for edge in M.nodes()}
apex_roles = {}
apex_walks = {}
for r in result["labels"]:
apex_roles[r["apex"]] = r["role"]
apex_walks.setdefault(r["apex"], []).append(r["walk"])
cut_records = []
cut_number = 1
for c in result.get("cap_cuts", []):
cut_records.append((cut_number, c["medial_vertex"], "cap", c))
cut_number += 1
for d in sorted(result["results"]):
rec = result["results"][d]
g, bij = rec["g"], rec["bij"]
for c in rec["cuts"]:
if c.vertex is None:
continue
cut_records.append((cut_number, bij[f"a{c.vertex}"], d, c))
cut_number += 1
L = []
A = L.append
A(f"\\begin{{tikzpicture}}[scale={scale},")
A(" base/.style={black!12, line width=0.25pt},")
A(" med/.style={black!38, line width=0.32pt},")
A(" annv/.style={circle, draw=black!70, fill=black!18, inner sep=1.0pt},")
A(" levone/.style={circle, draw=orange!75!black, fill=orange!20, inner sep=1.2pt},")
A(" levtwo/.style={circle, draw=violet!70!black, fill=violet!18, inner sep=1.2pt},")
A(" levthree/.style={circle, draw=teal!70!black, fill=teal!18, inner sep=1.2pt},")
A(" knownv/.style={circle, draw=red!70!black, fill=red!24, inner sep=1.5pt},")
A(" elbl/.style={font=\\tiny, text=black!70, inner sep=0.2pt},")
A(" dlbl/.style={font=\\tiny\\bfseries, text=black, inner sep=0.5pt},")
A(" cut/.style={red!80!black, line width=1.0pt},")
A(" cutlbl/.style={font=\\tiny, text=red!75!black}]")
def pt_med(edge):
x, y = medial_pos[edge]
return f"({x:.3f},{y:.3f})"
def pt_src(v):
x, y = pos[v]
return f"({x:.3f},{y:.3f})"
for u, v in sorted(result["G"].edges()):
A(f"\\draw[base] {pt_src(u)}--{pt_src(v)};")
for u, v in M.edges():
A(f"\\draw[med] {pt_med(u)}--{pt_med(v)};")
def chain_style(edge):
u, v = edge
lu, lv = levels[u], levels[v]
if lu != lv:
return "annv"
if edge in apex_roles:
return "knownv"
return {1: "levone", 2: "levtwo", 3: "levthree"}.get(lu, "annv")
for mv in sorted(M.nodes()):
A(f"\\node[{chain_style(mv)}] at {pt_med(mv)} {{}};")
for mv in sorted(M.nodes()):
x, y = medial_pos[mv]
A(f"\\node[elbl] at ({x:.3f},{y:.3f}) [yshift=-4.8pt] {{{_edge_label(mv)}}};")
for mv in sorted(apex_walks):
x, y = medial_pos[mv]
label = ",".join(str(w) for w in sorted(apex_walks[mv]))
A(f"\\node[dlbl] at ({x:.3f},{y:.3f}) [yshift=5.0pt] {{{label}}};")
for number, mv, _d, _cut in cut_records:
u, v = mv
ux, uy = pos[u]
vx, vy = pos[v]
mx, my = medial_pos[mv]
ex, ey = vx - ux, vy - uy
length = math.hypot(ex, ey) or 1.0
dx, dy = -0.035 * ey / length, 0.035 * ex / length
A(f"\\draw[cut] ({mx - dx:.3f},{my - dy:.3f})--({mx + dx:.3f},{my + dy:.3f});")
A(f"\\node[cutlbl] at ({mx + 2.4 * dx:.3f},{my + 2.4 * dy:.3f}) {{cut {number}}};")
A("\\end{tikzpicture}")
return "\n".join(L)
def medial_tikz(result: dict, scale: float = 7.0) -> str:
"""Two-panel TikZ for Figure 3: the source graph and the midpoint drawing of
its medial graph with all medial vertices labelled, plus the tire
walk-depth labels and cuts."""
pos = _source_layout(result["G"])
source = _source_graph_tikz(result, pos, scale=0.58 * scale)
medial = _medial_midpoint_tikz(result, pos, scale=scale)
return "\n".join([
"\\begin{tabular}{c}",
source,
"\\\\[-0.25ex]",
"{\\scriptsize source graph $G$}",
"\\\\[1.0ex]",
medial,
"\\\\[-0.25ex]",
"{\\scriptsize medial graph $M(G)$ at edge midpoints}",
"\\end{tabular}",
])
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("-n", type=int, default=20)
parser.add_argument("--seed", type=int, default=72)
parser.add_argument("--scale", type=float, default=1.6)
parser.add_argument("--min-degree", type=int, default=5,
help="reject random graphs below this minimum degree")
parser.add_argument("--attempts", type=int, default=1000,
help="number of consecutive seeds to try for --min-degree")
parser.add_argument("--whole", action="store_true",
help="draw the whole medial graph M(G) with all cuts, "
"instead of one panel per tread")
args = parser.parse_args()
if args.whole:
result = run_experiment(n=args.n, seed=args.seed,
min_degree=args.min_degree, attempts=args.attempts)
treads = sorted(result["results"])
print(f"% whole medial graph: n={args.n} seed={args.seed} "
f"graph_seed={result['graph_seed']} min_degree={result['min_degree']} "
f"source={result['source']} recognised treads={treads} "
f"|M(G)|={result['M'].number_of_nodes()}")
print(medial_tikz(result, scale=args.scale if args.scale != 1.6 else 7.0))
return
result, panels = tikz_panels(args.n, args.seed, scale=args.scale,
min_degree=args.min_degree, attempts=args.attempts)
treads = sorted(result["results"])
print(f"% medial tire cut: n={args.n} seed={args.seed} "
f"graph_seed={result['graph_seed']} min_degree={result['min_degree']} "
f"source={result['source']} recognised treads={treads}")
if not panels:
print("% (no recognised full medial tire graphs for this graph)")
for d, panel in zip(treads, panels):
g = result["results"][d]["g"]
print(f"% --- tread {d}: |A(T)|={g.n} word={g.tooth_word} "
f"bites={sorted(g.bites)} ---")
print(panel)
if __name__ == "__main__":
main()
@@ -1,47 +0,0 @@
# Full medial tire cut walk 1
- base vertices: 20
- deep-embedded vertices: 30
- deep-embedded edges: 84
- graph seed: 59
- deep-embedded minimum degree: 3
- chosen face: (8, 9, 19)
- chosen source cap vertex: 24
- root entry tooth: e2 (apex medial vertex = level-1 edge (19, 8))
- recognised treads: 11
- skipped treads: [((0, 0), 'only 0 up teeth')]
- removed source-dual edges: 29
- annular/cap cuts: 12
- up-apex cuts: 17
- **source-dual cut is a tree: True** (56 dual faces, 55 edges, 1 component(s), acyclic=True)
## Walk-distance labelling of the source-dual cut
Each dual face (vertex of the source-dual cut) is labelled by its distance, within the cut, from the **cap down tooth of the first entry**: the triangular face `(8, 24, 19)` = `{source 24, edge (19, 8)}` (dual node 34).
- maximum distance: 21
- distance histogram (faces by distance): `{0: 1, 1: 2, 2: 2, 3: 2, 4: 2, 5: 2, 6: 3, 7: 3, 8: 4, 9: 4, 10: 4, 11: 4, 12: 3, 13: 3, 14: 2, 15: 2, 16: 1, 17: 2, 18: 3, 19: 4, 20: 2, 21: 1}`
- dual cut figure: `full_medial_tire_cut_walk_1_dual.png`
- tire cut grid: `full_medial_tire_cut_walk_1_tires.png`
- combined PDF: `full_medial_tire_cut_walk_1.pdf`
| tread | depth | component | annular | up | singleton down | bite apexes | entry | closing cuts | up-apex cuts | shared/entry skipped |
|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
| T1 | 1 | 0 | 9 | 3 | 6 | 0 | e2 | 1 | 2 | 1 |
| T2 | 2 | 0 | 17 | 6 | 11 | 0 | e15 | 1 | 5 | 1 |
| T3 | 3 | 0 | 3 | 3 | 0 | 0 | e1 | 1 | 2 | 1 |
| T4 | 3 | 1 | 3 | 3 | 0 | 0 | e1 | 1 | 2 | 1 |
| T5 | 3 | 2 | 3 | 3 | 0 | 0 | e1 | 1 | 2 | 1 |
| T6 | 3 | 3 | 3 | 3 | 0 | 0 | e0 | 1 | 2 | 1 |
| T7 | 3 | 4 | 3 | 3 | 0 | 0 | e1 | 1 | 2 | 1 |
| T8 | 3 | 5 | 3 | 3 | 0 | 0 | e1 | 1 | 2 | 1 |
| T9 | 3 | 6 | 3 | 3 | 0 | 0 | e0 | 1 | 2 | 1 |
| T10 | 3 | 7 | 3 | 3 | 0 | 0 | e2 | 1 | 2 | 1 |
| T11 | 3 | 8 | 3 | 3 | 0 | 0 | e2 | 1 | 2 | 1 |
## Removed Source-Dual Edges
- annular/cap: `[(0, 20), (0, 21), (1, 6), (7, 8), (11, 25), (11, 26), (12, 27), (15, 29), (16, 28), (19, 24), (22, 5), (23, 4)]`
- up apexes: `[(0, 5), (1, 5), (2, 3), (2, 7), (4, 5), (8, 9), (10, 3), (10, 18), (11, 16), (12, 15), (12, 16), (13, 14), (13, 15), (14, 4), (16, 17), (18, 6), (19, 9)]`
Binary file not shown.

Before

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 458 KiB

@@ -1,39 +0,0 @@
# Full medial tire cut walk 2
- base vertices: 20
- deep-embedded vertices: 21
- deep-embedded edges: 57
- graph seed: 2
- deep-embedded minimum degree: 3
- chosen face: (4, 12, 11)
- chosen source cap vertex: 20
- root entry tooth: e3 (apex medial vertex = level-1 edge (11, 4))
- recognised treads: 3
- skipped treads: [((0, 0), 'only 0 up teeth')]
- removed source-dual edges: 20
- annular/cap cuts: 5
- up-apex cuts: 15
- **source-dual cut is a tree: True** (38 dual faces, 37 edges, 1 component(s), acyclic=True)
## Walk-distance labelling of the source-dual cut
Each dual face (vertex of the source-dual cut) is labelled by its distance, within the cut, from the **cap down tooth of the first entry**: the triangular face `(4, 20, 11)` = `{source 20, edge (11, 4)}` (dual node 15).
- maximum distance: 17
- distance histogram (faces by distance): `{0: 1, 1: 2, 2: 2, 3: 2, 4: 2, 5: 3, 6: 3, 7: 3, 8: 3, 9: 3, 10: 4, 11: 3, 12: 2, 13: 1, 14: 1, 15: 1, 16: 1, 17: 1}`
- dual cut figure: `full_medial_tire_cut_walk_2_dual.png`
- tire cut grid: `full_medial_tire_cut_walk_2_tires.png`
- combined PDF: `full_medial_tire_cut_walk_2.pdf`
| tread | depth | component | annular | up | singleton down | bite apexes | entry | closing cuts | up-apex cuts | shared/entry skipped |
|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
| T1 | 1 | 0 | 10 | 3 | 7 | 0 | e3 | 1 | 2 | 1 |
| T2 | 2 | 0 | 15 | 7 | 8 | 0 | e3 | 1 | 6 | 1 |
| T3 | 3 | 0 | 10 | 8 | 0 | 1 | e7 | 2 | 7 | 1 |
## Removed Source-Dual Edges
- annular/cap: `[(3, 4), (3, 9), (8, 9), (11, 20), (15, 7)]`
- up apexes: `[(0, 3), (0, 5), (1, 2), (1, 6), (2, 9), (10, 18), (11, 12), (12, 4), (13, 5), (13, 19), (14, 6), (14, 15), (15, 16), (16, 17), (18, 19)]`
Binary file not shown.

Before

Width:  |  Height:  |  Size: 243 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

@@ -1,200 +0,0 @@
"""Regenerate the ``full_walk`` contents (``.md`` report + ``_dual.png`` +
``_tires.png`` + combined ``.pdf``) for each configured medial tire cut walk.
Each walk fixes a reproducible source: a base maximal planar 5-connected graph
``random_maximal_planar_5_connected(n, seed)``, a chosen triangular face, the
deep-embedding cap vertex placed inside that face as the level source, and a
root entry tooth. The source-dual cut, its walk-distance labelling, and the
figures are all read off the deep embedding ``G'`` (whose dual-cut figure is now
drawn on a *valid straight-line embedding* of ``G'`` -- see
``medial_tire_dual_cut_experiment._straight_line_source_layout``).
Run with the repo venv::
../../../.venv/bin/python generate_full_walk.py # all walks
../../../.venv/bin/python generate_full_walk.py --walk 1 # just walk 1
"""
from __future__ import annotations
import argparse
import os
import sys
import networkx as nx
_HERE = os.path.dirname(os.path.abspath(__file__))
_EXP = os.path.dirname(_HERE)
sys.path.insert(0, _EXP)
import medial_tire_dual_cut_experiment as E # noqa: E402
from run_medial_tire_cut_experiment import ( # noqa: E402
random_maximal_planar_5_connected,
)
# --------------------------------------------------------------------------- #
# Walk configurations. Each is fully reproducible from (n, seed, face, entry).
# --------------------------------------------------------------------------- #
WALKS = [
{
"index": 1,
"title": "Full medial tire cut walk 1",
"n": 20,
"seed": 59,
"face": (8, 9, 19),
"entry": 2,
},
{
"index": 2,
"title": "Full medial tire cut walk 2",
"n": 20,
"seed": 2,
"face": (4, 12, 11),
"entry": 3,
},
]
def build_result(cfg):
"""Build the source-dual cut result dict for one walk configuration."""
G, graph_seed = random_maximal_planar_5_connected(
cfg["n"], cfg["seed"], min_connectivity=5)
G_prime, cap, depth = E.deep_embedding(G, cfg["face"])
result = E.medial_tire_dual_cut(G_prime, cap, cfg["entry"])
result["base_graph"] = G
result["chosen_face"] = tuple(cfg["face"])
result["cap_vertex"] = cap
result["deep_depth"] = depth
result["graph_seed"] = graph_seed
result["base_min_degree"] = min(dict(G.degree()).values())
result["base_connectivity"] = nx.node_connectivity(G)
result["min_degree"] = min(dict(G_prime.degree()).values())
result["connectivity"] = nx.node_connectivity(G_prime)
return result
def _tree_report(result):
"""``(is_tree, n_faces, n_edges, n_components, acyclic)`` for the cut."""
cut = E.dual_cut_graph(result)
n = cut.number_of_nodes()
e = cut.number_of_edges()
comps = nx.number_connected_components(cut)
return nx.is_tree(cut), n, e, comps, (e == n - comps)
def _distance_histogram(dist):
"""Histogram of dual faces by walk distance, as an ordered dict."""
hist = {}
for d in dist.values():
hist[d] = hist.get(d, 0) + 1
return {k: hist[k] for k in sorted(hist)}
def render_markdown(result, cfg):
"""The walk report in the committed ``.md`` format."""
G = result["G"]
base = result["base_graph"]
removed = result["removed_dual_edges"]
res = result["results"]
i = cfg["index"]
em = result["entry_medial_vertex"]
is_tree, n_faces, n_edges, comps, acyclic = _tree_report(result)
dist, root = E.dual_cut_distances(result)
root_face = result["faces"][root]
hist = _distance_histogram(dist)
max_dist = max(dist.values()) if dist else 0
lines = [
f"# {cfg['title']}",
"",
f"- base vertices: {base.number_of_nodes()}",
f"- deep-embedded vertices: {G.number_of_nodes()}",
f"- deep-embedded edges: {G.number_of_edges()}",
f"- graph seed: {result['graph_seed']}",
f"- deep-embedded minimum degree: {result['min_degree']}",
f"- chosen face: {result['chosen_face']}",
f"- chosen source cap vertex: {result['source']}",
f"- root entry tooth: e{result['entry_edge']} "
f"(apex medial vertex = level-1 edge {em})",
f"- recognised treads: {len(res)}",
f"- skipped treads: {result['skipped']}",
f"- removed source-dual edges: {len(removed)}",
f"- annular/cap cuts: {len(result['annular_cut_edges'])}",
f"- up-apex cuts: {len(result['apex_cut_edges'])}",
"",
f"- **source-dual cut is a tree: {is_tree}** ({n_faces} dual faces, "
f"{n_edges} edges, {comps} component(s), acyclic={acyclic})",
"",
"## Walk-distance labelling of the source-dual cut",
"",
"Each dual face (vertex of the source-dual cut) is labelled by its "
"distance, within the cut, from the **cap down tooth of the first "
f"entry**: the triangular face `{root_face}` = "
f"`{{source {result['source']}, edge {em}}}` (dual node {root}).",
"",
f"- maximum distance: {max_dist}",
f"- distance histogram (faces by distance): `{hist}`",
"",
f"- dual cut figure: `full_medial_tire_cut_walk_{i}_dual.png`",
f"- tire cut grid: `full_medial_tire_cut_walk_{i}_tires.png`",
f"- combined PDF: `full_medial_tire_cut_walk_{i}.pdf`",
"",
"| tread | depth | component | annular | up | singleton down | "
"bite apexes | entry | closing cuts | up-apex cuts | "
"shared/entry skipped |",
"|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|",
]
for idx, key in enumerate(sorted(res), start=1):
d, comp = key
rec = res[key]
g, bij, entry = rec["g"], rec["bij"], rec["entry_edge"]
up = len(g.up_edges)
apex = len(E.up_apex_cuts(g, entry, bij))
lines.append(
f"| T{idx} | {d} | {comp} | {g.n} | {up} | "
f"{len(g.singleton_down_edges)} | {len(g.bites)} | e{entry} | "
f"{len(rec['cuts'])} | {apex} | {up - apex} |")
lines += [
"",
"## Removed Source-Dual Edges",
"",
f"- annular/cap: `{sorted(result['annular_cut_edges'])}`",
f"- up apexes: `{sorted(result['apex_cut_edges'])}`",
"",
]
return "\n".join(lines)
def generate(cfg):
"""Build one walk and write its ``.md`` report and three figures."""
result = build_result(cfg)
i = cfg["index"]
stem = os.path.join(_HERE, f"full_medial_tire_cut_walk_{i}")
with open(f"{stem}.md", "w") as fh:
fh.write(render_markdown(result, cfg))
E.draw_png(result, f"{stem}_dual.png")
E.draw_tire_cuts_png(result, f"{stem}_tires.png")
E.draw_combined_pdf(result, f"{stem}.pdf")
print(f"walk {i}: wrote {stem}.md + _dual.png + _tires.png + .pdf")
def main():
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--walk", type=int, default=None,
help="regenerate only this walk index (default: all)")
args = parser.parse_args()
for cfg in WALKS:
if args.walk is None or cfg["index"] == args.walk:
generate(cfg)
if __name__ == "__main__":
main()
Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

@@ -1,17 +0,0 @@
"""Compatibility wrapper for the medial tire cut labelling script."""
from __future__ import annotations
import os
import sys
PAPER_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
LIB_DIR = os.path.join(PAPER_DIR, "lib")
if LIB_DIR not in sys.path:
sys.path.insert(0, LIB_DIR)
from medial_tire_cut_labelling import main
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff
@@ -1,620 +0,0 @@
"""Medial tire cut experiment.
End-to-end experiment for the *Medial Tire Cuts* paper:
1. Generate a 5-connected maximal planar graph G on n vertices, using
``plantri -c5`` when available and verifying node connectivity.
2. Build its medial graph M(G).
3. Take the nested tire decomposition at one random vertex level source: the
BFS-level treads, each realized as a FullMedialTireGraph.
4. Walk-depth label and cut each full medial tire graph, chaining the labels
down the tire tree, and assemble one final cut graph of M(G) with a global
label map.
This script produces *data* (the final cut graph and its labels); it draws
nothing. Anything for the paper (figures) lives in a separate script that
imports ``run_experiment`` from here.
Chaining rule (walk depths across the tire tree).
* The root tread (no recognised parent) is entered at an arbitrary up tooth
with walk depth 0.
* A child tread is entered at the up tooth whose apex is the *same medial
vertex* as the parent's down tooth of lowest walk depth -- a parent down
tooth and the child up tooth glued to it across the shared level cycle are
the same medial vertex of M(G). The entry up tooth's walk depth is that
parent down tooth's depth + 1, and the walk increments locally from there.
* The source cap contributes one additional cut. It is placed at the
counter-clockwise source edge incident to the cap down tooth whose apex is
the root tread's entry up tooth.
Run with the repo venv (networkx + scipy): ``.venv/bin/python``.
"""
from __future__ import annotations
import argparse
import json
import os
import random
import subprocess
import sys
from collections import defaultdict
import networkx as nx
# Reuse the realization pipeline from the medial tire decompositions paper, and
# the walk-depth labelling/cut from this paper's companion script.
_HERE = os.path.dirname(os.path.abspath(__file__))
_MTD = os.path.normpath(os.path.join(
_HERE, "..", "..",
"medial_tire_decompositions_of_plane_triangulations", "experiments"))
_PAPER_DIR = os.path.dirname(_HERE)
_CUT_LIB = os.path.join(_PAPER_DIR, "lib")
sys.path.insert(0, _HERE)
sys.path.insert(0, _MTD)
sys.path.insert(0, _CUT_LIB)
from tire_realization_analysis import ( # noqa: E402
ekey, medial_graph, medial_tire_facemodel,
random_maximal_planar, recognise, triangular_faces,
)
from medial_tire_cut_labelling import door_bite, label_and_cut # noqa: E402
# --------------------------------------------------------------------------- #
# Component-based tread recognition.
# --------------------------------------------------------------------------- #
def _edge_face_data(faces):
edge_faces = defaultdict(list)
for i, face in enumerate(faces):
for x, y in ((face[0], face[1]), (face[1], face[2]), (face[2], face[0])):
edge_faces[ekey(x, y)].append(i)
return edge_faces
def _depth_components(faces, edge_faces, levels):
depths = [min(levels[v] for v in face) for face in faces]
dual_adj = defaultdict(set)
for incident in edge_faces.values():
for a in range(len(incident)):
for b in range(a + 1, len(incident)):
dual_adj[incident[a]].add(incident[b])
dual_adj[incident[b]].add(incident[a])
comps = []
seen = [False] * len(faces)
for start in range(len(faces)):
if seen[start]:
continue
depth = depths[start]
stack = [start]
comp = []
seen[start] = True
while stack:
face = stack.pop()
comp.append(face)
for other in dual_adj[face]:
if not seen[other] and depths[other] == depth:
seen[other] = True
stack.append(other)
comps.append((depth, tuple(sorted(comp))))
return comps
def _tread_from_component(faces, levels, face_indices):
tread_faces = [faces[i] for i in face_indices]
if not tread_faces:
return None
depth = min(min(levels[v] for v in face) for face in tread_faces)
annular, up, down = set(), set(), set()
face_of_down = defaultdict(int)
for face in tread_faces:
for x, y in ((face[0], face[1]), (face[1], face[2]), (face[2], face[0])):
e = ekey(x, y)
lx, ly = levels[x], levels[y]
if {lx, ly} == {depth, depth + 1}:
annular.add(e)
elif lx == ly == depth:
up.add(e)
elif lx == ly == depth + 1:
down.add(e)
face_of_down[e] += 1
if len(annular) < 3:
return None
return {
"tread_faces": tread_faces,
"annular": annular,
"up": up,
"down": down,
"bites": {e for e in down if face_of_down[e] == 2},
}
def _build_treads(faces, levels):
"""Recognise simple cycles inside connected depth components.
The returned ``treads`` keeps the existing simple-tire interface used by
the labelling code. ``tread_meta`` records the connected depth component
each simple cycle came from, so compound tires can be chained through
shared up apexes rather than seeded as unrelated roots.
"""
treads, skipped, tread_meta = {}, [], {}
edge_faces = _edge_face_data(faces)
comps = sorted(_depth_components(faces, edge_faces, levels),
key=lambda item: (item[0], item[1]))
component_count = defaultdict(int)
tire_count = defaultdict(int)
for depth, face_indices in comps:
component = component_count[depth]
component_count[depth] += 1
tread = _tread_from_component(faces, levels, face_indices)
if tread is None:
skipped.append(((depth, component), "no tread faces"))
continue
if len(tread["up"]) < 3:
skipped.append(((depth, component), f"only {len(tread['up'])} up teeth"))
continue
tires = recognise(medial_tire_facemodel(tread["tread_faces"]), tread)
if not tires:
skipped.append(((depth, component), "no annular cycle recognised as a tire"))
continue
compound = (depth, component)
cycle_count = len(tires)
for cycle, gb in enumerate(tires):
key = (depth, tire_count[depth])
tire_count[depth] += 1
treads[key] = gb
tread_meta[key] = {
"compound": compound,
"cycle": cycle,
"cycle_count": cycle_count,
"face_indices": face_indices,
}
return treads, skipped, tread_meta
# --------------------------------------------------------------------------- #
# 4. Walk-depth labelling and cut, chained down the tire tree.
# --------------------------------------------------------------------------- #
def _apex_vertex(g, bij, edge):
"""The medial vertex that is the apex of the tooth on annular ``edge``."""
return bij[g.apex_of_edge(edge)]
def _label_treads(treads, results, root_entry_edge=None, tread_meta=None):
"""Fill ``results[(d, c)]`` with the walk-depth labelling and cuts for every
recognised tire ``c`` of every tread depth ``d``, chaining child entries to
parent down teeth.
``treads`` maps ``(depth, component)`` -> ``(g, bij)``; a tread depth may
carry several tires (one per disjoint annular cycle). The root tire
``(root_d, 0)`` is entered at ``root_entry_edge`` when given -- it must be
one of that tire's up teeth -- otherwise at an arbitrary up tooth. Each
other tire chains to whichever parent-depth down tooth (across all parent
tires) shares its apex, at the lowest parent walk depth.
"""
if not treads:
return
depths = sorted({k[0] for k in treads})
root_d = depths[0]
for d in depths:
# apex medial vertex -> child start depth, over all parent-depth tires
parent_down = {}
for pk in (k for k in treads if k[0] == d - 1):
pg, pbij = treads[pk]
pdepth = results[pk]["depth"]
for e in pg.down_edges:
apex = _apex_vertex(pg, pbij, e)
value = pdepth[e] + 1
if apex not in parent_down or value < parent_down[apex]:
parent_down[apex] = value
has_parent = any(k[0] == d - 1 for k in treads)
pending = sorted(k for k in treads if k[0] == d)
while pending:
progressed = False
deferred = []
use_sibling_entries = has_parent and not any(
parent_down.keys() & {treads[key][1][f"u{m}"]
for m in treads[key][0].up_edges}
for key in pending
if key not in results
)
for key in pending:
if key in results:
continue
g, bij = treads[key]
child_up_apex = {bij[f"u{m}"]: m for m in g.up_edges}
entry = None
if not has_parent:
if (key == (root_d, 0) and root_entry_edge is not None
and root_entry_edge in g.up_edges):
entry = (root_entry_edge, 0)
else:
entry = (g.up_edges[0], 0) # arbitrary entry
else:
best = None
for apex, value in parent_down.items():
if apex in child_up_apex and (best is None or value < best[1]):
best = (child_up_apex[apex], value)
if best is not None: # chains to a parent down tooth
entry = best
elif use_sibling_entries:
compound = (
tread_meta.get(key, {}).get("compound")
if tread_meta is not None else None
)
sibling_best = None
if compound is not None:
for sk, srec in results.items():
if sk[0] != d or srec.get("compound") != compound:
continue
sg, sbij = srec["g"], srec["bij"]
for edge in sg.up_edges:
apex = sbij[f"u{edge}"]
if apex not in child_up_apex:
continue
value = srec["depth"][edge] + 1
if (sibling_best is None
or value > sibling_best[1]):
sibling_best = (child_up_apex[apex], value)
if sibling_best is not None:
entry = sibling_best
if entry is None:
deferred.append(key)
continue
entry_edge, start_depth = entry
depth, cuts = label_and_cut(g, entry_edge, start_depth=start_depth)
results[key] = {"g": g, "bij": bij, "entry_edge": entry_edge,
"start_depth": start_depth, "depth": depth,
"cuts": cuts}
if tread_meta is not None and key in tread_meta:
results[key].update(tread_meta[key])
progressed = True
if progressed:
pending = deferred
continue
# Degenerate component: no parent or labelled sibling gives an
# entry. Seed it so any remaining sibling cycles can chain to it.
key = deferred[0]
g, bij = treads[key]
entry_edge, start_depth = g.up_edges[0], 0
depth, cuts = label_and_cut(g, entry_edge, start_depth=start_depth)
results[key] = {"g": g, "bij": bij, "entry_edge": entry_edge,
"start_depth": start_depth, "depth": depth,
"cuts": cuts}
if tread_meta is not None and key in tread_meta:
results[key].update(tread_meta[key])
pending = deferred[1:]
def _cap_cut(G, emb, source, levels, results):
"""The source-cap cut determined by the first recognised tread's entry.
If the root tread enters at an up tooth whose apex is the level-1 edge
``xy``, then ``xy`` is a down tooth of the source cap. Cut the
counter-clockwise source edge incident to that down tooth. The returned
record also stores the local neighbour split used to unzip the medial
vertex in the whole medial graph.
"""
if not results:
return []
root_depth = min(results)
rec = results[root_depth]
g, bij = rec["g"], rec["bij"]
x, y = _apex_vertex(g, bij, rec["entry_edge"])
if levels.get(x) != 1 or levels.get(y) != 1:
return []
order = list(emb.neighbors_cw_order(source))
if x not in order or y not in order:
return []
next_cw = {v: order[(i + 1) % len(order)] for i, v in enumerate(order)}
prev_cw = {v: order[(i - 1) % len(order)] for i, v in enumerate(order)}
if next_cw[x] == y:
cut_endpoint, other_endpoint = x, y
elif next_cw[y] == x:
cut_endpoint, other_endpoint = y, x
else:
return []
other_cap_endpoint = prev_cw[cut_endpoint]
mv = ekey(source, cut_endpoint)
return [{
"medial_vertex": mv,
"down_tooth": ekey(cut_endpoint, other_endpoint),
"neighbours_a": [
ekey(source, other_endpoint),
ekey(cut_endpoint, other_endpoint),
],
"neighbours_b": [
ekey(source, other_cap_endpoint),
ekey(cut_endpoint, other_cap_endpoint),
],
}]
# --------------------------------------------------------------------------- #
# Assemble one final cut graph of M(G) with a global label map.
# --------------------------------------------------------------------------- #
def _assemble_cut_graph(M, results, cap_cuts=None):
"""Apply every tread's cuts to M(G).
Each cut duplicates an annular medial vertex, splitting its four incident
medial edges along the slit between the two teeth meeting there: the tooth
on the previous annular edge (with that edge's far annular vertex) goes to
one copy, the tooth on the next annular edge to the other.
Returns ``(H, label_records, warnings)`` where ``H`` is the cut graph (a
networkx graph whose split vertices are keyed ``(medial_vertex, "A"/"B",
tread)``) and ``label_records`` lists every tooth's walk depth.
"""
# Per cut annular vertex: map each original neighbour -> which copy keeps it.
split = {} # medial_vertex -> {neighbour_medial_vertex: copy_node}
warnings = []
for i, c in enumerate(cap_cuts or []):
mv = c["medial_vertex"]
if mv in split:
warnings.append(f"cap cut at {mv} was already cut; skipped")
continue
copy_a = (mv, "A", f"cap{i}")
copy_b = (mv, "B", f"cap{i}")
split[mv] = {
**{v: copy_a for v in c["neighbours_a"]},
**{v: copy_b for v in c["neighbours_b"]},
}
for key in sorted(results):
td = key[0]
g, bij = results[key]["g"], results[key]["bij"]
n = g.n
for c in results[key]["cuts"]:
kk = c.vertex
if kk is None:
continue
mv = bij[f"a{kk}"]
if mv in split:
warnings.append(f"annular vertex a{kk} of tread {key} cut twice; "
f"second cut not applied")
continue
e_prev, e_next = (kk - 1) % n, kk
copy_a = (mv, "A", td)
copy_b = (mv, "B", td)
split[mv] = {
bij[f"a{(kk - 1) % n}"]: copy_a,
_apex_vertex(g, bij, e_prev): copy_a,
bij[f"a{(kk + 1) % n}"]: copy_b,
_apex_vertex(g, bij, e_next): copy_b,
}
def resolve(node, other):
return split[node][other] if node in split else node
H = nx.Graph()
H.add_nodes_from(v for v in M.nodes() if v not in split)
for v, copies in split.items():
H.add_nodes_from(set(copies.values()))
for u, v in M.edges():
H.add_edge(resolve(u, v), resolve(v, u))
label_records = []
for key in sorted(results):
td = key[0]
g, bij, depth = results[key]["g"], results[key]["bij"], results[key]["depth"]
for k in range(g.n):
role = ("up" if g.tooth_word[k] == "U"
else "bite" if door_bite(g, k) is not None else "down")
label_records.append({
"tread": td, "comp": key[1], "edge": k, "role": role,
"apex": _apex_vertex(g, bij, k), "walk": depth[k],
})
return H, label_records, warnings
# --------------------------------------------------------------------------- #
# Driver.
# --------------------------------------------------------------------------- #
def random_maximal_planar_5_connected(n: int, seed: int, flips: int = 400,
min_connectivity: int = 5,
attempts: int = 1000) -> tuple[nx.Graph, int]:
"""Generate a maximal planar graph with node connectivity at least
``min_connectivity``. The returned seed is the actual sample seed used."""
if min_connectivity <= 0:
return random_maximal_planar(n, seed, flips=flips), seed
if min_connectivity >= 5:
plantri = os.path.normpath(os.path.join(_HERE, "..", "..", "..",
"plantri", "plantri"))
if os.path.exists(plantri):
data = subprocess.check_output(
[plantri, "-c5", "-g", str(n)],
stderr=subprocess.DEVNULL)
graphs = [
line for line in data.splitlines()
if line and not line.startswith(b">>")
]
if graphs:
for offset in range(len(graphs)):
G = nx.from_graph6_bytes(graphs[(seed + offset) % len(graphs)])
G = nx.convert_node_labels_to_integers(G)
if nx.node_connectivity(G) >= min_connectivity:
return G, seed + offset
for offset in range(attempts):
sample_seed = seed + offset
G = random_maximal_planar(n, sample_seed, flips=flips)
if nx.node_connectivity(G) >= min_connectivity:
return G, sample_seed
raise RuntimeError(
f"no random maximal planar graph on {n} vertices with "
f"node connectivity >= {min_connectivity} found in {attempts} attempts "
f"starting at seed {seed}")
def run_experiment(n: int = 12, seed: int = 0, flips: int = 400,
min_connectivity: int = 5, attempts: int = 1000,
min_degree: int | None = None) -> dict:
"""Run the full pipeline and return a structured result.
Result keys: ``n, seed, G, M, source, treads`` (dict depth -> (g, bij)),
``results`` (dict depth -> labelling/cut record), ``skipped`` (list of
(depth, reason)), ``cut_graph`` (networkx graph), ``labels`` (list of tooth
records), ``warnings``.
"""
if min_degree is not None:
min_connectivity = max(min_connectivity, min_degree)
G, graph_seed = random_maximal_planar_5_connected(
n, seed, flips=flips, min_connectivity=min_connectivity, attempts=attempts)
faces, emb = triangular_faces(G)
M = medial_graph(G)
source = random.Random(f"source-{graph_seed}").choice(sorted(G.nodes()))
levels = nx.single_source_shortest_path_length(G, source)
treads, skipped, tread_meta = _build_treads(faces, levels)
results = {}
_label_treads(treads, results, tread_meta=tread_meta)
cap_cuts = _cap_cut(G, emb, source, levels, results)
cut_graph, labels, warnings = _assemble_cut_graph(M, results, cap_cuts=cap_cuts)
return {
"n": n, "seed": seed, "graph_seed": graph_seed,
"min_degree": min(dict(G.degree()).values()),
"connectivity": nx.node_connectivity(G),
"G": G, "M": M, "source": source,
"treads": treads, "tread_meta": tread_meta,
"results": results, "cap_cuts": cap_cuts,
"skipped": skipped,
"cut_graph": cut_graph, "labels": labels, "warnings": warnings,
}
# --------------------------------------------------------------------------- #
# Serialization / reporting.
# --------------------------------------------------------------------------- #
def _vname(v) -> str:
"""Stable string name for a medial vertex (an edge key) or a split node."""
if isinstance(v, tuple) and len(v) == 3 and v[1] in ("A", "B"):
mv, side, d = v
return f"{mv[0]}-{mv[1]}#{side}@T{d}"
return f"{v[0]}-{v[1]}"
def to_json(result: dict) -> dict:
res = result["results"]
treads_out = []
for key in sorted(res):
d, comp = key
g, bij = res[key]["g"], res[key]["bij"]
depth, cuts = res[key]["depth"], res[key]["cuts"]
teeth = [{
"edge": k,
"role": ("up" if g.tooth_word[k] == "U"
else "bite" if door_bite(g, k) is not None else "down"),
"apex": _vname(_apex_vertex(g, bij, k)),
"walk": depth[k],
} for k in range(g.n)]
treads_out.append({
"depth": d, "comp": comp, "n": g.n, "tooth_word": g.tooth_word,
"bites": sorted(list(b) for b in g.bites),
"entry_edge": res[key]["entry_edge"], "start_depth": res[key]["start_depth"],
"teeth": teeth,
"cuts": [{
"annular_index": c.vertex,
"annular_vertex": _vname(bij[f"a{c.vertex}"]),
"last_edge": c.last_tooth, "closing_edge": c.closing_tooth,
} for c in cuts],
})
H = result["cut_graph"]
return {
"n": result["n"], "seed": result["seed"],
"graph_seed": result["graph_seed"], "min_degree": result["min_degree"],
"connectivity": result["connectivity"],
"source": result["source"],
"graph_edges": sorted([int(u), int(v)] for u, v in result["G"].edges()),
"medial_vertices": result["M"].number_of_nodes(),
"skipped": [[d, why] for d, why in result["skipped"]],
"cap_cuts": [{
"medial_vertex": _vname(c["medial_vertex"]),
"down_tooth": _vname(c["down_tooth"]),
} for c in result["cap_cuts"]],
"treads": treads_out,
"cut_graph": {
"nodes": sorted(_vname(v) for v in H.nodes()),
"edges": sorted([_vname(u), _vname(v)] for u, v in H.edges()),
},
"labels": [{
"tread": r["tread"], "comp": r.get("comp", 0),
"edge": r["edge"], "role": r["role"],
"apex": _vname(r["apex"]), "walk": r["walk"],
} for r in result["labels"]],
"warnings": result["warnings"],
}
def summary(result: dict) -> str:
H, res = result["cut_graph"], result["results"]
lines = [
f"random maximal planar graph: n={result['n']} requested seed={result['seed']} "
f"graph seed={result['graph_seed']} "
f"({result['G'].number_of_edges()} edges, "
f"connectivity {result['connectivity']}, min degree {result['min_degree']})",
f"medial graph M(G): {result['M'].number_of_nodes()} vertices",
f"level source: vertex {result['source']}",
f"recognised tires (depth, component): {sorted(res)}",
f"skipped treads: {result['skipped']}",
]
for key in sorted(res):
d, comp = key
g = res[key]["g"]
ncuts = len(res[key]["cuts"])
lines.append(
f" tread {d}.{comp}: |A(T)|={g.n} word={g.tooth_word} "
f"bites={sorted(g.bites)} entry=e{res[key]['entry_edge']} "
f"start_depth={res[key]['start_depth']} cuts={ncuts}")
lines.append(
f"final cut graph: {H.number_of_nodes()} vertices, "
f"{H.number_of_edges()} edges, "
f"{len(result['cap_cuts']) + sum(len(r['cuts']) for r in res.values())} cuts total")
if result["warnings"]:
lines.append("warnings: " + "; ".join(result["warnings"]))
return "\n".join(lines)
def main() -> None:
parser = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("-n", type=int, default=12, help="number of vertices")
parser.add_argument("--seed", type=int, default=0)
parser.add_argument("--flips", type=int, default=400,
help="number of random diagonal flips when building G")
parser.add_argument("--min-connectivity", type=int, default=5,
help="reject random graphs below this node connectivity")
parser.add_argument("--min-degree", type=int, default=None,
help="compatibility alias; also raises min-connectivity")
parser.add_argument("--attempts", type=int, default=1000,
help="number of consecutive seeds to try for sampling")
parser.add_argument("--json", metavar="PATH",
help="write the full result as JSON to PATH")
args = parser.parse_args()
result = run_experiment(n=args.n, seed=args.seed, flips=args.flips,
min_connectivity=args.min_connectivity,
min_degree=args.min_degree,
attempts=args.attempts)
print(summary(result))
if args.json:
with open(args.json, "w") as fh:
json.dump(to_json(result), fh, indent=2)
print(f"wrote {args.json}")
if __name__ == "__main__":
main()
Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

-1
View File
@@ -1 +0,0 @@
"""Reusable medial tire cut helpers."""
@@ -1,431 +0,0 @@
"""Walk-depth labelling and cut of a full medial tire graph.
Implements the procedure of Definition 2.1 ("Walk-depth labelling and cut") of
the *Medial Tire Cuts* paper:
1. Pick an arbitrary up tooth, the entry tooth; it has walk depth d.
2. Traverse all teeth bounding the inner face incident to the entry tooth
clockwise until reaching the entry tooth, incrementing the walk depth by 1
for each tooth traversed.
3. On reaching the last tooth in the face, perform a cut by duplicating the
annular vertex at which the traversal closes (the annular vertex shared by
the last tooth and the closing tooth).
4. Find the tooth t of highest walk depth that is a member of a bite.
5. If t is incident to a face F with unlabelled teeth, traverse the teeth of F
starting from t in the direction of the unlabelled tooth incident to t
(sharing an annular vertex), incrementing the walk depth as you go.
6. Repeat steps 3-5 until all teeth are labelled.
7. Cut the apex of every up tooth, except entry teeth and except any apex
vertex that is shared by two up teeth.
The full medial tire graph model (annular cycle A(T), up/down teeth, bites, the
auxiliary plane graph B(T) and its inner faces) is the one from the companion
``full_medial_tire_generator.py`` of the medial tire decompositions paper, which
we import.
Teeth are identified with the annular edges that carry them: edge i sits on the
annular vertices a_i and a_{(i+1) mod n} and carries exactly one tooth. A bite
(i, j) carries two teeth, one on edge i and one on edge j, that share the bite
apex p. The inner non-tooth faces of B(T) are the root face (written ``None``)
and one inner-gap face per bite.
"""
from __future__ import annotations
import argparse
import math
import os
import sys
from collections import Counter
from collections.abc import Mapping
# Import the full medial tire model from the companion paper's lib directory.
_GEN_DIR = os.path.normpath(os.path.join(
os.path.dirname(__file__), "..", "..",
"medial_tire_decompositions_of_plane_triangulations", "lib",
))
sys.path.insert(0, _GEN_DIR)
from full_medial_tire_generator import ( # noqa: E402
FullMedialTireGraph,
has_incident_bite,
innermost_bite,
satisfies_bite_face_condition,
)
Face = "tuple[int, int] | None" # a bite (i, j), or None for the root face
# ---------------------------------------------------------------------------
# Face structure of B(T).
# ---------------------------------------------------------------------------
def parent_face(graph: FullMedialTireGraph, bite: tuple[int, int]) -> Face:
"""The face directly enclosing ``bite``: the minimal-span bite strictly
containing it, or the root face ``None``."""
i, j = bite
enclosing = [b for b in graph.bites if b[0] < i and b[1] > j]
if not enclosing:
return None
return min(enclosing, key=lambda b: b[1] - b[0])
def door_bite(graph: FullMedialTireGraph, edge: int) -> tuple[int, int] | None:
"""The bite that ``edge`` is a door of (i.e. a bite edge), or None."""
for b in graph.bites:
if edge in b:
return b
return None
def faces_bordered(graph: FullMedialTireGraph, edge: int) -> list[Face]:
"""The inner non-tooth faces whose boundary the tooth on ``edge`` lies on.
A bite door borders two faces (its bite's gap and that bite's parent); any
other tooth borders the single face directly containing its edge.
"""
bite = door_bite(graph, edge)
if bite is not None:
return [bite, parent_face(graph, bite)]
return [innermost_bite(edge, graph.bites)]
def face_boundary(graph: FullMedialTireGraph, face: Face) -> list[int]:
"""The teeth (annular edges) bounding ``face``, in clockwise cyclic order.
Clockwise is increasing edge index. For the root face the boundary is read
around the whole cycle; for a bite gap (i, j) it is read along the arc
i, i+1, ..., j and closes through the bite apex. Edges enclosed by a child
bite are skipped (they belong to the child's gap face).
"""
n = graph.n
arc = range(n) if face is None else range(face[0], face[1] + 1)
return [k for k in arc if face in faces_bordered(graph, k)]
def all_faces(graph: FullMedialTireGraph) -> list[Face]:
return [None] + sorted(graph.bites)
def shared_annular_vertex(graph: FullMedialTireGraph, e1: int, e2: int) -> int | None:
"""The annular vertex a_k shared by edges ``e1`` and ``e2``, or None."""
n = graph.n
common = {e1, (e1 + 1) % n} & {e2, (e2 + 1) % n}
return next(iter(common)) if common else None
# ---------------------------------------------------------------------------
# The walk-depth labelling and cut.
# ---------------------------------------------------------------------------
class Cut:
"""A cut performed when a face traversal closes: the duplicated annular
vertex, together with the last labelled tooth and the closing tooth that
share it, and the face being closed."""
__slots__ = ("vertex", "last_tooth", "closing_tooth", "face", "order")
def __init__(self, vertex, last_tooth, closing_tooth, face, order):
self.vertex = vertex
self.last_tooth = last_tooth
self.closing_tooth = closing_tooth
self.face = face
self.order = order
def __repr__(self):
f = "root" if self.face is None else f"bite{self.face}"
return (f"Cut(order={self.order}, a{self.vertex}, "
f"last=e{self.last_tooth}, closing=e{self.closing_tooth}, face={f})")
def label_and_cut(graph: FullMedialTireGraph, entry_edge: int,
start_depth: int = 0) -> tuple[dict[int, int], list[Cut]]:
"""Run the procedure starting from up tooth ``entry_edge``.
Returns ``(depth, cuts)`` where ``depth`` maps each annular edge (tooth) to
its walk depth, and ``cuts`` is the list of cuts in the order performed.
"""
if graph.tooth_word[entry_edge] != "U":
raise ValueError(f"entry edge {entry_edge} is not an up tooth")
depth: dict[int, int] = {}
cuts: list[Cut] = []
counter = start_depth
def traverse(face: Face, start_edge: int, is_entry: bool) -> None:
nonlocal counter
boundary = face_boundary(graph, face)
m = len(boundary)
pos = boundary.index(start_edge)
if is_entry:
depth[start_edge] = counter
counter += 1
direction = +1
else:
# head toward the unlabelled tooth incident to the door t
direction = +1 if boundary[(pos + 1) % m] not in depth else -1
last_new = start_edge
i = pos
while True:
i = (i + direction) % m
edge = boundary[i]
if edge in depth: # the closing tooth
cuts.append(Cut(
vertex=shared_annular_vertex(graph, last_new, edge),
last_tooth=last_new, closing_tooth=edge,
face=face, order=len(cuts),
))
return
depth[edge] = counter
counter += 1
last_new = edge
# Steps 1-3: the entry face.
traverse(innermost_bite(entry_edge, graph.bites), entry_edge, is_entry=True)
# Steps 4-6: descend (or ascend) through bites, deepest first. The root
# face is ``None``, so we use a distinct sentinel for "no unlabelled face".
_MISSING = object()
while len(depth) < graph.n:
labelled_bite_teeth = sorted(
(e for e in depth if door_bite(graph, e) is not None),
key=lambda e: depth[e], reverse=True,
)
for t in labelled_bite_teeth:
target = next((F for F in faces_bordered(graph, t)
if any(e not in depth for e in face_boundary(graph, F))),
_MISSING)
if target is not _MISSING:
traverse(target, t, is_entry=False)
break
else:
break # no progress possible
return depth, cuts
def up_apex_cuts(graph: FullMedialTireGraph, entry_edge: int,
bij: Mapping[str, object] | None = None,
shared_apexes: set[object] | None = None) -> dict[int, object]:
"""Up-tooth apex cuts prescribed after the walk-depth traversal.
The returned dict maps each cut up-tooth edge to the apex vertex to
duplicate. Entry teeth are not cut. If ``bij`` is supplied, it maps the
model vertex names (``u{i}``) into the ambient medial graph; this lets a
real tread suppress cuts at a vertex that is the shared apex of two up
teeth. Without ``bij`` the model vertex names are used directly.
"""
apex_by_edge = {
i: (bij[f"u{i}"] if bij is not None else graph.apex_of_edge(i))
for i in graph.up_edges
}
shared_apexes = shared_apexes or set()
multiplicity = Counter(apex_by_edge.values())
return {
i: apex
for i, apex in apex_by_edge.items()
if i != entry_edge and multiplicity[apex] == 1 and apex not in shared_apexes
}
# ---------------------------------------------------------------------------
# TikZ rendering.
# ---------------------------------------------------------------------------
def _coords(graph: FullMedialTireGraph,
r_ann=1.0, r_up=1.46, r_down=0.60) -> dict[str, tuple[float, float]]:
n = graph.n
def ang(k): # a_0 at the top, increasing k clockwise
return math.radians(90.0 - k * 360.0 / n)
def edge_mid_dir(i): # angle of the bisector of edge i's two endpoints
a0, a1 = ang(i), ang((i + 1) % n)
return math.atan2(math.sin(a0) + math.sin(a1), math.cos(a0) + math.cos(a1))
pos = {f"a{k}": (r_ann * math.cos(ang(k)), r_ann * math.sin(ang(k)))
for k in range(n)}
for i in graph.up_edges:
a = edge_mid_dir(i)
pos[f"u{i}"] = (r_up * math.cos(a), r_up * math.sin(a))
for i in graph.singleton_down_edges:
a = edge_mid_dir(i)
pos[f"d{i}"] = (r_down * math.cos(a), r_down * math.sin(a))
for (i, j) in graph.bites:
pts = [pos[f"a{i}"], pos[f"a{(i + 1) % n}"],
pos[f"a{j}"], pos[f"a{(j + 1) % n}"]]
cx = sum(p[0] for p in pts) / 4.0
cy = sum(p[1] for p in pts) / 4.0
pos[f"p{i}_{j}"] = (0.9 * cx, 0.9 * cy)
return pos
def _edge_midpoint(pos, graph, edge):
n = graph.n
a, b = pos[f"a{edge}"], pos[f"a{(edge + 1) % n}"]
return (0.5 * (a[0] + b[0]), 0.5 * (a[1] + b[1]))
def to_tikz(graph: FullMedialTireGraph,
depth: dict[int, int] | None = None,
cuts: list[Cut] | None = None,
entry_edge: int | None = None,
scale: float = 2.2) -> str:
"""A standalone ``tikzpicture`` for ``graph``; if ``depth`` is given, draw
the walk-depth labels and (with ``cuts``) the cut marks."""
pos = _coords(graph)
n = graph.n
L = []
A = L.append
A(f"\\begin{{tikzpicture}}[scale={scale},")
A(" ann/.style={circle, fill=black, inner sep=1.0pt},")
A(" upv/.style={circle, draw=blue!70!black, fill=blue!12, inner sep=1.4pt},")
A(" downv/.style={circle, draw=red!70!black, fill=red!12, inner sep=1.4pt},")
A(" bitev/.style={circle, draw=red!70!black, fill=red!32, inner sep=1.7pt},")
A(" cyc/.style={black, line width=1.0pt},")
A(" tth/.style={black!55, line width=0.4pt},")
A(" lbl/.style={font=\\scriptsize},")
A(" dlbl/.style={font=\\scriptsize\\bfseries, text=black},")
A(" cut/.style={red!80!black, line width=1.3pt},")
A(" cutlbl/.style={font=\\tiny, text=red!75!black}]")
def pt(name):
x, y = pos[name]
return f"({x:.3f},{y:.3f})"
# annular cycle
cyc = "--".join(pt(f"a{k}") for k in range(n)) + "--cycle"
A(f"\\draw[cyc] {cyc};")
# spokes
for i in graph.up_edges:
A(f"\\draw[tth] {pt(f'u{i}')}--{pt(f'a{i}')} {pt(f'u{i}')}--{pt(f'a{(i+1)%n}')};")
for i in graph.singleton_down_edges:
A(f"\\draw[tth] {pt(f'd{i}')}--{pt(f'a{i}')} {pt(f'd{i}')}--{pt(f'a{(i+1)%n}')};")
for (i, j) in graph.bites:
apex = f"p{i}_{j}"
for e in (i, j):
A(f"\\draw[tth] {pt(apex)}--{pt(f'a{e}')} {pt(apex)}--{pt(f'a{(e+1)%n}')};")
# vertices
for k in range(n):
A(f"\\node[ann] at {pt(f'a{k}')} {{}};")
for i in graph.up_edges:
A(f"\\node[upv] at {pt(f'u{i}')} {{}};")
for i in graph.singleton_down_edges:
A(f"\\node[downv] at {pt(f'd{i}')} {{}};")
for (i, j) in sorted(graph.bites):
A(f"\\node[bitev] at {pt(f'p{i}_{j}')} {{}};")
# walk-depth labels: placed along the spoke from apex toward the edge mid
if depth is not None:
for edge in range(n):
apex = graph.apex_of_edge(edge)
ax, ay = pos[apex]
mx, my = _edge_midpoint(pos, graph, edge)
f = 0.5
lx, ly = ax + f * (mx - ax), ay + f * (my - ay)
A(f"\\node[dlbl] at ({lx:.3f},{ly:.3f}) {{{depth[edge]}}};")
# cut marks: a short red slit across the duplicated annular vertex
if cuts:
for c in cuts:
if c.vertex is None:
continue
vx, vy = pos[f"a{c.vertex}"]
rad = math.atan2(vy, vx)
dx, dy = 0.16 * math.cos(rad), 0.16 * math.sin(rad)
A(f"\\draw[cut] ({vx-dx:.3f},{vy-dy:.3f})--({vx+dx:.3f},{vy+dy:.3f});")
lx, ly = vx + 0.30 * math.cos(rad), vy + 0.30 * math.sin(rad)
A(f"\\node[cutlbl] at ({lx:.3f},{ly:.3f}) {{cut {c.order+1}}};")
# up-tooth apex cuts: tangential slits, excluding the entry tooth and any
# up apex shared by two up teeth.
if entry_edge is not None:
for i in up_apex_cuts(graph, entry_edge):
vx, vy = pos[f"u{i}"]
rad = math.atan2(vy, vx)
tx, ty = -math.sin(rad), math.cos(rad)
A(f"\\draw[cut] ({vx-0.12*tx:.3f},{vy-0.12*ty:.3f})--"
f"({vx+0.12*tx:.3f},{vy+0.12*ty:.3f});")
if entry_edge is not None:
ex, ey = pos[graph.apex_of_edge(entry_edge)]
rad = math.atan2(ey, ex)
tx, ty = ex + 0.34 * math.cos(rad), ey + 0.34 * math.sin(rad)
A(f"\\node[lbl, text=blue!60!black] at ({tx:.3f},{ty:.3f}) {{entry}};")
A("\\end{tikzpicture}")
return "\n".join(L)
# ---------------------------------------------------------------------------
# Worked example and CLI.
# ---------------------------------------------------------------------------
def worked_example() -> FullMedialTireGraph:
"""A clean 8-tooth piece: one bite (0,4), three down singletons 1,2,3 in its
gap, three up teeth 5,6,7 in the root face."""
return FullMedialTireGraph(n=8, tooth_word="DDDDDUUU", bites=frozenset({(0, 4)}))
def _check(graph: FullMedialTireGraph) -> None:
assert not has_incident_bite(graph.bites, graph.n), "bite uses incident edges"
assert satisfies_bite_face_condition(graph.tooth_word, graph.bites), \
"violates the bite-face condition"
assert graph.tooth_word.count("U") >= 3, "fewer than three up teeth"
def _describe(graph, depth, cuts, entry_edge) -> str:
lines = ["edge type walk-depth"]
for e in range(graph.n):
t = graph.tooth_word[e]
kind = {"U": "up"}.get(t, "down")
if door_bite(graph, e) is not None:
kind = "bite"
lines.append(f" e{e} {kind:<5} {depth[e]}")
lines.append("cuts (in order):")
for c in cuts:
f = "root" if c.face is None else f"bite{c.face}"
lines.append(f" cut {c.order+1}: duplicate a{c.vertex} "
f"(closing tooth e{c.closing_tooth} of {f})")
apex_cuts = up_apex_cuts(graph, entry_edge)
if apex_cuts:
lines.append("up-apex cuts:")
for edge, apex in apex_cuts.items():
lines.append(f" duplicate {apex} for up tooth e{edge}")
return "\n".join(lines)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--entry", default="u5",
help="entry up tooth, as an edge index or apex name like u5")
parser.add_argument("--start-depth", type=int, default=0)
parser.add_argument("--tikz", choices=["plain", "labelled", "both"],
help="emit TikZ for the worked example")
args = parser.parse_args()
entry = args.entry
edge = int(entry[1:]) if isinstance(entry, str) and entry.startswith("u") else int(entry)
graph = worked_example()
_check(graph)
depth, cuts = label_and_cut(graph, edge, start_depth=args.start_depth)
if args.tikz == "plain":
print(to_tikz(graph))
elif args.tikz == "labelled":
print(to_tikz(graph, depth=depth, cuts=cuts, entry_edge=edge))
elif args.tikz == "both":
print("% --- plain ---")
print(to_tikz(graph))
print("% --- labelled + cut ---")
print(to_tikz(graph, depth=depth, cuts=cuts, entry_edge=edge))
else:
print(f"worked example: n={graph.n} word={graph.tooth_word} "
f"bites={sorted(graph.bites)} entry=e{edge}")
print(_describe(graph, depth, cuts, edge))
if __name__ == "__main__":
main()
-29
View File
@@ -1,29 +0,0 @@
\relax
\citation{bauerfeld-medial-tire}
\citation{bauerfeld-medial-tire}
\citation{bauerfeld-medial-tire}
\@writefile{toc}{\contentsline {section}{\tocsection {}{1}{Introduction}}{1}{}\protected@file@percent }
\@writefile{toc}{\contentsline {section}{\tocsection {}{2}{Cutting a full medial tire graph}}{1}{}\protected@file@percent }
\newlabel{def:walk-depth-cut}{{2.1}{1}}
\citation{bauerfeld-medial-tire}
\citation{bauerfeld-medial-tire}
\newlabel{rem:closing-tooth}{{2.2}{2}}
\newlabel{ex:worked-cut}{{2.3}{2}}
\@writefile{toc}{\contentsline {section}{\tocsection {}{3}{Chaining across the tire tree}}{2}{}\protected@file@percent }
\citation{bauerfeld-medial-tire}
\@writefile{lof}{\contentsline {figure}{\numberline {1}{\ignorespaces A full medial tire graph (left) and its walk-depth labelling and cut (right), from Example\nonbreakingspace 2.3\hbox {}. Black vertices are the annular medial vertices of the cycle $A(T)$; blue vertices are up-tooth apexes, red vertices are down-tooth apexes, and the larger red vertex is the shared apex of the bite on annular edges $0$ and $4$. On the right, each tooth carries its walk depth, and the two red slits mark the cuts: \emph {cut\nonbreakingspace 1} duplicates $a_5$ as the root-face traversal closes, and \emph {cut\nonbreakingspace 2} duplicates $a_1$ as the bite's inner-gap face closes. After the cuts the only bounded faces are the eight teeth.}}{3}{}\protected@file@percent }
\newlabel{fig:worked-cut}{{1}{3}}
\newlabel{rem:chaining-candidates}{{3.1}{3}}
\newlabel{ex:real-cut}{{3.2}{4}}
\@writefile{lof}{\contentsline {figure}{\numberline {2}{\ignorespaces The recognised tread $T_2$ of the medial tire decomposition of a random maximal planar graph on $20$ vertices (Example\nonbreakingspace 3.2\hbox {}), with its walk-depth labelling and cut. Black vertices are the annular medial vertices of $A(T)$; blue vertices are up-tooth apexes and red vertices down-tooth apexes, the larger red vertex being the shared apex of the bite on annular edges $2$ and $5$. Each tooth carries its walk depth; the red slits are the two cuts.}}{4}{}\protected@file@percent }
\newlabel{fig:real-cut}{{2}{4}}
\bibcite{bauerfeld-medial-tire}{1}
\newlabel{tocindent-1}{0pt}
\newlabel{tocindent0}{12.7778pt}
\newlabel{tocindent1}{17.77782pt}
\newlabel{tocindent2}{0pt}
\newlabel{tocindent3}{0pt}
\@writefile{lof}{\contentsline {figure}{\numberline {3}{\ignorespaces The source graph $G$ and the whole medial graph $M(G)$ of the minimum-degree-$5$ maximal planar graph on $20$ vertices generated by \texttt {plantri -m5} at seed $59$. The source vertex $5$ is highlighted in the top panel. In the bottom panel, each medial vertex is placed at the midpoint of its corresponding source edge and labelled by that edge. Black vertices come from source edges between consecutive levels; coloured vertices come from source edges within a single level of the chain. The red-highlighted vertices, walk-depth labels, and seven red slits are the computed source-cap cut and full-medial-tire labelling cuts for the recognised treads $T_1$ and $T_2$. Drawn by \texttt {experiments/draw\_medial\_tire\_cut.py} with \texttt {--whole --min-degree 5}.}}{5}{}\protected@file@percent }
\newlabel{fig:whole-medial}{{3}{5}}
\@writefile{toc}{\contentsline {section}{\tocsection {}{}{References}}{5}{}\protected@file@percent }
\gdef \@abspage@last{5}
Binary file not shown.
-450
View File
@@ -1,450 +0,0 @@
%% filename: amsart-template.tex
%% American Mathematical Society
%% AMS-LaTeX v.2 template for use with amsart
%% ====================================================================
\documentclass{amsart}
\usepackage{amssymb}
\usepackage{graphicx}
\usepackage{tikz}
\usetikzlibrary{backgrounds}
\newtheorem{theorem}{Theorem}[section]
\newtheorem{lemma}[theorem]{Lemma}
\newtheorem{corollary}[theorem]{Corollary}
\newtheorem{proposition}[theorem]{Proposition}
\newtheorem{conjecture}[theorem]{Conjecture}
\theoremstyle{definition}
\newtheorem{definition}[theorem]{Definition}
\newtheorem{example}[theorem]{Example}
\newtheorem{xca}[theorem]{Exercise}
\theoremstyle{remark}
\newtheorem{remark}[theorem]{Remark}
\numberwithin{equation}{section}
\begin{document}
\title{Medial Tire Cuts}
% author one information
\author{Eric Bauerfeld}
\address{}
\curraddr{}
\email{}
\thanks{}
\subjclass[2010]{Primary }
\keywords{plane graph, triangulation, medial graph, tire graph, Tait coloring, Four Colour Theorem}
\date{}
\dedicatory{}
\begin{abstract}
Starting from the medial tire decomposition of a plane triangulation, we
study the cuts that medial tires make in the full medial graph. We will
show how to use medial tires to decompose the medial graph into a tree of
three faces.
\end{abstract}
\maketitle
\section{Introduction}
This paper builds on the medial tire decomposition
of~\cite{bauerfeld-medial-tire}. For a plane triangulation $G$ with
fixed embedding we use freely the terminology and notation introduced
there: the full medial graph $M(G)$, its decomposition into full medial
tire graphs $\mathsf{M}(T)$ indexed by the treads $T$ of the tire tree
$\mathcal{T}(G,S)$ at a level source $S$, the annular medial cycle
$A(T)$, and the boundary medial vertex sets.
We will show how to use medial tires to decompose the medial graph into
a tree of three faces.
\section{Cutting a full medial tire graph}
We first describe a procedure that simultaneously \emph{labels} and
\emph{cuts} a single full medial tire graph $\mathsf{M}(T)$ so that,
after the cuts, the only faces are the outer face and $3$-faces
(triangles)---the teeth of~\cite{bauerfeld-medial-tire}. The labelling
assigns to each tooth an integer \emph{walk depth}; the cuts break the
cyclic adjacencies of the teeth so that what remains is a tree of
$3$-faces.
By a \emph{cut} we mean the duplication of a single vertex of
$\mathsf{M}(T)$: the vertex is split into two copies and the embedding is
slit open along it (a planar unzip), separating the faces that meet only
at that vertex. A cut therefore reduces the number of bounded faces that
are not teeth.
Throughout we use the teeth, up and down teeth, apexes, bites, the
annular medial cycle $A(T)$, and the auxiliary plane graph $B(T)$
of~\cite{bauerfeld-medial-tire}. Each tooth is a $3$-face of
$\mathsf{M}(T)$, and the inner faces of $B(T)$ (the root face and the
bite inner-gap faces) are the larger faces to be cut into teeth.
\begin{definition}[Walk-depth labelling and cut]
\label{def:walk-depth-cut}
Let $\mathsf{M}(T)$ be a full medial tire graph. Assign walk depths and
cuts as follows.
\begin{enumerate}
\item Pick an arbitrary up tooth, the \emph{entry tooth}. It has walk
depth $d$.
\item Traverse all the teeth that bound the inner face incident to the
entry tooth clockwise until we reach the entry tooth, incrementing the
walk depth by $1$ for each tooth traversed. (The \emph{inner face
incident to the entry tooth} is the inner face of $B(T)$ whose boundary
contains the annular edge of $A(T)$ carrying the entry tooth.)
\item When you reach the last tooth in the face, perform a \emph{cut}
by duplicating the annular vertex at which the traversal closes---the
annular vertex of $A(T)$ shared by the last tooth and the entry tooth.
\item Find the tooth $t$ with the highest walk depth which is a member
of a bite.
\item If $t$ is incident to a face $F$ with unlabelled teeth, traverse
the teeth in $F$ starting from $t$ in the direction of the tooth
incident to $t$ which is unlabelled, and increment the walk depth by
$1$ as you travel. (Here a tooth is \emph{incident to $t$} when it
shares an annular vertex of $A(T)$ with $t$.)
\item Repeat steps (3)--(5) until all teeth have been labelled.
\item Finally, perform an apex cut at every up tooth except an entry
tooth. If the same medial vertex is the apex of two up teeth, do not
cut that shared apex vertex.
\end{enumerate}
\end{definition}
\begin{remark}[Entry and shared up-apex exceptions]
\label{rem:up-apex-cut-exceptions}
For a single full medial tire graph there is one entry tooth. In a
chained tire decomposition each tread has its own entry tooth, inherited
from the parent side or chosen at the root. These entry triangles are
left uncut. Shared up-apex vertices are also left uncut: the intended
cut set contains the apexes of singleton up teeth only.
\end{remark}
\begin{remark}[Closing tooth of a descended face]
\label{rem:closing-tooth}
For the entry face the traversal of step (2) closes by returning to the
entry tooth, so the cut of step (3) duplicates the annular vertex shared
by the last tooth and the entry tooth. For a face $F$ entered in step
(5), the traversal instead closes upon reaching an already-labelled
tooth: the other tooth of the bite through which $F$ was entered. In
both cases the cut of step (3) duplicates the annular vertex shared by
the last newly labelled tooth and this \emph{closing tooth}. Since both
teeth of a bite are labelled while traversing its parent face, every
descended face closes on such a tooth.
\end{remark}
\begin{example}[A worked walk-depth labelling and cut]
\label{ex:worked-cut}
Figure~\ref{fig:worked-cut} shows a full medial tire graph with annular
cycle of length $8$, generated by the full medial tire generator
of~\cite{bauerfeld-medial-tire}. Its eight teeth are: three up teeth on
the annular edges $5,6,7$ in the root face; one bite pairing the annular
edges $0$ and $4$; and three singleton down teeth on the annular edges
$1,2,3$ lying in that bite's inner-gap face.
Take the up tooth on edge $5$ as the entry tooth, with walk depth $0$.
Its inner face is the root face, bounded by the teeth on edges
$5,6,7,0,4$ in clockwise order. Step (2) labels them
\[
5\mapsto 0,\quad 6\mapsto 1,\quad 7\mapsto 2,\quad
0\mapsto 3,\quad 4\mapsto 4,
\]
and step (3) cuts by duplicating the annular vertex $a_5$ shared by the
last tooth (edge $4$) and the entry tooth (edge $5$). The highest-depth
bite tooth is now the one on edge $4$ (walk depth $4$); it is incident to
the still-unlabelled inner-gap face of the bite $(0,4)$. Entering that
face from edge $4$ toward its unlabelled neighbour, step (5) labels the
three down teeth
\[
3\mapsto 5,\quad 2\mapsto 6,\quad 1\mapsto 7,
\]
and closes on the already-labelled bite tooth of edge $0$, so step (3)
cuts by duplicating the annular vertex $a_1$
(Remark~\ref{rem:closing-tooth}). All eight teeth are now labelled, and
the closing cuts are followed by apex cuts at the non-entry up teeth on
edges $6$ and $7$. The labelling and cuts are produced by the script
\texttt{lib/medial\_tire\_cut\_labelling.py}.
\end{example}
\begin{figure}[h]
\centering
\begin{tikzpicture}[scale=1.6,
ann/.style={circle, fill=black, inner sep=1.0pt},
upv/.style={circle, draw=blue!70!black, fill=blue!12, inner sep=1.4pt},
downv/.style={circle, draw=red!70!black, fill=red!12, inner sep=1.4pt},
bitev/.style={circle, draw=red!70!black, fill=red!32, inner sep=1.7pt},
cyc/.style={black, line width=1.0pt},
tth/.style={black!55, line width=0.4pt},
lbl/.style={font=\scriptsize},
dlbl/.style={font=\scriptsize\bfseries, text=black},
cut/.style={red!80!black, line width=1.3pt},
cutlbl/.style={font=\tiny, text=red!75!black}]
\draw[cyc] (0.000,1.000)--(0.707,0.707)--(1.000,0.000)--(0.707,-0.707)--(0.000,-1.000)--(-0.707,-0.707)--(-1.000,-0.000)--(-0.707,0.707)--cycle;
\draw[tth] (-1.349,-0.559)--(-0.707,-0.707) (-1.349,-0.559)--(-1.000,-0.000);
\draw[tth] (-1.349,0.559)--(-1.000,-0.000) (-1.349,0.559)--(-0.707,0.707);
\draw[tth] (-0.559,1.349)--(-0.707,0.707) (-0.559,1.349)--(0.000,1.000);
\draw[tth] (0.554,0.230)--(0.707,0.707) (0.554,0.230)--(1.000,0.000);
\draw[tth] (0.554,-0.230)--(1.000,0.000) (0.554,-0.230)--(0.707,-0.707);
\draw[tth] (0.230,-0.554)--(0.707,-0.707) (0.230,-0.554)--(0.000,-1.000);
\draw[tth] (0.000,-0.000)--(0.000,1.000) (0.000,-0.000)--(0.707,0.707);
\draw[tth] (0.000,-0.000)--(0.000,-1.000) (0.000,-0.000)--(-0.707,-0.707);
\node[ann] at (0.000,1.000) {};
\node[ann] at (0.707,0.707) {};
\node[ann] at (1.000,0.000) {};
\node[ann] at (0.707,-0.707) {};
\node[ann] at (0.000,-1.000) {};
\node[ann] at (-0.707,-0.707) {};
\node[ann] at (-1.000,-0.000) {};
\node[ann] at (-0.707,0.707) {};
\node[upv] at (-1.349,-0.559) {};
\node[upv] at (-1.349,0.559) {};
\node[upv] at (-0.559,1.349) {};
\node[downv] at (0.554,0.230) {};
\node[downv] at (0.554,-0.230) {};
\node[downv] at (0.230,-0.554) {};
\node[bitev] at (0.000,-0.000) {};
\end{tikzpicture}
\qquad
\begin{tikzpicture}[scale=1.6,
ann/.style={circle, fill=black, inner sep=1.0pt},
upv/.style={circle, draw=blue!70!black, fill=blue!12, inner sep=1.4pt},
downv/.style={circle, draw=red!70!black, fill=red!12, inner sep=1.4pt},
bitev/.style={circle, draw=red!70!black, fill=red!32, inner sep=1.7pt},
cyc/.style={black, line width=1.0pt},
tth/.style={black!55, line width=0.4pt},
lbl/.style={font=\scriptsize},
dlbl/.style={font=\scriptsize\bfseries, text=black},
cut/.style={red!80!black, line width=1.3pt},
cutlbl/.style={font=\tiny, text=red!75!black}]
\draw[cyc] (0.000,1.000)--(0.707,0.707)--(1.000,0.000)--(0.707,-0.707)--(0.000,-1.000)--(-0.707,-0.707)--(-1.000,-0.000)--(-0.707,0.707)--cycle;
\draw[tth] (-1.349,-0.559)--(-0.707,-0.707) (-1.349,-0.559)--(-1.000,-0.000);
\draw[tth] (-1.349,0.559)--(-1.000,-0.000) (-1.349,0.559)--(-0.707,0.707);
\draw[tth] (-0.559,1.349)--(-0.707,0.707) (-0.559,1.349)--(0.000,1.000);
\draw[tth] (0.554,0.230)--(0.707,0.707) (0.554,0.230)--(1.000,0.000);
\draw[tth] (0.554,-0.230)--(1.000,0.000) (0.554,-0.230)--(0.707,-0.707);
\draw[tth] (0.230,-0.554)--(0.707,-0.707) (0.230,-0.554)--(0.000,-1.000);
\draw[tth] (0.000,-0.000)--(0.000,1.000) (0.000,-0.000)--(0.707,0.707);
\draw[tth] (0.000,-0.000)--(0.000,-1.000) (0.000,-0.000)--(-0.707,-0.707);
\node[ann] at (0.000,1.000) {};
\node[ann] at (0.707,0.707) {};
\node[ann] at (1.000,0.000) {};
\node[ann] at (0.707,-0.707) {};
\node[ann] at (0.000,-1.000) {};
\node[ann] at (-0.707,-0.707) {};
\node[ann] at (-1.000,-0.000) {};
\node[ann] at (-0.707,0.707) {};
\node[upv] at (-1.349,-0.559) {};
\node[upv] at (-1.349,0.559) {};
\node[upv] at (-0.559,1.349) {};
\node[downv] at (0.554,0.230) {};
\node[downv] at (0.554,-0.230) {};
\node[downv] at (0.230,-0.554) {};
\node[bitev] at (0.000,-0.000) {};
\node[dlbl] at (0.177,0.427) {3};
\node[dlbl] at (0.704,0.292) {7};
\node[dlbl] at (0.704,-0.292) {6};
\node[dlbl] at (0.292,-0.704) {5};
\node[dlbl] at (-0.177,-0.427) {4};
\node[dlbl] at (-1.101,-0.456) {0};
\node[dlbl] at (-1.101,0.456) {1};
\node[dlbl] at (-0.456,1.101) {2};
\draw[cut] (-0.594,-0.594)--(-0.820,-0.820);
\node[cutlbl] at (-0.919,-0.919) {cut 1};
\draw[cut] (0.594,0.594)--(0.820,0.820);
\node[cutlbl] at (0.919,0.919) {cut 2};
\node[lbl, text=blue!60!black] at (-1.663,-0.689) {entry};
\end{tikzpicture}
\caption{A full medial tire graph (left) and its walk-depth labelling and
cut (right), from Example~\ref{ex:worked-cut}. Black vertices are the
annular medial vertices of the cycle $A(T)$; blue vertices are up-tooth
apexes, red vertices are down-tooth apexes, and the larger red vertex is
the shared apex of the bite on annular edges $0$ and $4$. On the right,
each tooth carries its walk depth, and the two red slits mark the cuts:
\emph{cut~1} duplicates $a_5$ as the root-face traversal closes, and
\emph{cut~2} duplicates $a_1$ as the bite's inner-gap face closes. After
the cuts the only bounded faces are the eight teeth.}
\label{fig:worked-cut}
\end{figure}
\section{Chaining across the tire tree}
Definition~\ref{def:walk-depth-cut} labels and cuts a single full medial
tire graph. We extend it to the whole medial graph $M(G)$ through the
medial tire decomposition of~\cite{bauerfeld-medial-tire}: the tire tree
decomposes $M(G)$ into full medial tire graphs $\mathsf{M}(T)$, one per
tread $T$, glued along their boundary medial vertices. A parent tread's
inner level cycle is a child tread's outer level cycle, and the boundary
medial vertices on that shared cycle belong to both treads.
The key incidence is this. A \emph{boundary} (singleton) down tooth of a
parent tread and the up tooth of the child tread glued to it across the
shared level cycle have the \emph{same apex}: both apexes are the same
medial vertex of $M(G)$, namely the medial vertex of an edge with both
endpoints on the shared level cycle. We use this to carry the walk depth
from a parent into its children.
We label tread by tread, outward from the root:
\begin{itemize}
\item a tread with no parent in the decomposition---in particular the
innermost recognised tread---is treated as a \emph{root} and entered at
an arbitrary up tooth with walk depth $0$;
\item a child tread is entered at the up tooth whose apex is the parent's
boundary down tooth of lowest walk depth; that entry up tooth's walk depth
is one more than that down tooth's, and the walk then increments locally
within the child as in Definition~\ref{def:walk-depth-cut}.
\end{itemize}
The source cap contributes one additional cut before the recognised
treads are assembled. If the root tread enters at an up tooth whose apex
is the cap down tooth $xy$, we cut the cap annular vertex corresponding
to the counter-clockwise source edge incident to $xy$. In the example of
Figure~\ref{fig:whole-medial}, the root entry apex is the cap down tooth
$14\!-\!4$, so the cap cut is placed at the medial vertex $14\!-\!5$.
\begin{remark}[Candidate down teeth for chaining]
\label{rem:chaining-candidates}
The down teeth eligible to fix a child's entry are exactly the
\emph{boundary} (singleton) down teeth of the parent: those lying in a
single tread face, whose apex is the shared boundary medial vertex glued to
a child up tooth. A bite's two down teeth are \emph{not} eligible. By the
definition of a bite in~\cite{bauerfeld-medial-tire} its annular edge borders
two tread faces, so a bite tooth is interior to the parent tread and its
apex is a boundary medial vertex of no child. Hence ``the down tooth of
lowest walk depth'' is read among the boundary down teeth only; a bite of
even lower walk depth is skipped.
\end{remark}
Applying every tread's cuts to $M(G)$ assembles the per-tread labellings
and cuts into a single cut graph of $M(G)$ together with a global
walk-depth label map. This pipeline---random maximal planar graph, medial
graph, tire decomposition at a vertex level source, and chained walk-depth
labelling and cut---is carried out by the experiment script
\texttt{experiments/run\_medial\_tire\_cut\_experiment.py}.
\begin{example}[A medial tire cut from a random graph]
\label{ex:real-cut}
Run on a random maximal planar graph on $20$ vertices (seed $72$, level
source vertex $9$), the experiment yields a single recognised tread
$T_2$, drawn in Figure~\ref{fig:real-cut} with the walk-depth labelling
and cut emitted by the graphics companion
\texttt{experiments/draw\_medial\_tire\_cut.py}. Its annular cycle has
length $8$, with up teeth on annular edges $0,3,4$, singleton down teeth
on $1,6,7$, and a bite on the non-incident annular edges $2$ and $5$ (the
central shared apex). Entering at the up tooth on edge $0$ with walk
depth $0$, the root face is labelled in order ($0,1,2$ then $3,4,5$) and
\emph{cut~1} duplicates $a_0$ as it closes; the walk then descends through
the bite into its inner-gap face, labelling the two teeth there ($6,7$),
and \emph{cut~2} duplicates $a_3$ as that face closes. The two cuts leave
only the outer face and the eight teeth as $3$-faces.
\end{example}
\begin{figure}[h]
\centering
\begin{tikzpicture}[scale=1.6,
ann/.style={circle, fill=black, inner sep=1.0pt},
upv/.style={circle, draw=blue!70!black, fill=blue!12, inner sep=1.4pt},
downv/.style={circle, draw=red!70!black, fill=red!12, inner sep=1.4pt},
bitev/.style={circle, draw=red!70!black, fill=red!32, inner sep=1.7pt},
cyc/.style={black, line width=1.0pt},
tth/.style={black!55, line width=0.4pt},
lbl/.style={font=\scriptsize},
dlbl/.style={font=\scriptsize\bfseries, text=black},
cut/.style={red!80!black, line width=1.3pt},
cutlbl/.style={font=\tiny, text=red!75!black}]
\draw[cyc] (0.000,1.000)--(0.707,0.707)--(1.000,0.000)--(0.707,-0.707)--(0.000,-1.000)--(-0.707,-0.707)--(-1.000,-0.000)--(-0.707,0.707)--cycle;
\draw[tth] (0.559,1.349)--(0.000,1.000) (0.559,1.349)--(0.707,0.707);
\draw[tth] (0.559,-1.349)--(0.707,-0.707) (0.559,-1.349)--(0.000,-1.000);
\draw[tth] (-0.559,-1.349)--(0.000,-1.000) (-0.559,-1.349)--(-0.707,-0.707);
\draw[tth] (0.554,0.230)--(0.707,0.707) (0.554,0.230)--(1.000,0.000);
\draw[tth] (-0.554,0.230)--(-1.000,-0.000) (-0.554,0.230)--(-0.707,0.707);
\draw[tth] (-0.230,0.554)--(-0.707,0.707) (-0.230,0.554)--(0.000,1.000);
\draw[tth] (0.000,-0.318)--(1.000,0.000) (0.000,-0.318)--(0.707,-0.707);
\draw[tth] (0.000,-0.318)--(-0.707,-0.707) (0.000,-0.318)--(-1.000,-0.000);
\node[ann] at (0.000,1.000) {};
\node[ann] at (0.707,0.707) {};
\node[ann] at (1.000,0.000) {};
\node[ann] at (0.707,-0.707) {};
\node[ann] at (0.000,-1.000) {};
\node[ann] at (-0.707,-0.707) {};
\node[ann] at (-1.000,-0.000) {};
\node[ann] at (-0.707,0.707) {};
\node[upv] at (0.559,1.349) {};
\node[upv] at (0.559,-1.349) {};
\node[upv] at (-0.559,-1.349) {};
\node[downv] at (0.554,0.230) {};
\node[downv] at (-0.554,0.230) {};
\node[downv] at (-0.230,0.554) {};
\node[bitev] at (0.000,-0.318) {};
\node[dlbl] at (0.456,1.101) {0};
\node[dlbl] at (0.704,0.292) {1};
\node[dlbl] at (0.427,-0.336) {2};
\node[dlbl] at (0.456,-1.101) {7};
\node[dlbl] at (-0.456,-1.101) {6};
\node[dlbl] at (-0.427,-0.336) {3};
\node[dlbl] at (-0.704,0.292) {4};
\node[dlbl] at (-0.292,0.704) {5};
\draw[cut] (0.000,0.840)--(0.000,1.160);
\node[cutlbl] at (0.000,1.300) {cut 1};
\draw[cut] (0.594,-0.594)--(0.820,-0.820);
\node[cutlbl] at (0.919,-0.919) {cut 2};
\node[lbl, text=blue!60!black] at (0.689,1.663) {entry};
\end{tikzpicture}
\caption{The recognised tread $T_2$ of the medial tire decomposition of a
random maximal planar graph on $20$ vertices
(Example~\ref{ex:real-cut}), with its walk-depth labelling and cut. Black
vertices are the annular medial vertices of $A(T)$; blue vertices are
up-tooth apexes and red vertices down-tooth apexes, the larger red vertex
being the shared apex of the bite on annular edges $2$ and $5$. Each
tooth carries its walk depth; the red slits are the two cuts.}
\label{fig:real-cut}
\end{figure}
Figure~\ref{fig:whole-medial} repeats the whole-medial-graph drawing on a
random maximal planar graph on $20$ vertices with minimum degree $5$
(plantri seed $59$, level source vertex $5$). The experiment recognises
two full medial tire treads, $T_1$ and $T_2$, and produces seven cuts:
one source-cap cut and six full-tread cuts. The
top panel shows the source triangulation with its level source
highlighted; the bottom panel draws $M(G)$ on the same straight-line
embedding by placing each medial vertex at the midpoint of its
corresponding source edge. Every medial vertex is labelled by that source
edge. Black vertices correspond to source edges joining consecutive
levels, and coloured vertices correspond to source edges within one level.
The red-highlighted vertices, walk-depth labels, and red slits are the
computed full-medial-tire labelling and cuts.
\begin{figure}[h]
\centering
\input{whole_medial_seed59_min5.tikz}
\caption{The source graph $G$ and the whole medial graph $M(G)$ of the
minimum-degree-$5$ maximal planar graph on $20$ vertices generated by
\texttt{plantri -m5} at seed $59$. The source vertex $5$ is highlighted
in the top panel. In the bottom panel, each medial vertex is placed at
the midpoint of its corresponding source edge and labelled by that edge.
Black vertices come from source edges between consecutive levels; coloured
vertices come from source edges within a single level of the chain. The
red-highlighted vertices, walk-depth labels, and seven red slits are the
computed source-cap cut and full-medial-tire labelling cuts for the
recognised treads $T_1$ and $T_2$. Drawn by
\texttt{experiments/draw\_medial\_tire\_cut.py} with
\texttt{--whole --min-degree 5}.}
\label{fig:whole-medial}
\end{figure}
\begin{thebibliography}{9}
\bibitem{bauerfeld-medial-tire}
E.~Bauerfeld,
\emph{Medial Tire Decompositions of Plane Triangulations},
manuscript (math-research repository), 2026.
\end{thebibliography}
\end{document}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 KiB

@@ -1,403 +0,0 @@
% whole medial graph: n=20 seed=59 graph_seed=59 min_degree=5 source=5 recognised treads=[1, 2] |M(G)|=54
\begin{tabular}{c}
\begin{tikzpicture}[scale=4.06,
sedge/.style={black!50, line width=0.35pt},
sv/.style={circle, draw=black!60, fill=white, inner sep=1.1pt},
srcv/.style={circle, draw=blue!75!black, fill=blue!18, line width=0.7pt, inner sep=1.8pt}]
\draw[sedge] (0.000,0.433)--(-0.500,-0.433);
\draw[sedge] (0.000,0.433)--(0.500,-0.433);
\draw[sedge] (0.000,0.433)--(0.084,-0.096);
\draw[sedge] (0.000,0.433)--(-0.018,0.026);
\draw[sedge] (0.000,0.433)--(-0.128,-0.048);
\draw[sedge] (-0.500,-0.433)--(0.500,-0.433);
\draw[sedge] (-0.500,-0.433)--(-0.128,-0.048);
\draw[sedge] (-0.500,-0.433)--(-0.073,-0.186);
\draw[sedge] (-0.500,-0.433)--(0.019,-0.303);
\draw[sedge] (0.500,-0.433)--(0.084,-0.096);
\draw[sedge] (0.500,-0.433)--(0.019,-0.303);
\draw[sedge] (0.500,-0.433)--(0.139,-0.245);
\draw[sedge] (0.084,-0.096)--(-0.018,0.026);
\draw[sedge] (0.084,-0.096)--(0.139,-0.245);
\draw[sedge] (0.084,-0.096)--(0.060,-0.177);
\draw[sedge] (0.084,-0.096)--(0.035,-0.147);
\draw[sedge] (0.084,-0.096)--(0.024,-0.131);
\draw[sedge] (0.084,-0.096)--(0.015,-0.113);
\draw[sedge] (0.084,-0.096)--(0.002,-0.076);
\draw[sedge] (-0.018,0.026)--(-0.128,-0.048);
\draw[sedge] (-0.018,0.026)--(0.002,-0.076);
\draw[sedge] (-0.018,0.026)--(-0.048,-0.081);
\draw[sedge] (-0.128,-0.048)--(-0.073,-0.186);
\draw[sedge] (-0.128,-0.048)--(-0.048,-0.081);
\draw[sedge] (-0.073,-0.186)--(0.019,-0.303);
\draw[sedge] (-0.073,-0.186)--(-0.048,-0.081);
\draw[sedge] (-0.073,-0.186)--(-0.023,-0.119);
\draw[sedge] (-0.073,-0.186)--(-0.012,-0.141);
\draw[sedge] (-0.073,-0.186)--(-0.003,-0.156);
\draw[sedge] (-0.073,-0.186)--(0.010,-0.177);
\draw[sedge] (-0.073,-0.186)--(0.031,-0.218);
\draw[sedge] (0.019,-0.303)--(0.139,-0.245);
\draw[sedge] (0.019,-0.303)--(0.031,-0.218);
\draw[sedge] (0.139,-0.245)--(0.060,-0.177);
\draw[sedge] (0.139,-0.245)--(0.031,-0.218);
\draw[sedge] (0.060,-0.177)--(0.035,-0.147);
\draw[sedge] (0.060,-0.177)--(0.010,-0.177);
\draw[sedge] (0.060,-0.177)--(0.031,-0.218);
\draw[sedge] (0.035,-0.147)--(0.024,-0.131);
\draw[sedge] (0.035,-0.147)--(-0.003,-0.156);
\draw[sedge] (0.035,-0.147)--(0.010,-0.177);
\draw[sedge] (0.024,-0.131)--(0.015,-0.113);
\draw[sedge] (0.024,-0.131)--(-0.012,-0.141);
\draw[sedge] (0.024,-0.131)--(-0.003,-0.156);
\draw[sedge] (0.015,-0.113)--(0.002,-0.076);
\draw[sedge] (0.015,-0.113)--(-0.023,-0.119);
\draw[sedge] (0.015,-0.113)--(-0.012,-0.141);
\draw[sedge] (0.002,-0.076)--(-0.048,-0.081);
\draw[sedge] (0.002,-0.076)--(-0.023,-0.119);
\draw[sedge] (-0.048,-0.081)--(-0.023,-0.119);
\draw[sedge] (-0.023,-0.119)--(-0.012,-0.141);
\draw[sedge] (-0.012,-0.141)--(-0.003,-0.156);
\draw[sedge] (-0.003,-0.156)--(0.010,-0.177);
\draw[sedge] (0.010,-0.177)--(0.031,-0.218);
\node[sv] at (0.000,0.433) {};
\node[sv] at (-0.500,-0.433) {};
\node[sv] at (0.500,-0.433) {};
\node[sv] at (0.084,-0.096) {};
\node[sv] at (-0.018,0.026) {};
\node[srcv] at (-0.128,-0.048) {};
\node[sv] at (-0.073,-0.186) {};
\node[sv] at (0.019,-0.303) {};
\node[sv] at (0.139,-0.245) {};
\node[sv] at (0.060,-0.177) {};
\node[sv] at (0.035,-0.147) {};
\node[sv] at (0.024,-0.131) {};
\node[sv] at (0.015,-0.113) {};
\node[sv] at (0.002,-0.076) {};
\node[sv] at (-0.048,-0.081) {};
\node[sv] at (-0.023,-0.119) {};
\node[sv] at (-0.012,-0.141) {};
\node[sv] at (-0.003,-0.156) {};
\node[sv] at (0.010,-0.177) {};
\node[sv] at (0.031,-0.218) {};
\node[font=\scriptsize, text=blue!70!black] at (-0.128,-0.133) {source 5};
\end{tikzpicture}
\\[-0.25ex]
{\scriptsize source graph $G$}
\\[1.0ex]
\begin{tikzpicture}[scale=7.0,
base/.style={black!12, line width=0.25pt},
med/.style={black!38, line width=0.32pt},
annv/.style={circle, draw=black!70, fill=black!18, inner sep=1.0pt},
levone/.style={circle, draw=orange!75!black, fill=orange!20, inner sep=1.2pt},
levtwo/.style={circle, draw=violet!70!black, fill=violet!18, inner sep=1.2pt},
levthree/.style={circle, draw=teal!70!black, fill=teal!18, inner sep=1.2pt},
knownv/.style={circle, draw=red!70!black, fill=red!24, inner sep=1.5pt},
elbl/.style={font=\tiny, text=black!70, inner sep=0.2pt},
dlbl/.style={font=\tiny\bfseries, text=black, inner sep=0.5pt},
cut/.style={red!80!black, line width=1.0pt},
cutlbl/.style={font=\tiny, text=red!75!black}]
\draw[base] (0.000,0.433)--(-0.500,-0.433);
\draw[base] (0.000,0.433)--(0.500,-0.433);
\draw[base] (0.000,0.433)--(0.084,-0.096);
\draw[base] (0.000,0.433)--(-0.018,0.026);
\draw[base] (0.000,0.433)--(-0.128,-0.048);
\draw[base] (-0.500,-0.433)--(0.500,-0.433);
\draw[base] (-0.500,-0.433)--(-0.128,-0.048);
\draw[base] (-0.500,-0.433)--(-0.073,-0.186);
\draw[base] (-0.500,-0.433)--(0.019,-0.303);
\draw[base] (0.500,-0.433)--(0.084,-0.096);
\draw[base] (0.500,-0.433)--(0.019,-0.303);
\draw[base] (0.500,-0.433)--(0.139,-0.245);
\draw[base] (0.084,-0.096)--(-0.018,0.026);
\draw[base] (0.084,-0.096)--(0.139,-0.245);
\draw[base] (0.084,-0.096)--(0.060,-0.177);
\draw[base] (0.084,-0.096)--(0.035,-0.147);
\draw[base] (0.084,-0.096)--(0.024,-0.131);
\draw[base] (0.084,-0.096)--(0.015,-0.113);
\draw[base] (0.084,-0.096)--(0.002,-0.076);
\draw[base] (-0.018,0.026)--(-0.128,-0.048);
\draw[base] (-0.018,0.026)--(0.002,-0.076);
\draw[base] (-0.018,0.026)--(-0.048,-0.081);
\draw[base] (-0.128,-0.048)--(-0.073,-0.186);
\draw[base] (-0.128,-0.048)--(-0.048,-0.081);
\draw[base] (-0.073,-0.186)--(0.019,-0.303);
\draw[base] (-0.073,-0.186)--(-0.048,-0.081);
\draw[base] (-0.073,-0.186)--(-0.023,-0.119);
\draw[base] (-0.073,-0.186)--(-0.012,-0.141);
\draw[base] (-0.073,-0.186)--(-0.003,-0.156);
\draw[base] (-0.073,-0.186)--(0.010,-0.177);
\draw[base] (-0.073,-0.186)--(0.031,-0.218);
\draw[base] (0.019,-0.303)--(0.139,-0.245);
\draw[base] (0.019,-0.303)--(0.031,-0.218);
\draw[base] (0.139,-0.245)--(0.060,-0.177);
\draw[base] (0.139,-0.245)--(0.031,-0.218);
\draw[base] (0.060,-0.177)--(0.035,-0.147);
\draw[base] (0.060,-0.177)--(0.010,-0.177);
\draw[base] (0.060,-0.177)--(0.031,-0.218);
\draw[base] (0.035,-0.147)--(0.024,-0.131);
\draw[base] (0.035,-0.147)--(-0.003,-0.156);
\draw[base] (0.035,-0.147)--(0.010,-0.177);
\draw[base] (0.024,-0.131)--(0.015,-0.113);
\draw[base] (0.024,-0.131)--(-0.012,-0.141);
\draw[base] (0.024,-0.131)--(-0.003,-0.156);
\draw[base] (0.015,-0.113)--(0.002,-0.076);
\draw[base] (0.015,-0.113)--(-0.023,-0.119);
\draw[base] (0.015,-0.113)--(-0.012,-0.141);
\draw[base] (0.002,-0.076)--(-0.048,-0.081);
\draw[base] (0.002,-0.076)--(-0.023,-0.119);
\draw[base] (-0.048,-0.081)--(-0.023,-0.119);
\draw[base] (-0.023,-0.119)--(-0.012,-0.141);
\draw[base] (-0.012,-0.141)--(-0.003,-0.156);
\draw[base] (-0.003,-0.156)--(0.010,-0.177);
\draw[base] (0.010,-0.177)--(0.031,-0.218);
\draw[med] (-0.250,0.000)--(-0.064,0.192);
\draw[med] (-0.250,0.000)--(0.250,0.000);
\draw[med] (-0.250,0.000)--(0.000,-0.433);
\draw[med] (-0.250,0.000)--(-0.314,-0.241);
\draw[med] (0.250,0.000)--(0.042,0.169);
\draw[med] (0.250,0.000)--(0.000,-0.433);
\draw[med] (0.250,0.000)--(0.292,-0.264);
\draw[med] (0.042,0.169)--(-0.009,0.230);
\draw[med] (0.042,0.169)--(0.292,-0.264);
\draw[med] (0.042,0.169)--(0.033,-0.035);
\draw[med] (-0.009,0.230)--(-0.064,0.192);
\draw[med] (-0.009,0.230)--(0.033,-0.035);
\draw[med] (-0.009,0.230)--(-0.073,-0.011);
\draw[med] (-0.064,0.192)--(-0.073,-0.011);
\draw[med] (-0.064,0.192)--(-0.314,-0.241);
\draw[med] (0.000,-0.433)--(-0.240,-0.368);
\draw[med] (0.000,-0.433)--(0.260,-0.368);
\draw[med] (-0.314,-0.241)--(-0.286,-0.310);
\draw[med] (-0.314,-0.241)--(-0.100,-0.117);
\draw[med] (-0.286,-0.310)--(-0.240,-0.368);
\draw[med] (-0.286,-0.310)--(-0.100,-0.117);
\draw[med] (-0.286,-0.310)--(-0.027,-0.245);
\draw[med] (-0.240,-0.368)--(-0.027,-0.245);
\draw[med] (-0.240,-0.368)--(0.260,-0.368);
\draw[med] (0.292,-0.264)--(0.319,-0.339);
\draw[med] (0.292,-0.264)--(0.111,-0.171);
\draw[med] (0.260,-0.368)--(0.319,-0.339);
\draw[med] (0.260,-0.368)--(0.079,-0.274);
\draw[med] (0.319,-0.339)--(0.079,-0.274);
\draw[med] (0.319,-0.339)--(0.111,-0.171);
\draw[med] (0.033,-0.035)--(0.043,-0.086);
\draw[med] (0.033,-0.035)--(-0.008,-0.025);
\draw[med] (0.111,-0.171)--(0.072,-0.136);
\draw[med] (0.111,-0.171)--(0.099,-0.211);
\draw[med] (0.072,-0.136)--(0.059,-0.122);
\draw[med] (0.072,-0.136)--(0.099,-0.211);
\draw[med] (0.072,-0.136)--(0.047,-0.162);
\draw[med] (0.059,-0.122)--(0.054,-0.113);
\draw[med] (0.059,-0.122)--(0.047,-0.162);
\draw[med] (0.059,-0.122)--(0.029,-0.139);
\draw[med] (0.054,-0.113)--(0.049,-0.104);
\draw[med] (0.054,-0.113)--(0.029,-0.139);
\draw[med] (0.054,-0.113)--(0.019,-0.122);
\draw[med] (0.049,-0.104)--(0.043,-0.086);
\draw[med] (0.049,-0.104)--(0.019,-0.122);
\draw[med] (0.049,-0.104)--(0.008,-0.095);
\draw[med] (0.043,-0.086)--(0.008,-0.095);
\draw[med] (0.043,-0.086)--(-0.008,-0.025);
\draw[med] (-0.073,-0.011)--(-0.033,-0.027);
\draw[med] (-0.073,-0.011)--(-0.088,-0.064);
\draw[med] (-0.008,-0.025)--(-0.033,-0.027);
\draw[med] (-0.008,-0.025)--(-0.023,-0.079);
\draw[med] (-0.033,-0.027)--(-0.023,-0.079);
\draw[med] (-0.033,-0.027)--(-0.088,-0.064);
\draw[med] (-0.100,-0.117)--(-0.088,-0.064);
\draw[med] (-0.100,-0.117)--(-0.060,-0.134);
\draw[med] (-0.088,-0.064)--(-0.060,-0.134);
\draw[med] (-0.027,-0.245)--(-0.021,-0.202);
\draw[med] (-0.027,-0.245)--(0.025,-0.260);
\draw[med] (-0.060,-0.134)--(-0.048,-0.153);
\draw[med] (-0.060,-0.134)--(-0.035,-0.100);
\draw[med] (-0.048,-0.153)--(-0.042,-0.164);
\draw[med] (-0.048,-0.153)--(-0.035,-0.100);
\draw[med] (-0.048,-0.153)--(-0.018,-0.130);
\draw[med] (-0.042,-0.164)--(-0.038,-0.171);
\draw[med] (-0.042,-0.164)--(-0.018,-0.130);
\draw[med] (-0.042,-0.164)--(-0.008,-0.149);
\draw[med] (-0.038,-0.171)--(-0.031,-0.182);
\draw[med] (-0.038,-0.171)--(-0.008,-0.149);
\draw[med] (-0.038,-0.171)--(0.003,-0.167);
\draw[med] (-0.031,-0.182)--(-0.021,-0.202);
\draw[med] (-0.031,-0.182)--(0.003,-0.167);
\draw[med] (-0.031,-0.182)--(0.021,-0.197);
\draw[med] (-0.021,-0.202)--(0.021,-0.197);
\draw[med] (-0.021,-0.202)--(0.025,-0.260);
\draw[med] (0.079,-0.274)--(0.025,-0.260);
\draw[med] (0.079,-0.274)--(0.085,-0.231);
\draw[med] (0.025,-0.260)--(0.085,-0.231);
\draw[med] (0.099,-0.211)--(0.085,-0.231);
\draw[med] (0.099,-0.211)--(0.045,-0.197);
\draw[med] (0.085,-0.231)--(0.045,-0.197);
\draw[med] (0.047,-0.162)--(0.035,-0.177);
\draw[med] (0.047,-0.162)--(0.022,-0.162);
\draw[med] (0.035,-0.177)--(0.045,-0.197);
\draw[med] (0.035,-0.177)--(0.021,-0.197);
\draw[med] (0.035,-0.177)--(0.022,-0.162);
\draw[med] (0.045,-0.197)--(0.021,-0.197);
\draw[med] (0.029,-0.139)--(0.016,-0.152);
\draw[med] (0.029,-0.139)--(0.010,-0.144);
\draw[med] (0.016,-0.152)--(0.022,-0.162);
\draw[med] (0.016,-0.152)--(0.003,-0.167);
\draw[med] (0.016,-0.152)--(0.010,-0.144);
\draw[med] (0.022,-0.162)--(0.003,-0.167);
\draw[med] (0.019,-0.122)--(0.006,-0.136);
\draw[med] (0.019,-0.122)--(0.001,-0.127);
\draw[med] (0.006,-0.136)--(0.010,-0.144);
\draw[med] (0.006,-0.136)--(-0.008,-0.149);
\draw[med] (0.006,-0.136)--(0.001,-0.127);
\draw[med] (0.010,-0.144)--(-0.008,-0.149);
\draw[med] (0.008,-0.095)--(-0.004,-0.116);
\draw[med] (0.008,-0.095)--(-0.011,-0.098);
\draw[med] (-0.004,-0.116)--(0.001,-0.127);
\draw[med] (-0.004,-0.116)--(-0.018,-0.130);
\draw[med] (-0.004,-0.116)--(-0.011,-0.098);
\draw[med] (0.001,-0.127)--(-0.018,-0.130);
\draw[med] (-0.023,-0.079)--(-0.011,-0.098);
\draw[med] (-0.023,-0.079)--(-0.035,-0.100);
\draw[med] (-0.011,-0.098)--(-0.035,-0.100);
\node[knownv] at (-0.250,0.000) {};
\node[annv] at (0.250,0.000) {};
\node[annv] at (0.042,0.169) {};
\node[knownv] at (-0.009,0.230) {};
\node[annv] at (-0.064,0.192) {};
\node[annv] at (0.000,-0.433) {};
\node[annv] at (-0.314,-0.241) {};
\node[knownv] at (-0.286,-0.310) {};
\node[annv] at (-0.240,-0.368) {};
\node[knownv] at (0.292,-0.264) {};
\node[knownv] at (0.260,-0.368) {};
\node[annv] at (0.319,-0.339) {};
\node[annv] at (0.033,-0.035) {};
\node[annv] at (0.111,-0.171) {};
\node[annv] at (0.072,-0.136) {};
\node[annv] at (-0.073,-0.011) {};
\node[annv] at (-0.100,-0.117) {};
\node[annv] at (-0.027,-0.245) {};
\node[annv] at (0.079,-0.274) {};
\node[knownv] at (0.099,-0.211) {};
\node[annv] at (0.059,-0.122) {};
\node[knownv] at (0.047,-0.162) {};
\node[knownv] at (0.029,-0.139) {};
\node[annv] at (0.016,-0.152) {};
\node[annv] at (0.022,-0.162) {};
\node[annv] at (0.054,-0.113) {};
\node[knownv] at (0.019,-0.122) {};
\node[annv] at (0.006,-0.136) {};
\node[annv] at (0.010,-0.144) {};
\node[annv] at (0.049,-0.104) {};
\node[annv] at (0.008,-0.095) {};
\node[annv] at (-0.004,-0.116) {};
\node[annv] at (0.001,-0.127) {};
\node[knownv] at (0.043,-0.086) {};
\node[annv] at (-0.008,-0.025) {};
\node[annv] at (-0.023,-0.079) {};
\node[knownv] at (-0.011,-0.098) {};
\node[knownv] at (-0.033,-0.027) {};
\node[annv] at (-0.088,-0.064) {};
\node[knownv] at (-0.060,-0.134) {};
\node[annv] at (-0.035,-0.100) {};
\node[annv] at (-0.048,-0.153) {};
\node[knownv] at (-0.018,-0.130) {};
\node[annv] at (-0.042,-0.164) {};
\node[knownv] at (-0.008,-0.149) {};
\node[annv] at (-0.038,-0.171) {};
\node[knownv] at (0.003,-0.167) {};
\node[annv] at (-0.031,-0.182) {};
\node[annv] at (0.035,-0.177) {};
\node[knownv] at (0.021,-0.197) {};
\node[annv] at (-0.021,-0.202) {};
\node[knownv] at (0.025,-0.260) {};
\node[annv] at (0.085,-0.231) {};
\node[annv] at (0.045,-0.197) {};
\node[elbl] at (-0.250,0.000) [yshift=-4.8pt] {$0\!{-}\!1$};
\node[elbl] at (0.250,0.000) [yshift=-4.8pt] {$0\!{-}\!2$};
\node[elbl] at (0.042,0.169) [yshift=-4.8pt] {$0\!{-}\!3$};
\node[elbl] at (-0.009,0.230) [yshift=-4.8pt] {$0\!{-}\!4$};
\node[elbl] at (-0.064,0.192) [yshift=-4.8pt] {$0\!{-}\!5$};
\node[elbl] at (0.000,-0.433) [yshift=-4.8pt] {$1\!{-}\!2$};
\node[elbl] at (-0.314,-0.241) [yshift=-4.8pt] {$1\!{-}\!5$};
\node[elbl] at (-0.286,-0.310) [yshift=-4.8pt] {$1\!{-}\!6$};
\node[elbl] at (-0.240,-0.368) [yshift=-4.8pt] {$1\!{-}\!7$};
\node[elbl] at (0.292,-0.264) [yshift=-4.8pt] {$2\!{-}\!3$};
\node[elbl] at (0.260,-0.368) [yshift=-4.8pt] {$2\!{-}\!7$};
\node[elbl] at (0.319,-0.339) [yshift=-4.8pt] {$2\!{-}\!8$};
\node[elbl] at (0.033,-0.035) [yshift=-4.8pt] {$3\!{-}\!4$};
\node[elbl] at (0.111,-0.171) [yshift=-4.8pt] {$3\!{-}\!8$};
\node[elbl] at (0.072,-0.136) [yshift=-4.8pt] {$3\!{-}\!9$};
\node[elbl] at (-0.073,-0.011) [yshift=-4.8pt] {$4\!{-}\!5$};
\node[elbl] at (-0.100,-0.117) [yshift=-4.8pt] {$5\!{-}\!6$};
\node[elbl] at (-0.027,-0.245) [yshift=-4.8pt] {$6\!{-}\!7$};
\node[elbl] at (0.079,-0.274) [yshift=-4.8pt] {$7\!{-}\!8$};
\node[elbl] at (0.099,-0.211) [yshift=-4.8pt] {$8\!{-}\!9$};
\node[elbl] at (0.059,-0.122) [yshift=-4.8pt] {$10\!{-}\!3$};
\node[elbl] at (0.047,-0.162) [yshift=-4.8pt] {$10\!{-}\!9$};
\node[elbl] at (0.029,-0.139) [yshift=-4.8pt] {$10\!{-}\!11$};
\node[elbl] at (0.016,-0.152) [yshift=-4.8pt] {$10\!{-}\!17$};
\node[elbl] at (0.022,-0.162) [yshift=-4.8pt] {$10\!{-}\!18$};
\node[elbl] at (0.054,-0.113) [yshift=-4.8pt] {$11\!{-}\!3$};
\node[elbl] at (0.019,-0.122) [yshift=-4.8pt] {$11\!{-}\!12$};
\node[elbl] at (0.006,-0.136) [yshift=-4.8pt] {$11\!{-}\!16$};
\node[elbl] at (0.010,-0.144) [yshift=-4.8pt] {$11\!{-}\!17$};
\node[elbl] at (0.049,-0.104) [yshift=-4.8pt] {$12\!{-}\!3$};
\node[elbl] at (0.008,-0.095) [yshift=-4.8pt] {$12\!{-}\!13$};
\node[elbl] at (-0.004,-0.116) [yshift=-4.8pt] {$12\!{-}\!15$};
\node[elbl] at (0.001,-0.127) [yshift=-4.8pt] {$12\!{-}\!16$};
\node[elbl] at (0.043,-0.086) [yshift=-4.8pt] {$13\!{-}\!3$};
\node[elbl] at (-0.008,-0.025) [yshift=-4.8pt] {$13\!{-}\!4$};
\node[elbl] at (-0.023,-0.079) [yshift=-4.8pt] {$13\!{-}\!14$};
\node[elbl] at (-0.011,-0.098) [yshift=-4.8pt] {$13\!{-}\!15$};
\node[elbl] at (-0.033,-0.027) [yshift=-4.8pt] {$14\!{-}\!4$};
\node[elbl] at (-0.088,-0.064) [yshift=-4.8pt] {$14\!{-}\!5$};
\node[elbl] at (-0.060,-0.134) [yshift=-4.8pt] {$14\!{-}\!6$};
\node[elbl] at (-0.035,-0.100) [yshift=-4.8pt] {$14\!{-}\!15$};
\node[elbl] at (-0.048,-0.153) [yshift=-4.8pt] {$15\!{-}\!6$};
\node[elbl] at (-0.018,-0.130) [yshift=-4.8pt] {$15\!{-}\!16$};
\node[elbl] at (-0.042,-0.164) [yshift=-4.8pt] {$16\!{-}\!6$};
\node[elbl] at (-0.008,-0.149) [yshift=-4.8pt] {$16\!{-}\!17$};
\node[elbl] at (-0.038,-0.171) [yshift=-4.8pt] {$17\!{-}\!6$};
\node[elbl] at (0.003,-0.167) [yshift=-4.8pt] {$17\!{-}\!18$};
\node[elbl] at (-0.031,-0.182) [yshift=-4.8pt] {$18\!{-}\!6$};
\node[elbl] at (0.035,-0.177) [yshift=-4.8pt] {$18\!{-}\!9$};
\node[elbl] at (0.021,-0.197) [yshift=-4.8pt] {$18\!{-}\!19$};
\node[elbl] at (-0.021,-0.202) [yshift=-4.8pt] {$19\!{-}\!6$};
\node[elbl] at (0.025,-0.260) [yshift=-4.8pt] {$19\!{-}\!7$};
\node[elbl] at (0.085,-0.231) [yshift=-4.8pt] {$19\!{-}\!8$};
\node[elbl] at (0.045,-0.197) [yshift=-4.8pt] {$19\!{-}\!9$};
\node[dlbl] at (-0.250,0.000) [yshift=5.0pt] {4};
\node[dlbl] at (-0.009,0.230) [yshift=5.0pt] {2};
\node[dlbl] at (-0.286,-0.310) [yshift=5.0pt] {6};
\node[dlbl] at (0.292,-0.264) [yshift=5.0pt] {3,18};
\node[dlbl] at (0.260,-0.368) [yshift=5.0pt] {5,17};
\node[dlbl] at (0.099,-0.211) [yshift=5.0pt] {13,14};
\node[dlbl] at (0.047,-0.162) [yshift=5.0pt] {11,12};
\node[dlbl] at (0.029,-0.139) [yshift=5.0pt] {7,8};
\node[dlbl] at (0.019,-0.122) [yshift=5.0pt] {5,6};
\node[dlbl] at (0.043,-0.086) [yshift=5.0pt] {1,2};
\node[dlbl] at (-0.011,-0.098) [yshift=5.0pt] {3,13};
\node[dlbl] at (-0.033,-0.027) [yshift=5.0pt] {0};
\node[dlbl] at (-0.060,-0.134) [yshift=5.0pt] {12};
\node[dlbl] at (-0.018,-0.130) [yshift=5.0pt] {4,11};
\node[dlbl] at (-0.008,-0.149) [yshift=5.0pt] {9,10};
\node[dlbl] at (0.003,-0.167) [yshift=5.0pt] {9,10};
\node[dlbl] at (0.021,-0.197) [yshift=5.0pt] {8,15};
\node[dlbl] at (0.025,-0.260) [yshift=5.0pt] {7,16};
\draw[cut] (-0.075,-0.032)--(-0.101,-0.097);
\node[cutlbl] at (-0.120,-0.142) {cut 1};
\draw[cut] (-0.026,-0.044)--(-0.020,-0.114);
\node[cutlbl] at (-0.016,-0.162) {cut 2};
\draw[cut] (0.058,-0.138)--(0.041,-0.070);
\node[cutlbl] at (0.030,-0.023) {cut 3};
\draw[cut] (-0.004,-0.102)--(0.016,-0.169);
\node[cutlbl] at (0.029,-0.217) {cut 4};
\draw[cut] (0.085,-0.146)--(0.034,-0.097);
\node[cutlbl] at (-0.001,-0.063) {cut 5};
\draw[cut] (0.035,-0.212)--(0.035,-0.142);
\node[cutlbl] at (0.034,-0.093) {cut 6};
\draw[cut] (0.079,-0.183)--(0.144,-0.158);
\node[cutlbl] at (0.190,-0.142) {cut 7};
\end{tikzpicture}
\\[-0.25ex]
{\scriptsize medial graph $M(G)$ at edge midpoints}
\end{tabular}
@@ -1,382 +0,0 @@
% whole medial graph: n=20 seed=72 source=9 recognised treads=[2] |M(G)|=54
\begin{tabular}{c}
\begin{tikzpicture}[scale=4.06,
sedge/.style={black!50, line width=0.35pt},
sv/.style={circle, draw=black!60, fill=white, inner sep=1.1pt},
srcv/.style={circle, draw=blue!75!black, fill=blue!18, line width=0.7pt, inner sep=1.8pt}]
\draw[sedge] (0.000,0.433)--(-0.500,-0.433);
\draw[sedge] (0.000,0.433)--(0.500,-0.433);
\draw[sedge] (0.000,0.433)--(0.027,-0.257);
\draw[sedge] (0.000,0.433)--(0.199,-0.203);
\draw[sedge] (0.000,0.433)--(-0.158,-0.086);
\draw[sedge] (-0.500,-0.433)--(0.500,-0.433);
\draw[sedge] (-0.500,-0.433)--(0.027,-0.257);
\draw[sedge] (-0.500,-0.433)--(0.012,-0.355);
\draw[sedge] (-0.500,-0.433)--(-0.170,-0.348);
\draw[sedge] (-0.500,-0.433)--(-0.218,-0.346);
\draw[sedge] (-0.500,-0.433)--(-0.158,-0.086);
\draw[sedge] (-0.500,-0.433)--(-0.230,-0.345);
\draw[sedge] (0.500,-0.433)--(0.027,-0.257);
\draw[sedge] (0.500,-0.433)--(0.199,-0.203);
\draw[sedge] (0.500,-0.433)--(0.012,-0.355);
\draw[sedge] (0.500,-0.433)--(0.234,-0.280);
\draw[sedge] (0.500,-0.433)--(0.292,-0.291);
\draw[sedge] (0.500,-0.433)--(0.151,-0.341);
\draw[sedge] (0.500,-0.433)--(0.254,-0.323);
\draw[sedge] (0.027,-0.257)--(0.199,-0.203);
\draw[sedge] (0.027,-0.257)--(0.012,-0.355);
\draw[sedge] (0.027,-0.257)--(0.234,-0.280);
\draw[sedge] (0.027,-0.257)--(-0.170,-0.348);
\draw[sedge] (0.027,-0.257)--(0.151,-0.341);
\draw[sedge] (0.027,-0.257)--(0.254,-0.323);
\draw[sedge] (0.027,-0.257)--(-0.218,-0.346);
\draw[sedge] (0.027,-0.257)--(0.063,-0.317);
\draw[sedge] (0.027,-0.257)--(0.146,-0.243);
\draw[sedge] (0.027,-0.257)--(-0.158,-0.086);
\draw[sedge] (0.027,-0.257)--(0.124,-0.234);
\draw[sedge] (0.027,-0.257)--(-0.230,-0.345);
\draw[sedge] (0.199,-0.203)--(0.234,-0.280);
\draw[sedge] (0.199,-0.203)--(0.292,-0.291);
\draw[sedge] (0.199,-0.203)--(0.233,-0.249);
\draw[sedge] (0.199,-0.203)--(0.221,-0.241);
\draw[sedge] (0.199,-0.203)--(0.146,-0.243);
\draw[sedge] (0.199,-0.203)--(0.218,-0.231);
\draw[sedge] (0.199,-0.203)--(0.124,-0.234);
\draw[sedge] (0.012,-0.355)--(-0.170,-0.348);
\draw[sedge] (0.012,-0.355)--(0.151,-0.341);
\draw[sedge] (0.012,-0.355)--(0.063,-0.317);
\draw[sedge] (0.234,-0.280)--(0.292,-0.291);
\draw[sedge] (0.234,-0.280)--(0.233,-0.249);
\draw[sedge] (0.234,-0.280)--(0.254,-0.323);
\draw[sedge] (0.234,-0.280)--(0.221,-0.241);
\draw[sedge] (0.234,-0.280)--(0.146,-0.243);
\draw[sedge] (0.292,-0.291)--(0.233,-0.249);
\draw[sedge] (0.233,-0.249)--(0.221,-0.241);
\draw[sedge] (0.233,-0.249)--(0.218,-0.231);
\draw[sedge] (-0.170,-0.348)--(-0.218,-0.346);
\draw[sedge] (0.151,-0.341)--(0.063,-0.317);
\draw[sedge] (-0.218,-0.346)--(-0.230,-0.345);
\draw[sedge] (0.221,-0.241)--(0.218,-0.231);
\draw[sedge] (0.146,-0.243)--(0.124,-0.234);
\node[sv] at (0.000,0.433) {};
\node[sv] at (-0.500,-0.433) {};
\node[sv] at (0.500,-0.433) {};
\node[sv] at (0.027,-0.257) {};
\node[sv] at (0.199,-0.203) {};
\node[sv] at (0.012,-0.355) {};
\node[sv] at (0.234,-0.280) {};
\node[sv] at (0.292,-0.291) {};
\node[sv] at (0.233,-0.249) {};
\node[srcv] at (-0.170,-0.348) {};
\node[sv] at (0.151,-0.341) {};
\node[sv] at (0.254,-0.323) {};
\node[sv] at (-0.218,-0.346) {};
\node[sv] at (0.221,-0.241) {};
\node[sv] at (0.063,-0.317) {};
\node[sv] at (0.146,-0.243) {};
\node[sv] at (0.218,-0.231) {};
\node[sv] at (-0.158,-0.086) {};
\node[sv] at (0.124,-0.234) {};
\node[sv] at (-0.230,-0.345) {};
\node[font=\scriptsize, text=blue!70!black] at (-0.170,-0.433) {source 9};
\end{tikzpicture}
\\[-0.25ex]
{\scriptsize source graph $G$}
\\[1.0ex]
\begin{tikzpicture}[scale=7.0,
base/.style={black!12, line width=0.25pt},
med/.style={black!38, line width=0.32pt},
annv/.style={circle, draw=black!70, fill=black!18, inner sep=1.0pt},
levone/.style={circle, draw=orange!75!black, fill=orange!20, inner sep=1.2pt},
levtwo/.style={circle, draw=violet!70!black, fill=violet!18, inner sep=1.2pt},
levthree/.style={circle, draw=teal!70!black, fill=teal!18, inner sep=1.2pt},
knownv/.style={circle, draw=red!70!black, fill=red!24, inner sep=1.5pt},
elbl/.style={font=\tiny, text=black!70, inner sep=0.2pt},
dlbl/.style={font=\tiny\bfseries, text=black, inner sep=0.5pt},
cut/.style={red!80!black, line width=1.0pt},
cutlbl/.style={font=\tiny, text=red!75!black}]
\draw[base] (0.000,0.433)--(-0.500,-0.433);
\draw[base] (0.000,0.433)--(0.500,-0.433);
\draw[base] (0.000,0.433)--(0.027,-0.257);
\draw[base] (0.000,0.433)--(0.199,-0.203);
\draw[base] (0.000,0.433)--(-0.158,-0.086);
\draw[base] (-0.500,-0.433)--(0.500,-0.433);
\draw[base] (-0.500,-0.433)--(0.027,-0.257);
\draw[base] (-0.500,-0.433)--(0.012,-0.355);
\draw[base] (-0.500,-0.433)--(-0.170,-0.348);
\draw[base] (-0.500,-0.433)--(-0.218,-0.346);
\draw[base] (-0.500,-0.433)--(-0.158,-0.086);
\draw[base] (-0.500,-0.433)--(-0.230,-0.345);
\draw[base] (0.500,-0.433)--(0.027,-0.257);
\draw[base] (0.500,-0.433)--(0.199,-0.203);
\draw[base] (0.500,-0.433)--(0.012,-0.355);
\draw[base] (0.500,-0.433)--(0.234,-0.280);
\draw[base] (0.500,-0.433)--(0.292,-0.291);
\draw[base] (0.500,-0.433)--(0.151,-0.341);
\draw[base] (0.500,-0.433)--(0.254,-0.323);
\draw[base] (0.027,-0.257)--(0.199,-0.203);
\draw[base] (0.027,-0.257)--(0.012,-0.355);
\draw[base] (0.027,-0.257)--(0.234,-0.280);
\draw[base] (0.027,-0.257)--(-0.170,-0.348);
\draw[base] (0.027,-0.257)--(0.151,-0.341);
\draw[base] (0.027,-0.257)--(0.254,-0.323);
\draw[base] (0.027,-0.257)--(-0.218,-0.346);
\draw[base] (0.027,-0.257)--(0.063,-0.317);
\draw[base] (0.027,-0.257)--(0.146,-0.243);
\draw[base] (0.027,-0.257)--(-0.158,-0.086);
\draw[base] (0.027,-0.257)--(0.124,-0.234);
\draw[base] (0.027,-0.257)--(-0.230,-0.345);
\draw[base] (0.199,-0.203)--(0.234,-0.280);
\draw[base] (0.199,-0.203)--(0.292,-0.291);
\draw[base] (0.199,-0.203)--(0.233,-0.249);
\draw[base] (0.199,-0.203)--(0.221,-0.241);
\draw[base] (0.199,-0.203)--(0.146,-0.243);
\draw[base] (0.199,-0.203)--(0.218,-0.231);
\draw[base] (0.199,-0.203)--(0.124,-0.234);
\draw[base] (0.012,-0.355)--(-0.170,-0.348);
\draw[base] (0.012,-0.355)--(0.151,-0.341);
\draw[base] (0.012,-0.355)--(0.063,-0.317);
\draw[base] (0.234,-0.280)--(0.292,-0.291);
\draw[base] (0.234,-0.280)--(0.233,-0.249);
\draw[base] (0.234,-0.280)--(0.254,-0.323);
\draw[base] (0.234,-0.280)--(0.221,-0.241);
\draw[base] (0.234,-0.280)--(0.146,-0.243);
\draw[base] (0.292,-0.291)--(0.233,-0.249);
\draw[base] (0.233,-0.249)--(0.221,-0.241);
\draw[base] (0.233,-0.249)--(0.218,-0.231);
\draw[base] (-0.170,-0.348)--(-0.218,-0.346);
\draw[base] (0.151,-0.341)--(0.063,-0.317);
\draw[base] (-0.218,-0.346)--(-0.230,-0.345);
\draw[base] (0.221,-0.241)--(0.218,-0.231);
\draw[base] (0.146,-0.243)--(0.124,-0.234);
\draw[med] (-0.250,0.000)--(-0.079,0.174);
\draw[med] (-0.250,0.000)--(0.250,0.000);
\draw[med] (-0.250,0.000)--(0.000,-0.433);
\draw[med] (-0.250,0.000)--(-0.329,-0.259);
\draw[med] (0.250,0.000)--(0.100,0.115);
\draw[med] (0.250,0.000)--(0.000,-0.433);
\draw[med] (0.250,0.000)--(0.350,-0.318);
\draw[med] (0.014,0.088)--(-0.079,0.174);
\draw[med] (0.014,0.088)--(0.100,0.115);
\draw[med] (0.014,0.088)--(0.113,-0.230);
\draw[med] (0.014,0.088)--(-0.065,-0.171);
\draw[med] (0.100,0.115)--(0.350,-0.318);
\draw[med] (0.100,0.115)--(0.113,-0.230);
\draw[med] (-0.079,0.174)--(-0.065,-0.171);
\draw[med] (-0.079,0.174)--(-0.329,-0.259);
\draw[med] (0.000,-0.433)--(-0.244,-0.394);
\draw[med] (0.000,-0.433)--(0.256,-0.394);
\draw[med] (-0.236,-0.345)--(-0.365,-0.389);
\draw[med] (-0.236,-0.345)--(-0.329,-0.259);
\draw[med] (-0.236,-0.345)--(-0.065,-0.171);
\draw[med] (-0.236,-0.345)--(-0.102,-0.301);
\draw[med] (-0.244,-0.394)--(-0.335,-0.390);
\draw[med] (-0.244,-0.394)--(-0.079,-0.351);
\draw[med] (-0.244,-0.394)--(0.256,-0.394);
\draw[med] (-0.335,-0.390)--(-0.359,-0.389);
\draw[med] (-0.335,-0.390)--(-0.194,-0.347);
\draw[med] (-0.335,-0.390)--(-0.079,-0.351);
\draw[med] (-0.359,-0.389)--(-0.365,-0.389);
\draw[med] (-0.359,-0.389)--(-0.224,-0.345);
\draw[med] (-0.359,-0.389)--(-0.194,-0.347);
\draw[med] (-0.329,-0.259)--(-0.065,-0.171);
\draw[med] (-0.365,-0.389)--(-0.102,-0.301);
\draw[med] (-0.365,-0.389)--(-0.224,-0.345);
\draw[med] (0.264,-0.345)--(0.377,-0.378);
\draw[med] (0.264,-0.345)--(0.325,-0.387);
\draw[med] (0.264,-0.345)--(0.140,-0.290);
\draw[med] (0.264,-0.345)--(0.089,-0.299);
\draw[med] (0.350,-0.318)--(0.396,-0.362);
\draw[med] (0.350,-0.318)--(0.245,-0.247);
\draw[med] (0.256,-0.394)--(0.325,-0.387);
\draw[med] (0.256,-0.394)--(0.081,-0.348);
\draw[med] (0.367,-0.357)--(0.396,-0.362);
\draw[med] (0.367,-0.357)--(0.377,-0.378);
\draw[med] (0.367,-0.357)--(0.244,-0.302);
\draw[med] (0.367,-0.357)--(0.263,-0.286);
\draw[med] (0.396,-0.362)--(0.263,-0.286);
\draw[med] (0.396,-0.362)--(0.245,-0.247);
\draw[med] (0.325,-0.387)--(0.081,-0.348);
\draw[med] (0.325,-0.387)--(0.089,-0.299);
\draw[med] (0.377,-0.378)--(0.140,-0.290);
\draw[med] (0.377,-0.378)--(0.244,-0.302);
\draw[med] (0.113,-0.230)--(0.076,-0.246);
\draw[med] (0.113,-0.230)--(0.162,-0.218);
\draw[med] (0.019,-0.306)--(-0.071,-0.302);
\draw[med] (0.019,-0.306)--(0.045,-0.287);
\draw[med] (0.019,-0.306)--(-0.079,-0.351);
\draw[med] (0.019,-0.306)--(0.038,-0.336);
\draw[med] (0.131,-0.268)--(0.140,-0.290);
\draw[med] (0.131,-0.268)--(0.087,-0.250);
\draw[med] (0.131,-0.268)--(0.190,-0.262);
\draw[med] (0.131,-0.268)--(0.244,-0.302);
\draw[med] (-0.071,-0.302)--(-0.096,-0.301);
\draw[med] (-0.071,-0.302)--(-0.079,-0.351);
\draw[med] (-0.071,-0.302)--(-0.194,-0.347);
\draw[med] (0.089,-0.299)--(0.045,-0.287);
\draw[med] (0.089,-0.299)--(0.107,-0.329);
\draw[med] (0.140,-0.290)--(0.244,-0.302);
\draw[med] (-0.096,-0.301)--(-0.102,-0.301);
\draw[med] (-0.096,-0.301)--(-0.194,-0.347);
\draw[med] (-0.096,-0.301)--(-0.224,-0.345);
\draw[med] (0.045,-0.287)--(0.107,-0.329);
\draw[med] (0.045,-0.287)--(0.038,-0.336);
\draw[med] (0.087,-0.250)--(0.076,-0.246);
\draw[med] (0.087,-0.250)--(0.135,-0.239);
\draw[med] (0.087,-0.250)--(0.190,-0.262);
\draw[med] (0.076,-0.246)--(0.162,-0.218);
\draw[med] (0.076,-0.246)--(0.135,-0.239);
\draw[med] (-0.102,-0.301)--(-0.224,-0.345);
\draw[med] (0.217,-0.241)--(0.173,-0.223);
\draw[med] (0.217,-0.241)--(0.210,-0.222);
\draw[med] (0.217,-0.241)--(0.190,-0.262);
\draw[med] (0.217,-0.241)--(0.227,-0.260);
\draw[med] (0.245,-0.247)--(0.216,-0.226);
\draw[med] (0.245,-0.247)--(0.262,-0.270);
\draw[med] (0.216,-0.226)--(0.209,-0.217);
\draw[med] (0.216,-0.226)--(0.262,-0.270);
\draw[med] (0.216,-0.226)--(0.225,-0.240);
\draw[med] (0.210,-0.222)--(0.209,-0.217);
\draw[med] (0.210,-0.222)--(0.219,-0.236);
\draw[med] (0.210,-0.222)--(0.227,-0.260);
\draw[med] (0.173,-0.223)--(0.162,-0.218);
\draw[med] (0.173,-0.223)--(0.190,-0.262);
\draw[med] (0.173,-0.223)--(0.135,-0.239);
\draw[med] (0.209,-0.217)--(0.225,-0.240);
\draw[med] (0.209,-0.217)--(0.219,-0.236);
\draw[med] (0.162,-0.218)--(0.135,-0.239);
\draw[med] (0.081,-0.348)--(0.038,-0.336);
\draw[med] (0.081,-0.348)--(0.107,-0.329);
\draw[med] (0.038,-0.336)--(0.107,-0.329);
\draw[med] (0.263,-0.286)--(0.233,-0.265);
\draw[med] (0.263,-0.286)--(0.262,-0.270);
\draw[med] (0.233,-0.265)--(0.227,-0.260);
\draw[med] (0.233,-0.265)--(0.227,-0.245);
\draw[med] (0.233,-0.265)--(0.262,-0.270);
\draw[med] (0.227,-0.260)--(0.227,-0.245);
\draw[med] (0.227,-0.245)--(0.225,-0.240);
\draw[med] (0.227,-0.245)--(0.219,-0.236);
\draw[med] (0.225,-0.240)--(0.219,-0.236);
\node[annv] at (-0.250,0.000) {};
\node[levtwo] at (0.250,0.000) {};
\node[annv] at (0.014,0.088) {};
\node[levtwo] at (0.100,0.115) {};
\node[levtwo] at (-0.079,0.174) {};
\node[annv] at (0.000,-0.433) {};
\node[levone] at (-0.236,-0.345) {};
\node[levone] at (-0.244,-0.394) {};
\node[annv] at (-0.335,-0.390) {};
\node[levone] at (-0.359,-0.389) {};
\node[annv] at (-0.329,-0.259) {};
\node[annv] at (-0.365,-0.389) {};
\node[annv] at (0.264,-0.345) {};
\node[knownv] at (0.350,-0.318) {};
\node[annv] at (0.256,-0.394) {};
\node[knownv] at (0.367,-0.357) {};
\node[annv] at (0.396,-0.362) {};
\node[annv] at (0.113,-0.230) {};
\node[levone] at (0.019,-0.306) {};
\node[annv] at (0.131,-0.268) {};
\node[annv] at (-0.071,-0.302) {};
\node[knownv] at (0.217,-0.241) {};
\node[annv] at (0.245,-0.247) {};
\node[annv] at (0.216,-0.226) {};
\node[annv] at (-0.079,-0.351) {};
\node[annv] at (0.263,-0.286) {};
\node[annv] at (0.233,-0.265) {};
\node[knownv] at (0.262,-0.270) {};
\node[levtwo] at (0.325,-0.387) {};
\node[annv] at (0.089,-0.299) {};
\node[annv] at (0.081,-0.348) {};
\node[levtwo] at (0.107,-0.329) {};
\node[levtwo] at (0.377,-0.378) {};
\node[annv] at (0.140,-0.290) {};
\node[levtwo] at (0.244,-0.302) {};
\node[levone] at (-0.096,-0.301) {};
\node[annv] at (-0.194,-0.347) {};
\node[annv] at (-0.224,-0.345) {};
\node[annv] at (0.210,-0.222) {};
\node[annv] at (0.227,-0.260) {};
\node[knownv] at (0.227,-0.245) {};
\node[knownv] at (0.219,-0.236) {};
\node[annv] at (0.045,-0.287) {};
\node[annv] at (0.038,-0.336) {};
\node[annv] at (0.087,-0.250) {};
\node[levtwo] at (0.173,-0.223) {};
\node[levtwo] at (0.190,-0.262) {};
\node[levtwo] at (0.135,-0.239) {};
\node[annv] at (0.209,-0.217) {};
\node[knownv] at (0.225,-0.240) {};
\node[annv] at (-0.065,-0.171) {};
\node[annv] at (0.076,-0.246) {};
\node[levtwo] at (0.162,-0.218) {};
\node[annv] at (-0.102,-0.301) {};
\node[elbl] at (-0.250,0.000) [yshift=-4.8pt] {$0\!{-}\!1$};
\node[elbl] at (0.250,0.000) [yshift=-4.8pt] {$0\!{-}\!2$};
\node[elbl] at (0.014,0.088) [yshift=-4.8pt] {$0\!{-}\!3$};
\node[elbl] at (0.100,0.115) [yshift=-4.8pt] {$0\!{-}\!4$};
\node[elbl] at (-0.079,0.174) [yshift=-4.8pt] {$0\!{-}\!17$};
\node[elbl] at (0.000,-0.433) [yshift=-4.8pt] {$1\!{-}\!2$};
\node[elbl] at (-0.236,-0.345) [yshift=-4.8pt] {$1\!{-}\!3$};
\node[elbl] at (-0.244,-0.394) [yshift=-4.8pt] {$1\!{-}\!5$};
\node[elbl] at (-0.335,-0.390) [yshift=-4.8pt] {$1\!{-}\!9$};
\node[elbl] at (-0.359,-0.389) [yshift=-4.8pt] {$1\!{-}\!12$};
\node[elbl] at (-0.329,-0.259) [yshift=-4.8pt] {$1\!{-}\!17$};
\node[elbl] at (-0.365,-0.389) [yshift=-4.8pt] {$1\!{-}\!19$};
\node[elbl] at (0.264,-0.345) [yshift=-4.8pt] {$2\!{-}\!3$};
\node[elbl] at (0.350,-0.318) [yshift=-4.8pt] {$2\!{-}\!4$};
\node[elbl] at (0.256,-0.394) [yshift=-4.8pt] {$2\!{-}\!5$};
\node[elbl] at (0.367,-0.357) [yshift=-4.8pt] {$2\!{-}\!6$};
\node[elbl] at (0.396,-0.362) [yshift=-4.8pt] {$2\!{-}\!7$};
\node[elbl] at (0.113,-0.230) [yshift=-4.8pt] {$3\!{-}\!4$};
\node[elbl] at (0.019,-0.306) [yshift=-4.8pt] {$3\!{-}\!5$};
\node[elbl] at (0.131,-0.268) [yshift=-4.8pt] {$3\!{-}\!6$};
\node[elbl] at (-0.071,-0.302) [yshift=-4.8pt] {$3\!{-}\!9$};
\node[elbl] at (0.217,-0.241) [yshift=-4.8pt] {$4\!{-}\!6$};
\node[elbl] at (0.245,-0.247) [yshift=-4.8pt] {$4\!{-}\!7$};
\node[elbl] at (0.216,-0.226) [yshift=-4.8pt] {$4\!{-}\!8$};
\node[elbl] at (-0.079,-0.351) [yshift=-4.8pt] {$5\!{-}\!9$};
\node[elbl] at (0.263,-0.286) [yshift=-4.8pt] {$6\!{-}\!7$};
\node[elbl] at (0.233,-0.265) [yshift=-4.8pt] {$6\!{-}\!8$};
\node[elbl] at (0.262,-0.270) [yshift=-4.8pt] {$7\!{-}\!8$};
\node[elbl] at (0.325,-0.387) [yshift=-4.8pt] {$10\!{-}\!2$};
\node[elbl] at (0.089,-0.299) [yshift=-4.8pt] {$10\!{-}\!3$};
\node[elbl] at (0.081,-0.348) [yshift=-4.8pt] {$10\!{-}\!5$};
\node[elbl] at (0.107,-0.329) [yshift=-4.8pt] {$10\!{-}\!14$};
\node[elbl] at (0.377,-0.378) [yshift=-4.8pt] {$11\!{-}\!2$};
\node[elbl] at (0.140,-0.290) [yshift=-4.8pt] {$11\!{-}\!3$};
\node[elbl] at (0.244,-0.302) [yshift=-4.8pt] {$11\!{-}\!6$};
\node[elbl] at (-0.096,-0.301) [yshift=-4.8pt] {$12\!{-}\!3$};
\node[elbl] at (-0.194,-0.347) [yshift=-4.8pt] {$12\!{-}\!9$};
\node[elbl] at (-0.224,-0.345) [yshift=-4.8pt] {$12\!{-}\!19$};
\node[elbl] at (0.210,-0.222) [yshift=-4.8pt] {$13\!{-}\!4$};
\node[elbl] at (0.227,-0.260) [yshift=-4.8pt] {$13\!{-}\!6$};
\node[elbl] at (0.227,-0.245) [yshift=-4.8pt] {$13\!{-}\!8$};
\node[elbl] at (0.219,-0.236) [yshift=-4.8pt] {$13\!{-}\!16$};
\node[elbl] at (0.045,-0.287) [yshift=-4.8pt] {$14\!{-}\!3$};
\node[elbl] at (0.038,-0.336) [yshift=-4.8pt] {$14\!{-}\!5$};
\node[elbl] at (0.087,-0.250) [yshift=-4.8pt] {$15\!{-}\!3$};
\node[elbl] at (0.173,-0.223) [yshift=-4.8pt] {$15\!{-}\!4$};
\node[elbl] at (0.190,-0.262) [yshift=-4.8pt] {$15\!{-}\!6$};
\node[elbl] at (0.135,-0.239) [yshift=-4.8pt] {$15\!{-}\!18$};
\node[elbl] at (0.209,-0.217) [yshift=-4.8pt] {$16\!{-}\!4$};
\node[elbl] at (0.225,-0.240) [yshift=-4.8pt] {$16\!{-}\!8$};
\node[elbl] at (-0.065,-0.171) [yshift=-4.8pt] {$17\!{-}\!3$};
\node[elbl] at (0.076,-0.246) [yshift=-4.8pt] {$18\!{-}\!3$};
\node[elbl] at (0.162,-0.218) [yshift=-4.8pt] {$18\!{-}\!4$};
\node[elbl] at (-0.102,-0.301) [yshift=-4.8pt] {$19\!{-}\!3$};
\node[dlbl] at (0.350,-0.318) [yshift=5.0pt] {6};
\node[dlbl] at (0.367,-0.357) [yshift=5.0pt] {7};
\node[dlbl] at (0.217,-0.241) [yshift=5.0pt] {0};
\node[dlbl] at (0.262,-0.270) [yshift=5.0pt] {2,3};
\node[dlbl] at (0.227,-0.245) [yshift=5.0pt] {1};
\node[dlbl] at (0.219,-0.236) [yshift=5.0pt] {5};
\node[dlbl] at (0.225,-0.240) [yshift=5.0pt] {4};
\draw[cut] (0.241,-0.204)--(0.180,-0.239);
\node[cutlbl] at (0.137,-0.263) {cut 1};
\draw[cut] (0.256,-0.320)--(0.269,-0.251);
\node[cutlbl] at (0.279,-0.203) {cut 2};
\end{tikzpicture}
\\[-0.25ex]
{\scriptsize medial graph $M(G)$ at edge midpoints}
\end{tabular}
@@ -1,18 +0,0 @@
# Random medial tire decomposition 1
- original vertices: 30
- original edges: 84
- original node connectivity: 5
- augmented vertices: 33
- augmented edges: 93
- same-level faces filled: 3
- source vertex: 14
- tire-tree nodes: 4
- tire-tree edges: 3
| node | depth | faces | annular cycles | annular | up | singleton down | bite apexes |
|--:|--:|--:|--:|--:|--:|--:|--:|
| T0 | 2 | 23 | 1 | 23 | 10 | 13 | 0 |
| T1 | 1 | 15 | 1 | 15 | 5 | 10 | 0 |
| T2 | 3 | 14 | 4 | 14 | 11 | 0 | 0 |
| T3 | 3 | 5 | 1 | 5 | 5 | 0 | 0 |
@@ -1,16 +0,0 @@
"""Compatibility wrapper for the medial tire decomposition drawing script."""
from __future__ import annotations
import os
import sys
PAPER_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if PAPER_DIR not in sys.path:
sys.path.insert(0, PAPER_DIR)
from lib.draw_random_medial_tire_decompositions import main
if __name__ == "__main__":
main()
@@ -1,16 +1,311 @@
"""Compatibility wrapper for the medial tire generator now kept in ../lib."""
"""Exhaustive generator for full medial tire graphs, indexed by |A(T)|.
Model (Definitions/Remarks 3.1--3.9 of the medial tire decompositions paper).
* The annular medial vertices induce a cycle A(T), the *annular cycle*
(Theorem 3.3). Write n = |A(T)| for its number of vertices = number of
annular faces = number of annular edges e_0,...,e_{n-1}.
* Each edge e_i of A(T) carries exactly one tooth (a triangle of M(T))
whose third vertex is a non-annular apex (Definition 3.4). A tooth is an
*up tooth* (apex in the outer region) or a *down tooth* (apex in the inner
region). We record the tooth types as a word in {U, D}^n.
* No two up teeth share an apex; at most two down teeth share an apex
(Remark 3.5). Two down teeth sharing an apex form a *bite* (Definition
3.7). So the down teeth are partitioned into singletons and bite pairs.
A bite pairs two down-edges and is drawn as an apex inside the disk with
spokes to the four endpoints; bites must be mutually non-crossing, i.e.
the bite pairs form a non-crossing (laminar) matching of the down-edges.
The two annular edges of a bite must be non-incident (Definition 3.7):
they share no annular vertex, so cyclically adjacent edges cannot pair.
* There are at least three up teeth (Remark 3.6).
* Bite-face condition (Remark 3.8). Let B(T) = A(T) together with the bite
apexes. Its interior non-tooth faces are the root face plus one inner-gap
face per bite. A singleton down tooth lies in the innermost bite enclosing
its edge (or in the root face if none). For every interior non-tooth face
the number of down-tooth apexes lying in that face must be 0 or at least 3.
Equivalently: no face holds exactly one or two singleton down teeth.
The generator enumerates, for a given n, every (tooth word, bite matching)
pair satisfying these properties and emits the resulting full medial tire
graph as an explicit vertex/edge structure. Configurations may optionally be
reduced modulo the dihedral symmetry of the cycle.
"""
from __future__ import annotations
import os
import sys
import argparse
import itertools
from collections import defaultdict
from dataclasses import dataclass
from functools import lru_cache
from typing import Iterator
PAPER_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if PAPER_DIR not in sys.path:
sys.path.insert(0, PAPER_DIR)
# A bite is an unordered pair of down-edge indices (i, j) with i < j.
Bite = tuple[int, int]
Matching = frozenset[Bite]
from lib.full_medial_tire_generator import * # noqa: F401,F403
from lib.full_medial_tire_generator import main
# ---------------------------------------------------------------------------
# Non-crossing (laminar) matchings of the down edges.
# ---------------------------------------------------------------------------
@lru_cache(maxsize=None)
def noncrossing_matchings(positions: tuple[int, ...]) -> tuple[Matching, ...]:
"""All non-crossing partial matchings of ``positions`` (sorted ascending).
Bite pairs drawn inside the disk are non-crossing iff, read in cyclic
order, no two pairs interleave. Cutting the cycle at the gap before the
first edge turns this into ordinary non-crossing interval matchings, which
obey the Catalan recursion below.
"""
if not positions:
return (frozenset(),)
head, *rest = positions
out: list[Matching] = []
# head left unmatched (a singleton down tooth, if its edge is down)
for tail in noncrossing_matchings(tuple(rest)):
out.append(tail)
# head matched with positions[k]; the strictly-enclosed block must be
# matched within itself to stay non-crossing.
for k in range(1, len(positions)):
partner = positions[k]
inside = tuple(positions[1:k])
outside = tuple(positions[k + 1:])
for m_in in noncrossing_matchings(inside):
for m_out in noncrossing_matchings(outside):
out.append(frozenset({(head, partner)}) | m_in | m_out)
return tuple(out)
# ---------------------------------------------------------------------------
# The bite-face condition (Remark 3.8).
# ---------------------------------------------------------------------------
def incident_edges(i: int, j: int, n: int) -> bool:
"""Whether annular edges i and j share an annular vertex on the n-cycle."""
return (j - i) % n == 1 or (i - j) % n == 1
def has_incident_bite(bites: Matching, n: int) -> bool:
"""Whether any bite pairs two incident (cyclically adjacent) edges."""
return any(incident_edges(i, j, n) for i, j in bites)
def innermost_bite(edge: int, bites: Matching) -> Bite | None:
"""The minimal-span bite whose open interval contains ``edge``, or None."""
enclosing = [b for b in bites if b[0] < edge < b[1]]
if not enclosing:
return None
return min(enclosing, key=lambda b: b[1] - b[0])
def face_singleton_counts(
tooth_word: str, bites: Matching
) -> dict[Bite | None, int]:
"""Down-singletons per interior non-tooth face of B(T).
The key ``None`` is the root face; a bite key is that bite's inner-gap
face. Faces with no singletons are simply absent from the result.
"""
matched = {edge for pair in bites for edge in pair}
counts: dict[Bite | None, int] = defaultdict(int)
for edge, tooth in enumerate(tooth_word):
if tooth != "D" or edge in matched:
continue # only singleton down teeth contribute apexes
counts[innermost_bite(edge, bites)] += 1
return dict(counts)
def satisfies_bite_face_condition(tooth_word: str, bites: Matching) -> bool:
"""Remark 3.8: every non-tooth face holds 0 or >=3 down-tooth apexes."""
return all(count >= 3 for count in face_singleton_counts(tooth_word, bites).values())
# ---------------------------------------------------------------------------
# The full medial tire graph as an explicit object.
# ---------------------------------------------------------------------------
@dataclass(frozen=True)
class FullMedialTireGraph:
"""A full medial tire graph M(T) determined by its combinatorial data.
Vertices are named:
a{k} annular medial vertex k (k = 0..n-1), forming A(T);
u{i} apex of the up tooth on edge i;
d{i} apex of the singleton down tooth on edge i;
p{i}_{j} apex of the bite pairing edges i and j (i < j).
"""
n: int
tooth_word: str
bites: Matching
@property
def up_edges(self) -> tuple[int, ...]:
return tuple(i for i, t in enumerate(self.tooth_word) if t == "U")
@property
def down_edges(self) -> tuple[int, ...]:
return tuple(i for i, t in enumerate(self.tooth_word) if t == "D")
@property
def bite_edges(self) -> frozenset[int]:
return frozenset(edge for pair in self.bites for edge in pair)
@property
def singleton_down_edges(self) -> tuple[int, ...]:
bite = self.bite_edges
return tuple(i for i in self.down_edges if i not in bite)
def apex_of_edge(self, edge: int) -> str:
if self.tooth_word[edge] == "U":
return f"u{edge}"
for i, j in self.bites:
if edge in (i, j):
return f"p{i}_{j}"
return f"d{edge}"
def vertices(self) -> list[str]:
verts = [f"a{k}" for k in range(self.n)]
for i in self.up_edges:
verts.append(f"u{i}")
for i in self.singleton_down_edges:
verts.append(f"d{i}")
for i, j in sorted(self.bites):
verts.append(f"p{i}_{j}")
return verts
def edges(self) -> list[tuple[str, str]]:
n = self.n
out: list[tuple[str, str]] = []
# annular cycle A(T)
for k in range(n):
out.append((f"a{k}", f"a{(k + 1) % n}"))
# singleton teeth (up and down): two spokes each
for i in self.up_edges:
out += [(f"u{i}", f"a{i}"), (f"u{i}", f"a{(i + 1) % n}")]
for i in self.singleton_down_edges:
out += [(f"d{i}", f"a{i}"), (f"d{i}", f"a{(i + 1) % n}")]
# bites: a shared apex with four spokes
for i, j in sorted(self.bites):
apex = f"p{i}_{j}"
for edge in (i, j):
out += [(apex, f"a{edge}"), (apex, f"a{(edge + 1) % n}")]
return [tuple(sorted(e)) for e in out]
def canonical_key(self) -> tuple:
"""Representative under the dihedral group of the cycle (rotations and
reflections), so symmetric configurations collapse to one key."""
n = self.n
best: tuple | None = None
for a in (1, -1):
for b in range(n):
relabel = lambda i: (a * i + b) % n
word = [""] * n
for i, t in enumerate(self.tooth_word):
word[relabel(i)] = t
mapped = tuple(sorted(
tuple(sorted((relabel(i), relabel(j)))) for i, j in self.bites
))
key = (tuple(word), mapped)
if best is None or key < best:
best = key
return best
# ---------------------------------------------------------------------------
# Enumeration.
# ---------------------------------------------------------------------------
def generate(
n: int, min_up_teeth: int = 3, dedup: bool = False
) -> Iterator[FullMedialTireGraph]:
"""Yield every full medial tire graph whose annular cycle has size ``n``.
``min_up_teeth`` defaults to 3 (Remark 3.6). With ``dedup`` set, only one
representative per dihedral symmetry class is returned.
"""
seen: set[tuple] = set()
for word_tuple in itertools.product("UD", repeat=n):
tooth_word = "".join(word_tuple)
if tooth_word.count("U") < min_up_teeth:
continue
down = tuple(i for i, t in enumerate(tooth_word) if t == "D")
for bites in noncrossing_matchings(down):
if has_incident_bite(bites, n):
continue
if not satisfies_bite_face_condition(tooth_word, bites):
continue
graph = FullMedialTireGraph(n=n, tooth_word=tooth_word, bites=bites)
if dedup:
key = graph.canonical_key()
if key in seen:
continue
seen.add(key)
yield graph
# ---------------------------------------------------------------------------
# CLI.
# ---------------------------------------------------------------------------
def figure_one() -> FullMedialTireGraph:
"""The example graph of Figure 1 (Remark 3.8): 12 edges, one bite (0,6)."""
return FullMedialTireGraph(
n=12,
tooth_word="DDDDDUDUUUUU", # edges 0-4,6 down; 5,7,8,9,10,11 up
bites=frozenset({(0, 6)}),
)
def describe(graph: FullMedialTireGraph) -> str:
counts = face_singleton_counts(graph.tooth_word, graph.bites)
face_strs = []
for face, c in sorted(counts.items(), key=lambda kv: (kv[0] is not None, kv[0])):
name = "root" if face is None else f"bite{face}"
face_strs.append(f"{name}:{c}")
bites = ",".join(f"({i},{j})" for i, j in sorted(graph.bites)) or "-"
faces = " ".join(face_strs) or "-"
return (
f"word={graph.tooth_word} up={len(graph.up_edges)} "
f"down={len(graph.down_edges)} bites={bites} faces[{faces}]"
)
def run(args: argparse.Namespace) -> None:
if args.check_figure:
g = figure_one()
print("Figure 1 check:")
print(f" {describe(g)}")
ok = satisfies_bite_face_condition(g.tooth_word, g.bites)
print(f" satisfies Remark 3.8: {ok} (expect True; faces 4 and 0)")
print()
for n in range(args.min_n, args.max_n + 1):
graphs = list(generate(n, min_up_teeth=args.min_up, dedup=args.dedup))
label = "classes" if args.dedup else "graphs"
print(f"n={n}: {len(graphs)} {label}")
if args.show:
for g in graphs[: args.show]:
print(f" {describe(g)}")
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--min-n", type=int, default=3)
parser.add_argument("--max-n", type=int, default=8)
parser.add_argument("--min-up", type=int, default=3, help="Remark 3.6 bound")
parser.add_argument("--dedup", action="store_true",
help="reduce modulo dihedral symmetry of the cycle")
parser.add_argument("--show", type=int, default=0,
help="print up to this many graphs per n")
parser.add_argument("--check-figure", action="store_true",
help="verify the Figure 1 example against Remark 3.8")
run(parser.parse_args())
if __name__ == "__main__":
@@ -1,17 +0,0 @@
# Random medial tire decomposition 1
- original vertices: 30
- original edges: 84
- original node connectivity: 5
- augmented vertices: 31
- augmented edges: 87
- same-level faces filled: 1
- source vertex: 9
- tire-tree nodes: 3
- tire-tree edges: 2
| node | depth | faces | annular cycles | annular | up | singleton down | bite apexes |
|--:|--:|--:|--:|--:|--:|--:|--:|
| T0 | 1 | 16 | 1 | 16 | 6 | 10 | 0 |
| T1 | 2 | 20 | 1 | 20 | 10 | 10 | 0 |
| T2 | 3 | 16 | 3 | 16 | 12 | 0 | 1 |
@@ -1,18 +0,0 @@
# Random medial tire decomposition 2
- original vertices: 30
- original edges: 84
- original node connectivity: 5
- augmented vertices: 32
- augmented edges: 90
- same-level faces filled: 2
- source vertex: 4
- tire-tree nodes: 4
- tire-tree edges: 3
| node | depth | faces | annular cycles | annular | up | singleton down | bite apexes |
|--:|--:|--:|--:|--:|--:|--:|--:|
| T0 | 1 | 16 | 1 | 16 | 7 | 9 | 0 |
| T1 | 2 | 17 | 1 | 17 | 9 | 8 | 0 |
| T2 | 3 | 14 | 1 | 14 | 8 | 4 | 1 |
| T3 | 4 | 6 | 2 | 6 | 5 | 0 | 0 |
@@ -19,25 +19,14 @@ is a bite.
from __future__ import annotations
import os
import itertools
import random
import sys
from collections import defaultdict
import networkx as nx
import numpy as np
import scipy.spatial
PAPER_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if PAPER_DIR not in sys.path:
sys.path.insert(0, PAPER_DIR)
from lib.medial_tire_decomposition import (
ekey,
extract_tread,
medial_tire_facemodel,
recognise,
)
def random_sphere_triangulation(n: int, seed: int) -> nx.Graph:
"""A random maximal planar graph: convex hull of random points on S^2."""
@@ -51,6 +40,19 @@ def random_sphere_triangulation(n: int, seed: int) -> nx.Graph:
return g
def medial_tire_facemodel(tread_faces) -> nx.Graph:
"""Full medial tire graph M(T): the ambient tread-face model. Each tread
face contributes a triangle on its three edge-medial-vertices; no medial
edges between two same-boundary edges (those come from non-tread corners)."""
Mt = nx.Graph()
for f in tread_faces:
es = [ekey(f[0], f[1]), ekey(f[1], f[2]), ekey(f[2], f[0])]
Mt.add_nodes_from(es)
for a in range(3):
Mt.add_edge(es[a], es[(a + 1) % 3])
return Mt
# --------------------------------------------------------------------------- #
# Maximal planar graph on n vertices: stacked seed + random diagonal flips.
# --------------------------------------------------------------------------- #
@@ -87,6 +89,10 @@ def random_maximal_planar(n: int, seed: int, flips: int = 400) -> nx.Graph:
return g
def ekey(u, v):
return (u, v) if (str(u), u) <= (str(v), v) else (v, u)
def triangular_faces(g: nx.Graph):
ok, emb = nx.check_planarity(g)
if not ok:
@@ -140,12 +146,147 @@ def proper_3_colorings(M: nx.Graph, limit: int):
# --------------------------------------------------------------------------- #
# Classify colourings of recognised full medial tire graphs.
# Tread extraction from a BFS-level decomposition.
# --------------------------------------------------------------------------- #
def extract_tread(faces, levels, d):
"""Tread T_d: faces spanning levels {d, d+1}. Returns its edge classes."""
tread_faces = []
for f in faces:
lv = [levels[x] for x in f]
if min(lv) == d and max(lv) == d + 1:
tread_faces.append(f)
if not tread_faces:
return None
annular, up, down = set(), set(), set()
face_of_down = defaultdict(int)
for f in tread_faces:
for x, y in ((f[0], f[1]), (f[1], f[2]), (f[2], f[0])):
e = ekey(x, y)
lx, ly = levels[x], levels[y]
if {lx, ly} == {d, d + 1}:
annular.add(e)
elif lx == ly == d:
up.add(e)
elif lx == ly == d + 1:
down.add(e)
face_of_down[e] += 1
bites = {e for e in down if face_of_down[e] == 2}
return {
"tread_faces": tread_faces,
"annular": annular, "up": up, "down": down, "bites": bites,
}
def annular_cycle_order(M: nx.Graph, annular: set):
"""Cyclic order of the annular medial vertices (they induce a cycle)."""
sub = M.subgraph(annular)
if sub.number_of_nodes() == 0 or any(sub.degree(v) != 2 for v in sub):
return None
if not nx.is_connected(sub):
return None
start = next(iter(annular))
order = [start]
prev = None
cur = start
while True:
nbrs = [w for w in sub.neighbors(cur) if w != prev]
if not nbrs:
break
nxt = nbrs[0]
if nxt == start:
break
order.append(nxt)
prev, cur = cur, nxt
return order if len(order) == len(annular) else None
# --------------------------------------------------------------------------- #
# Recognise a genuine tread as a FullMedialTireGraph and classify colourings.
# --------------------------------------------------------------------------- #
from full_medial_tire_generator import FullMedialTireGraph
from kempe_valid_colorings import classify as kempe_classify
def _linear_cut(n, bite_pairs):
"""Rotate the cycle so the bite pairs become linear non-crossing intervals."""
for r in range(n):
rel = [tuple(sorted(((i - r) % n, (j - r) % n))) for i, j in bite_pairs]
ok = True
for a, b in rel:
for c, d in rel:
if (a, b) != (c, d) and (a < c < b < d or c < a < d < b):
ok = False
break
if not ok:
break
if ok:
return r, rel
return None
def recognise(M, tread):
"""Return (FullMedialTireGraph, bijection fmt-name -> medial vertex) or None.
``M`` here is the tread-face model M(T) (cycle + teeth + bites)."""
annular = tread["annular"]
order = annular_cycle_order(M, annular)
if order is None or len(order) < 3:
return None
n = len(order)
ann_set = set(annular)
apex_of_edge = []
for i in range(n):
a, b = order[i], order[(i + 1) % n]
common = [w for w in set(M.neighbors(a)) & set(M.neighbors(b)) if w not in ann_set]
if len(common) != 1:
return None
apex_of_edge.append(common[0])
up = set(tread["up"])
# bite apex: serves two cycle edges (== adjacent to four annular vertices)
apex_positions = defaultdict(list)
for i, ap in enumerate(apex_of_edge):
apex_positions[ap].append(i)
tooth = []
bite_pairs = []
for ap, positions in apex_positions.items():
if len(positions) == 2:
bite_pairs.append(tuple(sorted(positions)))
for i, ap in enumerate(apex_of_edge):
tooth.append("U" if ap in up else "D")
cut = _linear_cut(n, bite_pairs)
if cut is None:
return None
r, rel_bites = cut
word = [""] * n
for i in range(n):
word[(i - r) % n] = tooth[i]
g = FullMedialTireGraph(n=n, tooth_word="".join(word), bites=frozenset(rel_bites))
# bijection fmt-name -> medial vertex
bij = {}
for k in range(n):
bij[f"a{k}"] = order[(k + r) % n]
for i in g.up_edges:
bij[f"u{i}"] = apex_of_edge[(i + r) % n]
for i in g.singleton_down_edges:
bij[f"d{i}"] = apex_of_edge[(i + r) % n]
for (i, j) in sorted(g.bites):
bij[f"p{i}_{j}"] = apex_of_edge[(i + r) % n]
# verify the reconstructed graph is edge-faithful to the tread-face M(T)
mt_edges = {ekey(*e) for e in M.edges()}
rec_edges = {ekey(bij[u], bij[v]) for u, v in g.edges()}
if rec_edges != mt_edges:
return None
return g, bij
def canonical(coloring, ordered):
remap, out = {}, []
for v in ordered:
@@ -200,29 +341,32 @@ def iter_pieces(seed: int, color_limit: int = 400000):
if tread is None or len(tread["up"]) < 3:
continue
mt = medial_tire_facemodel(tread["tread_faces"])
for comp, (g, bij) in enumerate(recognise(mt, tread)):
mt_nodes = list(bij.values())
name_of = {v: k for k, v in bij.items()}
rec = recognise(mt, tread)
if rec is None:
continue
g, bij = rec
mt_nodes = list(bij.values())
name_of = {v: k for k, v in bij.items()}
realized = set()
for col in global_colorings:
realized.add(canonical({v: col[v] for v in mt_nodes}, mt_nodes))
realized = set()
for col in global_colorings:
realized.add(canonical({v: col[v] for v in mt_nodes}, mt_nodes))
colorings = []
seen = set()
for col in proper_3_colorings_subgraph(mt, mt_nodes):
key = canonical(col, mt_nodes)
if key in seen:
continue
seen.add(key)
fmt_col = {name_of[v]: c for v, c in col.items()}
balanced = kempe_classify(g, fmt_col).valid
is_real = key in realized
cat = ("Invalid" if not balanced
else "Realized" if is_real else "Unrealized")
colorings.append((fmt_col, cat))
meta = {"source": s, "tread": d, "comp": comp}
yield (meta, g, colorings)
colorings = []
seen = set()
for col in proper_3_colorings_subgraph(mt, mt_nodes):
key = canonical(col, mt_nodes)
if key in seen:
continue
seen.add(key)
fmt_col = {name_of[v]: c for v, c in col.items()}
balanced = kempe_classify(g, fmt_col).valid
is_real = key in realized
cat = ("Invalid" if not balanced
else "Realized" if is_real else "Unrealized")
colorings.append((fmt_col, cat))
meta = {"source": s, "tread": d}
yield (meta, g, colorings)
def analyse(seed: int, color_limit: int = 400000):
@@ -1 +0,0 @@
"""Reusable helpers for medial tire decomposition experiments."""
@@ -1,860 +0,0 @@
"""Draw medial tire decompositions of random 5-connected triangulations.
The source graphs come from ``plantri -c5`` in graph6 format. For each sampled
30-vertex triangulation, this script chooses a random source vertex, builds the
BFS depth-component tire tree, and draws both the tire tree and the medial
tread model for each depth component.
"""
from __future__ import annotations
import argparse
import math
import os
import random
import subprocess
import sys
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path
PAPER_DIR = Path(__file__).resolve().parents[1]
REPO_ROOT = PAPER_DIR.parents[1]
os.environ.setdefault(
"MPLCONFIGDIR", str(PAPER_DIR / "experiments" / ".matplotlib-cache")
)
os.environ.setdefault("XDG_CACHE_HOME", str(PAPER_DIR / "experiments" / ".cache"))
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
from matplotlib.patches import PathPatch
from matplotlib.path import Path as MplPath
import networkx as nx
if str(PAPER_DIR) not in sys.path:
sys.path.insert(0, str(PAPER_DIR))
from lib.medial_tire_decomposition import (
annular_cycle_components,
ekey,
medial_tire_facemodel,
)
@dataclass(frozen=True)
class TreadNode:
idx: int
depth: int
face_indices: tuple[int, ...]
annular: frozenset
up: frozenset
down: frozenset
bites: frozenset
medial: nx.Graph
annular_cycles: tuple[tuple, ...]
@dataclass(frozen=True)
class Augmentation:
graph: nx.Graph
added_vertices: tuple[int, ...]
filled_faces: tuple[tuple[int, tuple[int, int, int], int], ...]
def sample_plantri_graphs(n: int, count: int, seed: int, scan_limit: int) -> list[nx.Graph]:
cmd = [str(REPO_ROOT / "plantri" / "plantri"), "-g", "-c5", str(n)]
rng = random.Random(seed)
sample: list[tuple[int, nx.Graph]] = []
seen = 0
with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc:
assert proc.stdout is not None
for raw in proc.stdout:
line = raw.strip()
if not line or line.startswith(b">>"):
continue
graph = nx.from_graph6_bytes(line)
if nx.node_connectivity(graph) < 5:
continue
seen += 1
if len(sample) < count:
sample.append((seen, graph))
else:
j = rng.randrange(seen)
if j < count:
sample[j] = (seen, graph)
if seen >= scan_limit:
proc.terminate()
break
proc.wait(timeout=10)
if len(sample) < count:
raise RuntimeError(f"only found {len(sample)} graphs after scanning {seen}")
return [graph for _ordinal, graph in sample]
def triangular_faces(g: nx.Graph):
ok, emb = nx.check_planarity(g)
if not ok:
raise ValueError("not planar")
seen = set()
faces = []
for u, v in list(emb.edges()):
if (u, v) in seen:
continue
face = tuple(emb.traverse_face(u, v, mark_half_edges=seen))
faces.append(face)
return faces
def edge_face_data(faces):
face_edges = []
edge_faces: dict[tuple, list[int]] = defaultdict(list)
for i, face in enumerate(faces):
edges = {
ekey(face[0], face[1]),
ekey(face[1], face[2]),
ekey(face[2], face[0]),
}
face_edges.append(edges)
for edge in edges:
edge_faces[edge].append(i)
return face_edges, edge_faces
def augment_same_level_faces(g: nx.Graph, source: int) -> Augmentation:
"""Stack a new vertex into every facial triangle with one BFS level.
If a triangular face has all three vertices at level d, the new vertex is
adjacent to those three vertices and therefore has level d + 1. This turns
the same-level region into three adjacent-level tread faces before the tire
decomposition is extracted.
"""
levels = nx.single_source_shortest_path_length(g, source)
faces = triangular_faces(g)
augmented = g.copy()
next_vertex = max(augmented.nodes()) + 1
added = []
filled = []
for face in faces:
face_levels = {levels[v] for v in face}
if len(face_levels) != 1:
continue
new_vertex = next_vertex
next_vertex += 1
augmented.add_node(new_vertex)
augmented.add_edges_from((new_vertex, v) for v in face)
added.append(new_vertex)
filled.append((new_vertex, tuple(face), next(iter(face_levels))))
return Augmentation(
graph=augmented,
added_vertices=tuple(added),
filled_faces=tuple(filled),
)
def depth_components(faces, face_edges, edge_faces, levels):
depths = [min(levels[v] for v in face) for face in faces]
dual_adj: dict[int, set[int]] = defaultdict(set)
for incident in edge_faces.values():
for a in range(len(incident)):
for b in range(a + 1, len(incident)):
dual_adj[incident[a]].add(incident[b])
dual_adj[incident[b]].add(incident[a])
comps = []
seen = [False] * len(faces)
for start in range(len(faces)):
if seen[start]:
continue
depth = depths[start]
stack = [start]
comp = []
seen[start] = True
while stack:
face = stack.pop()
comp.append(face)
for other in dual_adj[face]:
if not seen[other] and depths[other] == depth:
seen[other] = True
stack.append(other)
comps.append((depth, tuple(sorted(comp))))
return comps, depths, dual_adj
def tread_from_component(faces, levels, face_indices):
tread_faces = [faces[i] for i in face_indices]
if not tread_faces:
return None
depth = min(min(levels[v] for v in face) for face in tread_faces)
annular, up, down = set(), set(), set()
face_of_down = defaultdict(int)
for face in tread_faces:
for x, y in ((face[0], face[1]), (face[1], face[2]), (face[2], face[0])):
e = ekey(x, y)
lx, ly = levels[x], levels[y]
if {lx, ly} == {depth, depth + 1}:
annular.add(e)
elif lx == ly == depth:
up.add(e)
elif lx == ly == depth + 1:
down.add(e)
face_of_down[e] += 1
if len(annular) < 3:
return None
return {
"tread_faces": tread_faces,
"annular": annular,
"up": up,
"down": down,
"bites": {e for e in down if face_of_down[e] == 2},
}
def build_tire_tree(g: nx.Graph, source: int, augment: bool = True):
augmentation = augment_same_level_faces(g, source) if augment else Augmentation(g, (), ())
work_graph = augmentation.graph
faces = triangular_faces(work_graph)
face_edges, edge_faces = edge_face_data(faces)
levels = nx.single_source_shortest_path_length(work_graph, source)
comps, depths, dual_adj = depth_components(faces, face_edges, edge_faces, levels)
comp_of_face = {}
for comp_idx, (_depth, face_indices) in enumerate(comps):
for face in face_indices:
comp_of_face[face] = comp_idx
nodes: list[TreadNode] = []
comp_to_node = {}
for comp_idx, (depth, face_indices) in enumerate(comps):
tread = tread_from_component(faces, levels, face_indices)
if tread is None or len(tread["up"]) < 3:
continue
mt = medial_tire_facemodel(tread["tread_faces"])
annular_cycles = tuple(annular_cycle_components(mt, tread["annular"]))
if not annular_cycles:
continue
node = TreadNode(
idx=len(nodes),
depth=depth,
face_indices=face_indices,
annular=frozenset(tread["annular"]),
up=frozenset(tread["up"]),
down=frozenset(tread["down"]),
bites=frozenset(tread["bites"]),
medial=mt,
annular_cycles=annular_cycles,
)
comp_to_node[comp_idx] = node.idx
nodes.append(node)
tree_edges = set()
for comp_idx, (depth, face_indices) in enumerate(comps):
if comp_idx not in comp_to_node:
continue
child = comp_to_node[comp_idx]
parent_candidates = set()
for face in face_indices:
for other in dual_adj[face]:
other_comp = comp_of_face[other]
if depths[other] == depth - 1 and other_comp in comp_to_node:
parent_candidates.add(comp_to_node[other_comp])
for parent in parent_candidates:
tree_edges.add((parent, child))
return augmentation, faces, levels, nodes, sorted(tree_edges)
def graph_layout(g: nx.Graph):
try:
return nx.planar_layout(g)
except nx.NetworkXException:
return nx.spring_layout(g, seed=0)
def draw_base_graph(ax, g, levels, source, added_vertices=()):
pos = graph_layout(g)
max_level = max(levels.values())
cmap = plt.get_cmap("viridis", max_level + 1)
node_colors = [cmap(levels[v]) for v in g.nodes()]
nx.draw_networkx_edges(g, pos, ax=ax, edge_color="#cbd5e1", width=0.8)
added_set = set(added_vertices)
nx.draw_networkx_nodes(
g,
pos,
ax=ax,
node_color=node_colors,
node_size=[
150 if v == source else 96 if v in added_set else 72
for v in g.nodes()
],
edgecolors=[
"#dc2626" if v == source else "#7c3aed" if v in added_set else "#111827"
for v in g.nodes()
],
linewidths=[
1.8 if v == source else 1.2 if v in added_set else 0.45
for v in g.nodes()
],
)
labels = {v: str(v) for v in g.nodes()}
nx.draw_networkx_labels(g, pos, labels=labels, ax=ax, font_size=5)
ax.set_title(
f"Augmented G, source {source}; vertex levels 0..{max_level}",
fontsize=10,
)
ax.set_aspect("equal")
ax.axis("off")
def tree_positions(nodes: list[TreadNode], tree_edges):
children: dict[int, list[int]] = defaultdict(list)
has_parent = set()
for parent, child in tree_edges:
children[parent].append(child)
has_parent.add(child)
roots = [node.idx for node in nodes if node.idx not in has_parent]
for child_list in children.values():
child_list.sort(key=lambda idx: (nodes[idx].depth, idx))
x_counter = 0
pos = {}
def place(idx, depth):
nonlocal x_counter
if not children[idx]:
pos[idx] = (x_counter, -depth)
x_counter += 1
return pos[idx][0]
xs = [place(child, depth + 1) for child in children[idx]]
x = sum(xs) / len(xs)
pos[idx] = (x, -depth)
return x
for root in sorted(roots, key=lambda idx: (nodes[idx].depth, idx)):
place(root, 0)
x_counter += 1
return pos
def draw_tire_tree(ax, nodes: list[TreadNode], tree_edges):
pos = tree_positions(nodes, tree_edges)
for parent, child in tree_edges:
x0, y0 = pos[parent]
x1, y1 = pos[child]
ax.plot([x0, x1], [y0, y1], color="#374151", lw=1.0, zorder=1)
for node in nodes:
x, y = pos[node.idx]
ax.text(
x,
y,
f"T{node.idx}\nd={node.depth}\n{len(node.annular_cycles)} cycle(s)",
ha="center",
va="center",
fontsize=8,
bbox={
"boxstyle": "round,pad=0.32",
"facecolor": "#fef3c7",
"edgecolor": "#111827",
"linewidth": 0.9,
},
zorder=3,
)
ax.set_title("Depth-component tire tree", fontsize=10)
if pos:
xs = [p[0] for p in pos.values()]
ys = [p[1] for p in pos.values()]
ax.set_xlim(min(xs) - 1.0, max(xs) + 1.0)
ax.set_ylim(min(ys) - 0.7, max(ys) + 0.7)
ax.axis("off")
def vertex_xy(k: int, n: int, radius: float, rotation: float = 0.0) -> tuple[float, float]:
angle = math.pi / 2 + rotation - 2 * math.pi * k / n
return radius * math.cos(angle), radius * math.sin(angle)
def edge_midpoint_angle(i: int, n: int, rotation: float = 0.0) -> float:
return math.pi / 2 + rotation - 2 * math.pi * (i + 0.5) / n
def annular_cycle_edges(node: TreadNode) -> set[tuple]:
edges = set()
for order in node.annular_cycles:
for i, a in enumerate(order):
b = order[(i + 1) % len(order)]
edges.add(tuple(sorted((a, b))))
return edges
def shared_up_apex_occurrences(node: TreadNode) -> dict[tuple, list[tuple[int, int]]]:
occurrences: dict[tuple, list[tuple[int, int]]] = defaultdict(list)
annular = set(node.annular)
for cycle_idx, order in enumerate(node.annular_cycles):
n = len(order)
for i, a in enumerate(order):
b = order[(i + 1) % n]
apexes = [
w for w in set(node.medial.neighbors(a)) & set(node.medial.neighbors(b))
if w not in annular
]
for apex in apexes:
if apex in node.up:
occurrences[apex].append((cycle_idx, i))
return {apex: where for apex, where in occurrences.items() if len(where) > 1}
def compound_cycle_rotations(node: TreadNode) -> list[float]:
rotations = [0.0] * len(node.annular_cycles)
shared = shared_up_apex_occurrences(node)
by_cycle: dict[int, list[int]] = defaultdict(list)
for occurrences in shared.values():
for cycle_idx, edge_idx in occurrences:
by_cycle[cycle_idx].append(edge_idx)
for cycle_idx, edge_indices in by_cycle.items():
n = len(node.annular_cycles[cycle_idx])
sx = sy = 0.0
for edge_idx in edge_indices:
angle = edge_midpoint_angle(edge_idx, n)
sx += math.cos(angle)
sy += math.sin(angle)
if sx or sy:
rotations[cycle_idx] = math.pi / 2 - math.atan2(sy, sx)
return rotations
def compound_cycle_order(node: TreadNode) -> list[int]:
shared = shared_up_apex_occurrences(node)
adj: dict[int, set[int]] = defaultdict(set)
for occurrences in shared.values():
cycles = sorted({cycle_idx for cycle_idx, _edge_idx in occurrences})
for a, b in zip(cycles, cycles[1:]):
adj[a].add(b)
adj[b].add(a)
remaining = set(range(len(node.annular_cycles)))
order = []
while remaining:
candidates = sorted(
remaining,
key=lambda idx: (len(adj[idx]) if adj[idx] else 999, idx),
)
start = candidates[0]
stack = [(start, None)]
while stack:
current, parent = stack.pop()
if current not in remaining:
continue
remaining.remove(current)
order.append(current)
children = sorted(
(neighbor for neighbor in adj[current] if neighbor != parent),
key=lambda idx: (len(adj[idx]), idx),
reverse=True,
)
for child in children:
stack.append((child, current))
return order
def cycle_layout(
node: TreadNode,
rotations: list[float],
display_order: list[int] | None = None,
cycle_spacing: float = 3.25,
):
cycle_count = len(node.annular_cycles)
if display_order is None:
display_order = list(range(cycle_count))
rank = {cycle_idx: i for i, cycle_idx in enumerate(display_order)}
offsets = [cycle_spacing * (rank[i] - (cycle_count - 1) / 2) for i in range(cycle_count)]
ann_positions: dict[tuple[int, tuple], tuple[float, float]] = {}
apex_positions: dict[tuple[int, tuple], tuple[float, float]] = {}
apex_corners: dict[tuple[int, tuple], list[tuple[float, float]]] = defaultdict(list)
for cycle_idx, order in enumerate(node.annular_cycles):
n = len(order)
dx = offsets[cycle_idx]
rotation = rotations[cycle_idx]
for k, vertex in enumerate(order):
x, y = vertex_xy(k, n, 1.0, rotation)
ann_positions[(cycle_idx, vertex)] = (dx + x, y)
for i, a in enumerate(order):
b = order[(i + 1) % n]
apexes = [
w for w in set(node.medial.neighbors(a)) & set(node.medial.neighbors(b))
if w not in node.annular
]
for apex in apexes:
key = (cycle_idx, apex)
apex_corners[key].extend([
ann_positions[(cycle_idx, a)],
ann_positions[(cycle_idx, b)],
])
if key in apex_positions:
continue
angle = edge_midpoint_angle(i, n, rotation)
radius = 1.42 if apex in node.up else 0.58
apex_positions[key] = (
dx + radius * math.cos(angle),
radius * math.sin(angle),
)
for key, corners in apex_corners.items():
_cycle_idx, apex = key
if apex in node.bites and corners:
cx = sum(p[0] for p in corners) / len(corners)
cy = sum(p[1] for p in corners) / len(corners)
apex_positions[key] = (0.82 * cx, 0.82 * cy)
return offsets, ann_positions, apex_positions, apex_corners
def orientation(a, b, c) -> float:
return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
def segments_cross(a, b, c, d) -> bool:
eps = 1e-9
if max(a[0], b[0]) + eps < min(c[0], d[0]):
return False
if max(c[0], d[0]) + eps < min(a[0], b[0]):
return False
if max(a[1], b[1]) + eps < min(c[1], d[1]):
return False
if max(c[1], d[1]) + eps < min(a[1], b[1]):
return False
o1 = orientation(a, b, c)
o2 = orientation(a, b, d)
o3 = orientation(c, d, a)
o4 = orientation(c, d, b)
return (o1 * o2 < -eps) and (o3 * o4 < -eps)
def polylines_cross(first, second) -> bool:
for i in range(len(first) - 1):
for j in range(len(second) - 1):
if segments_cross(first[i], first[i + 1], second[j], second[j + 1]):
return True
return False
def quadratic_points(start, control, end, samples: int = 40):
points = []
for k in range(samples + 1):
t = k / samples
x = (1 - t) ** 2 * start[0] + 2 * (1 - t) * t * control[0] + t ** 2 * end[0]
y = (1 - t) ** 2 * start[1] + 2 * (1 - t) * t * control[1] + t ** 2 * end[1]
points.append((x, y))
return points
def crossing_free_shared_arcs(
node: TreadNode,
apex_positions,
top_y: float,
bottom_y: float,
):
shared = shared_up_apex_occurrences(node)
arc_specs = []
routed = []
pairs = []
for apex, occurrences in shared.items():
ordered = sorted(occurrences, key=lambda item: apex_positions[(item[0], apex)][0])
for left, right in zip(ordered, ordered[1:]):
pairs.append((apex, left, right))
pairs.sort(key=lambda item: (
abs(apex_positions[(item[1][0], item[0])][0] - apex_positions[(item[2][0], item[0])][0]),
min(item[1][0], item[2][0]),
item[0],
))
for arc_idx, (apex, left, right) in enumerate(pairs):
start = apex_positions[(left[0], apex)]
end = apex_positions[(right[0], apex)]
if start[0] > end[0]:
start, end = end, start
for lane in range(64):
candidates = [
((start[0] + end[0]) / 2, top_y + 0.42 + 0.18 * lane),
((start[0] + end[0]) / 2, bottom_y - 0.42 - 0.18 * lane),
]
for control in candidates:
points = quadratic_points(start, control, end)
if not any(polylines_cross(points, existing) for existing in routed):
routed.append(points)
arc_specs.append((start, control, end))
break
else:
continue
break
else:
control = ((start[0] + end[0]) / 2, top_y + 0.42 + 0.18 * (64 + arc_idx))
routed.append(quadratic_points(start, control, end))
arc_specs.append((start, control, end))
return arc_specs
def draw_tread_cycles(ax, node: TreadNode, connect_shared: bool):
rotations = compound_cycle_rotations(node) if connect_shared else [0.0] * len(node.annular_cycles)
display_order = compound_cycle_order(node) if connect_shared else None
offsets, ann_positions, apex_positions, apex_corners = cycle_layout(
node, rotations, display_order=display_order
)
for cycle_idx, order in enumerate(node.annular_cycles):
cyc_x = [ann_positions[(cycle_idx, v)][0] for v in order] + [
ann_positions[(cycle_idx, order[0])][0]
]
cyc_y = [ann_positions[(cycle_idx, v)][1] for v in order] + [
ann_positions[(cycle_idx, order[0])][1]
]
ax.plot(cyc_x, cyc_y, color="black", lw=1.3, zorder=2)
for key, corners in apex_corners.items():
pos = apex_positions[key]
for corner in corners:
ax.plot([pos[0], corner[0]], [pos[1], corner[1]], color="#9ca3af", lw=0.5)
if connect_shared and apex_positions:
all_positions = list(ann_positions.values()) + list(apex_positions.values())
top_y = max(p[1] for p in all_positions)
bottom_y = min(p[1] for p in all_positions)
for start, control, end in crossing_free_shared_arcs(
node, apex_positions, top_y, bottom_y
):
path = MplPath([start, control, end], [MplPath.MOVETO, MplPath.CURVE3, MplPath.CURVE3])
ax.add_patch(
PathPatch(
path,
facecolor="none",
edgecolor="#475569",
lw=0.8,
linestyle=(0, (1.2, 2.0)),
zorder=1,
)
)
for (_cycle_idx, apex), pos in apex_positions.items():
if apex in node.up:
color, size, edgecolor = "#2563eb", 13, "none"
elif apex in node.bites:
color, size, edgecolor = "#7f1d1d", 24, "black"
else:
color, size, edgecolor = "#dc2626", 13, "none"
ax.scatter(
[pos[0]],
[pos[1]],
s=size,
color=color,
edgecolors=edgecolor,
linewidths=0.4,
zorder=3,
)
if ann_positions:
ax.scatter(
[p[0] for p in ann_positions.values()],
[p[1] for p in ann_positions.values()],
s=9,
color="black",
zorder=4,
)
xs = [p[0] for p in list(ann_positions.values()) + list(apex_positions.values())]
ys = [p[1] for p in list(ann_positions.values()) + list(apex_positions.values())]
if connect_shared:
ys.append(max(ys) + 1.2)
ys.append(min(ys) - 1.2)
xpad = 1.1 if connect_shared else 1.7
ypad = 0.25 if connect_shared else 0.0
ax.set_xlim(min(xs, default=min(offsets, default=0.0)) - xpad, max(xs, default=max(offsets, default=0.0)) + xpad)
ax.set_ylim(min(ys, default=-1.65) - 0.25, max(ys, default=1.65) + ypad)
ax.set_aspect("equal")
ax.axis("off")
def draw_tread_model(ax, node: TreadNode):
if len(node.annular_cycles) > 1:
draw_tread_cycles(ax, node, connect_shared=True)
singleton_down = set(node.down) - set(node.bites)
ax.set_title(
f"T{node.idx} d={node.depth}: {len(node.annular_cycles)} annular cycle(s)\n"
f"ann={len(node.annular)} up={len(node.up)} down={len(singleton_down)} "
f"bite={len(node.bites)}",
fontsize=6.4,
pad=1.5,
)
return
draw_tread_cycles(ax, node, connect_shared=False)
singleton_down = set(node.down) - set(node.bites)
ax.set_title(
f"T{node.idx} d={node.depth}: {len(node.annular_cycles)} annular cycle(s)\n"
f"ann={len(node.annular)} up={len(node.up)} down={len(singleton_down)} "
f"bite={len(node.bites)}",
fontsize=6.4,
pad=1.5,
)
def draw_medial_tire_grid(fig, outer_spec, nodes):
if not nodes:
ax = fig.add_subplot(outer_spec)
ax.text(0.5, 0.5, "No medial treads extracted", ha="center")
ax.axis("off")
return
cols = min(3, max(1, math.ceil(math.sqrt(len(nodes)))))
rows = math.ceil(len(nodes) / cols)
sub = outer_spec.subgridspec(rows, cols, wspace=0.08, hspace=0.35)
for i in range(rows * cols):
ax = fig.add_subplot(sub[i // cols, i % cols])
if i < len(nodes):
draw_tread_model(ax, nodes[i])
else:
ax.axis("off")
def write_index(
path: Path,
graph_idx: int,
source: int,
original_graph: nx.Graph,
augmentation: Augmentation,
nodes,
tree_edges,
):
g = augmentation.graph
lines = [
f"# Random medial tire decomposition {graph_idx}",
"",
f"- original vertices: {original_graph.number_of_nodes()}",
f"- original edges: {original_graph.number_of_edges()}",
f"- original node connectivity: {nx.node_connectivity(original_graph)}",
f"- augmented vertices: {g.number_of_nodes()}",
f"- augmented edges: {g.number_of_edges()}",
f"- same-level faces filled: {len(augmentation.added_vertices)}",
f"- source vertex: {source}",
f"- tire-tree nodes: {len(nodes)}",
f"- tire-tree edges: {len(tree_edges)}",
"",
"| node | depth | faces | annular cycles | annular | up | singleton down | bite apexes |",
"|--:|--:|--:|--:|--:|--:|--:|--:|",
]
for node in nodes:
singleton_down = set(node.down) - set(node.bites)
lines.append(
f"| T{node.idx} | {node.depth} | {len(node.face_indices)} | "
f"{len(node.annular_cycles)} | {len(node.annular)} | {len(node.up)} | "
f"{len(singleton_down)} | {len(node.bites)} |"
)
path.write_text("\n".join(lines) + "\n")
def draw_case(out_dir: Path, graph_idx: int, g: nx.Graph, source: int, augment: bool = True):
augmentation, _faces, levels, nodes, tree_edges = build_tire_tree(g, source, augment=augment)
work_graph = augmentation.graph
fig = plt.figure(figsize=(17, 10))
spec = fig.add_gridspec(2, 2, width_ratios=[1.15, 1.0], height_ratios=[1.0, 1.25])
ax_graph = fig.add_subplot(spec[0, 0])
ax_tree = fig.add_subplot(spec[1, 0])
draw_base_graph(ax_graph, work_graph, levels, source, augmentation.added_vertices)
draw_tire_tree(ax_tree, nodes, tree_edges)
draw_medial_tire_grid(fig, spec[:, 1], nodes)
fig.suptitle(
f"Random 5-connected maximal planar graph {graph_idx}: "
f"n={g.number_of_nodes()} (+{len(augmentation.added_vertices)}), "
f"source={source}",
fontsize=13,
)
legend = [
Line2D([0], [0], marker="o", color="w", label="source",
markerfacecolor="#fde68a", markeredgecolor="#dc2626", markersize=8),
Line2D([0], [0], marker="o", color="w", label="inserted vertex",
markerfacecolor="#fde68a", markeredgecolor="#7c3aed", markersize=8),
Line2D([0], [0], color="black", lw=1.3, label="annular cycle A(T)"),
Line2D([0], [0], color="#475569", lw=0.8, linestyle=(0, (1.2, 2.0)),
label="shared up apex"),
Line2D([0], [0], marker="o", color="w", label="up tooth",
markerfacecolor="#2563eb", markersize=6),
Line2D([0], [0], marker="o", color="w", label="down tooth",
markerfacecolor="#dc2626", markersize=6),
Line2D([0], [0], marker="o", color="w", label="bite apex",
markerfacecolor="#7f1d1d", markeredgecolor="black", markersize=6),
]
fig.legend(handles=legend, loc="lower center", ncol=6, fontsize=9)
fig.subplots_adjust(left=0.03, right=0.99, top=0.92, bottom=0.08, wspace=0.08, hspace=0.16)
png = out_dir / f"random_c5_n30_medial_tire_decomposition_{graph_idx}.png"
pdf = out_dir / f"random_c5_n30_medial_tire_decomposition_{graph_idx}.pdf"
fig.savefig(png, dpi=180)
fig.savefig(pdf)
plt.close(fig)
write_index(
out_dir / f"random_c5_n30_medial_tire_decomposition_{graph_idx}.md",
graph_idx,
source,
g,
augmentation,
nodes,
tree_edges,
)
return png, pdf, len(nodes), sum(len(node.annular_cycles) for node in nodes)
def run(args: argparse.Namespace):
out_dir = Path(args.out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
if args.graph6:
graph = nx.from_graph6_bytes(args.graph6.encode())
if nx.node_connectivity(graph) < 5:
raise ValueError("--graph6 graph must be 5-connected")
graphs = [graph]
if args.source is None:
raise ValueError("--source is required with --graph6")
sources = [args.source]
else:
graphs = sample_plantri_graphs(args.n, args.count, args.seed, args.scan_limit)
rng = random.Random(args.seed + 101)
sources = [rng.choice(list(graph.nodes())) for graph in graphs]
for i, (graph, source) in enumerate(zip(graphs, sources), start=1):
png, pdf, node_count, annular_cycle_count = draw_case(
out_dir, i, graph, source, augment=not args.no_augment_same_level_faces
)
print(
f"case {i}: source={source}, connectivity={nx.node_connectivity(graph)}, "
f"tire nodes={node_count}, annular cycles={annular_cycle_count}"
)
print(f" wrote {png}")
print(f" wrote {pdf}")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--n", type=int, default=30)
parser.add_argument("--count", type=int, default=2)
parser.add_argument("--seed", type=int, default=20260615)
parser.add_argument("--scan-limit", type=int, default=500)
parser.add_argument("--graph6", help="draw this graph6 graph instead of sampling")
parser.add_argument("--source", type=int, help="source vertex for --graph6")
parser.add_argument(
"--no-augment-same-level-faces",
action="store_true",
help="skip the same-level-face vertex insertion step",
)
parser.add_argument(
"--out-dir",
default=str(PAPER_DIR / "experiments" / "random_medial_tire_decompositions"),
)
run(parser.parse_args())
if __name__ == "__main__":
main()
@@ -1,312 +0,0 @@
"""Exhaustive generator for full medial tire graphs, indexed by |A(T)|.
Model (Definitions/Remarks 3.1--3.9 of the medial tire decompositions paper).
* The annular medial vertices induce a cycle A(T), the *annular cycle*
(Theorem 3.3). Write n = |A(T)| for its number of vertices = number of
annular faces = number of annular edges e_0,...,e_{n-1}.
* Each edge e_i of A(T) carries exactly one tooth (a triangle of M(T))
whose third vertex is a non-annular apex (Definition 3.4). A tooth is an
*up tooth* (apex in the outer region) or a *down tooth* (apex in the inner
region). We record the tooth types as a word in {U, D}^n.
* No two up teeth share an apex; at most two down teeth share an apex
(Remark 3.5). Two down teeth sharing an apex form a *bite* (Definition
3.7). So the down teeth are partitioned into singletons and bite pairs.
A bite pairs two down-edges and is drawn as an apex inside the disk with
spokes to the four endpoints; bites must be mutually non-crossing, i.e.
the bite pairs form a non-crossing (laminar) matching of the down-edges.
The two annular edges of a bite must be non-incident (Definition 3.7):
they share no annular vertex, so cyclically adjacent edges cannot pair.
* There are at least three up teeth (Remark 3.6).
* Bite-face condition (Remark 3.8). Let B(T) = A(T) together with the bite
apexes. Its interior non-tooth faces are the root face plus one inner-gap
face per bite. A singleton down tooth lies in the innermost bite enclosing
its edge (or in the root face if none). For every interior non-tooth face
the number of down-tooth apexes lying in that face must be 0 or at least 3.
Equivalently: no face holds exactly one or two singleton down teeth.
The generator enumerates, for a given n, every (tooth word, bite matching)
pair satisfying these properties and emits the resulting full medial tire
graph as an explicit vertex/edge structure. Configurations may optionally be
reduced modulo the dihedral symmetry of the cycle.
"""
from __future__ import annotations
import argparse
import itertools
from collections import defaultdict
from dataclasses import dataclass
from functools import lru_cache
from typing import Iterator
# A bite is an unordered pair of down-edge indices (i, j) with i < j.
Bite = tuple[int, int]
Matching = frozenset[Bite]
# ---------------------------------------------------------------------------
# Non-crossing (laminar) matchings of the down edges.
# ---------------------------------------------------------------------------
@lru_cache(maxsize=None)
def noncrossing_matchings(positions: tuple[int, ...]) -> tuple[Matching, ...]:
"""All non-crossing partial matchings of ``positions`` (sorted ascending).
Bite pairs drawn inside the disk are non-crossing iff, read in cyclic
order, no two pairs interleave. Cutting the cycle at the gap before the
first edge turns this into ordinary non-crossing interval matchings, which
obey the Catalan recursion below.
"""
if not positions:
return (frozenset(),)
head, *rest = positions
out: list[Matching] = []
# head left unmatched (a singleton down tooth, if its edge is down)
for tail in noncrossing_matchings(tuple(rest)):
out.append(tail)
# head matched with positions[k]; the strictly-enclosed block must be
# matched within itself to stay non-crossing.
for k in range(1, len(positions)):
partner = positions[k]
inside = tuple(positions[1:k])
outside = tuple(positions[k + 1:])
for m_in in noncrossing_matchings(inside):
for m_out in noncrossing_matchings(outside):
out.append(frozenset({(head, partner)}) | m_in | m_out)
return tuple(out)
# ---------------------------------------------------------------------------
# The bite-face condition (Remark 3.8).
# ---------------------------------------------------------------------------
def incident_edges(i: int, j: int, n: int) -> bool:
"""Whether annular edges i and j share an annular vertex on the n-cycle."""
return (j - i) % n == 1 or (i - j) % n == 1
def has_incident_bite(bites: Matching, n: int) -> bool:
"""Whether any bite pairs two incident (cyclically adjacent) edges."""
return any(incident_edges(i, j, n) for i, j in bites)
def innermost_bite(edge: int, bites: Matching) -> Bite | None:
"""The minimal-span bite whose open interval contains ``edge``, or None."""
enclosing = [b for b in bites if b[0] < edge < b[1]]
if not enclosing:
return None
return min(enclosing, key=lambda b: b[1] - b[0])
def face_singleton_counts(
tooth_word: str, bites: Matching
) -> dict[Bite | None, int]:
"""Down-singletons per interior non-tooth face of B(T).
The key ``None`` is the root face; a bite key is that bite's inner-gap
face. Faces with no singletons are simply absent from the result.
"""
matched = {edge for pair in bites for edge in pair}
counts: dict[Bite | None, int] = defaultdict(int)
for edge, tooth in enumerate(tooth_word):
if tooth != "D" or edge in matched:
continue # only singleton down teeth contribute apexes
counts[innermost_bite(edge, bites)] += 1
return dict(counts)
def satisfies_bite_face_condition(tooth_word: str, bites: Matching) -> bool:
"""Remark 3.8: every non-tooth face holds 0 or >=3 down-tooth apexes."""
return all(count >= 3 for count in face_singleton_counts(tooth_word, bites).values())
# ---------------------------------------------------------------------------
# The full medial tire graph as an explicit object.
# ---------------------------------------------------------------------------
@dataclass(frozen=True)
class FullMedialTireGraph:
"""A full medial tire graph M(T) determined by its combinatorial data.
Vertices are named:
a{k} annular medial vertex k (k = 0..n-1), forming A(T);
u{i} apex of the up tooth on edge i;
d{i} apex of the singleton down tooth on edge i;
p{i}_{j} apex of the bite pairing edges i and j (i < j).
"""
n: int
tooth_word: str
bites: Matching
@property
def up_edges(self) -> tuple[int, ...]:
return tuple(i for i, t in enumerate(self.tooth_word) if t == "U")
@property
def down_edges(self) -> tuple[int, ...]:
return tuple(i for i, t in enumerate(self.tooth_word) if t == "D")
@property
def bite_edges(self) -> frozenset[int]:
return frozenset(edge for pair in self.bites for edge in pair)
@property
def singleton_down_edges(self) -> tuple[int, ...]:
bite = self.bite_edges
return tuple(i for i in self.down_edges if i not in bite)
def apex_of_edge(self, edge: int) -> str:
if self.tooth_word[edge] == "U":
return f"u{edge}"
for i, j in self.bites:
if edge in (i, j):
return f"p{i}_{j}"
return f"d{edge}"
def vertices(self) -> list[str]:
verts = [f"a{k}" for k in range(self.n)]
for i in self.up_edges:
verts.append(f"u{i}")
for i in self.singleton_down_edges:
verts.append(f"d{i}")
for i, j in sorted(self.bites):
verts.append(f"p{i}_{j}")
return verts
def edges(self) -> list[tuple[str, str]]:
n = self.n
out: list[tuple[str, str]] = []
# annular cycle A(T)
for k in range(n):
out.append((f"a{k}", f"a{(k + 1) % n}"))
# singleton teeth (up and down): two spokes each
for i in self.up_edges:
out += [(f"u{i}", f"a{i}"), (f"u{i}", f"a{(i + 1) % n}")]
for i in self.singleton_down_edges:
out += [(f"d{i}", f"a{i}"), (f"d{i}", f"a{(i + 1) % n}")]
# bites: a shared apex with four spokes
for i, j in sorted(self.bites):
apex = f"p{i}_{j}"
for edge in (i, j):
out += [(apex, f"a{edge}"), (apex, f"a{(edge + 1) % n}")]
return [tuple(sorted(e)) for e in out]
def canonical_key(self) -> tuple:
"""Representative under the dihedral group of the cycle (rotations and
reflections), so symmetric configurations collapse to one key."""
n = self.n
best: tuple | None = None
for a in (1, -1):
for b in range(n):
relabel = lambda i: (a * i + b) % n
word = [""] * n
for i, t in enumerate(self.tooth_word):
word[relabel(i)] = t
mapped = tuple(sorted(
tuple(sorted((relabel(i), relabel(j)))) for i, j in self.bites
))
key = (tuple(word), mapped)
if best is None or key < best:
best = key
return best
# ---------------------------------------------------------------------------
# Enumeration.
# ---------------------------------------------------------------------------
def generate(
n: int, min_up_teeth: int = 3, dedup: bool = False
) -> Iterator[FullMedialTireGraph]:
"""Yield every full medial tire graph whose annular cycle has size ``n``.
``min_up_teeth`` defaults to 3 (Remark 3.6). With ``dedup`` set, only one
representative per dihedral symmetry class is returned.
"""
seen: set[tuple] = set()
for word_tuple in itertools.product("UD", repeat=n):
tooth_word = "".join(word_tuple)
if tooth_word.count("U") < min_up_teeth:
continue
down = tuple(i for i, t in enumerate(tooth_word) if t == "D")
for bites in noncrossing_matchings(down):
if has_incident_bite(bites, n):
continue
if not satisfies_bite_face_condition(tooth_word, bites):
continue
graph = FullMedialTireGraph(n=n, tooth_word=tooth_word, bites=bites)
if dedup:
key = graph.canonical_key()
if key in seen:
continue
seen.add(key)
yield graph
# ---------------------------------------------------------------------------
# CLI.
# ---------------------------------------------------------------------------
def figure_one() -> FullMedialTireGraph:
"""The example graph of Figure 1 (Remark 3.8): 12 edges, one bite (0,6)."""
return FullMedialTireGraph(
n=12,
tooth_word="DDDDDUDUUUUU", # edges 0-4,6 down; 5,7,8,9,10,11 up
bites=frozenset({(0, 6)}),
)
def describe(graph: FullMedialTireGraph) -> str:
counts = face_singleton_counts(graph.tooth_word, graph.bites)
face_strs = []
for face, c in sorted(counts.items(), key=lambda kv: (kv[0] is not None, kv[0])):
name = "root" if face is None else f"bite{face}"
face_strs.append(f"{name}:{c}")
bites = ",".join(f"({i},{j})" for i, j in sorted(graph.bites)) or "-"
faces = " ".join(face_strs) or "-"
return (
f"word={graph.tooth_word} up={len(graph.up_edges)} "
f"down={len(graph.down_edges)} bites={bites} faces[{faces}]"
)
def run(args: argparse.Namespace) -> None:
if args.check_figure:
g = figure_one()
print("Figure 1 check:")
print(f" {describe(g)}")
ok = satisfies_bite_face_condition(g.tooth_word, g.bites)
print(f" satisfies Remark 3.8: {ok} (expect True; faces 4 and 0)")
print()
for n in range(args.min_n, args.max_n + 1):
graphs = list(generate(n, min_up_teeth=args.min_up, dedup=args.dedup))
label = "classes" if args.dedup else "graphs"
print(f"n={n}: {len(graphs)} {label}")
if args.show:
for g in graphs[: args.show]:
print(f" {describe(g)}")
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--min-n", type=int, default=3)
parser.add_argument("--max-n", type=int, default=8)
parser.add_argument("--min-up", type=int, default=3, help="Remark 3.6 bound")
parser.add_argument("--dedup", action="store_true",
help="reduce modulo dihedral symmetry of the cycle")
parser.add_argument("--show", type=int, default=0,
help="print up to this many graphs per n")
parser.add_argument("--check-figure", action="store_true",
help="verify the Figure 1 example against Remark 3.8")
run(parser.parse_args())
if __name__ == "__main__":
main()
@@ -1,188 +0,0 @@
"""Medial tire decomposition helpers for plane triangulation experiments."""
from __future__ import annotations
from collections import defaultdict
import networkx as nx
from .full_medial_tire_generator import FullMedialTireGraph
def ekey(u, v):
return (u, v) if (str(u), u) <= (str(v), v) else (v, u)
def medial_tire_facemodel(tread_faces) -> nx.Graph:
"""Build the ambient tread-face model of a full medial tire graph M(T)."""
mt = nx.Graph()
for f in tread_faces:
es = [ekey(f[0], f[1]), ekey(f[1], f[2]), ekey(f[2], f[0])]
mt.add_nodes_from(es)
for a in range(3):
mt.add_edge(es[a], es[(a + 1) % 3])
return mt
def extract_tread(faces, levels, d):
"""Tread T_d: faces spanning levels {d, d+1}. Return its edge classes."""
tread_faces = []
for f in faces:
lv = [levels[x] for x in f]
if min(lv) == d and max(lv) == d + 1:
tread_faces.append(f)
if not tread_faces:
return None
annular, up, down = set(), set(), set()
face_of_down = defaultdict(int)
for f in tread_faces:
for x, y in ((f[0], f[1]), (f[1], f[2]), (f[2], f[0])):
e = ekey(x, y)
lx, ly = levels[x], levels[y]
if {lx, ly} == {d, d + 1}:
annular.add(e)
elif lx == ly == d:
up.add(e)
elif lx == ly == d + 1:
down.add(e)
face_of_down[e] += 1
bites = {e for e in down if face_of_down[e] == 2}
return {
"tread_faces": tread_faces,
"annular": annular,
"up": up,
"down": down,
"bites": bites,
}
def _cycle_order(sub: nx.Graph, comp):
"""Cyclic order of a simple 2-regular component, or None."""
csub = sub.subgraph(comp)
if csub.number_of_nodes() < 3 or any(csub.degree(v) != 2 for v in csub):
return None
start = next(iter(comp))
order = [start]
prev, cur = None, start
while True:
nbrs = [w for w in csub.neighbors(cur) if w != prev]
if not nbrs:
break
nxt = nbrs[0]
if nxt == start:
break
order.append(nxt)
prev, cur = cur, nxt
return order if len(order) == csub.number_of_nodes() else None
def annular_cycle_order(M: nx.Graph, annular: set):
"""Cyclic order of annular medial vertices when they induce one cycle."""
sub = M.subgraph(annular)
if not annular or not nx.is_connected(sub):
return None
return _cycle_order(sub, set(annular))
def annular_cycle_components(M: nx.Graph, annular: set):
"""Cyclic orders of annular medial vertices, one per cycle component."""
sub = M.subgraph(annular)
orders = []
for comp in nx.connected_components(sub):
order = _cycle_order(sub, comp)
if order is not None:
orders.append(order)
return orders
def _linear_cut(n, bite_pairs):
"""Rotate the cycle so bite pairs become linear non-crossing intervals."""
for r in range(n):
rel = [tuple(sorted(((i - r) % n, (j - r) % n))) for i, j in bite_pairs]
ok = True
for a, b in rel:
for c, d in rel:
if (a, b) != (c, d) and (a < c < b < d or c < a < d < b):
ok = False
break
if not ok:
break
if ok:
return r, rel
return None
def _recognise_one(M, order, up, ann_global):
"""Recognise one annular cycle as a FullMedialTireGraph."""
n = len(order)
if n < 3:
return None
ann_set = set(order)
apex_of_edge = []
for i in range(n):
a, b = order[i], order[(i + 1) % n]
common = [
w for w in set(M.neighbors(a)) & set(M.neighbors(b))
if w not in ann_global
]
if len(common) != 1:
return None
apex_of_edge.append(common[0])
apex_positions = defaultdict(list)
for i, ap in enumerate(apex_of_edge):
apex_positions[ap].append(i)
bite_pairs = [
tuple(sorted(positions))
for positions in apex_positions.values()
if len(positions) == 2
]
tooth = ["U" if ap in up else "D" for ap in apex_of_edge]
cut = _linear_cut(n, bite_pairs)
if cut is None:
return None
r, rel_bites = cut
word = [""] * n
for i in range(n):
word[(i - r) % n] = tooth[i]
graph = FullMedialTireGraph(
n=n, tooth_word="".join(word), bites=frozenset(rel_bites)
)
bij = {}
for k in range(n):
bij[f"a{k}"] = order[(k + r) % n]
for i in graph.up_edges:
bij[f"u{i}"] = apex_of_edge[(i + r) % n]
for i in graph.singleton_down_edges:
bij[f"d{i}"] = apex_of_edge[(i + r) % n]
for i, j in sorted(graph.bites):
bij[f"p{i}_{j}"] = apex_of_edge[(i + r) % n]
sub_nodes = ann_set | set(apex_of_edge)
sub_edges = {ekey(*e) for e in M.subgraph(sub_nodes).edges()}
rec_edges = {ekey(bij[u], bij[v]) for u, v in graph.edges()}
if rec_edges != sub_edges:
return None
return graph, bij
def recognise(M, tread):
"""Recognise the tread's medial-tire structure.
A tread's annular frontier may be several disjoint cycles, each its own
full medial tire graph. Returns one ``(FullMedialTireGraph, bijection)``
pair per annular cycle component that recognises.
"""
up = set(tread["up"])
ann_global = set(tread["annular"])
tires = []
for order in annular_cycle_components(M, tread["annular"]):
rec = _recognise_one(M, order, up, ann_global)
if rec is not None:
tires.append(rec)
return tires
@@ -1,4 +1,5 @@
\relax
\citation{bauerfeld-medial-pigeonhole}
\citation{bauerfeld-nested-tire-decompositions}
\citation{bauerfeld-nested-tire-decompositions}
\@writefile{toc}{\contentsline {section}{\tocsection {}{1}{Introduction}}{1}{}\protected@file@percent }
@@ -12,39 +13,31 @@
\newlabel{def:full-medial-tire}{{3.1}{2}}
\newlabel{thm:annular-medial-colour-bound}{{3.3}{3}}
\newlabel{def:annular-teeth}{{3.4}{3}}
\citation{bauerfeld-nested-tire-decompositions}
\citation{bauerfeld-nested-tire-decompositions}
\newlabel{rem:teeth-sharing}{{3.5}{4}}
\newlabel{rem:teeth-sharing}{{3.5}{3}}
\newlabel{rem:up-teeth-count}{{3.6}{4}}
\newlabel{def:bite}{{3.7}{4}}
\newlabel{rem:bite-face-count}{{3.8}{4}}
\@writefile{lof}{\contentsline {figure}{\numberline {1}{\ignorespaces A full medial tire graph $\mathsf {M}(T)$ illustrating the tooth terminology. The thick cycle is the annular medial cycle $A(T)$, whose black vertices are the annular medial vertices. Each edge of $A(T)$ carries one tooth: up teeth (blue apexes, outer-boundary medial vertices) point into the outer region, and down teeth (red apexes, inner-boundary medial vertices) point into the inner region. The two down teeth meeting at the central shared apex (larger red vertex) form a bite; that shared apex splits the inner region into two faces, one with four down teeth on its boundary and one with none.}}{4}{}\protected@file@percent }
\newlabel{fig:medial-teeth-example}{{1}{4}}
\newlabel{def:boundary-medial-vertices}{{3.9}{4}}
\newlabel{def:medial-restriction-relation}{{3.10}{4}}
\@writefile{toc}{\contentsline {section}{\tocsection {}{4}{Decomposition}}{4}{}\protected@file@percent }
\newlabel{cor:medial-tire-decomposition}{{4.1}{4}}
\@writefile{lof}{\contentsline {figure}{\numberline {1}{\ignorespaces A simple medial tire graph $\mathsf {M}(T)$ illustrating the tooth terminology. The thick cycle is the annular medial cycle $A(T)$, whose black vertices are the annular medial vertices. Each edge of $A(T)$ carries one tooth: up teeth (blue apexes, outer-boundary medial vertices) point into the outer region, and down teeth (red apexes, inner-boundary medial vertices) point into the inner region. The two down teeth meeting at the central shared apex (larger red vertex) form a bite; that shared apex splits the inner region into two faces, one with four down teeth on its boundary and one with none.}}{5}{}\protected@file@percent }
\newlabel{fig:medial-teeth-example}{{1}{5}}
\@writefile{lof}{\contentsline {figure}{\numberline {2}{\ignorespaces Three six-face simple medial tire graphs found by the boundary-state restriction search. Black vertices are annular medial vertices; blue vertices are outer boundary medial vertices and red vertices are inner boundary medial vertices. The word below each diagram records the outer/inner type of the six annular faces in cyclic order. Boundary states are identified only up to colour permutation, not by rotation or reflection of the boundary order.}}{5}{}\protected@file@percent }
\citation{bauerfeld-nested-tire-decompositions}
\@writefile{lof}{\contentsline {figure}{\numberline {2}{\ignorespaces Three six-face full medial tire graphs found by the boundary-state restriction search. Black vertices are annular medial vertices; blue vertices are outer boundary medial vertices and red vertices are inner boundary medial vertices. The word below each diagram records the outer/inner type of the six annular faces in cyclic order. Boundary states are identified only up to colour permutation, not by rotation or reflection of the boundary order.}}{5}{}\protected@file@percent }
\newlabel{fig:medial-restriction-worst-cases}{{2}{5}}
\@writefile{lof}{\contentsline {figure}{\numberline {3}{\ignorespaces A proper vertex $3$-colouring of the full medial graph of the first seven-vertex counterexample found by the experiment. The medial vertex labelled $ij$ corresponds to the edge $(i,j)$ of the triangulation. For the vertex-source decomposition at source $1$, the highlighted annular medial cycle has colour counts $(2,2,2)$, so it is not coloured with two colours except at at most one vertex.}}{6}{}\protected@file@percent }
\newlabel{fig:medial-annular-cycle-counterexample}{{3}{6}}
\newlabel{def:compatible-family}{{4.2}{6}}
\newlabel{prop:gluing-criterion}{{4.3}{6}}
\@writefile{toc}{\contentsline {section}{\tocsection {}{5}{A medial pigeonhole programme}}{6}{}\protected@file@percent }
\newlabel{def:medial-boundary-state}{{5.1}{7}}
\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}{9}}
\@writefile{lof}{\contentsline {figure}{\numberline {3}{\ignorespaces A proper vertex $3$-colouring of the full medial graph of the first seven-vertex counterexample found by the experiment. The medial vertex labelled $ij$ corresponds to the edge $(i,j)$ of the triangulation. For the vertex-source decomposition at source $1$, the highlighted annular medial cycle has colour counts $(2,2,2)$, so it is not coloured with two colours except at at most one vertex.}}{5}{}\protected@file@percent }
\newlabel{fig:medial-annular-cycle-counterexample}{{3}{5}}
\newlabel{def:medial-restriction-relation}{{3.10}{5}}
\citation{bauerfeld-nested-tire-decompositions}
\bibcite{bauerfeld-nested-tire-decompositions}{1}
\bibcite{tait-original}{2}
\bibcite{bauerfeld-medial-pigeonhole}{2}
\bibcite{tait-original}{3}
\newlabel{tocindent-1}{0pt}
\newlabel{tocindent0}{12.7778pt}
\newlabel{tocindent1}{17.77782pt}
\newlabel{tocindent2}{29.38873pt}
\newlabel{tocindent3}{0pt}
\@writefile{toc}{\contentsline {section}{\tocsection {}{}{References}}{11}{}\protected@file@percent }
\gdef \@abspage@last{11}
\@writefile{toc}{\contentsline {section}{\tocsection {}{4}{Decomposition}}{6}{}\protected@file@percent }
\newlabel{cor:medial-tire-decomposition}{{4.1}{6}}
\newlabel{def:compatible-family}{{4.2}{6}}
\newlabel{prop:gluing-criterion}{{4.3}{6}}
\@writefile{toc}{\contentsline {section}{\tocsection {}{}{References}}{6}{}\protected@file@percent }
\gdef \@abspage@last{6}
@@ -1,5 +1,5 @@
# Fdb version 3
["pdflatex"] 1781554446 "paper.tex" "paper.pdf" "paper" 1781554447
["pdflatex"] 1781210675 "paper.tex" "paper.pdf" "paper" 1781210676
"/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" 1781554447 4210 3b62c5dd250f159f0e3d42ba3a3a7308 "pdflatex"
"paper.tex" 1781554440 42216 46cb5902d4210f9324b1231139c3e122 ""
"paper.aux" 1781210676 4206 870862ca1c6762f39fd7ed9def109a09 "pdflatex"
"paper.tex" 1781210650 40922 403b0b9df57192dbf02362b0b06705c3 ""
(generated)
"paper.aux"
"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) 15 JUN 2026 16:14
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 14 JUN 2026 21:07
entering extended mode
restricted \write18 enabled.
%&-line parsing enabled.
@@ -496,7 +496,7 @@ e
))
[1{/usr/local/texlive/2022/texmf-var/fonts/map/pdftex/updmap/pdftex.map}]
[2] [3]
Overfull \hbox (62.13657pt too wide) in paragraph at lines 371--380
Overfull \hbox (62.13657pt too wide) in paragraph at lines 366--375
[][]
[]
@@ -506,42 +506,39 @@ LaTeX Warning: `h' float specifier changed to `ht'.
LaTeX Warning: `h' float specifier changed to `ht'.
LaTeX Warning: `h' float specifier changed to `ht'.
[4] [5] [6] [7] [8] [9] [10] [11] (./paper.aux) )
[4] [5] [6] (./paper.aux) )
Here is how much of TeX's memory you used:
14419 strings out of 478268
283755 string characters out of 5846347
618029 words of memory out of 5000000
32248 multiletter control sequences out of 15000+600000
14413 strings out of 478268
283613 string characters out of 5846347
608323 words of memory out of 5000000
32242 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,738b,838s stack positions out of 10000i,1000n,20000p,200000b,200000s
</usr/local/texlive/2022/texmf
-dist/fonts/type1/public/amsfonts/cm/cmbx10.pfb></usr/local/texlive/2022/texmf-
dist/fonts/type1/public/amsfonts/cm/cmbx8.pfb></usr/local/texlive/2022/texmf-di
st/fonts/type1/public/amsfonts/cm/cmcsc10.pfb></usr/local/texlive/2022/texmf-di
st/fonts/type1/public/amsfonts/cm/cmmi10.pfb></usr/local/texlive/2022/texmf-dis
t/fonts/type1/public/amsfonts/cm/cmmi5.pfb></usr/local/texlive/2022/texmf-dist/
fonts/type1/public/amsfonts/cm/cmmi7.pfb></usr/local/texlive/2022/texmf-dist/fo
nts/type1/public/amsfonts/cm/cmmi8.pfb></usr/local/texlive/2022/texmf-dist/font
s/type1/public/amsfonts/cm/cmr10.pfb></usr/local/texlive/2022/texmf-dist/fonts/
type1/public/amsfonts/cm/cmr7.pfb></usr/local/texlive/2022/texmf-dist/fonts/typ
e1/public/amsfonts/cm/cmr8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/
public/amsfonts/cm/cmss10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/p
ublic/amsfonts/cm/cmsy10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/pu
blic/amsfonts/cm/cmsy5.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/publ
ic/amsfonts/cm/cmsy6.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public
/amsfonts/cm/cmsy7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/a
msfonts/cm/cmti10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/am
sfonts/cm/cmti8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsf
onts/cm/cmtt8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfon
ts/symbols/msam10.pfb>
Output written on paper.pdf (11 pages, 278916 bytes).
84i,8n,89p,736b,838s stack positions out of 10000i,1000n,20000p,200000b,200000s
</usr/local/texlive/2022/texmf-dist/fonts/type1/publ
ic/amsfonts/cm/cmbx10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/publi
c/amsfonts/cm/cmbx8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/
amsfonts/cm/cmcsc10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/
amsfonts/cm/cmmi10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/a
msfonts/cm/cmmi5.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/ams
fonts/cm/cmmi7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfo
nts/cm/cmmi8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfont
s/cm/cmr10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/
cm/cmr7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/
cmr8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cms
s10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy
10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy5
.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy6.p
fb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy7.pfb
></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti10.pfb>
</usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmti8.pfb></
usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmtt8.pfb></us
r/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/symbols/msam10.pfb>
Output written on paper.pdf (6 pages, 250239 bytes).
PDF statistics:
138 PDF objects out of 1000 (max. 8388607)
86 compressed objects within 1 object stream
121 PDF objects out of 1000 (max. 8388607)
74 compressed objects within 1 object stream
0 named destinations out of 1000 (max. 500000)
13 words of extra memory for PDF output out of 10000 (max. 10000000)
@@ -53,9 +53,12 @@ isomorphic to the medial graph of the planar dual $G^*$, and proper
$3$-vertex-colourings of $M(G)$ are equivalent to proper
$3$-edge-colourings of the cubic dual. Thus Tait's reformulation of
the Four Colour Theorem may be studied through proper vertex
$3$-colourings of medial subgraphs. We define medial tire pieces,
their boundary-state restriction relations, and a chain-pigeonhole
conjecture for compatible medial boundary states across the tire tree.
$3$-colourings of medial subgraphs. We define medial tire pieces and
their boundary-state restriction relations, and show that a proper
vertex $3$-colouring of $M(G)$ amounts to a compatible selection of
these boundary states across the tire tree. The resulting
pigeonhole programme for the Four Colour Theorem is developed
in~\cite{bauerfeld-medial-pigeonhole}.
\end{abstract}
\maketitle
@@ -159,27 +162,19 @@ colours.
\section{Medial tire pieces}
\begin{definition}[Simple and compound medial tire graphs]
\begin{definition}[Full medial tire graph]
\label{def:full-medial-tire}
Let $T$ be a tire tread in the tire tree $\mathcal{T}(G,S)$ supplied
by~\cite{bauerfeld-nested-tire-decompositions}. The \emph{medial tire
graph} of $T$, denoted $\mathsf{M}(T)$, is the subgraph of $M(G)$
induced by the medial vertices $m_e$ with $e$ an edge of $G$ incident
to at least one triangular face in the tread $T$. The medial vertices
corresponding to annular edges of $T$ are called \emph{annular medial
vertices}.
We call $\mathsf{M}(T)$ a \emph{simple medial tire graph} if its
annular medial vertices induce a single cycle. We call
$\mathsf{M}(T)$ a \emph{compound medial tire graph} if it is associated
to a connected depth component of tread faces but its annular medial
vertices induce more than one cycle. In a compound medial tire graph,
annular teeth are understood cycle-by-cycle, and up-tooth apexes
belonging to different annular cycles may coincide.
by~\cite{bauerfeld-nested-tire-decompositions}. The \emph{full medial
tire graph} of $T$, denoted $\mathsf{M}(T)$, is the subgraph of
$M(G)$ induced by the medial vertices $m_e$ with $e$ an edge of $G$
incident to at least one triangular face in the tread $T$. The medial
vertices corresponding to annular edges of $T$ are called
\emph{annular medial vertices}.
\end{definition}
\begin{remark}
In the ambient-triangulation setting, the simple medial tire graph
In the ambient-triangulation setting, the full medial tire graph
$\mathsf{M}(T)$ coincides with the omitted-edge medial tire graph
studied in~\cite{bauerfeld-nested-tire-decompositions}. Indeed, the
medial edges of $\mathsf{M}(T)$ are contributed by corners of annular
@@ -369,7 +364,7 @@ interior.
\node[lbl, anchor=east] at (192:1.86) (L0) {region with 0 down teeth};
\draw[lead] (L0.east) -- (180:0.45);
\end{tikzpicture}
\caption{A simple medial tire graph $\mathsf{M}(T)$ illustrating the tooth
\caption{A full medial tire graph $\mathsf{M}(T)$ illustrating the tooth
terminology. The thick cycle is the annular medial cycle $A(T)$, whose
black vertices are the annular medial vertices. Each edge of $A(T)$
carries one tooth: up teeth (blue apexes, outer-boundary medial vertices)
@@ -461,7 +456,7 @@ its boundary and one with none.}
\node at (0,-2.15) {\scriptsize $\min |R_T(\alpha)|=1$};
\end{scope}
\end{tikzpicture}
\caption{Three six-face simple medial tire graphs found by the boundary-state
\caption{Three six-face full medial tire graphs found by the boundary-state
restriction search. Black vertices are annular medial vertices; blue
vertices are outer boundary medial vertices and red vertices are inner
boundary medial vertices. The word below each diagram records the
@@ -609,11 +604,9 @@ parent and children.
Let $G$ be a plane triangulation with level source $S$. The tire-tree
decomposition $\mathcal{T}(G,S)$ of
\cite{bauerfeld-nested-tire-decompositions} induces a rooted
decomposition of the full medial graph $M(G)$ into medial tire graphs
$\{\mathsf{M}(T): T \in V(\mathcal{T}(G,S))\}$, glued along their
boundary medial vertex sets. A node of this decomposition may be a
simple medial tire graph or a compound medial tire graph, depending on
whether its annular medial vertices induce one cycle or several.
decomposition of the full medial graph $M(G)$ into full medial tire
graphs $\{\mathsf{M}(T): T \in V(\mathcal{T}(G,S))\}$, glued along
their boundary medial vertex sets.
\end{corollary}
\begin{proof}
@@ -666,351 +659,6 @@ properness is already enforced by one of the local colourings. Hence
$\varphi$ is a proper vertex $3$-colouring of $M(G)$.
\end{proof}
\section{A medial pigeonhole programme}
The restriction relation $R_T$ records exactly the local information
needed to pass a medial $3$-colouring through a tire. In a nested
chain
\[
T_0 \supset T_1 \supset \cdots \supset T_k,
\]
the outer boundary state of $T_{i+1}$ must match an inner boundary
state allowed by $R_{T_i}$. Thus a proof of the Four Colour Theorem in
this framework would follow from a structural reason that these
restriction sets cannot remain mutually disjoint along every branch of
the tire tree.
In this chaining step, the inner side of a parent simple medial tire is
read by its singleton down-tooth apex vertices. If the child side is a
compound medial tire, then the parent's singleton down-tooth apex
vertices are incident to---indeed, are identified with---the up-tooth
apex vertices of the compound medial tire, interpreted cycle-by-cycle
on its annular medial cycles. Equivalently, the primal edges
represented by the parent's singleton down-tooth apexes are exactly the
level-cycle interface edges represented on the child side as up-tooth
apexes. This is the boundary identification along which the medial
boundary states are chained.
\begin{definition}[Medial boundary state]
\label{def:medial-boundary-state}
A \emph{medial boundary state} on a boundary set
$\partial\mathsf{M}(T)$ is a proper vertex $3$-colouring of the
subgraph induced by that boundary set, considered up to permutation of
the three colours and the dihedral symmetries of the boundary walk
when that boundary is a cycle.
\end{definition}
\begin{conjecture}[Medial chain-pigeonhole principle]
\label{conj:medial-chain-pigeonhole}
There is a function $N(k)$ such that the following holds. Let
$T_0 \supset T_1 \supset \cdots \supset T_{N(k)}$ be a nested chain of
tire treads whose relevant boundary medial walks have length at most
$k$. Then two adjacent restriction relations in the chain have
compatible medial boundary states after colour permutation and boundary
symmetry. Equivalently, the chain contains a local gluing step that
cannot be obstructed by disjoint proper vertex $3$-colouring
restrictions.
\end{conjecture}
\begin{conjecture}[Medial tire route to the Four Colour Theorem]
\label{conj:medial-route-fct}
For every plane triangulation $G$ and every level source $S$, the
restriction relations $\{R_T : T \in V(\mathcal{T}(G,S))\}$ admit a
compatible selection of boundary states across the tire tree. Hence
$M(G)$ is properly vertex $3$-colourable, $G^*$ is properly
$3$-edge-colourable, and $G$ is properly $4$-vertex-colourable.
\end{conjecture}
\begin{remark}
Conjecture~\ref{conj:medial-route-fct} is equivalent in strength to
the Four Colour Theorem when combined with Tait's correspondence. The
point of the formulation is not to weaken the target theorem, but to
move the obstruction into finite boundary-state restrictions carried by
annular medial tire pieces.
\end{remark}
\subsection{Kempe-cycle conservation across medial tires}
We now record an additional structure carried by proper
$3$-colourings of medial graphs. This structure will be useful for
describing how colourings glue across level cycles.
Let $G$ be a plane triangulation and let $M=M(G)$ be its medial graph.
Let
\[
\varphi:V(M)\to\{1,2,3\}
\]
be a proper $3$-colouring of $M$. For a two-element colour set
$P=\{a,b\}\subseteq\{1,2,3\}$, let $M_P$ denote the subgraph of $M$
induced by the vertices of colours $a$ and $b$.
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 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.
\end{lemma}
\begin{proof}
Let $v\in V(M_P)$. In the medial graph $M$, the vertex $v$ has degree
$4$. Since $\varphi$ is a proper $3$-colouring, none of the neighbours
of $v$ has colour $\varphi(v)$. Thus all four neighbours of $v$ have
one of the two colours different from $\varphi(v)$.
In the medial graph of a plane triangulation, the neighbours of a
medial vertex occur in two opposite pairs corresponding to the two
faces incident with the corresponding edge of $G$. Around each such
triangular face, the three medial vertices receive all three colours.
Consequently, at $v$ there are exactly two neighbours of each colour
different from $\varphi(v)$. It follows that, in the subgraph induced
by any two colours $P$, every vertex has degree $2$. Hence each
component of $M_P$ is a cycle.
\end{proof}
Let $T$ be a medial tire region. We regard $T$ as an annular transition
region whose boundary consists of one outer level cycle and finitely
many inner level cycles:
\[
\partial T = C_0\sqcup C_1\sqcup\cdots\sqcup C_m.
\]
Here $C_0$ is the outer level cycle of $T$, and the cycles
$C_1,\ldots,C_m$ are the inner level cycles. Each inner level cycle
$C_i$ is also the outer level cycle of the corresponding child region
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
of $C$ without also leaving it.
Equivalently, the incidences of $K$ with $C$ are paired by the
components of $K$ lying on the child side of $C$, and also paired by the
components of $K$ lying on the parent side of $C$.
\end{lemma}
\begin{proof}
By the preceding lemma, $K$ is a cycle. The level cycle $C$ separates
the sphere into two closed regions, which we call the parent side and
the child side. Consider the intersection of $K$ with one of these
regions. Since $K$ is a cycle, no component of this intersection can
have exactly one boundary endpoint on $C$. Each component is either
closed within the region, or is a path with two boundary endpoints on
$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 simple
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 simple 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<m<j$, or to the root face if there
is none.
\end{itemize}
Bite apexes are never incident to a valid face in this sense.
For a colour pair $P=\{a,b\}$ write $\nu_P(F)$ for the number of tooth
apexes incident to $F$ that are coloured $a$ or $b$---equivalently, that
lie on a $P$-Kempe chain. The colouring $\varphi$ is
\emph{Kempe-balanced} if $\nu_P(F)$ is even for every valid face $F$ and
every colour pair $P$.
\end{definition}
\begin{remark}[Necessity of Kempe-balance]
\label{rem:kempe-balance-necessary}
A proper $3$-colouring of $\mathsf{M}(T)$ can be part of a proper
$3$-colouring of the whole medial graph $M(G)$ only when it is
Kempe-balanced: if $\varphi$ is the restriction to $\mathsf{M}(T)$ of a
proper $3$-colouring of $M(G)$, then $\varphi$ is Kempe-balanced.
Equivalently, a colouring of $\mathsf{M}(T)$ that fails the parity
condition at some valid face and colour pair cannot extend to a proper
$3$-colouring of $M(G)$. This is an instance of Kempe-cycle
conservation (Lemma~\ref{lem:kempe-conservation}). The tooth apexes
incident to a valid face are boundary medial vertices
(Definition~\ref{def:boundary-medial-vertices}) lying on a single level
cycle of the tire decomposition: the up-tooth apexes lie on the outer
level cycle, and the singleton down-tooth apexes incident to an interior
non-tooth face lie on the inner level cycle bounding that face. In the
$4$-regular graph $M(G)$ each $P$-Kempe chain of $\mathsf{M}(T)$ closes
up into a $P$-Kempe cycle, which by Lemma~\ref{lem:kempe-conservation}
meets each level cycle in an even number of $P$-coloured incidences; for
a given valid face these incidences are exactly its incident tooth
apexes coloured $a$ or $b$, whence $\nu_P(F)$ is even.
This argument is verified computationally. For bite-free pieces---capped
triangulated annuli on annular cycles of length $6,8,10,12$---every proper
$3$-colouring of $M(G)$ restricts to a Kempe-balanced colouring. The same
holds for pieces carrying a bite, including the case where singleton down
teeth lie in the bite's inner-gap face: there the inner level cycle splits
into a child level cycle per gap, and conservation across each child cycle
supplies the parity (in the checked example the three singleton down apexes
of a bite gap are a rainbow in every restriction).
\end{remark}
More generally, let $T$ be a medial tire region with boundary
\[
\partial T = C_0\sqcup C_1\sqcup\cdots\sqcup C_m.
\]
For a $P$-Kempe cycle $K$, every component of $K\cap T$ is either a
cycle contained in $T$, or a path with two endpoints on
$\partial T$. Thus the $P$-Kempe arcs inside $T$ define a pairing of
the $P$-coloured boundary incidences of
\[
C_0\sqcup C_1\sqcup\cdots\sqcup C_m.
\]
This motivates the following refinement of boundary states.
\begin{definition}[Kempe-enhanced boundary state]
Let $T$ be a medial tire region with outer level cycle $C_0$ and inner
level cycles $C_1,\ldots,C_m$. Let
\[
\mathcal C(T)=C_0\sqcup C_1\sqcup\cdots\sqcup C_m.
\]
A \emph{Kempe-enhanced boundary state} on $T$ consists of the following
data:
\begin{enumerate}
\item a boundary colouring
\[
\alpha:V(\mathcal C(T))\to\{1,2,3\};
\]
\item for each colour pair
\[
P\in\{\{1,2\},\{2,3\},\{3,1\}\},
\]
a pairing $\pi_P$ of the $P$-coloured boundary incidences of
$\mathcal C(T)$ induced by the $P$-Kempe arcs lying inside $T$.
\end{enumerate}
We write such a state as
\[
\kappa=(\alpha,\pi_{12},\pi_{23},\pi_{31}).
\]
\end{definition}
Given a proper $3$-colouring $\varphi$ of the medial tire graph
$M(T)$, the restriction of $\varphi$ to the boundary level cycles gives
the boundary colouring $\alpha$, while the two-colour Kempe arcs inside
$T$ give the pairings $\pi_{12},\pi_{23},\pi_{31}$. Thus $\varphi$
determines a Kempe-enhanced boundary state, denoted
\[
\kappa_T(\varphi).
\]
\begin{definition}[Kempe-enhanced restriction relation]
The \emph{Kempe-enhanced restriction relation} of $T$ is
\[
\mathcal K_T
=
\left\{
\kappa_T(\varphi):
\varphi \text{ is a proper }3\text{-colouring of } M(T)
\right\}.
\]
This refines the ordinary boundary-colouring relation by recording not
only which boundary colourings extend across $T$, but also how the
two-colour Kempe cycles are routed through the annular tire region.
\end{definition}
The annular structure of a tire is useful in two distinct ways. First,
it gives a bounded transition region between level cycles: the colouring
of the annular medial cycle controls, and in many cases determines, the
colouring of the remaining medial tire vertices. Thus the number of
possible transition states is bounded in terms of the annular structure,
rather than the total size of the subtree below the tire. Second, it
describes how the outer level cycle and the inner level cycles are
related by Kempe arcs. The level cycles are the gluing interfaces, while
the annular tire is the transition operator between them.
\begin{definition}[Kempe-compatible gluing]
Let $T$ be a medial tire region and let $U$ be a child region glued to
$T$ along a common level cycle $C$. Thus $C$ is an inner level cycle of
$T$ and the outer level cycle of $U$.
Let
\[
\kappa_T=(\alpha_T,\pi^T_{12},\pi^T_{23},\pi^T_{31})
\in \mathcal K_T
\]
and
\[
\kappa_U=(\alpha_U,\pi^U_{12},\pi^U_{23},\pi^U_{31})
\in \mathcal K_U.
\]
We say that $\kappa_T$ and $\kappa_U$ are \emph{Kempe-compatible along
$C$} if:
\begin{enumerate}
\item the boundary colourings agree on $C$:
\[
\alpha_T|_{V(C)}=\alpha_U|_{V(C)};
\]
\item for each colour pair
\[
P\in\{\{1,2\},\{2,3\},\{3,1\}\},
\]
the pairings $\pi^T_P$ and $\pi^U_P$ compose along the
$P$-coloured incidences of $C$ without producing an unpaired endpoint.
\end{enumerate}
When these conditions hold, the composed pairings determine a
Kempe-enhanced boundary state on the exposed boundary of
$T\cup_C U$.
\end{definition}
In these terms, gluing local colourings is not merely a matter of
matching boundary colours. The colourings must also route their
two-colour Kempe arcs compatibly across every shared level cycle. The
ordinary restriction relation records whether a boundary colouring can
be extended locally; the Kempe-enhanced relation additionally records
the conservation of Kempe-cycle flow through the annular transition
region.
For a tire with one outer level cycle and several inner level cycles,
\[
\partial T=C_0\sqcup C_1\sqcup\cdots\sqcup C_m,
\]
the parent tire may correlate the boundary states on the different
inner cycles. The Kempe-enhanced relation records this correlation as
a system of pairings among the $P$-coloured incidences of all boundary
level cycles simultaneously. Thus one should view a medial tire as a
multi-output transition operator
\[
\mathcal K_T:
C_0 \leadsto (C_1,\ldots,C_m),
\]
rather than as an independent collection of binary transitions.
The guiding principle is therefore:
\begin{quote}
Level cycles are the interfaces used for gluing, while annular tire
regions are the bounded transition regions that route Kempe cycles
between those interfaces.
\end{quote}
\begin{thebibliography}{9}
\bibitem{bauerfeld-nested-tire-decompositions}
@@ -1018,6 +666,11 @@ E.~Bauerfeld,
\emph{Nested Tire Decompositions of Plane Triangulations},
manuscript (math-research repository), 2026.
\bibitem{bauerfeld-medial-pigeonhole}
E.~Bauerfeld,
\emph{The Medial Pigeonhole Programme},
manuscript (math-research repository), 2026.
\bibitem{tait-original}
P.~G. Tait,
\emph{Remarks on the colourings of maps},