Add transfer-relation & uniform-family probes (chained-seam / pigeonhole)
Pursue the paper's medial pigeonhole programme (R_T restriction relation, chain-pigeonhole conjecture) at the data level. Findings: R_T (outer<->inner boundary necklace, one Kempe-balanced colouring) is genuinely coupled, not a product of its projections. A uniform per-size boundary-state family threading every tile EXISTS at n=9 (unique per size, the balanced-block necklaces 0011/000011/012/00012 -- not monochromatic), but FAILS at n=12: size-7 seams admit no universal state (|D[7]|=0; near-universal 0001112 realised on 210/211 boundaries, blocked by one tile). So the uniform "same state everywhere" shortcut breaks once large odd seams appear and universals vanish as the tile population grows; the per-interface pigeonhole choice is genuinely needed. Pairwise gluability still holds, so this locates the conjecture's difficulty rather than obstructing gluing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+144
@@ -0,0 +1,144 @@
|
||||
"""Transfer relation R_T and the chained-seam (medial pigeonhole) question.
|
||||
|
||||
Each tire carries a restriction relation R_T between its OUTER boundary state
|
||||
(up-tooth apexes) and its INNER boundary state (singleton down-tooth apexes on
|
||||
its single inner face), realised jointly by one Kempe-balanced 3-colouring.
|
||||
Boundary states are taken up to colour permutation AND dihedral symmetry of the
|
||||
boundary walk (paper Def "medial boundary state") -- i.e. as necklaces.
|
||||
|
||||
A nested chain T_0 ⊃ T_1 ⊃ ... glues iff consecutive boundary states match:
|
||||
inner-state(T_i) = outer-state(T_{i+1}). The chain-pigeonhole conjecture asks
|
||||
whether such a chain can ever be obstructed.
|
||||
|
||||
We restrict to no-bite tiles (single inner face = root, all down teeth
|
||||
singletons), so outer size p = #up and inner size q = n - p, and study:
|
||||
|
||||
1. R_T coupling: is R_T a full product proj_out × proj_in, or does the inner
|
||||
state genuinely constrain the outer state?
|
||||
2. Common(p,q): boundary-state pairs realised by EVERY tile of that size class
|
||||
-- a pair here can be used uniformly regardless of which tile sits there.
|
||||
3. Uniform pass-through: is there a choice of one state sigma_m per level size m
|
||||
with (sigma_p, sigma_q) in Common(p,q) for every size class? If so, putting
|
||||
sigma_m on every level cycle glues ANY such stack with no pigeonhole needed.
|
||||
|
||||
Summary numbers only.
|
||||
|
||||
Run: python3 kempe_transfer_relation_probe.py --n 9
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import itertools
|
||||
from collections import defaultdict
|
||||
|
||||
from full_medial_tire_generator import generate
|
||||
from kempe_valid_colorings import classify_colorings
|
||||
from kempe_up_tooth_sequences import canonical_sequence, seq_str
|
||||
|
||||
|
||||
def necklace(seq: tuple[int, ...]) -> tuple[int, ...]:
|
||||
"""Boundary state: canonical under rotation, reflection, and colour perm."""
|
||||
n = len(seq)
|
||||
return min(canonical_sequence(s[r:] + s[:r])
|
||||
for s in (seq, seq[::-1]) for r in range(n))
|
||||
|
||||
|
||||
def admissible_necklaces(m: int) -> set[tuple[int, ...]]:
|
||||
out = set()
|
||||
for combo in itertools.product((0, 1, 2), repeat=m):
|
||||
if all(combo.count(k) % 2 == m % 2 for k in (0, 1, 2)):
|
||||
out.add(necklace(combo))
|
||||
return out
|
||||
|
||||
|
||||
def cuts(neck: tuple[int, ...]) -> int:
|
||||
"""Cyclic colour-changes -- low = 'blocky', easy-to-present boundary."""
|
||||
return sum(1 for i in range(len(neck)) if neck[i] != neck[(i + 1) % len(neck)])
|
||||
|
||||
|
||||
def tile_relation(n, g):
|
||||
"""R_T as a set of (outer-necklace, inner-necklace) pairs for a no-bite tile.
|
||||
Returns None unless the tile has p>=3 up teeth and q>=3 singleton downs."""
|
||||
p = len(g.up_edges)
|
||||
downs = g.singleton_down_edges
|
||||
if g.bites or p < 3 or len(downs) < 3:
|
||||
return None
|
||||
R = set()
|
||||
for c, v in classify_colorings(g, dedup_colors=True):
|
||||
if not v.valid:
|
||||
continue
|
||||
o = necklace(tuple(c[f"u{e}"] for e in g.up_edges))
|
||||
i = necklace(tuple(c[f"d{e}"] for e in downs))
|
||||
R.add((o, i))
|
||||
return (p, len(downs)), R
|
||||
|
||||
|
||||
def run(args):
|
||||
n = args.n
|
||||
by_class: dict[tuple[int, int], list[set]] = defaultdict(list)
|
||||
for g in generate(n, min_up_teeth=3, dedup=True):
|
||||
res = tile_relation(n, g)
|
||||
if res is None:
|
||||
continue
|
||||
(p, q), R = res
|
||||
by_class[(p, q)].append(R)
|
||||
|
||||
print(f"n={n}: transfer relation R_T over (outer, inner) boundary necklaces "
|
||||
f"(no-bite tiles)\n")
|
||||
print(f"{'(p,q)':>7} {'#tiles':>6} {'|adm_p|':>7} {'|adm_q|':>7} "
|
||||
f"{'out real':>8} {'in real':>7} {'product?':>8} {'|Common|':>8}")
|
||||
print("-" * 70)
|
||||
common: dict[tuple[int, int], set] = {}
|
||||
for (p, q) in sorted(by_class):
|
||||
rels = by_class[(p, q)]
|
||||
adm_p, adm_q = len(admissible_necklaces(p)), len(admissible_necklaces(q))
|
||||
proj_out = set().union(*[{o for o, _ in R} for R in rels])
|
||||
proj_in = set().union(*[{i for _, i in R} for R in rels])
|
||||
# is every tile's R_T a full product of its own projections?
|
||||
all_product = all(
|
||||
R == {(o, i) for o in {a for a, _ in R} for i in {b for _, b in R}}
|
||||
for R in rels
|
||||
)
|
||||
com = set.intersection(*rels) if rels else set()
|
||||
common[(p, q)] = com
|
||||
print(f"{str((p, q)):>7} {len(rels):>6} {adm_p:>7} {adm_q:>7} "
|
||||
f"{len(proj_out):>8} {len(proj_in):>7} {str(all_product):>8} {len(com):>8}")
|
||||
|
||||
# Uniform pass-through CSP: pick sigma_m per size so (sigma_p, sigma_q) in
|
||||
# Common(p,q) for every class. Brute force over candidate states per size.
|
||||
print("\nUniform pass-through (one state per level size, glues any stack):")
|
||||
sizes = sorted({s for cls in common for s in cls})
|
||||
cand = {m: sorted({o for (o, _) in common.get((m, n - m), set())} |
|
||||
{i for (_, i) in common.get((n - m, m), set())} |
|
||||
admissible_necklaces(m))
|
||||
for m in sizes}
|
||||
# constraints: for each class (p,q) present, (sigma_p,sigma_q) in Common(p,q)
|
||||
classes = [(p, q) for (p, q) in common if common[(p, q)]]
|
||||
empty_classes = [c for c in common if not common[c]]
|
||||
if empty_classes:
|
||||
print(f" classes with EMPTY Common (no pair works for all tiles): {empty_classes}")
|
||||
found = None
|
||||
for assignment in itertools.product(*[cand[m] for m in sizes]):
|
||||
sigma = dict(zip(sizes, assignment))
|
||||
if all((sigma[p], sigma[q]) in common[(p, q)] for (p, q) in classes):
|
||||
found = sigma
|
||||
break
|
||||
if found:
|
||||
print(" FEASIBLE -- uniform family exists:")
|
||||
for m in sizes:
|
||||
print(f" size {m}: {seq_str(found[m])} (cuts={cuts(found[m])})")
|
||||
print(" => every no-bite nested stack at this n glues with no pigeonhole.")
|
||||
else:
|
||||
print(" INFEASIBLE -- no single state-per-size family glues all tiles;")
|
||||
print(" the uniform strategy fails, so chaining genuinely couples.")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--n", type=int, default=9)
|
||||
run(parser.parse_args())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
"""Is there a uniform boundary-state family threading every tile (branches too)?
|
||||
|
||||
A *uniform family* assigns one boundary state sigma_m (a necklace) to every
|
||||
level-cycle size m. A tile is *threaded* if one Kempe-balanced 3-colouring shows
|
||||
sigma_p on its outer face (p up teeth) AND sigma_{q_k} on every inner face k (each
|
||||
a >=3-singleton-down face). If a family threads every tile, assigning sigma_m to
|
||||
every level cycle glues ANY tire tree -- including branching (bite) nodes -- with
|
||||
no pigeonhole. Infeasibility means the medial pigeonhole is genuinely needed.
|
||||
|
||||
This is a constraint-satisfaction search:
|
||||
* domain D[m] = necklaces realisable on EVERY size-m boundary (outer or inner)
|
||||
across all tiles -- the per-size universal states;
|
||||
* per-tile constraint = some Kempe-balanced colouring realises the chosen
|
||||
sigma on its outer face and all its inner faces simultaneously.
|
||||
|
||||
Reports whether a uniform family exists and, if so, one witness.
|
||||
|
||||
Run: python3 kempe_uniform_family_probe.py --n 9
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from collections import defaultdict
|
||||
|
||||
from full_medial_tire_generator import generate, innermost_bite
|
||||
from kempe_valid_colorings import classify_colorings
|
||||
from kempe_transfer_relation_probe import necklace, cuts
|
||||
from kempe_up_tooth_sequences import seq_str
|
||||
|
||||
|
||||
def inner_faces(g):
|
||||
faces = defaultdict(list)
|
||||
for e in g.singleton_down_edges:
|
||||
faces[innermost_bite(e, g.bites)].append(e)
|
||||
return [sorted(es) for es in faces.values() if len(es) >= 3]
|
||||
|
||||
|
||||
class Tile:
|
||||
__slots__ = ("p", "qsizes", "joint", "is_bite", "word", "bites")
|
||||
|
||||
def __init__(self, g):
|
||||
faces = inner_faces(g)
|
||||
self.p = len(g.up_edges)
|
||||
self.qsizes = [len(f) for f in faces]
|
||||
self.is_bite = bool(g.bites)
|
||||
self.word = g.tooth_word
|
||||
self.bites = ",".join(f"({i},{j})" for i, j in sorted(g.bites)) or "-"
|
||||
# realisable joint signatures: (outer_neck, (inner_neck_by_face...))
|
||||
self.joint = set()
|
||||
for c, v in classify_colorings(g, dedup_colors=True):
|
||||
if not v.valid:
|
||||
continue
|
||||
o = necklace(tuple(c[f"u{e}"] for e in g.up_edges))
|
||||
ins = tuple(necklace(tuple(c[f"d{e}"] for e in f)) for f in faces)
|
||||
self.joint.add((o, ins))
|
||||
|
||||
|
||||
def run(args):
|
||||
n = args.n
|
||||
tiles = []
|
||||
realizable = defaultdict(lambda: defaultdict(set)) # size -> "marginal" sets list
|
||||
per_size_real = defaultdict(list) # size -> list of realizable-necklace sets
|
||||
for g in generate(n, min_up_teeth=3, dedup=True):
|
||||
if not inner_faces(g):
|
||||
continue
|
||||
t = Tile(g)
|
||||
tiles.append(t)
|
||||
# marginal realisable sets per boundary (for domain restriction)
|
||||
outs = {o for o, _ in t.joint}
|
||||
per_size_real[t.p].append(outs)
|
||||
for k, q in enumerate(t.qsizes):
|
||||
ins_k = {ins[k] for _, ins in t.joint}
|
||||
per_size_real[q].append(ins_k)
|
||||
|
||||
# D[m] = necklaces realisable on EVERY size-m boundary (per-size universal)
|
||||
D = {m: set.intersection(*sets) for m, sets in per_size_real.items()}
|
||||
sizes = sorted(D)
|
||||
n_bite = sum(1 for t in tiles if t.is_bite)
|
||||
n_branch = sum(1 for t in tiles if len(t.qsizes) >= 2)
|
||||
print(f"n={n}: uniform-family CSP over {len(tiles)} tiles "
|
||||
f"({n_bite} bite, {n_branch} branching with >=2 inner faces)\n")
|
||||
print("per-size universal domain |D[m]|:",
|
||||
" ".join(f"{m}:{len(D[m])}" for m in sizes))
|
||||
|
||||
# Backtracking search over sigma_m in D[m], with per-tile joint constraints.
|
||||
order = sizes
|
||||
sigma: dict[int, tuple] = {}
|
||||
|
||||
def tile_ok(t):
|
||||
# all sizes the tile touches must be assigned to test it
|
||||
if t.p not in sigma or any(q not in sigma for q in t.qsizes):
|
||||
return True # not yet fully constrained
|
||||
want = (sigma[t.p], tuple(sigma[q] for q in t.qsizes))
|
||||
return want in t.joint
|
||||
|
||||
def consistent():
|
||||
return all(tile_ok(t) for t in tiles)
|
||||
|
||||
def bt(idx):
|
||||
if idx == len(order):
|
||||
return dict(sigma)
|
||||
m = order[idx]
|
||||
for s in sorted(D[m]):
|
||||
sigma[m] = s
|
||||
if consistent():
|
||||
res = bt(idx + 1)
|
||||
if res is not None:
|
||||
return res
|
||||
del sigma[m]
|
||||
return None
|
||||
|
||||
sol = bt(0)
|
||||
if sol:
|
||||
print("\n=> FEASIBLE: a uniform family threads every tile (branches included).")
|
||||
for m in sizes:
|
||||
print(f" size {m}: {seq_str(sol[m])} (cuts={cuts(sol[m])})")
|
||||
print(" Assigning sigma_m to every level cycle glues any tire tree at "
|
||||
"this n with no pigeonhole.")
|
||||
else:
|
||||
# diagnose: which tiles can't be threaded by any domain choice
|
||||
print("\n=> INFEASIBLE: no uniform family threads all tiles.")
|
||||
# find tiles whose joint set never matches the per-size domains
|
||||
bad = []
|
||||
for t in tiles:
|
||||
if not any(o in D.get(t.p, set()) and
|
||||
all(ins[k] in D.get(t.qsizes[k], set()) for k in range(len(t.qsizes)))
|
||||
for o, ins in t.joint):
|
||||
bad.append(t)
|
||||
print(f" {len(bad)} tile(s) cannot use any per-size universal jointly:")
|
||||
for t in bad[:20]:
|
||||
print(f" word={t.word} bites={t.bites} outer={t.p} inner={t.qsizes}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--n", type=int, default=9)
|
||||
run(parser.parse_args())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user