Add Heawood boundary-restriction experiments and findings note

Experiments probing the cluster restriction set R_K / Phi: R_K is a Z/3
zonotope (not a GF(3) subspace), the "richness" invariant is an artifact
of non-shrinking annuli, the interface gluing always works on interior
cycles (forced by 4CT), and the maximal constraint achievable on an
n-cycle is a floor of 2^(n-2) -- already reached by the trivial tire.
Note boundary_restriction_structure.tex writes these up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 02:12:54 -04:00
parent 351ae0cdfe
commit 60c9f1d3a8
8 changed files with 1087 additions and 0 deletions
@@ -0,0 +1,182 @@
"""
Does the richness invariant survive BRANCHING?
For a separating cycle C bounding a disk G_C (away from the source), the achievable
outer-interface set is
Phi(C) = { (lambda*(v))_{v in C} : lambda in {+1,-1}^{F(G_C)},
sum_{f ∋ w} lambda(f) ≡ 0 for every
truly-interior vertex w of G_C }.
This is the exact value the recursive transfer operator produces at C (interior
consistency = all the descendant gluings already performed; seam/boundary vertices
are deferred, exactly as in the recursion). We compute Phi(C) by constrained
enumeration over real triangulations and test the candidate self-similar invariant
non-empty & closed under sign flip & full single-position marginals
separately at BRANCH nodes (region encloses >=2 disjoint deeper sub-tires) and at
LINEAR nodes (one child), to see whether branching breaks it.
"""
import sys
from collections import defaultdict, deque
from itertools import product
import numpy as np
from scipy.spatial import Delaunay
def delaunay(n, rng):
pts = rng.random((n, 2))
tri = Delaunay(pts)
faces = [tuple(int(x) for x in s) for s in tri.simplices]
hull = set(int(v) for e in tri.convex_hull for v in e)
return faces, hull
def build(faces):
adj = defaultdict(set)
efaces = defaultdict(list)
vfaces = defaultdict(list)
for fi, (a, b, c) in enumerate(faces):
adj[a] |= {b, c}; adj[b] |= {a, c}; adj[c] |= {a, b}
for e in ((a, b), (b, c), (a, c)):
efaces[frozenset(e)].append(fi)
for v in (a, b, c):
vfaces[v].append(fi)
fadj = [set() for _ in faces]
for fl in efaces.values():
for i in fl:
for j in fl:
if i != j:
fadj[i].add(j)
return adj, fadj, vfaces
def bfs(adj, src):
lev = {src: 0}; q = deque([src])
while q:
u = q.popleft()
for w in adj[u]:
if w not in lev:
lev[w] = lev[u] + 1; q.append(w)
return lev
def components(face_ids, fadj):
idset = set(face_ids)
seen = set(); comps = []
for s in face_ids:
if s in seen:
continue
comp = []; stack = [s]; seen.add(s)
while stack:
u = stack.pop(); comp.append(u)
for w in fadj[u]:
if w in idset and w not in seen:
seen.add(w); stack.append(w)
comps.append(comp)
return comps
def sign_closed(S):
return all(tuple((3 - x) % 3 for x in s) in S for s in S)
def marginals_full(S, k):
return all({s[i] for s in S} == {0, 1, 2} for i in range(k))
def phi_of_region(comp_faces, faces, vfaces, lev, d, cap):
"""Constrained-enumeration Phi on the outer (level-d) cycle of a region."""
Gc = comp_faces
if not (1 <= len(Gc) <= cap):
return None
Gcset = set(Gc)
verts = sorted(set(v for fi in Gc for v in faces[fi]))
# truly-interior: every global incident face is inside G_C (=> level > d)
interior = [v for v in verts if all(f in Gcset for f in vfaces[v])]
boundary_C = [v for v in verts if lev[v] == d and v not in interior]
if not boundary_C:
return None
F = len(Gc)
fidx = {fi: j for j, fi in enumerate(Gc)}
# incidence rows
Bint = np.zeros((len(interior), F), dtype=np.int64)
for r, w in enumerate(interior):
for fi in vfaces[w]:
if fi in Gcset:
Bint[r, fidx[fi]] = 1
Cinc = np.zeros((len(boundary_C), F), dtype=np.int64)
for r, v in enumerate(boundary_C):
for fi in vfaces[v]:
if fi in Gcset:
Cinc[r, fidx[fi]] = 1
labs = np.array(list(product((1, 2), repeat=F)), dtype=np.int64)
if len(interior):
ok = np.all((labs @ Bint.T) % 3 == 0, axis=1)
labs = labs[ok]
if labs.shape[0] == 0:
return set(), len(boundary_C)
outer = (labs @ Cinc.T) % 3
return set(map(tuple, np.unique(outer, axis=0))), len(boundary_C)
def main():
seed = int(sys.argv[1]) if len(sys.argv) > 1 else 0
nprng = np.random.default_rng(seed)
CAP = 18
stats = {True: [0, 0, 0, 0], False: [0, 0, 0, 0]} # branch: [n, nonempty, sign, marg]
examples_fail = []
for _ in range(300):
faces, hull = delaunay(int(nprng.integers(14, 34)), nprng)
adj, fadj, vfaces = build(faces)
lev = bfs(adj, min(hull))
if len(lev) != len(adj):
continue
depth = [min(lev[v] for v in faces[fi]) for fi in range(len(faces))]
maxd = max(depth)
for d in range(1, maxd + 1):
fge = [fi for fi in range(len(faces)) if depth[fi] >= d]
for comp in components(fge, fadj):
if not (1 <= len(comp) <= CAP):
continue
deeper = [fi for fi in comp if depth[fi] >= d + 1]
n_children = len(components(deeper, fadj))
is_branch = n_children >= 2
res = phi_of_region(comp, faces, vfaces, lev, d, CAP)
if res is None:
continue
S, k = res
rec = stats[is_branch]
rec[0] += 1
rec[1] += bool(S)
rec[2] += (bool(S) and sign_closed(S))
rec[3] += (bool(S) and marginals_full(S, k))
if S and not marginals_full(S, k) and len(examples_fail) < 6:
examples_fail.append((is_branch, n_children, len(comp), k,
len(S)))
for branch in (False, True):
n, ne, sg, mg = stats[branch]
tag = "BRANCH (>=2 children)" if branch else "LINEAR (1 child)"
if n:
print(f"{tag}: {n} regions")
print(f" non-empty : {ne}/{n} ({100*ne/n:.1f}%)")
print(f" sign-closed : {sg}/{n} ({100*sg/n:.1f}%)")
print(f" marginals-full : {mg}/{n} ({100*mg/n:.1f}%)")
else:
print(f"{tag}: 0 regions")
if examples_fail:
print("\n marginals-NOT-full examples (branch?,n_children,|G_C|,|C|,|Phi|):")
for e in examples_fail:
print(f" {e}")
else:
print("\n richness (incl. full marginals) held on every region tested.")
if __name__ == "__main__":
main()
@@ -0,0 +1,167 @@
"""
Construct the triangulated disk (= nested tire substructure) that MAXIMALLY
constrains its outer cycle.
For a triangulated disk D with boundary cycle C = (0..n-1), the achievable outer
Heawood set is
Phi(D) = { (lambda*(v))_{v in C} : lambda in {+1,-1}^{faces},
sum_{f ∋ w} lambda(f) ≡ 0 for every interior vertex w } .
Phi depends only on the disk triangulation (no BFS/tree needed). We want the disk
minimising |Phi| -- the worst case for the pigeonhole. Note Phi is always
sign-closed and non-empty, so |Phi| >= 1, and |Phi| = 1 forces Phi = { all-zeros }.
Key local fact: a degree-3 interior vertex (one Apollonian stack) has incident
faces f1,f2,f3 with lambda(f1)+lambda(f2)+lambda(f3) ≡ 0 mod 3 over +/-1 values,
which forces f1=f2=f3. So stacking chains equalities and collapses Phi.
We (a) randomly search disks built by Apollonian stacking, and (b) try a
deterministic deep-stack construction, reporting the smallest Phi found.
"""
import random
import sys
from itertools import product
import numpy as np
def fan_triangulation(n):
"""n-gon (0..n-1) triangulated as a fan from vertex 0. No interior vertex."""
return [(0, i, i + 1) for i in range(1, n - 1)]
def stack(faces, idx, v):
a, b, c = faces[idx]
faces[idx] = (a, b, v)
faces.append((b, c, v))
faces.append((a, c, v))
def phi(faces, n, cap):
"""Phi on boundary 0..n-1; interior = vertices >= n."""
verts = set(v for f in faces for v in f)
interior = sorted(v for v in verts if v >= n)
F = len(faces)
if F > cap:
return None
# incidence
Bint = np.zeros((len(interior), F), dtype=np.int64)
iindex = {w: r for r, w in enumerate(interior)}
Cinc = np.zeros((n, F), dtype=np.int64)
for j, (a, b, c) in enumerate(faces):
for v in (a, b, c):
if v >= n:
Bint[iindex[v], j] = 1
else:
Cinc[v, j] = 1
labs = np.array(list(product((1, 2), repeat=F)), dtype=np.int64)
if len(interior):
keep = np.all((labs @ Bint.T) % 3 == 0, axis=1)
labs = labs[keep]
if labs.shape[0] == 0:
return set()
outer = (labs @ Cinc.T) % 3
return set(map(tuple, np.unique(outer, axis=0)))
def disp(s):
return tuple(-1 if int(x) == 2 else int(x) for x in s)
def gf3_rank(rows):
M = [[int(x) % 3 for x in r] for r in rows]
if not M:
return 0
nc = len(M[0]); r = 0
for c in range(nc):
piv = next((i for i in range(r, len(M)) if M[i][c] % 3), None)
if piv is None:
continue
M[r], M[piv] = M[piv], M[r]
inv = M[r][c] % 3
M[r] = [(x * inv) % 3 for x in M[r]]
for i in range(len(M)):
if i != r and M[i][c] % 3:
fct = M[i][c] % 3
M[i] = [(M[i][k] - fct * M[r][k]) % 3 for k in range(nc)]
r += 1
if r == len(M):
break
return r
def describe(P):
P = list(P)
sign_closed = all(tuple((3 - x) % 3 for x in s) in set(P) for s in P)
s0 = P[0]
D = [tuple((np.array(s) - np.array(s0)) % 3) for s in P]
rank = gf3_rank(D)
affine = (len(P) == 3 ** rank)
pow2 = (len(P) & (len(P) - 1)) == 0
return (f"sign-closed={sign_closed} affine-GF3={affine} "
f"|Phi|={len(P)} (power-of-2={pow2}) hull-dim={rank}")
def random_disk(n, n_stacks, rng):
faces = fan_triangulation(n)
nxt = n
for _ in range(n_stacks):
stack(faces, rng.randrange(len(faces)), nxt)
nxt += 1
return faces
def deep_stack_disk(n, n_stacks):
"""Always stack into the most-recently created face -> deep equality chain."""
faces = fan_triangulation(n)
nxt = n
for _ in range(n_stacks):
stack(faces, len(faces) - 1, nxt)
nxt += 1
return faces
def search(n, cap=18, trials=400, seed=0):
rng = random.Random(seed)
best = (10 ** 9, None, None)
max_stacks = (cap - (n - 2)) // 2
# random search
for _ in range(trials):
k = rng.randint(0, max_stacks)
faces = random_disk(n, k, rng)
P = phi(faces, n, cap)
if P is None:
continue
if len(P) < best[0]:
best = (len(P), k, P)
# deterministic deep stack at max depth
for k in range(max_stacks + 1):
faces = deep_stack_disk(n, k)
P = phi(faces, n, cap)
if P is not None and len(P) < best[0]:
best = (len(P), k, P)
size, k, P = best
print(f"n={n}: min |Phi| = {size} (= 2^(n-2) = {2**(n-2)}?) "
f"interior vertices = {k}, max stacks at cap {cap} = {max_stacks}")
print(f" {describe(P)}")
for s in sorted(P)[:6]:
print(f" {disp(s)}")
if len(P) > 6:
print(f" ... (+{len(P)-6} more)")
return size
def main():
ns = [int(x) for x in sys.argv[1:]] or [4, 5, 6, 7]
print("Searching for maximally-constraining disks (min |Phi|)\n")
for n in ns:
# bigger cap for small n
cap = 18 if n <= 6 else 16
search(n, cap=cap)
print()
if __name__ == "__main__":
main()
@@ -0,0 +1,129 @@
"""
Search for a UNIVERSAL Heawood boundary sequence for a tire graph.
Fix an outer boundary cycle B_out of length n (the interface at which a tire
glues to its parent). Each way of filling the annulus -- an inner boundary of
size m together with a spoke triangulation ("inner graph") -- gives a tire whose
annular faces induce a set of realisable outer Heawood sequences
R_out(tire) = { (lambda*(v0), ..., lambda*(v_{n-1})) : lambda in {+1,-1}^F }
{0,1,-1}^n .
A *universal sequence* for B_out is one realisable for EVERY inner graph, i.e. a
member of the intersection ∩_tire R_out(tire). If a universal sequence existed,
a parent could always present its negation and glue to any child regardless of
the child's interior.
Note: chords of the inner outerplanar graph O lie inside B_in and bound no
annular face, so they do not change R_out -- only (n, m, spoke-path) do. And
intersecting over a SUBFAMILY of inner graphs can only OVERestimate the true
intersection, so finding the intersection empty over simple-cycle inner fills is
already conclusive that NO universal sequence exists.
"""
import sys
from itertools import combinations, product
import numpy as np
def lattice_paths(n_outer, m_inner):
"""All spoke triangulations: strings with n_outer 'O' moves, m_inner 'I'."""
N = n_outer + m_inner
for opos in combinations(range(N), n_outer):
opos = set(opos)
yield "".join("O" if i in opos else "I" for i in range(N))
def annular_faces(n, m, path):
"""Faces (triangles) of the annulus between outer n-cycle (0..n-1) and inner
m-cycle (n..n+m-1) under the spoke path. Starts at spoke (outer0, inner0)."""
faces = []
i = j = 0
for mv in path:
if mv == "O":
faces.append((i % n, (i + 1) % n, n + (j % m)))
i += 1
else:
faces.append((i % n, n + (j % m), n + ((j + 1) % m)))
j += 1
return faces
def fan_faces(n):
"""m = 1 degenerate inner boundary: a wheel/fan, center = vertex n."""
return [(i, (i + 1) % n, n) for i in range(n)]
def realisable_outer(n, faces):
"""Set of outer Heawood sequences over all +/-1 face labellings."""
F = len(faces)
A = np.zeros((n, F), dtype=np.int64) # outer-vertex x face incidence
for f, tri in enumerate(faces):
for v in tri:
if v < n:
A[v, f] = 1
labs = np.array(list(product((1, 2), repeat=F)), dtype=np.int64)
vals = (labs @ A.T) % 3
# display residues in {0, 1, -1}: 2 -> -1
vals = np.where(vals == 2, -1, vals)
return set(tuple(int(x) for x in row) for row in np.unique(vals, axis=0))
def tires_for(n, m_max, fcap):
"""Yield (label, faces) for inner fills of an n-outer tire."""
yield (f"m=1 fan", fan_faces(n))
for m in range(2, m_max + 1):
if n + m > fcap:
continue
for path in lattice_paths(n, m):
yield (f"m={m} {path}", annular_faces(n, m, path))
def run(n, m_max=7, fcap=13):
inter = None
n_tires = 0
min_set = (10**9, None)
shrink_trace = []
for label, faces in tires_for(n, m_max, fcap):
R = realisable_outer(n, faces)
n_tires += 1
if len(R) < min_set[0]:
min_set = (len(R), label)
if inter is None:
inter = set(R)
else:
before = len(inter)
inter &= R
if len(inter) < before:
shrink_trace.append((n_tires, label, len(inter)))
if not inter:
break
print(f"n={n}: {n_tires} tires tried, "
f"smallest single R_out = {min_set[0]} ({min_set[1]})")
if inter:
print(f" UNIVERSAL sequences found: {len(inter)}")
for s in sorted(inter)[:12]:
print(f" {s}")
else:
print(f" NO universal sequence: intersection emptied after "
f"{n_tires} tires")
print(" intersection size as tires were added (last few shrinks):")
for t in shrink_trace[-6:]:
print(f" after tire {t[0]:4d} ({t[1]}): |∩| = {t[2]}")
return bool(inter)
def main():
if len(sys.argv) > 1:
ns = [int(sys.argv[1])]
else:
ns = [3, 4, 5, 6]
print("Searching for universal Heawood boundary sequences\n")
for n in ns:
run(n)
print()
if __name__ == "__main__":
main()
@@ -0,0 +1,172 @@
"""
Transfer operator for the Heawood program, in the cleanest self-similar setting:
a chain of annular tires with n_out = n_in = n. Each tire's labelling map sends
+/-1 face labels to (outer sequence, inner sequence). Gluing a child below means
the parent's inner sequence must negate (mod 3) the child's achievable outer
sequence. So the achievable outer-interface set propagates UP the chain by
Phi(parent) = { outer(lambda) : lambda in {+-1}^F,
inner(lambda) in -Phi(child) }.
This is a monotone set-operator on subsets of (Z/3)^n. Iterating it models a
deepening nested chain; we look for a FIXED POINT (absorbing set) and test which
candidate self-similar invariants the limit satisfies:
* non-empty
* closed under the global sign flip s -> -s
* local marginals: does every position attain all of {0,1,-1}?
* is it an affine GF(3) subspace? (we expect NO -- R_T is a zonotope)
* does a linear/parity constraint cut it out?
Sequences are stored mod 3 in {0,1,2}; printed in {0,1,-1} (2 -> -1).
"""
import sys
from itertools import product
import numpy as np
def annular_tire(n_out, n_in, path):
"""Faces between outer cycle 0..n_out-1 and inner cycle n_out..n_out+n_in-1."""
faces = []
i = j = 0
for mv in path:
if mv == "O":
faces.append((i % n_out, (i + 1) % n_out, n_out + (j % n_in)))
i += 1
else:
faces.append((i % n_out, n_out + (j % n_in), n_out + ((j + 1) % n_in)))
j += 1
return faces
def labelling_pairs(n_out, n_in, faces):
"""All (outer_seq, inner_seq) over lambda in {+1,-1}^F, as Z/3 tuples."""
F = len(faces)
Ao = np.zeros((n_out, F), dtype=np.int64)
Ai = np.zeros((n_in, F), dtype=np.int64)
for f, (a, b, c) in enumerate(faces):
for v in (a, b, c):
if v < n_out:
Ao[v, f] = 1
else:
Ai[v - n_out, f] = 1
labs = np.array(list(product((1, 2), repeat=F)), dtype=np.int64)
outer = (labs @ Ao.T) % 3
inner = (labs @ Ai.T) % 3
return [(tuple(o), tuple(i)) for o, i in zip(outer.tolist(), inner.tolist())]
def make_operator(pairs):
def op(phi_child):
neg = {tuple((3 - x) % 3 for x in s) for s in phi_child}
return {o for (o, inn) in pairs if inn in neg}
return op
def iterate_to_fixed(op, start, max_iter=50):
phi = frozenset(start)
seen = [phi]
for _ in range(max_iter):
nxt = frozenset(op(phi))
if nxt == phi:
return phi, "fixed", len(seen)
if nxt in seen:
return nxt, "cycle", len(seen)
phi = nxt
seen.append(phi)
return phi, "no-converge", len(seen)
# ----------------- invariant tests -------------------------------------------
def disp(s):
return tuple(-1 if x == 2 else x for x in s)
def gf3_rank(rows):
M = [[x % 3 for x in r] for r in rows]
if not M:
return 0
nc = len(M[0]); r = 0
for c in range(nc):
piv = next((i for i in range(r, len(M)) if M[i][c] % 3), None)
if piv is None:
continue
M[r], M[piv] = M[piv], M[r]
inv = M[r][c] % 3 # 1->1, 2->2 are self-inverse mod 3
M[r] = [(x * inv) % 3 for x in M[r]]
for i in range(len(M)):
if i != r and M[i][c] % 3:
f = M[i][c] % 3
M[i] = [(M[i][k] - f * M[r][k]) % 3 for k in range(nc)]
r += 1
if r == len(M):
break
return r
def is_affine(S):
S = list(S)
if len(S) <= 1:
return True
s0 = S[0]
D = [tuple((np.array(s) - np.array(s0)) % 3) for s in S]
return len(S) == 3 ** gf3_rank(D)
def marginals_full(S, n):
return all({s[i] for s in S} == {0, 1, 2} for i in range(n))
def sign_closed(S):
return all(tuple((3 - x) % 3 for x in s) in S for s in S)
def linear_constraints(S, n):
"""Dimension of the space of linear forms vanishing on S-s0 (codim of hull)."""
S = list(S)
if len(S) <= 1:
return n
s0 = S[0]
D = [tuple((np.array(s) - np.array(s0)) % 3) for s in S]
return n - gf3_rank(D)
def analyse(tag, S, n):
print(f" [{tag}] |Phi|={len(S)} of 3^{n}={3**n} "
f"sign-closed={sign_closed(S)} marginals-full={marginals_full(S,n)} "
f"affine={is_affine(S)} hull-codim={linear_constraints(S,n)}")
def run(n, paths=None):
if paths is None:
# a few distinct same-n annular triangulations
paths = ["OI" * n, "O" * n + "I" * n, ("OOI" * n)[:2 * n]]
paths = [p for p in paths if p.count("O") == n and p.count("I") == n]
print(f"=== n={n} ===")
full = set(product((0, 1, 2), repeat=n))
for path in paths:
faces = annular_tire(n, n, path)
pairs = labelling_pairs(n, n, faces)
op = make_operator(pairs)
single = set(o for (o, _) in pairs) # leaf: full single-tire outer set
fixed, how, steps = iterate_to_fixed(op, single)
# also iterate from the universal start (all sequences allowed below)
fixed2, how2, _ = iterate_to_fixed(op, full)
print(f" path={path}: single-tire |outer|={len(single)}; "
f"iterate->{how} in {steps} steps; "
f"same-limit-from-full={fixed==fixed2}")
analyse("limit", fixed, n)
sample = sorted(disp(s) for s in fixed)[:8]
print(f" sample of limit set: {sample}")
print()
def main():
ns = [int(x) for x in sys.argv[1:]] or [4, 5, 6]
print("Transfer-operator fixed points on same-n annular tire chains\n")
for n in ns:
run(n)
if __name__ == "__main__":
main()