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:
+182
@@ -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()
|
||||
+167
@@ -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()
|
||||
+129
@@ -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()
|
||||
+172
@@ -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()
|
||||
Reference in New Issue
Block a user