"""For every chord-apex+Kempe colouring, walk the K_b cycle, and at each K_b-vertex classify the c-edge as 'local LEFT' or 'local RIGHT' of the walking direction, using the CW embedding at the vertex. At v with incoming K-edge to neighbour u_in and outgoing K-edge to u_out, look at the CW cyclic order of v's three neighbours. Going CW from u_in: - if we hit the c-neighbour before u_out: c-edge is local RIGHT. - if we hit u_out before the c-neighbour: c-edge is local LEFT. Tally over consecutive pairs (v_0, v_1) on K_b: combinations of (same h_phi?, same local side?). Predicted biconditional: h_phi(v_0) == h_phi(v_1) <==> c-edges on OPPOSITE local sides. (Equivalently, in global terms, same Heawood at consecutive K-vertices forces c-edges to alternate inside/outside of K_b.) Run with: sage experiments/check_heawood_local_side.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_final_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 c_edge_local_side(v, c_color, col, edges, embedding, u_in, u_out): """At vertex v, walking K_b from u_in into v and out to u_out, find the c-coloured neighbour of v and return 'R' (between u_in and u_out in CW) or 'L' (between u_out and u_in in CW).""" nbrs_cw = embedding[v] # list of neighbours in clockwise order n = len(nbrs_cw) pos = {u: i for i, u in enumerate(nbrs_cw)} if u_in not in pos or u_out not in pos: return None p_in = pos[u_in] p_out = pos[u_out] # find c-neighbour c_nbr = None for u in nbrs_cw: ei = edge_idx(edges, frozenset((v, u))) if ei is not None and col[ei] == c_color: c_nbr = u break if c_nbr is None: return None p_c = pos[c_nbr] # Walk CW from p_in (skipping p_in itself): for offset in range(1, n): idx = (p_in + offset) % n if idx == p_c: return 'R' if idx == p_out: return 'L' return None def test_one(D): """Tally (same h, same local side) over consecutive K_b pairs.""" D.is_planar(set_embedding=True) n_col = 0 counts = {(True, True): 0, (True, False): 0, (False, True): 0, (False, False): 0} skipped = 0 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 try: h = heawood_numbers(H, edges, col) except RuntimeError: skipped += 1 continue merged_idx = edge_idx(edges, named['merged']) a = col[merged_idx] for b in range(3): if b == a: continue c_color = 3 - a - b walk = trace_kempe_cycle(edges, col, merged_idx, (a, b)) # walk[k] = (edge_idx, leave_vertex). Reconstruct vertex # sequence (the vertex we end at after edge k = the # leave_vertex of edge k = the start vertex of edge k+1). L = len(walk) if L == 0: continue # at vertex v = walk[k][1], we arrived via edge walk[k] # and leave via edge walk[(k+1) % L]. sides = [] h_vals = [] for k in range(L): v = walk[k][1] in_edge_idx = walk[k][0] out_edge_idx = walk[(k + 1) % L][0] # find u_in (the other endpoint of in_edge) and u_out. in_e = edges[in_edge_idx] out_e = edges[out_edge_idx] 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, c_color, col, edges, emb, u_in, u_out) sides.append(side) h_vals.append(h[v]) # Tally consecutive pairs for k in range(L): s0 = sides[k]; s1 = sides[(k + 1) % L] h0 = h_vals[k]; h1 = h_vals[(k + 1) % L] if s0 is None or s1 is None: continue same_h = (h0 == h1) same_side = (s0 == s1) counts[(same_h, same_side)] += 1 return n_col, counts, skipped def main(max_n=18, time_budget_per_n=1800): print(f"(same h, same local-c-side) on consecutive K_b pairs, " f"n in [12, {max_n}]\n") grand_col = 0 grand_counts = {(True, True): 0, (True, False): 0, (False, True): 0, (False, False): 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 counts_n = {(True, True): 0, (True, False): 0, (False, True): 0, (False, False): 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) n_col_i, c_i, sk_i = test_one(D) n_col_n += n_col_i for k, v in c_i.items(): counts_n[k] = counts_n.get(k, 0) + v elapsed = time.time() - start total_pairs = sum(counts_n.values()) print(f"n={n}: {n_col_n} col., {total_pairs} pairs [{elapsed:.0f}s]") print(f" {counts_n}") sys.stdout.flush() grand_col += n_col_n for k, v in counts_n.items(): grand_counts[k] = grand_counts.get(k, 0) + v print() print("=" * 78) print(f"Grand totals (n in [12, {max_n}], {grand_col} col., " f"{sum(grand_counts.values())} pairs):") print(f" (same h=Y, same side=Y): {grand_counts[(True, True)]}") print(f" (same h=Y, same side=N): {grand_counts[(True, False)]}") print(f" (same h=N, same side=Y): {grand_counts[(False, True)]}") print(f" (same h=N, same side=N): {grand_counts[(False, False)]}") total = sum(grand_counts.values()) if total > 0: for k, v in grand_counts.items(): print(f" {k}: {100*v/total:.2f}%") if (grand_counts[(True, True)] == 0 and grand_counts[(False, False)] == 0 and total > 0): print(f" *** PREDICTED BICONDITIONAL HOLDS: " f"same h <==> different side ***") elif grand_counts[(True, True)] == 0 and total > 0: print(f" empirical implication: same h ==> different side") elif grand_counts[(False, False)] == 0 and total > 0: print(f" empirical implication: different h ==> same side") else: print(f" no clean biconditional") if __name__ == '__main__': main()