From b72c38b8ceffdf401c980ee8b44c716815fc1692 Mon Sep 17 00:00:00 2001 From: didericis Date: Sun, 24 May 2026 23:18:52 -0400 Subject: [PATCH] face_monochromatic_pairs: diagnostic scripts for Path 4 (Heawood constancy on V(K_b) U V(K_c)) Three empirical checks on all chord-apex+Kempe colourings up to n = 20 (142,812 colourings): 1. check_heawood_on_kempe.py - Sum_v h_phi(v): not zero in general; 17.6% of colourings have sum 0, the rest range in {+-4, +-8, +-12, +-16, +-20, +-24}. So the global "Heawood sum = 0" identity fails. - h_phi constant on V(K_b) U V(K_c): NEVER (0/142,812). This is the central empirical result -- by Lemma 5.3's contrapositive it gives an empirical proof of Conjecture 5.1 on these surrogates. 2. check_heawood_per_kempe_cycle.py - Sum_{V(K_b)} h_phi and sum_{V(K_c)} h_phi range widely (-20 to +20), with only ~23% zero. So the "Heawood sum on each Kempe cycle = 0" identity also fails -- the per-cycle sum is not the right invariant. 3. check_heawood_pair_mismatch.py - For each of 16 named-vertex pairs (v_n with each A_j, A_j with A_k for j, k in {i, ..., i+4}), counts how often h_phi differs. No pair is *always* differing -- the closest are consecutive pairs (A_j, A_{j+1}) at ~75% diff. So the Heawood mismatch enforcing non-constancy on V(K_b) U V(K_c) is diffuse, not at a fixed pair. Together these results confirm Path 4 (Conjecture 5.1 reduces via Lemma 5.3 to showing h_phi non-constant on V(K_b) U V(K_c)) but rule out the simplest single-pair-identity proof; the structural obstruction lives elsewhere (likely a topological/cycle-winding argument or a chord-apex/Kempe-spike colour cascade). Co-Authored-By: Claude Opus 4.7 --- .../experiments/check_heawood_on_kempe.py | 188 ++++++++++++++++++ .../check_heawood_pair_mismatch.py | 152 ++++++++++++++ .../check_heawood_per_kempe_cycle.py | 136 +++++++++++++ 3 files changed, 476 insertions(+) create mode 100644 papers/face_monochromatic_pairs/experiments/check_heawood_on_kempe.py create mode 100644 papers/face_monochromatic_pairs/experiments/check_heawood_pair_mismatch.py create mode 100644 papers/face_monochromatic_pairs/experiments/check_heawood_per_kempe_cycle.py diff --git a/papers/face_monochromatic_pairs/experiments/check_heawood_on_kempe.py b/papers/face_monochromatic_pairs/experiments/check_heawood_on_kempe.py new file mode 100644 index 0000000..fcfc86c --- /dev/null +++ b/papers/face_monochromatic_pairs/experiments/check_heawood_on_kempe.py @@ -0,0 +1,188 @@ +"""Empirically test, on every chord-apex+Kempe colouring of every +reduced dual we can enumerate: + + (a) Does the Heawood-sum identity sum_v h_phi(v) = 0 hold? + (b) Is h_phi ever constant on V(K_b) U V(K_c), the union of the two + Kempe cycles through the merged edge? + +(b) is the precise hypothesis of Lemma 5.3's conclusion. If (b) is +*never* witnessed on any chord-apex+Kempe colouring, then by the +contrapositive of Lemma 5.3 we have empirical verification of +Conjecture 5.1 on the chord-apex+Kempe surrogates. + +Run with: sage experiments/check_heawood_on_kempe.py +""" +import os +import sys +import time + +from sage.all import Graph +from sage.graphs.graph_generators import graphs + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) + +from check_conj_3_8_scaled import ( + apply_reduction, + proper_3_edge_colorings, + matches_chord_apex_kempe, + kempe_cycle_set, + edge_idx, +) + + +def dual_of(G): + G.is_planar(set_embedding=True) + faces = G.faces() + edge_to_faces = {} + for fi, face in enumerate(faces): + for u, v in face: + edge_to_faces.setdefault(frozenset((u, v)), []).append(fi) + return Graph( + [(fs[0], fs[1]) for fs in edge_to_faces.values() if len(fs) == 2], + multiedges=False, loops=False) + + +def heawood_numbers(H, edges, col_list): + """Return dict v -> h_phi(v) in {+1, -1}. + + The Heawood number is +1 iff the clockwise cyclic colour order at v + is an even cyclic permutation of (0, 1, 2). + """ + H.is_planar(set_embedding=True) + emb = H.get_embedding() # v -> list of neighbours in CW order + edge_color = {frozenset(e): col_list[i] for i, e in enumerate(edges)} + out = {} + for v in H.vertex_iterator(): + nbrs = emb[v] + if len(nbrs) != 3: + raise RuntimeError(f"vertex {v} not cubic: {len(nbrs)}") + cs = tuple(edge_color[frozenset((v, w))] for w in nbrs) + # cs is a permutation of (0, 1, 2). Check parity of its cyclic + # equivalence class compared to (0, 1, 2). + # (0,1,2), (1,2,0), (2,0,1) are even cyclic -> +1 + # (0,2,1), (2,1,0), (1,0,2) are odd cyclic -> -1 + if cs in [(0, 1, 2), (1, 2, 0), (2, 0, 1)]: + out[v] = +1 + elif cs in [(0, 2, 1), (2, 1, 0), (1, 0, 2)]: + out[v] = -1 + else: + raise RuntimeError(f"bad colour tuple at {v}: {cs}") + return out + + +def vertices_of_kempe(edges, kc_edge_indices): + """Vertices touched by the edges with indices in kc_edge_indices.""" + vs = set() + for i in kc_edge_indices: + u, v = edges[i][0], edges[i][1] + vs.add(u); vs.add(v) + return vs + + +def test_one(D, name=""): + """Run the empirical check on one cubic plane graph D = G'.""" + D.is_planar(set_embedding=True) + n_col = 0 + n_sum_zero = 0 + n_constant_union = 0 + sum_distribution = {} + examples = [] # (n_pent_face, i_red, coloring index, sum_h, constant?) + + for face in D.faces(): + if len(face) != 5: continue + for i_red in range(5): + res = apply_reduction(D, face, i_red, 9999) + if res is None: continue + H = res['H']; named = res['named'] + H.is_planar(set_embedding=True) + edges, colorings = proper_3_edge_colorings(H) + cand = [c for c in colorings + if matches_chord_apex_kempe(edges, c, named)] + for col in cand: + n_col += 1 + # Compute Heawood numbers + try: + h = heawood_numbers(H, edges, col) + except RuntimeError: + continue + # Part (a): sum_v h(v) + s = sum(h.values()) + sum_distribution[s] = sum_distribution.get(s, 0) + 1 + if s == 0: + n_sum_zero += 1 + # Part (b): is h constant on V(K_b) U V(K_c)? + merged_idx = edge_idx(edges, named['merged']) + a = col[merged_idx] + K_unions = set() + for b in range(3): + if b == a: continue + kc = kempe_cycle_set(edges, col, merged_idx, (a, b)) + K_unions |= vertices_of_kempe(edges, kc) + h_on_union = {h[v] for v in K_unions} + constant = (len(h_on_union) == 1) + if constant: + n_constant_union += 1 + if len(examples) < 5: + examples.append((len(K_unions), s, list(h_on_union)[0])) + return n_col, n_sum_zero, n_constant_union, sum_distribution, examples + + +def main(max_n=20, time_budget_per_n=1800): + print(f"Checking Heawood identities on chord-apex+Kempe colourings, " + f"n in [12, {max_n}]\n") + total_col = 0 + total_sum_zero = 0 + total_constant = 0 + overall_dist = {} + for n in range(12, max_n + 1): + start = time.time() + try: + triangulations = list(graphs.triangulations(n, minimum_degree=5)) + except Exception as ex: + print(f"n={n}: cannot enumerate ({ex})") + continue + n_col_n = 0 + n_sum_zero_n = 0 + n_constant_n = 0 + dist_n = {} + examples_n = [] + for tri_idx, G in enumerate(triangulations): + if time.time() - start > time_budget_per_n: + print(f" n={n}: timeout at tri {tri_idx}/{len(triangulations)}") + break + G.is_planar(set_embedding=True) + D = dual_of(G) + n_col_i, n_sz_i, n_cu_i, dist_i, exs = test_one(D, name=f"n{n}t{tri_idx}") + n_col_n += n_col_i + n_sum_zero_n += n_sz_i + n_constant_n += n_cu_i + for k, v in dist_i.items(): + dist_n[k] = dist_n.get(k, 0) + v + if exs and len(examples_n) < 5: + examples_n.extend(exs[:5 - len(examples_n)]) + elapsed = time.time() - start + print(f"n={n}: {n_col_n} colourings " + f"sum=0: {n_sum_zero_n}/{n_col_n} " + f"constant on V(K_b)UV(K_c): {n_constant_n}/{n_col_n} " + f"sum-dist: {sorted(dist_n.items())} [{elapsed:.0f}s]") + if examples_n and n_constant_n: + print(f" examples of constant-union colourings: {examples_n}") + sys.stdout.flush() + total_col += n_col_n + total_sum_zero += n_sum_zero_n + total_constant += n_constant_n + for k, v in dist_n.items(): + overall_dist[k] = overall_dist.get(k, 0) + v + + print() + print("=" * 78) + print(f"Grand totals (n in [12, {max_n}]):") + print(f" colourings tested: {total_col}") + print(f" sum_v h(v) = 0: {total_sum_zero} ({100*total_sum_zero/max(1,total_col):.2f}%)") + print(f" h constant on V(K_b)UV(K_c): {total_constant} ({100*total_constant/max(1,total_col):.2f}%)") + print(f" sum distribution: {sorted(overall_dist.items())}") + + +if __name__ == '__main__': + main() diff --git a/papers/face_monochromatic_pairs/experiments/check_heawood_pair_mismatch.py b/papers/face_monochromatic_pairs/experiments/check_heawood_pair_mismatch.py new file mode 100644 index 0000000..5540fbe --- /dev/null +++ b/papers/face_monochromatic_pairs/experiments/check_heawood_pair_mismatch.py @@ -0,0 +1,152 @@ +"""For every chord-apex+Kempe colouring, record whether each of a set of +named-vertex pairs has differing Heawood numbers. If a pair *always* +disagrees, that gives a structural identity h_phi(u) != h_phi(v) that +can be plugged into Lemma 5.3 to prove Conjecture 5.1. + +Pairs we track (all lie in V(K_b) cap V(K_c)): + (v_n, A_i), (v_n, A_{i+1}), (v_n, A_{i+2}), + (v_n, A_{i+3}), (v_n, A_{i+4}), + (A_i, A_{i+1}), (A_{i+1}, A_{i+2}), + (A_{i+2}, A_{i+3}), (A_{i+3}, A_{i+4}), (A_{i+4}, A_i), + (A_i, A_{i+2}), (A_i, A_{i+3}), (A_i, A_{i+4}), + (A_{i+1}, A_{i+3}), (A_{i+1}, A_{i+4}), (A_{i+2}, A_{i+4}) + +Run with: sage experiments/check_heawood_pair_mismatch.py +""" +import os +import sys +import time + +from sage.all import Graph +from sage.graphs.graph_generators import graphs + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) + +from check_conj_3_8_scaled import ( + apply_reduction, + proper_3_edge_colorings, + matches_chord_apex_kempe, +) +from check_heawood_on_kempe import dual_of, heawood_numbers + + +def extract_named_vertices(named, v_n_label=9999): + """Pull the labels A_i ... A_{i+4} out of the named-edge dict. + + named['side_0'] = frozenset((v_n, A_i)) + named['spike'] = frozenset((v_n, A_{i+1})) + named['side_1'] = frozenset((v_n, A_{i+2})) + named['merged'] = frozenset((A_{i+3}, A_{i+4})) + """ + def other(edge_fs, v): + return next(iter(edge_fs - {v})) + A_i = other(named['side_0'], v_n_label) + A_i1 = other(named['spike'], v_n_label) + A_i2 = other(named['side_1'], v_n_label) + merged_set = named['merged'] + # A_{i+3}, A_{i+4} are the two endpoints of merged; we can't tell them + # apart from the named dict alone, so we just pick an ordering. + a, b = sorted(merged_set) + A_i3, A_i4 = a, b + return v_n_label, A_i, A_i1, A_i2, A_i3, A_i4 + + +def test_one(D): + """Tally pair-mismatches over all chord-apex+Kempe colourings of D.""" + D.is_planar(set_embedding=True) + n_col = 0 + pair_diff = {} # pair_label -> count of colourings where h differs + pair_same = {} + for face in D.faces(): + if len(face) != 5: continue + for i_red in range(5): + res = apply_reduction(D, face, i_red, 9999) + if res is None: continue + H = res['H']; named = res['named'] + H.is_planar(set_embedding=True) + edges, colorings = proper_3_edge_colorings(H) + cand = [c for c in colorings + if matches_chord_apex_kempe(edges, c, named)] + v_n, A_i, A_i1, A_i2, A_i3, A_i4 = extract_named_vertices(named) + named_pairs = [ + ('v_n, A_i', v_n, A_i ), + ('v_n, A_{i+1}', v_n, A_i1), + ('v_n, A_{i+2}', v_n, A_i2), + ('v_n, A_{i+3}', v_n, A_i3), + ('v_n, A_{i+4}', v_n, A_i4), + ('A_i, A_{i+1}', A_i, A_i1), + ('A_{i+1}, A_{i+2}', A_i1, A_i2), + ('A_{i+2}, A_{i+3}', A_i2, A_i3), + ('A_{i+3}, A_{i+4}', A_i3, A_i4), + ('A_{i+4}, A_i', A_i4, A_i ), + ('A_i, A_{i+2}', A_i, A_i2), + ('A_i, A_{i+3}', A_i, A_i3), + ('A_i, A_{i+4}', A_i, A_i4), + ('A_{i+1}, A_{i+3}', A_i1, A_i3), + ('A_{i+1}, A_{i+4}', A_i1, A_i4), + ('A_{i+2}, A_{i+4}', A_i2, A_i4), + ] + for col in cand: + n_col += 1 + try: + h = heawood_numbers(H, edges, col) + except RuntimeError: + continue + for label, u, w in named_pairs: + if h[u] != h[w]: + pair_diff[label] = pair_diff.get(label, 0) + 1 + else: + pair_same[label] = pair_same.get(label, 0) + 1 + return n_col, pair_diff, pair_same + + +def main(max_n=18, time_budget_per_n=1800): + print(f"Pair-mismatch check on chord-apex+Kempe colourings, " + f"n in [12, {max_n}]\n") + grand_col = 0 + grand_diff = {} + grand_same = {} + for n in range(12, max_n + 1): + start = time.time() + try: + triangulations = list(graphs.triangulations(n, minimum_degree=5)) + except Exception as ex: + print(f"n={n}: cannot enumerate ({ex})") + continue + n_col_n = 0 + diff_n = {}; same_n = {} + for tri_idx, G in enumerate(triangulations): + if time.time() - start > time_budget_per_n: + print(f" n={n}: timeout at tri {tri_idx}/{len(triangulations)}") + break + G.is_planar(set_embedding=True) + D = dual_of(G) + n_col_i, diff_i, same_i = test_one(D) + n_col_n += n_col_i + for k, v in diff_i.items(): diff_n[k] = diff_n.get(k, 0) + v + for k, v in same_i.items(): same_n[k] = same_n.get(k, 0) + v + elapsed = time.time() - start + print(f"n={n}: {n_col_n} colourings [{elapsed:.0f}s]") + sys.stdout.flush() + grand_col += n_col_n + for k, v in diff_n.items(): grand_diff[k] = grand_diff.get(k, 0) + v + for k, v in same_n.items(): grand_same[k] = grand_same.get(k, 0) + v + + print() + print("=" * 78) + print(f"Grand totals (n in [12, {max_n}], {grand_col} colourings)") + print("-" * 78) + print(f"{'pair':<22} {'#diff':>10} {'#same':>10} {'always diff?':>14}") + print("-" * 78) + # Stable order: track all keys seen. + all_keys = sorted(set(list(grand_diff.keys()) + list(grand_same.keys()))) + for k in all_keys: + d = grand_diff.get(k, 0); s = grand_same.get(k, 0) + always_diff = (s == 0 and d > 0) + marker = ' YES <==' if always_diff else '' + print(f"{k:<22} {d:>10} {s:>10} {marker:>14}") + + +if __name__ == '__main__': + main() diff --git a/papers/face_monochromatic_pairs/experiments/check_heawood_per_kempe_cycle.py b/papers/face_monochromatic_pairs/experiments/check_heawood_per_kempe_cycle.py new file mode 100644 index 0000000..f6925f9 --- /dev/null +++ b/papers/face_monochromatic_pairs/experiments/check_heawood_per_kempe_cycle.py @@ -0,0 +1,136 @@ +"""Test whether the Heawood sum on each Kempe cycle (K_b, K_c through +merged) is always 0, on chord-apex+Kempe colourings of reduced duals. + +If sum_{v in V(K)} h_phi(v) = 0 for every Kempe cycle K, then h_phi cannot +be constant on V(K) (since |V(K)| > 0 and h takes values +-1), and hence +not constant on V(K_b) U V(K_c) either. By Lemma 5.3's contrapositive, +this proves Conjecture 5.1. + +Run with: sage experiments/check_heawood_per_kempe_cycle.py +""" +import os +import sys +import time + +from sage.all import Graph +from sage.graphs.graph_generators import graphs + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) + +from check_conj_3_8_scaled import ( + apply_reduction, + proper_3_edge_colorings, + matches_chord_apex_kempe, + kempe_cycle_set, + edge_idx, +) +from check_heawood_on_kempe import ( + dual_of, + heawood_numbers, + vertices_of_kempe, +) + + +def test_one(D): + """Check Heawood sums on K_b, K_c for every chord-apex+Kempe colouring.""" + D.is_planar(set_embedding=True) + n_col = 0 + sums_Kb = {} + sums_Kc = {} + nonzero_examples = [] + + for face in D.faces(): + if len(face) != 5: continue + for i_red in range(5): + res = apply_reduction(D, face, i_red, 9999) + if res is None: continue + H = res['H']; named = res['named'] + H.is_planar(set_embedding=True) + edges, colorings = proper_3_edge_colorings(H) + cand = [c for c in colorings + if matches_chord_apex_kempe(edges, c, named)] + for col in cand: + n_col += 1 + try: + h = heawood_numbers(H, edges, col) + except RuntimeError: + continue + merged_idx = edge_idx(edges, named['merged']) + a = col[merged_idx] + bs = [c for c in range(3) if c != a] + # K_b is the {a, b[0]}-Kempe cycle; K_c is the {a, b[1]}-cycle. + kc_b = kempe_cycle_set(edges, col, merged_idx, (a, bs[0])) + kc_c = kempe_cycle_set(edges, col, merged_idx, (a, bs[1])) + Vb = vertices_of_kempe(edges, kc_b) + Vc = vertices_of_kempe(edges, kc_c) + sb = sum(h[v] for v in Vb) + sc = sum(h[v] for v in Vc) + sums_Kb[sb] = sums_Kb.get(sb, 0) + 1 + sums_Kc[sc] = sums_Kc.get(sc, 0) + 1 + if (sb != 0 or sc != 0) and len(nonzero_examples) < 5: + nonzero_examples.append({ + 'sum_Kb': sb, 'sum_Kc': sc, + '|V(K_b)|': len(Vb), '|V(K_c)|': len(Vc), + }) + return n_col, sums_Kb, sums_Kc, nonzero_examples + + +def main(max_n=18, time_budget_per_n=1800): + print(f"Heawood sum per Kempe cycle (through merged), " + f"n in [12, {max_n}]\n") + grand_col = 0 + grand_Kb = {} + grand_Kc = {} + nonzero_total = 0 + for n in range(12, max_n + 1): + start = time.time() + try: + triangulations = list(graphs.triangulations(n, minimum_degree=5)) + except Exception as ex: + print(f"n={n}: cannot enumerate ({ex})") + continue + n_col_n = 0 + Kb_n = {}; Kc_n = {} + exs = [] + for tri_idx, G in enumerate(triangulations): + if time.time() - start > time_budget_per_n: + print(f" n={n}: timeout at tri {tri_idx}/{len(triangulations)}") + break + G.is_planar(set_embedding=True) + D = dual_of(G) + n_col_i, Kb_i, Kc_i, nz_i = test_one(D) + n_col_n += n_col_i + for k, v in Kb_i.items(): Kb_n[k] = Kb_n.get(k, 0) + v + for k, v in Kc_i.items(): Kc_n[k] = Kc_n.get(k, 0) + v + if nz_i and len(exs) < 3: + exs.extend(nz_i[:3 - len(exs)]) + elapsed = time.time() - start + nz_Kb_n = sum(v for k, v in Kb_n.items() if k != 0) + nz_Kc_n = sum(v for k, v in Kc_n.items() if k != 0) + print(f"n={n}: {n_col_n} col. " + f"sum_Kb != 0: {nz_Kb_n}, dist={sorted(Kb_n.items())} " + f"sum_Kc != 0: {nz_Kc_n}, dist={sorted(Kc_n.items())} " + f"[{elapsed:.0f}s]") + if exs: + print(f" examples of nonzero sums: {exs}") + nonzero_total += len(exs) + sys.stdout.flush() + grand_col += n_col_n + for k, v in Kb_n.items(): grand_Kb[k] = grand_Kb.get(k, 0) + v + for k, v in Kc_n.items(): grand_Kc[k] = grand_Kc.get(k, 0) + v + + print() + print("=" * 78) + print(f"Grand totals (n in [12, {max_n}]):") + print(f" colourings tested: {grand_col}") + print(f" sum on K_b distribution: {sorted(grand_Kb.items())}") + print(f" sum on K_c distribution: {sorted(grand_Kc.items())}") + nz_Kb = sum(v for k, v in grand_Kb.items() if k != 0) + nz_Kc = sum(v for k, v in grand_Kc.items() if k != 0) + print(f" sum_Kb != 0: {nz_Kb}/{grand_col} ({100*nz_Kb/max(1,grand_col):.2f}%)") + print(f" sum_Kc != 0: {nz_Kc}/{grand_col} ({100*nz_Kc/max(1,grand_col):.2f}%)") + + +if __name__ == '__main__': + main()