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>
This commit is contained in:
@@ -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}')
|
||||
@@ -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.')
|
||||
@@ -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()))
|
||||
@@ -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}')
|
||||
@@ -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}')
|
||||
Reference in New Issue
Block a user