nested_level_duals: scaffold paper (shelved for alternative approach)
New paper introducing the dual (inner/weak dual) of a maximal planar graph, dual depth (BFS-derived min level over a face's vertices), and a Tait-based framing of a minimal 4CT counterexample via nested level duals. Includes a dual-depth figure and its generator. Shelved per closing note in favour of an alternative approach. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
"""Draw a diagram illustrating dual depth.
|
||||
|
||||
We build a concentric ("stacked rings") triangulation G with a single level
|
||||
source S = {0} at the centre, so the vertices fall into clean BFS levels
|
||||
0, 1, 2, 3 by radius. We then overlay the inner (weak) dual G' -- one dual
|
||||
vertex per bounded triangular face -- and colour each dual vertex by its dual
|
||||
depth: the minimum level among the three vertices of the corresponding face.
|
||||
|
||||
The construction makes all three attainable dual depths (0, 1, 2) appear; the
|
||||
outer face (the level-3 triangle) is excluded from the inner dual.
|
||||
"""
|
||||
import math
|
||||
import os
|
||||
import networkx as nx
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.lines import Line2D
|
||||
|
||||
OUT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Construct G: centre 0 (level 0) plus three triangular rings at radii 1,2,3.
|
||||
# Vertex j of every ring sits at angle 90 + 120*j degrees, so spokes are radial.
|
||||
# ---------------------------------------------------------------------------
|
||||
RINGS = 3 # ring 3 is the outer-face triangle
|
||||
pos = {0: (0.0, 0.0)}
|
||||
ring = {0: [0]} # ring index -> vertex ids
|
||||
nxt = 1
|
||||
for r in range(1, RINGS + 1):
|
||||
ids = []
|
||||
for j in range(3):
|
||||
ang = math.radians(90 + 120 * j)
|
||||
pos[nxt] = (r * math.cos(ang), r * math.sin(ang))
|
||||
ids.append(nxt)
|
||||
nxt += 1
|
||||
ring[r] = ids
|
||||
|
||||
G = nx.Graph()
|
||||
G.add_nodes_from(pos)
|
||||
|
||||
# centre fan
|
||||
for v in ring[1]:
|
||||
G.add_edge(0, v)
|
||||
# ring triangles
|
||||
for r in range(1, RINGS + 1):
|
||||
a, b, c = ring[r]
|
||||
G.add_edges_from([(a, b), (b, c), (c, a)])
|
||||
# radial spokes + annulus diagonals (inner_j -- outer_{j+1})
|
||||
for r in range(1, RINGS):
|
||||
inner, outer = ring[r], ring[r + 1]
|
||||
for j in range(3):
|
||||
G.add_edge(inner[j], outer[j]) # spoke
|
||||
G.add_edge(inner[j], outer[(j + 1) % 3]) # diagonal
|
||||
|
||||
assert G.number_of_edges() == 3 * G.number_of_nodes() - 6, "not a triangulation"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Levels: BFS distance from the source S = {0}.
|
||||
# ---------------------------------------------------------------------------
|
||||
S = {0}
|
||||
level = nx.multi_source_dijkstra_path_length(G, S)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bounded faces (the inner dual's vertices). Listed explicitly from the
|
||||
# construction; the outer face (ring 3 triangle) is omitted.
|
||||
# ---------------------------------------------------------------------------
|
||||
faces = []
|
||||
a, b, c = ring[1]
|
||||
faces += [(0, a, b), (0, b, c), (0, c, a)] # centre fan
|
||||
for r in range(1, RINGS):
|
||||
inn, out = ring[r], ring[r + 1]
|
||||
for j in range(3):
|
||||
i0, i1 = inn[j], inn[(j + 1) % 3]
|
||||
o1 = out[(j + 1) % 3]
|
||||
o0 = out[j]
|
||||
faces += [(i0, i1, o1), (i0, o1, o0)] # the two annulus triangles
|
||||
|
||||
def dual_depth(face):
|
||||
return min(level[v] for v in face)
|
||||
|
||||
# dual vertex positions = face centroids
|
||||
dpos = {}
|
||||
ddepth = {}
|
||||
for f in faces:
|
||||
cx = sum(pos[v][0] for v in f) / 3.0
|
||||
cy = sum(pos[v][1] for v in f) / 3.0
|
||||
dpos[f] = (cx, cy)
|
||||
ddepth[f] = dual_depth(f)
|
||||
|
||||
# dual edges: two bounded faces sharing an edge of G
|
||||
edge_faces = {}
|
||||
for f in faces:
|
||||
for i in range(3):
|
||||
e = frozenset((f[i], f[(i + 1) % 3]))
|
||||
edge_faces.setdefault(e, []).append(f)
|
||||
dual_edges = [fs for fs in edge_faces.values() if len(fs) == 2]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Draw.
|
||||
# ---------------------------------------------------------------------------
|
||||
LEVEL_COLOR = {0: '#1e293b', 1: '#475569', 2: '#94a3b8', 3: '#cbd5e1'}
|
||||
DEPTH_COLOR = {0: '#16a34a', 1: '#2563eb', 2: '#dc2626'}
|
||||
|
||||
fig, ax = plt.subplots(figsize=(9, 9))
|
||||
|
||||
# G edges (light) and vertices (labelled by level)
|
||||
nx.draw_networkx_edges(G, pos, ax=ax, edge_color='#d1d5db', width=1.4)
|
||||
for v, (x, y) in pos.items():
|
||||
ax.scatter([x], [y], s=520, color=LEVEL_COLOR[level[v]],
|
||||
edgecolors='black', linewidths=1.0, zorder=3)
|
||||
ax.text(x, y, f'{v}\n$\\ell{{=}}{level[v]}$', ha='center', va='center',
|
||||
color='white', fontsize=8.5, fontweight='bold', zorder=4)
|
||||
|
||||
# dual edges (dashed) and dual vertices (squares, coloured by dual depth)
|
||||
for f0, f1 in dual_edges:
|
||||
(x0, y0), (x1, y1) = dpos[f0], dpos[f1]
|
||||
ax.plot([x0, x1], [y0, y1], color='#fb923c', lw=1.3, ls='--', zorder=2)
|
||||
for f, (x, y) in dpos.items():
|
||||
ax.scatter([x], [y], s=300, marker='s', color=DEPTH_COLOR[ddepth[f]],
|
||||
edgecolors='black', linewidths=0.8, zorder=5)
|
||||
ax.text(x, y, str(ddepth[f]), ha='center', va='center',
|
||||
color='white', fontsize=9, fontweight='bold', zorder=6)
|
||||
|
||||
legend = [
|
||||
Line2D([0], [0], marker='o', color='w', label='$G$ vertex (label = level $\\ell$)',
|
||||
markerfacecolor='#475569', markeredgecolor='black', markersize=12),
|
||||
Line2D([0], [0], color='#fb923c', ls='--', lw=1.3, label="dual edge of $G'$"),
|
||||
Line2D([0], [0], marker='s', color='w', label='dual depth $\\delta = 0$',
|
||||
markerfacecolor=DEPTH_COLOR[0], markeredgecolor='black', markersize=11),
|
||||
Line2D([0], [0], marker='s', color='w', label='dual depth $\\delta = 1$',
|
||||
markerfacecolor=DEPTH_COLOR[1], markeredgecolor='black', markersize=11),
|
||||
Line2D([0], [0], marker='s', color='w', label='dual depth $\\delta = 2$',
|
||||
markerfacecolor=DEPTH_COLOR[2], markeredgecolor='black', markersize=11),
|
||||
]
|
||||
ax.legend(handles=legend, loc='upper left', fontsize=10, framealpha=0.95)
|
||||
|
||||
ax.set_aspect('equal')
|
||||
ax.axis('off')
|
||||
ax.set_title("Dual depth in a stacked-ring triangulation $G$ with source $S=\\{0\\}$.\n"
|
||||
"Each bounded face carries a dual vertex (square) coloured by its dual "
|
||||
"depth\n$\\delta(d_f)=\\min_{v\\in V(f)}\\ell(v)$. "
|
||||
"The outer face (level-3 triangle) has no dual vertex.",
|
||||
fontsize=11)
|
||||
fig.tight_layout()
|
||||
|
||||
out = os.path.join(OUT_DIR, 'fig_dual_depth.png')
|
||||
fig.savefig(out, dpi=180, bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
|
||||
# console summary
|
||||
from collections import Counter
|
||||
print(f'n={G.number_of_nodes()} edges={G.number_of_edges()} '
|
||||
f'bounded faces={len(faces)} dual edges={len(dual_edges)}')
|
||||
print('dual depth distribution:', dict(sorted(Counter(ddepth.values()).items())))
|
||||
print(f'wrote {out}')
|
||||
Reference in New Issue
Block a user