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:
2026-05-21 20:59:13 -04:00
parent d7e83a45ac
commit 9bf4deac74
10 changed files with 481 additions and 21 deletions
@@ -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}')
@@ -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()