From d659bf40d5408f71ecd596895862398e5aa4c69a Mon Sep 17 00:00:00 2001 From: didericis Date: Mon, 25 May 2026 00:00:08 -0400 Subject: [PATCH] face_monochromatic_pairs: instrument K_b cap K_c size and per-cycle flip counts For each chord-apex+Kempe colouring, record: - |V(K_b)|, |V(K_c)|, |V(K_b) cap V(K_c)|, |V(K_b) cup V(K_c)| - "Flip count" on each cycle: #consecutive pairs whose third-colour edges lie on opposite local sides (= #same-Heawood pairs by Lemma A). Results (n in [12, 18], 13,800 colourings): - |V(K_b) cap V(K_c)| is NEVER 2 -- always >= 6. The two Kempe cycles through merged share many vertices. - Distributions of flip_Kb and flip_Kc are identical multisets (consistent with b <-> c symmetry of the construction). - But per-colouring, flip_Kb == flip_Kc only 39.65% of the time -- the symmetry is statistical, not pointwise. - Max observed flip count is 20, never the maximum possible |V(K)|. Consistent with h_phi never being constant on V(K_b) U V(K_c). The substantial overlap of K_b and K_c (>= 6 shared vertices) means the constancy hypothesis would impose simultaneous alternation constraints from both cycles at every shared vertex -- the topological "trap" the proof needs to exploit. Co-Authored-By: Claude Opus 4.7 --- ...heck_kempe_intersection_and_alternation.py | 198 ++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 papers/face_monochromatic_pairs/experiments/check_kempe_intersection_and_alternation.py diff --git a/papers/face_monochromatic_pairs/experiments/check_kempe_intersection_and_alternation.py b/papers/face_monochromatic_pairs/experiments/check_kempe_intersection_and_alternation.py new file mode 100644 index 0000000..9a6a753 --- /dev/null +++ b/papers/face_monochromatic_pairs/experiments/check_kempe_intersection_and_alternation.py @@ -0,0 +1,198 @@ +"""For every chord-apex+Kempe colouring of every reduced dual we can +enumerate, record: + + (1) |V(K_b)|, |V(K_c)|, |V(K_b) cap V(K_c)|, |V(K_b) cup V(K_c)|. + In particular: is |V(K_b) cap V(K_c)| typically 2 (just the + merged endpoints) or larger? + + (2) "Side-flip count" on each Kempe cycle: walking K_b in cycle + order, count the number of consecutive K_b-pairs (v_0, v_1) + where the c-edges at v_0 and v_1 lie on opposite local sides. + Same for K_c with b-edges. + + Under Lemma A (now empirically confirmed), the flip count + equals the number of "same-Heawood" consecutive pairs on the + cycle. So: + - constant Heawood on K_b => flip count = |V(K_b)| (max) + - perfectly alternating h on K_b => flip count = 0 + Anything in between corresponds to a Heawood pattern that's + neither constant nor perfectly alternating. + + We tally the flip counts for K_b and K_c, and whether they + match or differ across the same colouring. + +Run with: sage experiments/check_kempe_intersection_and_alternation.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, + trace_kempe_cycle, + edge_idx, + kempe_cycle_set, +) +from check_heawood_on_kempe import dual_of, heawood_numbers, vertices_of_kempe +from check_heawood_local_side import c_edge_local_side + + +def cycle_data(H, edges, col, emb, merged_idx, a, b): + """Return (V_cycle, side_flips) for the {a, b}-Kempe cycle through + the edge at merged_idx. side_flips counts consecutive pairs whose + third-colour edges lie on opposite local sides (= #{same-h pairs} + via Lemma A).""" + walk = trace_kempe_cycle(edges, col, merged_idx, (a, b)) + if not walk: + return set(), 0 + L = len(walk) + third_color = 3 - a - b + sides = [] + V_cycle = set() + for k in range(L): + v = walk[k][1] + V_cycle.add(v) + in_e = edges[walk[k][0]] + out_e = edges[walk[(k + 1) % L][0]] + u_in = in_e[0] if in_e[1] == v else in_e[1] + u_out = out_e[0] if out_e[1] == v else out_e[1] + side = c_edge_local_side(v, third_color, col, edges, emb, + u_in, u_out) + sides.append(side) + flips = 0 + for k in range(L): + s0 = sides[k]; s1 = sides[(k + 1) % L] + if s0 is not None and s1 is not None and s0 != s1: + flips += 1 + return V_cycle, flips + + +def test_one(D): + D.is_planar(set_embedding=True) + n_col = 0 + rec = { + 'cap_size': {}, # |V(K_b) cap V(K_c)| histogram + 'cap_eq_2': 0, # how often the intersection is exactly 2 + 'cap_size_minus_2': {},# |V(K_b) cap V(K_c)| - 2 histogram + 'kb_size': {}, + 'kc_size': {}, + 'union_size': {}, + 'flip_kb': {}, # K_b side-flip count + 'flip_kc': {}, # K_c side-flip count + 'flip_match': 0, # how often flip_Kb == flip_Kc + 'flip_match_pct_bucket': {}, # bucket of |fkb - fkc| / max + } + 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) + emb = H.get_embedding() + 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 + merged_idx = edge_idx(edges, named['merged']) + a = col[merged_idx] + bs = [c for c in range(3) if c != a] + Vb, fb = cycle_data(H, edges, col, emb, merged_idx, a, bs[0]) + Vc, fc = cycle_data(H, edges, col, emb, merged_idx, a, bs[1]) + cap = Vb & Vc + un = Vb | Vc + rec['cap_size'][len(cap)] = rec['cap_size'].get(len(cap), 0) + 1 + rec['cap_size_minus_2'][len(cap) - 2] = \ + rec['cap_size_minus_2'].get(len(cap) - 2, 0) + 1 + if len(cap) == 2: + rec['cap_eq_2'] += 1 + rec['kb_size'][len(Vb)] = rec['kb_size'].get(len(Vb), 0) + 1 + rec['kc_size'][len(Vc)] = rec['kc_size'].get(len(Vc), 0) + 1 + rec['union_size'][len(un)] = rec['union_size'].get(len(un), 0) + 1 + rec['flip_kb'][fb] = rec['flip_kb'].get(fb, 0) + 1 + rec['flip_kc'][fc] = rec['flip_kc'].get(fc, 0) + 1 + if fb == fc: + rec['flip_match'] += 1 + return n_col, rec + + +def merge_into(grand, n_rec): + for key in ('cap_size', 'cap_size_minus_2', 'kb_size', 'kc_size', + 'union_size', 'flip_kb', 'flip_kc'): + for k, v in n_rec[key].items(): + grand[key][k] = grand[key].get(k, 0) + v + grand['cap_eq_2'] += n_rec['cap_eq_2'] + grand['flip_match'] += n_rec['flip_match'] + + +def main(max_n=18, time_budget_per_n=1800): + print(f"K_b/K_c intersection sizes and side-flip counts, " + f"n in [12, {max_n}]\n") + grand = { + 'cap_size': {}, 'cap_size_minus_2': {}, 'cap_eq_2': 0, + 'kb_size': {}, 'kc_size': {}, 'union_size': {}, + 'flip_kb': {}, 'flip_kc': {}, 'flip_match': 0, + } + grand_col = 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 + n_rec = { + 'cap_size': {}, 'cap_size_minus_2': {}, 'cap_eq_2': 0, + 'kb_size': {}, 'kc_size': {}, 'union_size': {}, + 'flip_kb': {}, 'flip_kc': {}, 'flip_match': 0, + } + 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) + ni, ri = test_one(D) + n_col_n += ni + merge_into(n_rec, ri) + elapsed = time.time() - start + print(f"n={n}: {n_col_n} col., [{elapsed:.0f}s]") + print(f" |V(K_b) cap V(K_c)| dist: {sorted(n_rec['cap_size'].items())}") + print(f" cap == 2: {n_rec['cap_eq_2']}/{n_col_n}") + print(f" flip_kb dist: {sorted(n_rec['flip_kb'].items())}") + print(f" flip_kc dist: {sorted(n_rec['flip_kc'].items())}") + print(f" flip_kb == flip_kc: {n_rec['flip_match']}/{n_col_n}") + sys.stdout.flush() + grand_col += n_col_n + merge_into(grand, n_rec) + + print() + print("=" * 78) + print(f"Grand totals (n in [12, {max_n}], {grand_col} colourings):") + print(f" |V(K_b) cap V(K_c)| distribution: " + f"{sorted(grand['cap_size'].items())}") + cap_2 = grand['cap_eq_2'] + print(f" |V(K_b) cap V(K_c)| == 2: {cap_2}/{grand_col} " + f"({100*cap_2/max(1,grand_col):.2f}%)") + print(f" |V(K_b)| dist: {sorted(grand['kb_size'].items())}") + print(f" |V(K_c)| dist: {sorted(grand['kc_size'].items())}") + print(f" |V(K_b) cup V(K_c)| dist: {sorted(grand['union_size'].items())}") + print(f" flip_kb dist: {sorted(grand['flip_kb'].items())}") + print(f" flip_kc dist: {sorted(grand['flip_kc'].items())}") + fm = grand['flip_match'] + print(f" flip_kb == flip_kc: {fm}/{grand_col} " + f"({100*fm/max(1,grand_col):.2f}%)") + + +if __name__ == '__main__': + main()