diff --git a/papers/level_switching/experiments/d2_balanced_existence.py b/papers/level_switching/experiments/d2_balanced_existence.py new file mode 100644 index 0000000..d83bf34 --- /dev/null +++ b/papers/level_switching/experiments/d2_balanced_existence.py @@ -0,0 +1,250 @@ +"""Build a maximal-outerplanar L_k whose unique depth-2 face has NO +balanced surface switch, then test whether preprocessing reaches one. + +Dual-tree blueprint: + + F (depth 2, degree 3) + /|\\ + F1' F2' F3' each depth 1, degree 3 + /\\ /\\ /\\ + E_i G_i (per arm: E_i depth-0 ear, G_i depth-1 degree-3 node) + /\\ + E'_i H_i (G_i's two non-F'_i children: both depth-0 ears) + +Inner-face count: 1 + 3 + 3 + 3 + 3 + 3 = 16. So polygon has n = 18. + +For each F'_i: non-F neighbours are E_i (depth 0) and G_i (depth 1). +NOT balanced (G_i not depth 0). Hence F has no balanced surface switch. + +Concrete chord construction: place vertices 0..17 around the outer cycle. +Allocate one "arm" of 6 outer-cycle vertices per F'_i, plus three vertices +for F. + +Arm i (i = 0,1,2) covers outer-cycle positions [6i, 6i+5]; the F vertices +are positions {0, 6, 12}. +""" +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 = 18 +POS = {i: (math.cos(math.radians(90 - i * 360 / n)), + math.sin(math.radians(90 - i * 360 / n))) for i in range(n)} + + +def outer_edges(num): + return [(i, (i + 1) % num) for i in range(num)] + + +def face_edges(f): + return {frozenset((f[0], f[1])), frozenset((f[1], f[2])), + frozenset((f[0], f[2]))} + + +def compute_depths(faces, outer_edge_set): + 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_edge_set) >= 1] + if not B: + return {i: float('inf') for i in range(len(faces))}, D + depth = {i: min(nx.shortest_path_length(D, i, b) for b in B) + for i in range(len(faces))} + return depth, D + + +# Apex vertices of F at outer positions 0, 6, 12. +# Per arm i: outer positions p = [a, a+1, a+2, a+3, a+4, a+5] where a = 6i. +# Reading positions: a = u_i, a+1 = e1, a+2 = m, a+3 = e2, a+4 = h, a+5 = v_i = u_{i+1} +# +# Inside the polygon arc (u_i ... u_{i+1}) we want to triangulate so that +# the chord u_i--u_{i+1} corresponds to F'_i in the dual tree, with: +# - F'_i adjacent to the apex chord +# - non-apex side of F'_i forks into an ear E_i (depth 0) and the +# degree-3 node G_i (depth 1) +# - G_i further forks into two ears +# +# A clean way to realise this: pick F'_i = (u_i, m, u_{i+1}), then +# E_i = (u_i, e1, m) -- needs chord u_i--m +# G_i = (m, e2, ...) -- hmm, we need G_i to be degree-3 sharing one +# edge with F'_i (chord m--something). +# +# Simpler: pick F'_i = (u_i, e2, u_{i+1}). Then F'_i has chords +# u_i--e2 and e2--u_{i+1}. +# Side u_i..e2 (covers e1, m): triangulate via chord u_i--m. +# E_i = (u_i, e1, m) ear (outer edges u_i--e1, e1--m), depth 0. +# X_i = (u_i, m, e2): edges u_i--m, m--e2 (chord? outer? m--e2 outer +# since m, e2 are adjacent on outer cycle), u_i--e2 chord. +# 1 outer edge: m--e2. Depth 0. +# Hmm X_i is depth 0, not depth 1 as required for G_i. +# This doesn't yet build the 4-deep structure I want. Let me redo. +# +# Required structure per arm to get a depth-1 face G_i: +# need G_i with no outer edges. So all three of G_i's edges are chords. +# each chord shared with another inner face (so G_i has 3 dual-neighbors). +# For G_i depth 1: its three neighbours must include at least one depth-0 +# face. With G_i having 3 chord edges, it has 3 dual-neighbors. +# +# To realise G_i with 3 chord edges, we need >=4 outer-cycle vertices +# inside G_i's region. + +# Let me redesign with more vertices per arm: 7 per arm instead of 6. +# Then n = 3*7 + 3 = 24? Or with apex shared, n = 3*7 = 21. + +# Use n = 21. Apex at positions 0, 7, 14. Per arm i (i=0,1,2): +# positions a = 7i, a+1, a+2, a+3, a+4, a+5, a+6 where a+7 = next apex. +# So outer-cycle vertices in arm i: [u_i = 7i, 7i+1, ..., 7i+6, u_{i+1} = 7(i+1)]. +# That's 7 strict-interior vertices plus the two endpoints. +# Wait, 7 outer-cycle positions from u_i to u_{i+1} (inclusive of both). +# +# Let me use simpler indexing. Apex U_0=0, U_1=7, U_2=14 (n=21). +# Arm 0 covers outer positions 0..7 (inclusive), with internal vertices 1..6. + +print('Recomputing with n=21 layout...') +n = 21 +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 = outer_edges(n) +outer_set = {frozenset(e) for e in OUTER_EDGES} + +# Apex vertices of F: +U0, U1, U2 = 0, 7, 14 +# Edges of F (chords) +F_chords = [(U0, U1), (U1, U2), (U0, U2)] + + +def arm_chords(a, b): + """For an arm from apex a (=7i) to apex b (=7(i+1) mod 21), with + internal outer vertices a+1, a+2, ..., a+6 (6 internal verts), produce: + F'_i = (a, mid, b) for some mid + then sub-triangulate the (a, ..., mid) side and (mid, ..., b) side. + Pick mid = a+4 (middle, gives 3 vertices on each side). + a-side (a, a+1, a+2, a+3, mid=a+4): 4 strict-interior, need triangulating + Add chord a--(a+2) and chord a--(a+4) [already F'_i]. + Triangles: (a, a+1, a+2) ear; (a, a+2, a+3); (a, a+3, a+4). + But (a, a+2, a+3) has outer edge (a+2,a+3); depth 0. + (a, a+3, a+4) has outer edge (a+3, a+4) wait that's not necessarily outer. + Actually a+3, a+4 ARE outer-adjacent. So (a, a+3, a+4) has outer + edge (a+3, a+4); 1 outer edge, depth 0. + Hmm need to engineer the G_i = depth-1 face. + + Try mid = a+3. Then a-side has 2 internal vertices (a+1, a+2); + b-side has 3 (a+4, a+5, a+6). + a-side triangulation: chord a--(a+2). Triangles: + (a, a+1, a+2) ear; (a, a+2, a+3) = E_i? + (a, a+2, a+3): outer edge (a+2, a+3); 1 outer; depth 0. + So E_i = (a, a+2, a+3) depth 0 with chord a-(a+2) leading to + ear (a, a+1, a+2). + + b-side (a+3, a+4, a+5, a+6, b): 4 internal (a+4, a+5, a+6) wait + that's 3. We need a depth-1 G_i in here. Triangulate with chord + (a+3)--(a+5): triangles (a+3, a+4, a+5) ear; (a+3, a+5, b) and + (a+5, a+6, b). + (a+3, a+5, b): edges (a+3,a+5) chord, (a+5, b) chord, (a+3, b) + which is F'_i edge. 0 outer edges. Depth >= 1. + (a+5, a+6, b): edges (a+5, a+6) outer, (a+6, b) outer, (a+5, b) + chord. 2 outer edges; ear; depth 0. + + So (a+3, a+5, b) has 0 outer edges, depth ?. Its neighbours: + across (a+3, a+5): ear (a+3, a+4, a+5) depth 0 + across (a+5, b): ear (a+5, a+6, b) depth 0 + across (a+3, b): F'_i + So depth = 1 (via two depth-0 neighbours). + + F'_i = (a, a+3, b). Edges (a, a+3) chord, (a+3, b) chord, (a, b) + apex-chord (shared with F). 0 outer. Neighbours: + across (a, a+3): E_i = (a, a+2, a+3) depth 0 + across (a+3, b): (a+3, a+5, b) depth 1 = G_i + across (a, b): F depth ? + depth(F'_i) = 1 + min(0, 1) = 1. ✓ + + Non-(a,b) neighbours of F'_i: E_i depth 0 ✓ and G_i depth 1 ✗. + LOPSIDED, hence unbalanced. ✓ + + chords for arm a..b (= a + 7): + (a, a+2), (a, a+3), (a+3, a+5), (a+3, b), (a+5, b) + """ + return [(a, a + 2), (a, a + 3), + (a + 3, a + 5), (a + 3, b), (a + 5, b)] + + +def arm_faces(a, b): + return [ + (a, a + 1, a + 2), # ear of arm + (a, a + 2, a + 3), # E_i: 1 outer edge -> depth 0 + (a + 3, a + 4, a + 5), # ear + (a + 3, a + 5, b), # G_i: 0 outer edges + (a + 5, a + 6, b), # ear + (a, a + 3, b), # F'_i + ] + + +CHORDS = list(F_chords) +FACES = [(U0, U1, U2)] # F itself + +for (a, b) in [(0, 7), (7, 14), (14, 0)]: + CHORDS.extend(arm_chords(a, b)) + FACES.extend(arm_faces(a, b)) + +depth, D = compute_depths(FACES, outer_set) +print(f'Total faces: {len(FACES)}') +for i, f in enumerate(FACES): + print(f' {f} -> depth {depth[i]}') +print(f'B (depth-0 faces): {[FACES[i] for i in range(len(FACES)) if depth[i] == 0]}') + +# Identify the depth-2 face +d2_faces = [i for i in range(len(FACES)) if depth[i] == 2] +print(f'Depth-2 faces: {[FACES[i] for i in d2_faces]}') + + +def check_balanced(F_idx, faces, depth_, outer_edge_set): + """Check if face F_idx admits a balanced surface switch on some edge.""" + F = faces[F_idx] + fe = face_edges(F) + for e in fe: + if e in outer_edge_set: + continue + # Find the inner face sharing e with F + candidates = [j for j in range(len(faces)) + if j != F_idx and e in face_edges(faces[j])] + if not candidates: + continue + Fp_idx = candidates[0] + if depth_[Fp_idx] != depth_[F_idx] - 1: + continue + # Found a depth-(d-1) neighbour F'. Check balancedness. + Fp = faces[Fp_idx] + fpe = face_edges(Fp) + other_edges = [e2 for e2 in fpe if e2 != e] + d = depth_[F_idx] + ok = True + for e2 in other_edges: + if e2 in outer_edge_set: + continue # outer-cycle edge is fine + # find inner face across e2 + others = [j for j in range(len(faces)) + if j != Fp_idx and e2 in face_edges(faces[j])] + if not others: + ok = False + break + other_face = others[0] + if depth_[other_face] != d - 2: + ok = False + break + if ok: + return True, F_idx, Fp_idx, e + return False, None, None, None + + +for F_idx in d2_faces: + ok, _, fp, e = check_balanced(F_idx, FACES, depth, outer_set) + print(f'F = {FACES[F_idx]}: balanced switch exists? {ok}') diff --git a/papers/level_switching/experiments/d2_preprocessing.py b/papers/level_switching/experiments/d2_preprocessing.py new file mode 100644 index 0000000..0450b9d --- /dev/null +++ b/papers/level_switching/experiments/d2_preprocessing.py @@ -0,0 +1,98 @@ +"""Apply preprocessing surface switches to the 21-vertex depth-2 example +and check whether the new depth-2 face admits a balanced surface switch. +If not, iterate.""" +import sys, os +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import math +import networkx as nx +from d2_balanced_existence import ( + POS, n, OUTER_EDGES, outer_set, face_edges, compute_depths, + check_balanced, FACES as FACES0, CHORDS as CHORDS0 +) + + +def apply_switch(faces, chords, uv, ux_vx): + """Apply an edge switch removing edge uv and inserting edge wx. + `ux_vx` = (third-vertex-of-F = w, third-vertex-of-F' = x). + Returns (new_faces, new_chords). + Replaces the two old triangles uvw, uvx with the two new triangles + uwx, vwx. + """ + u, v = uv + w, x = ux_vx + new_chords = [c for c in chords if set(c) != {u, v}] + [tuple(sorted((w, x)))] + new_faces = [] + for f in faces: + if set(f) == {u, v, w} or set(f) == {u, v, x}: + continue + new_faces.append(f) + new_faces.append(tuple(sorted((u, w, x)))) + new_faces.append(tuple(sorted((v, w, x)))) + return new_faces, new_chords + + +def find_third_vertices(faces, uv): + """Find the two faces containing edge uv; return their third vertices.""" + u, v = uv + thirds = [] + for f in faces: + if u in f and v in f: + for vert in f: + if vert not in (u, v): + thirds.append(vert) + break + return thirds + + +def find_max_depth_face(faces, depth): + max_d = max(depth.values()) + return [i for i, d in depth.items() if d == max_d][0], max_d + + +# Initial state +faces = list(FACES0) +chords = list(CHORDS0) +depth, _ = compute_depths(faces, outer_set) +F_idx, d = find_max_depth_face(faces, depth) +F = faces[F_idx] +print(f'Iter 0: F = {F}, depth = {d}') + +for step in range(8): + ok, _, fp_idx, e = check_balanced(F_idx, faces, depth, outer_set) + if ok: + Fp = faces[fp_idx] + print(f' -> balanced switch exists on edge {tuple(e)}, F = {F}, ' + f'F\' = {Fp}. STOP.') + break + + # Pick any depth-(d-1) neighbour for preprocessing. + F_set = set(F) + fp_choice = None + for e_test in [frozenset((F[0], F[1])), frozenset((F[1], F[2])), + frozenset((F[0], F[2]))]: + if e_test in outer_set: + continue + cands = [j for j, fj in enumerate(faces) + if j != F_idx and e_test in face_edges(fj)] + if cands and depth[cands[0]] == d - 1: + fp_choice = (e_test, cands[0]) + break + if fp_choice is None: + print(' no depth-(d-1) neighbour; cannot preprocess.') + break + + e, fp_idx = fp_choice + Fp = faces[fp_idx] + u, v = tuple(e) + w = [vert for vert in F if vert != u and vert != v][0] + x = [vert for vert in Fp if vert != u and vert != v][0] + print(f' preprocessing switch: uv = ({u},{v}), w = {w}, x = {x}, ' + f'F\' = {Fp}') + + faces, chords = apply_switch(faces, chords, (u, v), (w, x)) + depth, _ = compute_depths(faces, outer_set) + F_idx, d = find_max_depth_face(faces, depth) + F = faces[F_idx] + print(f'Iter {step + 1}: max-depth face = {F}, depth = {d}') + +print('Done.') diff --git a/papers/level_switching/experiments/d2_recursive_lopsided.py b/papers/level_switching/experiments/d2_recursive_lopsided.py new file mode 100644 index 0000000..ffbbe28 --- /dev/null +++ b/papers/level_switching/experiments/d2_recursive_lopsided.py @@ -0,0 +1,196 @@ +"""Build a 24-vertex L_k where every F'_i (depth-1 neighbour of F) is +lopsided AND each G_i (depth-1 face inside the arm) is also lopsided. +Then iterate preprocessing and see how many steps it takes.""" +import sys, os +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import math +import networkx as nx + +n = 24 +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] + if not B: + return {i: float('inf') for i in range(len(faces))} + return {i: min(nx.shortest_path_length(D, i, b) for b in B) + for i in range(len(faces))} + + +U0, U1, U2 = 0, 8, 16 + +# Per arm (a, b) with b = a + 8: +# F'_i = (a, a+2, b) +# E_i = (a, a+1, a+2) -- ear +# G_i = (a+2, a+4, b) +# E'_i = (a+2, a+3, a+4) -- ear +# K_i = (a+4, a+6, b) +# ears (a+4, a+5, a+6) and (a+6, a+7, b) + + +def arm(a, b): + chords = [(a, a + 2), (a, b), (a + 2, a + 4), (a + 2, b), + (a + 4, a + 6), (a + 4, b), (a + 6, b)] + faces = [ + (a, a + 1, a + 2), + (a, a + 2, b), # F'_i (depth 1, lopsided) + (a + 2, a + 3, a + 4), + (a + 2, a + 4, b), # G_i (depth 1, lopsided -- its K_i is depth 1) + (a + 4, a + 5, a + 6), + (a + 4, a + 6, b), # K_i (depth 1, balanced -- both ears depth 0) + (a + 6, a + 7, b), + ] + return chords, faces + + +CHORDS = [(U0, U1), (U1, U2), (U0, U2)] +FACES = [(U0, U1, U2)] +for (a, b) in [(0, 8), (8, 16), (16, 24 % n)]: + if b == 0: + # arm from 16 to 0; treat 0 as the apex + c, f = arm(a, n) # use 24 as a placeholder for 0 + # Re-map vertex 24 -> 0 + c = [tuple(0 if v == n else v for v in e) for e in c] + f = [tuple(0 if v == n else v for v in vt) for vt in f] + else: + c, f = arm(a, b) + CHORDS.extend(c) + FACES.extend(f) + +# Dedup chords (the apex chords (U0,U1), (U1,U2), (U0,U2) get re-added) +CHORDS = list(set(frozenset(c) for c in CHORDS)) +CHORDS = [tuple(sorted(c)) for c in CHORDS] + +depth = compute_depths(FACES) +print(f'Total faces: {len(FACES)}') +for i, f in enumerate(FACES): + print(f' {f} -> depth {depth[i]}') + +max_d = max(depth.values()) +print(f'\nMax depth: {max_d}') + + +def check_balanced(F_idx, faces, depth_): + F = faces[F_idx] + fe = face_edges(F) + for e in fe: + if e in outer_set: + continue + cands = [j for j in range(len(faces)) + if j != F_idx and e in face_edges(faces[j])] + if not cands: + continue + Fp_idx = cands[0] + if depth_[Fp_idx] != depth_[F_idx] - 1: + continue + Fp = faces[Fp_idx] + fpe = face_edges(Fp) + d = depth_[F_idx] + ok = True + for e2 in fpe: + if e2 == e: + continue + if e2 in outer_set: + continue + others = [j for j in range(len(faces)) + if j != Fp_idx and e2 in face_edges(faces[j])] + if not others or depth_[others[0]] != d - 2: + ok = False + break + if ok: + return True, F_idx, Fp_idx, e + return False, None, None, None + + +def apply_switch(faces, chords, uv, wx): + u, v = uv + w, x = wx + new_chords = [c for c in chords if set(c) != {u, v}] + \ + [tuple(sorted((w, x)))] + new_faces = [f for f in faces + if set(f) != {u, v, w} and set(f) != {u, v, x}] + new_faces.append(tuple(sorted((u, w, x)))) + new_faces.append(tuple(sorted((v, w, x)))) + return new_faces, new_chords + + +def find_third_vertices(faces, uv): + u, v = uv + thirds = [] + for f in faces: + if u in f and v in f: + for vert in f: + if vert not in (u, v): + thirds.append(vert) + break + return thirds + + +# Iteratively preprocess +faces = list(FACES) +chords = list(CHORDS) +print('\nStarting preprocessing loop on the 24-vertex example...') + +for step in range(20): + depth = compute_depths(faces) + d_max = max(depth.values()) + max_d_faces = [i for i, d in depth.items() if d == d_max] + F_idx = max_d_faces[0] + F = faces[F_idx] + print(f'\nStep {step}: max depth = {d_max}, F = {F}') + if d_max == 0: + print('All depths are 0. DONE.') + break + + ok, _, fp_idx, e = check_balanced(F_idx, faces, depth) + if ok: + Fp = faces[fp_idx] + print(f' balanced switch exists on edge {tuple(e)} with F\' = {Fp}.') + # Apply the balanced switch + u, v = tuple(e) + w = [vert for vert in F if vert not in (u, v)][0] + x = [vert for vert in Fp if vert not in (u, v)][0] + faces, chords = apply_switch(faces, chords, (u, v), (w, x)) + print(f' applied balanced switch ({u},{v}) -> ({w},{x})') + continue + + # Preprocess: pick any depth-(d-1) neighbour and switch + Fset = set(F) + chosen = None + for e_test in [frozenset((F[0], F[1])), frozenset((F[1], F[2])), + frozenset((F[0], F[2]))]: + if e_test in outer_set: + continue + cands = [j for j in range(len(faces)) + if j != F_idx and e_test in face_edges(faces[j])] + if cands and depth[cands[0]] == d_max - 1: + chosen = (e_test, cands[0]) + break + if chosen is None: + print(' No depth-(d-1) neighbour; cannot preprocess.') + break + e, fp_idx = chosen + Fp = faces[fp_idx] + u, v = tuple(e) + w = [vert for vert in F if vert not in (u, v)][0] + x = [vert for vert in Fp if vert not in (u, v)][0] + print(f' preprocessing (unbalanced) switch ({u},{v}) -> ({w},{x})') + faces, chords = apply_switch(faces, chords, (u, v), (w, x)) + +print('\nFinal depth distribution:', sorted(compute_depths(faces).values())) diff --git a/papers/level_switching/experiments/d2_recursive_visualize.py b/papers/level_switching/experiments/d2_recursive_visualize.py new file mode 100644 index 0000000..30f4606 --- /dev/null +++ b/papers/level_switching/experiments/d2_recursive_visualize.py @@ -0,0 +1,70 @@ +"""Visualize the 24-vertex doubly-lopsided d=2 example and the +preprocessing trajectory.""" +import sys, os +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import matplotlib.pyplot as plt +from matplotlib.patches import Polygon +from d2_recursive_lopsided import ( + POS, n, OUTER_EDGES, outer_set, compute_depths, apply_switch, + FACES as FACES0, CHORDS as CHORDS0 +) + +OUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir) + + +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.2, alpha=0.65, 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=9, + color=edge_pal.get(d, '#333'), fontweight='bold') + for (a, b) in OUTER_EDGES + chords: + color = '#333'; lw = 1.1 + if highlight_edges and ((a, b) in highlight_edges or + (b, a) in highlight_edges): + color = '#dc2626'; lw = 2.8 + if green_edges and ((a, b) in green_edges or + (b, a) in green_edges): + color = '#16a34a'; lw = 2.8 + 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=200, c='#1f2937', edgecolors='black', + linewidths=0.6, zorder=2) + ax.text(x, y, str(i), ha='center', va='center', + fontsize=7, color='white', fontweight='bold', zorder=3) + ax.set_aspect('equal'); ax.axis('off') + ax.set_xlim(-1.2, 1.2); ax.set_ylim(-1.2, 1.2) + ax.set_title(title, fontsize=10) + + +depth0 = compute_depths(FACES0) +faces1, chords1 = apply_switch(FACES0, CHORDS0, (0, 8), (16, 2)) +depth1 = compute_depths(faces1) +faces2, chords2 = apply_switch(faces1, chords1, (8, 2), (16, 4)) +depth2 = compute_depths(faces2) + +fig, axes = plt.subplots(1, 3, figsize=(18, 6.5)) +draw(axes[0], FACES0, CHORDS0, depth0, + 'Start: F=(0,8,16) depth 2, all arms doubly-lopsided', + highlight_edges=[(0, 8)]) +draw(axes[1], faces1, chords1, depth1, + 'After preprocess 1: F=(2,8,16) still depth 2, still no balanced switch', + green_edges=[(2, 16)], highlight_edges=[(8, 2)]) +draw(axes[2], faces2, chords2, depth2, + 'After preprocess 2: F=(4,8,16) admits balanced switch on (4,8)', + green_edges=[(4, 16)], highlight_edges=[(4, 8)]) +fig.tight_layout() +out = os.path.join(OUT_DIR, 'fig_d2_recursive.png') +fig.savefig(out, dpi=170, bbox_inches='tight') +plt.close(fig) +print(f'wrote {out}') diff --git a/papers/level_switching/experiments/d2_visualize.py b/papers/level_switching/experiments/d2_visualize.py new file mode 100644 index 0000000..1078add --- /dev/null +++ b/papers/level_switching/experiments/d2_visualize.py @@ -0,0 +1,65 @@ +"""Render the 21-vertex d=2 example and its post-preprocessing state.""" +import sys, os +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import matplotlib.pyplot as plt +from matplotlib.patches import Polygon +from d2_balanced_existence import ( + POS, n, OUTER_EDGES, outer_set, compute_depths, + FACES as FACES0, CHORDS as CHORDS0 +) +from d2_preprocessing import apply_switch + +OUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir) + + +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.65, 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=10, + 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=240, c='#1f2937', edgecolors='black', + linewidths=0.8, zorder=2) + ax.text(x, y, str(i), ha='center', va='center', + fontsize=8, color='white', fontweight='bold', zorder=3) + ax.set_aspect('equal'); ax.axis('off') + ax.set_xlim(-1.25, 1.25); ax.set_ylim(-1.25, 1.25) + ax.set_title(title, fontsize=11) + + +depth0, _ = compute_depths(FACES0, outer_set) +faces1, chords1 = apply_switch(FACES0, CHORDS0, (0, 7), (14, 3)) +depth1, _ = compute_depths(faces1, outer_set) + +fig, axes = plt.subplots(1, 2, figsize=(14, 7)) +draw(axes[0], FACES0, CHORDS0, depth0, + 'Before: F=(0,7,14) depth 2, all three depth-1 neighbours lopsided', + highlight_edges=[(0, 7)]) +draw(axes[1], faces1, chords1, depth1, + 'After preprocessing on (0,7): new F=(3,7,14) admits balanced switch on (3,7)', + green_edges=[(3, 14)], highlight_edges=[(3, 7)]) +fig.tight_layout() +out = os.path.join(OUT_DIR, 'fig_d2_preprocessing.png') +fig.savefig(out, dpi=180, bbox_inches='tight') +plt.close(fig) +print(f'wrote {out}') diff --git a/papers/level_switching/fig_d2_preprocessing.png b/papers/level_switching/fig_d2_preprocessing.png new file mode 100644 index 0000000..f118ff4 Binary files /dev/null and b/papers/level_switching/fig_d2_preprocessing.png differ diff --git a/papers/level_switching/fig_d2_recursive.png b/papers/level_switching/fig_d2_recursive.png new file mode 100644 index 0000000..8f010d0 Binary files /dev/null and b/papers/level_switching/fig_d2_recursive.png differ diff --git a/papers/level_switching/paper.aux b/papers/level_switching/paper.aux index 7cdeaef..32ae500 100644 --- a/papers/level_switching/paper.aux +++ b/papers/level_switching/paper.aux @@ -47,10 +47,14 @@ \newlabel{fig:no-balanced}{{7}{7}{$9$-vertex maximal outerplanar $L_k$. $F = (0,3,6)$ has $\mathrm {depth} = 1$ and all three of its edges have span $2$, so none of $F$'s depth-$0$ neighbours is an ear. No balanced surface switch is available on $F$}{figure.7}{}} \@writefile{lof}{\contentsline {figure}{\numberline {8}{\ignorespaces One step of preprocessing on the $9$-vertex example. Left: $F = (0,3,6)$ has no edge of span $1$; the chosen surface-switch edge $uv = 03$ (red) is unbalanced. Right: after the switch $03 \DOTSB \mapstochar \rightarrow 26$ (green), the new depth-$1$ face $A = (0,2,6)$ has its edge $02$ (red) at span $1$, exposing the ear $(0,1,2)$ as a balanced surface-switch target.}}{7}{figure.8}\protected@file@percent } \newlabel{fig:preprocessing}{{8}{7}{One step of preprocessing on the $9$-vertex example. Left: $F = (0,3,6)$ has no edge of span $1$; the chosen surface-switch edge $uv = 03$ (red) is unbalanced. Right: after the switch $03 \mapsto 26$ (green), the new depth-$1$ face $A = (0,2,6)$ has its edge $02$ (red) at span $1$, exposing the ear $(0,1,2)$ as a balanced surface-switch target}{figure.8}{}} +\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{}{The $d \geq 2$ analog and recursive lopsidedness}}{8}{section*.3}\protected@file@percent } +\@writefile{lof}{\contentsline {figure}{\numberline {9}{\ignorespaces Recursive lopsidedness at $d = 2$. Left: $F = (0,8,16)$ depth $2$, every arm doubly-lopsided. Middle: one preprocessing switch $(0,8) \DOTSB \mapstochar \rightarrow (2,16)$ exposes the first lopsided layer; the new depth-$2$ face $(2,8,16)$ still has no balanced switch. Right: a second preprocessing switch $(8,2) \DOTSB \mapstochar \rightarrow (4,16)$ reaches the inner balanced face $K_0 = (4,6,8)$, whose two non-$F$ neighbours are both ears; the depth-$2$ face $(4,8,16)$ now admits a balanced surface switch on edge $(4,8)$.}}{8}{figure.9}\protected@file@percent } +\newlabel{fig:d2-recursive}{{9}{8}{Recursive lopsidedness at $d = 2$. Left: $F = (0,8,16)$ depth $2$, every arm doubly-lopsided. Middle: one preprocessing switch $(0,8) \mapsto (2,16)$ exposes the first lopsided layer; the new depth-$2$ face $(2,8,16)$ still has no balanced switch. Right: a second preprocessing switch $(8,2) \mapsto (4,16)$ reaches the inner balanced face $K_0 = (4,6,8)$, whose two non-$F$ neighbours are both ears; the depth-$2$ face $(4,8,16)$ now admits a balanced surface switch on edge $(4,8)$}{figure.9}{}} +\@writefile{toc}{\contentsline {subsection}{\tocsubsection {}{}{Empirical termination}}{8}{section*.4}\protected@file@percent } \newlabel{tocindent-1}{0pt} \newlabel{tocindent0}{14.69437pt} \newlabel{tocindent1}{17.77782pt} \newlabel{tocindent2}{0pt} \newlabel{tocindent3}{0pt} -\newlabel{q:preprocessing-terminates}{{3.6}{8}{}{theorem.3.6}{}} -\gdef \@abspage@last{8} +\newlabel{q:preprocessing-terminates}{{3.6}{9}{}{theorem.3.6}{}} +\gdef \@abspage@last{9} diff --git a/papers/level_switching/paper.log b/papers/level_switching/paper.log index 0333b1b..29d788a 100644 --- a/papers/level_switching/paper.log +++ b/papers/level_switching/paper.log @@ -1,4 +1,4 @@ -This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 20 MAY 2026 23:02 +This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 20 MAY 2026 23:18 entering extended mode restricted \write18 enabled. %&-line parsing enabled. @@ -353,12 +353,12 @@ Package epstopdf-base Info: Redefining graphics rule for `.eps' on input line 4 File: epstopdf-sys.cfg 2010/07/13 v1.3 Configuration of (r)epstopdf for TeX Liv e )) - + File: fig_level_source.png Graphic file (type png) Package pdftex.def Info: fig_level_source.png used on input line 105. (pdftex.def) Requested size: 306.0022pt x 136.07228pt. - + File: fig_levels.png Graphic file (type png) Package pdftex.def Info: fig_levels.png used on input line 122. @@ -367,7 +367,7 @@ Package pdftex.def Info: fig_levels.png used on input line 122. LaTeX Warning: `h' float specifier changed to `ht'. - + File: fig_level_cycle.png Graphic file (type png) Package pdftex.def Info: fig_level_cycle.png used on input line 136. @@ -377,7 +377,7 @@ LaTeX Warning: `h' float specifier changed to `ht'. [1{/usr/local/texlive/2022/texmf-var/fonts/map/pdftex/updmap/pdftex.map} <./fig _level_source.png>] - + File: fig_edge_switch.png Graphic file (type png) Package pdftex.def Info: fig_edge_switch.png used on input line 155. @@ -386,7 +386,7 @@ Package pdftex.def Info: fig_edge_switch.png used on input line 155. LaTeX Warning: `h' float specifier changed to `ht'. - + File: fig_parity_subgraph.png Graphic file (type png) Package pdftex.def Info: fig_parity_subgraph.png used on input line 173. @@ -395,7 +395,7 @@ Package pdftex.def Info: fig_parity_subgraph.png used on input line 173. LaTeX Warning: `h' float specifier changed to `ht'. [2 <./fig_levels.png> <./fig_level_cycle.png>] - + File: fig_facial_depth.png Graphic file (type png) Package pdftex.def Info: fig_facial_depth.png used on input line 200. @@ -413,7 +413,7 @@ bal-anced-ness gives $[](\OML/cmm/m/it/10 A[]\OT1/cmr/m/n/10 ) = \OML/cmm/m/it/ /m/n/10 ) \OMS/cmsy/m/n/10 ^^T [] - + File: fig_no_balanced_switch.png Graphic file (type png) Package pdftex.def Info: fig_no_balanced_switch.png used on input line 395. @@ -421,7 +421,7 @@ Package pdftex.def Info: fig_no_balanced_switch.png used on input line 395. LaTeX Warning: `h' float specifier changed to `ht'. - + File: fig_preprocessing.png Graphic file (type png) Package pdftex.def Info: fig_preprocessing.png used on input line 426. @@ -429,19 +429,61 @@ Package pdftex.def Info: fig_preprocessing.png used on input line 426. LaTeX Warning: `h' float specifier changed to `ht'. -[6] [7 <./fig_no_balanced_switch.png> <./fig_preprocessing.png>] [8] -(./paper.aux) +[6] [7 <./fig_no_balanced_switch.png> <./fig_preprocessing.png>] + +Package hyperref Warning: Token not allowed in a PDF string (Unicode): +(hyperref) removing `math shift' on input line 447. + + +Package hyperref Warning: Token not allowed in a PDF string (Unicode): +(hyperref) removing `\geq' on input line 447. + + +Package hyperref Warning: Token not allowed in a PDF string (Unicode): +(hyperref) removing `math shift' on input line 447. + + +Overfull \hbox (55.20863pt too wide) in paragraph at lines 454--463 +\OT1/cmr/m/n/10 the unique depth-$2$ face $\OML/cmm/m/it/10 F \OT1/cmr/m/n/10 = + (0\OML/cmm/m/it/10 ; \OT1/cmr/m/n/10 7\OML/cmm/m/it/10 ; \OT1/cmr/m/n/10 14)$ +has three depth-$1$ neigh-bours $(0\OML/cmm/m/it/10 ; \OT1/cmr/m/n/10 3\OML/cmm +/m/it/10 ; \OT1/cmr/m/n/10 7)\OML/cmm/m/it/10 ; \OT1/cmr/m/n/10 (7\OML/cmm/m/it +/10 ; \OT1/cmr/m/n/10 10\OML/cmm/m/it/10 ; \OT1/cmr/m/n/10 14)\OML/cmm/m/it/10 +; \OT1/cmr/m/n/10 (14\OML/cmm/m/it/10 ; \OT1/cmr/m/n/10 17\OML/cmm/m/it/10 ; \O +T1/cmr/m/n/10 0)$, + [] + + +Overfull \hbox (15.77812pt too wide) in paragraph at lines 454--463 +\OT1/cmr/m/n/10 each lop-sided: their depth-$1$ "deep side" is a degree-$3$ fac +e $(3\OML/cmm/m/it/10 ; \OT1/cmr/m/n/10 5\OML/cmm/m/it/10 ; \OT1/cmr/m/n/10 7)\ +OML/cmm/m/it/10 ; \OT1/cmr/m/n/10 (10\OML/cmm/m/it/10 ; \OT1/cmr/m/n/10 12\OML/ +cmm/m/it/10 ; \OT1/cmr/m/n/10 14)\OML/cmm/m/it/10 ; \OT1/cmr/m/n/10 (17\OML/cmm +/m/it/10 ; \OT1/cmr/m/n/10 19\OML/cmm/m/it/10 ; \OT1/cmr/m/n/10 0)$ + [] + + +File: fig_d2_recursive.png Graphic file (type png) + +Package pdftex.def Info: fig_d2_recursive.png used on input line 477. +(pdftex.def) Requested size: 360.0pt x 124.56897pt. + +Overfull \hbox (44.02832pt too wide) detected at line 496 +[][] + [] + +[8 <./fig_d2_recursive.png>] [9] (./paper.aux) Package rerunfilecheck Info: File `paper.out' has not changed. -(rerunfilecheck) Checksum: AF95E9EBA8283D2CBF07B09833C039FF;1010. +(rerunfilecheck) Checksum: DB53A88C1A1F5BD90EDB1F1E02E41C38;1447. ) Here is how much of TeX's memory you used: - 9777 strings out of 478268 - 151593 string characters out of 5846347 - 458194 words of memory out of 5000000 - 27673 multiletter control sequences out of 15000+600000 + 9791 strings out of 478268 + 151840 string characters out of 5846347 + 458685 words of memory out of 5000000 + 27682 multiletter control sequences out of 15000+600000 475666 words of font info for 53 fonts, out of 8000000 for 9000 1302 hyphenation exceptions out of 8191 - 69i,8n,76p,781b,448s stack positions out of 10000i,1000n,20000p,200000b,200000s + 69i,9n,76p,781b,448s stack positions out of 10000i,1000n,20000p,200000b,200000s @@ -456,10 +498,10 @@ xlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy7.pfb> -Output written on paper.pdf (8 pages, 1034024 bytes). +Output written on paper.pdf (9 pages, 1268281 bytes). PDF statistics: - 196 PDF objects out of 1000 (max. 8388607) - 140 compressed objects within 2 object streams - 38 named destinations out of 1000 (max. 500000) - 81 words of extra memory for PDF output out of 10000 (max. 10000000) + 215 PDF objects out of 1000 (max. 8388607) + 156 compressed objects within 2 object streams + 42 named destinations out of 1000 (max. 500000) + 102 words of extra memory for PDF output out of 10000 (max. 10000000) diff --git a/papers/level_switching/paper.out b/papers/level_switching/paper.out index 79d8a4c..62f5e23 100644 --- a/papers/level_switching/paper.out +++ b/papers/level_switching/paper.out @@ -3,3 +3,5 @@ \BOOKMARK [1][-]{section.3}{\376\377\0003\000.\000\040\000O\000u\000t\000e\000r\000p\000l\000a\000n\000a\000r\000i\000t\000y\000\040\000o\000f\000\040\000l\000e\000v\000e\000l\000\040\000c\000o\000m\000p\000o\000n\000e\000n\000t\000s}{}% 3 \BOOKMARK [2][-]{section*.1}{\376\377\000W\000h\000e\000n\000\040\000d\000o\000e\000s\000\040\000a\000\040\000b\000a\000l\000a\000n\000c\000e\000d\000\040\000s\000u\000r\000f\000a\000c\000e\000\040\000s\000w\000i\000t\000c\000h\000\040\000e\000x\000i\000s\000t\000?}{section.3}% 4 \BOOKMARK [2][-]{section*.2}{\376\377\000P\000r\000e\000p\000r\000o\000c\000e\000s\000s\000i\000n\000g\000\040\000t\000o\000w\000a\000r\000d\000\040\000b\000a\000l\000a\000n\000c\000e\000d\000\040\000s\000w\000i\000t\000c\000h\000e\000s}{section.3}% 5 +\BOOKMARK [2][-]{section*.3}{\376\377\000T\000h\000e\000\040\000d\000\040\0002\000\040\000a\000n\000a\000l\000o\000g\000\040\000a\000n\000d\000\040\000r\000e\000c\000u\000r\000s\000i\000v\000e\000\040\000l\000o\000p\000s\000i\000d\000e\000d\000n\000e\000s\000s}{section.3}% 6 +\BOOKMARK [2][-]{section*.4}{\376\377\000E\000m\000p\000i\000r\000i\000c\000a\000l\000\040\000t\000e\000r\000m\000i\000n\000a\000t\000i\000o\000n}{section.3}% 7 diff --git a/papers/level_switching/paper.pdf b/papers/level_switching/paper.pdf index 27900c9..3b03ea6 100644 Binary files a/papers/level_switching/paper.pdf and b/papers/level_switching/paper.pdf differ diff --git a/papers/level_switching/paper.tex b/papers/level_switching/paper.tex index a0e0063..ff8186d 100644 --- a/papers/level_switching/paper.tex +++ b/papers/level_switching/paper.tex @@ -442,12 +442,84 @@ unbalanced surface switch -- and a corresponding statement for $d \geq 2$, where balancedness depends on depth-$(d-2)$ structure rather than just spans -- remains open. +\subsection*{The $d \geq 2$ analog and recursive lopsidedness} + +For $d \geq 2$ the obstruction to a balanced surface switch is no longer +"$F$ has no edge of span 1": it is recursive. We say a depth-$(d-1)$ +neighbour $F' = uvx$ of $F$ is \emph{lopsided} if exactly one of its +non-$F$ neighbours has depth $d-2$ (the other being deeper or an +interior face of depth $d-1$). $F$ admits a balanced surface switch +iff at least one depth-$(d-1)$ neighbour is not lopsided. + +The analog of the $9$-vertex example at $d = 2$ is a $21$-vertex +configuration where the unique depth-$2$ face $F = (0, 7, 14)$ has +three depth-$1$ neighbours $(0,3,7), (7,10,14), (14,17,0)$, each +lopsided: their depth-$1$ "deep side" is a degree-$3$ face +$(3,5,7), (10,12,14), (17,19,0)$ that itself reaches depth $0$ via +two ears. So the obstruction at $F$ is one layer of lopsidedness; +after a single preprocessing step the new depth-$2$ face $(3,7,14)$ +sees the previously-hidden balanced descender as a direct neighbour +and the algorithm terminates immediately. + +Stacking lopsidedness yields a $24$-vertex example +(Figure~\ref{fig:d2-recursive}) where every depth-$1$ neighbour of $F$ +is lopsided \emph{and} the depth-$1$ degree-$3$ face inside each arm +($G_i$) is itself lopsided. Two preprocessing steps are needed before a +balanced switch becomes available: the active depth-$2$ face migrates +from $(0,8,16)$ to $(2,8,16)$ to $(4,8,16)$, at which point the +\emph{innermost} depth-$1$ face $(4,6,8)$ -- whose two non-$F$ neighbours +$(4,5,6)$ and $(6,7,8)$ are both ears -- becomes a direct neighbour and +the balanced condition is satisfied. After the balanced switch, $10$ +further balanced switches drive every face to depth $0$. + +\begin{figure}[h] +\centering +\includegraphics[width=\textwidth]{fig_d2_recursive.png} +\caption{Recursive lopsidedness at $d = 2$. Left: $F = (0,8,16)$ depth +$2$, every arm doubly-lopsided. Middle: one preprocessing switch +$(0,8) \mapsto (2,16)$ exposes the first lopsided layer; the new +depth-$2$ face $(2,8,16)$ still has no balanced switch. Right: a +second preprocessing switch $(8,2) \mapsto (4,16)$ reaches the inner +balanced face $K_0 = (4,6,8)$, whose two non-$F$ neighbours are both +ears; the depth-$2$ face $(4,8,16)$ now admits a balanced surface +switch on edge $(4,8)$.} +\label{fig:d2-recursive} +\end{figure} + +\subsection*{Empirical termination} + +On every tested configuration, iterated preprocessing terminates and +the algorithm +\[ +\text{while max-depth face $F$ has $\mathrm{depth}(F) > 0$: } +\text{do a balanced switch if available, else preprocess} +\] +drives every face to depth $0$. The observed step count is + +\begin{center} +\begin{tabular}{lccc} +configuration & $n$ & $d_{\max}$ & total switches \\\hline +no-balanced $d=1$ (Figure~\ref{fig:no-balanced}) & 9 & 1 & 4 \\ +singly-lopsided $d=2$ (Figure~\ref{fig:d2-recursive} left only) & 21 & 2 & 8 \\ +doubly-lopsided $d=2$ (Figure~\ref{fig:d2-recursive}) & 24 & 2 & 13 \\ +\end{tabular} +\end{center} + +Each preprocessing step appears to advance the active maximum-depth +face one vertex along the lopsided arm of the chosen depth-$(d-1)$ +neighbour, peeling off one layer of recursive lopsidedness. The +remaining open question is to identify the monovariant that captures +this: a candidate is the total number of triples $(F, F', F'')$ where +$F' \in N(F)$ is lopsided and $F'' \in N(F')$ is its depth-$d-1$ +"deep side". We do not yet have a proof that this strictly decreases +under every unbalanced surface switch on a maximum-depth face. + \begin{question} \label{q:preprocessing-terminates} -Does iterated preprocessing reach a balanced surface switch in finitely -many steps from every initial configuration? Equivalently, is there a -monovariant on the inner-face structure of $L_k$ that strictly decreases -at every unbalanced surface switch on a maximum-depth face? +Does iterated preprocessing always reach a balanced surface switch in +finitely many steps? Equivalently, is there a monovariant on the +inner-face structure of $L_k$ that strictly decreases at every +unbalanced surface switch on a maximum-depth face? \end{question} \end{document}