Fix walkthrough figure to use verified planar embeddings
The walkthrough previously used a concentric layout whose outer-triangle->ring spokes can cross -- not a valid plane embedding. Rebuild draw_walkthrough.py on networkx planar_layout with an explicit crossing check: G, G', and the medial M(G') drawn at edge midpoints are each verified crossing-free before rendering. G' is embedded once and reused for panels B/C/D; G reuses it when still planar. The medial-at-midpoints drawing is planar except for the medial triangle of the geometric outer face (its midpoint-chords would cut across the unbounded region), so those three edges are detected via the convex hull and omitted; the remainder is verified crossing-free. Note updated to describe the embedding. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+166
-79
@@ -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), "
|
||||
|
||||
+5
-1
@@ -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)*
|
||||
|
||||
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 263 KiB After Width: | Height: | Size: 188 KiB |
Reference in New Issue
Block a user