Files
math-research/papers/level_switching/experiments/v_c_question_diagram.py
T
didericis c947ce75ff Add Even Level Graph Generators paper + extend Level Switching reachability
- New paper papers/even_level_graph_generators/: defines Even Level
  Graph (every level cycle even), derived level graphs, intertwining
  trees, and the disjunction conjecture (every maximal planar graph is
  a derived level graph or intertwining tree). Empirically tested
  through n=11: every iso class is at least an intertwining tree, so
  the disjunction holds trivially in this range. The intertwining tree
  disjunct fails at the Tutte graph dual (n=25), so the disjunction
  becomes non-trivial past some unknown threshold.

- Level Switching paper: adds Section 4 (Reachability via edge
  switches) with the two-step argument (Sleator-Tarjan-Thurston for
  Case 1; face-merges for Case 2) and Theorem 4.1 (O(n) edge switches
  suffice to reach all-depth-0).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:44:39 -04:00

159 lines
5.4 KiB
Python

"""Annotated diagram so the user can answer the v_c-rotation
clarification questions.
Shows the 9-vertex L_k with e_0 = (0, 3) highlighted, both candidate
v_c vertices (0 and 3) labelled, and the four (v_c, direction) fans
laid out around each."""
import os
import math
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon, FancyArrowPatch
OUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir)
n = 9
POS = {i: (math.cos(math.radians(90 - i * 360 / n)),
math.sin(math.radians(90 - i * 360 / n))) for i in range(n)}
OUTER_EDGES = [(i, (i + 1) % n) for i in range(n)]
CHORDS = [(0, 2), (0, 3), (3, 5), (3, 6), (0, 6), (6, 8)]
# Inner faces
FACES = {
(0, 1, 2): 0,
(0, 2, 3): 0,
(3, 4, 5): 0,
(3, 5, 6): 0,
(6, 7, 8): 0,
(6, 8, 0): 0,
(0, 3, 6): 1, # F
}
F_idx_face = (0, 3, 6)
Fp_face = (0, 2, 3)
e0 = (0, 3)
def cw_order_around(v):
"""Sort vertices adjacent to v by clockwise angle (decreasing
angle from v, starting at angle 90)."""
adj = set()
for f in FACES:
if v in f:
for u in f:
if u != v:
adj.add(u)
# angle of each adjacent vertex
def angle(u):
return (90 - u * 360 / n) % 360
return sorted(adj, key=lambda u: -angle(u))
fig, axes = plt.subplots(1, 2, figsize=(14, 7))
palette = {0: '#86efac', 1: '#fde68a'}
edge_pal = {0: '#16a34a', 1: '#d97706'}
def draw_base(ax, title):
# Fill faces by depth
for face, depth in FACES.items():
poly = Polygon([POS[v] for v in face], closed=True,
facecolor=palette[depth], edgecolor=edge_pal[depth],
linewidth=1.2, alpha=0.5, zorder=0)
ax.add_patch(poly)
# Edges
for (a, b) in OUTER_EDGES + CHORDS:
color, lw = '#333', 1.2
if {a, b} == set(e0):
color, lw = '#dc2626', 3.4 # e_0 highlighted
ax.plot([POS[a][0], POS[b][0]], [POS[a][1], POS[b][1]],
color=color, linewidth=lw, zorder=1)
# Vertices
for i, (x, y) in POS.items():
color = '#3b82f6' if i in e0 else '#1f2937'
size = 470 if i in e0 else 280
ax.scatter([x], [y], s=size, c=color, edgecolors='black',
linewidths=1.2, zorder=2)
ax.text(x, y, str(i), ha='center', va='center',
fontsize=11 if i in e0 else 9,
color='white', fontweight='bold', zorder=3)
# Annotate F and F'
cx_F = sum(POS[v][0] for v in F_idx_face) / 3
cy_F = sum(POS[v][1] for v in F_idx_face) / 3
ax.text(cx_F, cy_F + 0.1, r'$F$', ha='center', fontsize=14,
color='#92400e', fontweight='bold', zorder=4)
ax.text(cx_F, cy_F - 0.1, '(depth 1)', ha='center', fontsize=9,
color='#92400e', zorder=4)
cx_Fp = sum(POS[v][0] for v in Fp_face) / 3
cy_Fp = sum(POS[v][1] for v in Fp_face) / 3
ax.text(cx_Fp + 0.1, cy_Fp, r"$F'$" + '\n(depth 0)', ha='center',
fontsize=10, color='#16a34a', fontweight='bold', zorder=4)
# F''s outer-cycle edge: (2, 3)
px, py = POS[2], POS[3]
mid = ((px[0] + py[0]) / 2, (px[1] + py[1]) / 2)
ax.annotate("outer edge of $F'$",
xy=(mid[0] + 0.05, mid[1] - 0.05),
xytext=(1.35, 0.5),
fontsize=9, color='#16a34a',
arrowprops=dict(arrowstyle='->', color='#16a34a',
lw=1.0))
ax.set_aspect('equal'); ax.axis('off')
ax.set_xlim(-1.5, 1.7); ax.set_ylim(-1.4, 1.4)
ax.set_title(title, fontsize=12)
def annotate_fan(ax, vc, label):
"""Draw arrows around v_c showing CW order of edges."""
cw_neighbours = cw_order_around(vc)
# rotate so the e_0 neighbour comes first
e0_other = [u for u in e0 if u != vc][0]
idx = cw_neighbours.index(e0_other)
cw_neighbours = cw_neighbours[idx:] + cw_neighbours[:idx]
px, py = POS[vc]
# Draw a partial arc around v_c
for i, u in enumerate(cw_neighbours):
# midpoint between v_c and u, slightly inside
ux, uy = POS[u]
midx = px * 0.7 + ux * 0.3
midy = py * 0.7 + uy * 0.3
# is edge on outer cycle?
is_outer = (min(vc, u), max(vc, u)) in OUTER_EDGES or \
(max(vc, u), min(vc, u)) in OUTER_EDGES
marker_color = '#1d4ed8' if not is_outer else '#dc2626'
marker = 'o'
ax.scatter([midx], [midy], s=120, c=marker_color, marker=marker,
edgecolors='white', linewidths=1.5, zorder=5)
ax.text(midx, midy, str(i + 1), ha='center', va='center',
fontsize=8, color='white', fontweight='bold', zorder=6)
# Label
ax.text(px + 0.05, py + 0.25, label, ha='center',
fontsize=11, color='#1d4ed8', fontweight='bold')
draw_base(axes[0], r'Option A: $v_c = 0$ (CW order: 1=$(0,3)$, 2=$(0,6)$, 3=$(0,8)$ outer ...)')
annotate_fan(axes[0], 0, r'$v_c = 0$')
draw_base(axes[1], r'Option B: $v_c = 3$ (CW order: 1=$(0,3)$, 2=$(2,3)$ outer ...)')
annotate_fan(axes[1], 3, r'$v_c = 3$')
# Add legend below
fig.text(0.5, 0.02,
'Blue circles = chord edges (would be switched). '
'Red circles = outer-cycle edges (algorithm stops here). '
'Numbers = clockwise order around $v_c$ starting from $e_0$.',
ha='center', fontsize=10)
fig.tight_layout(rect=[0, 0.04, 1, 1])
out = os.path.join(OUT_DIR, 'fig_v_c_question.png')
fig.savefig(out, dpi=180, bbox_inches='tight')
plt.close(fig)
print(f'wrote {out}')