Prove intertwining-tree ⟺ Hamiltonian-dual; test the 6 Holton-McKay duals
- Add Theorem: maximal planar G is an intertwining tree iff its dual G* is Hamiltonian (Tait-style Jordan-curve argument). Consequence: smallest non-intertwining-tree triangulations are the 6 duals of the 38-vertex Holton-McKay graphs, at n=21. - Load the 6 graphs from McKay's authoritative planar_code file (nonham38m4.pc), verified: 38 vertices, cubic, planar, non-Hamiltonian. - All 6 duals confirmed not intertwining trees (exhaustive 2^20 check). - 2 of 6 duals are themselves Even Level Graphs (sources 9, 10), hence derived level graphs -- first cases where the derived disjunct does work the intertwining-tree disjunct cannot. - Remaining 4: bounded E/O-orbit search inconclusive; status open. This is the first genuinely undetermined instance of the conjecture. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
"""Parse plantri -a output for 5-connected triangulations at n=21 and
|
||||
check each for the intertwining-tree property (equivalently, whether its
|
||||
dual is Hamiltonian)."""
|
||||
import subprocess
|
||||
import networkx as nx
|
||||
import time
|
||||
|
||||
PLANTRI = '/Users/didericis/Code/math-research/plantri/plantri'
|
||||
|
||||
|
||||
def parse_plantri_a(line):
|
||||
"""Parse one line of plantri -a output into a networkx graph.
|
||||
Format: 'n adj0,adj1,...' where adj_i is a string of letters."""
|
||||
parts = line.strip().split(' ', 1)
|
||||
n = int(parts[0])
|
||||
adj_lists = parts[1].split(',')
|
||||
G = nx.Graph()
|
||||
G.add_nodes_from(range(n))
|
||||
for i, adj in enumerate(adj_lists):
|
||||
for ch in adj:
|
||||
j = ord(ch) - ord('a')
|
||||
G.add_edge(i, j)
|
||||
return G
|
||||
|
||||
|
||||
def is_intertwining_tree(G):
|
||||
"""Search all 2-partitions for one giving two induced trees."""
|
||||
nodes = list(G.nodes())
|
||||
n = len(nodes)
|
||||
for mask in range(1, 2 ** (n - 1)):
|
||||
A = [nodes[0]] + [nodes[i + 1] for i in range(n - 1) if (mask >> i) & 1]
|
||||
B = [nodes[i + 1] for i in range(n - 1) if not ((mask >> i) & 1)]
|
||||
if not B:
|
||||
continue
|
||||
if nx.is_tree(G.subgraph(A)) and nx.is_tree(G.subgraph(B)):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
out = subprocess.run([PLANTRI, '-c5', '-a', '21'],
|
||||
capture_output=True, text=True)
|
||||
lines = [l for l in out.stdout.splitlines() if l.strip()]
|
||||
print(f'{len(lines)} triangulations to check')
|
||||
|
||||
t0 = time.time()
|
||||
n_inter = 0
|
||||
non_inter = []
|
||||
for idx, line in enumerate(lines):
|
||||
G = parse_plantri_a(line)
|
||||
if G.number_of_nodes() != 21 or G.number_of_edges() != 3 * 21 - 6:
|
||||
print(f' line {idx}: parse issue, n={G.number_of_nodes()}, '
|
||||
f'm={G.number_of_edges()}')
|
||||
continue
|
||||
if is_intertwining_tree(G):
|
||||
n_inter += 1
|
||||
else:
|
||||
non_inter.append(idx)
|
||||
if (idx + 1) % 50 == 0:
|
||||
print(f' ...{idx+1}/{len(lines)} done '
|
||||
f'({time.time()-t0:.1f}s), {n_inter} intertwining so far')
|
||||
print(f'\nResult: {n_inter}/{len(lines)} are intertwining trees')
|
||||
print(f'Non-intertwining-tree (dual non-Hamiltonian): {non_inter}')
|
||||
print(f'elapsed: {time.time()-t0:.1f}s')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Parse McKay's planar_code file of the 6 non-Hamiltonian 38-vertex
|
||||
cubic planar graphs, verify them, and build their duals (21-vertex
|
||||
triangulations)."""
|
||||
import networkx as nx
|
||||
|
||||
|
||||
def parse_planar_code(path):
|
||||
"""Parse a >>planar_code<< file. Returns list of (G, embedding-order)."""
|
||||
with open(path, 'rb') as f:
|
||||
data = f.read()
|
||||
header = b'>>planar_code<<'
|
||||
assert data.startswith(header), data[:20]
|
||||
pos = len(header)
|
||||
graphs = []
|
||||
while pos < len(data):
|
||||
n = data[pos]
|
||||
pos += 1
|
||||
adj = {i: [] for i in range(n)}
|
||||
for v in range(n):
|
||||
while True:
|
||||
w = data[pos]
|
||||
pos += 1
|
||||
if w == 0:
|
||||
break
|
||||
adj[v].append(w - 1) # 1-indexed -> 0-indexed
|
||||
G = nx.Graph()
|
||||
G.add_nodes_from(range(n))
|
||||
for v in range(n):
|
||||
for w in adj[v]:
|
||||
G.add_edge(v, w)
|
||||
graphs.append((G, adj))
|
||||
return graphs
|
||||
|
||||
|
||||
def is_hamiltonian(G, time_limit_nodes=10_000_000):
|
||||
"""Backtracking Hamiltonicity check (good for cubic graphs)."""
|
||||
nodes = list(G.nodes())
|
||||
n = len(nodes)
|
||||
adj = {v: list(G.neighbors(v)) for v in nodes}
|
||||
start = nodes[0]
|
||||
visited = {start}
|
||||
count = [0]
|
||||
|
||||
def bt(v, depth):
|
||||
count[0] += 1
|
||||
if count[0] > time_limit_nodes:
|
||||
raise TimeoutError
|
||||
if depth == n:
|
||||
return start in adj[v]
|
||||
for w in adj[v]:
|
||||
if w not in visited:
|
||||
visited.add(w)
|
||||
if bt(w, depth + 1):
|
||||
return True
|
||||
visited.discard(w)
|
||||
return False
|
||||
|
||||
return bt(start, 1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
graphs = parse_planar_code('/tmp/nonham38m4.pc')
|
||||
print(f'Parsed {len(graphs)} graphs from McKay planar_code file')
|
||||
for i, (G, adj) in enumerate(graphs):
|
||||
n = G.number_of_nodes()
|
||||
m = G.number_of_edges()
|
||||
degs = sorted(set(dict(G.degree()).values()))
|
||||
planar = nx.check_planarity(G)[0]
|
||||
try:
|
||||
ham = is_hamiltonian(G)
|
||||
except TimeoutError:
|
||||
ham = 'timeout'
|
||||
print(f' graph {i}: n={n}, m={m}, degrees={degs}, '
|
||||
f'planar={planar}, hamiltonian={ham}')
|
||||
Binary file not shown.
@@ -0,0 +1,61 @@
|
||||
"""For each of the 6 Holton-McKay graphs (38-vertex non-Hamiltonian cubic
|
||||
planar), build the dual (21-vertex triangulation), confirm it is NOT an
|
||||
intertwining tree, then test whether it is a valid derived level graph."""
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, '/Users/didericis/Code/math-research/papers/'
|
||||
'level_resolutions_of_maximal_planar_graphs/experiments')
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import networkx as nx
|
||||
import time
|
||||
from load_holton_mckay import parse_planar_code
|
||||
from tutte_dual_treecolor import dual_triangulation
|
||||
|
||||
|
||||
def is_intertwining_tree(G, time_limit=120.0):
|
||||
"""Search all 2-partitions for two induced trees. Time-limited."""
|
||||
nodes = list(G.nodes())
|
||||
n = len(nodes)
|
||||
t0 = time.time()
|
||||
checked = 0
|
||||
for mask in range(1, 2 ** (n - 1)):
|
||||
checked += 1
|
||||
if checked % 200000 == 0 and time.time() - t0 > time_limit:
|
||||
return None, checked
|
||||
A = [nodes[0]] + [nodes[i + 1] for i in range(n - 1) if (mask >> i) & 1]
|
||||
B = [nodes[i + 1] for i in range(n - 1) if not ((mask >> i) & 1)]
|
||||
if not B:
|
||||
continue
|
||||
if nx.is_tree(G.subgraph(A)) and nx.is_tree(G.subgraph(B)):
|
||||
return (tuple(sorted(A)), tuple(sorted(B))), checked
|
||||
return False, checked
|
||||
|
||||
|
||||
def main():
|
||||
graphs = parse_planar_code('/tmp/nonham38m4.pc')
|
||||
print(f'{len(graphs)} Holton-McKay graphs loaded\n')
|
||||
|
||||
for i, (G, adj) in enumerate(graphs):
|
||||
D, faces = dual_triangulation(G)
|
||||
n = D.number_of_nodes()
|
||||
m = D.number_of_edges()
|
||||
is_tri = (m == 3 * n - 6)
|
||||
print(f'graph {i}: dual has n={n}, m={m}, triangulation={is_tri}')
|
||||
|
||||
# Verify NOT intertwining tree
|
||||
t0 = time.time()
|
||||
result, checked = is_intertwining_tree(D, time_limit=180.0)
|
||||
dt = time.time() - t0
|
||||
if result is False:
|
||||
print(f' intertwining tree: NO '
|
||||
f'(checked all {checked} partitions, {dt:.1f}s) '
|
||||
f'[consistent with non-Hamiltonian dual]')
|
||||
elif result is None:
|
||||
print(f' intertwining tree: search timed out after {checked} '
|
||||
f'partitions ({dt:.1f}s)')
|
||||
else:
|
||||
print(f' intertwining tree: YES (UNEXPECTED) partition={result}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,167 @@
|
||||
"""Test the disjunction on the Tutte graph dual: a 25-vertex
|
||||
triangulation whose dual is a Tait counterexample. By the
|
||||
intertwining-tree⇔dual-Hamiltonian equivalence, the dual is not
|
||||
intertwining tree. So the conjecture requires it to be a derived level
|
||||
graph.
|
||||
|
||||
Approach: backward BFS from G in the E/O-switch graph. A predecessor of
|
||||
H is H' = H - wx + uv where uv is the diagonal of wx's quadrilateral in
|
||||
H and uv has same-parity endpoints (so the forward switch on uv in H' is
|
||||
valid).
|
||||
|
||||
For each candidate vertex-labelling of G (each potential level source
|
||||
in some Even Level Graph), we BFS backward and check if any reached state
|
||||
is itself an Even Level Graph for that labelling.
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, '/Users/didericis/Code/math-research/papers/'
|
||||
'level_resolutions_of_maximal_planar_graphs/experiments')
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import networkx as nx
|
||||
import time
|
||||
from tutte_dual_treecolor import dual_triangulation
|
||||
from test_conjecture import (
|
||||
bfs_levels, is_even_level_graph,
|
||||
)
|
||||
|
||||
|
||||
def is_tree(subg):
|
||||
return nx.is_tree(subg) if subg.number_of_nodes() > 0 else True
|
||||
|
||||
|
||||
def is_intertwining_tree(G, time_limit=10.0):
|
||||
"""Search for a 2-partition (A, B) such that G[A] and G[B] are trees.
|
||||
Returns the partition or None. Time-limited."""
|
||||
nodes = list(G.nodes())
|
||||
n = len(nodes)
|
||||
t0 = time.time()
|
||||
for mask in range(1, 2 ** (n - 1)):
|
||||
if time.time() - t0 > time_limit:
|
||||
return None # timed out, unknown
|
||||
A = [nodes[0]] + [nodes[i + 1] for i in range(n - 1) if (mask >> i) & 1]
|
||||
B = [nodes[i + 1] for i in range(n - 1) if not ((mask >> i) & 1)]
|
||||
if not A or not B:
|
||||
continue
|
||||
if is_tree(G.subgraph(A)) and is_tree(G.subgraph(B)):
|
||||
return (tuple(A), tuple(B))
|
||||
return False # no partition works
|
||||
|
||||
|
||||
def planar_diagonal(G, u, v):
|
||||
"""Given edge uv in triangulation G, return the diagonal of the
|
||||
quadrilateral around uv (= the two third vertices of the triangles
|
||||
at uv)."""
|
||||
ok, emb = nx.check_planarity(G)
|
||||
if not ok:
|
||||
return None
|
||||
f1 = emb.traverse_face(u, v)
|
||||
f2 = emb.traverse_face(v, u)
|
||||
if len(f1) != 3 or len(f2) != 3:
|
||||
return None
|
||||
w = next(x for x in f1 if x != u and x != v)
|
||||
x = next(y for y in f2 if y != u and y != v)
|
||||
if w == x:
|
||||
return None
|
||||
return (w, x)
|
||||
|
||||
|
||||
def backward_predecessors(G, labels):
|
||||
"""Yield (G', uv, wx) where G' →forward G via switching uv (E/O in
|
||||
labels). For each edge wx of G whose diagonal uv has same-parity
|
||||
endpoints AND uv is not already an edge of G."""
|
||||
for u, v in list(G.edges()):
|
||||
diag = planar_diagonal(G, u, v)
|
||||
if diag is None:
|
||||
continue
|
||||
w, x = diag
|
||||
if labels[w] % 2 != labels[x] % 2:
|
||||
continue # diagonal not E/O, can't be reverse-switched
|
||||
if G.has_edge(w, x):
|
||||
continue # would create multi-edge
|
||||
Gp = G.copy()
|
||||
Gp.remove_edge(u, v)
|
||||
Gp.add_edge(w, x)
|
||||
yield Gp, (u, v), (w, x)
|
||||
|
||||
|
||||
def find_elg_via_backward_bfs(G_start, labels, max_states=50000,
|
||||
time_limit=120.0):
|
||||
"""BFS backward from G_start. If we hit a triangulation that is an
|
||||
Even Level Graph for the same labelling (specifically, where the
|
||||
labels match BFS levels from some source in this triangulation),
|
||||
return it."""
|
||||
t0 = time.time()
|
||||
seen = {frozenset(frozenset(e) for e in G_start.edges())}
|
||||
frontier = [G_start]
|
||||
rounds = 0
|
||||
while frontier and len(seen) < max_states:
|
||||
if time.time() - t0 > time_limit:
|
||||
return None, len(seen), rounds, 'timed out'
|
||||
new = []
|
||||
for H in frontier:
|
||||
# Check if H is an Even Level Graph for some source
|
||||
# whose BFS levels match `labels` mod 2.
|
||||
for v in H.nodes():
|
||||
is_elg, lvls = is_even_level_graph(H, frozenset({v}))
|
||||
if not is_elg or lvls is None:
|
||||
continue
|
||||
# Check if lvls parities match labels mod 2 (up to swap)
|
||||
same = all(lvls[u] % 2 == labels[u] % 2 for u in H.nodes())
|
||||
opp = all(lvls[u] % 2 != labels[u] % 2 for u in H.nodes())
|
||||
if same or opp:
|
||||
return H, len(seen), rounds, ('match' if same else 'swapped')
|
||||
for Hp, uv, wx in backward_predecessors(H, labels):
|
||||
sig = frozenset(frozenset(e) for e in Hp.edges())
|
||||
if sig in seen:
|
||||
continue
|
||||
seen.add(sig)
|
||||
new.append(Hp)
|
||||
if len(seen) >= max_states:
|
||||
break
|
||||
if len(seen) >= max_states:
|
||||
break
|
||||
frontier = new
|
||||
rounds += 1
|
||||
return None, len(seen), rounds, 'exhausted' if not frontier else 'capped'
|
||||
|
||||
|
||||
def main():
|
||||
G_orig = nx.tutte_graph()
|
||||
D, _ = dual_triangulation(G_orig)
|
||||
n = D.number_of_nodes()
|
||||
print(f'Tutte graph dual: n={n}, edges={D.number_of_edges()}')
|
||||
|
||||
# Confirm not intertwining tree (we already know this, but verify)
|
||||
print('Verifying NOT intertwining tree (time-limited)...')
|
||||
result = is_intertwining_tree(D, time_limit=60.0)
|
||||
if result is False:
|
||||
print(' confirmed: no 2-tree partition exists')
|
||||
elif result is None:
|
||||
print(' search timed out; cannot confirm')
|
||||
else:
|
||||
print(f' SURPRISE: is intertwining tree with partition {result}')
|
||||
|
||||
# Try various labellings: BFS from each vertex of D, take levels.
|
||||
# For each labelling, do backward BFS to find ELG.
|
||||
print('\nSearching for backward path to an Even Level Graph...')
|
||||
for src in list(D.nodes())[:5]: # first 5 sources
|
||||
labels = bfs_levels(D, frozenset({src}))
|
||||
print(f' source={src}, label distribution: '
|
||||
f'even={sum(1 for l in labels.values() if l % 2 == 0)}, '
|
||||
f'odd={sum(1 for l in labels.values() if l % 2 == 1)}')
|
||||
result, n_states, rnds, status = find_elg_via_backward_bfs(
|
||||
D, labels, max_states=20000, time_limit=60.0)
|
||||
if result is not None:
|
||||
print(f' FOUND ELG ancestor ({status}, '
|
||||
f'{n_states} states, {rnds} rounds)')
|
||||
return
|
||||
else:
|
||||
print(f' no ELG ancestor found ({status}, '
|
||||
f'{n_states} states, {rnds} rounds)')
|
||||
|
||||
print('No ELG found in backward orbit with the labellings tried.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user