Add Level Switching paper with surface-switch framework
Defines level cycles, edge switches, surface switches, and facial depth on level components of plane triangulations. Proves outerplanarity of level components and a depth-descent lemma. Introduces balanced surface switches and proves they remove a depth-d level cycle while creating 1-2 new depth-(d-1) cycles. Documents the 9-vertex counterexample where no balanced switch exists and sketches preprocessing toward balancedness, leaving general termination open. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
"""9-vertex L_k where the unique depth-1 face has NO balanced surface switch.
|
||||
|
||||
Outer cycle: 0..8. Triangulated with chords 0-2, 0-3, 3-5, 3-6, 0-6, 6-8.
|
||||
Central triangle F = (0,3,6) has depth 1; its three neighbours
|
||||
(0,2,3), (3,5,6), (6,8,0) are all depth 0 but each has only ONE
|
||||
outer-cycle edge (not two), so none is an "ear" of F.
|
||||
|
||||
For d = 1, balancedness requires F' to be an ear of uv (both non-uv
|
||||
edges on the outer cycle). No neighbour of F qualifies.
|
||||
"""
|
||||
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 = 9
|
||||
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)]
|
||||
CHORDS = [(0, 2), (0, 3), (3, 5), (3, 6), (0, 6), (6, 8)]
|
||||
|
||||
FACES = [
|
||||
(0, 1, 2), # ear
|
||||
(0, 2, 3), # 1 outer edge, depth 0
|
||||
(3, 4, 5), # ear
|
||||
(3, 5, 6), # 1 outer edge, depth 0
|
||||
(6, 7, 8), # ear
|
||||
(6, 8, 0), # 1 outer edge, depth 0
|
||||
(0, 3, 6), # central, depth 1 -- the troublemaker
|
||||
]
|
||||
|
||||
|
||||
def face_edges(f):
|
||||
return {frozenset((f[0], f[1])), frozenset((f[1], f[2])),
|
||||
frozenset((f[0], f[2]))}
|
||||
|
||||
|
||||
outer_set = {frozenset(e) for e in OUTER_EDGES}
|
||||
|
||||
D = nx.Graph()
|
||||
D.add_nodes_from(range(len(FACES)))
|
||||
for i, fi in enumerate(FACES):
|
||||
for j, fj in enumerate(FACES):
|
||||
if i < j and face_edges(fi) & face_edges(fj):
|
||||
D.add_edge(i, j)
|
||||
|
||||
B = [i for i, f in enumerate(FACES)
|
||||
if len(face_edges(f) & outer_set) >= 1]
|
||||
depth = {i: min(nx.shortest_path_length(D, i, b) for b in B)
|
||||
for i in range(len(FACES))}
|
||||
|
||||
palette = {0: '#86efac', 1: '#fde68a', 2: '#fca5a5'}
|
||||
edge_pal = {0: '#16a34a', 1: '#d97706', 2: '#dc2626'}
|
||||
|
||||
fig, ax = plt.subplots(figsize=(7, 7))
|
||||
for i, f in enumerate(FACES):
|
||||
d = depth[i]
|
||||
poly = Polygon([POS[v] for v in f], closed=True,
|
||||
facecolor=palette[d], edgecolor=edge_pal[d],
|
||||
linewidth=1.6, alpha=0.7, zorder=0)
|
||||
ax.add_patch(poly)
|
||||
cx = sum(POS[v][0] for v in f) / 3
|
||||
cy = sum(POS[v][1] for v in f) / 3
|
||||
ax.text(cx, cy, rf'$\mathrm{{depth}}={d}$',
|
||||
ha='center', va='center', fontsize=10,
|
||||
color=edge_pal[d], fontweight='bold')
|
||||
|
||||
# Mark the three "bad" chord edges (would-be-switched edges of F that
|
||||
# fail balancedness because the chord side has no outer-cycle edge to
|
||||
# pair with).
|
||||
F_edges = [(0, 3), (3, 6), (0, 6)]
|
||||
for (a, b) in OUTER_EDGES + CHORDS:
|
||||
color = '#333'; lw = 1.2
|
||||
if (a, b) in F_edges or (b, a) in F_edges:
|
||||
color = '#dc2626'; 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=300, c='#1f2937', edgecolors='black',
|
||||
linewidths=1.0, zorder=2)
|
||||
ax.text(x, y, str(i), ha='center', va='center',
|
||||
fontsize=10, color='white', fontweight='bold', zorder=3)
|
||||
|
||||
ax.set_aspect('equal'); ax.axis('off')
|
||||
ax.set_xlim(-1.3, 1.3); ax.set_ylim(-1.3, 1.3)
|
||||
ax.set_title('Depth-1 face with no balanced surface switch',
|
||||
fontsize=12)
|
||||
fig.tight_layout()
|
||||
out = os.path.join(OUT_DIR, 'fig_no_balanced_switch.png')
|
||||
fig.savefig(out, dpi=180, bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
print(f'wrote {out}')
|
||||
for i, f in enumerate(FACES):
|
||||
print(f' {f} -> depth {depth[i]}')
|
||||
@@ -0,0 +1,179 @@
|
||||
"""Counterexample showing that a surface switch on the edge between a
|
||||
depth-d face F and a depth-(d-1) face F' can create a new face of depth
|
||||
d (not d-1) when the depth-0 neighbor of F' lies on only one side of
|
||||
the shared edge.
|
||||
|
||||
14-vertex maximal outerplanar L_k. Outer cycle order:
|
||||
u, p1, p2, p3, x, q1, v, b1, b2, b3, w, a1, a2, a3 -> u
|
||||
|
||||
Central triangle F = (u, v, w) has depth 2.
|
||||
F' = (u, v, x) has depth 1 (its depth-0 neighbor is the q1-ear on the
|
||||
v-side of x; its u-side neighbor A_ux is depth 1).
|
||||
A_uw = (u, a2, w), A_vw = (v, b2, w) are both depth 1.
|
||||
|
||||
Surface switch on uv: flip uv -> wx. New faces are
|
||||
A = (u, w, x) and B = (v, w, x).
|
||||
B inherits the depth-0 q1-ear, so depth(B) = 1.
|
||||
A's neighbors are A_uw (1), A_ux (1), B (1), so depth(A) = 2 = d. BAD.
|
||||
"""
|
||||
import os
|
||||
import math
|
||||
import networkx as nx
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.patches import Polygon
|
||||
|
||||
OUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir)
|
||||
|
||||
OUTER = ['u', 'p1', 'p2', 'p3', 'x', 'q1', 'v',
|
||||
'b1', 'b2', 'b3', 'w', 'a1', 'a2', 'a3']
|
||||
n = len(OUTER)
|
||||
|
||||
POS = {}
|
||||
for i, name in enumerate(OUTER):
|
||||
a = math.radians(90 - i * (360 / n))
|
||||
POS[name] = (math.cos(a), math.sin(a))
|
||||
|
||||
OUTER_EDGES = [(OUTER[i], OUTER[(i + 1) % n]) for i in range(n)]
|
||||
|
||||
CHORDS_BEFORE = [
|
||||
('u', 'v'), ('u', 'w'), ('v', 'w'), # central F = (u,v,w)
|
||||
('u', 'x'), ('v', 'x'), # F' = (u,v,x)
|
||||
('u', 'a2'), ('a2', 'w'), # A_uw = (u,a2,w)
|
||||
('v', 'b2'), ('b2', 'w'), # A_vw = (v,b2,w)
|
||||
('u', 'p2'), ('p2', 'x'), # A_ux = (u,p2,x)
|
||||
]
|
||||
|
||||
CHORDS_AFTER = [c for c in CHORDS_BEFORE if set(c) != {'u', 'v'}] + [('w', 'x')]
|
||||
|
||||
FACES_BEFORE = [
|
||||
('u', 'v', 'w'), # F (depth 2 -- bad)
|
||||
('u', 'v', 'x'), # F' (depth 1)
|
||||
('u', 'a2', 'w'), # A_uw (depth 1)
|
||||
('v', 'b2', 'w'), # A_vw (depth 1)
|
||||
('u', 'p2', 'x'), # A_ux (depth 1)
|
||||
('v', 'q1', 'x'), # A_vx (depth 0)
|
||||
('u', 'a1', 'a2'), # depth 0
|
||||
('a2', 'a3', 'w'), # depth 0
|
||||
('v', 'b1', 'b2'), # depth 0
|
||||
('b2', 'b3', 'w'), # depth 0
|
||||
('u', 'p1', 'p2'), # depth 0
|
||||
('p2', 'p3', 'x'), # depth 0
|
||||
]
|
||||
|
||||
FACES_AFTER = [
|
||||
('u', 'w', 'x'), # A (depth 2 -- still bad!)
|
||||
('v', 'w', 'x'), # B (depth 1)
|
||||
('u', 'a2', 'w'),
|
||||
('v', 'b2', 'w'),
|
||||
('u', 'p2', 'x'),
|
||||
('v', 'q1', 'x'),
|
||||
('u', 'a1', 'a2'),
|
||||
('a2', 'a3', 'w'),
|
||||
('v', 'b1', 'b2'),
|
||||
('b2', 'b3', 'w'),
|
||||
('u', 'p1', 'p2'),
|
||||
('p2', 'p3', 'x'),
|
||||
]
|
||||
|
||||
|
||||
def compute_depths(faces, chords):
|
||||
"""Compute facial depth for each face using threshold-1 definition."""
|
||||
outer_set = {frozenset(e) for e in OUTER_EDGES}
|
||||
|
||||
def face_edges(f):
|
||||
return {frozenset((f[0], f[1])), frozenset((f[1], f[2])),
|
||||
frozenset((f[0], f[2]))}
|
||||
|
||||
# Build inner-face dual
|
||||
D = nx.Graph()
|
||||
D.add_nodes_from(range(len(faces)))
|
||||
for i, fi in enumerate(faces):
|
||||
for j, fj in enumerate(faces):
|
||||
if i < j and face_edges(fi) & face_edges(fj):
|
||||
D.add_edge(i, j)
|
||||
|
||||
B = [i for i, f in enumerate(faces)
|
||||
if len(face_edges(f) & outer_set) >= 1]
|
||||
|
||||
depth = {}
|
||||
for i in range(len(faces)):
|
||||
if not B:
|
||||
depth[i] = float('inf')
|
||||
continue
|
||||
depth[i] = min(nx.shortest_path_length(D, i, b) for b in B)
|
||||
return depth
|
||||
|
||||
|
||||
def draw_panel(ax, faces, chords, depth, title, highlight_edge=None,
|
||||
highlight_face=None):
|
||||
palette = {0: '#86efac', 1: '#fde68a', 2: '#fca5a5'}
|
||||
edge_pal = {0: '#16a34a', 1: '#d97706', 2: '#dc2626'}
|
||||
|
||||
for i, f in enumerate(faces):
|
||||
d = depth[i]
|
||||
face_color = palette.get(d, '#ddd')
|
||||
face_edge = edge_pal.get(d, '#333')
|
||||
lw = 1.4
|
||||
if highlight_face is not None and i == highlight_face:
|
||||
face_edge = '#7c2d12'
|
||||
lw = 3.0
|
||||
poly = Polygon([POS[v] for v in f], closed=True,
|
||||
facecolor=face_color, edgecolor=face_edge,
|
||||
linewidth=lw, alpha=0.7, zorder=0)
|
||||
ax.add_patch(poly)
|
||||
cx = sum(POS[v][0] for v in f) / 3
|
||||
cy = sum(POS[v][1] for v in f) / 3
|
||||
ax.text(cx, cy, str(d), ha='center', va='center', fontsize=11,
|
||||
color=edge_pal.get(d, '#333'), fontweight='bold')
|
||||
|
||||
# Draw edges
|
||||
all_edges = OUTER_EDGES + list(chords)
|
||||
for (a, b) in all_edges:
|
||||
color = '#333'; lw = 1.2
|
||||
if highlight_edge is not None and {a, b} == set(highlight_edge):
|
||||
color = '#dc2626'; lw = 3.5
|
||||
elif (a, b) == ('w', 'x') or (a, b) == ('x', 'w'):
|
||||
color = '#16a34a'; lw = 3.0
|
||||
ax.plot([POS[a][0], POS[b][0]], [POS[a][1], POS[b][1]],
|
||||
color=color, linewidth=lw, zorder=1)
|
||||
|
||||
# Draw vertices
|
||||
for name, (x, y) in POS.items():
|
||||
ax.scatter([x], [y], s=260, c='#1f2937', edgecolors='black',
|
||||
linewidths=0.8, zorder=2)
|
||||
ax.text(x, y, name, ha='center', va='center',
|
||||
fontsize=8, color='white', fontweight='bold', zorder=3)
|
||||
|
||||
ax.set_aspect('equal'); ax.axis('off')
|
||||
ax.set_xlim(-1.35, 1.35); ax.set_ylim(-1.35, 1.35)
|
||||
ax.set_title(title, fontsize=12)
|
||||
|
||||
|
||||
def main():
|
||||
depth_before = compute_depths(FACES_BEFORE, CHORDS_BEFORE)
|
||||
depth_after = compute_depths(FACES_AFTER, CHORDS_AFTER)
|
||||
|
||||
print("BEFORE:")
|
||||
for i, f in enumerate(FACES_BEFORE):
|
||||
print(f" {f} -> depth {depth_before[i]}")
|
||||
print("AFTER:")
|
||||
for i, f in enumerate(FACES_AFTER):
|
||||
print(f" {f} -> depth {depth_after[i]}")
|
||||
|
||||
fig, axes = plt.subplots(1, 2, figsize=(15, 7.5))
|
||||
draw_panel(axes[0], FACES_BEFORE, CHORDS_BEFORE, depth_before,
|
||||
r'Before: $F=(u,v,w)$ depth 2, $F\'=(u,v,x)$ depth 1',
|
||||
highlight_edge=('u', 'v'), highlight_face=0)
|
||||
draw_panel(axes[1], FACES_AFTER, CHORDS_AFTER, depth_after,
|
||||
r'After surface switch on $uv$: $A=(u,w,x)$ still depth 2!',
|
||||
highlight_face=0)
|
||||
|
||||
fig.tight_layout()
|
||||
out = os.path.join(OUT_DIR, 'fig_counterexample_surface_switch.png')
|
||||
fig.savefig(out, dpi=180, bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
print(f'wrote {out}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,370 @@
|
||||
"""Generate the three definition figures for the Level Switching paper.
|
||||
|
||||
Uses a stacked 7-vertex triangulation T:
|
||||
outer triangle {0,1,2}, inner vertex 3 connected to all three,
|
||||
then vertices 4,5,6 inserted into faces (1,2,3),(0,2,3),(0,1,3).
|
||||
"""
|
||||
import os
|
||||
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)
|
||||
|
||||
# Vertex positions (hand-placed for a clean planar drawing).
|
||||
POS = {
|
||||
0: (-1.5, -0.9),
|
||||
1: (1.5, -0.9),
|
||||
2: (0.0, 1.6),
|
||||
3: (0.0, 0.0),
|
||||
4: (0.55, 0.2), # in face (1,2,3)
|
||||
5: (-0.55, 0.2), # in face (0,2,3)
|
||||
6: (0.0, -0.55), # in face (0,1,3)
|
||||
}
|
||||
|
||||
EDGES = [
|
||||
(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3), # K_4
|
||||
(4, 1), (4, 2), (4, 3), # stack in (1,2,3)
|
||||
(5, 0), (5, 2), (5, 3), # stack in (0,2,3)
|
||||
(6, 0), (6, 1), (6, 3), # stack in (0,1,3)
|
||||
]
|
||||
|
||||
|
||||
def make_graph():
|
||||
G = nx.Graph()
|
||||
G.add_nodes_from(POS.keys())
|
||||
G.add_edges_from(EDGES)
|
||||
return G
|
||||
|
||||
|
||||
def draw_base(ax, G, node_colors, node_size=520, font_color='white',
|
||||
edge_color='#555', edge_width=1.4):
|
||||
nx.draw_networkx_edges(G, POS, ax=ax, edge_color=edge_color, width=edge_width)
|
||||
nx.draw_networkx_nodes(G, POS, ax=ax, node_color=node_colors,
|
||||
node_size=node_size, edgecolors='black', linewidths=1.0)
|
||||
nx.draw_networkx_labels(G, POS, ax=ax, font_color=font_color,
|
||||
font_size=11, font_weight='bold')
|
||||
ax.set_aspect('equal')
|
||||
ax.axis('off')
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Figure 1: Level source (face source vs. degree-3 vertex source)
|
||||
# ---------------------------------------------------------------------------
|
||||
def fig_level_source():
|
||||
G = make_graph()
|
||||
fig, axes = plt.subplots(1, 2, figsize=(10, 5))
|
||||
|
||||
# Panel A: face source S = {0,1,2}
|
||||
ax = axes[0]
|
||||
face_S = {0, 1, 2}
|
||||
colors = ['#ef4444' if v in face_S else '#cbd5e1' for v in G.nodes()]
|
||||
# Highlight the source face
|
||||
tri = Polygon([POS[v] for v in [0, 1, 2]], closed=True,
|
||||
facecolor='#fecaca', edgecolor='#ef4444', linewidth=2.0,
|
||||
alpha=0.45, zorder=0)
|
||||
ax.add_patch(tri)
|
||||
draw_base(ax, G, colors)
|
||||
ax.set_title(r'Face source $S = \{0,1,2\}$', fontsize=12)
|
||||
|
||||
# Panel B: degree-3 vertex source S = {4}
|
||||
ax = axes[1]
|
||||
vert_S = {4}
|
||||
colors = ['#ef4444' if v in vert_S else '#cbd5e1' for v in G.nodes()]
|
||||
draw_base(ax, G, colors)
|
||||
ax.set_title(r'Degree-3 vertex source $S = \{4\}$', fontsize=12)
|
||||
|
||||
fig.tight_layout()
|
||||
out = os.path.join(OUT_DIR, 'fig_level_source.png')
|
||||
fig.savefig(out, dpi=200, bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
print(f'wrote {out}')
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Figure 2: Levels (BFS distance from a source)
|
||||
# ---------------------------------------------------------------------------
|
||||
def fig_levels():
|
||||
G = make_graph()
|
||||
source = 4 # degree-3 vertex source
|
||||
levels = nx.single_source_shortest_path_length(G, source)
|
||||
|
||||
# Color by level
|
||||
palette = {0: '#ef4444', 1: '#f59e0b', 2: '#3b82f6'}
|
||||
colors = [palette[levels[v]] for v in G.nodes()]
|
||||
|
||||
# Labels = level numbers
|
||||
labels = {v: rf'$\ell={levels[v]}$' for v in G.nodes()}
|
||||
|
||||
fig, ax = plt.subplots(figsize=(6.5, 5.5))
|
||||
nx.draw_networkx_edges(G, POS, ax=ax, edge_color='#555', width=1.4)
|
||||
nx.draw_networkx_nodes(G, POS, ax=ax, node_color=colors,
|
||||
node_size=720, edgecolors='black', linewidths=1.0)
|
||||
# Draw vertex id slightly above, level label inside
|
||||
for v, (x, y) in POS.items():
|
||||
ax.text(x, y, str(v), ha='center', va='center',
|
||||
fontsize=10, fontweight='bold', color='white')
|
||||
ax.text(x + 0.18, y + 0.18, rf'$\ell={levels[v]}$',
|
||||
fontsize=10, color='black',
|
||||
bbox=dict(boxstyle='round,pad=0.15',
|
||||
facecolor='white', edgecolor='#999', linewidth=0.6))
|
||||
ax.set_aspect('equal')
|
||||
ax.axis('off')
|
||||
ax.set_title(r'Levels $\ell_G(v)$ from source $S=\{4\}$', fontsize=12)
|
||||
fig.tight_layout()
|
||||
out = os.path.join(OUT_DIR, 'fig_levels.png')
|
||||
fig.savefig(out, dpi=200, bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
print(f'wrote {out}')
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Figure 3: Parity subgraph (even and odd induced subgraphs)
|
||||
# ---------------------------------------------------------------------------
|
||||
def fig_parity_subgraph():
|
||||
G = make_graph()
|
||||
source = 4
|
||||
levels = nx.single_source_shortest_path_length(G, source)
|
||||
parity = {v: levels[v] % 2 for v in G.nodes()}
|
||||
|
||||
even = [v for v in G.nodes() if parity[v] == 0]
|
||||
odd = [v for v in G.nodes() if parity[v] == 1]
|
||||
even_color = '#3b82f6' # blue
|
||||
odd_color = '#f59e0b' # orange
|
||||
|
||||
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
|
||||
|
||||
# Panel A: full triangulation, vertices coloured by parity
|
||||
ax = axes[0]
|
||||
colors = [even_color if parity[v] == 0 else odd_color for v in G.nodes()]
|
||||
draw_base(ax, G, colors)
|
||||
ax.set_title(r"$G'$ with vertices coloured by $\ell_G$ mod 2", fontsize=12)
|
||||
|
||||
# Panel B: even parity subgraph (induced on even vertices)
|
||||
ax = axes[1]
|
||||
# Draw all edges faintly, then the induced subgraph in colour
|
||||
nx.draw_networkx_edges(G, POS, ax=ax, edge_color='#ddd', width=1.0)
|
||||
even_sub = G.subgraph(even)
|
||||
nx.draw_networkx_edges(even_sub, POS, ax=ax, edge_color=even_color, width=2.4)
|
||||
node_colors = [even_color if v in even else '#e5e7eb' for v in G.nodes()]
|
||||
nx.draw_networkx_nodes(G, POS, ax=ax, node_color=node_colors,
|
||||
node_size=520, edgecolors='black', linewidths=1.0)
|
||||
nx.draw_networkx_labels(G, POS, ax=ax,
|
||||
font_color='white', font_size=11, font_weight='bold')
|
||||
ax.set_aspect('equal')
|
||||
ax.axis('off')
|
||||
ax.set_title(r"Even parity subgraph $E_{G,S}(G')$", fontsize=12)
|
||||
|
||||
# Panel C: odd parity subgraph
|
||||
ax = axes[2]
|
||||
nx.draw_networkx_edges(G, POS, ax=ax, edge_color='#ddd', width=1.0)
|
||||
odd_sub = G.subgraph(odd)
|
||||
nx.draw_networkx_edges(odd_sub, POS, ax=ax, edge_color=odd_color, width=2.4)
|
||||
node_colors = [odd_color if v in odd else '#e5e7eb' for v in G.nodes()]
|
||||
nx.draw_networkx_nodes(G, POS, ax=ax, node_color=node_colors,
|
||||
node_size=520, edgecolors='black', linewidths=1.0)
|
||||
nx.draw_networkx_labels(G, POS, ax=ax,
|
||||
font_color='white', font_size=11, font_weight='bold')
|
||||
ax.set_aspect('equal')
|
||||
ax.axis('off')
|
||||
ax.set_title(r"Odd parity subgraph $O_{G,S}(G')$", fontsize=12)
|
||||
|
||||
fig.tight_layout()
|
||||
out = os.path.join(OUT_DIR, 'fig_parity_subgraph.png')
|
||||
fig.savefig(out, dpi=200, bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
print(f'wrote {out}')
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Figure: Level cycle (simple cycle within a single level)
|
||||
# ---------------------------------------------------------------------------
|
||||
def fig_level_cycle():
|
||||
G = make_graph()
|
||||
source = 4
|
||||
levels = nx.single_source_shortest_path_length(G, source)
|
||||
|
||||
palette = {0: '#ef4444', 1: '#f59e0b', 2: '#3b82f6'}
|
||||
colors = [palette[levels[v]] for v in G.nodes()]
|
||||
|
||||
# Level cycle: 1-2-3-1 lies entirely in L_1
|
||||
cycle_edges = [(1, 2), (2, 3), (1, 3)]
|
||||
|
||||
fig, ax = plt.subplots(figsize=(6.5, 5.5))
|
||||
nx.draw_networkx_edges(G, POS, ax=ax, edge_color='#bbb', width=1.2)
|
||||
nx.draw_networkx_edges(G, POS, edgelist=cycle_edges, ax=ax,
|
||||
edge_color='#dc2626', width=3.4)
|
||||
nx.draw_networkx_nodes(G, POS, ax=ax, node_color=colors,
|
||||
node_size=620, edgecolors='black', linewidths=1.0)
|
||||
nx.draw_networkx_labels(G, POS, ax=ax, font_color='white',
|
||||
font_size=11, font_weight='bold')
|
||||
# Annotate levels in small floating labels
|
||||
for v, (x, y) in POS.items():
|
||||
ax.text(x + 0.18, y + 0.18, rf'$\ell={levels[v]}$',
|
||||
fontsize=9, color='black',
|
||||
bbox=dict(boxstyle='round,pad=0.12',
|
||||
facecolor='white', edgecolor='#999', linewidth=0.5))
|
||||
ax.set_aspect('equal')
|
||||
ax.axis('off')
|
||||
ax.set_title(r'Level cycle in $L_1 = \{1,2,3\}$ (highlighted)', fontsize=12)
|
||||
fig.tight_layout()
|
||||
out = os.path.join(OUT_DIR, 'fig_level_cycle.png')
|
||||
fig.savefig(out, dpi=200, bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
print(f'wrote {out}')
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Figure: Edge switch (flip on a level-cycle edge)
|
||||
# ---------------------------------------------------------------------------
|
||||
def fig_edge_switch():
|
||||
G = make_graph()
|
||||
source = 4
|
||||
levels = nx.single_source_shortest_path_length(G, source)
|
||||
palette = {0: '#ef4444', 1: '#f59e0b', 2: '#3b82f6'}
|
||||
colors = [palette[levels[v]] for v in G.nodes()]
|
||||
|
||||
# We switch edge (1,2), which lies in the L_1 cycle 1-2-3-1.
|
||||
# Its two adjacent faces in T are (0,1,2) and (1,2,4); the flip
|
||||
# removes 1-2 and adds 0-4.
|
||||
removed = (1, 2)
|
||||
added = (0, 4)
|
||||
|
||||
Gprime = G.copy()
|
||||
Gprime.remove_edge(*removed)
|
||||
Gprime.add_edge(*added)
|
||||
|
||||
fig, axes = plt.subplots(1, 2, figsize=(12, 5.5))
|
||||
|
||||
# Panel A: before — highlight the level-cycle edge to be switched
|
||||
ax = axes[0]
|
||||
other_edges = [e for e in G.edges() if set(e) != set(removed)]
|
||||
nx.draw_networkx_edges(G, POS, edgelist=other_edges, ax=ax,
|
||||
edge_color='#bbb', width=1.2)
|
||||
nx.draw_networkx_edges(G, POS, edgelist=[removed], ax=ax,
|
||||
edge_color='#dc2626', width=3.4)
|
||||
nx.draw_networkx_nodes(G, POS, ax=ax, node_color=colors,
|
||||
node_size=560, edgecolors='black', linewidths=1.0)
|
||||
nx.draw_networkx_labels(G, POS, ax=ax, font_color='white',
|
||||
font_size=11, font_weight='bold')
|
||||
ax.set_aspect('equal'); ax.axis('off')
|
||||
ax.set_title(r'Before: edge $1\!-\!2$ lies on the $L_1$ cycle',
|
||||
fontsize=12)
|
||||
|
||||
# Panel B: after — the new edge highlighted in green
|
||||
ax = axes[1]
|
||||
other_edges = [e for e in Gprime.edges() if set(e) != set(added)]
|
||||
nx.draw_networkx_edges(Gprime, POS, edgelist=other_edges, ax=ax,
|
||||
edge_color='#bbb', width=1.2)
|
||||
nx.draw_networkx_edges(Gprime, POS, edgelist=[added], ax=ax,
|
||||
edge_color='#16a34a', width=3.4)
|
||||
nx.draw_networkx_nodes(Gprime, POS, ax=ax, node_color=colors,
|
||||
node_size=560, edgecolors='black', linewidths=1.0)
|
||||
nx.draw_networkx_labels(Gprime, POS, ax=ax, font_color='white',
|
||||
font_size=11, font_weight='bold')
|
||||
ax.set_aspect('equal'); ax.axis('off')
|
||||
ax.set_title(r'After: $1\!-\!2$ replaced by $0\!-\!4$',
|
||||
fontsize=12)
|
||||
|
||||
fig.tight_layout()
|
||||
out = os.path.join(OUT_DIR, 'fig_edge_switch.png')
|
||||
fig.savefig(out, dpi=200, bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
print(f'wrote {out}')
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Figure: Facial depth (depths in an outerplanar L_k)
|
||||
# ---------------------------------------------------------------------------
|
||||
def fig_facial_depth():
|
||||
import math
|
||||
# 12-vertex maximal outerplanar graph with 3-fold symmetry.
|
||||
# Central triangle (0,4,8); three "in-between" triangles (0,2,4),
|
||||
# (4,6,8), (0,8,10) sit between the central triangle and the
|
||||
# outer "ears" (0,1,2), (2,3,4), (4,5,6), (6,7,8), (8,9,10),
|
||||
# (10,11,0).
|
||||
n = 12
|
||||
pos = {}
|
||||
for i in range(n):
|
||||
a = math.radians(90 - i * (360 / n))
|
||||
pos[i] = (math.cos(a), math.sin(a))
|
||||
|
||||
outer_edges = [(i, (i + 1) % n) for i in range(n)]
|
||||
diagonals = [(0, 2), (2, 4), (4, 6), (6, 8), (8, 10), (10, 0), # "short" chords
|
||||
(0, 4), (4, 8), (0, 8)] # central triangle
|
||||
L = nx.Graph()
|
||||
L.add_nodes_from(pos)
|
||||
L.add_edges_from(outer_edges + diagonals)
|
||||
|
||||
inner_faces = [
|
||||
(0, 1, 2), (2, 3, 4), (4, 5, 6),
|
||||
(6, 7, 8), (8, 9, 10), (10, 11, 0), # 6 outer "ears"
|
||||
(0, 2, 4), (4, 6, 8), (0, 8, 10), # 3 in-between
|
||||
(0, 4, 8), # central
|
||||
]
|
||||
|
||||
# Build dual graph on inner faces: edge iff faces share an edge
|
||||
def face_edges(f):
|
||||
a, b, c = f
|
||||
return {frozenset((a, b)), frozenset((b, c)), frozenset((a, c))}
|
||||
|
||||
outer_edge_set = {frozenset(e) for e in outer_edges}
|
||||
D = nx.Graph()
|
||||
D.add_nodes_from(range(len(inner_faces)))
|
||||
for i, fi in enumerate(inner_faces):
|
||||
for j, fj in enumerate(inner_faces):
|
||||
if i < j and face_edges(fi) & face_edges(fj):
|
||||
D.add_edge(i, j)
|
||||
|
||||
# Boundary set B: faces whose bounding level cycle has >= 1 outer-cycle edge
|
||||
B = [i for i, f in enumerate(inner_faces)
|
||||
if len(face_edges(f) & outer_edge_set) >= 1]
|
||||
|
||||
# Depth = min distance in D to any face in B
|
||||
depth = {}
|
||||
for i in range(len(inner_faces)):
|
||||
dists = [nx.shortest_path_length(D, i, b) for b in B]
|
||||
depth[i] = min(dists)
|
||||
|
||||
# Colour by depth
|
||||
depth_color = {0: '#86efac', 1: '#fde68a', 2: '#fca5a5'}
|
||||
depth_edge = {0: '#16a34a', 1: '#d97706', 2: '#dc2626'}
|
||||
|
||||
fig, ax = plt.subplots(figsize=(7, 7))
|
||||
# Fill faces by depth
|
||||
for i, f in enumerate(inner_faces):
|
||||
poly = Polygon([pos[v] for v in f], closed=True,
|
||||
facecolor=depth_color[depth[i]],
|
||||
edgecolor=depth_edge[depth[i]],
|
||||
linewidth=1.5, alpha=0.7, zorder=0)
|
||||
ax.add_patch(poly)
|
||||
cx = sum(pos[v][0] for v in f) / 3
|
||||
cy = sum(pos[v][1] for v in f) / 3
|
||||
ax.text(cx, cy, rf'$\mathrm{{depth}}={depth[i]}$',
|
||||
ha='center', va='center', fontsize=10,
|
||||
color=depth_edge[depth[i]], fontweight='bold')
|
||||
|
||||
# Draw the graph on top
|
||||
nx.draw_networkx_edges(L, pos, ax=ax, edge_color='#333', width=1.4)
|
||||
nx.draw_networkx_nodes(L, pos, ax=ax, node_color='#1f2937',
|
||||
node_size=320, edgecolors='black', linewidths=1.0)
|
||||
nx.draw_networkx_labels(L, pos, ax=ax, font_color='white',
|
||||
font_size=10, font_weight='bold')
|
||||
|
||||
ax.set_aspect('equal'); ax.axis('off')
|
||||
ax.set_xlim(-1.3, 1.3); ax.set_ylim(-1.3, 1.3)
|
||||
ax.set_title(r'Facial depth in an outerplanar $L_k$', fontsize=12)
|
||||
fig.tight_layout()
|
||||
out = os.path.join(OUT_DIR, 'fig_facial_depth.png')
|
||||
fig.savefig(out, dpi=200, bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
print(f'wrote {out}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
fig_level_source()
|
||||
fig_levels()
|
||||
fig_level_cycle()
|
||||
fig_edge_switch()
|
||||
fig_parity_subgraph()
|
||||
fig_facial_depth()
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Demonstrate the preprocessing strategy on the 9-vertex example.
|
||||
|
||||
Start: F = (0,3,6) at depth 1, no balanced surface switch exists
|
||||
(F has no edge of "span 1" -- no edge with a single outer-cycle
|
||||
vertex between the endpoints, hence no ear neighbour).
|
||||
|
||||
Step: perform the (unbalanced) surface switch on edge uv = 03, with
|
||||
F' = (0,2,3) and third vertex x = 2; in Case (ii) the flip removes 03
|
||||
and adds wx = 62.
|
||||
|
||||
Result: A = (0,2,6) at depth 1 has edge 02 at span 1, so the ear
|
||||
(0,1,2) is now a balanced-switch target.
|
||||
"""
|
||||
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 = 9
|
||||
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]
|
||||
return {i: min(nx.shortest_path_length(D, i, b) for b in B)
|
||||
for i in range(len(faces))}
|
||||
|
||||
|
||||
CHORDS_BEFORE = [(0, 2), (0, 3), (3, 5), (3, 6), (0, 6), (6, 8)]
|
||||
FACES_BEFORE = [
|
||||
(0, 1, 2), (0, 2, 3), (3, 4, 5), (3, 5, 6),
|
||||
(6, 7, 8), (6, 8, 0), (0, 3, 6),
|
||||
]
|
||||
|
||||
# After non-balanced switch on edge 03: remove 03, add 26
|
||||
CHORDS_AFTER = [c for c in CHORDS_BEFORE if set(c) != {0, 3}] + [(2, 6)]
|
||||
FACES_AFTER = [
|
||||
(0, 1, 2), (3, 4, 5), (3, 5, 6),
|
||||
(6, 7, 8), (6, 8, 0),
|
||||
(0, 2, 6), (2, 3, 6),
|
||||
]
|
||||
|
||||
|
||||
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.7, zorder=0)
|
||||
ax.add_patch(poly)
|
||||
cx = sum(POS[v][0] for v in f) / 3
|
||||
cy = sum(POS[v][1] for v in f) / 3
|
||||
ax.text(cx, cy, str(d), ha='center', va='center', fontsize=11,
|
||||
color=edge_pal.get(d, '#333'), fontweight='bold')
|
||||
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=270, c='#1f2937', edgecolors='black',
|
||||
linewidths=1.0, zorder=2)
|
||||
ax.text(x, y, str(i), ha='center', va='center',
|
||||
fontsize=9, color='white', fontweight='bold', zorder=3)
|
||||
ax.set_aspect('equal'); ax.axis('off')
|
||||
ax.set_xlim(-1.3, 1.3); ax.set_ylim(-1.3, 1.3)
|
||||
ax.set_title(title, fontsize=11)
|
||||
|
||||
|
||||
depth_before = compute_depths(FACES_BEFORE)
|
||||
depth_after = compute_depths(FACES_AFTER)
|
||||
print('BEFORE:')
|
||||
for i, f in enumerate(FACES_BEFORE):
|
||||
print(f' {f} -> depth {depth_before[i]}')
|
||||
print('AFTER:')
|
||||
for i, f in enumerate(FACES_AFTER):
|
||||
print(f' {f} -> depth {depth_after[i]}')
|
||||
|
||||
fig, axes = plt.subplots(1, 2, figsize=(14, 7))
|
||||
draw(axes[0], FACES_BEFORE, CHORDS_BEFORE, depth_before,
|
||||
'Before: F=(0,3,6) depth 1; spans (2,2,2) so no ear neighbour',
|
||||
highlight_edges=[(0, 3)])
|
||||
draw(axes[1], FACES_AFTER, CHORDS_AFTER, depth_after,
|
||||
'After non-balanced switch 03->26: A=(0,2,6) depth 1; edge 02 has span 1',
|
||||
green_edges=[(2, 6)], highlight_edges=[(0, 2)])
|
||||
|
||||
fig.tight_layout()
|
||||
out = os.path.join(OUT_DIR, 'fig_preprocessing.png')
|
||||
fig.savefig(out, dpi=180, bbox_inches='tight')
|
||||
plt.close(fig)
|
||||
print(f'wrote {out}')
|
||||
Reference in New Issue
Block a user