diff --git a/papers/coloring_nested_tire_graphs/experiments/check_r2.py b/papers/coloring_nested_tire_graphs/experiments/check_r2.py new file mode 100644 index 0000000..07c2278 --- /dev/null +++ b/papers/coloring_nested_tire_graphs/experiments/check_r2.py @@ -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() diff --git a/papers/coloring_nested_tire_graphs/experiments/draw_r2_violation_example.py b/papers/coloring_nested_tire_graphs/experiments/draw_r2_violation_example.py new file mode 100644 index 0000000..139c7de --- /dev/null +++ b/papers/coloring_nested_tire_graphs/experiments/draw_r2_violation_example.py @@ -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() diff --git a/papers/coloring_nested_tire_graphs/experiments/draw_r2_violation_v2.py b/papers/coloring_nested_tire_graphs/experiments/draw_r2_violation_v2.py new file mode 100644 index 0000000..40c4e24 --- /dev/null +++ b/papers/coloring_nested_tire_graphs/experiments/draw_r2_violation_v2.py @@ -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() diff --git a/papers/coloring_nested_tire_graphs/experiments/r2_violation_real_example.png b/papers/coloring_nested_tire_graphs/experiments/r2_violation_real_example.png new file mode 100644 index 0000000..eee9e01 Binary files /dev/null and b/papers/coloring_nested_tire_graphs/experiments/r2_violation_real_example.png differ diff --git a/papers/coloring_nested_tire_graphs/experiments/r2_violation_schematic.png b/papers/coloring_nested_tire_graphs/experiments/r2_violation_schematic.png new file mode 100644 index 0000000..5bdac37 Binary files /dev/null and b/papers/coloring_nested_tire_graphs/experiments/r2_violation_schematic.png differ diff --git a/papers/coloring_nested_tire_graphs/experiments/r2_violation_schematic.py b/papers/coloring_nested_tire_graphs/experiments/r2_violation_schematic.py new file mode 100644 index 0000000..9f6b755 --- /dev/null +++ b/papers/coloring_nested_tire_graphs/experiments/r2_violation_schematic.py @@ -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}') diff --git a/papers/coloring_nested_tire_graphs/experiments/r2_violation_v2.png b/papers/coloring_nested_tire_graphs/experiments/r2_violation_v2.png new file mode 100644 index 0000000..7f16368 Binary files /dev/null and b/papers/coloring_nested_tire_graphs/experiments/r2_violation_v2.png differ diff --git a/papers/coloring_nested_tire_graphs/paper.aux b/papers/coloring_nested_tire_graphs/paper.aux index 1a99d60..5f89a21 100644 --- a/papers/coloring_nested_tire_graphs/paper.aux +++ b/papers/coloring_nested_tire_graphs/paper.aux @@ -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} diff --git a/papers/coloring_nested_tire_graphs/paper.log b/papers/coloring_nested_tire_graphs/paper.log index d74af80..95b8b1e 100644 --- a/papers/coloring_nested_tire_graphs/paper.log +++ b/papers/coloring_nested_tire_graphs/paper.log @@ -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}] - +[2 <./fig_dual_depth.png>] + File: fig_tire_example.png Graphic file (type 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/cmcsc10.pfb>< -/usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/cm/cmex10.pfb> -Output written on paper.pdf (5 pages, 486337 bytes). + +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 diff --git a/papers/coloring_nested_tire_graphs/paper.pdf b/papers/coloring_nested_tire_graphs/paper.pdf index 8f8f3d7..3a5764f 100644 Binary files a/papers/coloring_nested_tire_graphs/paper.pdf and b/papers/coloring_nested_tire_graphs/paper.pdf differ diff --git a/papers/coloring_nested_tire_graphs/paper.tex b/papers/coloring_nested_tire_graphs/paper.tex index 7c89aa1..1ee12d5 100644 --- a/papers/coloring_nested_tire_graphs/paper.tex +++ b/papers/coloring_nested_tire_graphs/paper.tex @@ -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}