"""For the 1,314 bad chord-apex+Kempe colourings (with sub-case (ii.B) + P_1 ∉ V(K_b) ∪ V(K_c)), check the *face structure* induced by S: - Is the S-cycle a face boundary of the reduced dual? - For each S-edge, what are the 2 adjacent faces? - How many of those F_ext faces are G'-pentagons? - Does the bound (# G'-pentagons hit ≤ |S|) hold? If S being a single cycle is also a face boundary, then # G'-pentagons hit by S ≤ |S| (one per S-edge, with the F1 interior face being a |S|-length face = not a pentagon since |S| ≥ 6). Combined with the p_G ≥ 7 lower bound, this closes |S| ≤ 6 cases. Run with: sage experiments/check_S_face_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_final_scaled import ( apply_reduction, proper_3_edge_colorings, matches_chord_apex_kempe, kempe_cycle_set, edge_idx, ) from check_heawood_on_kempe import dual_of, vertices_of_kempe def test_one(D): D.is_planar(set_embedding=True) bad_count = 0 # |S| -> # bad colourings of that size where S is a face boundary is_face_boundary = {} # |S| -> # bad colourings of that size by_size = {} # |S| -> distribution of (# G'-pentagons hit by S) pentagons_hit_by_size = {} # |S| -> distribution of (# G'-pentagons total in reduced dual) pent_total_by_size = {} 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)] v_n = 9999 for col in cand: # Identify bad sub-case (ii.B) target = {named['side_0'], named['spike']} lower_flank = None for f in H.faces(): if target.issubset({frozenset(e) for e in f}): lower_flank = f; break if lower_flank is None or len(lower_flank) != 5: continue arc_verts = [e[0] for e in lower_flank] if v_n not in arc_verts: continue k = arc_verts.index(v_n) cyc = arc_verts[k:] + arc_verts[:k] A_i = next(iter(named['side_0'] - {v_n})) A_ip1 = next(iter(named['spike'] - {v_n})) if cyc[1] == A_i and cyc[4] == A_ip1: P_1, P_2 = cyc[2], cyc[3] elif cyc[1] == A_ip1 and cyc[4] == A_i: P_2, P_1 = cyc[2], cyc[3] else: continue merged_idx = edge_idx(edges, named['merged']) c_col = col[merged_idx] c_0_col = col[edge_idx(edges, named['side_0'])] c_1_col = col[edge_idx(edges, named['side_1'])] e_AiP1 = edge_idx(edges, frozenset((A_i, P_1))) e_P1P2 = edge_idx(edges, frozenset((P_1, P_2))) if e_AiP1 is None or e_P1P2 is None: continue if col[e_AiP1] != c_1_col or col[e_P1P2] != c_0_col: continue a = c_col other = [x for x in range(3) if x != a] kc_b = kempe_cycle_set(edges, col, merged_idx, (a, other[0])) kc_c = kempe_cycle_set(edges, col, merged_idx, (a, other[1])) V_b = vertices_of_kempe(edges, kc_b) V_c = vertices_of_kempe(edges, kc_c) V_union = V_b | V_c S = set(H.vertices()) - V_union if P_1 in V_union: continue bad_count += 1 S_size = len(S) by_size[S_size] = by_size.get(S_size, 0) + 1 # Is S a face boundary? # Find a face whose boundary vertices = S exactly. S_is_face = False for f in H.faces(): verts = {u for (u, v) in f} | {v for (u, v) in f} if verts == S: S_is_face = True break if S_is_face: is_face_boundary[S_size] = ( is_face_boundary.get(S_size, 0) + 1) # # G'-pentagons (= not adjacent to F_v's modification) def is_g_prime_pentagon(f): if len(f) != 5: return False f_edges_set = {frozenset(e) for e in f} if (named['side_0'] in f_edges_set or named['side_1'] in f_edges_set or named['spike'] in f_edges_set or named['merged'] in f_edges_set): return False return True p_total = 0 p_hit = 0 for f in H.faces(): if not is_g_prime_pentagon(f): continue p_total += 1 verts = {u for (u, v) in f} | {v for (u, v) in f} if verts & S: p_hit += 1 pent_dist = pentagons_hit_by_size.setdefault(S_size, {}) pent_dist[p_hit] = pent_dist.get(p_hit, 0) + 1 tot_dist = pent_total_by_size.setdefault(S_size, {}) tot_dist[p_total] = tot_dist.get(p_total, 0) + 1 return bad_count, by_size, is_face_boundary, pentagons_hit_by_size, pent_total_by_size def main(max_n=20, time_budget_per_n=1800): print("Face structure of S-cycle in bad chord-apex+Kempe colourings.\n") grand_bad = 0 grand_size = {} grand_face_b = {} grand_pent_hit = {} grand_pent_tot = {} 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_bad_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}") break G.is_planar(set_embedding=True) D = dual_of(G) nb, bs, ifb, ph, pt = test_one(D) n_bad_n += nb for k, v in bs.items(): grand_size[k] = grand_size.get(k, 0) + v for k, v in ifb.items(): grand_face_b[k] = grand_face_b.get(k, 0) + v for sz, dist in ph.items(): for k, v in dist.items(): grand_pent_hit.setdefault(sz, {}) grand_pent_hit[sz][k] = grand_pent_hit[sz].get(k, 0) + v for sz, dist in pt.items(): for k, v in dist.items(): grand_pent_tot.setdefault(sz, {}) grand_pent_tot[sz][k] = grand_pent_tot[sz].get(k, 0) + v elapsed = time.time() - start print(f"n={n}: {n_bad_n} bad colourings [{elapsed:.0f}s]") sys.stdout.flush() grand_bad += n_bad_n print() print("=" * 70) print(f"Total bad colourings: {grand_bad}") print("\nIs S-cycle a face boundary of reduced dual?") for sz in sorted(grand_size): tot = grand_size[sz] face_count = grand_face_b.get(sz, 0) pct = 100 * face_count / max(tot, 1) print(f" |S| = {sz}: {face_count} / {tot} ({pct:.1f}%) yes") print("\nDistribution of # G'-pentagons HIT by S, by |S|:") for sz in sorted(grand_pent_hit): print(f" |S| = {sz}:") for h, c in sorted(grand_pent_hit[sz].items()): print(f" {h} pentagons hit: {c}") print("\nDistribution of # G'-pentagons TOTAL, by |S|:") for sz in sorted(grand_pent_tot): print(f" |S| = {sz}:") for t, c in sorted(grand_pent_tot[sz].items()): print(f" {t} pentagons total: {c}") print("\nKey check: is # hit ≤ |S| always?") for sz in sorted(grand_pent_hit): max_hit = max(grand_pent_hit[sz].keys()) leq = max_hit <= sz print(f" |S| = {sz}: max # hit = {max_hit}, " f"{'≤' if leq else '>'} |S| ({'✓' if leq else '✗ violated'})") if __name__ == '__main__': main()