diff --git a/papers/even_level_graph_generators/experiments/draw_split_decomposition.py b/papers/even_level_graph_generators/experiments/draw_split_decomposition.py new file mode 100644 index 0000000..e075036 --- /dev/null +++ b/papers/even_level_graph_generators/experiments/draw_split_decomposition.py @@ -0,0 +1,167 @@ +"""Draw the two former "failures" (n=9 bipyramid, n=10 bipyramid+stacked) the +RIGHT way: a proper 4-colouring whose 4 colours split into two complementary +pairs, each inducing an outerplanar (bipartite => even-cycle) subgraph. + +Top row: the planar drawing with the 4-colouring; edges of the two split +classes drawn in two styles. Bottom row: the two complementary subgraphs shown +separately, each annotated outerplanar (tree / forest / even cycle). + +Renders into this experiments folder. +""" +import os +from itertools import combinations + +import networkx as nx +import matplotlib.pyplot as plt + +T24 = [(0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (0, 7), (0, 8), + (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), + (2, 3), (2, 4), (3, 5), (4, 8), (5, 6), (6, 7), (7, 8)] +T94 = [(0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (0, 7), (0, 8), (0, 9), + (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), + (2, 3), (2, 4), (3, 5), (4, 8), (5, 6), (6, 7), (7, 8), + (7, 9), (8, 9)] + +CMAP = {0: '#444444', 1: '#d62728', 2: '#1f77b4', 3: '#2ca02c'} +CNAME = {0: 'grey', 1: 'red', 2: 'blue', 3: 'green'} +SPLITS = [({0, 1}, {2, 3}), ({0, 2}, {1, 3}), ({0, 3}, {1, 2})] + + +def is_outerplanar(G): + if G.number_of_nodes() <= 3: + return True + H = G.copy() + apex = max(H.nodes()) + 1 + for v in G.nodes(): + H.add_edge(apex, v) + return nx.check_planarity(H)[0] + + +def enumerate_4colorings(G): + nodes = list(G.nodes()) + adj = {v: set(G.neighbors(v)) for v in nodes} + coloring = {} + + def bt(i, mx): + if i == len(nodes): + yield dict(coloring) + return + v = nodes[i] + used = {coloring[w] for w in adj[v] if w in coloring} + for c in range(min(3, mx + 1) + 1): + if c in used: + continue + coloring[v] = c + yield from bt(i + 1, max(mx, c)) + del coloring[v] + + yield from bt(0, -1) + + +def find_good_split(G): + for col in enumerate_4colorings(G): + for A, B in SPLITS: + va = [v for v in G if col[v] in A] + vb = [v for v in G if col[v] in B] + GA, GB = G.subgraph(va), G.subgraph(vb) + if (is_outerplanar(GA) and nx.is_bipartite(GA) + and is_outerplanar(GB) and nx.is_bipartite(GB)): + return col, (A, B) + return None, None + + +def bipyramid_pos(rim_cycle, apexA, apexB): + k = len(rim_cycle) + order = rim_cycle[1:] + [rim_cycle[0]] + pos = {} + for i, v in enumerate(order): + x = 1.7 * (1 - 2 * i / (k - 1)) + y = 1.0 * (x / 1.7) ** 2 + pos[v] = (x, y) + pos[apexA] = (0.0, 0.62) + pos[apexB] = (0.0, -1.7) + return pos + + +def describe(G): + if G.number_of_edges() == 0: + return "edgeless (isolated vertices)" + if nx.is_forest(G): + return "forest (tree, no cycles)" + girth_even = all(len(c) % 2 == 0 for c in nx.cycle_basis(G)) + comps = nx.number_connected_components(G) + tag = "even cycles" if girth_even else "ODD CYCLE!" + return f"outerplanar, {tag}, {comps} component(s)" + + +def draw_case(fig, gs_row, edges, pos, title): + G = nx.Graph(edges) + col, split = find_good_split(G) + A, B = split + node_colors = [CMAP[col[v]] for v in G.nodes()] + ea = [e for e in G.edges() if col[e[0]] in A and col[e[1]] in A] + eb = [e for e in G.edges() if col[e[0]] in B and col[e[1]] in B] + ecross = [e for e in G.edges() if e not in ea and e not in eb] + + # left: whole graph, both classes highlighted + ax = fig.add_subplot(gs_row[0]) + nx.draw_networkx_edges(G, pos, ax=ax, edgelist=ecross, + edge_color='#dddddd', width=1.0) + nx.draw_networkx_edges(G, pos, ax=ax, edgelist=ea, + edge_color='#e8860a', width=3.0) + nx.draw_networkx_edges(G, pos, ax=ax, edgelist=eb, + edge_color='#7b2fbf', width=3.0, style='dashed') + nx.draw_networkx_nodes(G, pos, node_color=node_colors, node_size=720, + edgecolors='black', linewidths=1.4, ax=ax) + nx.draw_networkx_labels(G, pos, ax=ax, font_color='white', + font_weight='bold', font_size=11) + an = "/".join(CNAME[c] for c in sorted(A)) + bn = "/".join(CNAME[c] for c in sorted(B)) + ax.set_title(f"{title}\nsplit [{an}] (orange solid) | " + f"[{bn}] (purple dashed)", fontsize=10) + ax.axis('off'); ax.set_aspect('equal') + + # middle: subgraph A alone + GA = G.subgraph([v for v in G if col[v] in A]) + axA = fig.add_subplot(gs_row[1]) + nx.draw_networkx_edges(GA, pos, ax=axA, edge_color='#e8860a', width=3.0) + nx.draw_networkx_nodes(GA, pos, node_color=[CMAP[col[v]] for v in GA], + node_size=720, edgecolors='black', + linewidths=1.4, ax=axA) + nx.draw_networkx_labels(GA, pos, ax=axA, font_color='white', + font_weight='bold', font_size=11) + axA.set_title(f"[{an}] subgraph\n{describe(GA)}", fontsize=9) + axA.axis('off'); axA.set_aspect('equal') + + # right: subgraph B alone + GB = G.subgraph([v for v in G if col[v] in B]) + axB = fig.add_subplot(gs_row[2]) + nx.draw_networkx_edges(GB, pos, ax=axB, edge_color='#7b2fbf', width=3.0) + nx.draw_networkx_nodes(GB, pos, node_color=[CMAP[col[v]] for v in GB], + node_size=720, edgecolors='black', + linewidths=1.4, ax=axB) + nx.draw_networkx_labels(GB, pos, ax=axB, font_color='white', + font_weight='bold', font_size=11) + axB.set_title(f"[{bn}] subgraph\n{describe(GB)}", fontsize=9) + axB.axis('off'); axB.set_aspect('equal') + + +rim = [2, 3, 5, 6, 7, 8, 4] +pos24 = bipyramid_pos(rim, 0, 1) +pos94 = dict(pos24) +pos94[9] = ((pos24[0][0] + pos24[7][0] + pos24[8][0]) / 3, + (pos24[0][1] + pos24[7][1] + pos24[8][1]) / 3) + +fig = plt.figure(figsize=(15, 10)) +gs = fig.add_gridspec(2, 3) +draw_case(fig, [gs[0, 0], gs[0, 1], gs[0, 2]], T24, pos24, + "n=9 T24: 7-gonal bipyramid") +draw_case(fig, [gs[1, 0], gs[1, 1], gs[1, 2]], T94, pos94, + "n=10 T94: bipyramid + stacked vertex 9") +fig.suptitle("Both decompose: 4-colouring -> 2+2 colour split -> two " + "complementary outerplanar even-cycle subgraphs", fontsize=12) +fig.tight_layout(rect=[0, 0, 1, 0.97]) +out = os.path.join(os.path.dirname(os.path.abspath(__file__)), + 'split_decomposition.png') +fig.savefig(out, dpi=140) +print(f"wrote {out}") diff --git a/papers/even_level_graph_generators/experiments/split_decomposition.png b/papers/even_level_graph_generators/experiments/split_decomposition.png new file mode 100644 index 0000000..c240fe4 Binary files /dev/null and b/papers/even_level_graph_generators/experiments/split_decomposition.png differ diff --git a/papers/even_level_graph_generators/experiments/two_color_split_survey.py b/papers/even_level_graph_generators/experiments/two_color_split_survey.py new file mode 100644 index 0000000..b6b8ef7 --- /dev/null +++ b/papers/even_level_graph_generators/experiments/two_color_split_survey.py @@ -0,0 +1,125 @@ +"""Corrected sanity check for the nested-outerplanar-shells construction. + +The construction splits the FOUR colours into TWO complementary pairs (two +parity classes). Each pair induces a bipartite subgraph; the two subgraphs +partition the vertex set. For a nested-shell decomposition we want BOTH +complementary subgraphs to be outerplanar (bipartite => only even cycles, so +"even cycles" is automatic; outerplanar is the binding condition). + +There are exactly 3 ways to split {0,1,2,3} into two pairs: + {0,1}|{2,3}, {0,2}|{1,3}, {0,3}|{1,2}. + +Criterion (per triangulation): does SOME proper 4-colouring admit SOME split +whose two complementary subgraphs are both outerplanar? + +This is the right test (an earlier version wrongly demanded that all SIX +colour pairs be outerplanar, which odd bipyramids fail on the apex/heavy-rim +pair -- but that pair is never one we'd use). + +Usage: python3 two_color_split_survey.py [n_max] (default 10) +""" +import sys +import os +import time + +import networkx as nx + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, '/Users/didericis/Code/math-research/papers/' + 'level_resolutions_of_maximal_planar_graphs/experiments') +from triangulation_gen import enumerate_all_triangulations + +# the 3 ways to split 4 colours into two complementary pairs +SPLITS = [(({0, 1}), ({2, 3})), + (({0, 2}), ({1, 3})), + (({0, 3}), ({1, 2}))] + + +def is_outerplanar(G): + if G.number_of_nodes() <= 3: + return True + H = G.copy() + apex = max(H.nodes()) + 1 + for v in G.nodes(): + H.add_edge(apex, v) + return nx.check_planarity(H)[0] + + +def enumerate_4colorings(G): + nodes = list(G.nodes()) + adj = {v: set(G.neighbors(v)) for v in nodes} + coloring = {} + + def bt(i, mx): + if i == len(nodes): + yield dict(coloring) + return + v = nodes[i] + used = {coloring[w] for w in adj[v] if w in coloring} + for c in range(min(3, mx + 1) + 1): + if c in used: + continue + coloring[v] = c + yield from bt(i + 1, max(mx, c)) + del coloring[v] + + yield from bt(0, -1) + + +def outerplanar_even(G): + """Outerplanar AND every cycle even (i.e. bipartite). Disconnected ok. + For a two-colour subgraph of a proper colouring bipartiteness is automatic, + but we verify it explicitly so the criterion is honestly enforced.""" + return is_outerplanar(G) and nx.is_bipartite(G) + + +def good_split(G, col): + """Return the first (sideA, sideB) split whose two complementary + subgraphs are both outerplanar-with-even-cycles, or None. + Disconnected subgraphs are allowed.""" + for A, B in SPLITS: + va = [v for v in G if col[v] in A] + vb = [v for v in G if col[v] in B] + if outerplanar_even(G.subgraph(va)) and outerplanar_even(G.subgraph(vb)): + return (A, B) + return None + + +def has_good_coloring(G): + """True iff SOME proper 4-colouring admits a valid 2+2 split. + Early-exits on the first good colouring.""" + for col in enumerate_4colorings(G): + if good_split(G, col) is not None: + return True + return False + + +def survey_n(n): + t0 = time.time() + tris = enumerate_all_triangulations(n) + n_good = 0 + bad = [] + for gi, G in enumerate(tris): + ok = has_good_coloring(G) + if ok: + n_good += 1 + else: + bad.append((gi, G)) + return n, len(tris), n_good, bad, time.time() - t0 + + +def main(): + n_max = int(sys.argv[1]) if len(sys.argv) > 1 else 10 + print(f"{'n':>3} {'tris':>6} {'has 2+2 split':>14} {'time(s)':>8}") + print("-" * 38) + for n in range(6, n_max + 1): + n, ntri, ngood, bad, dt = survey_n(n) + flag = "" if ngood == ntri else " <-- GAP" + print(f"{n:>3} {ntri:>6} {ngood:>9}/{ntri:<4} {dt:>8.1f}{flag}") + for gi, G in bad[:5]: + edges = sorted(tuple(sorted(e)) for e in G.edges()) + print(f" no split: T{gi} edges={edges}") + + +if __name__ == "__main__": + main()