diff --git a/papers/medial_tire_decompositions_of_plane_triangulations/experiments/draw_walkthrough.py b/papers/medial_tire_decompositions_of_plane_triangulations/experiments/draw_walkthrough.py new file mode 100644 index 0000000..c4f386a --- /dev/null +++ b/papers/medial_tire_decompositions_of_plane_triangulations/experiments/draw_walkthrough.py @@ -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) diff --git a/papers/medial_tire_decompositions_of_plane_triangulations/experiments/dump_walkthrough.py b/papers/medial_tire_decompositions_of_plane_triangulations/experiments/dump_walkthrough.py new file mode 100644 index 0000000..3956a9c --- /dev/null +++ b/papers/medial_tire_decompositions_of_plane_triangulations/experiments/dump_walkthrough.py @@ -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() diff --git a/papers/medial_tire_decompositions_of_plane_triangulations/experiments/even_program_walkthrough.md b/papers/medial_tire_decompositions_of_plane_triangulations/experiments/even_program_walkthrough.md new file mode 100644 index 0000000..ec59f13 --- /dev/null +++ b/papers/medial_tire_decompositions_of_plane_triangulations/experiments/even_program_walkthrough.md @@ -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`. diff --git a/papers/medial_tire_decompositions_of_plane_triangulations/experiments/even_program_walkthrough.png b/papers/medial_tire_decompositions_of_plane_triangulations/experiments/even_program_walkthrough.png new file mode 100644 index 0000000..b3891de Binary files /dev/null and b/papers/medial_tire_decompositions_of_plane_triangulations/experiments/even_program_walkthrough.png differ diff --git a/papers/medial_tire_decompositions_of_plane_triangulations/experiments/kempe_even_program_harness.py b/papers/medial_tire_decompositions_of_plane_triangulations/experiments/kempe_even_program_harness.py index 2053164..bc98478 100644 --- a/papers/medial_tire_decompositions_of_plane_triangulations/experiments/kempe_even_program_harness.py +++ b/papers/medial_tire_decompositions_of_plane_triangulations/experiments/kempe_even_program_harness.py @@ -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. # ---------------------------------------------------------------------------