Add interface-admissibility probe; confirm parity characterization at n=12

For each interface size m, compare the realized census vocabulary (outer
up-tooth apexes and inner singleton-down apexes) against the full
parity-admissible set. At n=12, m=3..8 every parity-admissible sequence is
realized on both faces (counts 1,4,10,31,91,274; none missing), and up==down
throughout -- the n=9 result is n-independent and scales to m=8. Validated
against the known n=9 answer before running n=12.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 22:47:46 -04:00
parent d094a310d8
commit c56da7bb23
@@ -0,0 +1,105 @@
"""Does the realized gluing-interface vocabulary still equal the full
parity-admissible set at larger n?
For a given n, and each interface size m, compare:
* REALIZED: the distinct canonical apex colour sequences (census reading,
i.e. orientation-honest) that Kempe-balanced colourings actually present on
an interface of m apexes -- separately for the outer face (up-tooth apexes)
and an inner face (singleton down-tooth apexes on one face);
* ADMISSIBLE: every colour-permutation-canonical length-m sequence whose three
colour counts share m's parity (the outer-face Kempe-parity necessary
condition).
At n=9 realized == admissible for every m (3..6), and up == down. This probe
checks whether that persists. Summary numbers only -- no notes, no figures.
Run: python3 kempe_interface_admissibility_probe.py --n 12 --m 3 4 5 6
"""
from __future__ import annotations
import argparse
import itertools
import sys
import time
from collections import defaultdict
from full_medial_tire_generator import generate, innermost_bite
from kempe_valid_colorings import classify_colorings
from kempe_up_tooth_sequences import (
canonical_sequence,
dihedral_reading_sequences,
seq_str,
)
def admissible_sequences(m: int) -> set[tuple[int, ...]]:
"""Every canonical length-m sequence with all three colour counts sharing
m's parity (the parity-admissible interface alphabet)."""
out: set[tuple[int, ...]] = set()
for combo in itertools.product((0, 1, 2), repeat=m):
counts = [combo.count(k) for k in (0, 1, 2)]
if all(c % 2 == m % 2 for c in counts):
out.add(canonical_sequence(combo))
return out
def realized(n: int, m: int):
"""(up_set, down_set): census sequences realized on outer / inner interfaces
of exactly m apexes, over all Kempe-balanced colourings of all M(T)."""
up: set[tuple[int, ...]] = set()
down: set[tuple[int, ...]] = set()
up_graphs = down_configs = 0
for g in generate(n, min_up_teeth=3, dedup=True):
do_up = len(g.up_edges) == m
faces = defaultdict(list)
for e in g.singleton_down_edges:
faces[innermost_bite(e, g.bites)].append(e)
down_faces = [sorted(es) for es in faces.values() if len(es) == m]
if not do_up and not down_faces:
continue
valid = [c for c, v in classify_colorings(g, dedup_colors=True) if v.valid]
if do_up:
up_graphs += 1
for c in valid:
up |= dihedral_reading_sequences(n, c, g.up_edges, "u")
for edges in down_faces:
down_configs += 1
for c in valid:
down |= dihedral_reading_sequences(n, c, edges, "d")
return up, down, up_graphs, down_configs
def run(args):
n = args.n
print(f"n={n}: realized gluing-interface vocabulary vs full parity-admissible set\n")
header = f"{'m':>2} {'admiss':>7} | {'up real':>8} {'up=adm':>7} {'#M(T)':>6} | " \
f"{'dn real':>8} {'dn=adm':>7} {'#cfg':>6} | {'up=dn':>6} {'sec':>5}"
print(header)
print("-" * len(header))
for m in args.m:
t0 = time.time()
adm = admissible_sequences(m)
up, down, ng, nc = realized(n, m)
dt = time.time() - t0
print(f"{m:>2} {len(adm):>7} | {len(up):>8} {str(up == adm):>7} {ng:>6} | "
f"{len(down):>8} {str(down == adm):>7} {nc:>6} | "
f"{str(up == down):>6} {dt:>5.0f}")
sys.stdout.flush()
if up != adm:
missing = sorted(seq_str(s) for s in (adm - up))
print(f" up missing {len(missing)}: {missing[:12]}{' ...' if len(missing) > 12 else ''}")
if down != adm:
missing = sorted(seq_str(s) for s in (adm - down))
print(f" down missing {len(missing)}: {missing[:12]}{' ...' if len(missing) > 12 else ''}")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--n", type=int, default=12)
parser.add_argument("--m", type=int, nargs="+", default=[3, 4, 5, 6])
run(parser.parse_args())
if __name__ == "__main__":
main()