diff --git a/papers/coloring_nested_tire_graphs/experiments/cut_depth_label.py b/papers/coloring_nested_tire_graphs/experiments/cut_depth_label.py index 500036a..98289bd 100644 --- a/papers/coloring_nested_tire_graphs/experiments/cut_depth_label.py +++ b/papers/coloring_nested_tire_graphs/experiments/cut_depth_label.py @@ -130,37 +130,52 @@ def find_six_edge_cut(G, prefer_balanced=True, prefer_matching=True): return unique[0] -def apply_procedure(G, S, cut, side_label='0'): +def apply_procedure(G, S, cut, base_pos, side_label='0', + pendant_start_id=None): """Build G'_i from G[S] plus pendant edges at degree-2 vertices. - Per the user's procedure: - a. V = vertices of degree 2 in the induced subgraph (i.e., - original cubic vertices that have exactly 1 cut edge, - hence degree 3 - 1 = 2 in the induced subgraph). - b. Add 1 pendant edge per v in V. - c. Label pendants depth 0, BFS-propagate. - Returns (graph, pos, edge_depths, deg2_vertices, pendant_map, - high_deg_loss). high_deg_loss tracks vertices with >= 2 cut - edges that don't receive pendants under the strict procedure. - """ + Reuses positions from base_pos (a dict v -> (x, y) for G'); pendant + vertices are placed near their boundary vertex, offset in the + direction of the cut edge's other endpoint.""" induced_edges = [(u, v) for (u, v) in G.edges(labels=False) if u in S and v in S] H = Graph(induced_edges, multiedges=False, loops=False) - # Ensure all vertices of S are present (some may be isolated) for v in S: H.add_vertex(v) - # Compute degree of each vertex in induced subgraph induced_deg = {v: H.degree(v) for v in S} - # V = degree-2 vertices V_deg2 = sorted([v for v in S if induced_deg[v] == 2]) high_loss = sorted([v for v in S if induced_deg[v] < 2]) pendant_edges = [] - next_pendant_id = (max(G.vertices()) + 1) + if pendant_start_id is None: + pendant_start_id = max(G.vertices()) + 1 + next_pendant_id = pendant_start_id pendant_to_boundary = {} + # Position pendants: for each boundary vertex v in V_deg2, find + # its cut-edge neighbor across the cut and offset the pendant in + # that direction. + cut_neighbor = {} + for (u, w) in cut: + if u in S and w not in S: + cut_neighbor.setdefault(u, []).append(w) + if w in S and u not in S: + cut_neighbor.setdefault(w, []).append(u) + pos = dict(base_pos) for v in V_deg2: H.add_edge(v, next_pendant_id) pendant_edges.append((min(v, next_pendant_id), max(v, next_pendant_id))) pendant_to_boundary[next_pendant_id] = v + # Position: pendant goes in direction of cut neighbor + nbrs = cut_neighbor.get(v, []) + if nbrs: + nx_, ny_ = base_pos[nbrs[0]] + vx, vy = base_pos[v] + dx, dy = nx_ - vx, ny_ - vy + norm = (dx * dx + dy * dy) ** 0.5 or 1.0 + pos[next_pendant_id] = (vx + 0.35 * dx / norm, + vy + 0.35 * dy / norm) + else: + vx, vy = base_pos[v] + pos[next_pendant_id] = (vx + 0.3, vy) next_pendant_id += 1 # BFS-label edges by depth edge_depth = {} @@ -178,26 +193,24 @@ def apply_procedure(G, S, cut, side_label='0'): if e_nb not in edge_depth: edge_depth[e_nb] = d + 1 queue.append((e_nb, d + 1)) - # Layout: use Sage's planar embedding - pos = H.layout(layout='planar') if H.is_planar() else H.layout() return H, pos, edge_depth, V_deg2, pendant_to_boundary, high_loss def draw_labeled_graph(ax, H, pos, edge_depth, boundary_vertices, pendant_to_boundary, title): + H_verts = set(H.vertices()) max_depth = max(edge_depth.values()) if edge_depth else 0 - cmap = plt.get_cmap('viridis', max_depth + 1) - # Draw edges by depth + cmap = plt.get_cmap('viridis', max_depth + 2) legend_handles = [] for d in range(max_depth + 1): - color = cmap(d / max(max_depth, 1)) + color = cmap((d + 0.5) / (max_depth + 1)) for e, ed in edge_depth.items(): if ed != d: continue u, v = e (x1, y1) = pos[u] (x2, y2) = pos[v] ax.plot([x1, x2], [y1, y2], color=color, - linewidth=2.0 + (1.5 if d == 0 else 0), + linewidth=2.3, linestyle=('--' if d == 0 else '-'), zorder=1) legend_handles.append( @@ -205,21 +218,69 @@ def draw_labeled_graph(ax, H, pos, edge_depth, boundary_vertices, linestyle='--' if d == 0 else '-', label=f'depth {d}') ) - # Draw vertices - for v, (x, y) in pos.items(): + for v in H_verts: + x, y = pos[v] if v in pendant_to_boundary: - ax.plot(x, y, 's', color='#ffaa66', markersize=8, zorder=2, + ax.plot(x, y, 's', color='#ffaa66', markersize=9, zorder=2, markeredgecolor='#aa5500') elif v in boundary_vertices: - ax.plot(x, y, 'o', color='#ff5555', markersize=10, zorder=2, - markeredgecolor='#aa0000') + ax.plot(x, y, 'o', color='#ff5555', markersize=11, zorder=3, + markeredgecolor='#aa0000', markeredgewidth=1.0) else: - ax.plot(x, y, 'o', color='#888', markersize=6, zorder=2) + ax.plot(x, y, 'o', color='#666', markersize=7, zorder=2) ax.set_aspect('equal') ax.axis('off') ax.set_title(title, fontsize=11) ax.legend(handles=legend_handles, loc='upper left', - bbox_to_anchor=(1.02, 1.0), fontsize=8, frameon=False) + bbox_to_anchor=(1.02, 1.0), fontsize=9, frameon=False) + + +def compute_nice_layout(G): + """Compute a nice layout for G. Tries several methods, picks the + best by a heuristic that penalizes long/crossing edges.""" + # First try Sage's planar layout from the planar embedding. + G.is_planar(set_embedding=True) + pos_planar = G.layout(layout='planar') + # Spring layout as fallback / alternative. + try: + pos_spring = G.layout(layout='spring', iterations=200) + except Exception: + pos_spring = None + # Use networkx's planar layout if available. + try: + import networkx as nx + nxg = nx.Graph() + for (u, v) in G.edges(labels=False): + nxg.add_edge(u, v) + pos_nx_planar = nx.planar_layout(nxg) + pos_nx_planar = {k: tuple(v) for k, v in pos_nx_planar.items()} + except Exception: + pos_nx_planar = None + + def edge_length_variance(pos): + if pos is None: + return float('inf') + lengths = [] + for (u, v) in G.edges(labels=False): + x1, y1 = pos[u]; x2, y2 = pos[v] + lengths.append(((x2 - x1)**2 + (y2 - y1)**2) ** 0.5) + if not lengths: + return float('inf') + mean = sum(lengths) / len(lengths) + var = sum((l - mean)**2 for l in lengths) / len(lengths) + return var / (mean * mean + 1e-9) # coefficient of variation^2 + + options = [ + ('sage-planar', pos_planar), + ('sage-spring', pos_spring), + ('nx-planar', pos_nx_planar), + ] + scored = [(edge_length_variance(p), name, p) for name, p in options if p] + scored.sort() + name, _, pos = scored[0][1], scored[0][0], scored[0][2] + print(f'Selected layout: {name} ' + f'(edge-length CV^2 = {scored[0][0]:.4f})') + return pos def main(): @@ -230,12 +291,11 @@ def main(): gs = parse_planar_code(HM_FILE) print(f'Loaded {len(gs)} Holton-McKay graphs') - # Pick the first one G = gs[0] print(f'Graph 0: {G.order()} vertices, {G.size()} edges, ' - f'cubic={all(d == 3 for d in G.degree())}, planar={G.is_planar()}') + f'cubic={all(d == 3 for d in G.degree())}, ' + f'planar={G.is_planar()}') - # Find a 6-edge cut S, cut = find_six_edge_cut(G) if S is None: print('No 6-edge cut found by greedy search.') @@ -243,36 +303,78 @@ def main(): print(f'Found 6-edge cut: |S| = {len(S)}, |cut| = {len(cut)}') print(f'Cut edges: {cut}') + # Compute a single nice layout for G' once + base_pos = compute_nice_layout(G) + S0 = frozenset(S) S1 = frozenset(G.vertices()) - S0 - H0, pos0, ed0, bv0, pmap0, hl0 = apply_procedure(G, S0, cut, '0') - H1, pos1, ed1, bv1, pmap1, hl1 = apply_procedure(G, S1, cut, '1') + H0, pos0, ed0, bv0, pmap0, hl0 = apply_procedure( + G, S0, cut, base_pos, side_label='0', + pendant_start_id=max(G.vertices()) + 1) + H1, pos1, ed1, bv1, pmap1, hl1 = apply_procedure( + G, S1, cut, base_pos, side_label='1', + pendant_start_id=max(G.vertices()) + 1 + len(bv0)) - print(f"G'_0: {H0.order()} vertices ({len(S0)} original + " - f'{len(pmap0)} pendant), {H0.size()} edges') - print(f" V (degree-2 vertices receiving pendants): {len(bv0)} vertices") - print(f" Multi-cut vertices not in V: {hl0}") - print(f' Max depth: {max(ed0.values())}') - print(f"G'_1: {H1.order()} vertices ({len(S1)} original + " - f'{len(pmap1)} pendant), {H1.size()} edges') - print(f" V (degree-2 vertices receiving pendants): {len(bv1)} vertices") - print(f" Multi-cut vertices not in V: {hl1}") - print(f' Max depth: {max(ed1.values())}') + print(f"G'_0: {H0.order()} vertices, {H0.size()} edges, " + f"|V|={len(bv0)}, max depth={max(ed0.values())}") + print(f"G'_1: {H1.order()} vertices, {H1.size()} edges, " + f"|V|={len(bv1)}, max depth={max(ed1.values())}") - # Draw - fig, (ax0, ax1) = plt.subplots(1, 2, figsize=(16, 8.0)) - draw_labeled_graph(ax0, H0, pos0, ed0, bv0, pmap0, + # Three-panel figure: G' (full), G'_0, G'_1, all with same positions + # Choose figure size based on aspect ratio of the layout. + xs = [p[0] for p in base_pos.values()] + ys = [p[1] for p in base_pos.values()] + xrange = max(xs) - min(xs) + yrange = max(ys) - min(ys) + aspect = xrange / yrange if yrange > 0 else 1.0 + panel_w = 6.5 + panel_h = panel_w / aspect + fig, axes = plt.subplots(3, 1, figsize=(panel_w * 1.4, 3 * panel_h * 1.05)) + + # Panel 0: G' (full) with cut edges highlighted + ax = axes[0] + cut_set = set(frozenset(e) for e in cut) + for (u, v) in G.edges(labels=False): + x1, y1 = base_pos[u]; x2, y2 = base_pos[v] + if frozenset((u, v)) in cut_set: + ax.plot([x1, x2], [y1, y2], color='#d62728', + linewidth=2.5, linestyle='--', zorder=2) + else: + ax.plot([x1, x2], [y1, y2], color='#bbbbbb', + linewidth=1.2, zorder=1) + for v, (x, y) in base_pos.items(): + if v in S0: + ax.plot(x, y, 'o', color='#4c72b0', markersize=8, zorder=3) + else: + ax.plot(x, y, 'o', color='#dd8452', markersize=8, zorder=3) + ax.set_title(f"$G'$ = Holton-McKay #0 with 6-edge cut highlighted\n" + f"Blue = $S$ ($|S|={len(S0)}$); " + f"orange = $V \\setminus S$ ($|V\\setminus S|={len(S1)}$); " + f"red dashed = cut", + fontsize=10) + ax.set_aspect('equal'); ax.axis('off') + + # Panels 1 and 2: G'_0 and G'_1 in the same layout + draw_labeled_graph(axes[1], H0, pos0, ed0, bv0, pmap0, f"$G'_0$ (|S| = {len(S0)}, |V| = {len(bv0)}, " f"max depth = {max(ed0.values())})") - draw_labeled_graph(ax1, H1, pos1, ed1, bv1, pmap1, + draw_labeled_graph(axes[2], H1, pos1, ed1, bv1, pmap1, f"$G'_1$ (|S| = {len(S1)}, |V| = {len(bv1)}, " f"max depth = {max(ed1.values())})") - fig.suptitle("Cut-and-depth-label procedure on Holton-McKay graph #0 " - "(38 vertices, cubic, planar, non-Hamiltonian)\n" - "Dashed orange = pendant edges (depth 0); colored by " - "BFS depth from pendants in line-graph sense", - fontsize=11, y=1.00) + + # Force same axis limits for the three panels (using base_pos) + xs = [p[0] for p in base_pos.values()] + ys = [p[1] for p in base_pos.values()] + xmin, xmax = min(xs) - 0.5, max(xs) + 0.5 + ymin, ymax = min(ys) - 0.5, max(ys) + 0.5 + for ax in axes: + ax.set_xlim(xmin, xmax) + ax.set_ylim(ymin, ymax) + + fig.suptitle("Cut-and-depth-label procedure on Holton-McKay graph #0\n" + "Same vertex positions used across all three panels.", + fontsize=12, y=1.00) plt.tight_layout() plt.savefig(out, dpi=160, bbox_inches='tight') plt.close() diff --git a/papers/coloring_nested_tire_graphs/notes/fig_cut_depth_label.png b/papers/coloring_nested_tire_graphs/notes/fig_cut_depth_label.png index 01a3ca9..1a761a3 100644 Binary files a/papers/coloring_nested_tire_graphs/notes/fig_cut_depth_label.png and b/papers/coloring_nested_tire_graphs/notes/fig_cut_depth_label.png differ