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 index c4f386a..0735230 100644 --- a/papers/medial_tire_decompositions_of_plane_triangulations/experiments/draw_walkthrough.py +++ b/papers/medial_tire_decompositions_of_plane_triangulations/experiments/draw_walkthrough.py @@ -16,11 +16,16 @@ colour order = (1,0,2). Panels: {(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. +Embedding: networkx planar_layout (canonical-ordering straight-line embedding), +recentred, with EVERY panel verified crossing-free before drawing -- G, G', and +the medial M(G') drawn at edge midpoints. G' is embedded once and reused for +panels B/C/D; G reuses it (minus w, with the diagonal 3-4 restored) when that is +still crossing-free, else it is embedded independently. Run with the repo venv +python (numpy + matplotlib + networkx). """ import os, random import numpy as np +import networkx as nx import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt @@ -29,82 +34,48 @@ 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 planar_pos(g): + nxg = nx.Graph() + nxg.add_nodes_from(g.rot) + for ed in g.edges(): + a, b = tuple(ed); nxg.add_edge(a, b) + ok, _ = nx.check_planarity(nxg) + assert ok, "graph not planar?!" + pos = nx.planar_layout(nxg) + pts = np.array([pos[v] for v in g.rot]); c = pts.mean(axis=0) + s = np.abs(pts - c).max() + return {v: ((pos[v][0]-c[0])/s, (pos[v][1]-c[1])/s) for v in g.rot} + + +def seg_cross(p, q, r, s): + def o(a, b, c): + return (b[0]-a[0])*(c[1]-a[1]) - (b[1]-a[1])*(c[0]-a[0]) + d1, d2, d3, d4 = o(r, s, p), o(r, s, q), o(p, q, r), o(p, q, s) + return ((d1 > 0) != (d2 > 0)) and ((d3 > 0) != (d4 > 0)) + + +def crossings(edges, pos): + bad = [] + for i in range(len(edges)): + a, b = edges[i] + for j in range(i+1, len(edges)): + c, d = edges[j] + if len({a, b, c, d}) < 4: + continue + if seg_cross(pos[a], pos[b], pos[c], pos[d]): + bad.append((edges[i], edges[j])) + return bad + 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) - +# ---- build G and G' ------------------------------------------------------ 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) +ring = [c for k, c in an.seams if k == 1][0] prep = H._prep_gadgets(g.copy(), outer) template, an_g, gadgets = prep @@ -112,39 +83,155 @@ 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) +quad = H.quad_of(gg, w, u, v) # (3,0,4,8) +# embed G' (verified), reuse for G if still crossing-free else embed G alone +posGp = planar_pos(gg) +edgesGp = [tuple(e) for e in gg.edges()] +badGp = crossings(edgesGp, posGp) +print("G' crossings:", badGp if badGp else "NONE") +assert not badGp + +posG = {vv: posGp[vv] for vv in g.rot} +edgesG = [tuple(e) for e in g.edges()] +badG = crossings(edgesG, posG) +if badG: + print("G reuse crossed; embedding G independently") + posG = planar_pos(g) + badG = crossings([tuple(e) for e in g.edges()], posG) +print("G crossings:", badG if badG else "NONE") +assert not badG + +# medial drawn at edge midpoints. The medial drawing at midpoints is planar +# EXCEPT for the medial triangle of whichever face is geometrically OUTER +# (its three midpoint-chords would cut straight across the unbounded region), so +# we omit exactly those three edges, then verify the rest are crossing-free. +def convex_hull(points): + pts = sorted(points) + def cross(o, a, b): + return (a[0]-o[0])*(b[1]-o[1]) - (a[1]-o[1])*(b[0]-o[0]) + lo = [] + for p in pts: + while len(lo) >= 2 and cross(lo[-2], lo[-1], p) <= 0: + lo.pop() + lo.append(p) + up = [] + for p in reversed(pts): + while len(up) >= 2 and cross(up[-2], up[-1], p) <= 0: + up.pop() + up.append(p) + return lo[:-1] + up[:-1] + +adjm = H.medial_adj(gg) +mpos = {m: mid(posGp[tuple(m)[0]], posGp[tuple(m)[1]]) for m in adjm} +hull = convex_hull(list(posGp.values())) +pos2v = {tuple(p): v for v, p in posGp.items()} +outer_face = {pos2v[tuple(p)] for p in hull} +print("geometric outer face (hull):", sorted(outer_face)) +medges = [] +seen = set() +for m in adjm: + for b in adjm[m]: + k = frozenset((m, b)) + if k in seen: + continue + seen.add(k) + # skip the medial edge joining two edges of the outer face + if set(m) <= outer_face and set(b) <= outer_face: + continue + medges.append((m, b)) +badM = crossings(medges, mpos) +print("M(G') crossings (outer-face medial triangle omitted):", + badM if badM else "NONE") +assert not badM + +# ---- colourings ---------------------------------------------------------- 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 +# ---- drawing ------------------------------------------------------------- +def lims(ax, pos): + xs = [p[0] for p in pos.values()]; ys = [p[1] for p in pos.values()] + ax.set_xlim(min(xs)-0.25, max(xs)+0.25); ax.set_ylim(min(ys)-0.25, max(ys)+0.3) + +def draw_graph(ax, gr, pos, level=None, bold_cycle=None, shade_quad=None, wvert=None): + if shade_quad: + ax.add_patch(Polygon([pos[vv] for vv 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 gr.edges(): + a, b = tuple(ed); pa, pb = pos[a], pos[b] + hot = ed in bold + ax.plot([pa[0], pb[0]], [pa[1], pb[1]], + color="#d62728" if hot else "#888888", + lw=2.8 if hot else 1.1, zorder=2) + for vv, p in pos.items(): + c = "#d62728" if (wvert is not None and vv == wvert) else "#222222" + ax.plot(*p, "o", ms=18, mfc="white", mec=c, mew=1.7, zorder=5) + ax.annotate(str(vv), p, ha="center", va="center", fontsize=9, + fontweight="bold", color=c, zorder=6) + if level is not None: + ax.annotate(f"L{level[vv]}", p, textcoords="offset points", + xytext=(11, 10), fontsize=6.5, color="#999999", zorder=6) + +def draw_medial(ax, pos, col, halo=None, restored=None): + for ed in gg.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) + for m, b in medges: + pa, pb = mpos[m], mpos[b] + ax.plot([pa[0], pb[0]], [pa[1], pb[1]], color="#c6c6c6", 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) + ax.plot(*mid(pos[a], pos[b]), "s", color=PAL[col[H.e(a, b)]], ms=13, + mec="#d62728", mew=2.0, zorder=7) + halo = halo or set() + for m, p in mpos.items(): + if m not in col: + continue + if m in halo: + ax.plot(*p, "o", color="#000000", ms=16, zorder=5) + ax.plot(*p, "o", color=PAL[col[m]], ms=10.5, mec="black", mew=0.8, zorder=6) + 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) + ax.set_aspect("equal"); ax.axis("off") draw_graph(axes[0], g, posG, level=an.level, bold_cycle=ring) +lims(axes[0], posG) 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) + "odd level-1 seam = 5-cycle 3-4-5-6-7 (red)\n" + "verified straight-line planar embedding", fontsize=9) + draw_graph(axes[1], gg, posGp, level=an2.level, bold_cycle=ring2, shade_quad=quad, wvert=w) +lims(axes[1], posGp) 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) +draw_medial(axes[2], posGp, col0, halo=quad_med) +lims(axes[2], posGp) 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)) + +draw_medial(axes[3], posGp, col1, halo=comp, restored=(3, 4)) +lims(axes[3], posGp) 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), " 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 index ec59f13..bc7d622 100644 --- 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 @@ -38,7 +38,11 @@ The 14 triangular faces (one is the outer/unbounded face) are (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. +The figure draws G (and G′, and the medial M(G′) at edge midpoints) with a +straight-line planar embedding from `networkx.planar_layout`, each **verified +crossing-free** before rendering (the medial triangle of the geometric outer +face is omitted, since its midpoint-chords would otherwise cut across the +unbounded region). ## Step 2 — Pick the source and read off levels *(panel A)* 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 index b3891de..89308df 100644 Binary files a/papers/medial_tire_decompositions_of_plane_triangulations/experiments/even_program_walkthrough.png and b/papers/medial_tire_decompositions_of_plane_triangulations/experiments/even_program_walkthrough.png differ