Files
math-research/papers/level_switching/experiments/d2_recursive_lopsided.py
didericis 77093cb0b0 Extend Level Switching paper with d>=2 preprocessing analysis
Add 21-vertex and 24-vertex examples showing recursive lopsidedness
at d=2. Empirically confirm that the iterated algorithm (balanced
switch when available, preprocess otherwise) drives every face to
depth 0 on all tested configurations. Frame the remaining open
question as identifying a strictly-decreasing monovariant under
unbalanced preprocessing switches.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 23:20:06 -04:00

197 lines
6.2 KiB
Python

"""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()))