Files
math-research/papers/nested_level_duals/experiments/draw_dual_depth.py
T
didericis 435f055d82 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>
2026-05-22 17:36:11 -04:00

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}')