Add small-n ELG counting experiment (iso, rooted)

count_elgs.py enumerates triangulation iso-classes and counts Even Level
Graphs (G,v) per n: iso-classes (sources up to Aut) and flag-rooted
(4E/|Aut| * s, an exact integer since Aut acts freely on flags).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-22 12:20:18 -04:00
parent dcb4316eca
commit 8fde9494d8
5 changed files with 194 additions and 9 deletions
@@ -0,0 +1,109 @@
"""
Count Even Level Graphs (ELGs) for small n.
An ELG is a pair (G, S) where G is a plane triangulation on n vertices,
S = {v} a single source vertex, and every level subgraph L_k (vertices at
BFS-distance k from v) is bipartite -- equivalently, every level cycle is
even (Definition: even-level-graph, sibling paper).
We report per n:
- tri : # iso-classes of plane triangulations (maximal planar graphs)
- elg_iso : # iso-classes of ELG *pairs* (G,v), i.e. valid sources counted
up to Aut(G)
- elg_root : # flag-rooted ELGs = sum over iso-classes of 4E/|Aut(G)| * s(G),
E = 3n-6, s(G) = # valid sources. Aut(G) acts freely on the 4E
flags, so each term is an exact integer -- the small-integer,
automorphism-free count (the natural one for closed forms).
"""
import time
import networkx as nx
from networkx.algorithms.isomorphism import GraphMatcher
from triangulation_gen import enumerate_all_triangulations
def is_even_level_graph(G, source):
"""Every level subgraph L_k (BFS distance k from source) is bipartite,
equivalently every level cycle is even. Mirrors test_conjecture.py in
the even_level_graph_generators paper."""
levels = {v: float("inf") for v in G.nodes()}
from collections import deque
dq = deque()
for s in source:
levels[s] = 0
dq.append(s)
while dq:
v = dq.popleft()
for w in G[v]:
if levels[w] > levels[v] + 1:
levels[w] = levels[v] + 1
dq.append(w)
if any(l == float("inf") for l in levels.values()):
return False, None # disconnected: source can't reach all vertices
for k in range(max(levels.values()) + 1):
L_k = G.subgraph([v for v in G.nodes() if levels[v] == k])
if not nx.is_bipartite(L_k):
return False, None
return True, levels
def automorphisms(G):
"""All automorphisms of G as node->node dict mappings."""
return list(GraphMatcher(G, G).isomorphisms_iter())
def valid_sources(G):
"""Vertices v such that (G, {v}) is an ELG."""
return [v for v in G.nodes()
if is_even_level_graph(G, frozenset({v}))[0]]
def source_orbits(G, sources, autos):
"""Number of Aut(G)-orbits among the given source vertices."""
src = set(sources)
seen, orbits = set(), 0
for v in sources:
if v in seen:
continue
orbits += 1
for a in autos:
seen.add(a[v])
return orbits
def count_for_n(n):
tris = enumerate_all_triangulations(n)
flags = 4 * (3 * n - 6) # # flags of any n-vertex triangulation, E = 3n-6
elg_iso = 0
elg_root = 0
n_elg_tris = 0 # triangulations admitting at least one ELG source
for G in tris:
srcs = valid_sources(G)
if not srcs:
continue
n_elg_tris += 1
autos = automorphisms(G)
aut_size = len(autos)
elg_iso += source_orbits(G, srcs, autos)
# Aut acts freely on flags, so flags//aut_size is exact per class.
assert flags % aut_size == 0, (n, aut_size, flags)
elg_root += (flags // aut_size) * len(srcs)
return {
"tri": len(tris),
"tri_with_elg": n_elg_tris,
"elg_iso": elg_iso,
"elg_root": elg_root,
}
if __name__ == "__main__":
import sys
ns = [int(x) for x in sys.argv[1:]] or [4, 5, 6, 7, 8]
print(f"{'n':>3} {'tri':>6} {'tri+ELG':>8} {'elg_iso':>8} "
f"{'elg_root':>9} {'time':>6}")
for n in ns:
t0 = time.time()
r = count_for_n(n)
print(f"{n:>3} {r['tri']:>6} {r['tri_with_elg']:>8} "
f"{r['elg_iso']:>8} {r['elg_root']:>9} "
f"{time.time()-t0:>5.1f}s")