diff --git a/papers/medial_tire_decompositions_of_plane_triangulations/experiments/kempe_transfer_relation_probe.py b/papers/medial_tire_decompositions_of_plane_triangulations/experiments/kempe_transfer_relation_probe.py new file mode 100644 index 0000000..3e5ccde --- /dev/null +++ b/papers/medial_tire_decompositions_of_plane_triangulations/experiments/kempe_transfer_relation_probe.py @@ -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() diff --git a/papers/medial_tire_decompositions_of_plane_triangulations/experiments/kempe_uniform_family_probe.py b/papers/medial_tire_decompositions_of_plane_triangulations/experiments/kempe_uniform_family_probe.py new file mode 100644 index 0000000..bdc880e --- /dev/null +++ b/papers/medial_tire_decompositions_of_plane_triangulations/experiments/kempe_uniform_family_probe.py @@ -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()