coloring_nested_tire_graphs: chain half analysis + tree DP empirical test

NEW NOTE: chain_half_analysis.tex (4 pages).

Formulates the chain half of the loose conjecture as a tree DP
over the cut tire forest, identifies what's proven vs. open:

PROVEN
- Tree structure (high-side forest): from
  cut_tire_tree_structure.tex
- S_3-equivariance of the DP: trivial Lemma in this note
- Per-tire half for spoke-only cut tires (n ≥ 3): Prop 1.13

OPEN / GAPS DISCOVERED
1. Cut tires are NOT in general spoke-only. H_d can have degree-3
   vertices (= branch points), making face boundaries non-simple
   cycles.  Dodecahedron 6-edge cut yields H_1 with one face of
   length 20 over only 11 distinct vertices.  Prop 1.13's count
   2^n + 2(-1)^n applies only to spoke-only tires.

2. OUT-only projection loses S_3 orbit info.  The per-tire half
   guarantees a full S_3 orbit on the JOINT (in + out) projection,
   but restricting to OUT spokes can collapse to |A|=3 (constant
   tuples). Empirically observed ~20% of the time on test cases.
   Correct DP must track joint projection (analog of
   tire_fiber_step2.tex's joint-support tracking).

3. Non-emptiness preservation through the DP is the genuine open
   piece (Conj. in this note + Strong per-tire extendibility).

EMPIRICAL TESTS
- chain_dp_test.py: simple cycle DP (assumes spoke-only).
- chain_dp_general.py: handles branched faces via brute-force
  3-edge-coloring enumeration (cut off at 12 edges/tire for
  tractability).
- chain_dp_debug.py: diagnostic for inspecting H_d face structure.

The general test reveals all three gaps above when run on
Dodecahedron + HM #0.  Cross-cut R_0 ∩ R_1 should be non-empty
for both (they are 3-edge-colorable), but the heuristic
parent-finding plus OUT-only projection produce false negatives.

Status table at end of note summarizes what's needed to close.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-26 22:49:20 -04:00
parent 415c33cfc3
commit 203b005336
7 changed files with 1414 additions and 0 deletions
@@ -0,0 +1,79 @@
"""Debug the chain DP: examine the structure of cut tires on a small
example to understand why R_i is empty.
"""
import os
import sys
import itertools
from collections import defaultdict
from sage.all import Graph, graphs
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from cut_depth_label import (
parse_planar_code, HM_FILE, find_six_edge_cut,
apply_procedure, compute_nice_layout,
)
from cut_tire_tree import build_tree
from tree_structure_sweep import all_six_edge_cuts
def examine_cut(G, cut_idx, max_cuts=10):
cuts = all_six_edge_cuts(G, max_cuts=max_cuts)
if cut_idx >= len(cuts):
return
S, cut_edges = cuts[cut_idx]
base_pos = compute_nice_layout(G)
S0 = S
S1 = frozenset(G.vertices()) - S
print(f'=== Cut #{cut_idx}: |S0|={len(S0)}, |S1|={len(S1)} ===')
for side, S_side in [('0', S0), ('1', S1)]:
print(f'\n--- Side {side} ---')
try:
H, pos, ed, _, _, _ = apply_procedure(
G, S_side, cut_edges, base_pos, side,
pendant_start_id=max(G.vertices()) + 1 +
(0 if side == '0' else 200))
except Exception as e:
print(f' apply failed: {e}')
continue
# Depth distribution
from collections import Counter
depth_count = Counter(ed.values())
print(f' edge depths: {dict(depth_count)}')
print(f' |H| = {H.order()} vertices, {H.size()} edges')
print(f' max depth: {max(ed.values()) if ed else "N/A"}')
if not ed:
continue
faces_by_depth, parent_of, _ = build_tree(H, ed)
for d in sorted(faces_by_depth.keys()):
faces = faces_by_depth[d]
print(f' H_{d}: {len(faces)} faces')
for f_idx, face in enumerate(faces[:3]):
verts = set()
for u, v in face:
verts.add(u)
verts.add(v)
# For each vertex, count incident H edges
bdry_v_data = []
for v in verts:
incident = list(H.neighbors(v))
incident_depths = [ed[tuple(sorted((v, nb)))]
for nb in incident]
bdry_v_data.append((v, incident_depths))
# Show first few
print(f' face {f_idx}: |bdry|={len(face)}, '
f'verts={len(verts)}, sample: {bdry_v_data[:3]}')
def main():
G = graphs.DodecahedralGraph()
G.is_planar(set_embedding=True)
examine_cut(G, cut_idx=2, max_cuts=5)
if __name__ == '__main__':
main()
@@ -0,0 +1,358 @@
"""Generalized chain DP for cut tires (handles branched H_d faces).
A cut tire T_d^{(f)} is treated as:
- Edge set E_f = edges in the boundary walk of face f of H_d.
- Vertex set V_f = vertices incident to E_f.
- For each v in V_f with deg_{H_d}(v) = 2: one labeled pendant
(in or out depending on the depth of the non-H_d edge at v).
- For each v in V_f with deg_{H_d}(v) = 3: no pendant.
Proper 3-edge-coloring of T = (E_f pendants) such that all edges
incident at every v in V_f have distinct colors. (The "underlying"
graph treats E_f and pendants as edges in a planar multigraph; a
branch vertex has 3 incident E_f edges all distinct.)
Out-spokes projection: tuple of colors on labeled-OUT pendants
(depth d-1 edges).
Chain DP bottom-up:
- At each tire, enumerate proper 3-edge-colorings.
- Restrict to those compatible with at least one combination
of out-spoke colorings from each child.
- Project onto own out spokes.
Verify:
(a) At each tire: |A(T)| > 0, contains full S_3 orbit.
(b) R_0 ∩ R_1 non-empty at the cut.
"""
import os
import sys
import itertools
from collections import defaultdict, Counter
from sage.all import Graph, graphs
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from cut_depth_label import (
parse_planar_code, HM_FILE,
apply_procedure, compute_nice_layout,
)
from cut_tire_tree import build_tree
from tree_structure_sweep import all_six_edge_cuts
def cut_tire_structure(H, edge_depth, d, face):
"""Extract structure of cut tire T_d^{(face)}.
Returns dict:
'edges_indexed': list of (e_id, edge_tuple) for cycle edges.
Order: by appearance in face walk (so duplicates
can happen for branch points, but we use each
edge once).
'pendants_in': dict v -> (pendant_id, depth=d+1 edge)
'pendants_out': dict v -> (pendant_id, depth=d-1 edge)
'edges_at_v': dict v -> list of edge_ids incident to v
(each appearance of v in walk = 1 cycle edge)
'cycle_edges': list of (cycle_edge_id, edge_tuple)
"""
# First normalize: count each unique cycle edge once
cycle_e_set = set()
cycle_edges = []
for u, v in face:
e = tuple(sorted((u, v)))
if e not in cycle_e_set:
cycle_e_set.add(e)
cycle_edges.append(e)
# Vertices on this face boundary
verts_set = set()
for u, v in cycle_edges:
verts_set.add(u)
verts_set.add(v)
# Edges incident to each vertex (cycle edges only)
edges_at_v = defaultdict(list)
for ce_id, (a, b) in enumerate(cycle_edges):
edges_at_v[a].append(ce_id)
edges_at_v[b].append(ce_id)
# Pendants: at each degree-2 vertex of H, identify the third edge
pendants_in = {}
pendants_out = {}
pendant_counter = [len(cycle_edges)] # next available edge id
for v in verts_set:
H_deg = sum(1 for nb in H.neighbors(v)
if tuple(sorted((v, nb))) in cycle_e_set)
# Wait — that's just degree restricted to face cycle edges.
# But pendant attaches to the H-edge of different depth.
# Look at ALL edges of H incident to v:
for nb in H.neighbors(v):
e_full = tuple(sorted((v, nb)))
d_e = edge_depth.get(e_full)
if d_e is None:
continue
if d_e == d:
continue # skip cycle edges
elif d_e == d + 1:
pid = pendant_counter[0]
pendant_counter[0] += 1
pendants_in[v] = (pid, e_full)
edges_at_v[v].append(pid)
elif d_e == d - 1:
pid = pendant_counter[0]
pendant_counter[0] += 1
pendants_out[v] = (pid, e_full)
edges_at_v[v].append(pid)
return {
'cycle_edges': cycle_edges,
'pendants_in': pendants_in,
'pendants_out': pendants_out,
'edges_at_v': dict(edges_at_v),
'n_edges': pendant_counter[0],
}
def proper_3_colorings_general(struct):
"""Enumerate all proper 3-edge-colorings of cut tire.
Edge ids 0..n_cycle_edges-1 are cycle edges; n_cycle...n_edges-1 are
pendants. Constraint: at each vertex v, the colors of edges in
edges_at_v[v] are all distinct."""
n_e = struct['n_edges']
edges_at_v = struct['edges_at_v']
# Edge-coloring constraint reduces to: per vertex, colors distinct.
# Since vertex can have ≤ 3 incident edges in cubic (since each
# vertex in original G' is cubic), and our cut tire is subgraph,
# each v has at most 3 incident edges in the cut tire.
# We need all colors at each v distinct.
out = []
# Naive enumeration: 3^n_e then filter. n_e is small (~10-50)
# for our test cases, so doable.
if n_e > 12:
return None # too big
for assign in itertools.product([0, 1, 2], repeat=n_e):
ok = True
for v, eids in edges_at_v.items():
colors = [assign[eid] for eid in eids]
if len(set(colors)) != len(colors):
ok = False
break
if ok:
out.append(assign)
return out
def s3_orbit(tup):
"""S_3 orbit of color tuple under permutations of {0,1,2}."""
orbit = set()
for perm in itertools.permutations([0, 1, 2]):
orbit.add(tuple(perm[c] for c in tup))
return orbit
def has_full_s3_orbit(s):
"""Does set s of tuples contain a 6-element S_3 orbit?"""
if not s:
return False
for tup in s:
orb = s3_orbit(tup)
if len(orb) == 6 and orb <= s:
return True
return False
def chain_dp_general(faces_by_depth, parent_of, H, edge_depth):
"""Run chain DP, returns dict node -> A(T) (set of out-spoke
color tuples)."""
# Build structures for each tire
structs = {}
for d, faces in faces_by_depth.items():
for f_idx, face in enumerate(faces):
structs[(d, f_idx)] = cut_tire_structure(H, edge_depth, d, face)
# Build children list
children = defaultdict(list)
for node, p in parent_of.items():
if p is not None and p != ('cut', None):
children[p].append(node)
# Order by depth (process leaves first)
max_d = max(d for (d, _) in structs)
A = {}
for d in range(max_d, 0, -1):
for (dd, f_idx), struct in list(structs.items()):
if dd != d:
continue
node = (d, f_idx)
cs = proper_3_colorings_general(struct)
if cs is None:
A[node] = None # too big
continue
# Pendant-IN edge ids: those whose pendant_id is in pendants_in
in_pids = {v: pid for v, (pid, _) in struct['pendants_in'].items()}
out_pids = {v: pid for v, (pid, _) in struct['pendants_out'].items()}
in_v_sorted = sorted(in_pids.keys())
out_v_sorted = sorted(out_pids.keys())
# Find correspondence: each pendant-IN edge of this tire
# = full edge in G'_i. We need to find which child cycle
# edge it corresponds to.
# The pendant-IN at v has full edge e (in struct).
# Among children, find child c whose cycle_edges contains e.
child_links = [] # (child_node, parent_v -> child_edge_id)
for ch in children.get(node, []):
if ch not in A:
continue
if A[ch] is None:
child_links.append((ch, None)) # too big, skip
continue
ch_struct = structs[ch]
ch_out_v = sorted(ch_struct['pendants_out'].keys())
ch_out_full = {v: ch_struct['pendants_out'][v][1]
for v in ch_out_v}
# Match: parent's pendant-IN edge = child's pendant-OUT edge?
# No wait — parent's pendant-IN is a depth-(d+1) edge.
# Child T_{d+1}^{(f')} has cycle edges of depth d+1.
# So parent's pendant-IN edge = a cycle edge of child.
p_v_to_child_cycle_eid = {}
for v_p, (pid_p, e_full_p) in struct['pendants_in'].items():
# Find edge in child's cycle_edges with same e_full
for ce_id, e_c in enumerate(ch_struct['cycle_edges']):
if e_c == e_full_p:
p_v_to_child_cycle_eid[v_p] = ce_id
break
child_links.append((ch, p_v_to_child_cycle_eid))
# For each coloring, check compatibility with each child
achievable = set()
for assign in cs:
# Determine parent's pendant-IN colors
p_in_colors = {v: assign[in_pids[v]] for v in in_pids}
# For each child, check if there's a child-coloring with
# the same colors at the corresponding cycle edges.
# We use child's A in terms of OUT-SPOKE colors, but we
# need to constrain CYCLE colors. So we need to recompute
# per-child compatibility from child's allowed colorings.
# ALTERNATE: instead of using A(ch) (out-spoke restriction),
# track full child colorings. This blows up but is correct.
# Simpler: track child's set of valid (cycle, out) colorings.
# For now we use the WEAKER check: out-spoke compatibility
# via "any child coloring at all", since A(ch) doesn't
# carry cycle-edge info.
# Since the chain compatibility is on PARENT'S IN spokes =
# CHILD'S CYCLE EDGES (not out spokes), we actually need
# the child's projection on cycle edges, not out spokes.
# For this test, let's use child's full coloring set.
ok = True
for ch, link in child_links:
if link is None:
# child too big — skip compatibility check
continue
ch_full_colorings = proper_3_colorings_general(structs[ch])
if ch_full_colorings is None:
continue
# Find a ch_coloring with matching cycle-edge colors
found = False
for ch_assign in ch_full_colorings:
match = True
for v_p, ce_id_c in link.items():
if ch_assign[ce_id_c] != p_in_colors[v_p]:
match = False
break
if match:
found = True
break
if not found:
ok = False
break
if ok:
# Project to out spokes
if out_v_sorted:
tup = tuple(assign[out_pids[v]] for v in out_v_sorted)
else:
tup = ()
achievable.add(tup)
A[node] = achievable
return A, structs
def test_on_graph(G, name, max_cuts=3):
cuts = all_six_edge_cuts(G, max_cuts=max_cuts)
print(f'\n=== {name}: V={G.order()}, {len(cuts)} cuts tested ===',
flush=True)
base_pos = compute_nice_layout(G)
n_tests, n_pertire_orbit_ok, n_root_ok, n_cross_ok = 0, 0, 0, 0
per_tire_failures = []
cross_failures = []
for cut_idx, (S, cut_edges) in enumerate(cuts):
S0, S1 = S, frozenset(G.vertices()) - S
if len(S0) < 4 or len(S1) < 4:
continue
Rs = {}
for side, S_side in [('0', S0), ('1', S1)]:
try:
H, pos, ed, _, _, _ = apply_procedure(
G, S_side, cut_edges, base_pos, side,
pendant_start_id=max(G.vertices()) + 1 +
(0 if side == '0' else 200))
except Exception:
continue
if not ed:
continue
faces_by_depth, parent_of, _ = build_tree(H, ed)
try:
A, structs = chain_dp_general(
faces_by_depth, parent_of, H, ed)
except Exception as e:
print(f' cut #{cut_idx}/{side}: DP failed: {e}', flush=True)
continue
# Per-tire check
for node, a in A.items():
if a is None:
continue
n_tests += 1
if a and has_full_s3_orbit(a):
n_pertire_orbit_ok += 1
else:
per_tire_failures.append({
'cut': cut_idx, 'side': side, 'node': node,
'n_edges': structs[node]['n_edges'],
'|A|': len(a),
'has_orbit': has_full_s3_orbit(a) if a else False,
})
# Roots
R_i = set()
for node in A:
if node[0] == 1 and A[node]:
R_i |= A[node]
if R_i and has_full_s3_orbit(R_i):
n_root_ok += 1
Rs[side] = R_i
if '0' in Rs and '1' in Rs:
if Rs['0'] & Rs['1']:
n_cross_ok += 1
else:
cross_failures.append({
'cut': cut_idx,
'|R0|': len(Rs['0']),
'|R1|': len(Rs['1']),
})
print(f' Per-tire A(T) full-orbit count: {n_pertire_orbit_ok}/{n_tests}',
flush=True)
if per_tire_failures:
print(f' First per-tire failures:')
for f in per_tire_failures[:5]:
print(f' {f}')
print(f' R_i has full orbit (root sides): {n_root_ok}', flush=True)
print(f' Cross-cut R_0 ∩ R_1 != empty: {n_cross_ok}', flush=True)
if cross_failures:
print(f' Cross failures: {cross_failures[:3]}')
def main():
G = graphs.DodecahedralGraph()
G.is_planar(set_embedding=True)
test_on_graph(G, 'Dodecahedron', max_cuts=3)
gs = parse_planar_code(HM_FILE)
test_on_graph(gs[0], 'HM_0', max_cuts=3)
if __name__ == '__main__':
main()
@@ -0,0 +1,363 @@
"""Empirical test of the chain DP for the loose conjecture.
For each (G, cut, side), build the cut tire forest. For each cut tire
T = D(T) (cycle + pendants), enumerate proper 3-edge-colorings of T.
Tree DP bottom-up: at each node, compute the set of out-spoke
colorings compatible with at least one valid child combination.
Verify at roots: R_i != empty, contains a full S_3 orbit.
Verify across cut: R_0 ∩ R_1 != empty, contains a full S_3 orbit.
Per the chain_half_analysis.tex note, this directly tests:
- Conjecture (non-emptiness preservation): A(T_p) non-empty if
children's A's are non-empty.
- Bottom-line: R_0 ∩ R_1 non-empty.
"""
import os
import sys
import itertools
from collections import defaultdict
from sage.all import Graph, graphs
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
from cut_depth_label import (
parse_planar_code, HM_FILE, find_six_edge_cut,
apply_procedure, compute_nice_layout,
)
from cut_tire_tree import build_tree
def order_cycle(cycle_edges):
"""Order edges into a cyclic walk. Returns (verts_in_order,
edges_in_order) or (None, None) if not a simple cycle.
edges_in_order[i] connects verts_in_order[i] and verts_in_order[i+1]."""
n = len(cycle_edges)
if n < 2:
return None, None
e_set = {tuple(sorted(e)) for e in cycle_edges}
adj = defaultdict(list)
for u, v in e_set:
adj[u].append(v)
adj[v].append(u)
# Every vertex must have exactly 2 neighbors for a simple cycle
if any(len(adj[v]) != 2 for v in adj):
return None, None
start = next(iter(adj))
walk = [start]
prev = None
cur = start
while True:
nxt = [w for w in adj[cur] if w != prev]
if not nxt:
return None, None
nxt = nxt[0]
if nxt == start and len(walk) == n:
break
walk.append(nxt)
prev = cur
cur = nxt
if len(walk) > n:
return None, None
if len(walk) != n:
return None, None
edges_in_order = [tuple(sorted((walk[i], walk[(i + 1) % n])))
for i in range(n)]
return walk, edges_in_order
def get_tire_data(H, edge_depth, d, face):
"""For cut tire T_d^{(face)}, return (verts_in_order, cycle_edges,
in_spokes, out_spokes) or None if face is not a simple cycle.
in_spokes: dict {v: spoke_edge} for depth d+1 spokes
out_spokes: dict {v: spoke_edge} for depth d-1 spokes
"""
cycle_e = [tuple(sorted(e)) for e in face]
walk, edges_ord = order_cycle(cycle_e)
if walk is None:
return None
in_sp, out_sp = {}, {}
cycle_e_set = set(edges_ord)
for v in walk:
for nb in H.neighbors(v):
e = tuple(sorted((v, nb)))
if e in cycle_e_set:
continue
d_e = edge_depth.get(e)
if d_e is None:
continue
if d_e == d + 1:
in_sp[v] = e
elif d_e == d - 1:
out_sp[v] = e
return walk, edges_ord, in_sp, out_sp
def proper_3_colorings(verts, cycle_edges, in_sp, out_sp):
"""Enumerate proper 3-edge-colorings of D(T): cycle + spokes.
Returns list of dicts {edge: color}.
For each cycle coloring c[i] (i = 0..n-1) with c[i] != c[i+1]:
At vertex v_i, the two cycle edges adjacent are c[i-1] and c[i].
Spoke at v_i (if present) gets the third color.
"""
n = len(cycle_edges)
out = []
for assign in itertools.product([0, 1, 2], repeat=n):
# Proper cycle
bad = False
for i in range(n):
if assign[i] == assign[(i + 1) % n]:
bad = True
break
if bad:
continue
col = {cycle_edges[i]: assign[i] for i in range(n)}
# Determine spoke colors
for i, v in enumerate(verts):
c_left = assign[(i - 1) % n]
c_right = assign[i]
third = ({0, 1, 2} - {c_left, c_right})
if len(third) != 1:
# Degenerate (n=2 case)
bad = True
break
third_c = third.pop()
if v in in_sp:
col[in_sp[v]] = third_c
if v in out_sp:
col[out_sp[v]] = third_c
if not bad:
out.append(col)
return out
def s3_orbit(tup):
"""Generate S_3 orbit of a color tuple under permutations of {0,1,2}."""
orbit = set()
for perm in itertools.permutations([0, 1, 2]):
orbit.add(tuple(perm[c] for c in tup))
return orbit
def has_full_s3_orbit(s):
"""Does set s (of tuples) contain a 6-element S_3 orbit?"""
for tup in s:
if len(s3_orbit(tup)) == 6 and s3_orbit(tup) <= s:
return True
return False
def chain_dp(faces_by_depth, parent_of, H, edge_depth, side_label):
"""Run the chain DP on the cut tire forest.
For each tire T_d^{(f)}, compute:
- tire_data: (verts, cycle_e, in_sp, out_sp)
- colorings: all proper 3-edge-colorings
- A(T): set of (out-spoke-color-tuples), restricted by
compatibility with all children.
Returns: {node: A(T)}, where each A(T) is a set of tuples.
Also returns dict of issues found.
"""
tire_data = {}
for d, faces in faces_by_depth.items():
for f_idx, face in enumerate(faces):
td = get_tire_data(H, edge_depth, d, face)
if td is not None:
tire_data[(d, f_idx)] = td
# Build children list
children = defaultdict(list)
for node, p in parent_of.items():
if p is not None and p != ('cut', None):
children[p].append(node)
# Process bottom-up: order nodes by depth descending
nodes_by_depth = defaultdict(list)
for node in tire_data:
nodes_by_depth[node[0]].append(node)
max_d = max(nodes_by_depth) if nodes_by_depth else 0
A = {} # node -> set of out-spoke-color tuples
fully_S3 = {} # node -> bool: A(T) contains full S_3 orbit
issues = []
for d in range(max_d, 0, -1):
for node in nodes_by_depth[d]:
verts, cycle_e, in_sp, out_sp = tire_data[node]
if not out_sp and d > 1:
# Internal/leaf without out spokes — degenerate
A[node] = set()
continue
cs = proper_3_colorings(verts, cycle_e, in_sp, out_sp)
# Order out-spoke positions (by vertex)
out_sp_verts = sorted(out_sp.keys())
in_sp_verts = sorted(in_sp.keys())
# For each child, build map: parent-in-spoke-vertex (= parent
# boundary vertex with in spoke) -> child-out-spoke-vertex.
# Equivalent: the in-spoke edge of parent = an out-spoke edge
# of one of its children. The shared endpoint is the parent's
# boundary vertex.
child_outsp_maps = []
for ch in children.get(node, []):
if ch not in tire_data:
continue
ch_verts, ch_cycle, ch_in, ch_out = tire_data[ch]
# Match: parent's in-spoke edge == child's out-spoke edge
p_in_to_ch_out_v = {}
for v_p, e_p in in_sp.items():
# Find the vertex w of child with out-spoke e_p
for v_c, e_c in ch_out.items():
if e_p == e_c:
p_in_to_ch_out_v[v_p] = (ch, v_c)
break
child_outsp_maps.append((ch, p_in_to_ch_out_v))
# For each coloring, check compatibility with each child's A
achievable_out = set()
for col in cs:
# Extract parent's in-spoke colors
p_in_colors_by_v = {v: col[in_sp[v]] for v in in_sp}
# For each child, verify the corresponding out-spoke
# color (= our in-spoke color) is in A(child)
ok = True
for ch, mapping in child_outsp_maps:
if ch not in A:
ok = False # child has no achievable (shouldn't happen)
break
ch_verts, ch_cycle, ch_in, ch_out = tire_data[ch]
ch_out_verts_ordered = sorted(ch_out.keys())
# Build the child's out-spoke tuple as we'd need
required_ch_out = {}
for v_p, (ch_id, v_c) in mapping.items():
if ch_id != ch:
continue
required_ch_out[v_c] = p_in_colors_by_v[v_p]
# If some out-spoke vertex of child isn't covered by
# any parent in spoke, the value is FREE in A.
# So we check: exists tup in A(ch) with tup[i] =
# required_ch_out[v] for each i where v is constrained.
found_any = False
for tup in A[ch]:
match = True
for i, v in enumerate(ch_out_verts_ordered):
if v in required_ch_out and tup[i] != required_ch_out[v]:
match = False
break
if match:
found_any = True
break
if not found_any:
ok = False
break
if ok:
# Extract parent's out-spoke colors as a tuple
if out_sp_verts:
tup = tuple(col[out_sp[v]] for v in out_sp_verts)
achievable_out.add(tup)
else:
# No out spokes (root with empty out projection) —
# represent as singleton empty tuple
achievable_out.add(())
A[node] = achievable_out
fully_S3[node] = has_full_s3_orbit(achievable_out)
if not achievable_out:
issues.append({
'type': 'empty_after_chain',
'node': node,
'side': side_label,
'n_children': len(children.get(node, [])),
})
return A, fully_S3, issues, tire_data
def run_test(G, name, max_cuts=5):
"""Run chain DP test on G across several 6-edge cuts."""
from tree_structure_sweep import all_six_edge_cuts
cuts = all_six_edge_cuts(G, max_cuts=max_cuts)
print(f'\n=== {name}: V={G.order()}, E={G.size()}, '
f'{len(cuts)} cuts tested ===', flush=True)
base_pos = compute_nice_layout(G)
n_cuts = 0
n_empty = 0
n_no_orbit = 0
n_crossok = 0
total_issues = []
for cut_idx, (S, cut_edges) in enumerate(cuts):
S0 = S
S1 = frozenset(G.vertices()) - S
if len(S0) < 4 or len(S1) < 4:
continue
A_per_side = {}
for side, S_side in [('0', S0), ('1', S1)]:
try:
H, pos, ed, _, _, _ = apply_procedure(
G, S_side, cut_edges, base_pos, side,
pendant_start_id=max(G.vertices()) + 1 +
(0 if side == '0' else 200))
except Exception as e:
print(f' cut #{cut_idx} side {side}: apply_procedure '
f'failed: {e}', flush=True)
continue
faces_by_depth, parent_of, _ = build_tree(H, ed)
A, fully_S3, issues, tire_data = chain_dp(
faces_by_depth, parent_of, H, ed, side)
total_issues.extend(issues)
# Look at root tires (depth-1)
root_A = []
for node in A:
if node[0] == 1:
root_A.append((node, A[node]))
# Combine root A's into R_i: union over roots
R_i = set()
for node, a in root_A:
R_i |= a
A_per_side[side] = R_i
if not R_i:
n_empty += 1
print(f' cut #{cut_idx} side {side}: R_i empty!', flush=True)
elif not any(len(s3_orbit(t)) == 6 and s3_orbit(t) <= R_i
for t in R_i):
n_no_orbit += 1
print(f' cut #{cut_idx} side {side}: R_i lacks full S_3 '
f'orbit (|R_i|={len(R_i)})', flush=True)
if '0' in A_per_side and '1' in A_per_side:
n_cuts += 1
# Cross-cut check: R_0 and R_1 are on the same 6 cut edges,
# but indexed by which side's roots. Need bijection.
# For now, compare structurally: do they share orbits?
R0, R1 = A_per_side['0'], A_per_side['1']
common = R0 & R1
if common:
n_crossok += 1
else:
print(f' cut #{cut_idx}: R_0 ∩ R_1 = empty '
f'(|R_0|={len(R0)}, |R_1|={len(R1)})', flush=True)
print(f' cuts tested: {n_cuts}, R_i empty: {n_empty}, '
f'R_i no full orbit: {n_no_orbit}, R_0 ∩ R_1 non-empty: '
f'{n_crossok}/{n_cuts}', flush=True)
return total_issues
def main():
# Small test: Dodecahedron
G = graphs.DodecahedralGraph()
G.is_planar(set_embedding=True)
issues_dodec = run_test(G, 'Dodecahedron', max_cuts=5)
# HM #0
gs = parse_planar_code(HM_FILE)
issues_hm0 = run_test(gs[0], 'HM_0', max_cuts=5)
print('\n=== Summary of issues ===', flush=True)
print(f'Dodecahedron: {len(issues_dodec)} issues')
print(f'HM_0: {len(issues_hm0)} issues')
if __name__ == '__main__':
main()
@@ -0,0 +1,18 @@
\relax
\@writefile{toc}{\contentsline {paragraph}{Per-tire half.}{1}{}\protected@file@percent }
\@writefile{toc}{\contentsline {paragraph}{Chain half.}{1}{}\protected@file@percent }
\@writefile{toc}{\contentsline {paragraph}{(1) Leaves.}{1}{}\protected@file@percent }
\@writefile{toc}{\contentsline {paragraph}{(2) Internal nodes.}{1}{}\protected@file@percent }
\@writefile{toc}{\contentsline {paragraph}{(3) Roots and the cut.}{2}{}\protected@file@percent }
\@writefile{toc}{\contentsline {paragraph}{(4) Cross-cut.}{2}{}\protected@file@percent }
\newlabel{conj:non-empty-prop}{{}{2}}
\@writefile{toc}{\contentsline {paragraph}{Why it isn't trivial.}{2}{}\protected@file@percent }
\@writefile{toc}{\contentsline {paragraph}{Why it's plausible.}{2}{}\protected@file@percent }
\@writefile{toc}{\contentsline {paragraph}{What would close the proof.}{2}{}\protected@file@percent }
\@writefile{toc}{\contentsline {paragraph}{What goes wrong.}{3}{}\protected@file@percent }
\@writefile{toc}{\contentsline {paragraph}{Consequence for Prop 1.13.}{3}{}\protected@file@percent }
\@writefile{toc}{\contentsline {paragraph}{Two ways forward.}{3}{}\protected@file@percent }
\@writefile{toc}{\contentsline {paragraph}{What this changes in the chain DP.}{4}{}\protected@file@percent }
\@writefile{toc}{\contentsline {paragraph}{Second issue: out-spoke projection loses S$_3$ orbit.}{4}{}\protected@file@percent }
\@writefile{toc}{\contentsline {paragraph}{Third issue: heuristic parent-finding.}{4}{}\protected@file@percent }
\gdef \@abspage@last{4}
@@ -0,0 +1,317 @@
This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 26 MAY 2026 22:49
entering extended mode
restricted \write18 enabled.
%&-line parsing enabled.
**chain_half_analysis.tex
(./chain_half_analysis.tex
LaTeX2e <2021-11-15> patch level 1
L3 programming layer <2022-02-24>
(/usr/local/texlive/2022/texmf-dist/tex/latex/base/article.cls
Document Class: article 2021/10/04 v1.4n Standard LaTeX document class
(/usr/local/texlive/2022/texmf-dist/tex/latex/base/size11.clo
File: size11.clo 2021/10/04 v1.4n Standard LaTeX file (size option)
)
\c@part=\count185
\c@section=\count186
\c@subsection=\count187
\c@subsubsection=\count188
\c@paragraph=\count189
\c@subparagraph=\count190
\c@figure=\count191
\c@table=\count192
\abovecaptionskip=\skip47
\belowcaptionskip=\skip48
\bibindent=\dimen138
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsmath.sty
Package: amsmath 2021/10/15 v2.17l AMS math features
\@mathmargin=\skip49
For additional information on amsmath, use the `?' option.
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amstext.sty
Package: amstext 2021/08/26 v2.01 AMS text
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsgen.sty
File: amsgen.sty 1999/11/30 v2.0 generic functions
\@emptytoks=\toks16
\ex@=\dimen139
))
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsbsy.sty
Package: amsbsy 1999/11/29 v1.2d Bold Symbols
\pmbraise@=\dimen140
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsmath/amsopn.sty
Package: amsopn 2021/08/26 v2.02 operator names
)
\inf@bad=\count193
LaTeX Info: Redefining \frac on input line 234.
\uproot@=\count194
\leftroot@=\count195
LaTeX Info: Redefining \overline on input line 399.
\classnum@=\count196
\DOTSCASE@=\count197
LaTeX Info: Redefining \ldots on input line 496.
LaTeX Info: Redefining \dots on input line 499.
LaTeX Info: Redefining \cdots on input line 620.
\Mathstrutbox@=\box50
\strutbox@=\box51
\big@size=\dimen141
LaTeX Font Info: Redeclaring font encoding OML on input line 743.
LaTeX Font Info: Redeclaring font encoding OMS on input line 744.
\macc@depth=\count198
\c@MaxMatrixCols=\count199
\dotsspace@=\muskip16
\c@parentequation=\count266
\dspbrk@lvl=\count267
\tag@help=\toks17
\row@=\count268
\column@=\count269
\maxfields@=\count270
\andhelp@=\toks18
\eqnshift@=\dimen142
\alignsep@=\dimen143
\tagshift@=\dimen144
\tagwidth@=\dimen145
\totwidth@=\dimen146
\lineht@=\dimen147
\@envbody=\toks19
\multlinegap=\skip50
\multlinetaggap=\skip51
\mathdisplay@stack=\toks20
LaTeX Info: Redefining \[ on input line 2938.
LaTeX Info: Redefining \] on input line 2939.
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amssymb.sty
Package: amssymb 2013/01/14 v3.01 AMS font symbols
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/amsfonts.sty
Package: amsfonts 2013/01/14 v3.01 Basic AMSFonts support
\symAMSa=\mathgroup4
\symAMSb=\mathgroup5
LaTeX Font Info: Redeclaring math symbol \hbar on input line 98.
LaTeX Font Info: Overwriting math alphabet `\mathfrak' in version `bold'
(Font) U/euf/m/n --> U/euf/b/n on input line 106.
))
(/usr/local/texlive/2022/texmf-dist/tex/latex/amscls/amsthm.sty
Package: amsthm 2020/05/29 v2.20.6
\thm@style=\toks21
\thm@bodyfont=\toks22
\thm@headfont=\toks23
\thm@notefont=\toks24
\thm@headpunct=\toks25
\thm@preskip=\skip52
\thm@postskip=\skip53
\thm@headsep=\skip54
\dth@everypar=\toks26
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphicx.sty
Package: graphicx 2021/09/16 v1.2d Enhanced LaTeX Graphics (DPC,SPQR)
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/keyval.sty
Package: keyval 2014/10/28 v1.15 key=value parser (DPC)
\KV@toks@=\toks27
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/graphics.sty
Package: graphics 2021/03/04 v1.4d Standard LaTeX Graphics (DPC,SPQR)
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics/trig.sty
Package: trig 2021/08/11 v1.11 sin cos tan (DPC)
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics-cfg/graphics.cfg
File: graphics.cfg 2016/06/04 v1.11 sample graphics configuration
)
Package graphics Info: Driver file: pdftex.def on input line 107.
(/usr/local/texlive/2022/texmf-dist/tex/latex/graphics-def/pdftex.def
File: pdftex.def 2020/10/05 v1.2a Graphics/color driver for pdftex
))
\Gin@req@height=\dimen148
\Gin@req@width=\dimen149
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/geometry/geometry.sty
Package: geometry 2020/01/02 v5.9 Page Geometry
(/usr/local/texlive/2022/texmf-dist/tex/generic/iftex/ifvtex.sty
Package: ifvtex 2019/10/25 v1.7 ifvtex legacy package. Use iftex instead.
(/usr/local/texlive/2022/texmf-dist/tex/generic/iftex/iftex.sty
Package: iftex 2022/02/03 v1.0f TeX engine tests
))
\Gm@cnth=\count271
\Gm@cntv=\count272
\c@Gm@tempcnt=\count273
\Gm@bindingoffset=\dimen150
\Gm@wd@mp=\dimen151
\Gm@odd@mp=\dimen152
\Gm@even@mp=\dimen153
\Gm@layoutwidth=\dimen154
\Gm@layoutheight=\dimen155
\Gm@layouthoffset=\dimen156
\Gm@layoutvoffset=\dimen157
\Gm@dimlist=\toks28
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/booktabs/booktabs.sty
Package: booktabs 2020/01/12 v1.61803398 Publication quality tables
\heavyrulewidth=\dimen158
\lightrulewidth=\dimen159
\cmidrulewidth=\dimen160
\belowrulesep=\dimen161
\belowbottomsep=\dimen162
\aboverulesep=\dimen163
\abovetopsep=\dimen164
\cmidrulesep=\dimen165
\cmidrulekern=\dimen166
\defaultaddspace=\dimen167
\@cmidla=\count274
\@cmidlb=\count275
\@aboverulesep=\dimen168
\@belowrulesep=\dimen169
\@thisruleclass=\count276
\@lastruleclass=\count277
\@thisrulewidth=\dimen170
)
(/usr/local/texlive/2022/texmf-dist/tex/latex/l3backend/l3backend-pdftex.def
File: l3backend-pdftex.def 2022-02-07 L3 backend support: PDF output (pdfTeX)
\l__color_backend_stack_int=\count278
\l__pdf_internal_box=\box52
)
(./chain_half_analysis.aux)
\openout1 = `chain_half_analysis.aux'.
LaTeX Font Info: Checking defaults for OML/cmm/m/it on input line 17.
LaTeX Font Info: ... okay on input line 17.
LaTeX Font Info: Checking defaults for OMS/cmsy/m/n on input line 17.
LaTeX Font Info: ... okay on input line 17.
LaTeX Font Info: Checking defaults for OT1/cmr/m/n on input line 17.
LaTeX Font Info: ... okay on input line 17.
LaTeX Font Info: Checking defaults for T1/cmr/m/n on input line 17.
LaTeX Font Info: ... okay on input line 17.
LaTeX Font Info: Checking defaults for TS1/cmr/m/n on input line 17.
LaTeX Font Info: ... okay on input line 17.
LaTeX Font Info: Checking defaults for OMX/cmex/m/n on input line 17.
LaTeX Font Info: ... okay on input line 17.
LaTeX Font Info: Checking defaults for U/cmr/m/n on input line 17.
LaTeX Font Info: ... okay on input line 17.
(/usr/local/texlive/2022/texmf-dist/tex/context/base/mkii/supp-pdf.mkii
[Loading MPS to PDF converter (version 2006.09.02).]
\scratchcounter=\count279
\scratchdimen=\dimen171
\scratchbox=\box53
\nofMPsegments=\count280
\nofMParguments=\count281
\everyMPshowfont=\toks29
\MPscratchCnt=\count282
\MPscratchDim=\dimen172
\MPnumerator=\count283
\makeMPintoPDFobject=\count284
\everyMPtoPDFconversion=\toks30
) (/usr/local/texlive/2022/texmf-dist/tex/latex/epstopdf-pkg/epstopdf-base.sty
Package: epstopdf-base 2020-01-24 v2.11 Base part for package epstopdf
Package epstopdf-base Info: Redefining graphics rule for `.eps' on input line 4
85.
(/usr/local/texlive/2022/texmf-dist/tex/latex/latexconfig/epstopdf-sys.cfg
File: epstopdf-sys.cfg 2010/07/13 v1.3 Configuration of (r)epstopdf for TeX Liv
e
))
*geometry* driver: auto-detecting
*geometry* detected driver: pdftex
*geometry* verbose mode - [ preamble ] result:
* driver: pdftex
* paper: <default>
* layout: <same size as paper>
* layoutoffset:(h,v)=(0.0pt,0.0pt)
* modes:
* h-part:(L,W,R)=(72.26999pt, 469.75502pt, 72.26999pt)
* v-part:(T,H,B)=(72.26999pt, 650.43001pt, 72.26999pt)
* \paperwidth=614.295pt
* \paperheight=794.96999pt
* \textwidth=469.75502pt
* \textheight=650.43001pt
* \oddsidemargin=0.0pt
* \evensidemargin=0.0pt
* \topmargin=-37.0pt
* \headheight=12.0pt
* \headsep=25.0pt
* \topskip=11.0pt
* \footskip=30.0pt
* \marginparwidth=59.0pt
* \marginparsep=10.0pt
* \columnsep=10.0pt
* \skip\footins=10.0pt plus 4.0pt minus 2.0pt
* \hoffset=0.0pt
* \voffset=0.0pt
* \mag=1000
* \@twocolumnfalse
* \@twosidefalse
* \@mparswitchfalse
* \@reversemarginfalse
* (1in=72.27pt=25.4mm, 1cm=28.453pt)
LaTeX Font Info: Trying to load font information for U+msa on input line 18.
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsa.fd
File: umsa.fd 2013/01/14 v3.01 AMS symbols A
)
LaTeX Font Info: Trying to load font information for U+msb on input line 18.
(/usr/local/texlive/2022/texmf-dist/tex/latex/amsfonts/umsb.fd
File: umsb.fd 2013/01/14 v3.01 AMS symbols B
)
Overfull \hbox (31.8519pt too wide) in paragraph at lines 32--37
\OT1/cmr/bx/n/10.95 Chain half.[] \OT1/cmr/m/n/10.95 Com-pos-ing per-tire pro-
jec-tions through the cut-tire for-est (\OT1/cmtt/m/n/10.95 cut[]tire[]tree[]st
ructure.tex\OT1/cmr/m/n/10.95 ,
[]
Package amsmath Warning: Foreign command \atop;
(amsmath) \frac or \genfrac should be used instead
(amsmath) on input line 58.
[1
{/usr/local/texlive/2022/texmf-var/fonts/map/pdftex/updmap/pdftex.map}]
Overfull \hbox (41.57079pt too wide) in paragraph at lines 134--142
\OT1/cmr/bx/n/10.95 Why it's plau-si-ble.[] \OT1/cmr/m/n/10.95 Em-pir-i-cal da
ta from the partial- tire-dual chain pi-geon-hole (\OT1/cmtt/m/n/10.95 tire[]fi
ber[]step2.tex\OT1/cmr/m/n/10.95 ):
[]
[2] [3] [4] (./chain_half_analysis.aux) )
Here is how much of TeX's memory you used:
3256 strings out of 478268
48448 string characters out of 5846347
348617 words of memory out of 5000000
21443 multiletter control sequences out of 15000+600000
479693 words of font info for 69 fonts, out of 8000000 for 9000
1141 hyphenation exceptions out of 8191
55i,8n,62p,236b,241s stack positions out of 10000i,1000n,20000p,200000b,200000s
{/usr/local/texlive/2022/texmf-dist/fo
nts/enc/dvips/cm-super/cm-super-ts1.enc}</usr/local/texlive/2022/texmf-dist/fon
ts/type1/public/amsfonts/cm/cmbx10.pfb></usr/local/texlive/2022/texmf-dist/font
s/type1/public/amsfonts/cm/cmbx12.pfb></usr/local/texlive/2022/texmf-dist/fonts
/type1/public/amsfonts/cm/cmex10.pfb></usr/local/texlive/2022/texmf-dist/fonts/
type1/public/amsfonts/cm/cmmi10.pfb></usr/local/texlive/2022/texmf-dist/fonts/t
ype1/public/amsfonts/cm/cmmi12.pfb></usr/local/texlive/2022/texmf-dist/fonts/ty
pe1/public/amsfonts/cm/cmmi6.pfb></usr/local/texlive/2022/texmf-dist/fonts/type
1/public/amsfonts/cm/cmmi8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/
public/amsfonts/cm/cmr10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/pu
blic/amsfonts/cm/cmr17.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/publ
ic/amsfonts/cm/cmr6.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/
amsfonts/cm/cmr7.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/ams
fonts/cm/cmr8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfon
ts/cm/cmsy10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfont
s/cm/cmsy8.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/
cm/cmti10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/amsfonts/c
m/cmtt10.pfb></usr/local/texlive/2022/texmf-dist/fonts/type1/public/cm-super/sf
rm1095.pfb>
Output written on chain_half_analysis.pdf (4 pages, 216066 bytes).
PDF statistics:
103 PDF objects out of 1000 (max. 8388607)
62 compressed objects within 1 object stream
0 named destinations out of 1000 (max. 500000)
1 words of extra memory for PDF output out of 10000 (max. 10000000)
@@ -0,0 +1,279 @@
\documentclass[11pt]{article}
\usepackage{amsmath,amssymb,amsthm}
\usepackage{graphicx}
\usepackage{geometry}
\usepackage{booktabs}
\geometry{margin=1in}
\title{Chain half of the loose conjecture: tree DP and where it gates}
\author{}
\date{}
\newtheorem*{prop}{Proposition}
\newtheorem*{lem}{Lemma}
\newtheorem*{conj}{Conjecture}
\newtheorem*{obs}{Observation}
\begin{document}
\maketitle
\section*{Recap}
The loose chain pigeonhole conjecture (k$\ge 2$ form,
\texttt{cut\_depth\_label.tex}) has two halves:
\paragraph{Per-tire half.} For every cut tire $T$ with $\ge 2$
in/out spokes total, the joint projection $\pi(T) \subseteq
\{1,2,3\}^k$ is non-empty, $S_3$-closed, and contains a full $S_3$
orbit of size $6$. Proven for spoke-only cut tires
($T'_{\mathrm{ann}}$ = simple cycle $C_n$, $n \ge 3$) via Prop 1.13
of \texttt{paper.tex}.
\paragraph{Chain half.} Composing per-tire projections through the
cut-tire forest (\texttt{cut\_tire\_tree\_structure.tex}, rigorously
proved) yields $\mathcal{R}_i \ne \emptyset$ on each side $i$ of the
cut, with $\mathcal{R}_0 \cap \mathcal{R}_1 \ne \emptyset$ containing
a common $S_3$-orbit at the cut.
\section*{Tree DP formulation of the chain half}
The high-side cut tires of $G'_i$ form a forest (rigorously proved).
Process tires bottom-up:
\paragraph{(1) Leaves.} A leaf cut tire $T_{d_{\max}}^{(f)}$ has
no children. Its ``achievable projection'' onto its out spokes
(depth-$(d_{\max} - 1)$ direction) is
\[
A(T) := \pi_{\mathrm{out}}(T),
\]
the projection of the per-tire projection $\pi(T)$ onto the out
spokes alone (in spokes are unconstrained at leaves). By per-tire,
$|A(T)| \ge 6$ with full $S_3$-orbit.
\paragraph{(2) Internal nodes.} For a cut tire $T_p = T_d^{(f)}$
with children $T_{c_1}, \dots, T_{c_r}$, define:
\[
A(T_p) := \left\{\sigma_{\mathrm{out}}(T_p) : \exists\, \chi
\text{ proper edge $3$-coloring of } T_p \text{ such that}
\atop \forall\, j : \chi|_{\text{in-spokes corresponding to }
T_{c_j}} \in A(T_{c_j})'\right\},
\]
where $A(T_{c_j})'$ is the ``transferred-back'' achievable
projection of child $T_{c_j}$ onto the corresponding parent in-spoke
positions.
The transfer back: an in spoke of $T_p$ at parent boundary vertex
$v$ corresponds to a depth-$(d+1)$ edge $e^{**}$ in $G'_i$. This
$e^{**}$ is a face-boundary edge of $T_{c_j}$ (the unique child
whose face contains $v$ as a boundary endpoint of $e^{**}$). So the
parent's in-spoke color at $v$ = child's face-boundary-edge color at
$e^{**}$.
For the projection $A(T_{c_j})$ to constrain parent's in spokes,
we'd need to know how child's spoke colors determine cycle edge
colors at specific positions. In the spoke-only case (Prop 1.13),
each spoke color at $u$ equals the ``third color'' = the unique
color not appearing on the two cycle edges incident at $u$. So
spoke colors determine the constraint set on cycle colors at each
vertex.
\paragraph{(3) Roots and the cut.} $T_1^{(\cdot)}$ are roots. Their
out spokes are the pendant edges $=$ the cut configuration $\sigma_i$.
The achievable cut configurations:
\[
\mathcal{R}_i := \bigcup_{\text{roots } T_1^{(f)}} A(T_1^{(f)})
\quad (\text{or restricted, depending on how root constraints compose}).
\]
\paragraph{(4) Cross-cut.} $G'$ is properly $3$-edge-colourable iff
$\mathcal{R}_0 \cap \mathcal{R}_1 \ne \emptyset$ (under the
bijection between the two sides' cut edges).
\section*{What's preserved through the tree DP}
\subsection*{$S_3$-closure: preserved}
\begin{lem}[$S_3$-equivariance of tree DP]
If every $A(T_{c_j})$ is $S_3$-closed (under diagonal action on
colours), then $A(T_p)$ is $S_3$-closed.
\end{lem}
\begin{proof}
The proper-edge-coloring constraint at every vertex is preserved by
$S_3$ acting on colours uniformly. Applying $\pi \in S_3$ to a
valid $\chi$ for $T_p$ gives another valid $\chi$. Compatibility
with children: parent's in spokes are uniformly $\pi$-shifted, and
each child's $A(T_{c_j})$ is $S_3$-closed by hypothesis, so the
shifted parent in spokes still hit $A(T_{c_j})$. Hence $\pi(\sigma)
\in A(T_p)$ for any $\sigma \in A(T_p)$.
\end{proof}
By induction from leaves (where $A = \pi$ is $S_3$-closed by
per-tire half), every $A(T)$ is $S_3$-closed. This is the easy
half.
\subsection*{Non-emptiness: open, but constrained}
\begin{conj}[Non-emptiness preservation]
\label{conj:non-empty-prop}
If every $A(T_{c_j})$ contains a full $S_3$-orbit of size $6$, then
$A(T_p)$ also contains a full $S_3$-orbit.
\end{conj}
This is the genuine open piece of the chain half.
\paragraph{Why it isn't trivial.} Two $S_3$-closed subsets of
$\{1, 2, 3\}^k$ can have empty intersection even if both contain
$S_3$-orbits. Example: orbit of $(1, 2, 3)$ vs orbit of $(1, 1, 2)$
in $\{1,2,3\}^3$ are disjoint.
So the conjecture would require a structural reason that parent +
children combined always have at least one common assignment with
all 3 colours present (= a full $S_3$-orbit).
\paragraph{Why it's plausible.} Empirical data from the partial-
tire-dual chain pigeonhole (\texttt{tire\_fiber\_step2.tex}):
$23/23$ pairwise compatibility tests succeeded, with the
intersections containing $S_3$-orbits and structured by ``rainbow''
or similar canonical orbits
(\texttt{orbit\_decomposition.tex}). These results suggest a
structural reason for non-emptiness; we just don't have a clean
proof.
\paragraph{What would close the proof.} Show that for spoke-only
cut tires, the per-tire projection $\pi(T)$ has the property:
\emph{for any specified colours on the in spokes that come from
$S_3$-orbits, there is a compatible parent coloring}. Specifically:
\begin{conj}[Strong per-tire extendibility]
Let $T$ be a spoke-only cut tire with face boundary a simple cycle
$C_n$ ($n \ge 3$). For any $\sigma_{\mathrm{in}} \in \{1, 2, 3\}^{n_{\mathrm{in}}}$
such that $\sigma_{\mathrm{in}}$ lies in a non-trivial $S_3$-orbit
(i.e.\ uses $\ge 2$ colours and is in $\pi(T)$'s $S_3$-symmetric
support), there exists a proper edge $3$-coloring $\chi$ of $T$ with
$\chi|_{\mathrm{in-spokes}} = \sigma_{\mathrm{in}}$ and
$\chi|_{\mathrm{out-spokes}}$ a non-trivial $S_3$-orbit on the
out-spoke side.
\end{conj}
If this conjecture holds, the chain DP preserves non-emptiness: for
each non-trivial parent $\sigma_{\mathrm{in}}$ (= child's face
boundary edges) provided by children, parent has a coloring with
out spokes in a non-trivial $S_3$-orbit, hence $A(T_p)$ contains a
full $S_3$-orbit.
\section*{Empirical next step}
Cut-tire tree DP empirical test:
\begin{enumerate}
\item For each test graph (HM \#0 through \#5, dodecahedron,
BuckyBall), build the cut tire forest on each side.
\item For each leaf, compute $A(T_{\mathrm{leaf}})$.
\item Bottom-up propagate $A(\cdot)$ to roots.
\item Compare $\mathcal{R}_0 \cap \mathcal{R}_1$ at the cut.
\end{enumerate}
This is the analogue of \texttt{tire\_fiber\_step2.tex} for the
cut-tire setting. If empirically $\mathcal{R}_0 \cap \mathcal{R}_1
\ne \emptyset$ universally, the chain half is on firm empirical
ground; the proof would still need Conjecture~\ref{conj:non-empty-prop}
or a structural shortcut.
\section*{Caveat discovered empirically: cut tires are not spoke-only}
A first-pass empirical test
(\texttt{experiments/chain\_dp\_test.py}) on the dodecahedron and
Holton--McKay \#0 revealed a structural complication: \emph{cut
tires are not in general spoke-only}.
\paragraph{What goes wrong.} $H_d$ may have vertices of degree $3$
(all three incident edges have depth $d$). In a cubic ambient
graph $G'_i$, this happens when three depth-$d$ edges meet at a
single vertex. At such a vertex:
\begin{itemize}
\item There is no third edge available to be a spoke.
\item The face boundary walk of $H_d$ visits the vertex
\emph{twice} (as a branch point).
\end{itemize}
For example, in the dodecahedron, a $6$-edge cut produces $H_1$
with $1$ face whose boundary has length $20$ but only $11$ distinct
vertices --- $9$ branch-point visits.
\paragraph{Consequence for Prop 1.13.} The per-tire half (proven
via Prop 1.13) covers \emph{spoke-only} cut tires (face boundary =
simple cycle $C_n$, $n \ge 3$). It does \emph{not} cover branched
cut tires.
\paragraph{Two ways forward.}
\begin{enumerate}
\item \emph{Restrict.} Identify which graphs $G$ have the property
that every cut tire of every $6$-edge cut is spoke-only. This
is a genuine \emph{a priori} restriction.
\item \emph{Generalise.} Extend the per-tire half to branched cut
tires. Proper edge $3$-coloring on such a structure is
well-defined and probably has a non-empty $S_3$-closed
projection by similar arguments, but the explicit count
$2^n + 2(-1)^n$ no longer applies.
\end{enumerate}
\paragraph{What this changes in the chain DP.} The chain DP is
still well-formed: enumerate proper $3$-edge-colorings of each cut
tire (now allowing branches), project to out spokes, restrict via
children. The per-tire half just needs the generalized form.
\paragraph{Second issue: out-spoke projection loses S$_3$ orbit.}
The per-tire half guarantees a full $S_3$ orbit on the \emph{joint}
in+out spoke projection $\pi(T)$. After restricting to OUT spokes
only (which is what the parent uses), the projection $A(T)$ may
contain fewer than $6$ elements --- e.g.\ all out spokes might be
forced to a constant tuple by some structural symmetry, giving
$|A(T)| = 3$ (= $\{(0,0,\dots), (1,1,\dots), (2,2,\dots)\}$).
Initial empirical runs on the dodecahedron and HM \#0 see exactly
this happen at $\sim 20\%$ of cut tires. This is \emph{not} a bug
in the per-tire half; it is a genuine limitation of the OUT-only
projection.
The correct chain DP formulation should track the joint (in + out)
projection, not just OUT. This is the analogue of
\texttt{tire\_fiber\_step2.tex}'s joint-support tracking.
\paragraph{Third issue: heuristic parent-finding.} The current
\texttt{cut\_tire\_tree.find\_parent\_face} uses a vertex-overlap
heuristic (smallest face among overlapping candidates). Per the
high-side proposition (\texttt{cut\_tire\_tree\_structure.tex}),
the geometrically-correct parent is unique, but the empirical
script does not enforce this --- it picks by smallest-face
heuristic, which can mis-attribute children to wrong parents.
For a rigorous empirical test, parent assignment should use the
planar embedding's face-in-face containment, not vertex overlap.
\section*{Net status of the loose conjecture}
\begin{center}
\small
\begin{tabular}{lll}
\toprule
component & status & note \\
\midrule
Per-tire half (spoke-only $n \ge 3$) & proven & Prop 1.13 \\
Per-tire half (branched) & open, needed for generality & \\
Tree structure (forest, high-side) & proven & \texttt{cut\_tire\_tree\_structure.tex} \\
Chain DP $S_3$-equivariance & proven & this note, Lemma \\
Joint vs.\ OUT projection issue & flagged & need joint-support tracking \\
Chain DP non-emptiness preservation & open & Conj.\ \ref{conj:non-empty-prop} \\
Bottom-line $\mathcal{R}_0 \cap \mathcal{R}_1 \ne \emptyset$ & open, $G$-colorability gives it & \\
\bottomrule
\end{tabular}
\end{center}
The chain half reduces to multiple structural claims:
\begin{itemize}
\item per-tire half for branched cut tires;
\item joint-support DP (track $\pi(T)$, not just OUT projection);
\item non-emptiness preservation (Conj.\ \ref{conj:non-empty-prop}
or Strong per-tire extendibility).
\end{itemize}
None are in hand yet. The $S_3$-equivariance and forest structure
are. The full chain half is genuinely open and requires more work.
\end{document}