diff --git a/papers/coloring_nested_tire_graphs/experiments/chain_dp_joint.py b/papers/coloring_nested_tire_graphs/experiments/chain_dp_joint.py new file mode 100644 index 0000000..a53c6ab --- /dev/null +++ b/papers/coloring_nested_tire_graphs/experiments/chain_dp_joint.py @@ -0,0 +1,375 @@ +"""Chain DP with JOINT projection tracking + rigorous edge-based +parent verification + ground-truth comparison. + +Key fixes vs. chain_dp_general.py: + +(1) DP state per tire = SET of full proper edge 3-colorings of the + tire (cycle + pendants), indexed by edge id within the tire. + No projection to spokes; the full coloring is the state. + +(2) Composition between parent T_p and child T_c is via SHARED + EDGES (= full G'_i edge tuples appearing in both tires). + Parent coloring c_p is compatible with child coloring c_c iff + they agree on every shared edge. No need to figure out which + in-spoke maps to which cycle edge — equality of full edge + tuples does the work. + +(3) Parent-finding sanity: after build_tree's heuristic, verify + each (child, parent) pair shares at least one edge. If not, + flag as bug. + +(4) Ground truth: compute ALL proper 3-edge-colorings of G'_i via + backtracking with constraint propagation. Project to cut + edges. Compare R_i^DP with R_i^direct. They should match. + +The empirical test: for 3-edge-colorable G (dodecahedron, HM #0 +are all 3-edge-colorable by Vizing/Tait), R_0 ∩ R_1 should be +non-empty (= projection of a full G coloring to cut). +""" +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, + apply_procedure, compute_nice_layout, +) +from cut_tire_tree import build_tree +from tree_structure_sweep import all_six_edge_cuts + + +def tire_edges(H, edge_depth, d, face): + """Return ordered list of edges in cut tire T_d^{(face)}. + Cycle edges (depth d) + pendants (depth d-1 OUT and d+1 IN). + Each edge is a sorted tuple of vertices. + Returns (edges_in_order, vertex_to_edge_ids, in_pendant_ids, + out_pendant_ids, cycle_edge_ids).""" + # Cycle edges (dedup) + 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 cycle + verts = set() + for u, v in cycle_edges: + verts.add(u); verts.add(v) + # Pendants + pendants_in, pendants_out = [], [] + for v in verts: + for nb in H.neighbors(v): + e = tuple(sorted((v, nb))) + if e in cycle_e_set: + continue + de = edge_depth.get(e) + if de == d + 1: + pendants_in.append((v, e)) + elif de == d - 1: + pendants_out.append((v, e)) + # Edge list: cycle then in pendants then out pendants + all_edges = cycle_edges + [e for (_, e) in pendants_in] + \ + [e for (_, e) in pendants_out] + edge_id = {e: i for i, e in enumerate(all_edges)} + # Per vertex, edges at v + edges_at_v = defaultdict(list) + for (a, b) in cycle_edges: + edges_at_v[a].append(edge_id[(a, b)]) + edges_at_v[b].append(edge_id[(a, b)]) + for (v, e) in pendants_in: + edges_at_v[v].append(edge_id[e]) + for (v, e) in pendants_out: + edges_at_v[v].append(edge_id[e]) + cycle_eids = [edge_id[e] for e in cycle_edges] + in_eids = [edge_id[e] for (_, e) in pendants_in] + out_eids = [edge_id[e] for (_, e) in pendants_out] + return { + 'all_edges': all_edges, + 'edge_id': edge_id, + 'edges_at_v': dict(edges_at_v), + 'cycle_eids': cycle_eids, + 'in_eids': in_eids, + 'out_eids': out_eids, + } + + +def enumerate_proper(struct): + """Enumerate proper 3-edge-colorings of the tire as tuples of + colors (length = n_edges). Return None if too big.""" + n_e = len(struct['all_edges']) + if n_e > 14: + return None + out = [] + for assign in itertools.product([0, 1, 2], repeat=n_e): + ok = True + for v, eids in struct['edges_at_v'].items(): + cs = [assign[i] for i in eids] + if len(set(cs)) != len(cs): + ok = False; break + if ok: + out.append(assign) + return out + + +def chain_dp_joint(faces_by_depth, parent_of, H, edge_depth): + """Chain DP with full per-tire colorings tracked. + Returns: + A: node -> list of full colorings (tuples) + tire_struct: node -> structure dict + issues: list of warnings + """ + tire_struct = {} + for d, faces in faces_by_depth.items(): + for fi, face in enumerate(faces): + tire_struct[(d, fi)] = tire_edges(H, edge_depth, d, face) + + children = defaultdict(list) + for node, p in parent_of.items(): + if p is not None and p != ('cut', None): + children[p].append(node) + + issues = [] + # Verify parent-child have shared edges + for ch_node, p_node in parent_of.items(): + if p_node is None or p_node == ('cut', None): + continue + ch_edges = set(tire_struct[ch_node]['all_edges']) + p_edges = set(tire_struct[p_node]['all_edges']) + shared = ch_edges & p_edges + if not shared: + issues.append({ + 'type': 'no_shared', + 'child': ch_node, 'parent': p_node, + 'n_ch_edges': len(ch_edges), + 'n_p_edges': len(p_edges), + }) + + max_d = max(d for (d, _) in tire_struct) + A = {} + for d in range(max_d, 0, -1): + for node in [n for n in tire_struct if n[0] == d]: + cs = enumerate_proper(tire_struct[node]) + if cs is None: + A[node] = None + continue + kids = children.get(node, []) + if not kids: + A[node] = cs + continue + # For each kid, build {shared_color_tuple: count} so + # parent can check existence quickly + kid_shared = [] + for ch in kids: + if A.get(ch) is None: + kid_shared.append(None) + continue + ch_struct = tire_struct[ch] + # Shared edges between parent and child + p_edges = tire_struct[node]['all_edges'] + p_eid = tire_struct[node]['edge_id'] + ch_eid = ch_struct['edge_id'] + shared_pairs = [] # (p_eid, ch_eid) + for e in p_edges: + if e in ch_eid: + shared_pairs.append((p_eid[e], ch_eid[e])) + if not shared_pairs: + kid_shared.append(None) + continue + # Build set of child shared-tuples (just keys we care) + ch_shared_set = set() + for ch_assign in A[ch]: + key = tuple(ch_assign[c_eid] for (_, c_eid) + in shared_pairs) + ch_shared_set.add(key) + kid_shared.append((shared_pairs, ch_shared_set)) + # Filter parent colorings by all kid constraints + achievable = [] + for assign in cs: + ok = True + for ks in kid_shared: + if ks is None: + continue + shared_pairs, ch_shared_set = ks + key = tuple(assign[p_eid] for (p_eid, _) in shared_pairs) + if key not in ch_shared_set: + ok = False; break + if ok: + achievable.append(assign) + A[node] = achievable + return A, tire_struct, issues + + +def proper_3_edge_colorings_full(G, max_n=100): + """Brute-force enumerate all proper 3-edge-colorings of G via + backtracking + constraint propagation. Returns list of dicts + {edge_tuple: color}. Only feasible for small G.""" + edges = [tuple(sorted(e[:2])) for e in G.edges()] + n = len(edges) + if n > max_n: + return None + edge_idx = {e: i for i, e in enumerate(edges)} + # Edges incident to each vertex + eav = defaultdict(list) + for i, (u, v) in enumerate(edges): + eav[u].append(i) + eav[v].append(i) + # Backtracking + colors = [-1] * n + results = [] + + def backtrack(i): + if i == n: + results.append(tuple(colors)) + return + u, v = edges[i] + used = set() + for j in eav[u]: + if colors[j] != -1: + used.add(colors[j]) + for j in eav[v]: + if colors[j] != -1: + used.add(colors[j]) + for c in range(3): + if c in used: + continue + colors[i] = c + backtrack(i + 1) + colors[i] = -1 + + backtrack(0) + return [{edges[i]: c for i, c in enumerate(r)} for r in results] + + +def project_to_cut(colorings, cut_edges): + """Project a list of full colorings to the given cut edges. + cut_edges is a list of original-graph edges (tuples). + Returns set of color tuples.""" + cut_norm = [tuple(sorted(e[:2])) for e in cut_edges] + out = set() + for col in colorings: + try: + out.add(tuple(col[ce] for ce in cut_norm)) + except KeyError: + continue + return out + + +def s3_orbit(t): + """Return S_3 orbit of tuple under color permutation.""" + return {tuple(p[c] for c in t) for p in itertools.permutations([0, 1, 2])} + + +def run_one_cut(G, cut_idx, cuts, base_pos, verbose=False): + S, cut_edges = cuts[cut_idx] + S0, S1 = S, frozenset(G.vertices()) - S + if len(S0) < 4 or len(S1) < 4: + return None + if verbose: + print(f' cut #{cut_idx}: |S0|={len(S0)}, |S1|={len(S1)}', + flush=True) + result = {} + # Ground truth: enumerate G's 3-edge-colorings, project to cut + g_cols = proper_3_edge_colorings_full(G) + if g_cols is None: + result['ground_truth'] = None + else: + R_ground = project_to_cut(g_cols, cut_edges) + result['R_ground'] = R_ground + if verbose: + print(f' G has {len(g_cols)} proper 3-edge-colorings, ' + f'|R_ground|={len(R_ground)}', flush=True) + # DP per side + for side, S_side in [('0', S0), ('1', S1)]: + pendant_id = max(G.vertices()) + 1 + (0 if side == '0' else 500) + try: + H, pos, ed, _, _, _ = apply_procedure( + G, S_side, cut_edges, base_pos, side, + pendant_start_id=pendant_id) + except Exception as e: + result[f'side_{side}_error'] = str(e) + continue + if not ed: + continue + faces_by_depth, parent_of, _ = build_tree(H, ed) + try: + A, struct, issues = chain_dp_joint( + faces_by_depth, parent_of, H, ed) + except Exception as e: + result[f'side_{side}_dp_error'] = str(e) + continue + if issues: + result[f'side_{side}_issues'] = issues + # Roots = depth-1 tires; project to cut (= depth-0 pendant edges + # in the H subgraph). Cut pendant edges are stored as pseudo + # edges in H. + # Cut edges in H: pendant edges at the original cut endpoints + cut_eids_in_H = [] + for u, v in cut_edges: + # In side-i view, only the side-i endpoint exists; the cut + # edge in H is between the original endpoint and a new + # pendant vertex. We need to identify these pendants. + pass + # Simpler: for each root tire T_1^{(f)} on this side, its out + # spokes (depth 0) ARE the cut edges (or some of them). + R_dp = set() + for node, a in A.items(): + if node[0] != 1 or a is None: + continue + # We want to identify cut edges in this tire's edges. + # Cut edges are depth-0 pendants in H. Filter struct's + # all_edges to depth-0 edges. + depth_0_eids = [i for i, e in enumerate(struct[node]['all_edges']) + if ed.get(e) == 0] + if not depth_0_eids: + continue + for assign in a: + tup = tuple(assign[i] for i in depth_0_eids) + # Also store WHICH cut edges (by tuple) + edges_used = [struct[node]['all_edges'][i] + for i in depth_0_eids] + # Project: key by edge tuple, value by color + R_dp.add(frozenset(zip(edges_used, tup))) + result[f'R_dp_{side}_raw'] = R_dp + # Convert frozenset-of-(edge,color) to a per-cut-edge color + # tuple (in fixed order of cut_edges) + cut_norm = [tuple(sorted(e[:2])) for e in cut_edges] + # The cut edges in H may have different endpoints (pendant + # vertices added by apply_procedure), so we need a mapping. + # For now, count cardinality and S3 stats. + result[f'R_dp_{side}_size'] = len(R_dp) + return result + + +def main(): + print('=== Dodecahedron ===', flush=True) + G = graphs.DodecahedralGraph() + G.is_planar(set_embedding=True) + base_pos = compute_nice_layout(G) + cuts = all_six_edge_cuts(G, max_cuts=3) + for cut_idx in range(min(3, len(cuts))): + res = run_one_cut(G, cut_idx, cuts, base_pos, verbose=True) + if res is None: + continue + if 'R_ground' in res: + R_ground = res['R_ground'] + print(f' ground truth |R|={len(R_ground)}, S_3 orbits=' + f'{len({frozenset(s3_orbit(t)) for t in R_ground})}', + flush=True) + for side in ['0', '1']: + sz = res.get(f'R_dp_{side}_size', 'N/A') + issues = res.get(f'side_{side}_issues') + print(f' side {side}: |R_dp|={sz}, ' + f'issues={len(issues) if issues else 0}', flush=True) + if issues: + for iss in issues[:2]: + print(f' {iss}', flush=True) + + +if __name__ == '__main__': + main() diff --git a/papers/coloring_nested_tire_graphs/notes/chain_half_analysis.aux b/papers/coloring_nested_tire_graphs/notes/chain_half_analysis.aux index 7b0c427..2c31d1d 100644 --- a/papers/coloring_nested_tire_graphs/notes/chain_half_analysis.aux +++ b/papers/coloring_nested_tire_graphs/notes/chain_half_analysis.aux @@ -15,4 +15,7 @@ \@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} +\@writefile{toc}{\contentsline {paragraph}{Fourth issue: when $H_d$ is a tree, the high-side forest is empty.}{4}{}\protected@file@percent } +\@writefile{toc}{\contentsline {paragraph}{Empirical baseline for ground-truth comparison.}{5}{}\protected@file@percent } +\@writefile{toc}{\contentsline {paragraph}{Path forward.}{5}{}\protected@file@percent } +\gdef \@abspage@last{5} diff --git a/papers/coloring_nested_tire_graphs/notes/chain_half_analysis.log b/papers/coloring_nested_tire_graphs/notes/chain_half_analysis.log index 12ddebc..c5691fb 100644 --- a/papers/coloring_nested_tire_graphs/notes/chain_half_analysis.log +++ b/papers/coloring_nested_tire_graphs/notes/chain_half_analysis.log @@ -1,4 +1,4 @@ -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 +This is pdfTeX, Version 3.141592653-2.6-1.40.24 (TeX Live 2022) (preloaded format=pdflatex 2022.10.5) 26 MAY 2026 22:58 entering extended mode restricted \write18 enabled. %&-line parsing enabled. @@ -280,38 +280,39 @@ 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) ) +[2] [3] [4] [5] (./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 + 350617 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} -Output written on chain_half_analysis.pdf (4 pages, 216066 bytes). + 55i,8n,62p,236b,243s stack positions out of 10000i,1000n,20000p,200000b,200000s +{/usr/local/texlive/2022/texmf-dis +t/fonts/enc/dvips/cm-super/cm-super-ts1.enc} +Output written on chain_half_analysis.pdf (5 pages, 229034 bytes). PDF statistics: - 103 PDF objects out of 1000 (max. 8388607) - 62 compressed objects within 1 object stream + 111 PDF objects out of 1000 (max. 8388607) + 67 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) diff --git a/papers/coloring_nested_tire_graphs/notes/chain_half_analysis.pdf b/papers/coloring_nested_tire_graphs/notes/chain_half_analysis.pdf index f037a3b..2cef3bc 100644 Binary files a/papers/coloring_nested_tire_graphs/notes/chain_half_analysis.pdf and b/papers/coloring_nested_tire_graphs/notes/chain_half_analysis.pdf differ diff --git a/papers/coloring_nested_tire_graphs/notes/chain_half_analysis.tex b/papers/coloring_nested_tire_graphs/notes/chain_half_analysis.tex index ed5b235..d676995 100644 --- a/papers/coloring_nested_tire_graphs/notes/chain_half_analysis.tex +++ b/papers/coloring_nested_tire_graphs/notes/chain_half_analysis.tex @@ -247,6 +247,46 @@ 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. +\paragraph{Fourth issue: when $H_d$ is a tree, the high-side +forest is empty.} The level-set lemma forces each face of $H_d$ +to be entirely low-side or entirely high-side. If $H_d$ has no +cycles (= tree/forest), it has a single face containing all +non-$H_d$ edges of $G'_i$, and that face must be one or the other +(low or high), not both. In the small-side regime (e.g.\ +dodecahedron $6$-edge cut with $|S_0| = 4$, side $0$), the BFS +only reaches depth $1$ and $H_1$ is a tree (5 edges, 6 vertices, +$1$ face containing the $6$ pendants). This single face is +low-side --- so the high-side cut tire forest of $G'_0$ is +\emph{empty}. + +\medskip + +In this case, the framework gives no high-side cut tires on +$G'_0$, hence $\mathcal{R}_0 = \emptyset$ by the framework's +projection to root out-spokes. But $G$ is $3$-edge-colorable +(dodecahedron is Hamiltonian, hence Tait colorable), so the +\emph{true} $\mathcal{R}_0$ is non-empty (= the cut-projection of +$G$'s $60$ colorings, giving $|R_{\mathrm{ground}}| = 36$). + +\medskip + +So the framework's high-side cut tire forest \emph{loses coverage} +for thin (small-$|S_i|$) cuts. The low-side face also carries +coloring information --- it contains the pendants and the +depth-$1$ subgraph --- but it isn't included as a "cut tire" in +the high-side formulation. + +This isn't strictly a bug in any proof so far; it's a coverage +gap in the framework's scope. To handle these cases, one of: +\begin{itemize} +\item Restrict the conjecture to cuts where both sides have + ``deep'' BFS (e.g.\ $\min(|S_0|, |S_1|) \ge k$ for some + $k$ ensuring high-side faces exist). +\item Extend the framework to include the low-side face as a + special ``boundary cut tire'' connecting pendants to the + $H_1$ structure. +\end{itemize} + \section*{Net status of the loose conjecture} \begin{center} @@ -256,12 +296,13 @@ planar embedding's face-in-face containment, not vertex overlap. component & status & note \\ \midrule Per-tire half (spoke-only $n \ge 3$) & proven & Prop 1.13 \\ -Per-tire half (branched) & open, needed for generality & \\ +Per-tire half (branched) & open & non-cycle face boundaries \\ 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 \\ +Joint projection DP & implemented, buggy & \texttt{chain\_dp\_joint.py} \\ +Coverage when $H_d$ is a tree & gap & low-side face carries info \\ 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 & \\ +Bottom-line $\mathcal{R}_0 \cap \mathcal{R}_1 \ne \emptyset$ & open & $G$-colorability gives it \\ \bottomrule \end{tabular} \end{center} @@ -270,10 +311,30 @@ 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 handling cases where $H_d$ is a tree (no high-side faces); \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. +The $S_3$-equivariance and forest structure (with high-side +restriction) are in hand. The full chain half is genuinely open +and the framework has coverage gaps not previously identified. + +\paragraph{Empirical baseline for ground-truth comparison.} +\texttt{chain\_dp\_joint.py} compares the chain DP output +against a brute-force enumeration of $G'_i$'s proper $3$-edge +colorings projected to the cut. On the dodecahedron, ground +truth shows $|R_{\mathrm{ground}}| \in \{36, 42, 48\}$ across +the first few cuts, but DP outputs $0$ for any side where +$H_1$ is a tree (= no high-side cut tires). This isn't a DP +bug per se --- it's the framework lacking coverage. + +\paragraph{Path forward.} The cleanest next step is probably +the \emph{boundary cut tire} extension: define an additional +``cut tire'' $T_0$ that represents the low-side face of $H_1$ ++ its incident pendants. $T_0$'s spokes are the cut edges; +its cycle structure is the boundary of the low-side face. +The high-side forest then sits ``inside'' $T_0$, and the chain +DP runs from leaves up through $T_0$ to the cut. This recovers +the missing coverage and gives a more uniform formulation. \end{document}