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:
+181
@@ -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()
|
||||
+150
@@ -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()
|
||||
Reference in New Issue
Block a user