From a29d145cecbcdf4446d444918ce51833f4b259b7 Mon Sep 17 00:00:00 2001 From: didericis Date: Mon, 25 May 2026 00:32:07 -0400 Subject: [PATCH] face_monochromatic_pairs: cycle-side splits and shared-vertex transitions For each chord-apex+Kempe colouring (n in [12, 18]), record: (1) (#L, #R) split of c-edge sides along K_b and K_c. #L == #R only in 35.43% of colourings (the rest have unbalanced sides -- consistent with the empirical Heawood non-constancy). (2) Ordered sequence of (i_b mod 2, i_c mod 2) parity pairs at shared K_b cap K_c vertices in K_b walk order, plus a tally of transitions in the 4-state space. Two clean structural observations on the transition matrix: (A) i_b parity strictly alternates between consecutive shared K_b-vertices. Every transition goes (0, *) -> (1, *) or (1, *) -> (0, *); transitions within (0, *) or within (1, *) are never observed. So shared positions on K_b alternate even/odd in walk order -- the gap on K_b between consecutive shared vertices is always odd. (B) From odd-i_b states, i_c parity must flip too: (1, 0) only transitions to (0, 1) and (1, 1) only to (0, 0). From even-i_b states, both i_c outcomes occur. (B) is explained structurally: at an odd-i_b shared vertex K_b leaves via the a-edge (which is also on K_c), so K_b and K_c traverse the same edge and K_c advances exactly one step, flipping i_c. At an even-i_b shared vertex K_b leaves via the b-edge (off K_c), so K_c advances at its own pace and i_c can be either parity at the next shared vertex. Co-Authored-By: Claude Opus 4.7 --- .../check_shared_sequence_and_winding.py | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 papers/face_monochromatic_pairs/experiments/check_shared_sequence_and_winding.py diff --git a/papers/face_monochromatic_pairs/experiments/check_shared_sequence_and_winding.py b/papers/face_monochromatic_pairs/experiments/check_shared_sequence_and_winding.py new file mode 100644 index 0000000..b1c9b02 --- /dev/null +++ b/papers/face_monochromatic_pairs/experiments/check_shared_sequence_and_winding.py @@ -0,0 +1,192 @@ +"""Two diagnostics: + + (1) Cycle-closure / winding count on K_b and K_c. + For each cycle, count the c-edges (off-cycle) at K-vertices that + lie on the local LEFT vs local RIGHT side of the walking direction. + Under constancy (h constant on V(K_b) U V(K_c)), Lemma A predicts + sides alternate, so #LEFT == #RIGHT == |V(K)|/2 exactly. We tally + the empirical (#LEFT, #RIGHT) split for K_b and K_c. + + (2) Ordered sequence of (i_b mod 2, i_c mod 2) parity pairs at shared + vertices, sorted by i_b position. We look at the sequence of + transitions in the 4-state space {(0,0), (0,1), (1,0), (1,1)}, + and ask whether the empirical sequences are constrained. + +Run with: sage experiments/check_shared_sequence_and_winding.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 +from check_heawood_local_side import c_edge_local_side + + +def walk_positions(walk): + pos = {} + for k, (_, leave_v) in enumerate(walk): + if leave_v not in pos: + pos[leave_v] = k + return pos + + +def collect_cycle_sides(H, walk, edges, col, emb, third_color): + """For each step k of walk, compute side ('L' or 'R') of the + third_color edge at the leave_v vertex relative to the walking + direction (in_edge = walk[k], out_edge = walk[k+1]). + Returns list of sides indexed by k. + """ + L = len(walk) + sides = [] + for k in range(L): + v = walk[k][1] + 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] + s = c_edge_local_side(v, third_color, col, edges, emb, u_in, u_out) + sides.append(s) + return sides + + +def test_one(D): + D.is_planar(set_embedding=True) + n_col = 0 + # (1) Side-split distribution: (#L, #R, L_cycle). + kb_split = {}; kc_split = {} + # (2) Ordered sequence of (i_b mod 2, i_c mod 2) at shared verts in + # K_b walk order: sequences (as tuples). + seq_dist = {} # signature tuple -> count + # Aggregate transition counts between the 4 parity states + transitions = {} # (prev_state, curr_state) -> count + 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] + b_color, c_color = bs[0], bs[1] + walk_b = trace_kempe_cycle(edges, col, merged_idx, (a, b_color)) + walk_c = trace_kempe_cycle(edges, col, merged_idx, (a, c_color)) + pos_b = walk_positions(walk_b) + pos_c = walk_positions(walk_c) + shared = set(pos_b.keys()) & set(pos_c.keys()) + # (1) + sides_b = collect_cycle_sides(H, walk_b, edges, col, emb, c_color) + sides_c = collect_cycle_sides(H, walk_c, edges, col, emb, b_color) + Lb_len = len(walk_b); Lc_len = len(walk_c) + Lb = sum(1 for s in sides_b if s == 'L') + Rb = sum(1 for s in sides_b if s == 'R') + Lc = sum(1 for s in sides_c if s == 'L') + Rc = sum(1 for s in sides_c if s == 'R') + kb_split[(Lb, Rb)] = kb_split.get((Lb, Rb), 0) + 1 + kc_split[(Lc, Rc)] = kc_split.get((Lc, Rc), 0) + 1 + # (2) + seq = [] + for k in range(Lb_len): + v = walk_b[k][1] + if v in shared: + seq.append((k % 2, pos_c[v] % 2)) + # Signature: count occurrences of each state in order. + sig = tuple(seq) + seq_dist[sig] = seq_dist.get(sig, 0) + 1 + # Transitions: + for i in range(len(seq)): + prev = seq[i - 1] # wraps to seq[-1] + curr = seq[i] + transitions[(prev, curr)] = transitions.get( + (prev, curr), 0) + 1 + return n_col, kb_split, kc_split, seq_dist, transitions + + +def main(max_n=18, time_budget_per_n=1800): + print(f"Cycle-side splits and shared-vertex sequence, " + f"n in [12, {max_n}]\n") + grand_col = 0 + grand_kb = {}; grand_kc = {} + grand_seq = {} + grand_tr = {} + 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, kb_i, kc_i, seq_i, tr_i = test_one(D) + n_col_n += ni + for k, v in kb_i.items(): grand_kb[k] = grand_kb.get(k, 0) + v + for k, v in kc_i.items(): grand_kc[k] = grand_kc.get(k, 0) + v + for k, v in seq_i.items(): grand_seq[k] = grand_seq.get(k, 0) + v + for k, v in tr_i.items(): grand_tr[k] = grand_tr.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 (n in [12, {max_n}], {grand_col} colourings)") + + print(f"\n K_b (#L, #R) distribution:") + for k, v in sorted(grand_kb.items()): + print(f" {k}: {v}") + print(f"\n K_c (#L, #R) distribution:") + for k, v in sorted(grand_kc.items()): + print(f" {k}: {v}") + # Check: is #L == #R always? + kb_equal = sum(v for (L, R), v in grand_kb.items() if L == R) + kc_equal = sum(v for (L, R), v in grand_kc.items() if L == R) + total_kb = sum(grand_kb.values()) + total_kc = sum(grand_kc.values()) + print(f"\n K_b: #L == #R in {kb_equal}/{total_kb} " + f"({100*kb_equal/max(1,total_kb):.2f}%)") + print(f" K_c: #L == #R in {kc_equal}/{total_kc} " + f"({100*kc_equal/max(1,total_kc):.2f}%)") + + print(f"\n Transition counts in the 4-state parity space " + f"((prev) -> (curr)):") + states = [(0,0), (0,1), (1,0), (1,1)] + for s1 in states: + for s2 in states: + c = grand_tr.get((s1, s2), 0) + print(f" {s1} -> {s2}: {c}") + + print(f"\n Top 10 most common shared-sequence signatures (by K_b order):") + top = sorted(grand_seq.items(), key=lambda kv: -kv[1])[:10] + for sig, c in top: + print(f" {sig}: {c}") + + +if __name__ == '__main__': + main()