Add worked walkthrough; factor explicit phase/colorder colouring
Refactor canonical_coloring into coloring_skeleton (phase-independent parts) +
canonical_coloring_explicit (explicit phases + DFS colour order) + a random-phase
wrapper for back-compat. This exposes the two control knobs deterministically so
they can be enumerated rather than only sampled.
Add a fully worked example on the smallest clean graph (ring [3,5]+hub, 9
vertices, one odd seam, no gadgets): even_program_walkthrough.md traces all six
stages -- generate G with embedding, pick source + BFS levels, choose the diamond
site that evens the level-5 seam, build M(G'), the canonical colouring (seam
mono-3, hub annulus alternates, root by DFS), and a real {1,2}-Kempe switch that
makes the diamond quad reducible. dump_walkthrough.py reproduces every number;
draw_walkthrough.py renders the 4-panel figure even_program_walkthrough.png.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+155
@@ -0,0 +1,155 @@
|
||||
"""Step-by-step picture of the even-level-cycle programme on the smallest clean
|
||||
example: the ring triangulation sizes=[3,5], leaf='hub' (rng seed 0), 9 vertices.
|
||||
|
||||
One odd level cycle (level 1, the 5-cycle 3-4-5-6-7), no terminal triangles, so
|
||||
the only surgery is a single DIAMOND. We walk the FIRST successful choice-set
|
||||
found by the sweep: insertion site = edge (3,4); colour phase = (0,); root DFS
|
||||
colour order = (1,0,2). Panels:
|
||||
|
||||
A G with its odd level-5 seam (BFS levels from the outer triangle 0-1-2).
|
||||
B G' = G + diamond w(=9) on edge (3,4): seam is now an even 6-cycle; the
|
||||
diamond quad 3-0-4-8 (restored diagonal 3-4) shaded.
|
||||
C medial M(G') with the canonical colouring BEFORE any switch: the four quad
|
||||
medials m(0,3),m(0,4),m(4,8),m(3,8) are ALL colour 1 -> diamond_condition
|
||||
fails (the obstruction).
|
||||
D after one {1,2}-Kempe switch on the component through m(0,3)
|
||||
{(0,3),(3,7),(3,8),(3,9)}: quad medials become 2,1,1,2 -> reducible;
|
||||
remove w, restored diagonal (3,4) takes the third colour 0.
|
||||
|
||||
Layout is concentric by BFS level (tire-tread model), the level-1 ring placed in
|
||||
seam-cycle order, so spokes read outward. Run with the repo venv python.
|
||||
"""
|
||||
import os, random
|
||||
import numpy as np
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.patches import Polygon
|
||||
import kempe_even_program_harness as H
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
PAL = {0: "#e6550d", 1: "#3182bd", 2: "#31a354"} # colours "1","2","3"(=2)
|
||||
RAD = {0: 2.7, 1: 1.45, 2: 0.0}
|
||||
|
||||
def concentric(g, outer, an, ring_order):
|
||||
pos = {}
|
||||
for i, v in enumerate(outer):
|
||||
a = 90 - i * 120
|
||||
pos[v] = (np.cos(np.radians(a)) * RAD[0], np.sin(np.radians(a)) * RAD[0])
|
||||
m = len(ring_order)
|
||||
for i, v in enumerate(ring_order):
|
||||
a = 90 + i * 360.0 / m
|
||||
pos[v] = (np.cos(np.radians(a)) * RAD[1], np.sin(np.radians(a)) * RAD[1])
|
||||
for v in sorted(g.rot):
|
||||
if v in pos: continue
|
||||
pos[v] = (0.0, 0.0) # hub (level 2)
|
||||
return pos
|
||||
|
||||
def mid(p, q): return ((p[0]+q[0])/2, (p[1]+q[1])/2)
|
||||
|
||||
def draw_graph(ax, g, pos, level=None, bold_cycle=None, shade_quad=None, wvert=None):
|
||||
if shade_quad:
|
||||
ax.add_patch(Polygon([pos[v] for v in shade_quad], closed=True,
|
||||
color="#ffe2bf", zorder=0))
|
||||
bold = set()
|
||||
if bold_cycle:
|
||||
for i in range(len(bold_cycle)):
|
||||
bold.add(frozenset((bold_cycle[i], bold_cycle[(i+1) % len(bold_cycle)])))
|
||||
for ed in g.edges():
|
||||
a, b = tuple(ed); pa, pb = pos[a], pos[b]
|
||||
if ed in bold:
|
||||
ax.plot([pa[0], pb[0]], [pa[1], pb[1]], color="#d62728", lw=2.8, zorder=2)
|
||||
else:
|
||||
ax.plot([pa[0], pb[0]], [pa[1], pb[1]], color="#888888", lw=1.0, zorder=1)
|
||||
for v, p in pos.items():
|
||||
c = "#d62728" if (wvert is not None and v == wvert) else "#222222"
|
||||
ax.plot(*p, "o", color="white", ms=17, zorder=4)
|
||||
ax.plot(*p, "o", ms=17, mfc="white", mec=c, mew=1.7, zorder=5)
|
||||
ax.annotate(str(v), p, ha="center", va="center", fontsize=9,
|
||||
fontweight="bold", color=c, zorder=6)
|
||||
if level is not None:
|
||||
ax.annotate(f"L{level[v]}", p, textcoords="offset points",
|
||||
xytext=(12, 10), fontsize=6.5, color="#999999", zorder=6)
|
||||
|
||||
def draw_medial(ax, g, pos, col, halo=None, restored=None):
|
||||
for ed in g.edges():
|
||||
a, b = tuple(ed); pa, pb = pos[a], pos[b]
|
||||
ax.plot([pa[0], pb[0]], [pa[1], pb[1]], color="#e3e3e3", lw=0.8, zorder=0)
|
||||
adj = H.medial_adj(g)
|
||||
mpos = {mm: mid(pos[tuple(mm)[0]], pos[tuple(mm)[1]]) for mm in adj}
|
||||
seen = set()
|
||||
for mm in adj:
|
||||
for b in adj[mm]:
|
||||
k = frozenset((mm, b))
|
||||
if k in seen: continue
|
||||
seen.add(k)
|
||||
pa, pb = mpos[mm], mpos[b]
|
||||
ax.plot([pa[0], pb[0]], [pa[1], pb[1]], color="#c9c9c9", lw=0.8, zorder=1)
|
||||
if restored is not None:
|
||||
a, b = restored
|
||||
ax.plot([pos[a][0], pos[b][0]], [pos[a][1], pos[b][1]], color="#d62728",
|
||||
lw=1.8, ls=":", zorder=2)
|
||||
rp = mid(pos[a], pos[b])
|
||||
ax.plot(*rp, "s", color=PAL[col[H.e(a, b)]], ms=13, mec="#d62728",
|
||||
mew=2.0, zorder=7)
|
||||
halo = halo or set()
|
||||
for mm, p in mpos.items():
|
||||
if mm not in col: continue
|
||||
if mm in halo:
|
||||
ax.plot(*p, "o", color="#000000", ms=16, zorder=5)
|
||||
ax.plot(*p, "o", color=PAL[col[mm]], ms=10.5, mec="black", mew=0.8, zorder=6)
|
||||
|
||||
rng = random.Random(0)
|
||||
g, outer = H.ring_triangulation([3, 5], 'hub', rng)
|
||||
an = H.Analysis(g.copy(), outer)
|
||||
ring = [k for k, c in an.seams if k == 1][0:1] and \
|
||||
[c for k, c in an.seams if k == 1][0]
|
||||
posG = concentric(g, outer, an, ring)
|
||||
|
||||
prep = H._prep_gadgets(g.copy(), outer)
|
||||
template, an_g, gadgets = prep
|
||||
gg = template.copy()
|
||||
w, u, v, x, t = gg.insert_diamond(3, 4)
|
||||
an2 = H.Analysis(gg, outer)
|
||||
ring2 = [c for k, c in an2.seams if k == 1][0]
|
||||
posGp = concentric(gg, outer, an2, ring2)
|
||||
posGp[w] = mid(posGp[3], posGp[4])
|
||||
quad = H.quad_of(gg, w, u, v) # (3,0,4,8)
|
||||
|
||||
col0, _ = H.canonical_coloring_explicit(gg, an2.level, outer, (0,), [1, 0, 2])
|
||||
col1 = dict(col0)
|
||||
adjm = H.medial_adj(gg)
|
||||
comp = H.kempe_component(col1, adjm, H.e(0, 3), (1, 2))
|
||||
H.switch(col1, comp, (1, 2))
|
||||
third = H.diamond_condition(col1, quad)
|
||||
col1[H.e(3, 4)] = third
|
||||
|
||||
fig, axes = plt.subplots(1, 4, figsize=(19, 5.4))
|
||||
for ax in axes:
|
||||
ax.set_aspect("equal"); ax.axis("off"); ax.set_xlim(-3.2, 3.2); ax.set_ylim(-3.2, 3.2)
|
||||
|
||||
draw_graph(axes[0], g, posG, level=an.level, bold_cycle=ring)
|
||||
axes[0].set_title("A. G (BFS levels from source triangle 0-1-2)\n"
|
||||
"odd level-1 seam = 5-cycle 3-4-5-6-7 (red)", fontsize=9)
|
||||
draw_graph(axes[1], gg, posGp, level=an2.level, bold_cycle=ring2,
|
||||
shade_quad=quad, wvert=w)
|
||||
axes[1].set_title("B. G' = G + diamond w=9 on edge (3,4)\n"
|
||||
"seam now even 6-cycle; quad 3-0-4-8 shaded", fontsize=9)
|
||||
quad_med = {H.e(quad[i], quad[(i+1) % 4]) for i in range(4)}
|
||||
draw_medial(axes[2], gg, posGp, col0, halo=quad_med)
|
||||
axes[2].set_title("C. M(G') canonical colour (phase 0, DFS order 1,0,2)\n"
|
||||
"quad medials m(0,3)m(0,4)m(4,8)m(3,8) ALL =1 (haloed)\n"
|
||||
"-> diamond_condition FAILS", fontsize=9)
|
||||
draw_medial(axes[3], gg, posGp, col1, halo=comp, restored=(3, 4))
|
||||
axes[3].set_title("D. after {1,2}-Kempe switch on comp through m(0,3)\n"
|
||||
"{(0,3),(3,7),(3,8),(3,9)} (haloed): quad -> 2,1,1,2\n"
|
||||
f"remove w; restored edge (3,4)=square takes colour {third}",
|
||||
fontsize=9)
|
||||
fig.suptitle("Even-level-cycle programme, worked example (ring [3,5]+hub, 9 "
|
||||
"vertices): one odd seam -> one diamond -> one Kempe switch -> "
|
||||
"proper 3-colouring of M(G). Colours: 1=orange(0), 2=blue(1), "
|
||||
"3=green(2).", fontsize=10)
|
||||
fig.tight_layout(rect=(0, 0, 1, 0.9))
|
||||
out = os.path.join(HERE, "even_program_walkthrough.png")
|
||||
fig.savefig(out, dpi=160)
|
||||
print("wrote", out)
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
"""Print every stage of the even-level-cycle programme on the smallest clean
|
||||
example (ring sizes=[3,5], leaf='hub', rng seed 0; 9 vertices) for the first
|
||||
choice-set the sweep succeeds on: site (3,4), phase (0,), DFS order (1,0,2).
|
||||
|
||||
This is the textual companion to even_program_walkthrough.md / .png.
|
||||
"""
|
||||
import random
|
||||
import kempe_even_program_harness as H
|
||||
|
||||
|
||||
def fz(m): # pretty-print an edge-medial
|
||||
return tuple(sorted(tuple(m)))
|
||||
|
||||
|
||||
def main():
|
||||
rng = random.Random(0)
|
||||
g, outer = H.ring_triangulation([3, 5], 'hub', rng)
|
||||
print("OUTER (source/root triangle):", outer)
|
||||
|
||||
print("\n# STEP 1: graph G (rotation system: vertex -> embedding-order neighbours)")
|
||||
for v in sorted(g.rot):
|
||||
print(f" {v}: {g.rot[v]}")
|
||||
print("faces:", [tuple(f) for f in g.faces()])
|
||||
|
||||
print("\n# STEP 2: levels (BFS from the source triangle) + seams")
|
||||
an = H.Analysis(g.copy(), outer)
|
||||
for v in sorted(an.level):
|
||||
print(f" v{v}: level {an.level[v]}")
|
||||
for k, cyc in an.seams:
|
||||
print(f" seam level {k}: {cyc} (len {len(cyc)}, "
|
||||
f"{'ODD' if len(cyc) % 2 else 'even'})")
|
||||
print(" terminal triangles (need leaf gadget):", an.terminal)
|
||||
|
||||
print("\n# STEP 3: diamond sites + chosen edge")
|
||||
template, an_g, gadgets = H._prep_gadgets(g.copy(), outer)
|
||||
sites = H._candidate_sites(an_g)
|
||||
print(" gadgets inserted:", gadgets)
|
||||
print(" candidate diamond edges (odd seam):", sites)
|
||||
combo = ((3, 4),)
|
||||
print(" chosen combo (first successful):", combo)
|
||||
gg = template.copy()
|
||||
dia = [gg.insert_diamond(a, b) for (a, b) in combo]
|
||||
print(" inserted (w,u,v,x,t):", dia)
|
||||
an2 = H.Analysis(gg, outer)
|
||||
print(" rot[w]:", gg.rot[dia[0][0]], " level[w]:", an2.level[dia[0][0]])
|
||||
for k, cyc in an2.seams:
|
||||
print(f" seam level {k} now: len {len(cyc)} "
|
||||
f"{'ODD' if len(cyc) % 2 else 'even'} {cyc}")
|
||||
|
||||
print("\n# STEP 4: medial graph M(G') (one vertex per edge of G')")
|
||||
adj = H.medial_adj(gg)
|
||||
print(f" |V(M)| = {len(adj)}")
|
||||
for m in sorted(adj, key=fz):
|
||||
print(f" m{fz(m)}: {sorted(fz(b) for b in adj[m])}")
|
||||
|
||||
print("\n# STEP 5: canonical colouring phases=(0,) colorder=(1,0,2)")
|
||||
phases, colorder = (0,), [1, 0, 2]
|
||||
sk, _ = H.coloring_skeleton(gg, an2.level, outer)
|
||||
for i, cyc in enumerate(sk['nonroot']):
|
||||
print(f" non-root annulus #{i} (len {len(cyc)}): {[fz(m) for m in cyc]}")
|
||||
print(" root annulus:", [fz(m) for m in sk['root']])
|
||||
print(" outer-trio (free/DFS):", [fz(m) for m in sk['outer_es']])
|
||||
col, _ = H.canonical_coloring_explicit(gg, an2.level, outer, phases, colorder)
|
||||
for m in sorted(col, key=fz):
|
||||
print(f" m{fz(m)} = {col[m]}")
|
||||
|
||||
print("\n# STEP 6: Kempe switch + diamond collapse")
|
||||
w, u, v, x, t = dia[0]
|
||||
quad = H.quad_of(gg, w, u, v)
|
||||
support = [H.e(quad[i], quad[(i + 1) % 4]) for i in range(4)]
|
||||
print(f" diamond w={w}, quad {quad} (diagonal {u}-{v})")
|
||||
print(" quad medials:", [fz(s) for s in support])
|
||||
print(" diamond_condition BEFORE switch:", H.diamond_condition(col, quad),
|
||||
" support:", {fz(s): col[s] for s in support})
|
||||
adjm = H.medial_adj(gg)
|
||||
comp = H.kempe_component(col, adjm, H.e(0, 3), (1, 2))
|
||||
print(" switch {1,2}-component through m(0,3):", sorted(fz(m) for m in comp))
|
||||
H.switch(col, comp, (1, 2))
|
||||
third = H.diamond_condition(col, quad)
|
||||
print(" diamond_condition AFTER switch:", third,
|
||||
" support:", {fz(s): col[s] for s in support})
|
||||
H.collapse_degree4(gg, col, w, u, v)
|
||||
col[H.e(u, v)] = third
|
||||
print(f" removed w; restored edge ({u},{v}) takes colour {third}")
|
||||
print(" proper 3-colouring of M(G)?", H.verify_proper(gg, col))
|
||||
print(" vertices back to original G?", set(gg.rot) == set(g.rot))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
# Even-level-cycle programme — a fully worked example
|
||||
|
||||
A step-by-step trace of the whole pipeline on the **smallest clean graph**: the
|
||||
synthetic ring triangulation `sizes=[3,5]`, `leaf='hub'` (generator
|
||||
`random.Random(0)`), **9 vertices**. It has exactly one odd level cycle and no
|
||||
terminal triangles, so the only surgery is a **single diamond** — which makes
|
||||
every stage small enough to print in full.
|
||||
|
||||
Everything below is the *actual* state produced by `kempe_even_program_harness.py`
|
||||
(regenerate the data with the dump at the end of this note; the figure is
|
||||
`even_program_walkthrough.png`, drawn by `draw_walkthrough.py`). We walk the
|
||||
**first choice-set the sweep finds that succeeds**:
|
||||
|
||||
> insertion site = edge `(3,4)` · colour phase = `(0,)` · root-DFS colour order = `(1,0,2)`
|
||||
|
||||
Colour convention throughout: values `{0,1,2}` are Tait colours "1,2,3"; `2` is
|
||||
the "colour 3" the seam is painted with. In the figure: `0`=orange, `1`=blue,
|
||||
`2`=green.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Generate the triangulation with a plane embedding *(panel A)*
|
||||
|
||||
`ring_triangulation([3,5], 'hub')` builds three concentric rings — an outer
|
||||
triangle, a 5-ring, and a hub — triangulating each annulus with a random tooth
|
||||
word and capping the centre with a hub vertex. The result is a genuine plane
|
||||
triangulation given by its rotation system (neighbours in embedding order):
|
||||
|
||||
```
|
||||
0: [4, 3, 7, 6, 5, 2, 1] 3: [0, 4, 8, 7] 6: [5, 0, 7, 8]
|
||||
1: [4, 0, 2, 5] 4: [3, 0, 1, 5, 8] 7: [6, 0, 3, 8]
|
||||
2: [5, 1, 0] 5: [4, 1, 2, 0, 6, 8] 8: [3, 4, 5, 6, 7]
|
||||
```
|
||||
|
||||
The 14 triangular faces (one is the outer/unbounded face) are
|
||||
```
|
||||
(4,3,8) (4,0,3) (4,1,0) (4,5,1) (4,8,5) (3,0,7) (3,7,8)
|
||||
(0,6,7) (0,5,6) (0,2,5) (0,1,2) (1,5,2) (5,8,6) (6,8,7)
|
||||
```
|
||||
and the 21 edges are the pairs appearing above. (Euler check: 9 − 21 + 14 = 2.)
|
||||
The figure draws this with a Tutte-style concentric layout.
|
||||
|
||||
## Step 2 — Pick the source and read off levels *(panel A)*
|
||||
|
||||
The **source** is the outer triangle, taken as the unbounded face `(0,1,2)`.
|
||||
A BFS from those three vertices assigns each vertex its **level** (graph distance
|
||||
to the source tread):
|
||||
|
||||
| level | vertices |
|
||||
|------:|----------|
|
||||
| 0 | 0, 1, 2 (the source triangle) |
|
||||
| 1 | 3, 4, 5, 6, 7 (the ring) |
|
||||
| 2 | 8 (the hub) |
|
||||
|
||||
The **level cycles ("seams")** are the same-level edge cycles at each depth ≥1:
|
||||
|
||||
```
|
||||
level 1: cycle 3-4-5-6-7 length 5 -> ODD
|
||||
```
|
||||
|
||||
There is exactly one seam and it is **odd**. There are **no terminal
|
||||
triangles**, so the leaf gadget never fires — the only surgery needed is a
|
||||
diamond on this one odd seam.
|
||||
|
||||
## Step 3 — Choose the edge(s) that make the level cycles even *(panel B)*
|
||||
|
||||
A diamond can be inserted on any seam edge whose two apexes straddle the
|
||||
neighbouring levels (`k−1` and `k+1`). For the level-1 seam, **all five** seam
|
||||
edges qualify:
|
||||
|
||||
```
|
||||
candidate diamond sites: (3,4) (4,5) (5,6) (6,7) (7,3)
|
||||
```
|
||||
|
||||
This is the choice the **site sweep** ranges over (here a 5-element design
|
||||
space). We take the **first one that leads to a full success: `(3,4)`**.
|
||||
|
||||
Insert the diamond on `(3,4)`:
|
||||
- delete the edge `(3,4)`;
|
||||
- add a new degree-4 vertex `w = 9` adjacent to `u=3, v=4` and the two apexes
|
||||
`x=0` (level 0) and `t=8` (level 2), with rotation `rot[9] = [3,0,4,8]`.
|
||||
|
||||
`w` lands at level 1, so the level-1 seam becomes the cycle
|
||||
```
|
||||
3-9-4-5-6-7 length 6 -> EVEN
|
||||
```
|
||||
Every level cycle is now even. The four-cycle `3-0-4-8` around `w` (diagonal the
|
||||
restored edge `3-4`) is the **diamond quad** we must later collapse — shaded in
|
||||
panel B.
|
||||
|
||||
## Step 4 — Build the medial graph M(G′) *(panels C, D)*
|
||||
|
||||
The medial graph has **one vertex per edge of G′** (24 of them) and joins two
|
||||
edge-medials iff the edges are consecutive around a common face. A 4-colouring
|
||||
of the triangulation = a proper **3-colouring of M(G′)**. The adjacency (each
|
||||
medial `m(a,b)` listed with its neighbours):
|
||||
|
||||
```
|
||||
m(0,1): (0,2)(0,4)(1,2)(1,4) m(3,7): (0,3)(0,7)(3,8)(7,8)
|
||||
m(0,2): (0,1)(0,5)(1,2)(2,5) m(3,8): (3,7)(3,9)(7,8)(8,9)
|
||||
m(0,3): (0,7)(0,9)(3,7)(3,9) m(3,9): (0,3)(0,9)(3,8)(8,9)
|
||||
m(0,4): (0,1)(0,9)(1,4)(4,9) m(4,5): (1,4)(1,5)(4,8)(5,8)
|
||||
m(0,5): (0,2)(0,6)(2,5)(5,6) m(4,8): (4,5)(4,9)(5,8)(8,9)
|
||||
m(0,6): (0,5)(0,7)(5,6)(6,7) m(4,9): (0,4)(0,9)(4,8)(8,9)
|
||||
m(0,7): (0,3)(0,6)(3,7)(6,7) m(5,6): (0,5)(0,6)(5,8)(6,8)
|
||||
m(0,9): (0,3)(0,4)(3,9)(4,9) m(5,8): (4,5)(4,8)(5,6)(6,8)
|
||||
m(1,2): (0,1)(0,2)(1,5)(2,5) m(6,7): (0,6)(0,7)(6,8)(7,8)
|
||||
m(1,4): (0,1)(0,4)(1,5)(4,5) m(6,8): (5,6)(5,8)(6,7)(7,8)
|
||||
m(1,5): (1,2)(1,4)(2,5)(4,5) m(7,8): (3,7)(3,8)(6,7)(6,8)
|
||||
m(2,5): (0,2)(0,5)(1,2)(1,5) m(8,9): (3,8)(3,9)(4,8)(4,9)
|
||||
```
|
||||
|
||||
## Step 5 — Canonical colouring (no 4CT): seam = 3, annuli alternate, root by DFS *(panel C)*
|
||||
|
||||
The canonical colouring is assembled from three deterministic ingredients plus
|
||||
the two control knobs (phase, DFS order):
|
||||
|
||||
1. **Every level-edge medial → colour 3 (=2).** The even seam `3-9-4-5-6-7`
|
||||
becomes **monochromatic 3**:
|
||||
`m(3,9)=m(4,9)=m(4,5)=m(5,6)=m(6,7)=m(3,7)=2`.
|
||||
2. **Each non-root annulus alternates {0,1} with a phase bit.** Here there is one
|
||||
non-root annulus — the hub spokes between levels 1 and 2:
|
||||
`[(8,9),(3,8),(7,8),(6,8),(5,8),(4,8)]` (length 6). With **phase 0** it is
|
||||
coloured `0,1,0,1,0,1`:
|
||||
`m(8,9)=0, m(3,8)=1, m(7,8)=0, m(6,8)=1, m(5,8)=0, m(4,8)=1`.
|
||||
3. **The root region** — the level-0↔1 spokes plus the three outer-triangle
|
||||
medials `m(0,1),m(0,2),m(1,2)` — is solved by a small DFS using colour
|
||||
priority **`(1,0,2)`**.
|
||||
|
||||
The resulting proper colouring of M(G′):
|
||||
|
||||
```
|
||||
m(0,1)=2 m(0,2)=1 m(0,3)=1 m(0,4)=1 m(0,5)=0 m(0,6)=1 m(0,7)=0 m(0,9)=0
|
||||
m(1,2)=0 m(1,4)=0 m(1,5)=1 m(2,5)=2 m(3,7)=2 m(3,8)=1 m(3,9)=2 m(4,5)=2
|
||||
m(4,8)=1 m(4,9)=2 m(5,6)=2 m(5,8)=0 m(6,7)=2 m(6,8)=1 m(7,8)=0 m(8,9)=0
|
||||
```
|
||||
|
||||
This is the "no-4CT" colouring of the **evened** graph — proper because the seam
|
||||
is even (a monochromatic-3 cycle around even-length annuli is consistent). The
|
||||
only thing standing between it and a colouring of the *original* G is the
|
||||
diamond.
|
||||
|
||||
## Step 6 — Kempe switch, then collapse the diamond *(panel D)*
|
||||
|
||||
To remove `w=9` we restore the diagonal `(3,4)` and recolour. The **degree-4
|
||||
removal condition** on the quad `3-0-4-8` reads: the opposite-corner medial pairs
|
||||
`(m_{30}, m_{04})` and `(m_{48}, m_{83})` must each be *distinct*, using ≤2
|
||||
colours total; the restored edge then takes the third.
|
||||
|
||||
Read the four quad medials off the canonical colouring:
|
||||
```
|
||||
m(0,3)=1 m(0,4)=1 m(4,8)=1 m(3,8)=1 ALL EQUAL
|
||||
```
|
||||
The first pair `(m_{30},m_{04}) = (1,1)` is **not** distinct → `diamond_condition`
|
||||
returns `None`. **This is the obstruction** the bare canonical colouring hits
|
||||
(haloed in panel C). It is *not* non-existence — a removable colouring exists; we
|
||||
just have to reach one by a Kempe switch.
|
||||
|
||||
**The switch.** The bounded search picks the **`{1,2}`-Kempe component through
|
||||
`m(0,3)`**:
|
||||
```
|
||||
component = { m(0,3), m(3,7), m(3,8), m(3,9) } (pair {1,2})
|
||||
```
|
||||
Swapping colours `1↔2` on this component flips `m(0,3): 1→2` and `m(3,8): 1→2`
|
||||
(the other two are already in `{1,2}` and toggle within the class). The quad
|
||||
medials become:
|
||||
```
|
||||
m(0,3)=2 m(0,4)=1 m(4,8)=1 m(3,8)=2
|
||||
```
|
||||
Now `(m_{30},m_{04}) = (2,1)` distinct ✓ and `(m_{48},m_{83}) = (1,2)` distinct ✓,
|
||||
two colours `{1,2}` used → `diamond_condition` returns the **third colour 0**.
|
||||
|
||||
**Collapse.** Delete `w`, restore edge `(3,4)`, and colour the restored medial
|
||||
`m(3,4) = 0` (the orange square in panel D). The result is verified to be a
|
||||
**proper 3-colouring of M(G)** on exactly the original 9 vertices — i.e. a
|
||||
Tait/4CT colouring of the original triangulation, obtained with no appeal to the
|
||||
4CT.
|
||||
|
||||
---
|
||||
|
||||
## Reproduce
|
||||
|
||||
```bash
|
||||
python3 dump_walkthrough.py # prints every step's data verbatim
|
||||
../../../.venv/bin/python draw_walkthrough.py # the 4-panel figure (repo venv: numpy+matplotlib)
|
||||
```
|
||||
|
||||
The reduction here genuinely exercises a Kempe switch. For larger graphs the
|
||||
same six steps run with more diamonds (one per odd seam, swept over all sites)
|
||||
and more phase/colour-order choices; the open difficulty is purely whether some
|
||||
(site, phase) choice lets every diamond's quad become reducible simultaneously
|
||||
— see `even_program_findings.md`.
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 263 KiB |
+53
-14
@@ -376,20 +376,23 @@ def order_cycle(verts, adj):
|
||||
return None
|
||||
|
||||
|
||||
def canonical_coloring(g: Tri, level, outer, rng=None):
|
||||
"""Canonical colouring of M(g): level-edge medials -> 2 ("colour 3"),
|
||||
non-root annuli alternate 0,1 (random phases), root region solved by DFS.
|
||||
Returns (coloring dict, adj) or (None, reason)."""
|
||||
def coloring_skeleton(g: Tri, level, outer):
|
||||
"""Phase/colorder-independent parts of the canonical colouring:
|
||||
adjacency, the base colouring (level-edge medials -> 2, "colour 3"), the
|
||||
non-root annular medial cycles in a deterministic order, the root cycle,
|
||||
and the free (root + outer-trio) region. Returns (skel, None) or
|
||||
(None, reason)."""
|
||||
adj = medial_adj(g)
|
||||
col = {}
|
||||
base = {}
|
||||
level_edges = [ed for ed in g.edges()
|
||||
if level[tuple(ed)[0]] == level[tuple(ed)[1]]]
|
||||
outer_es = {e(outer[0], outer[1]), e(outer[1], outer[2]),
|
||||
e(outer[2], outer[0])}
|
||||
for ed in level_edges:
|
||||
if ed not in outer_es:
|
||||
col[ed] = 2
|
||||
base[ed] = 2
|
||||
comps, _ = annular_cycles(g, level)
|
||||
nonroot = []
|
||||
root_comp = None
|
||||
free = set(outer_es)
|
||||
for comp in comps:
|
||||
@@ -405,13 +408,38 @@ def canonical_coloring(g: Tri, level, outer, rng=None):
|
||||
continue
|
||||
if len(cyc) % 2 != 0:
|
||||
return None, f"odd-non-root-annulus(len {len(cyc)})"
|
||||
phase = rng.randint(0, 1) if rng else 0
|
||||
for i, m in enumerate(cyc):
|
||||
col[m] = (i + phase) % 2
|
||||
nonroot.append(cyc)
|
||||
if root_comp is None:
|
||||
return None, "no-root-annulus"
|
||||
# solve the free region (root annulus + outer trio) by DFS; if that
|
||||
# fails, grow the defect region one level at a time.
|
||||
|
||||
def annulus_key(cyc):
|
||||
depth = min(min(level[a], level[b]) for a, b in (tuple(ed) for ed in cyc))
|
||||
return (depth, tuple(sorted(tuple(sorted(tuple(ed))) for ed in cyc)))
|
||||
|
||||
nonroot.sort(key=annulus_key)
|
||||
return {"adj": adj, "base": base, "nonroot": nonroot, "root": root_comp,
|
||||
"free": free, "outer_es": outer_es}, None
|
||||
|
||||
|
||||
def canonical_coloring_explicit(g: Tri, level, outer, phases, colorder):
|
||||
"""Canonical colouring with EXPLICIT control knobs:
|
||||
phases -- tuple of one bit per non-root annulus (in skeleton order):
|
||||
the {0,1} alternation phase of that annular medial cycle.
|
||||
colorder -- the colour-priority list [.,.,.] used by the root-region DFS.
|
||||
Returns (coloring dict, None) or (None, reason)."""
|
||||
skel, reason = coloring_skeleton(g, level, outer)
|
||||
if skel is None:
|
||||
return None, reason
|
||||
if len(phases) != len(skel["nonroot"]):
|
||||
return None, "phase-arity-mismatch"
|
||||
adj, free = skel["adj"], skel["free"]
|
||||
col = dict(skel["base"])
|
||||
for i, cyc in enumerate(skel["nonroot"]):
|
||||
ph = phases[i] & 1
|
||||
for j, m in enumerate(cyc):
|
||||
col[m] = (j + ph) % 2
|
||||
# solve the free region (root annulus + outer trio) by DFS; if that fails,
|
||||
# grow the defect region one level at a time.
|
||||
max_level = max(level.values())
|
||||
for grow in range(0, max_level + 1):
|
||||
free_now = set(free)
|
||||
@@ -423,9 +451,6 @@ def canonical_coloring(g: Tri, level, outer, rng=None):
|
||||
trial = {m: c for m, c in col.items() if m not in free_now}
|
||||
order = sorted(free_now,
|
||||
key=lambda m: -sum(1 for x in adj[m] if x in trial))
|
||||
colorder = [0, 1, 2]
|
||||
if rng:
|
||||
rng.shuffle(colorder)
|
||||
|
||||
def ok(m, c):
|
||||
return all(trial.get(x) != c for x in adj[m])
|
||||
@@ -452,6 +477,20 @@ def canonical_coloring(g: Tri, level, outer, rng=None):
|
||||
return None, "root-unsolvable"
|
||||
|
||||
|
||||
def canonical_coloring(g: Tri, level, outer, rng=None):
|
||||
"""Random-phase wrapper over canonical_coloring_explicit (back-compat).
|
||||
Returns (coloring dict, None) or (None, reason)."""
|
||||
skel, reason = coloring_skeleton(g, level, outer)
|
||||
if skel is None:
|
||||
return None, reason
|
||||
phases = tuple(rng.randint(0, 1) if rng else 0
|
||||
for _ in range(len(skel["nonroot"])))
|
||||
colorder = [0, 1, 2]
|
||||
if rng:
|
||||
rng.shuffle(colorder)
|
||||
return canonical_coloring_explicit(g, level, outer, phases, colorder)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Kempe switching + removal conditions.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user