coloring_nested_tire_graphs: relax Def 1.5 to allow non-simple inner-boundary walks; drop (R2)

The previous reading of Def 1.5 had B_in be a simple cycle on V(O).
This forced an artificial (R2) hypothesis on Lemma 1.7 ruling out
multi-hole topology of R_{C'}.  But the multi-hole structure is
already captured naturally by the inner outerplanar graph O itself:
when O is not 2-connected (has a bridge, cut-vertex, or multiple
components), its outer-face boundary is a closed walk rather than a
simple cycle, and that walk traverses bridges twice and visits cut-
vertices multiple times.

Changes:

- Definition 1.5: B_in is now defined as the closed walk in O that
  traces O's outer-face boundary in the inherited embedding.  It is
  a simple cycle when O is 2-connected and a non-simple closed walk
  in general.  B_out remains a simple cycle (or degenerate single
  vertex).

- Lemma 1.7: (R2) hypothesis removed.  Only (R1) (manifold) remains.
  The proof of the boundary structure is rewritten: we identify
  B_out as the source-side boundary cycle, O as G[V_{C'} ∩ L_{d+1}]
  (outerplanar by Lemma 2.6 of [bauerfeld-pds]), and B_in as O's
  outer-face boundary walk.  Multi-hole topology of R_{C'} is now
  captured by O being non-2-connected or disconnected, without
  needing an extra constraint.

- Remark 1.10 (was R1+R2): now Remark 1.10 (R1-when), explaining the
  pinch obstruction for (R1) and explicitly noting that multi-hole
  topology does NOT require an additional hypothesis under the new
  Definition 1.5.

Empirical motivation: in the brute-force search over maximal planar
graphs at n in [7, 11] (30,587 components from all single-vertex
sources), 171 components at depth 1 had R_{C'} with 3 boundary
cycles in the surface-classification sense (n_bdry > 2).  Each is a
valid tire graph under the relaxed definition: B_out is the level-d
cycle, O is the level-(d+1) outerplanar subgraph (non-2-connected
when this occurs), and B_in is its outer-face boundary walk.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 16:26:21 -04:00
parent 1ac80aa5cf
commit 717c8cf5ea
11 changed files with 903 additions and 121 deletions
@@ -0,0 +1,253 @@
"""Brute-force search for (R1)/(R2) violations in small maximal planar
graphs.
For each maximal planar graph G on n vertices (with delta(G) >= 3),
for each single-vertex source v_0 in V(G), compute:
- BFS depths from v_0.
- For each face, its dual depth (= min BFS level of vertices).
- For each depth d, connected components of the dual subgraph G'_d.
- For each component, check (R1) manifold property and count (R2)
boundary components.
Report:
- Per depth d, count of components and count of (R1)-violators,
(R2)-violators, both.
Run: sage -python experiments/check_r2.py [n_min] [n_max]
"""
import sys
import json
from collections import defaultdict
from sage.all import Graph
from sage.graphs.graph_generators import graphs
def face_verts(face):
"""face = list of edges [(u, v), (v, w), ...]. Return ordered vertex
cycle [u, v, w, ...]."""
if not face:
return []
verts = [face[0][0]]
for e in face:
verts.append(e[1])
return verts[:-1] # last = first
def face_edges_set(face):
return {frozenset(e) for e in face}
def find_components(face_depths, target_d):
"""face_depths: list of (face, depth, vertex_set).
Returns list of components; each component = list of (face_idx, face_depth_entry)."""
cands = [(i, fd) for i, fd in enumerate(face_depths) if fd[1] == target_d]
n = len(cands)
if n == 0:
return []
edge_to_idxs = defaultdict(list)
for k, (i, fd) in enumerate(cands):
for e in face_edges_set(fd[0]):
edge_to_idxs[e].append(k)
adj = [[] for _ in range(n)]
for idxs in edge_to_idxs.values():
if len(idxs) >= 2:
for a in idxs:
for b in idxs:
if a != b:
adj[a].append(b)
visited = [False] * n
comps = []
for s in range(n):
if visited[s]:
continue
stack = [s]
comp = []
while stack:
x = stack.pop()
if visited[x]:
continue
visited[x] = True
comp.append(cands[x])
for y in adj[x]:
if not visited[y]:
stack.append(y)
comps.append(comp)
return comps
def check_component(comp, G, emb):
"""For a connected component of same-depth faces, check R1 (manifold)
and count boundary components (R2)."""
# boundary edges: edges of G appearing in exactly one face of comp
edge_count = defaultdict(int)
for (_, fd) in comp:
for e in face_edges_set(fd[0]):
edge_count[e] += 1
bdry_edges = {e for e, c in edge_count.items() if c == 1}
if not bdry_edges:
# No boundary (closed surface). In a planar graph this shouldn't happen.
return {'is_manifold': False, 'n_boundary': 0, 'reason': 'no boundary'}
# R1 check: at each vertex v in V(comp), count how many faces of comp
# are incident to v, and check whether they form a contiguous arc in
# v's rotation in emb.
comp_face_edges = []
for (_, fd) in comp:
comp_face_edges.append(face_edges_set(fd[0]))
vertices = set()
for fes in comp_face_edges:
for e in fes:
vertices.update(e)
is_manifold = True
pinch_vertices = []
for v in vertices:
rot = emb[v] # cyclic neighbour order
# The faces incident to v in G are the triangles {v, rot[i], rot[i+1]}.
# Check which ones are in comp.
in_comp = []
for i in range(len(rot)):
u = rot[i]
w = rot[(i + 1) % len(rot)]
face_e = {frozenset({v, u}), frozenset({u, w}), frozenset({v, w})}
if any(face_e == fes for fes in comp_face_edges):
in_comp.append(True)
else:
in_comp.append(False)
# Count "runs" of True in cyclic sequence
n_true = sum(in_comp)
if n_true == 0:
continue
if n_true == len(in_comp):
continue # entire rotation in comp, vertex is interior
# Count transitions from False to True (cyclic)
n_runs = 0
for i in range(len(in_comp)):
if in_comp[i] and not in_comp[(i - 1) % len(in_comp)]:
n_runs += 1
if n_runs > 1:
is_manifold = False
pinch_vertices.append(v)
# R2 check: count boundary components.
# Build bdry_graph: undirected graph on vertices touching bdry_edges,
# edges = bdry_edges.
bdry_graph = defaultdict(set)
for e in bdry_edges:
u, w = list(e)
bdry_graph[u].add(w)
bdry_graph[w].add(u)
# Count connected components
seen = set()
n_components = 0
for v in bdry_graph:
if v in seen:
continue
n_components += 1
stack = [v]
while stack:
x = stack.pop()
if x in seen:
continue
seen.add(x)
for y in bdry_graph[x]:
if y not in seen:
stack.append(y)
return {'is_manifold': is_manifold,
'n_boundary': n_components,
'pinch_vertices': pinch_vertices,
'n_bdry_edges': len(bdry_edges),
'n_faces': len(comp)}
def search(n_min, n_max, max_violations=5, verbose=False):
r1_violations = []
r2_violations = []
summary = defaultdict(int)
for n in range(n_min, n_max + 1):
ntri = 0
for G in graphs.triangulations(n):
ntri += 1
G.is_planar(set_embedding=True)
emb = G.get_embedding()
faces = G.faces(embedding=emb)
for v_source in G.vertices():
dist = G.shortest_path_lengths(v_source)
face_depths = []
for face in faces:
verts = set()
for u, w in face:
verts.add(u); verts.add(w)
d = min(dist[u] for u in verts)
face_depths.append((face, d, verts))
max_d = max(fd[1] for fd in face_depths)
for target_d in range(max_d + 1):
comps = find_components(face_depths, target_d)
for comp_idx, comp in enumerate(comps):
result = check_component(comp, G, emb)
summary[('component', target_d)] += 1
if not result['is_manifold']:
summary[('r1_violation', target_d)] += 1
if len(r1_violations) < max_violations:
r1_violations.append({
'n': n, 'source': int(v_source),
'depth': target_d,
'graph_edges': sorted([sorted(e) for e in G.edges(labels=False)]),
'pinch_vertices': result['pinch_vertices'],
})
if result['is_manifold'] and result['n_boundary'] > 2:
summary[('r2_violation_manifold', target_d)] += 1
if len(r2_violations) < max_violations:
r2_violations.append({
'n': n, 'source': int(v_source),
'depth': target_d,
'n_boundary': result['n_boundary'],
'graph_edges': sorted([sorted(e) for e in G.edges(labels=False)]),
})
if verbose:
print(f"n={n}: scanned {ntri} triangulations")
sys.stdout.flush()
return r1_violations, r2_violations, summary
def main():
n_min = int(sys.argv[1]) if len(sys.argv) > 1 else 7
n_max = int(sys.argv[2]) if len(sys.argv) > 2 else 12
print(f"Searching maximal planar graphs n in [{n_min}, {n_max}]...")
r1s, r2s, summary = search(n_min, n_max, max_violations=5, verbose=True)
print()
print("=== Summary ===")
by_depth = defaultdict(lambda: {'components': 0, 'r1': 0, 'r2_manifold': 0})
for (kind, d), c in summary.items():
if kind == 'component':
by_depth[d]['components'] += c
elif kind == 'r1_violation':
by_depth[d]['r1'] += c
elif kind == 'r2_violation_manifold':
by_depth[d]['r2_manifold'] += c
print(f"{'depth':>6} {'components':>12} {'R1 viol':>10} {'R2 viol (manifold)':>20}")
for d in sorted(by_depth):
s = by_depth[d]
print(f"{d:>6} {s['components']:>12} {s['r1']:>10} {s['r2_manifold']:>20}")
print()
if r1s:
print(f"=== R1 violations (first {len(r1s)}) ===")
for v in r1s:
print(f" n={v['n']}, source={v['source']}, depth={v['depth']}, "
f"pinches={v['pinch_vertices']}")
if r2s:
print(f"=== R2 violations (manifold, n_bdry > 2) (first {len(r2s)}) ===")
for v in r2s:
print(f" n={v['n']}, source={v['source']}, depth={v['depth']}, "
f"n_bdry={v['n_boundary']}")
print(f" edges: {v['graph_edges']}")
if not r1s and not r2s:
print("No R1 or R2 violations found in this range.")
if __name__ == '__main__':
main()
@@ -0,0 +1,191 @@
"""Draw an actual maximal planar graph exhibiting an (R2) violation.
Uses the first such example found by experiments/check_r2.py:
n=10, source=4, depth=1, n_bdry=3.
"""
import os
import sys
from collections import defaultdict
from sage.all import Graph
from sage.graphs.graph_generators import graphs
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import matplotlib.pyplot as plt
import matplotlib.patches as patches
# Edges from the first n=10 R2-violating triangulation found.
EDGES = [(1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9),
(2, 3), (2, 8), (2, 9), (3, 4), (3, 5), (3, 6), (3, 7), (3, 8),
(3, 10), (4, 5), (5, 6), (6, 7), (6, 10), (7, 8), (7, 10), (8, 9)]
SOURCE = 4
DEPTH = 1
def main():
G = Graph(EDGES)
G.is_planar(set_embedding=True)
emb = G.get_embedding()
dist = G.shortest_path_lengths(SOURCE)
print(f"BFS distances from {SOURCE}: {dict(dist)}")
faces = G.faces(embedding=emb)
face_depths = []
for face in faces:
verts = set()
for u, v in face:
verts.add(u); verts.add(v)
d = min(dist[v] for v in verts)
face_depths.append((face, d, verts))
# Find depth-DEPTH faces and their connected components
target_faces = [(i, fd) for i, fd in enumerate(face_depths)
if fd[1] == DEPTH]
print(f"Found {len(target_faces)} faces at depth {DEPTH}")
edge_to_idxs = defaultdict(list)
for k, (i, fd) in enumerate(target_faces):
for u, v in fd[0]:
edge_to_idxs[frozenset({u, v})].append(k)
n = len(target_faces)
adj = [[] for _ in range(n)]
for idxs in edge_to_idxs.values():
if len(idxs) >= 2:
for a in idxs:
for b in idxs:
if a != b:
adj[a].append(b)
visited = [False] * n
comps = []
for s in range(n):
if visited[s]:
continue
stack = [s]; comp = []
while stack:
x = stack.pop()
if visited[x]: continue
visited[x] = True; comp.append(target_faces[x])
for y in adj[x]:
if not visited[y]: stack.append(y)
comps.append(comp)
print(f"Connected components at depth {DEPTH}: {len(comps)} sizes "
f"{[len(c) for c in comps]}")
# Pick the largest one
comp = max(comps, key=lambda c: len(c))
# Compute boundary edges of comp
edge_count = defaultdict(int)
comp_face_edges = []
for _, fd in comp:
fe = {frozenset({u, v}) for u, v in fd[0]}
comp_face_edges.append(fe)
for e in fe:
edge_count[e] += 1
bdry_edges = {e for e, c in edge_count.items() if c == 1}
# Boundary connected components
bdry_graph = defaultdict(set)
for e in bdry_edges:
u, v = list(e)
bdry_graph[u].add(v)
bdry_graph[v].add(u)
seen = set()
bdry_cycles = []
for v in bdry_graph:
if v in seen: continue
comp_v = set(); stack = [v]
while stack:
x = stack.pop()
if x in seen: continue
seen.add(x); comp_v.add(x)
for y in bdry_graph[x]:
if y not in seen: stack.append(y)
bdry_cycles.append(comp_v)
print(f"Boundary components: {len(bdry_cycles)}, vertex sets: {bdry_cycles}")
# Compute layout using sage's planar layout
pos = G.layout(layout='planar')
# Plot
fig, ax = plt.subplots(figsize=(9, 9))
# Fill in depth-1 component faces
for _, fd in comp:
face = fd[0]
cycle_verts = [edge[0] for edge in face]
poly = plt.Polygon([pos[v] for v in cycle_verts],
facecolor='#fff3cd', edgecolor='none',
alpha=0.55, zorder=0)
ax.add_patch(poly)
# Draw all edges of G
for u, v in G.edges(labels=False):
x1, y1 = pos[u]; x2, y2 = pos[v]
ax.plot([x1, x2], [y1, y2], color='lightgray', linewidth=0.8,
zorder=1)
# Highlight boundary edges of comp (in 3 different colors)
colors = ['#d62728', '#1f77b4', '#2ca02c']
cyclic_label = ['boundary 1', 'boundary 2', 'boundary 3']
for i, bc_verts in enumerate(bdry_cycles):
for e in bdry_edges:
if e <= bc_verts:
u, v = list(e)
x1, y1 = pos[u]; x2, y2 = pos[v]
ax.plot([x1, x2], [y1, y2], color=colors[i % 3],
linewidth=3.0, zorder=2)
# Draw vertices, color-coded by BFS level
level_colors = {0: 'black', 1: '#1f77b4', 2: '#d62728'}
for v in G.vertices():
x, y = pos[v]
c = level_colors.get(dist[v], 'gray')
ax.plot(x, y, 'o', color=c, markersize=22, zorder=3)
ax.annotate(f"{v}", (x, y), color='white', ha='center', va='center',
fontsize=11, fontweight='bold', zorder=4)
# Add depth annotation
ax.annotate(f"$\\ell={dist[v]}$", (x, y),
xytext=(x + 0.05, y + 0.05), fontsize=9, zorder=4,
color='black',
bbox=dict(boxstyle='round,pad=0.15',
facecolor='white', edgecolor='none',
alpha=0.75))
# Legend
legend_items = [
plt.Line2D([], [], marker='o', color='w', markerfacecolor='black',
markersize=12, label='source $v_0$ ($\\ell=0$)'),
plt.Line2D([], [], marker='o', color='w', markerfacecolor='#1f77b4',
markersize=12, label='$\\ell=1$'),
plt.Line2D([], [], marker='o', color='w', markerfacecolor='#d62728',
markersize=12, label='$\\ell=2$'),
plt.Line2D([], [], color='#fff3cd', linewidth=12,
label=f'$R_{{C^\\prime}}$ (depth-{DEPTH} component, '
f'{len(comp)} faces)'),
plt.Line2D([], [], color='#d62728', linewidth=3,
label='boundary cycle 1'),
plt.Line2D([], [], color='#1f77b4', linewidth=3,
label='boundary cycle 2'),
plt.Line2D([], [], color='#2ca02c', linewidth=3,
label='boundary cycle 3'),
]
ax.legend(handles=legend_items, loc='upper right', fontsize=9,
framealpha=0.95)
ax.set_title(f"(R2) violation: depth-{DEPTH} component with "
f"3 boundary cycles\n(maximal planar graph, $n=10$, "
f"source $v_0={SOURCE}$)",
fontsize=12)
ax.set_aspect('equal')
ax.axis('off')
out = os.path.join(HERE, 'r2_violation_real_example.png')
plt.savefig(out, dpi=160, bbox_inches='tight')
plt.close()
print(f"wrote {out}")
if __name__ == '__main__':
main()
@@ -0,0 +1,181 @@
"""Draw the n=10 R2-violating example with a planar layout that puts
source v_0=4 in the centre. We pick one of the depth-2 face triangles
({2,8,9} or {6,7,10}) as the outer face, which forces sage's planar
layout to put it on the outside and v_0 in the interior."""
import os
import sys
from collections import defaultdict
from sage.all import Graph
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.path import Path
EDGES = [(1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9),
(2, 3), (2, 8), (2, 9), (3, 4), (3, 5), (3, 6), (3, 7), (3, 8),
(3, 10), (4, 5), (5, 6), (6, 7), (6, 10), (7, 8), (7, 10), (8, 9)]
SOURCE = 4
DEPTH = 1
def main():
G = Graph(EDGES)
G.is_planar(set_embedding=True)
emb = G.get_embedding()
dist = G.shortest_path_lengths(SOURCE)
# Use sage's planar layout with external_face = the depth-2 face {2, 8, 9}.
# The 'external_face' parameter in sage takes a face (list of edges).
# We supply edges (2,8), (8,9), (9,2) in cyclic order.
# Force external face to be the depth-2 triangle {2, 8, 9} by
# giving an edge of that triangle.
pos = G.layout(layout='planar', external_face=(2, 8))
# Normalise positions
xs = [p[0] for p in pos.values()]
ys = [p[1] for p in pos.values()]
cx, cy = (max(xs) + min(xs)) / 2, (max(ys) + min(ys)) / 2
scale = max(max(xs) - min(xs), max(ys) - min(ys))
pos = {v: ((p[0] - cx) / scale, (p[1] - cy) / scale)
for v, p in pos.items()}
# Compute depth-1 component
faces = G.faces(embedding=emb)
face_depths = []
for face in faces:
verts = set()
for u, v in face:
verts.add(u); verts.add(v)
d = min(dist[v] for v in verts)
face_depths.append((face, d, verts))
depth1_faces = [fd for fd in face_depths if fd[1] == DEPTH]
# Boundary edges
edge_count = defaultdict(int)
for face, _, _ in depth1_faces:
for u, v in face:
edge_count[frozenset({u, v})] += 1
bdry_edges = {e for e, c in edge_count.items() if c == 1}
bdry_graph = defaultdict(set)
for e in bdry_edges:
u, v = list(e)
bdry_graph[u].add(v)
bdry_graph[v].add(u)
seen = set()
bdry_cycles = []
for v in bdry_graph:
if v in seen: continue
cv = set(); stack = [v]
while stack:
x = stack.pop()
if x in seen: continue
seen.add(x); cv.add(x)
for y in bdry_graph[x]:
if y not in seen: stack.append(y)
bdry_cycles.append(cv)
# Sort: source-side first (= contains a level-1 vertex), then by min vertex
def cyc_key(c):
levels = {dist[v] for v in c}
return (min(levels), min(c))
bdry_cycles.sort(key=cyc_key)
print(f"Boundary cycles: {bdry_cycles}")
# Plot
fig, ax = plt.subplots(figsize=(10, 9))
# Fill the depth-1 face union with one polygon: use the boundary curves
# Construct the outer boundary curve and the inner hole curves.
# The outer-facing boundary cycle (level d, source side) — but with
# external_face being a level-2 triangle, the "outer face" of the
# picture is the depth-2 triangle, and the depth-1 region is between.
# Easier: just fill each depth-1 triangle separately.
for face, _, _ in depth1_faces:
verts = [e[0] for e in face]
xy = [pos[v] for v in verts]
poly = plt.Polygon(xy, facecolor='#fff3cd', edgecolor='#e8d8a8',
linewidth=0.4, alpha=0.85, zorder=1)
ax.add_patch(poly)
# Draw ALL non-boundary edges of G (interior and outside) in light grey
for u, v in G.edges(labels=False):
if frozenset({u, v}) in bdry_edges:
continue
x1, y1 = pos[u]; x2, y2 = pos[v]
ax.plot([x1, x2], [y1, y2], color='lightgray', linewidth=0.9,
zorder=2)
# Boundary cycles in different colors
bdry_colors = ['#d62728', '#1f77b4', '#2ca02c']
bdry_labels = [
f'source-side boundary on $L_{{1}}$ (= $\\{{1, 3, 5\\}}$)',
f'inner boundary A on $L_{{2}}$',
f'inner boundary B on $L_{{2}}$',
]
for i, cyc in enumerate(bdry_cycles):
for e in bdry_edges:
if e <= cyc:
u, v = list(e)
x1, y1 = pos[u]; x2, y2 = pos[v]
ax.plot([x1, x2], [y1, y2], color=bdry_colors[i],
linewidth=3.5, zorder=3)
# Draw vertices
level_colors = {0: 'black', 1: '#1f77b4', 2: '#d62728'}
for v in G.vertices():
x, y = pos[v]
c = level_colors.get(dist[v], 'gray')
ax.plot(x, y, 'o', color=c, markersize=22, zorder=4)
ax.annotate(f"{v}", (x, y), color='white', ha='center', va='center',
fontsize=11, fontweight='bold', zorder=5)
# Source label
sx, sy = pos[SOURCE]
ax.annotate(f'source $v_0={SOURCE}$\n($\\ell=0$)',
xy=(sx, sy), xytext=(sx, sy - 0.10),
fontsize=10, ha='center', va='top', color='black', zorder=6,
bbox=dict(boxstyle='round,pad=0.25',
facecolor='white', edgecolor='gray', alpha=0.95))
legend_items = [
plt.Line2D([], [], marker='o', color='w', markerfacecolor='black',
markersize=11, label=f'source ($\\ell=0$)'),
plt.Line2D([], [], marker='o', color='w', markerfacecolor='#1f77b4',
markersize=11, label='$\\ell=1$'),
plt.Line2D([], [], marker='o', color='w', markerfacecolor='#d62728',
markersize=11, label='$\\ell=2$'),
patches.Patch(facecolor='#fff3cd', edgecolor='#e8d8a8',
label=f'$R_{{C^\\prime}}$ (depth-{DEPTH} component, '
f'{len(depth1_faces)} triangles)'),
]
for i, lbl in enumerate(bdry_labels):
legend_items.append(plt.Line2D([], [], color=bdry_colors[i],
linewidth=3.5, label=lbl))
ax.legend(handles=legend_items, loc='upper right', fontsize=9,
framealpha=0.95)
ax.set_title(
f'(R2) violation: depth-1 component with 3 boundary cycles\n'
f'planar embedding of n=10 maximal planar graph, '
f'source $v_0={SOURCE}$ at centre',
fontsize=12)
ax.set_aspect('equal')
ax.axis('off')
pad = 0.08
ax.set_xlim(min(p[0] for p in pos.values()) - pad,
max(p[0] for p in pos.values()) + pad)
ax.set_ylim(min(p[1] for p in pos.values()) - pad,
max(p[1] for p in pos.values()) + pad)
out = os.path.join(HERE, 'r2_violation_v2.png')
plt.savefig(out, dpi=160, bbox_inches='tight')
plt.close()
print(f'wrote {out}')
if __name__ == '__main__':
main()
Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

@@ -0,0 +1,154 @@
"""Schematic illustration of an (R2)-violating tire component.
A connected depth-d face component R_{C'} that has THREE boundary
components: one toward the source side (level d cycle), and two toward
the depth->d side (two distinct deeper lobes each enclosed by R_{C'}).
This is the topological "pair of pants" / trinion: a planar 2-manifold
with three boundary curves, Euler characteristic -1.
"""
import math
import os
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.patches import Circle, Polygon, FancyArrowPatch
def draw_r2_violation(filename):
fig, ax = plt.subplots(figsize=(8.5, 7))
# Outer boundary of R_{C'} -- a large ellipse-like blob
outer_r = 3.2
# Three holes
# Source hole on the left
source_center = (-1.6, 0.2)
source_r = 0.7
# Two deeper lobes on the right
lobe1_center = (1.3, 1.1)
lobe1_r = 0.55
lobe2_center = (1.3, -1.1)
lobe2_r = 0.55
# Draw the filled R_{C'} region: outer ellipse minus three discs.
# Using a Path with the outer circle and three reversed inner circles.
from matplotlib.path import Path
import numpy as np
def circle_verts(cx, cy, r, n=80, reverse=False):
ts = np.linspace(0, 2 * np.pi, n, endpoint=False)
if reverse:
ts = ts[::-1]
return [(cx + r * np.cos(t), cy + r * np.sin(t)) for t in ts]
outer_verts = circle_verts(0, 0, outer_r, n=120)
# Make outer ellipse instead of circle
outer_verts = [(1.4 * x, y) for (x, y) in outer_verts]
inner1 = circle_verts(*source_center, source_r, n=60, reverse=True)
inner2 = circle_verts(*lobe1_center, lobe1_r, n=60, reverse=True)
inner3 = circle_verts(*lobe2_center, lobe2_r, n=60, reverse=True)
verts = (outer_verts + [outer_verts[0]]
+ inner1 + [inner1[0]]
+ inner2 + [inner2[0]]
+ inner3 + [inner3[0]])
codes = ([Path.MOVETO] + [Path.LINETO] * (len(outer_verts) - 1) + [Path.CLOSEPOLY]
+ [Path.MOVETO] + [Path.LINETO] * (len(inner1) - 1) + [Path.CLOSEPOLY]
+ [Path.MOVETO] + [Path.LINETO] * (len(inner2) - 1) + [Path.CLOSEPOLY]
+ [Path.MOVETO] + [Path.LINETO] * (len(inner3) - 1) + [Path.CLOSEPOLY])
path = Path(verts, codes)
patch = patches.PathPatch(path, facecolor='#fff3cd', edgecolor='none',
alpha=0.85, zorder=1)
ax.add_patch(patch)
# Draw the three boundary curves on top
def plot_curve(verts, color, lw=2.5, label_pt=None, label_text=None,
label_offset=(0, 0), label_color=None):
xs = [v[0] for v in verts] + [verts[0][0]]
ys = [v[1] for v in verts] + [verts[0][1]]
ax.plot(xs, ys, color=color, linewidth=lw, zorder=2)
if label_pt is not None and label_text is not None:
ax.annotate(label_text, xy=label_pt,
xytext=(label_pt[0] + label_offset[0],
label_pt[1] + label_offset[1]),
fontsize=11, ha='center', va='center',
color=label_color or color, zorder=3,
bbox=dict(boxstyle='round,pad=0.3',
edgecolor='none', facecolor='white',
alpha=0.85))
plot_curve(outer_verts, '#1f77b4', lw=2.8)
plot_curve(inner1, '#d62728', lw=2.5)
plot_curve(inner2, '#d62728', lw=2.5)
plot_curve(inner3, '#d62728', lw=2.5)
# Source vertex marker in the source hole
sx, sy = source_center
ax.plot(sx, sy, 'o', color='black', markersize=12, zorder=4)
ax.annotate(r'$S = \{v_0\}$', xy=(sx, sy),
xytext=(sx, sy - 0.32),
fontsize=11, ha='center', va='top', zorder=4,
bbox=dict(boxstyle='round,pad=0.25',
edgecolor='gray', facecolor='white', alpha=0.95))
# Region labels
ax.annotate(r'$R_{C^\prime}$ (depth-$d$ region)',
xy=(-3.5, 2.6), fontsize=14, ha='left', va='center',
color='#9a7d2a', fontweight='bold')
# Labels for the three boundary cycles
ax.annotate(r'outer boundary' + '\n' + r'on $L_{d}$ (source side)',
xy=(-1.6 + 0.7 + 0.05, 0.2), xytext=(-2.0, -1.6),
fontsize=10, ha='center', color='#d62728',
arrowprops=dict(arrowstyle='->', color='#d62728', lw=1.2))
ax.annotate(r'lobe boundary on $L_{d+1}$',
xy=(1.3 + 0.55 - 0.05, 1.1), xytext=(3.0, 2.4),
fontsize=10, ha='left', color='#d62728',
arrowprops=dict(arrowstyle='->', color='#d62728', lw=1.2))
ax.annotate(r'lobe boundary on $L_{d+1}$',
xy=(1.3 + 0.55 - 0.05, -1.1), xytext=(3.0, -2.4),
fontsize=10, ha='left', color='#d62728',
arrowprops=dict(arrowstyle='->', color='#d62728', lw=1.2))
ax.annotate(r'outer ellipse $=$' + '\n' + r'outer face of $\Pi_G$',
xy=(-3.5 * 1.4 + 0.2, 0), xytext=(-4.8, 0),
fontsize=10, ha='center', color='#1f77b4',
arrowprops=dict(arrowstyle='->', color='#1f77b4', lw=1.2))
# Lobe interior annotations
ax.annotate('depth-$> d$\nlobe A',
xy=lobe1_center, fontsize=10, ha='center', va='center',
color='#444', style='italic')
ax.annotate('depth-$> d$\nlobe B',
xy=lobe2_center, fontsize=10, ha='center', va='center',
color='#444', style='italic')
# Title / caption text
ax.text(0, 3.4,
'(R2) violation: $R_{C^\\prime}$ has 3 boundary components',
fontsize=13, ha='center', fontweight='bold')
ax.text(0, -3.2,
r'$\chi(R_{C^\prime}) = V - E + F = 2 - n_{\mathrm{bdy}} = -1$',
fontsize=11, ha='center', color='#666')
ax.set_xlim(-5.5, 5.5)
ax.set_ylim(-3.6, 3.7)
ax.set_aspect('equal')
ax.axis('off')
plt.savefig(filename, dpi=160, bbox_inches='tight')
plt.close()
if __name__ == '__main__':
here = os.path.dirname(os.path.abspath(__file__))
out = os.path.join(here, 'r2_violation_schematic.png')
draw_r2_violation(out)
print(f'wrote {out}')
Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

+5 -4
View File
@@ -4,18 +4,19 @@
\@writefile{lof}{\contentsline {figure}{\numberline {1}{\ignorespaces Dual depth in a stacked-ring triangulation $G$ with level source $S = \{0\}$. Each $G$ vertex is labelled by its level $\ell $. Each bounded face carries a dual vertex (square, joined by dashed dual edges) coloured by its dual depth $\delta (d_f) = \qopname \relax m{min}_{v \in V(f)} \ell (v)$: the central fan has depth $0$, the inner annulus depth $1$, and the outer annulus depth $2$. The outer face (the level-$3$ triangle) is excluded from the inner dual and carries no dual vertex.}}{2}{}\protected@file@percent }
\newlabel{fig:dual-depth}{{1}{2}}
\newlabel{def:tire-graph}{{1.5}{2}}
\newlabel{rem:tire-counts}{{1.6}{2}}
\citation{bauerfeld-pds}
\@writefile{lof}{\contentsline {figure}{\numberline {2}{\ignorespaces A tire graph with non-degenerate boundaries: outer boundary $B_{\mathrm {out}}$ a $6$-cycle on vertices $0,\dots ,5$ (blue), inner boundary $B_{\mathrm {in}}$ a $4$-cycle on vertices $6,\dots ,9$ (red), inner outerplanar graph $O = B_{\mathrm {in}} \cup \{7\text {--}9\}$ (with one chord, orange), and $E_{\mathrm {ann}}$ (grey) tiling the annulus between $B_{\mathrm {out}}$ and $B_{\mathrm {in}}$ by ten triangular faces.}}{3}{}\protected@file@percent }
\newlabel{fig:tire-example}{{2}{3}}
\newlabel{rem:tire-counts}{{1.6}{3}}
\newlabel{lem:tire-component}{{1.7}{3}}
\newlabel{rem:tire-component-degenerate}{{1.8}{4}}
\citation{bauerfeld-pds}
\citation{bauerfeld-pds}
\bibcite{bauerfeld-pds}{1}
\newlabel{tocindent-1}{0pt}
\newlabel{tocindent0}{12.7778pt}
\newlabel{tocindent1}{17.77782pt}
\newlabel{tocindent2}{0pt}
\newlabel{tocindent3}{0pt}
\newlabel{rem:R1-R2-when}{{1.9}{5}}
\newlabel{rem:tire-component-degenerate}{{1.8}{5}}
\newlabel{rem:R1-when}{{1.9}{5}}
\@writefile{toc}{\contentsline {section}{\tocsection {}{}{References}}{5}{}\protected@file@percent }
\gdef \@abspage@last{5}
+25 -28
View File
@@ -1,4 +1,4 @@
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 25 MAY 2026 15:36
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 25 MAY 2026 16:26
entering extended mode
restricted \write18 enabled.
%&-line parsing enabled.
@@ -201,42 +201,39 @@ Package pdftex.def Info: fig_dual_depth.png used on input line 106.
LaTeX Warning: `h' float specifier changed to `ht'.
[1{/usr/local/texlive/2022/texmf-var/fonts/map/pdftex/updmap/pdftex.map}]
<fig_tire_example.png, id=20, 559.64081pt x 375.804pt>
[2 <./fig_dual_depth.png>]
<fig_tire_example.png, id=24, 559.64081pt x 375.804pt>
File: fig_tire_example.png Graphic file (type png)
<use fig_tire_example.png>
Package pdftex.def Info: fig_tire_example.png used on input line 154.
Package pdftex.def Info: fig_tire_example.png used on input line 158.
(pdftex.def) Requested size: 280.79956pt x 188.56097pt.
LaTeX Warning: `h' float specifier changed to `ht'.
[2 <./fig_dual_depth.png>] [3 <./fig_tire_example.png>] [4] [5] (./paper.aux) )
[3 <./fig_tire_example.png>] [4] [5] (./paper.aux) )
Here is how much of TeX's memory you used:
3007 strings out of 478268
42001 string characters out of 5846347
345166 words of memory out of 5000000
41998 string characters out of 5846347
343166 words of memory out of 5000000
21054 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,625b,289s stack positions out of 10000i,1000n,20000p,200000b,200000s
</usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmbx10.pfb><
/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmcsc10.pfb><
/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmex10.pfb></
usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi10.pfb></u
sr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi5.pfb></usr
/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmmi7.pfb></usr/l
ocal/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr10.pfb></usr/loc
al/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr5.pfb></usr/local/
texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr7.pfb></usr/local/tex
live/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmr8.pfb></usr/local/texliv
e/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy10.pfb></usr/local/texlive
/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy5.pfb></usr/local/texlive/2
022/texmf-dist/fonts/type1/public/amsfonts/cm/cmsy7.pfb></usr/local/texlive/202
2/texmf-dist/fonts/type1/public/amsfonts/cm/cmti10.pfb></usr/local/texlive/2022
/texmf-dist/fonts/type1/public/amsfonts/cm/cmti8.pfb></usr/local/texlive/2022/t
exmf-dist/fonts/type1/public/amsfonts/symbols/msam10.pfb>
Output written on paper.pdf (5 pages, 486337 bytes).
</usr/local/texlive/2022/t
exmf-dist/fonts/type1/public/amsfonts/cm/cmbx10.pfb></usr/local/texlive/2022/te
xmf-dist/fonts/type1/public/amsfonts/cm/cmcsc10.pfb></usr/local/texlive/2022/te
xmf-dist/fonts/type1/public/amsfonts/cm/cmex10.pfb></usr/local/texlive/2022/tex
mf-dist/fonts/type1/public/amsfonts/cm/cmmi10.pfb></usr/local/texlive/2022/texm
f-dist/fonts/type1/public/amsfonts/cm/cmmi5.pfb></usr/local/texlive/2022/texmf-
dist/fonts/type1/public/amsfonts/cm/cmmi7.pfb></usr/local/texlive/2022/texmf-di
st/fonts/type1/public/amsfonts/cm/cmr10.pfb></usr/local/texlive/2022/texmf-dist
/fonts/type1/public/amsfonts/cm/cmr5.pfb></usr/local/texlive/2022/texmf-dist/fo
nts/type1/public/amsfonts/cm/cmr7.pfb></usr/local/texlive/2022/texmf-dist/fonts
/type1/public/amsfonts/cm/cmr8.pfb></usr/local/texlive/2022/texmf-dist/fonts/ty
pe1/public/amsfonts/cm/cmsy10.pfb></usr/local/texlive/2022/texmf-dist/fonts/typ
e1/public/amsfonts/cm/cmsy5.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1
/public/amsfonts/cm/cmsy7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/p
ublic/amsfonts/cm/cmti10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/pu
blic/amsfonts/cm/cmti8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/publ
ic/amsfonts/symbols/msam10.pfb>
Output written on paper.pdf (5 pages, 486084 bytes).
PDF statistics:
105 PDF objects out of 1000 (max. 8388607)
61 compressed objects within 1 object stream
Binary file not shown.
+94 -89
View File
@@ -116,18 +116,23 @@ vertex.}
\begin{definition}[Tire graph]
\label{def:tire-graph}
A \emph{tire graph} consists of a plane graph $T$ together with two
\emph{boundary parts} $B_{\mathrm{out}}, B_{\mathrm{in}} \subseteq T$
and an \emph{inner outerplanar graph} $O \subseteq T$, where each of
$B_{\mathrm{out}}$ and the outer-face boundary $B_{\mathrm{in}}$ of $O$
is either
A \emph{tire graph} consists of a plane graph $T$ together with an
\emph{outer boundary} $B_{\mathrm{out}} \subseteq T$ and an \emph{inner
outerplanar graph} $O \subseteq T$ with $V(B_{\mathrm{out}}) \cap V(O)
= \emptyset$, where
\begin{itemize}
\item a simple cycle of length $\geq 3$, or
\item a single vertex (a \emph{degenerate} boundary),
\item $B_{\mathrm{out}}$ is either a simple cycle of length $\geq 3$
or a single vertex (a \emph{degenerate outer boundary});
\item $O$ is an outerplanar graph; its \emph{inner boundary}
$B_{\mathrm{in}}$ is the closed walk in $O$ that traces the
boundary of $O$'s outer face in the inherited embedding,
which is a simple cycle when $O$ is $2$-connected and a
non-simple closed walk in general (visiting bridges twice and
cut-vertices multiple times); if $|V(O)| = 1$, we say $T$ has
a \emph{degenerate inner boundary}.
\end{itemize}
with at most one of $B_{\mathrm{out}}, B_{\mathrm{in}}$ degenerate, and
$V(B_{\mathrm{out}}) \cap V(O) = \emptyset$. The vertex and edge sets
of $T$ are
At most one of $B_{\mathrm{out}}, B_{\mathrm{in}}$ may be degenerate.
The vertex and edge sets of $T$ are
\[
V(T) = V(B_{\mathrm{out}}) \cup V(O),
\qquad
@@ -137,16 +142,15 @@ where $E_{\mathrm{ann}}$ --- the \emph{annular edges} --- has the
property that, in the plane embedding of $T$, the closed planar region
$R$ bounded externally by $B_{\mathrm{out}}$ and internally by
$B_{\mathrm{in}}$ is partitioned into triangular faces of $T$ whose
union is $R$. The region $R$ is a closed annulus when both
$B_{\mathrm{out}}$ and $B_{\mathrm{in}}$ are cycles, and a closed disk
when exactly one of them is a single vertex.
union is $R$.
We call $B_{\mathrm{out}}$ the \emph{outer boundary}, $O$ the
\emph{inner outerplanar graph}, and $B_{\mathrm{in}}$ the \emph{inner
boundary} of $T$. A tire graph in which $B_{\mathrm{out}}$
(respectively $B_{\mathrm{in}}$) is a single vertex is said to have a
\emph{degenerate outer (respectively inner) boundary}; in either case
$T$ is a triangulated closed disk with that vertex as apex.
When $B_{\mathrm{out}}$ is a simple cycle and $O$ is $2$-connected,
$R$ is a closed annulus. More generally, $R$ is a planar
$2$-manifold with boundary whose inner boundary may be a closed walk
rather than a simple cycle, accommodating outerplanar inner graphs
with bridges, cut-vertices, or multiple connected components. When
either boundary is degenerate, $R$ is a closed disk with that vertex
as apex.
\end{definition}
\begin{figure}[h]
@@ -194,16 +198,17 @@ Assume:
equivalently, at every $v \in V_{C'}$ the faces of $F_{C'}$
incident to $v$ form a single contiguous arc in the rotation
around $v$ in $\Pi_G$.
\item[\emph{(R2)}] $R_{C'}$ has at most two boundary components.
\end{itemize}
Then $C$, with the inherited embedding, is a tire graph in the sense of
Definition~\ref{def:tire-graph}. Its outer boundary
$B_{\mathrm{out}}$ is the side of $R_{C'}$ closer to $S$ in $\Pi_G$,
namely the level-$d$ subgraph $G[V_{C'} \cap L_d]$; its inner boundary
$B_{\mathrm{in}}$ is the side farther from $S$, namely the
level-$(d+1)$ subgraph $G[V_{C'} \cap L_{d+1}]$; and the triangular
faces of $C$ inside the closed boundary region are exactly the faces of
$G$ in $F_{C'}$.
namely the level-$d$ subgraph $G[V_{C'} \cap L_d]$ (a simple cycle or
single vertex); its inner outerplanar graph is $O = G[V_{C'} \cap
L_{d+1}]$, and its inner boundary $B_{\mathrm{in}}$ is the outer-face
boundary closed walk of $O$ in the inherited embedding (a simple cycle
when $O$ is $2$-connected, a non-simple closed walk in general). The
triangular faces of $C$ inside the closed boundary region are exactly
the faces of $G$ in $F_{C'}$.
\end{lemma}
\begin{proof}
@@ -246,44 +251,46 @@ incident to it (by R1, see below), both of the same type, so its
level is fixed. Therefore each boundary component of $\partial R_{C'}$
is monochromatic in level.
\emph{Boundary components are simple cycles.} By hypothesis (R1),
$R_{C'}$ is a $2$-manifold with boundary, so locally at any boundary
point $p$ the region $R_{C'}$ is homeomorphic to a half-disk and the
link of $p$ in $\partial R_{C'}$ is an arc with two endpoints. In
particular, at every boundary vertex $v$ exactly two boundary edges
are incident, and the boundary walk traverses $v$ exactly once. Each
boundary component is therefore a simple closed walk in $G$ --- a
simple cycle, possibly degenerating to a single vertex if $v$ has no
incident boundary edges (which happens precisely at the BFS endpoints
$d = 0$ with $S = \{v_0\}$, or where an entire level set $V_{C'} \cap
L_{d+1}$ is empty).
\emph{Boundary structure.} By hypothesis (R1), $R_{C'}$ is a
$2$-manifold with boundary, so locally at any boundary point $p$ the
region $R_{C'}$ is homeomorphic to a half-disk; the boundary
$\partial R_{C'}$ is therefore a disjoint union of simple closed
curves in $|\Pi_G|$. Each such curve traces a closed walk in $G$
visiting each of its vertices exactly once (a simple cycle), and the
monochromaticity above forces the entire curve to lie in either
$L_d$ or $L_{d+1}$.
\emph{Topological type.} $R_{C'}$ is a connected, compact, planar
$2$-manifold with boundary; planarity gives orientability and genus
zero, so by the classification of surfaces $R_{C'}$ is homeomorphic
to a closed disk with $n - 1$ open disks removed, where $n \geq 1$ is
the number of boundary components. Hypothesis (R2) gives $n \leq 2$,
so $R_{C'}$ is either a closed disk ($n = 1$) or a closed annulus
($n = 2$).
\emph{Outer boundary.} Because $S$ lies on the outer face of $\Pi_G$,
the boundary curve(s) of $R_{C'}$ on the $L_d$ side are closer to $S$
in the embedding. In the inherited embedding of $C$, the unique
unbounded face is the merged region containing the rest of $\Pi_G$
outside $R_{C'}$ on the $S$ side, so its boundary --- a simple cycle
on $L_d$ (or a single vertex when $V_{C'} \cap L_d = \{v_0\}$, the
$d = 0$ case) --- serves as $B_{\mathrm{out}}$. We set
$B_{\mathrm{out}} := G[V_{C'} \cap L_d]$ if this is a cycle, and
the single vertex $\{v_0\}$ in the degenerate case.
\emph{Tire structure.} Because $S$ lies on the outer face of $\Pi_G$,
the level-$d$ vertices are closer to $S$ in $\Pi_G$ than the
level-$(d+1)$ vertices; in either the annulus or disk case the
boundary cycle on the $L_d$ side is the boundary of $R_{C'}$ facing
$S$ (the ``outer'' boundary), and the $L_{d+1}$ side is the boundary
facing the interior (the ``inner'' boundary). This identifies
$B_{\mathrm{out}} = G[V_{C'} \cap L_d]$ and $B_{\mathrm{in}} =
G[V_{C'} \cap L_{d+1}]$. In the disk case ($n = 1$) one of the two
level sets is a single vertex (the BFS endpoint at $d = 0$ with
$S = \{v_0\}$, or symmetrically at $d = D_{\max}$ where the inner
side collapses to a deepest vertex), giving the degenerate-boundary
case of Definition~\ref{def:tire-graph}.
\emph{Inner outerplanar graph.} By Lemma~2.6 of \cite{bauerfeld-pds},
$G[V_{C'} \cap L_{d+1}]$ is outerplanar. We set $O :=
G[V_{C'} \cap L_{d+1}]$. The boundary curve(s) of $R_{C'}$ on the
$L_{d+1}$ side are exactly the boundary of $O$'s outer face in the
inherited embedding; this outer-face boundary is a single closed walk
that traces around $O$ from the outside, traversing any bridge edge
twice and visiting cut-vertices multiple times. This walk is the
inner boundary $B_{\mathrm{in}}$. No further restriction on $O$'s
internal structure is needed: when $R_{C'}$ has more than two
boundary components in the surface-classification sense (i.e.\
several disjoint simple cycles on $L_{d+1}$), these correspond
precisely to the multiple connected components or bridge crossings of
$O$, and the outer-face boundary closed walk of $O$ captures them
collectively.
The triangular faces inside the closed boundary region of $C$ are by
construction the depth-$d$ faces in $F_{C'}$, and the edges of $C$ are
$E(B_{\mathrm{out}}) \cup E(O) \cup E_{\mathrm{ann}}$ where
$E_{\mathrm{ann}}$ are the edges of $G$ between $V_{C'} \cap L_d$ and
$V_{C'} \cap L_{d+1}$ that bound a face of $F_{C'}$.
\emph{Tire structure.} The triangular faces of $C$ inside the closed
boundary region are by construction the depth-$d$ faces in $F_{C'}$,
and the edges of $C$ are $E(B_{\mathrm{out}}) \cup E(O) \cup
E_{\mathrm{ann}}$ where $E_{\mathrm{ann}}$ are the edges of $G$
between $V_{C'} \cap L_d$ and $V_{C'} \cap L_{d+1}$ that bound a face
of $F_{C'}$.
\end{proof}
\begin{remark}
@@ -299,39 +306,37 @@ the level-$D_{\max}$ cycle as the outer boundary.
\end{remark}
\begin{remark}
\label{rem:R1-R2-when}
The two hypotheses of Lemma~\ref{lem:tire-component} hold in many
natural settings but can fail in general:
\emph{(R1) and the pinch obstruction.} Hypothesis (R1) fails at a
\emph{pinch vertex} $v \in V_{C'}$ when the faces of $F_{C'}$ incident
to $v$ split into two or more disjoint arcs of the rotation around $v$
in $\Pi_G$. Such a $v$ has at least four neighbours $w_i, w_{i+1},
w_j, w_{j+1}$ (with $i + 1 < j$) in cyclic order such that the faces
\label{rem:R1-when}
Hypothesis (R1) of Lemma~\ref{lem:tire-component} holds in many
natural settings but can fail. (R1) fails at a \emph{pinch vertex}
$v \in V_{C'}$ when the faces of $F_{C'}$ incident to $v$ split into
two or more disjoint arcs of the rotation around $v$ in $\Pi_G$.
Such a $v$ has at least four neighbours $w_i, w_{i+1}, w_j, w_{j+1}$
(with $i + 1 < j$) in cyclic order such that the faces
$\{v, w_i, w_{i+1}\}$ and $\{v, w_j, w_{j+1}\}$ are both depth-$d$
(both endpoints at level $\geq d$) while at least one face in each of
the rotation gaps between them carries depth $\neq d$. Concretely,
this occurs precisely when the cyclic level sequence
$\ell(w_1), \ldots, \ell(w_{\deg v})$ enters and leaves $\{d, d+1\}$
more than once. Whenever such a $v$ exists and the two arcs are
joined to a common component of $G'_d$ by some \emph{other} path of
depth-$d$ faces (not through $v$), the resulting $R_{C'}$ is a wedge
of two manifold regions at $v$, violating (R1).
while at least one face in each of the rotation gaps between them
carries depth $\neq d$. Concretely, this occurs precisely when the
cyclic level sequence $\ell(w_1), \ldots, \ell(w_{\deg v})$ enters and
leaves $\{d, d+1\}$ more than once. Whenever such a $v$ exists and
the two arcs are joined to a common component of $G'_d$ by some
\emph{other} path of depth-$d$ faces (not through $v$), the resulting
$R_{C'}$ is a wedge of two manifold regions at $v$, violating (R1).
\emph{(R2) and the multi-hole obstruction.} Hypothesis (R2) fails
when the depth-$d$ region $R_{C'}$ encloses two or more disjoint
depth-$> d$ sub-regions. In a BFS the depth-$< d$ region (the BFS
ball of radius $d - 1$) is connected, so at most one boundary
component of $R_{C'}$ can lie on the source side; (R2) is therefore
equivalent to ``the closure of the depth-$> d$ region adjacent to
$R_{C'}$ has at most one connected component.'' Multi-hole topology
arises when several disjoint depth-$> d$ ``lobes'' of $G$ sit inside
the same depth-$d$ component.
Multi-hole topology (where $R_{C'}$ encloses several disjoint
depth-$>d$ sub-regions) does \emph{not} require an additional
hypothesis: the inner outerplanar graph $O = G[V_{C'} \cap L_{d+1}]$
captures the multi-hole structure as a disconnected (or
non-$2$-connected) outerplanar graph, and its outer-face boundary
closed walk serves as $B_{\mathrm{in}}$. In particular, two disjoint
$L_{d+1}$ triangles inside $R_{C'}$ joined by a bridge edge in $O$
give a $B_{\mathrm{in}}$ that traverses the bridge twice and visits
the bridge endpoints twice each --- not a simple cycle, but a
well-defined closed walk on $V(O)$.
In the special case $d = 0$ with single-vertex source $S = \{v_0\}$
both hypotheses hold automatically: $R_{C'}$ is the star of $v_0$,
a topological closed disk with one boundary cycle (the link of $v_0$),
giving a tire graph with degenerate inner boundary $\{v_0\}$.
(R1) holds automatically: $R_{C'}$ is the star of $v_0$, a topological
closed disk with one boundary cycle (the link of $v_0$), giving a
tire graph with degenerate outer boundary $\{v_0\}$.
\end{remark}
\begin{thebibliography}{9}