coloring_nested_tire_graphs: SR + PDS chain consistency holds robustly

Redo step 2 and step 3 under SR (no chord effect, the correct model
under PDS where O-faces are not G-faces).

Step 2 (pairwise, 14 cases): all compatible. T_1's γ-side projection
saturates {1,2,3}^γ under outward PDS (m_1 ≥ γ from step 1), so
intersection = T_2's projection, always nonempty.

Step 3 (chain consistency, 10 chains up to 6 tires): all compatible.
Forward propagation along the chain shows monotonically growing
support sizes (roughly 3x per step), never empties. Free choice
accumulates outward.

Implication: chain consistency under SR + PDS is essentially
automatic for "open" chains. The remaining gap is the boundary
condition at the outermost level (e.g., the outer triangle of a
triangulated sphere has only 6 valid σ-permutations); whether the
forward state always contains one of those is the next experiment.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-26 12:15:41 -04:00
parent 570de6a171
commit a5332ab656
2 changed files with 286 additions and 0 deletions
@@ -0,0 +1,254 @@
"""Chain pigeonhole under SR + PDS, no chord constraints.
Under the correct PDS modelling, each tire's T'_{f'} has γ inner-spoke
pendants regardless of O's chord structure (chord edges of O become
G'-edges between inner-spoke vertices, but those edges are NOT in
T'_{f'} since neither endpoint is in V(f')). So the only inputs are
cycle lengths (m, k) per tire.
For each tire T with B_out length m and B_in length k:
Π_T ⊆ {1,2,3}^m × {1,2,3}^k
= { (σ_U, σ_D) : σ comes from a proper edge 3-colouring of C_{m+k} }
Step 2 (pairwise): for adjacent tires T_1, T_2 sharing γ, do
π_U(T_1)|_γ and π_D(T_2)|_γ overlap? By step-1 saturation, yes —
T_1's γ-side saturates {1,2,3}^γ when m_1 ≥ |γ|, etc.
Step 3 (chain consistency): chain T_1 | T_2 | ... | T_n. σ at each
shared cycle must be jointly consistent. Propagate forward via the
joint supports and check if state ever empties.
"""
from __future__ import annotations
import numpy as np
from itertools import product
from tire_fiber_chords import d_positions_for, u_positions_for
from tire_fiber_chords_fast import proper_cycle_colorings, induced_sigma_vec
_pi_cache: dict = {}
def joint_support_SR(m: int, k: int) -> set[tuple[tuple[int, ...], tuple[int, ...]]]:
"""Π_T = {(σ_U, σ_D)} pairs for SR tire (no chord)."""
key = (m, k)
if key in _pi_cache:
return _pi_cache[key]
n = m + k
d_pos = d_positions_for(m, k)
u_pos = u_positions_for(m, k)
c = proper_cycle_colorings(n)
sigma = induced_sigma_vec(c)
sigma_u = sigma[:, u_pos]
sigma_d = sigma[:, d_pos]
pairs = set(zip(map(tuple, sigma_u.tolist()),
map(tuple, sigma_d.tolist())))
_pi_cache[key] = pairs
return pairs
# --- Step 2 ---
def step2_SR(gamma: int, m_1: int, k_2: int) -> dict:
"""T_1 has (m=m_1, k=γ). T_2 has (m=γ, k=k_2)."""
pi1 = joint_support_SR(m_1, gamma)
pi2 = joint_support_SR(gamma, k_2)
sd1 = {p[1] for p in pi1} # γ-projection from T_1's side
su2 = {p[0] for p in pi2} # γ-projection from T_2's side
fwd = sd1 & su2
rev = sd1 & {s[::-1] for s in su2}
return {
'γ': gamma, 'm_1': m_1, 'k_2': k_2,
'|sd1|': len(sd1), '|su2|': len(su2), '3^γ': 3**gamma,
'sd1_saturates': len(sd1) == 3**gamma,
'su2_saturates': len(su2) == 3**gamma,
'fwd': len(fwd), 'rev': len(rev),
'compatible': bool(fwd or rev),
}
# --- Step 3 ---
def step3_chain(chain: list[tuple[int, int]]) -> dict:
"""Forward-propagate joint supports along a tire chain.
chain[i] = (m_i, k_i) for tire T_i. Adjacency requires k_{i+1} = m_i
(T_{i+1}'s B_in = T_i's B_out = shared γ).
State at step i = set of σ at the "current" shared cycle γ_i = L_i,
which is reachable through T_1, ..., T_i.
Returns: compatibility result and the state size trajectory.
"""
# Validate adjacency
for i in range(len(chain) - 1):
if chain[i][0] != chain[i + 1][1]:
return {'error': f'chain adjacency mismatch at i={i}: '
f'T_{i+1}.m={chain[i][0]} != T_{i+2}.k={chain[i+1][1]}'}
pis = [joint_support_SR(m, k) for (m, k) in chain]
# Initial state: σ at L_1 from T_1's σ_U-projection (any σ_D ok since
# the innermost boundary L_0 has no further constraint).
state = {p[0] for p in pis[0]}
trajectory = [len(state)]
for i in range(1, len(chain)):
new_state = set()
pi = pis[i]
# T_i has B_in = L_i, B_out = L_{i+1}. pair = (σ at L_{i+1}, σ at L_i).
# For each pair, if σ at L_i in current state, add σ at L_{i+1} to new state.
for sigma_outer, sigma_inner in pi:
if sigma_inner in state:
new_state.add(sigma_outer)
trajectory.append(len(new_state))
if not new_state:
return {
'chain': chain,
'compatible': False,
'failed_at_tire_index': i,
'trajectory': trajectory,
}
state = new_state
return {
'chain': chain,
'compatible': True,
'final_state_size': len(state),
'trajectory': trajectory,
}
def step3_chain_with_reflections(chain: list[tuple[int, int]]) -> dict:
"""Like step3_chain but also tries flipping orientation at each
junction (since adjacent tires may have reversed γ-orientations
in the actual plane embedding).
More expensive: 2^(len(chain)-1) orientation choices."""
n = len(chain)
pis = [joint_support_SR(m, k) for (m, k) in chain]
# Try every combination of orientation flips at junctions.
# flip[i] = True means flip σ at L_{i+1} when bridging T_i → T_{i+1}.
# Equivalent: reverse the σ_D of T_{i+1} before matching.
best = None
for flips in product([False, True], repeat=n - 1):
state = {p[0] for p in pis[0]}
traj = [len(state)]
ok = True
for i in range(1, n):
pi = pis[i]
new_state = set()
if flips[i - 1]:
# Reverse σ at L_i (the boundary between T_{i-1} and T_i)
state_match = state
for sigma_outer, sigma_inner in pi:
if sigma_inner[::-1] in state_match:
new_state.add(sigma_outer)
else:
for sigma_outer, sigma_inner in pi:
if sigma_inner in state:
new_state.add(sigma_outer)
traj.append(len(new_state))
if not new_state:
ok = False
break
state = new_state
if ok:
if best is None or len(state) > best['final_state_size']:
best = {
'chain': chain,
'compatible': True,
'final_state_size': len(state),
'trajectory': traj,
'flips': flips,
}
if best is not None:
return best
return {
'chain': chain,
'compatible': False,
'tried_orientations': 2 ** (n - 1),
}
# --- Test scenarios ---
PAIRWISE_CASES = [
# (γ, m_1, k_2): T_1 has B_in=γ; T_2 has B_out=γ.
(3, 3, 3),
(4, 4, 3),
(4, 4, 4),
(5, 5, 3),
(5, 6, 5),
(6, 6, 3),
(6, 6, 4),
(6, 6, 5),
(6, 6, 6),
(8, 8, 4),
(8, 8, 6),
(8, 10, 8),
(10, 10, 8),
(12, 12, 6),
]
# 3-tire and longer chains.
# Each tuple (m, k) -- adjacent (m_i, k_i), (m_{i+1}, k_{i+1}) need k_{i+1} = m_i.
CHAIN_CASES = [
# Strictly outward-growing PDS:
[(4, 3), (5, 4), (6, 5)],
[(4, 3), (6, 4), (8, 6)],
[(5, 4), (6, 5), (7, 6)],
[(6, 3), (8, 6), (10, 8)],
# Stalled growth:
[(4, 3), (5, 4), (5, 5)],
[(4, 3), (4, 4), (5, 4)],
# Shrinking (anti-PDS, for stress):
[(3, 4), (3, 3)], # would need k_2=m_1=3 -- ok
# Longer chains:
[(4, 3), (5, 4), (6, 5), (7, 6)],
[(4, 3), (5, 4), (6, 5), (7, 6), (8, 7)],
[(4, 3), (5, 4), (6, 5), (7, 6), (8, 7), (9, 8)],
]
def main():
print("=" * 80)
print("Step 2 under SR + PDS (no chord)")
print("=" * 80)
print(f"{'γ':>3} {'m_1':>4} {'k_2':>4} {'|sd1|':>6} {'|su2|':>6} {'3^γ':>7} "
f"{'sat1':>5} {'sat2':>5} {'fwd':>5} {'rev':>5} compat")
for γ, m_1, k_2 in PAIRWISE_CASES:
r = step2_SR(γ, m_1, k_2)
sat1 = "" if r['sd1_saturates'] else "·"
sat2 = "" if r['su2_saturates'] else "·"
ok = "YES" if r['compatible'] else "NO"
print(f"{γ:>3} {m_1:>4} {k_2:>4} {r['|sd1|']:>6} {r['|su2|']:>6} {r['3^γ']:>7} "
f"{sat1:>5} {sat2:>5} {r['fwd']:>5} {r['rev']:>5} {ok}")
print()
print("=" * 80)
print("Step 3: chain consistency (3+ tires) under SR + PDS")
print("=" * 80)
for chain in CHAIN_CASES:
adj_ok = all(chain[i][0] == chain[i + 1][1] for i in range(len(chain) - 1))
adj = "OK" if adj_ok else "(adjacency mismatch)"
if not adj_ok:
print(f" SKIP {chain}: {adj}")
continue
# Use the orientation-checking version
r = step3_chain_with_reflections(chain)
chain_str = " | ".join(f"({m},{k})" for (m, k) in chain)
if r.get('compatible'):
print(f" {chain_str}: COMPATIBLE, final state size {r['final_state_size']}, "
f"trajectory {r['trajectory']}")
else:
failed = r.get('failed_at_tire_index', '?')
print(f" {chain_str}: **INCOMPATIBLE** (failed at tire index {failed})")
if __name__ == '__main__':
main()
@@ -0,0 +1,32 @@
================================================================================
Step 2 under SR + PDS (no chord)
================================================================================
γ m_1 k_2 |sd1| |su2| 3^γ sat1 sat2 fwd rev compat
3 3 3 27 27 27 ✓ ✓ 27 27 YES
4 4 3 81 78 81 ✓ · 78 78 YES
4 4 4 81 81 81 ✓ ✓ 81 81 YES
5 5 3 243 171 243 ✓ · 171 171 YES
5 6 5 243 243 243 ✓ ✓ 243 243 YES
6 6 3 729 396 729 ✓ · 396 396 YES
6 6 4 729 549 729 ✓ · 549 549 YES
6 6 5 729 726 729 ✓ · 726 726 YES
6 6 6 729 729 729 ✓ ✓ 729 729 YES
8 8 4 6561 2943 6561 ✓ · 2943 2943 YES
8 8 6 6561 5601 6561 ✓ · 5601 5601 YES
8 10 8 6561 6561 6561 ✓ ✓ 6561 6561 YES
10 10 8 59049 53049 59049 ✓ · 53049 53049 YES
12 12 6 531441 160503 531441 ✓ · 160503 160503 YES
================================================================================
Step 3: chain consistency (3+ tires) under SR + PDS
================================================================================
(4,3) | (5,4) | (6,5): COMPATIBLE, final state size 714, trajectory [78, 234, 714]
(4,3) | (6,4) | (8,6): COMPATIBLE, final state size 4914, trajectory [78, 540, 4914]
(5,4) | (6,5) | (7,6): COMPATIBLE, final state size 2172, trajectory [240, 720, 2172]
(6,3) | (8,6) | (10,8): COMPATIBLE, final state size 46074, trajectory [396, 4410, 46074]
(4,3) | (5,4) | (5,5): COMPATIBLE, final state size 240, trajectory [78, 234, 240]
(4,3) | (4,4) | (5,4): COMPATIBLE, final state size 234, trajectory [78, 78, 234]
(3,4) | (3,3): COMPATIBLE, final state size 27, trajectory [27, 27]
(4,3) | (5,4) | (6,5) | (7,6): COMPATIBLE, final state size 2160, trajectory [78, 234, 708, 2160]
(4,3) | (5,4) | (6,5) | (7,6) | (8,7): COMPATIBLE, final state size 6528, trajectory [78, 234, 714, 2160, 6528]
(4,3) | (5,4) | (6,5) | (7,6) | (8,7) | (9,8): COMPATIBLE, final state size 19644, trajectory [78, 234, 714, 2160, 6516, 19644]