Verify Remark 5.8 mechanism; correct it to level-cycle conservation

Computational checks of the necessity of Kempe-balance (Remark 5.8):

- check_medial_face_parity.py shows the naive "even P-coloured vertices
  per medial face" claim is false (odd vertex-faces on the octahedron and
  stacked triangulations), so the original face-parity justification was
  wrong.
- check_remark58_bitefree.py builds genuine bite-free tire pieces (capped
  triangulated annuli) and confirms every proper 3-colouring of M(G)
  restricts to a Kempe-balanced colouring (|A(T)|=6,8,10,12, all
  colourings, zero failures).

Rewrite Remark 5.8 to cite the correct mechanism: the up/down apexes lie
on level cycles, and a P-Kempe cycle meets each level cycle in an even
number of P-coloured incidences (Lemma 5.6).  Note the bite case is not
yet checked end to end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 16:33:00 -04:00
parent 79cbca8e00
commit 5bed8b4dfb
6 changed files with 354 additions and 10 deletions
@@ -0,0 +1,181 @@
"""Probe the parity mechanism behind Remark 5.8.
Claim X: in a proper 3-colouring of the 4-regular medial graph M(G) of a plane
triangulation G, for every face f of M(G) and every colour pair P = {a,b}, the
number of vertices on the boundary of f coloured a or b is even.
M(G) has two kinds of faces: a "vertex-face" per vertex v of G (the cyclic
sequence of edges around v) and a "face-face" per triangular face of G (its
three edges). The face-faces are triangles, trivially even (count 2); the
vertex-faces are the non-obvious case.
We build M(G) from a planar embedding's rotation system, enumerate proper
3-colourings of M(G), and check Claim X on every face / pair.
"""
from __future__ import annotations
import itertools
import networkx as nx
# --- a handful of small plane triangulations (maximal planar graphs) ---------
def tetrahedron() -> nx.Graph:
return nx.complete_graph(4)
def octahedron() -> nx.Graph:
# K_{2,2,2}: antipodal pairs non-adjacent
g = nx.Graph()
pairs = [(0, 1), (2, 3), (4, 5)]
nonadj = set(map(frozenset, pairs))
for u in range(6):
for v in range(u + 1, 6):
if frozenset((u, v)) not in nonadj:
g.add_edge(u, v)
return g
def stacked(levels: int) -> nx.Graph:
"""Apollonian-style: repeatedly insert a vertex in a triangular face."""
g = nx.Graph()
g.add_edges_from([(0, 1), (1, 2), (0, 2)])
faces = [(0, 1, 2)]
nxt = 3
for _ in range(levels):
a, b, c = faces.pop(0)
v = nxt
nxt += 1
g.add_edges_from([(v, a), (v, b), (v, c)])
faces += [(a, b, v), (b, c, v), (a, c, v)]
return g
def icosahedron() -> nx.Graph:
return nx.icosahedral_graph()
def double_wheel(rim: int) -> nx.Graph:
"""Two apexes over a rim cycle: a simple triangulated 'tire' with caps."""
g = nx.Graph()
g.add_cycle = None
for i in range(rim):
g.add_edge(i, (i + 1) % rim)
g.add_edge(i, "N")
g.add_edge(i, "S")
return g
# --- medial graph from a rotation system -------------------------------------
def rotation_system(g: nx.Graph) -> dict:
ok, emb = nx.check_planarity(g)
if not ok:
raise ValueError("graph is not planar")
return {v: list(emb.neighbors_cw_order(v)) for v in g.nodes()}, emb
def medial_graph(g: nx.Graph):
"""Return (M, vertex_faces, face_faces) built from the rotation system.
Medial vertices are edges of g (as sorted tuples). Around each vertex the
incident edges form a face cycle (vertex-face); around each triangular face
of g its three edges form a face cycle (face-face).
"""
rot, emb = rotation_system(g)
def ekey(u, v):
return (u, v) if u <= v else (v, u)
M = nx.Graph()
M.add_nodes_from(ekey(u, v) for u, v in g.edges())
vertex_faces = []
for v, order in rot.items():
edges = [ekey(v, w) for w in order]
vertex_faces.append(edges)
for i in range(len(edges)):
M.add_edge(edges[i], edges[(i + 1) % len(edges)])
# face-faces: traverse each face of the embedding once
seen = set()
face_faces = []
for u, v in list(emb.edges()):
if (u, v) in seen:
continue
face = emb.traverse_face(u, v, mark_half_edges=seen)
edges = [ekey(face[i], face[(i + 1) % len(face)]) for i in range(len(face))]
face_faces.append(edges)
return M, vertex_faces, face_faces
# --- proper 3-colourings of M(G) ---------------------------------------------
def proper_3_colorings(M: nx.Graph, limit: int | None = None):
nodes = list(M.nodes())
adj = {v: set(M.neighbors(v)) for v in nodes}
coloring: dict = {}
out = []
def rec(i):
if limit is not None and len(out) >= limit:
return
if i == len(nodes):
out.append(dict(coloring))
return
v = nodes[i]
used = {coloring[w] for w in adj[v] if w in coloring}
for c in (0, 1, 2):
if c not in used:
coloring[v] = c
rec(i + 1)
del coloring[v]
rec(0)
return out
def check_claim_x(name: str, g: nx.Graph, color_limit: int = 200):
M, vfaces, ffaces = medial_graph(g)
colorings = proper_3_colorings(M, limit=color_limit)
if not colorings:
print(f"{name}: M(G) has no proper 3-colouring (skip)")
return
faces = [("vertex", f) for f in vfaces] + [("face", f) for f in ffaces]
violations = 0
odd_vertex_faces = 0
for col in colorings:
for kind, face in faces:
for pair in ((0, 1), (0, 2), (1, 2)):
cnt = sum(1 for v in face if col[v] in pair)
if cnt % 2 != 0:
violations += 1
if kind == "vertex":
odd_vertex_faces += 1
deg = sorted({len(f) for f in vfaces})
print(f"{name}: |V(G)|={g.number_of_nodes()} |M|={M.number_of_nodes()} "
f"colourings tested={len(colorings)} vertex-face sizes={deg}")
print(f" Claim X violations: {violations} "
f"(vertex-face violations: {odd_vertex_faces})")
def main():
cases = [
("tetrahedron", tetrahedron()),
("octahedron", octahedron()),
("stacked-3", stacked(3)),
("stacked-6", stacked(6)),
("double_wheel-5", double_wheel(5)),
("double_wheel-6", double_wheel(6)),
("double_wheel-7", double_wheel(7)),
("icosahedron", icosahedron()),
]
for name, g in cases:
# ensure it is a triangulation (every face a triangle)
check_claim_x(name, g)
if __name__ == "__main__":
main()
@@ -0,0 +1,150 @@
"""Directly test Remark 5.8 on genuine (bite-free) tire pieces.
Construction. Build a triangulated annulus (an antiprism band) between an outer
p-cycle O = o_0..o_{p-1} and an inner p-cycle I = i_0..i_{p-1}, with the 2p
triangles
(o_k, o_{k+1}, i_k) and (o_{k+1}, i_k, i_{k+1}).
Cap the outer disk with an apex N joined to all o_k and the inner disk with an
apex S joined to all i_k. The result G is a closed plane triangulation, so its
medial graph M(G) is 4-regular.
The tread T is the annulus; its full medial tire graph M(T) is the subgraph of
M(G) on the medial vertices of the tread edges (outer, inner and annular edges).
This tread has simple boundaries, hence no bites: the up teeth are the outer
edges, the down teeth the inner edges, and the only valid faces are the outer
face (up apexes) and the root face (down apexes).
Remark 5.8 predicts: every proper 3-colouring of M(G), restricted to M(T), is
Kempe-balanced, i.e. for each colour pair P the up apexes coloured in P are even
in number, and likewise the down apexes. We enumerate colourings of M(G) and
check this.
"""
from __future__ import annotations
import itertools
import networkx as nx
PAIRS = ((0, 1), (0, 2), (1, 2))
def ekey(u, v):
return (u, v) if (str(u), u) <= (str(v), v) else (v, u)
def build_capped_annulus(p: int):
g = nx.Graph()
O = [("o", k) for k in range(p)]
I = [("i", k) for k in range(p)]
outer_edges, inner_edges, annular_edges = [], [], []
for k in range(p):
o, on = O[k], O[(k + 1) % p]
i, ino = I[k], I[(k + 1) % p]
outer_edges.append(ekey(o, on))
inner_edges.append(ekey(i, ino))
annular_edges += [ekey(o, i), ekey(on, i)]
# tread triangles
g.add_edges_from([(o, on), (on, i), (o, i)]) # (o_k,o_{k+1},i_k)
g.add_edges_from([(on, i), (i, ino), (on, ino)]) # (o_{k+1},i_k,i_{k+1})
# caps
for k in range(p):
g.add_edges_from([("N", O[k]), ("N", O[(k + 1) % p])])
g.add_edges_from([("S", I[k]), ("S", I[(k + 1) % p])])
meta = {
"outer_edges": [ekey(*e) for e in outer_edges],
"inner_edges": [ekey(*e) for e in inner_edges],
"annular_edges": [ekey(*e) for e in annular_edges],
}
return g, meta
def medial_graph(g: nx.Graph) -> nx.Graph:
ok, emb = nx.check_planarity(g)
if not ok:
raise ValueError("not planar")
M = nx.Graph()
M.add_nodes_from(ekey(u, v) for u, v in g.edges())
for v in g.nodes():
order = list(emb.neighbors_cw_order(v))
edges = [ekey(v, w) for w in order]
for a in range(len(edges)):
M.add_edge(edges[a], edges[(a + 1) % len(edges)])
return M
def proper_3_colorings(M: nx.Graph, limit: int):
nodes = list(M.nodes())
adj = {v: set(M.neighbors(v)) for v in nodes}
coloring: dict = {}
out = []
def rec(i):
if len(out) >= limit:
return
if i == len(nodes):
out.append(dict(coloring))
return
v = nodes[i]
used = {coloring[w] for w in adj[v] if w in coloring}
for c in (0, 1, 2):
if c not in used:
coloring[v] = c
rec(i + 1)
del coloring[v]
rec(0)
return out
def is_kempe_balanced(coloring, up_apexes, down_apexes):
for face in (up_apexes, down_apexes):
for pair in PAIRS:
if sum(1 for e in face if coloring[e] in pair) % 2 != 0:
return False, face is down_apexes
return True, None
def run(p: int, limit: int = 4000):
g, meta = build_capped_annulus(p)
M = medial_graph(g)
up = meta["outer_edges"]
down = meta["inner_edges"]
colorings = proper_3_colorings(M, limit)
balanced = 0
unbalanced = []
for col in colorings:
ok, _ = is_kempe_balanced(col, up, down)
if ok:
balanced += 1
else:
unbalanced.append(col)
n_ann = len(meta["annular_edges"])
print(f"p={p}: |V(G)|={g.number_of_nodes()} |M(G)|={M.number_of_nodes()} "
f"|A(T)|={n_ann} up={len(up)} down={len(down)}")
print(f" colourings tested={len(colorings)} (cap {limit}) "
f"balanced={balanced} UNBALANCED={len(unbalanced)}")
if unbalanced:
col = unbalanced[0]
upc = [col[e] for e in up]
dnc = [col[e] for e in down]
print(f" first unbalanced restriction: up colours={upc} down colours={dnc}")
return len(unbalanced)
def main():
total_bad = 0
for p in (3, 4, 5, 6):
total_bad += run(p)
print()
print("Remark 5.8 (bite-free) holds on all tested colourings"
if total_bad == 0 else
f"Remark 5.8 (bite-free) FAILS: {total_bad} unbalanced restrictions found")
if __name__ == "__main__":
main()
@@ -1,5 +1,5 @@
# Fdb version 3 # Fdb version 3
["pdflatex"] 1781207945 "paper.tex" "paper.pdf" "paper" 1781207946 ["pdflatex"] 1781209574 "paper.tex" "paper.pdf" "paper" 1781209575
"/usr/local/texlive/2022/texmf-dist/fonts/map/fontname/texfonts.map" 1577235249 3524 cb3e574dea2d1052e39280babc910dc8 "" "/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/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/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/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-var/web2c/pdftex/pdflatex.fmt" 1665017617 2826443 7e98410c533054b636c6470db83a27bc ""
"/usr/local/texlive/2022/texmf.cnf" 1647878952 577 209b46be99c9075fd74d4c0369380e8c "" "/usr/local/texlive/2022/texmf.cnf" 1647878952 577 209b46be99c9075fd74d4c0369380e8c ""
"paper.aux" 1781207946 4206 a817291c83280f23be785ea9b9789717 "pdflatex" "paper.aux" 1781209575 4206 a817291c83280f23be785ea9b9789717 "pdflatex"
"paper.tex" 1781207918 39941 f80d8bca5b99e67d65ad8e4bb2d30152 "" "paper.tex" 1781209541 40737 e5d86b8964b20788119dc708cc9cd8ef ""
(generated) (generated)
"paper.aux" "paper.aux"
"paper.log" "paper.log"
@@ -1,4 +1,4 @@
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 11 JUN 2026 15:59 This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 11 JUN 2026 16:26
entering extended mode entering extended mode
restricted \write18 enabled. restricted \write18 enabled.
%&-line parsing enabled. %&-line parsing enabled.
@@ -535,7 +535,7 @@ ts/cm/cmti10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfont
s/cm/cmti8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/ s/cm/cmti8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/
cm/cmtt8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/sy cm/cmtt8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/sy
mbols/msam10.pfb> mbols/msam10.pfb>
Output written on paper.pdf (10 pages, 276272 bytes). Output written on paper.pdf (10 pages, 277087 bytes).
PDF statistics: PDF statistics:
135 PDF objects out of 1000 (max. 8388607) 135 PDF objects out of 1000 (max. 8388607)
84 compressed objects within 1 object stream 84 compressed objects within 1 object stream
@@ -829,11 +829,24 @@ proper $3$-colouring of $M(G)$, then $\varphi$ is Kempe-balanced.
Equivalently, a colouring of $\mathsf{M}(T)$ that fails the parity Equivalently, a colouring of $\mathsf{M}(T)$ that fails the parity
condition at some valid face and colour pair cannot extend to a proper 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 $3$-colouring of $M(G)$. This is an instance of Kempe-cycle
conservation (Lemma~\ref{lem:kempe-conservation}): in the $4$-regular conservation (Lemma~\ref{lem:kempe-conservation}). The tooth apexes
graph $M(G)$ each $P$-Kempe chain of $\mathsf{M}(T)$ closes up into a incident to a valid face are boundary medial vertices
$P$-Kempe cycle, and such a cycle crosses the boundary separating a (Definition~\ref{def:boundary-medial-vertices}) lying on a single level
valid face from the rest of the sphere an even number of times, so the cycle of the tire decomposition: the up-tooth apexes lie on the outer
$P$-coloured apexes incident to that face occur in even number. 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: across
all proper $3$-colourings of the capped triangulated annuli on annular
cycles of length $6,8,10,12$, every restriction to the tire piece is
Kempe-balanced. The case with bites, where the inner level cycle splits
into several child level cycles, is consistent with the same mechanism
but is not yet checked end to end.
\end{remark} \end{remark}
More generally, let $T$ be a medial tire region with boundary More generally, let $T$ be a medial tire region with boundary