Add 2+2 color-split outerplanar survey and decomposition figure
Sanity check for the nested-outerplanar-shells construction: every maximal planar graph through n=11 (1249 triangulations) admits a proper 4-coloring whose colors split into two complementary pairs, each inducing an outerplanar even-cycle (bipartite) subgraph. Disconnected halves are allowed. The odd-bipyramid "failures" of the earlier all-six-pairs test decompose correctly under the right 2+2 split. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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}")
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 255 KiB |
@@ -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()
|
||||
Reference in New Issue
Block a user