Files
didericis 7183dc1b67 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>
2026-05-20 23:08:22 -04:00

180 lines
6.1 KiB
Python

"""Counterexample showing that a surface switch on the edge between a
depth-d face F and a depth-(d-1) face F' can create a new face of depth
d (not d-1) when the depth-0 neighbor of F' lies on only one side of
the shared edge.
14-vertex maximal outerplanar L_k. Outer cycle order:
u, p1, p2, p3, x, q1, v, b1, b2, b3, w, a1, a2, a3 -> u
Central triangle F = (u, v, w) has depth 2.
F' = (u, v, x) has depth 1 (its depth-0 neighbor is the q1-ear on the
v-side of x; its u-side neighbor A_ux is depth 1).
A_uw = (u, a2, w), A_vw = (v, b2, w) are both depth 1.
Surface switch on uv: flip uv -> wx. New faces are
A = (u, w, x) and B = (v, w, x).
B inherits the depth-0 q1-ear, so depth(B) = 1.
A's neighbors are A_uw (1), A_ux (1), B (1), so depth(A) = 2 = d. BAD.
"""
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)
OUTER = ['u', 'p1', 'p2', 'p3', 'x', 'q1', 'v',
'b1', 'b2', 'b3', 'w', 'a1', 'a2', 'a3']
n = len(OUTER)
POS = {}
for i, name in enumerate(OUTER):
a = math.radians(90 - i * (360 / n))
POS[name] = (math.cos(a), math.sin(a))
OUTER_EDGES = [(OUTER[i], OUTER[(i + 1) % n]) for i in range(n)]
CHORDS_BEFORE = [
('u', 'v'), ('u', 'w'), ('v', 'w'), # central F = (u,v,w)
('u', 'x'), ('v', 'x'), # F' = (u,v,x)
('u', 'a2'), ('a2', 'w'), # A_uw = (u,a2,w)
('v', 'b2'), ('b2', 'w'), # A_vw = (v,b2,w)
('u', 'p2'), ('p2', 'x'), # A_ux = (u,p2,x)
]
CHORDS_AFTER = [c for c in CHORDS_BEFORE if set(c) != {'u', 'v'}] + [('w', 'x')]
FACES_BEFORE = [
('u', 'v', 'w'), # F (depth 2 -- bad)
('u', 'v', 'x'), # F' (depth 1)
('u', 'a2', 'w'), # A_uw (depth 1)
('v', 'b2', 'w'), # A_vw (depth 1)
('u', 'p2', 'x'), # A_ux (depth 1)
('v', 'q1', 'x'), # A_vx (depth 0)
('u', 'a1', 'a2'), # depth 0
('a2', 'a3', 'w'), # depth 0
('v', 'b1', 'b2'), # depth 0
('b2', 'b3', 'w'), # depth 0
('u', 'p1', 'p2'), # depth 0
('p2', 'p3', 'x'), # depth 0
]
FACES_AFTER = [
('u', 'w', 'x'), # A (depth 2 -- still bad!)
('v', 'w', 'x'), # B (depth 1)
('u', 'a2', 'w'),
('v', 'b2', 'w'),
('u', 'p2', 'x'),
('v', 'q1', 'x'),
('u', 'a1', 'a2'),
('a2', 'a3', 'w'),
('v', 'b1', 'b2'),
('b2', 'b3', 'w'),
('u', 'p1', 'p2'),
('p2', 'p3', 'x'),
]
def compute_depths(faces, chords):
"""Compute facial depth for each face using threshold-1 definition."""
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]))}
# Build inner-face dual
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]
depth = {}
for i in range(len(faces)):
if not B:
depth[i] = float('inf')
continue
depth[i] = min(nx.shortest_path_length(D, i, b) for b in B)
return depth
def draw_panel(ax, faces, chords, depth, title, highlight_edge=None,
highlight_face=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]
face_color = palette.get(d, '#ddd')
face_edge = edge_pal.get(d, '#333')
lw = 1.4
if highlight_face is not None and i == highlight_face:
face_edge = '#7c2d12'
lw = 3.0
poly = Polygon([POS[v] for v in f], closed=True,
facecolor=face_color, edgecolor=face_edge,
linewidth=lw, 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')
# Draw edges
all_edges = OUTER_EDGES + list(chords)
for (a, b) in all_edges:
color = '#333'; lw = 1.2
if highlight_edge is not None and {a, b} == set(highlight_edge):
color = '#dc2626'; lw = 3.5
elif (a, b) == ('w', 'x') or (a, b) == ('x', 'w'):
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)
# Draw vertices
for name, (x, y) in POS.items():
ax.scatter([x], [y], s=260, c='#1f2937', edgecolors='black',
linewidths=0.8, zorder=2)
ax.text(x, y, name, ha='center', va='center',
fontsize=8, color='white', fontweight='bold', zorder=3)
ax.set_aspect('equal'); ax.axis('off')
ax.set_xlim(-1.35, 1.35); ax.set_ylim(-1.35, 1.35)
ax.set_title(title, fontsize=12)
def main():
depth_before = compute_depths(FACES_BEFORE, CHORDS_BEFORE)
depth_after = compute_depths(FACES_AFTER, CHORDS_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=(15, 7.5))
draw_panel(axes[0], FACES_BEFORE, CHORDS_BEFORE, depth_before,
r'Before: $F=(u,v,w)$ depth 2, $F\'=(u,v,x)$ depth 1',
highlight_edge=('u', 'v'), highlight_face=0)
draw_panel(axes[1], FACES_AFTER, CHORDS_AFTER, depth_after,
r'After surface switch on $uv$: $A=(u,w,x)$ still depth 2!',
highlight_face=0)
fig.tight_layout()
out = os.path.join(OUT_DIR, 'fig_counterexample_surface_switch.png')
fig.savefig(out, dpi=180, bbox_inches='tight')
plt.close(fig)
print(f'wrote {out}')
if __name__ == '__main__':
main()