From e6880371ff8bc4bc41ce3e291602c9a64da34f1b Mon Sep 17 00:00:00 2001 From: didericis Date: Mon, 25 May 2026 00:53:55 -0400 Subject: [PATCH] face_monochromatic_pairs: minimum-flip and minority-location diagnostics Two more diagnostics on chord-apex+Kempe colourings (n <= 18, 13,800 colourings) probing how thin the non-constancy obstacle on V(K_b) is: 1. check_min_flip_structure.py - Flip count on K_b drops as low as 2 (at n = 18, 12 colourings): these have a single minority Heawood vertex on K_b. So the structural obstacle has NO slack: proving "at least 1 minority vertex on V(K_b)" is the bar. - All n=14 colourings (216) have flip count = 8 exactly. At larger n the distribution spreads. 2. check_minority_location.py - For colourings with K_b flip count <= 4, identify the minority Heawood vertices and tally where they sit: v_n : 12.86% A_{i+1} : 10.82% A_{i+2} : 8.98% A_i : 7.76% A_{i+4} : 5.31% A_{i+3} : 5.10% "other" : 49.18% - About half the minority vertices live on non-named vertices in the rest of G'. No single named vertex is *always* the minority. The obstruction is genuinely diffuse / global, not anchored to a specific structural location. These together imply that the structural proof of "h_phi non-constant on V(K_b)" must be global (no local "this vertex must flip" argument suffices) and handle the edge case where only one minority vertex exists. Likely requires a topological / homological / global counting argument. Co-Authored-By: Claude Opus 4.7 --- .../experiments/check_min_flip_structure.py | 129 ++++++++++++++++ .../experiments/check_minority_location.py | 141 ++++++++++++++++++ 2 files changed, 270 insertions(+) create mode 100644 papers/face_monochromatic_pairs/experiments/check_min_flip_structure.py create mode 100644 papers/face_monochromatic_pairs/experiments/check_minority_location.py diff --git a/papers/face_monochromatic_pairs/experiments/check_min_flip_structure.py b/papers/face_monochromatic_pairs/experiments/check_min_flip_structure.py new file mode 100644 index 0000000..d76a368 --- /dev/null +++ b/papers/face_monochromatic_pairs/experiments/check_min_flip_structure.py @@ -0,0 +1,129 @@ +"""For each chord-apex+Kempe colouring, walk K_b and record: + + - The Heawood-flip count along K_b (= # consecutive (v_k, v_{k+1}) + pairs with h_phi(v_k) != h_phi(v_{k+1})). + - The Heawood-flip count is at least 4 empirically (n >= 14). + +For colourings achieving the minimum flip count on K_b, dump the +sequence of (Heawood, edge-position-on-cycle) so we can see *where* +the flips fall. Are they at the merged edge? At spike? At specific +structural locations? Or spread out? + +Run with: sage experiments/check_min_flip_structure.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, +) +from check_heawood_on_kempe import dual_of, heawood_numbers + + +def test_one(D): + D.is_planar(set_embedding=True) + n_col = 0 + # For each Kempe cycle (K_b, K_c), record (flip_count, length). + flips_kb = {} # flip_count -> count + # For minimum-flip colourings, record the Heawood pattern + edge colours. + min_patterns = [] # tuples (flip_count, length, pattern, colours) + + cur_min = None + 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] + # Only check K_b for this script + walk_b = trace_kempe_cycle(edges, col, merged_idx, (a, bs[0])) + L = len(walk_b) + h_seq = [h[walk_b[k][1]] for k in range(L)] + # edge sequence: walk_b[k][0] is the edge index of the K_b + # edge ENTERING walk_b[k][1]. + e_colors = [col[walk_b[k][0]] for k in range(L)] + # Flip count: pairs (h_seq[k], h_seq[(k+1) % L]) that differ. + flips = sum(1 for k in range(L) if h_seq[k] != h_seq[(k+1) % L]) + flips_kb[flips] = flips_kb.get(flips, 0) + 1 + if cur_min is None or flips < cur_min: + cur_min = flips + min_patterns = [(flips, L, tuple(h_seq), tuple(e_colors))] + elif flips == cur_min and len(min_patterns) < 5: + min_patterns.append((flips, L, tuple(h_seq), tuple(e_colors))) + return n_col, flips_kb, cur_min, min_patterns + + +def main(max_n=18, time_budget_per_n=1800): + print(f"Min-flip Heawood pattern on K_b, n in [12, {max_n}]\n") + overall_min = None + overall_min_examples = [] + 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 + flips_n = {} + n_min = None + min_examples = [] + 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, fi, cm, exs = test_one(D) + n_col_n += ni + for k, v in fi.items(): flips_n[k] = flips_n.get(k, 0) + v + if cm is not None: + if n_min is None or cm < n_min: + n_min = cm + min_examples = list(exs) + elif cm == n_min and len(min_examples) < 5: + min_examples.extend(exs[:5 - len(min_examples)]) + elapsed = time.time() - start + print(f"n={n}: {n_col_n} col., flip_dist on K_b: " + f"{sorted(flips_n.items())}, min flip: {n_min} [{elapsed:.0f}s]") + if min_examples: + print(f" Examples of min-flip K_b (flip count = {n_min}):") + for ex in min_examples[:3]: + flips_x, L, h_pat, e_cols = ex + print(f" L={L}, flips={flips_x}") + print(f" h sequence : {h_pat}") + print(f" colour seq : {e_cols}") + sys.stdout.flush() + if overall_min is None or (n_min is not None and n_min < overall_min): + overall_min = n_min + overall_min_examples = min_examples + print() + print(f"Overall minimum flip count on K_b across all tested: {overall_min}") + + +if __name__ == '__main__': + main() diff --git a/papers/face_monochromatic_pairs/experiments/check_minority_location.py b/papers/face_monochromatic_pairs/experiments/check_minority_location.py new file mode 100644 index 0000000..3989e04 --- /dev/null +++ b/papers/face_monochromatic_pairs/experiments/check_minority_location.py @@ -0,0 +1,141 @@ +"""For colourings whose K_b Heawood pattern is "almost constant" (flip +count <= some threshold), identify *which* vertex carries the +minority Heawood. Is it consistently a "named" structural vertex +(v_n, A_{i+1}, A_{i+3}, A_{i+4}, ...) or distributed across +non-structural vertices? + +If the minority vertex is always a specific structural vertex, that +gives a *local* structural proof: the embedding + chord-apex forces +the minority's Heawood to differ from the majority's, ruling out +constancy on V(K_b). + +Run with: sage experiments/check_minority_location.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 + + +def named_vertices(named, v_n=9999): + def other(fs, v): + return next(iter(fs - {v})) + A_i = other(named['side_0'], v_n) + A_i1 = other(named['spike'], v_n) + A_i2 = other(named['side_1'], v_n) + A_i3, A_i4 = sorted(named['merged']) + return {'v_n': v_n, 'A_i': A_i, 'A_i1': A_i1, 'A_i2': A_i2, + 'A_i3': A_i3, 'A_i4': A_i4} + + +def test_one(D, flip_threshold=4): + D.is_planar(set_embedding=True) + n_col = 0 + # For colourings with K_b flip count <= flip_threshold, record what + # type of vertex carries the minority h on V(K_b). + minority_types = {} # 'v_n', 'A_i1', etc., or 'other' -> count + # Also: number of minority vertices on V(K_b) in these colourings. + minority_size_dist = {} + 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)] + named_v = named_vertices(named, 9999) + inv_named = {v: name for name, v in named_v.items()} + 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] + kc_b = kempe_cycle_set(edges, col, merged_idx, (a, bs[0])) + V_b = vertices_of_kempe(edges, kc_b) + # Compute flip count on K_b walk + walk = trace_kempe_cycle(edges, col, merged_idx, (a, bs[0])) + L = len(walk) + h_seq = [h[walk[k][1]] for k in range(L)] + flips = sum(1 for k in range(L) if h_seq[k] != h_seq[(k+1) % L]) + if flips > flip_threshold: continue + # Identify minority on V(K_b) + plus = sum(1 for v in V_b if h[v] == 1) + minus = sum(1 for v in V_b if h[v] == -1) + if plus == minus: continue # tie -- skip + minority_h = 1 if plus < minus else -1 + minority_vs = [v for v in V_b if h[v] == minority_h] + minority_size_dist[len(minority_vs)] = \ + minority_size_dist.get(len(minority_vs), 0) + 1 + for v in minority_vs: + name = inv_named.get(v, 'other') + minority_types[name] = minority_types.get(name, 0) + 1 + return n_col, minority_types, minority_size_dist + + +def main(max_n=18, time_budget_per_n=1800, flip_threshold=4): + print(f"Minority-vertex location on K_b for low-flip colourings, " + f"n in [12, {max_n}], flip <= {flip_threshold}\n") + grand_col = 0 + grand_types = {} + grand_sizes = {} + 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 + 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, mt, ms = test_one(D, flip_threshold) + n_col_n += ni + for k, v in mt.items(): grand_types[k] = grand_types.get(k, 0) + v + for k, v in ms.items(): grand_sizes[k] = grand_sizes.get(k, 0) + v + elapsed = time.time() - start + print(f"n={n}: {n_col_n} col., [{elapsed:.0f}s]") + sys.stdout.flush() + grand_col += n_col_n + print() + print("=" * 78) + print(f"Grand totals: colourings with K_b flip-count <= {flip_threshold} " + f"({grand_col} total colourings)") + total_min = sum(grand_sizes.values()) + print(f"\n Minority-set size distribution on V(K_b) " + f"(of low-flip colourings): {sorted(grand_sizes.items())}") + print(f"\n Type breakdown of minority vertices " + f"(over {sum(grand_types.values())} minority-vertex incidences):") + for name in ['v_n', 'A_i', 'A_i1', 'A_i2', 'A_i3', 'A_i4', 'other']: + c = grand_types.get(name, 0) + pct = 100 * c / max(1, sum(grand_types.values())) + print(f" {name:>5}: {c} ({pct:.2f}%)") + + +if __name__ == '__main__': + main()