"""Verify the structural characterization of the 30 residual chord-apex+Kempe colourings on which the G'-pentagon fallback empirically fails (= |S| = 8 AND all G'-pentagons are hit by S). Hypothesis: these are exactly the colourings on triangulations where v's cyclic neighbour-degree sequence has the form (5, 7, 7, 5, 5) (or cyclic shifts), and the reduction index i is chosen so that the two "7"s land on the flank positions (n_i, n_{i+1}) = (7, 7). For each of the 30 colourings, report: - parent triangulation order n_G, - cyclic degree sequence of v's 5 neighbours, - reduction index i, - p_G and # G'-pentagons hit by S, - the actual deciding face's length and type. If the hypothesis holds, all 30 are on (5, 7, 7, 5, 5)-type configurations. Run with: sage experiments/check_30_residual.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 is_g_prime_pentagon(f, named): if len(f) != 5: return False fset = {frozenset(e) for e in f} return not (named['side_0'] in fset or named['side_1'] in fset or named['spike'] in fset or named['merged'] in fset) def classify_face(f, named): fset = {frozenset(e) for e in f} has_s0 = named['side_0'] in fset has_s1 = named['side_1'] in fset has_sp = named['spike'] in fset has_m = named['merged'] in fset if has_s0 and has_sp and not has_s1 and not has_m: return 'flank-lower' if has_sp and has_s1 and not has_s0 and not has_m: return 'flank-upper' if has_s0 and has_s1 and has_m and not has_sp: return 'outer' if has_m and not (has_s0 or has_s1 or has_sp): return 'merged' if not (has_s0 or has_s1 or has_sp or has_m): return 'G-prime' return 'mixed' def test_one(D, G_parent): D.is_planar(set_embedding=True) G_parent.is_planar(set_embedding=True) emb_G = G_parent.get_embedding() examples = [] # 30 residual colourings # We need to map dual face → parent vertex (= the face's "dual vertex") # For each dual face we want to identify the parent vertex v. # The dual face's vertices correspond to triangular faces of G_parent # incident to v. # Build a face→parent-vertex map. parent_faces = G_parent.faces() face_to_dual_vertex_idx = {} # The dual vertices are indexed by face number. # Mapping: parent_face[fi] is a face, and dual vertex fi corresponds. # We need the reverse: given a degree-5 vertex v in G_parent, its # corresponding dual face = the face whose vertices are the 5 # parent_face-indices that contain v. v_to_dual_face = {} for v in G_parent.vertex_iterator(): if G_parent.degree(v) != 5: continue # Find the 5 parent_face-indices that contain v contains = [] for fi, pf in enumerate(parent_faces): for e in pf: if v in e: contains.append(fi) break if len(contains) != 5: continue v_to_dual_face[v] = contains # 5 dual vertex indices forming F_v boundary for face in D.faces(): if len(face) != 5: continue face_verts = [e[0] for e in face] # Identify which parent v this face corresponds to v_parent = None for v, dual_verts in v_to_dual_face.items(): if set(face_verts) == set(dual_verts): v_parent = v break if v_parent is None: continue # Get the cyclic degree sequence of v_parent's neighbours nbrs_cyclic = emb_G[v_parent] cyc_degs = [G_parent.degree(u) for u in nbrs_cyclic] 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 if len(S) != 8: continue # Check if hit = p_G = 8 (= all G'-pentagons hit) p_total = 0 p_hit = 0 for f in H.faces(): if not is_g_prime_pentagon(f, named): continue p_total += 1 verts = {u for (u, v) in f} | {v for (u, v) in f} if verts & S: p_hit += 1 if p_total != 8 or p_hit != 8: continue # This is a residual case. Find the actual deciding face. deciding_faces = [] for f in H.faces(): verts = {u for (u, v) in f} | {v for (u, v) in f} if not verts.issubset(V_union): continue L = len(f) if L % 3 == 0: continue deciding_faces.append((classify_face(f, named), L)) # Compute deg sequence around v_parent (cyclic) examples.append({ 'cyc_degs': cyc_degs, 'i_red': i_red, 'deciding_faces': deciding_faces, 'p_total': p_total, 'p_hit': p_hit, 'S_size': len(S), }) return examples def main(max_n=20, time_budget_per_n=1800): print("Verifying the 30 residual chord-apex+Kempe colourings.\n") grand_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_count = 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) exs = test_one(D, G) for ex in exs: ex['n_G'] = n ex['tri_idx'] = tri_idx n_count += len(exs) grand_examples.extend(exs) elapsed = time.time() - start print(f"n={n}: {n_count} residual colourings [{elapsed:.0f}s]") sys.stdout.flush() print() print("=" * 70) print(f"Total residual colourings: {len(grand_examples)}") # Count by cyclic deg sequence (canonical = min rotation) def canon(t): t = tuple(t) best = t n = len(t) for r in range(1, n): rot = t[r:] + t[:r] if rot < best: best = rot # Also reflections rev = t[::-1] for r in range(n): rot = rev[r:] + rev[:r] if rot < best: best = rot return best canon_dist = {} for ex in grand_examples: key = canon(ex['cyc_degs']) canon_dist[key] = canon_dist.get(key, 0) + 1 print("\nCyclic deg sequence distribution (canonical = lex-min " "over rotations + reflections):") for k in sorted(canon_dist, key=lambda x: -canon_dist[x]): c = canon_dist[k] print(f" {k}: {c}") # Decision face distribution df_dist = {} for ex in grand_examples: for (t, L) in ex['deciding_faces']: df_dist[(t, L)] = df_dist.get((t, L), 0) + 1 print("\nDeciding face (type, length) distribution:") for k in sorted(df_dist, key=lambda x: -df_dist[x]): t, L = k print(f" {t}, |f|={L}: {df_dist[k]}") # Print first 5 examples print("\nFirst 5 examples:") for ex in grand_examples[:5]: print(f" n={ex['n_G']}, tri#{ex['tri_idx']}, i={ex['i_red']}, " f"cyc_degs={ex['cyc_degs']}, " f"deciding={ex['deciding_faces']}") if __name__ == '__main__': main()