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 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 00:00:08 -04:00
parent cebe6e5dbd
commit d659bf40d5
@@ -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()