435f055d82
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>
155 lines
6.2 KiB
Python
155 lines
6.2 KiB
Python
"""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}')
|