Add Level Switching paper with surface-switch framework
Defines level cycles, edge switches, surface switches, and facial depth on level components of plane triangulations. Proves outerplanarity of level components and a depth-descent lemma. Introduces balanced surface switches and proves they remove a depth-d level cycle while creating 1-2 new depth-(d-1) cycles. Documents the 9-vertex counterexample where no balanced switch exists and sketches preprocessing toward balancedness, leaving general termination open. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
"""Demonstrate the preprocessing strategy on the 9-vertex example.
|
||||
|
||||
Start: F = (0,3,6) at depth 1, no balanced surface switch exists
|
||||
(F has no edge of "span 1" -- no edge with a single outer-cycle
|
||||
vertex between the endpoints, hence no ear neighbour).
|
||||
|
||||
Step: perform the (unbalanced) surface switch on edge uv = 03, with
|
||||
F' = (0,2,3) and third vertex x = 2; in Case (ii) the flip removes 03
|
||||
and adds wx = 62.
|
||||
|
||||
Result: A = (0,2,6) at depth 1 has edge 02 at span 1, so the ear
|
||||
(0,1,2) is now a balanced-switch target.
|
||||
"""
|
||||
import os
|
||||
import math
|
||||
import networkx as nx
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.patches import Polygon
|
||||
|
||||
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)]
|
||||
outer_set = {frozenset(e) for e in OUTER_EDGES}
|
||||
|
||||
|
||||
def face_edges(f):
|
||||
return {frozenset((f[0], f[1])), frozenset((f[1], f[2])),
|
||||
frozenset((f[0], f[2]))}
|
||||
|
||||
|
||||
def compute_depths(faces):
|
||||
D = nx.Graph()
|
||||
D.add_nodes_from(range(len(faces)))
|
||||
for i, fi in enumerate(faces):
|
||||
for j, fj in enumerate(faces):
|
||||
if i < j and face_edges(fi) & face_edges(fj):
|
||||
D.add_edge(i, j)
|
||||
B = [i for i, f in enumerate(faces)
|
||||
if len(face_edges(f) & outer_set) >= 1]
|
||||
return {i: min(nx.shortest_path_length(D, i, b) for b in B)
|
||||
for i in range(len(faces))}
|
||||
|
||||
|
||||
CHORDS_BEFORE = [(0, 2), (0, 3), (3, 5), (3, 6), (0, 6), (6, 8)]
|
||||
FACES_BEFORE = [
|
||||
(0, 1, 2), (0, 2, 3), (3, 4, 5), (3, 5, 6),
|
||||
(6, 7, 8), (6, 8, 0), (0, 3, 6),
|
||||
]
|
||||
|
||||
# After non-balanced switch on edge 03: remove 03, add 26
|
||||
CHORDS_AFTER = [c for c in CHORDS_BEFORE if set(c) != {0, 3}] + [(2, 6)]
|
||||
FACES_AFTER = [
|
||||
(0, 1, 2), (3, 4, 5), (3, 5, 6),
|
||||
(6, 7, 8), (6, 8, 0),
|
||||
(0, 2, 6), (2, 3, 6),
|
||||
]
|
||||
|
||||
|
||||
def draw(ax, faces, chords, depth, title, highlight_edges=None,
|
||||
green_edges=None):
|
||||
palette = {0: '#86efac', 1: '#fde68a', 2: '#fca5a5'}
|
||||
edge_pal = {0: '#16a34a', 1: '#d97706', 2: '#dc2626'}
|
||||
for i, f in enumerate(faces):
|
||||
d = depth[i]
|
||||
poly = Polygon([POS[v] for v in f], closed=True,
|
||||
facecolor=palette.get(d, '#ddd'),
|
||||
edgecolor=edge_pal.get(d, '#333'),
|
||||
linewidth=1.4, alpha=0.7, zorder=0)
|
||||
ax.add_patch(poly)
|
||||
cx = sum(POS[v][0] for v in f) / 3
|
||||
cy = sum(POS[v][1] for v in f) / 3
|
||||
ax.text(cx, cy, str(d), ha='center', va='center', fontsize=11,
|
||||
color=edge_pal.get(d, '#333'), fontweight='bold')
|
||||
for (a, b) in OUTER_EDGES + chords:
|
||||
color = '#333'; lw = 1.2
|
||||
if highlight_edges and ((a, b) in highlight_edges or
|
||||
(b, a) in highlight_edges):
|
||||
color = '#dc2626'; lw = 3.0
|
||||
if green_edges and ((a, b) in green_edges or
|
||||
(b, a) in green_edges):
|
||||
color = '#16a34a'; lw = 3.0
|
||||
ax.plot([POS[a][0], POS[b][0]], [POS[a][1], POS[b][1]],
|
||||
color=color, linewidth=lw, zorder=1)
|
||||
for i, (x, y) in POS.items():
|
||||
ax.scatter([x], [y], s=270, c='#1f2937', edgecolors='black',
|
||||
linewidths=1.0, zorder=2)
|
||||
ax.text(x, y, str(i), ha='center', va='center',
|
||||
fontsize=9, color='white', fontweight='bold', zorder=3)
|
||||
ax.set_aspect('equal'); ax.axis('off')
|
||||
ax.set_xlim(-1.3, 1.3); ax.set_ylim(-1.3, 1.3)
|
||||
ax.set_title(title, fontsize=11)
|
||||
|
||||
|
||||
depth_before = compute_depths(FACES_BEFORE)
|
||||
depth_after = compute_depths(FACES_AFTER)
|
||||
print('BEFORE:')
|
||||
for i, f in enumerate(FACES_BEFORE):
|
||||
print(f' {f} -> depth {depth_before[i]}')
|
||||
print('AFTER:')
|
||||
for i, f in enumerate(FACES_AFTER):
|
||||
print(f' {f} -> depth {depth_after[i]}')
|
||||
|
||||
fig, axes = plt.subplots(1, 2, figsize=(14, 7))
|
||||
draw(axes[0], FACES_BEFORE, CHORDS_BEFORE, depth_before,
|
||||
'Before: F=(0,3,6) depth 1; spans (2,2,2) so no ear neighbour',
|
||||
highlight_edges=[(0, 3)])
|
||||
draw(axes[1], FACES_AFTER, CHORDS_AFTER, depth_after,
|
||||
'After non-balanced switch 03->26: A=(0,2,6) depth 1; edge 02 has span 1',
|
||||
green_edges=[(2, 6)], highlight_edges=[(0, 2)])
|
||||
|
||||
fig.tight_layout()
|
||||
out = os.path.join(OUT_DIR, 'fig_preprocessing.png')
|
||||
fig.savefig(out, dpi=180, bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
print(f'wrote {out}')
|
||||
Reference in New Issue
Block a user