Prove outerplanarity and draft edge-flip resolution algorithm
- Promote Prop 3.1 (outerplanarity of level subgraphs) to Theorem 3.1 with a proof by contradiction via a BFS-path argument; drop the $n \leq 10$ caveat and the now-resolved open question. - Add Section 5 "An edge-flip resolution algorithm": apex classification of $L_k$-edges, bridge lemma, cross-level flip pass, definition of tricky-everywhere odd cycles and facial depth (seeded from inner faces with $\geq 2$ outer-face edges), and the depth-guided flip procedure. Observation 5.5 records empirical termination at $n = 9, 10, 11$; Question 5.6 asks if it holds in general. - Add experiments/depth_monovariant_check.py (sanity check over triangulation iso-classes, confirms the count-of-tricky-faces monovariant strictly decreases per flip on all 1400 tricky configs at $n \leq 11$), viz_cycling.py and debug_cycling.py, and cycling_visualization.png illustrating the depth-definition fix. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
"""Dump one cycling case to see what's going on."""
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import networkx as nx
|
||||
from collections import defaultdict
|
||||
from triangulation_gen import enumerate_all_triangulations
|
||||
from level_cycles import compute_levels, level_sources
|
||||
from depth_monovariant_check import (
|
||||
faces_of_subgraph, canonical_face, edges_of_face,
|
||||
apex_levels_for_edge, identify_outer_face, face_depths,
|
||||
is_both_k_everywhere, tricky_odd_faces, lowest_depth_neighbor_flip,
|
||||
)
|
||||
|
||||
|
||||
def dump(G, emb, levels, nodes_k, k, source_label):
|
||||
print(f" source: {source_label}, levels:")
|
||||
by_level = defaultdict(list)
|
||||
for v, lv in levels.items():
|
||||
by_level[lv].append(v)
|
||||
for lv in sorted(by_level):
|
||||
print(f" L_{lv} = {sorted(by_level[lv])}")
|
||||
print(f" examining L_{k} = {sorted(nodes_k)}")
|
||||
L_k = G.subgraph(nodes_k)
|
||||
print(f" L_k edges: {sorted(L_k.edges())}")
|
||||
faces = faces_of_subgraph(G, emb, nodes_k)
|
||||
print(f" faces of L_k (inherited embedding):")
|
||||
for f in faces:
|
||||
print(f" {f} len={len(f)} canon={canonical_face(f)}")
|
||||
outer = identify_outer_face(faces, G, emb, levels, k)
|
||||
print(f" outer face: {outer}")
|
||||
depths = face_depths(faces, outer)
|
||||
print(f" depths: {depths}")
|
||||
print(f" edge-by-edge apex-level pairs:")
|
||||
for f in faces:
|
||||
cf = canonical_face(f)
|
||||
if cf == outer:
|
||||
continue
|
||||
if len(f) < 3:
|
||||
continue
|
||||
print(f" face {f} (depth={depths.get(cf)}, len={len(f)}):")
|
||||
for (u, v) in edges_of_face(f):
|
||||
result = apex_levels_for_edge(G, emb, u, v, levels)
|
||||
if result is None:
|
||||
print(f" ({u},{v}): no apex")
|
||||
continue
|
||||
(la, lb), (w, x) = result
|
||||
print(f" ({u},{v}): apexes ({w}@L{la}, {x}@L{lb})")
|
||||
tricky = tricky_odd_faces(faces, G, emb, levels, k, depths, outer)
|
||||
print(f" tricky odd faces: {[(f, d) for cf, f, d in tricky]}")
|
||||
if tricky:
|
||||
cf_t, face_t, d_t = max(tricky, key=lambda x: x[2])
|
||||
print(f" picking deepest tricky face: {face_t} at depth {d_t}")
|
||||
flip = lowest_depth_neighbor_flip(face_t, G, emb, levels, k,
|
||||
depths, faces, outer)
|
||||
print(f" flip choice: {flip}")
|
||||
|
||||
|
||||
# n=10, tri_idx=6, source face (0,4,2), k=1
|
||||
tris = enumerate_all_triangulations(10)
|
||||
G = tris[6]
|
||||
ip, emb = nx.check_planarity(G)
|
||||
print(f"G edges: {sorted(G.edges())}")
|
||||
source = {0, 4, 2}
|
||||
levels = compute_levels(G, source)
|
||||
by_level = defaultdict(list)
|
||||
for v, lv in levels.items():
|
||||
by_level[lv].append(v)
|
||||
nodes_k = by_level[1]
|
||||
dump(G, emb, levels, nodes_k, 1, ('face', (0, 4, 2)))
|
||||
|
||||
print("\n--- after one flip ---")
|
||||
flip_choice = None
|
||||
faces = faces_of_subgraph(G, emb, nodes_k)
|
||||
outer = identify_outer_face(faces, G, emb, levels, 1)
|
||||
depths = face_depths(faces, outer)
|
||||
tricky = tricky_odd_faces(faces, G, emb, levels, 1, depths, outer)
|
||||
if tricky:
|
||||
cf_t, face_t, d_t = max(tricky, key=lambda x: x[2])
|
||||
flip_choice = lowest_depth_neighbor_flip(face_t, G, emb, levels, 1,
|
||||
depths, faces, outer)
|
||||
if flip_choice:
|
||||
u, v, w, x, _ = flip_choice
|
||||
Gp = G.copy()
|
||||
Gp.remove_edge(u, v)
|
||||
Gp.add_edge(w, x)
|
||||
print(f"Flipped ({u},{v}) -> ({w},{x})")
|
||||
ip2, emb_p = nx.check_planarity(Gp)
|
||||
dump(Gp, emb_p, levels, nodes_k, 1, ('face', (0, 4, 2)))
|
||||
+369
@@ -0,0 +1,369 @@
|
||||
"""
|
||||
Sanity check for the facial-depth monovariant on tricky (k,k) configurations.
|
||||
|
||||
For each (G, S, k) where L_k has at least one "tricky-everywhere" odd face
|
||||
(every edge is a (k,k) apex pair):
|
||||
- Run the proposed algorithm: pick the deepest tricky-everywhere odd face,
|
||||
flip the edge whose other-side face has min facial depth.
|
||||
- Iterate until no tricky-everywhere odd face remains, or step budget hits.
|
||||
- Track multiple candidate monovariants per flip.
|
||||
|
||||
Reports counts of (a) terminations within budget, (b) cycle/failure cases,
|
||||
(c) which monovariant (if any) decreases on every flip.
|
||||
"""
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
import networkx as nx
|
||||
from collections import defaultdict, deque
|
||||
from triangulation_gen import enumerate_all_triangulations
|
||||
from level_cycles import (
|
||||
compute_levels, get_all_faces, inherited_embedding, level_sources
|
||||
)
|
||||
|
||||
|
||||
# -- helpers ------------------------------------------------------------
|
||||
|
||||
def faces_of_subgraph(G, emb_G, nodes_k):
|
||||
if len(nodes_k) < 3:
|
||||
return []
|
||||
L_k = G.subgraph(nodes_k)
|
||||
if L_k.number_of_edges() < 3:
|
||||
return []
|
||||
try:
|
||||
emb_L = inherited_embedding(emb_G, nodes_k)
|
||||
return get_all_faces(emb_L)
|
||||
except Exception:
|
||||
ip, emb_L = nx.check_planarity(L_k)
|
||||
if not ip:
|
||||
return []
|
||||
return get_all_faces(emb_L)
|
||||
|
||||
|
||||
def canonical_face(face):
|
||||
rots = [tuple(face[i:] + face[:i]) for i in range(len(face))] \
|
||||
+ [tuple(face[::-1][i:] + face[::-1][:i]) for i in range(len(face))]
|
||||
return min(rots)
|
||||
|
||||
|
||||
def edges_of_face(face):
|
||||
n = len(face)
|
||||
return [(face[i], face[(i+1) % n]) for i in range(n)]
|
||||
|
||||
|
||||
def apex_levels_for_edge(G, emb_G, u, v, levels):
|
||||
if not G.has_edge(u, v):
|
||||
return None
|
||||
f1 = emb_G.traverse_face(u, v)
|
||||
f2 = emb_G.traverse_face(v, u)
|
||||
if len(f1) != 3 or len(f2) != 3:
|
||||
return None
|
||||
w = next(x for x in f1 if x != u and x != v)
|
||||
x = next(y for y in f2 if y != u and y != v)
|
||||
return (levels.get(w), levels.get(x)), (w, x)
|
||||
|
||||
|
||||
def identify_outer_face(faces, G, emb_G, levels, k):
|
||||
"""Outer face = the face whose 'outside' connects to lower levels.
|
||||
Score each face by how many edges have at least one apex at level < k
|
||||
on the opposite side; pick highest score, longest boundary as tiebreak."""
|
||||
best = (-1, -1, None)
|
||||
for face in faces:
|
||||
score = 0
|
||||
for (u, v) in edges_of_face(face):
|
||||
if not G.has_edge(u, v):
|
||||
continue
|
||||
f1 = emb_G.traverse_face(u, v)
|
||||
f2 = emb_G.traverse_face(v, u)
|
||||
if len(f1) != 3 or len(f2) != 3:
|
||||
continue
|
||||
w = next(x for x in f1 if x != u and x != v)
|
||||
x = next(y for y in f2 if y != u and y != v)
|
||||
if (levels.get(w, k) < k) or (levels.get(x, k) < k):
|
||||
score += 1
|
||||
key = (score, len(face))
|
||||
if key > (best[0], best[1]):
|
||||
best = (score, len(face), face)
|
||||
return canonical_face(best[2]) if best[2] is not None else None
|
||||
|
||||
|
||||
def face_depths(faces, outer_canon):
|
||||
"""BFS depth in the dual graph from the seed set:
|
||||
inner faces with >= 2 edges incident to the outer face."""
|
||||
edge_to_faces = defaultdict(list)
|
||||
canon_set = set()
|
||||
for face in faces:
|
||||
cf = canonical_face(face)
|
||||
canon_set.add(cf)
|
||||
for (a, b) in edges_of_face(face):
|
||||
edge_to_faces[frozenset([a, b])].append(cf)
|
||||
# Outer-face edges = edges of the outer face.
|
||||
outer_face_edges = set()
|
||||
for face in faces:
|
||||
if canonical_face(face) == outer_canon:
|
||||
for (a, b) in edges_of_face(face):
|
||||
outer_face_edges.add(frozenset([a, b]))
|
||||
break
|
||||
# Seed set: inner faces with >= 2 outer-face edges.
|
||||
seeds = []
|
||||
for face in faces:
|
||||
cf = canonical_face(face)
|
||||
if cf == outer_canon:
|
||||
continue
|
||||
count = 0
|
||||
for (a, b) in edges_of_face(face):
|
||||
if frozenset([a, b]) in outer_face_edges:
|
||||
count += 1
|
||||
if count >= 2:
|
||||
seeds.append(cf)
|
||||
if not seeds:
|
||||
# Fall back: no seeds means no "easy" odd face nearby. Return empty.
|
||||
return {}
|
||||
D = nx.Graph()
|
||||
for cf in canon_set:
|
||||
if cf != outer_canon:
|
||||
D.add_node(cf)
|
||||
for e, fs in edge_to_faces.items():
|
||||
if len(fs) == 2:
|
||||
f1, f2 = fs
|
||||
if f1 != outer_canon and f2 != outer_canon:
|
||||
D.add_edge(f1, f2)
|
||||
# Multi-source BFS.
|
||||
depths = {}
|
||||
queue = deque()
|
||||
for s in seeds:
|
||||
if s in D:
|
||||
depths[s] = 0
|
||||
queue.append(s)
|
||||
while queue:
|
||||
u = queue.popleft()
|
||||
for v in D.neighbors(u):
|
||||
if v not in depths:
|
||||
depths[v] = depths[u] + 1
|
||||
queue.append(v)
|
||||
return depths
|
||||
|
||||
|
||||
def is_both_k_everywhere(face, G, emb_G, levels, k):
|
||||
for (u, v) in edges_of_face(face):
|
||||
result = apex_levels_for_edge(G, emb_G, u, v, levels)
|
||||
if result is None:
|
||||
return False
|
||||
(la, lb), _ = result
|
||||
if (la, lb) != (k, k):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def tricky_odd_faces(faces, G, emb_G, levels, k, depths, outer_canon):
|
||||
"""Return list of (canon, face, depth) for odd faces that are tricky-
|
||||
everywhere AND have at least one (k,k) edge whose other-side face is
|
||||
not the outer face (i.e., we can flip toward an inner neighbor)."""
|
||||
out = []
|
||||
for face in faces:
|
||||
cf = canonical_face(face)
|
||||
if cf == outer_canon:
|
||||
continue
|
||||
if len(face) % 2 != 1:
|
||||
continue
|
||||
if not is_both_k_everywhere(face, G, emb_G, levels, k):
|
||||
continue
|
||||
d = depths.get(cf)
|
||||
if d is None:
|
||||
continue
|
||||
out.append((cf, face, d))
|
||||
return out
|
||||
|
||||
|
||||
def odd_faces_all(faces, depths, outer_canon):
|
||||
out = []
|
||||
for face in faces:
|
||||
cf = canonical_face(face)
|
||||
if cf == outer_canon:
|
||||
continue
|
||||
if len(face) % 2 != 1:
|
||||
continue
|
||||
d = depths.get(cf)
|
||||
if d is None:
|
||||
continue
|
||||
out.append((cf, face, d))
|
||||
return out
|
||||
|
||||
|
||||
def lowest_depth_neighbor_flip(face, G, emb_G, levels, k, depths, faces,
|
||||
outer_canon):
|
||||
"""Among (k,k) edges of `face`, return the flip toward the lowest-depth
|
||||
non-outer neighbor face. Returns (u, v, w, x, neighbor_depth) or None."""
|
||||
edge_to_other = {}
|
||||
face_canon = canonical_face(face)
|
||||
for f in faces:
|
||||
cf = canonical_face(f)
|
||||
if cf == face_canon:
|
||||
continue
|
||||
for (a, b) in edges_of_face(f):
|
||||
e = frozenset([a, b])
|
||||
edge_to_other.setdefault(e, cf)
|
||||
best = None
|
||||
for (u, v) in edges_of_face(face):
|
||||
result = apex_levels_for_edge(G, emb_G, u, v, levels)
|
||||
if result is None:
|
||||
continue
|
||||
(la, lb), (w, x) = result
|
||||
if (la, lb) != (k, k):
|
||||
continue
|
||||
if G.has_edge(w, x):
|
||||
continue
|
||||
e = frozenset([u, v])
|
||||
other_canon = edge_to_other.get(e)
|
||||
if other_canon is None or other_canon == outer_canon:
|
||||
continue
|
||||
other_depth = depths.get(other_canon)
|
||||
if other_depth is None:
|
||||
continue
|
||||
if best is None or other_depth < best[4]:
|
||||
best = (u, v, w, x, other_depth)
|
||||
return best
|
||||
|
||||
|
||||
# -- main routine -------------------------------------------------------
|
||||
|
||||
def analyze_config(G, emb, levels, nodes_k, k, max_steps=20):
|
||||
"""Run the user's algorithm iteratively. Returns:
|
||||
{'terminated': bool, 'steps': n, 'trace': list of monovariant tuples}
|
||||
A trace entry per step is (max_tricky_depth, num_tricky, max_odd_depth).
|
||||
"""
|
||||
Gc = G.copy()
|
||||
emb_c = emb
|
||||
trace = []
|
||||
for step in range(max_steps):
|
||||
faces = faces_of_subgraph(Gc, emb_c, nodes_k)
|
||||
if len(faces) < 2:
|
||||
return {'terminated': True, 'steps': step, 'trace': trace,
|
||||
'reason': 'no_faces'}
|
||||
outer = identify_outer_face(faces, Gc, emb_c, levels, k)
|
||||
depths = face_depths(faces, outer)
|
||||
if depths is None:
|
||||
return {'terminated': True, 'steps': step, 'trace': trace,
|
||||
'reason': 'no_depths'}
|
||||
odd_all = odd_faces_all(faces, depths, outer)
|
||||
tricky = tricky_odd_faces(faces, Gc, emb_c, levels, k, depths, outer)
|
||||
max_tricky = max((d for _, _, d in tricky), default=-1)
|
||||
max_odd = max((d for _, _, d in odd_all), default=-1)
|
||||
trace.append((max_tricky, len(tricky), max_odd))
|
||||
if not tricky:
|
||||
return {'terminated': True, 'steps': step, 'trace': trace,
|
||||
'reason': 'no_tricky'}
|
||||
# Pick deepest tricky face; flip toward lowest-depth neighbor.
|
||||
cf_t, face_t, d_t = max(tricky, key=lambda x: x[2])
|
||||
flip = lowest_depth_neighbor_flip(face_t, Gc, emb_c, levels, k,
|
||||
depths, faces, outer)
|
||||
if flip is None:
|
||||
return {'terminated': False, 'steps': step, 'trace': trace,
|
||||
'reason': 'no_flip'}
|
||||
u, v, w, x, _ = flip
|
||||
Gc.remove_edge(u, v)
|
||||
Gc.add_edge(w, x)
|
||||
ip, emb_c = nx.check_planarity(Gc)
|
||||
if not ip:
|
||||
return {'terminated': False, 'steps': step, 'trace': trace,
|
||||
'reason': 'nonplanar'}
|
||||
return {'terminated': False, 'steps': max_steps, 'trace': trace,
|
||||
'reason': 'budget'}
|
||||
|
||||
|
||||
def monovariant_decreases(trace, key):
|
||||
"""Does `trace[i][key]` strictly decrease at each step (except when
|
||||
the algorithm has already terminated)?"""
|
||||
vals = [t[key] for t in trace]
|
||||
for i in range(1, len(vals)):
|
||||
if vals[i] >= vals[i - 1]:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def run_check(n_values, max_steps=20):
|
||||
total_configs = 0
|
||||
terminated = 0
|
||||
nonterm = 0
|
||||
mono_max_tricky = 0
|
||||
mono_count_tricky = 0
|
||||
mono_max_odd = 0
|
||||
nonterm_examples = []
|
||||
|
||||
for n in n_values:
|
||||
print(f"\n=== n = {n} ===")
|
||||
tris = enumerate_all_triangulations(n)
|
||||
print(f" {len(tris)} iso-classes")
|
||||
for tri_idx, G in enumerate(tris):
|
||||
ip, emb = nx.check_planarity(G)
|
||||
if not ip:
|
||||
continue
|
||||
for kind, label, source_set in level_sources(G, emb):
|
||||
levels = compute_levels(G, source_set)
|
||||
by_level = defaultdict(list)
|
||||
for v, lv in levels.items():
|
||||
by_level[lv].append(v)
|
||||
for k, nodes_k in by_level.items():
|
||||
if k == 0:
|
||||
continue
|
||||
faces = faces_of_subgraph(G, emb, nodes_k)
|
||||
if len(faces) < 2:
|
||||
continue
|
||||
outer = identify_outer_face(faces, G, emb, levels, k)
|
||||
if outer is None:
|
||||
continue
|
||||
depths = face_depths(faces, outer)
|
||||
if depths is None:
|
||||
continue
|
||||
tricky = tricky_odd_faces(faces, G, emb, levels, k,
|
||||
depths, outer)
|
||||
if not tricky:
|
||||
continue
|
||||
total_configs += 1
|
||||
result = analyze_config(G, emb, levels, nodes_k, k,
|
||||
max_steps)
|
||||
if result['terminated']:
|
||||
terminated += 1
|
||||
else:
|
||||
nonterm += 1
|
||||
if len(nonterm_examples) < 5:
|
||||
nonterm_examples.append({
|
||||
'n': n, 'tri_idx': tri_idx,
|
||||
'source': (kind, label),
|
||||
'k': k,
|
||||
'reason': result['reason'],
|
||||
'trace': result['trace'],
|
||||
'steps': result['steps'],
|
||||
})
|
||||
# Monovariant check (only meaningful if trace has >= 2)
|
||||
tr = result['trace']
|
||||
if len(tr) >= 2:
|
||||
if monovariant_decreases(tr, 0):
|
||||
mono_max_tricky += 1
|
||||
if monovariant_decreases(tr, 1):
|
||||
mono_count_tricky += 1
|
||||
if monovariant_decreases(tr, 2):
|
||||
mono_max_odd += 1
|
||||
|
||||
print(f"\n=== summary ===")
|
||||
print(f"tricky configs encountered: {total_configs}")
|
||||
print(f" terminated within {max_steps} steps: {terminated}")
|
||||
print(f" non-terminating / failed: {nonterm}")
|
||||
print(f" candidate monovariants (strict decrease per step):")
|
||||
print(f" max depth of tricky faces: {mono_max_tricky}")
|
||||
print(f" count of tricky faces: {mono_count_tricky}")
|
||||
print(f" max depth of all odd faces: {mono_max_odd}")
|
||||
if nonterm_examples:
|
||||
print(f"\nnon-terminating examples (first {len(nonterm_examples)}):")
|
||||
for ex in nonterm_examples:
|
||||
print(f" n={ex['n']} tri={ex['tri_idx']} src={ex['source']} "
|
||||
f"k={ex['k']} reason={ex['reason']} steps={ex['steps']}")
|
||||
print(f" trace (max_tricky, n_tricky, max_odd): {ex['trace']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1:
|
||||
ns = [int(x) for x in sys.argv[1:]]
|
||||
else:
|
||||
ns = [9, 10]
|
||||
run_check(ns)
|
||||
@@ -0,0 +1,197 @@
|
||||
"""Visualize the cycling case for the (k,k) tricky-everywhere algorithm.
|
||||
|
||||
Shows L_1 of triangulation index 6 at n=10, with source face (0,4,2).
|
||||
Renders 4 steps of the algorithm side by side.
|
||||
"""
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import networkx as nx
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.patches as mpatches
|
||||
from collections import defaultdict
|
||||
from triangulation_gen import enumerate_all_triangulations
|
||||
from level_cycles import compute_levels
|
||||
from depth_monovariant_check import (
|
||||
faces_of_subgraph, canonical_face, edges_of_face, apex_levels_for_edge,
|
||||
identify_outer_face, face_depths, is_both_k_everywhere,
|
||||
tricky_odd_faces, lowest_depth_neighbor_flip,
|
||||
)
|
||||
|
||||
|
||||
def layout_outerplanar(nodes_k, outer_face, G_full):
|
||||
"""Place outer-face vertices on a circle in the cyclic order they appear,
|
||||
and interior vertices via a weighted average of their neighbors on the
|
||||
circle."""
|
||||
outer_list = list(outer_face)
|
||||
n = len(outer_list)
|
||||
pos = {}
|
||||
for i, v in enumerate(outer_list):
|
||||
angle = np.pi / 2 - 2 * np.pi * i / n
|
||||
pos[v] = np.array([np.cos(angle), np.sin(angle)])
|
||||
interior = [v for v in nodes_k if v not in pos]
|
||||
if interior:
|
||||
Lk = G_full.subgraph(nodes_k)
|
||||
for v in interior:
|
||||
nbrs = [u for u in Lk.neighbors(v) if u in pos]
|
||||
if nbrs:
|
||||
pos[v] = np.mean([pos[u] for u in nbrs], axis=0)
|
||||
else:
|
||||
pos[v] = np.array([0.0, 0.0])
|
||||
return pos
|
||||
|
||||
|
||||
def draw_panel(ax, G_full, emb, levels, nodes_k, k, title,
|
||||
highlight_edge=None, new_edge=None):
|
||||
"""Draw L_k with faces shaded by depth and tricky odd face highlighted."""
|
||||
faces = faces_of_subgraph(G_full, emb, nodes_k)
|
||||
outer = identify_outer_face(faces, G_full, emb, levels, k)
|
||||
depths = face_depths(faces, outer)
|
||||
tricky = tricky_odd_faces(faces, G_full, emb, levels, k, depths, outer)
|
||||
tricky_canons = {cf for cf, _, _ in tricky}
|
||||
|
||||
# Find the outer face actual tuple (for layout)
|
||||
outer_face = None
|
||||
for f in faces:
|
||||
if canonical_face(f) == outer:
|
||||
outer_face = f
|
||||
break
|
||||
pos = layout_outerplanar(nodes_k, outer_face, G_full)
|
||||
|
||||
# Shade faces by depth (skip outer face)
|
||||
max_depth = max(d for d in depths.values()) if depths else 1
|
||||
for f in faces:
|
||||
cf = canonical_face(f)
|
||||
if cf == outer:
|
||||
continue
|
||||
d = depths.get(cf, 0)
|
||||
if cf in tricky_canons:
|
||||
color = '#ff5555' # red for tricky
|
||||
alpha = 0.7
|
||||
else:
|
||||
# Gradient from light yellow (depth 1) to orange (deep)
|
||||
shade = 0.3 + 0.4 * (d / max(max_depth, 1))
|
||||
color = (1.0, 1.0 - shade * 0.3, 0.7 - shade * 0.4)
|
||||
alpha = 0.5
|
||||
poly = plt.Polygon([pos[v] for v in f], color=color, alpha=alpha,
|
||||
zorder=1)
|
||||
ax.add_patch(poly)
|
||||
# Label face center with depth
|
||||
cx, cy = np.mean([pos[v] for v in f], axis=0)
|
||||
face_len = len(f)
|
||||
parity_mark = ' (odd)' if face_len % 2 == 1 else ''
|
||||
tricky_mark = ' TRICKY' if cf in tricky_canons else ''
|
||||
ax.text(cx, cy, f"d={d}\nlen={face_len}{parity_mark}{tricky_mark}",
|
||||
fontsize=9, ha='center', va='center', zorder=3,
|
||||
fontweight='bold' if cf in tricky_canons else 'normal')
|
||||
|
||||
# Draw edges of L_k
|
||||
Lk = G_full.subgraph(nodes_k)
|
||||
hl_set = frozenset(highlight_edge) if highlight_edge else None
|
||||
new_set = frozenset(new_edge) if new_edge else None
|
||||
for u, v in Lk.edges():
|
||||
eset = frozenset([u, v])
|
||||
if hl_set and eset == hl_set:
|
||||
ax.plot([pos[u][0], pos[v][0]], [pos[u][1], pos[v][1]],
|
||||
color='red', linewidth=4.0, zorder=2.5,
|
||||
linestyle='--')
|
||||
else:
|
||||
ax.plot([pos[u][0], pos[v][0]], [pos[u][1], pos[v][1]],
|
||||
color='black', linewidth=1.6, zorder=2)
|
||||
# Sketch where the new edge will land (dotted green)
|
||||
if new_set:
|
||||
nu, nv = new_edge
|
||||
if nu in pos and nv in pos:
|
||||
ax.plot([pos[nu][0], pos[nv][0]], [pos[nu][1], pos[nv][1]],
|
||||
color='green', linewidth=2.5, zorder=2.5,
|
||||
linestyle=':')
|
||||
|
||||
# Draw vertices
|
||||
for v in nodes_k:
|
||||
ax.plot(pos[v][0], pos[v][1], 'o', color='black', markersize=12,
|
||||
zorder=4)
|
||||
ax.text(pos[v][0] * 1.13, pos[v][1] * 1.13, str(v), fontsize=14,
|
||||
ha='center', va='center', color='blue', zorder=5,
|
||||
fontweight='bold')
|
||||
|
||||
ax.set_title(title, fontsize=12)
|
||||
ax.set_aspect('equal')
|
||||
ax.axis('off')
|
||||
ax.set_xlim(-1.3, 1.3)
|
||||
ax.set_ylim(-1.3, 1.3)
|
||||
return tricky
|
||||
|
||||
|
||||
def main():
|
||||
tris = enumerate_all_triangulations(10)
|
||||
G = tris[6]
|
||||
ip, emb = nx.check_planarity(G)
|
||||
source = {0, 4, 2}
|
||||
levels = compute_levels(G, source)
|
||||
by_level = defaultdict(list)
|
||||
for v, lv in levels.items():
|
||||
by_level[lv].append(v)
|
||||
nodes_k = by_level[1]
|
||||
k = 1
|
||||
|
||||
fig, axes_grid = plt.subplots(2, 2, figsize=(14, 14))
|
||||
axes = axes_grid.flatten()
|
||||
fig.suptitle(
|
||||
f"Cycling case: n=10, tri[6], source face (0,4,2), $L_{k}$\n"
|
||||
f"Red = tricky-everywhere odd face (depth 2). Yellow = depth-1 odd "
|
||||
f"faces (have free edges). Algorithm flips a (k,k) edge of the red "
|
||||
f"face toward its lowest-depth neighbor each step.",
|
||||
fontsize=13
|
||||
)
|
||||
|
||||
Gc = G.copy()
|
||||
emb_c = emb
|
||||
for step in range(4):
|
||||
ax = axes[step]
|
||||
# Compute flip choice first so we can render highlight before draw
|
||||
faces = faces_of_subgraph(Gc, emb_c, nodes_k)
|
||||
outer = identify_outer_face(faces, Gc, emb_c, levels, k)
|
||||
depths = face_depths(faces, outer)
|
||||
tricky_pre = tricky_odd_faces(faces, Gc, emb_c, levels, k, depths,
|
||||
outer)
|
||||
flip = None
|
||||
if tricky_pre:
|
||||
cf_t, face_t, d_t = max(tricky_pre, key=lambda x: x[2])
|
||||
flip = lowest_depth_neighbor_flip(face_t, Gc, emb_c, levels, k,
|
||||
depths, faces, outer)
|
||||
hl = None
|
||||
new_e = None
|
||||
if flip is not None:
|
||||
u, v, w, x, _ = flip
|
||||
hl = (u, v)
|
||||
new_e = (w, x)
|
||||
tricky = draw_panel(ax, Gc, emb_c, levels, nodes_k, k,
|
||||
f"Step {step}",
|
||||
highlight_edge=hl, new_edge=new_e)
|
||||
if not tricky:
|
||||
ax.set_title(f"Step {step}: no tricky face — done",
|
||||
fontsize=12)
|
||||
continue
|
||||
if flip is None:
|
||||
ax.set_title(f"Step {step}: no flip available", fontsize=12)
|
||||
continue
|
||||
u, v, w, x, _ = flip
|
||||
ax.set_title(
|
||||
f"Step {step}: tricky face {face_t}\n"
|
||||
f"red-dashed: edge ({u},{v}) being flipped → "
|
||||
f"green-dotted: new edge ({w},{x})",
|
||||
fontsize=11)
|
||||
# Apply flip
|
||||
Gc.remove_edge(u, v)
|
||||
Gc.add_edge(w, x)
|
||||
ip2, emb_c = nx.check_planarity(Gc)
|
||||
|
||||
plt.tight_layout(rect=[0, 0, 1, 0.93])
|
||||
out = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
'..', 'cycling_visualization.png')
|
||||
plt.savefig(out, dpi=130, bbox_inches='tight')
|
||||
print(f"saved: {out}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user