Canonicalize tire symmetry and exhaust n=6 level-cycle supports

Quotient (m, k, path, chords, cycle) by the order-2(m+k) dihedral
action on the rung sequence; exhaustive sweep over outer 3..7, inner
7..9 yields 19 distinct supports with a unique floor at 252/732
realised by m=3, k=7, single-chord 6-face.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 10:28:30 -04:00
parent b175f0ab59
commit 57f5c2839a
2 changed files with 670 additions and 0 deletions
@@ -0,0 +1,429 @@
"""Support on genuine level-cycle candidates inside a tire.
Here a tested cycle is not an arbitrary tire boundary. It must be a
bounded face cycle of the inner outerplanar graph O, distinct from
O's outer-face boundary. This models a level/seam cycle as it appears
inside the parent's next-level graph, rather than treating it as an
outer boundary of a tire at the same level.
For each such cycle C, the support is the set of proper 4-colorings of
C that extend to a proper 4-coloring of the parent tire graph.
"""
from __future__ import annotations
import argparse
from functools import lru_cache
from itertools import combinations, permutations, product
from math import comb
COLORS = (0, 1, 2, 3)
def canonical_edge(u: int, v: int) -> tuple[int, int]:
return (u, v) if u < v else (v, u)
def cycle_edges(vertices: list[int]) -> set[tuple[int, int]]:
return {
canonical_edge(vertices[i], vertices[(i + 1) % len(vertices)])
for i in range(len(vertices))
}
def chord_crosses(c1: tuple[int, int], c2: tuple[int, int]) -> bool:
a, b = sorted(c1)
c, d = sorted(c2)
return (a < c < b < d) or (c < a < d < b)
def noncrossing_chord_sets(k: int, max_chords: int | None = None):
candidates = [
(a, b)
for a in range(k)
for b in range(a + 2, k)
if not (a == 0 and b == k - 1)
]
out = [()]
def rec(start: int, chosen: tuple[tuple[int, int], ...]) -> None:
if max_chords is not None and len(chosen) >= max_chords:
return
for idx in range(start, len(candidates)):
ch = candidates[idx]
if any(chord_crosses(ch, old) for old in chosen):
continue
nxt = chosen + (ch,)
out.append(nxt)
rec(idx + 1, nxt)
rec(0, ())
return tuple(sorted(set(out), key=lambda cs: (len(cs), cs)))
def lattice_paths(m: int, k: int):
n = m + k
for outer_positions in combinations(range(n), m):
outer_positions = set(outer_positions)
yield "".join("O" if i in outer_positions else "I" for i in range(n))
def tire_edges(m: int, k: int, path: str, chords: tuple[tuple[int, int], ...]) -> tuple[tuple[int, int], ...]:
outer = list(range(m))
inner = list(range(m, m + k))
edges = set()
edges |= cycle_edges(outer)
edges |= cycle_edges(inner)
for a, b in chords:
edges.add(canonical_edge(inner[a], inner[b]))
edges.add(canonical_edge(outer[0], inner[0]))
i = j = 0
for move in path:
if move == "O":
edges.add(canonical_edge(outer[(i + 1) % m], inner[j % k]))
i += 1
else:
edges.add(canonical_edge(outer[i % m], inner[(j + 1) % k]))
j += 1
return tuple(sorted(edges))
def proper_cycle_colorings(n: int) -> tuple[tuple[int, ...], ...]:
out = []
for colors in product(COLORS, repeat=n):
if all(colors[i] != colors[(i + 1) % n] for i in range(n)):
out.append(colors)
return tuple(out)
@lru_cache(maxsize=None)
def boundary_support(
n_vertices: int,
edges: tuple[tuple[int, int], ...],
boundary_vertices: tuple[int, ...],
) -> frozenset[tuple[int, ...]]:
assigned = {0: 0}
adj = {v: set() for v in range(n_vertices)}
for u, v in edges:
adj[u].add(v)
adj[v].add(u)
remaining = [v for v in range(n_vertices) if v not in assigned]
remaining.sort(key=lambda v: (-len(adj[v]), v))
support = set()
def dfs(index: int) -> None:
if index == len(remaining):
support.add(tuple(assigned[v] for v in boundary_vertices))
return
best_at = index
best_options = None
for pos in range(index, len(remaining)):
v = remaining[pos]
used = {assigned[w] for w in adj[v] if w in assigned}
options = tuple(c for c in COLORS if c not in used)
if best_options is None or len(options) < len(best_options):
best_at = pos
best_options = options
if len(options) <= 1:
break
if not best_options:
return
remaining[index], remaining[best_at] = remaining[best_at], remaining[index]
v = remaining[index]
for color in best_options:
assigned[v] = color
dfs(index + 1)
assigned.pop(v)
remaining[index], remaining[best_at] = remaining[best_at], remaining[index]
dfs(0)
return frozenset(support)
def rotate_tuple(values: tuple[int, ...], shift: int) -> tuple[int, ...]:
n = len(values)
return tuple(values[(i + shift) % n] for i in range(n))
def normalize_support(support: frozenset[tuple[int, ...]]) -> frozenset[tuple[int, ...]]:
out = set()
for state in support:
for perm in permutations(COLORS):
relabeled = tuple(perm[c] for c in state)
for seq in (relabeled, relabeled[::-1]):
for shift in range(len(seq)):
out.add(rotate_tuple(seq, shift))
return frozenset(out)
def describe_chords(chords: tuple[tuple[int, int], ...]) -> str:
return ",".join(f"{a}-{b}" for a, b in chords) if chords else "none"
def canonicalize_tire_cycle(
m: int,
k: int,
path: str,
chords: tuple[tuple[int, int], ...],
cycle: tuple[int, ...],
) -> tuple[str, tuple[tuple[int, int], ...], tuple[int, ...]]:
"""Canonical form of (tire, level cycle) under tire symmetries.
The tire has dihedral symmetry of order 2(m+k) on the rung sequence:
cyclic shift s = "start at rung s of the original path", and reflection
= "traverse in reverse" (the path string reverses, inner cycle direction
flips so chord (a,b) becomes ((k-a) mod k, (k-b) mod k)).
The level cycle is transported by the same rigid relabelling and then
normalized under its own D_n (the support is invariant under D_n on the
cycle's vertex sequence, so this stage is part of the canonical form).
"""
n_rung = m + k
n_cycle = len(cycle)
best = None
for reverse in (False, True):
if reverse:
p_base = path[::-1]
cs_base = tuple(
canonical_edge((k - a) % k, (k - b) % k) for a, b in chords
)
c_base = tuple((k - v) % k for v in reversed(cycle))
else:
p_base = path
cs_base = chords
c_base = cycle
prefix_i = [0]
for ch in p_base:
prefix_i.append(prefix_i[-1] + (1 if ch == "I" else 0))
for s in range(n_rung):
b_s = prefix_i[s]
shifted = p_base[s:] + p_base[:s]
new_cs = tuple(
sorted(
canonical_edge((a - b_s) % k, (b - b_s) % k)
for a, b in cs_base
)
)
new_c = tuple((v - b_s) % k for v in c_base)
norm_c = min(
tuple(orient[(r + i) % n_cycle] for i in range(n_cycle))
for orient in (new_c, new_c[::-1])
for r in range(n_cycle)
)
cand = (shifted, new_cs, norm_c)
if best is None or cand < best:
best = cand
return best
def interval_vertices(vertices: tuple[int, ...], a: int, b: int) -> tuple[int, ...]:
ia = vertices.index(a)
out = [a]
i = ia
while vertices[i] != b:
i = (i + 1) % len(vertices)
out.append(vertices[i])
return tuple(out)
def chord_inside(poly: tuple[int, ...], chord: tuple[int, int]) -> bool:
a, b = chord
return a in poly and b in poly
def polygon_faces(poly: tuple[int, ...], chords: tuple[tuple[int, int], ...]) -> list[tuple[int, ...]]:
"""Cells inside a polygon cut by noncrossing chords."""
usable = [ch for ch in chords if chord_inside(poly, ch)]
split = None
for a, b in usable:
if a not in poly or b not in poly:
continue
ia, ib = poly.index(a), poly.index(b)
distance = abs(ia - ib)
if distance in (1, len(poly) - 1):
continue
split = (a, b)
break
if split is None:
return [poly]
a, b = split
side1 = interval_vertices(poly, a, b)
side2 = interval_vertices(poly, b, a)
rest = tuple(ch for ch in usable if ch != split)
return polygon_faces(side1, rest) + polygon_faces(side2, rest)
def level_cycles_in_o(k: int, chords: tuple[tuple[int, int], ...]) -> tuple[tuple[int, ...], ...]:
"""Bounded face cycles of O that are not the outer boundary of O."""
if not chords:
return ()
outer = tuple(range(k))
faces = polygon_faces(outer, chords)
out = []
for face in faces:
if len(face) == k and set(face) == set(outer):
continue
out.append(face)
return tuple(out)
def enumerate_level_cycle_supports(
n: int,
outer_min: int,
outer_max: int,
inner_min: int,
inner_max: int,
max_chords: int | None,
max_paths: int | None,
canonicalize: bool = True,
progress: bool = False,
) -> tuple[dict[frozenset[tuple[int, ...]], list[dict]], dict]:
supports: dict[frozenset[tuple[int, ...]], list[dict]] = {}
seen_canon: set = set()
stats = {"raw": 0, "canonical": 0}
for m in range(outer_min, outer_max + 1):
for k in range(max(inner_min, n), inner_max + 1):
for chords in noncrossing_chord_sets(k, max_chords):
level_cycles = [cycle for cycle in level_cycles_in_o(k, chords) if len(cycle) == n]
if not level_cycles:
continue
for path_idx, path in enumerate(lattice_paths(m, k)):
if max_paths is not None and path_idx >= max_paths:
break
edges = None
for cycle in level_cycles:
stats["raw"] += 1
if canonicalize:
canon = canonicalize_tire_cycle(m, k, path, chords, cycle)
if canon in seen_canon:
continue
seen_canon.add(canon)
stats["canonical"] += 1
if edges is None:
edges = tire_edges(m, k, path, chords)
boundary = tuple(m + v for v in cycle)
support = normalize_support(boundary_support(m + k, edges, boundary))
meta = {
"m": m,
"k": k,
"path": path,
"chords": chords,
"cycle": cycle,
}
supports.setdefault(support, []).append(meta)
if progress:
print(
f" m={m} k={k} chords={describe_chords(chords)} "
f"raw={stats['raw']} canonical={stats['canonical']} "
f"supports={len(supports)}",
flush=True,
)
return supports, stats
def summarize(
n: int,
supports: dict[frozenset[tuple[int, ...]], list[dict]],
examples: int,
stats: dict | None = None,
) -> None:
all_colorings = len(proper_cycle_colorings(n))
if not supports:
print()
print(f"n={n}: no admissible level-cycle candidates in search window")
return
min_size = min(len(support) for support in supports)
max_size = max(len(support) for support in supports)
min_supports = [support for support in supports if len(support) == min_size]
pair_overlaps = [
len(a & b)
for i, a in enumerate(min_supports)
for b in min_supports[i:]
]
print()
print(f"n={n}")
if stats:
print(f" raw (tire,cycle) combos : {stats['raw']}")
print(f" canonical (tire,cycle) classes: {stats['canonical']}")
print(f" proper C_n colorings : {all_colorings}")
print(f" distinct level-cycle supports : {len(supports)}")
print(f" most restrictive support : {min_size}/{all_colorings} ({len(min_supports)} support types)")
print(f" least restrictive support : {max_size}/{all_colorings}")
print(f" overlap among max-restrict : min {min(pair_overlaps)}, max {max(pair_overlaps)}")
print(" examples:")
printed = 0
for support, metas in sorted(supports.items(), key=lambda item: (len(item[0]), len(item[1]))):
if len(support) != min_size:
continue
for meta in metas[:examples]:
print(
f" size={len(support)} m={meta['m']} k={meta['k']} "
f"path={meta['path']} chords={describe_chords(meta['chords'])} "
f"cycle={meta['cycle']}"
)
printed += 1
if printed >= examples:
return
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--n-min", type=int, default=3)
parser.add_argument("--n-max", type=int, default=7)
parser.add_argument("--outer-min", type=int, default=3)
parser.add_argument("--outer-max", type=int, default=7)
parser.add_argument("--inner-min", type=int, default=3)
parser.add_argument("--inner-max", type=int, default=9)
parser.add_argument("--max-chords", type=int, default=None)
parser.add_argument("--max-paths", type=int, default=None)
parser.add_argument("--examples", type=int, default=4)
parser.add_argument(
"--no-canonicalize",
action="store_true",
help="disable tire automorphism deduplication",
)
parser.add_argument(
"--progress",
action="store_true",
help="emit a one-line update after each (m, k, chord set)",
)
args = parser.parse_args()
print("Level-cycle support inside parent tire inner graph O")
print(" tested cycles: bounded face cycles of O, excluding O's outer boundary")
print(f" outer length range : {args.outer_min}..{args.outer_max}")
print(f" inner O size range : {args.inner_min}..{args.inner_max}")
print(f" max chords : {args.max_chords if args.max_chords is not None else 'all'}")
print(f" max paths/type : {args.max_paths if args.max_paths is not None else 'all'}")
for n in range(args.n_min, args.n_max + 1):
rough_paths = sum(
comb(m + k, m)
for m in range(args.outer_min, args.outer_max + 1)
for k in range(max(args.inner_min, n), args.inner_max + 1)
)
print(f"computing n={n} (rough path count before chords: {rough_paths})")
supports, stats = enumerate_level_cycle_supports(
n,
args.outer_min,
args.outer_max,
args.inner_min,
args.inner_max,
args.max_chords,
args.max_paths,
canonicalize=not args.no_canonicalize,
progress=args.progress,
)
summarize(n, supports, args.examples, stats)
if __name__ == "__main__":
main()
@@ -0,0 +1,241 @@
# Level-Cycle Support Findings
This is the corrected version of the boundary-overlap test.
The key modeling change is that a tested cycle must appear in the
level-cycle/seam role: as a bounded face cycle inside the parent
tire's inner outerplanar graph `O`, not as an arbitrary `B_out` or as
the outer-face boundary of `O`.
Executable:
```bash
python3 papers/coloring_nested_tire_graphs/experiments/level_cycle_support.py
```
## Model
For a parent tire `T = (B_out, O, E_ann)`, enumerate simple bounded
face cycles of `O`.
A cycle is admissible only if:
- it is a bounded face cycle of `O`;
- it is distinct from the outer-face boundary of `O`;
- it is tested in this inner/next-level role, never as an outer
boundary of a tire at the same level.
For each admissible cycle `C`, compute the set of proper 4-colorings
of `C` that extend to a proper 4-coloring of the parent tire graph.
Supports are normalized under color relabeling and dihedral symmetries
of `C`.
## Capped Validation Run
Command:
```bash
python3 -u papers/coloring_nested_tire_graphs/experiments/level_cycle_support.py \
--n-min 3 --n-max 6 --outer-max 5 --inner-max 7 --max-chords 2 --max-paths 30
```
Result:
```text
n=3
proper C_n colorings : 24
distinct level-cycle supports : 1
most restrictive support : 24/24
overlap among max-restrict : 24
n=4
proper C_n colorings : 84
distinct level-cycle supports : 3
most restrictive support : 60/84
overlap among max-restrict : 60
n=5
proper C_n colorings : 240
distinct level-cycle supports : 2
most restrictive support : 120/240
overlap among max-restrict : 120
n=6
proper C_n colorings : 732
distinct level-cycle supports : 13
most restrictive support : 252/732
overlap among max-restrict : 252
```
The most restrictive admissible `n=4` level-cycle support found here
has size `60/84`, and there is only one worst support type in this
capped window.
Representative `n=4` admissible witness:
```text
size=60
m=3, k=5
path=OOOIIIII
chords=0-2
cycle=(2, 3, 4, 0)
```
Here `O` is a 5-cycle with chord `0-2`. The tested 4-cycle is the
bounded face cycle `(2,3,4,0)`, not the outer-face boundary of `O`.
## Current Interpretation
With level-cycle admissibility enforced, the first-pass data no longer
supports a zero-overlap obstruction at `n=4`. In the capped search,
the maximum-restriction support type is unique for `n=3,4,5,6`, so
overlap among maximum restrictions is trivially the whole worst
support.
This is not yet a proof. The exact all-path `n=4` run with
`--max-chords 2` was stopped after it ran too long interactively. The
next implementation step should canonicalize outerplanar `O` and
annular paths, or switch to a transfer/DP support computation, before
claiming exhaustive results beyond small capped windows.
## Canonical Tire Quotient
The script now quotients out the dihedral symmetry of the tire on the
rung sequence. Concretely, a tire `T = (m, k, path, chords)` together
with a chosen level cycle `C` admits an order-`2(m+k)` action:
- cyclic shift `s`: "start counting from rung `s` of the original path".
This rotates the inner labelling by `-b_s mod k`, where `b_s` is the
number of inner steps in the first `s` moves; chords and `C` are
carried by the same shift.
- reflection: traverse the tire in the opposite direction. The path
string reverses and each inner label `j` flips to `(k - j) mod k`,
taking chord `(a, b)` to `((k - a) mod k, (k - b) mod k)` and reversing
`C`.
The canonical form of `(T, C)` is the lex-min `(path, chords, cycle)`
under that action, with `C` itself further minimised under its own
`D_n` (which the support already quotients out, so this stage is free).
Two `(T, C)` pairs in the same canonical class produce identical
normalised supports, so we compute the support once per class. Sanity
checks at `n = 3, 4, 5` reproduce the prior capped support sizes
(`24/24`, `60/84`, `120/240`) exactly while reducing raw work by
roughly 20×–25×.
## Exhaustive `n=6` Run
With the canonical quotient in place we can now exhaust a meaningful
window for `n = 6`. The smallest admissible inner ring is `k = 7`
(a `k`-cycle plus chords cannot host a non-outer bounded `6`-face for
`k < 7`).
Command:
```bash
python3 -u papers/coloring_nested_tire_graphs/experiments/level_cycle_support.py \
--n-min 6 --n-max 6 --outer-min 3 --outer-max 6 --inner-min 7 --inner-max 8 \
--progress --examples 6
```
Result:
```text
n=6
raw (tire,cycle) combos : 238506
canonical (tire,cycle) classes: 9144
proper C_6 colorings : 732
distinct level-cycle supports : 17
most restrictive support : 252/732 (1 support type)
least restrictive support : 732/732
overlap among max-restrict : 252
```
Compared to the earlier `--outer-max 5 --inner-max 7 --max-chords 2
--max-paths 30` capped run, the distinct-support count climbs from
`13` to `17`, but the **minimum support size is unchanged at `252/732`
and is still realised by a unique support type**. The minimum is
witnessed by the same minimal-`O` configuration that already showed up
at smaller `n`:
```text
size=252
m=3, k=7
chords=0-2
cycle=(2, 3, 4, 5, 6, 0)
```
i.e. the bounded `6`-face of a `7`-cycle with one chord, sitting inside
the thinnest annulus `(m=3)`. Adding outer length (`m` up to `6`),
inner length (`k` up to `8`), and unrestricted chord sets produces
strictly *less* restrictive supports — never a new floor.
### Wider sweep: `outer 3..7, inner 7..9`
Command:
```bash
python3 -u papers/coloring_nested_tire_graphs/experiments/level_cycle_support.py \
--n-min 6 --n-max 6 --outer-min 3 --outer-max 7 --inner-min 7 --inner-max 9 \
--progress --examples 6
```
Result:
```text
n=6
raw (tire,cycle) combos : 5662518
canonical (tire,cycle) classes: 186818
proper C_6 colorings : 732
distinct level-cycle supports : 19
most restrictive support : 252/732 (1 support type)
least restrictive support : 732/732
overlap among max-restrict : 252
```
Adding `k = 9` and `m = 7` brings 2 new distinct supports (`17 → 19`),
**both strictly above the floor**. The most restrictive support is
**still `252/732`, still unique, and still realised by the same
minimal-`O` witness** (`m=3, k=7, chord=0-2, cycle=(2,3,4,5,6,0)`).
The `m = 3` configurations dominated the floor in every (`m`, `k`)
sweep examined.
### What this changes
- The "most restrictive support is unique" claim for `n = 6` is now
backed by an exhaustive (not capped) sweep over `m ∈ [3, 7]`,
`k ∈ [7, 9]`, all chord sets, all annular paths.
- The earlier-feared zero-overlap obstruction at `n = 4` does not
reappear at `n = 6` either: overlap among maximum-restriction
supports is trivially the whole worst support, because there is only
one worst support.
- The witness for the floor is structurally the same across
`n = 3, 4, 5, 6`: a single chord on a `(n+1)`-cycle, embedded in the
thinnest annulus, with the tested cycle taken as the bounded
`n`-face. That stability is suggestive (it hints that the worst
level-cycle support is governed by a small canonical configuration)
but is not yet a theorem.
### Floor-stability-in-`m` conjecture
Across every searched `n`, the floor witness has been `m = 3`. This
is consistent with a **support-monotonicity** picture: subdividing any
outer or annular edge of an `m = 3` floor witness produces an `m = 4`
tire whose support strictly contains the original (the new degree-`2`
vertex adds coloring freedom, and the subdivided edge's endpoints are
no longer adjacent, so they may share a color). By induction the
floor over `m ≥ 3` is bounded above by the `m = 3` floor.
The empirical run confirms this bound is tight up to `m ≤ 7, k ≤ 9` at
`n = 6`: no `m ≥ 4` tire produced a support strictly smaller than the
`m = 3` floor. A theorem-style statement would be:
> **(Conjecture)** For every `n`, every `k ≥ n + 1`, and every chord
> set on `C_k` containing an `n`-face, the level-cycle support floor
> over all annular paths and all `m ≥ 3` is achieved at `m = 3`.
If true, this collapses the exhaustive search to `m = 3` only and
opens the door to closed-form support computation for the worst case.
The obstruction to a quick proof is exotic `m ≥ 4` configurations not
reachable by subdivision from an `m = 3` floor witness — none has
appeared empirically, but their non-existence is not yet ruled out.