diff --git a/papers/face_monochromatic_pairs/experiments/characterize_S_vertices.py b/papers/face_monochromatic_pairs/experiments/characterize_S_vertices.py new file mode 100644 index 0000000..c316260 --- /dev/null +++ b/papers/face_monochromatic_pairs/experiments/characterize_S_vertices.py @@ -0,0 +1,204 @@ +"""For the 1,314 bad chord-apex+Kempe colourings (where +Lemma flank-covering-hex empirically fails), characterize the set +S = V \\ (V(K_b) ∪ V(K_c)) -- the "uncovered" vertices. + +For each bad colouring, report: + - |S| (size of S); + - distribution of S-vertices' graph-distance from v_n (the spike + vertex), and from the merged edge; + - on which faces of the reduced dual the S-vertices sit (G'-pentagon, + G'-hexagon, F^♭_flank, F^♭_outer, F^♭_merged, etc.); + - per-colouring: how MANY G'-pentagons are hit by S (= how close to + "covering all G'-pentagons" we are; the pigeonhole would need + |S|*3 ≥ #pentagons). + +The goal is to find structural patterns we could exploit in a +discharging argument. + +Run with: sage experiments/characterize_S_vertices.py +""" +import os +import sys +import time +from collections import defaultdict + +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, + kempe_cycle_set, + edge_idx, +) +from check_heawood_on_kempe import dual_of, vertices_of_kempe + + +def classify_face(f, named): + """Classify a face of the reduced dual.""" + edges_set = {frozenset(e) for e in f} + has_s0 = named['side_0'] in edges_set + has_s1 = named['side_1'] in edges_set + has_sp = named['spike'] in edges_set + has_m = named['merged'] in edges_set + 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): + D.is_planar(set_embedding=True) + bad_count = 0 + S_size_dist = {} + S_dist_from_vn = {} # graph dist of S-vertex from v_n + S_face_types = {} # face types containing S-vertices + pentagons_hit_dist = {} # how many G'-pentagons hit by S, per colouring + 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 P_1, P_2 on lower flank, check 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 + # Compute K_b, K_c, S + 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 + # P_1 ∉ V_union check + if P_1 in V_union: continue + bad_count += 1 + S_size = len(S) + S_size_dist[S_size] = S_size_dist.get(S_size, 0) + 1 + # Distances from v_n + for s in S: + try: + d = H.distance(s, v_n) + except: + d = -1 + S_dist_from_vn[d] = S_dist_from_vn.get(d, 0) + 1 + # Face types containing S-vertices + for s in S: + for f in H.faces(): + verts = {u for (u, v) in f} | {v for (u, v) in f} + if s in verts: + t = classify_face(f, named) + S_face_types[t] = S_face_types.get(t, 0) + 1 + # # G'-pentagons hit by S + n_pent_hit = 0 + n_pent_total = 0 + for f in H.faces(): + if classify_face(f, named) != 'G-prime': continue + if len(f) != 5: continue + n_pent_total += 1 + verts = {u for (u, v) in f} | {v for (u, v) in f} + if verts & S: n_pent_hit += 1 + pent_pair = (n_pent_hit, n_pent_total) + pentagons_hit_dist[pent_pair] = ( + pentagons_hit_dist.get(pent_pair, 0) + 1) + return (bad_count, S_size_dist, S_dist_from_vn, S_face_types, + pentagons_hit_dist) + + +def main(max_n=20, time_budget_per_n=1800): + print("Structural characterization of S = V \\ (V(K_b) ∪ V(K_c))\n" + "on the 1,314 bad chord-apex+Kempe colourings.\n") + grand_bad = 0 + grand_S_size = {} + grand_S_dist = {} + grand_S_face = {} + grand_pent_hit = {} + 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}/{len(triangulations)}") + break + G.is_planar(set_embedding=True) + D = dual_of(G) + nb, ssize, sdist, sface, phit = test_one(D) + n_bad_n += nb + for k, v in ssize.items(): grand_S_size[k] = grand_S_size.get(k, 0) + v + for k, v in sdist.items(): grand_S_dist[k] = grand_S_dist.get(k, 0) + v + for k, v in sface.items(): grand_S_face[k] = grand_S_face.get(k, 0) + v + for k, v in phit.items(): grand_pent_hit[k] = grand_pent_hit.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("\n|S| distribution:") + for s in sorted(grand_S_size): + c = grand_S_size[s] + print(f" |S| = {s}: {c} ({100*c/max(grand_bad, 1):.2f}%)") + print("\nGraph distance of S-vertices from v_n:") + for d in sorted(grand_S_dist): + c = grand_S_dist[d] + total_S_verts = sum(grand_S_dist.values()) + print(f" dist = {d}: {c} ({100*c/max(total_S_verts, 1):.2f}%)") + print("\nFace types containing S-vertices:") + total = sum(grand_S_face.values()) + for t in sorted(grand_S_face, key=lambda k: -grand_S_face[k]): + c = grand_S_face[t] + print(f" {t}: {c} ({100*c/max(total, 1):.2f}%)") + print("\n(# G'-pentagons hit by S, # G'-pentagons total) per colouring:") + for pair in sorted(grand_pent_hit, key=lambda k: -grand_pent_hit[k])[:20]: + c = grand_pent_hit[pair] + h, t = pair + ratio = h / t if t > 0 else 0 + print(f" hit/total = {h}/{t} ({100*ratio:.0f}%): {c}") + + +if __name__ == '__main__': + main() diff --git a/papers/face_monochromatic_pairs/experiments/check_S_adjacency.py b/papers/face_monochromatic_pairs/experiments/check_S_adjacency.py new file mode 100644 index 0000000..e831774 --- /dev/null +++ b/papers/face_monochromatic_pairs/experiments/check_S_adjacency.py @@ -0,0 +1,166 @@ +"""For the 1,314 bad chord-apex+Kempe colourings, check whether +S-vertices form connected subgraphs (= are adjacent to each other), +which would explain why their pentagon-coverage is below 3|S|. + +For each bad colouring: + - Compute |S| and the induced subgraph H[S]. + - # connected components of H[S]. + - Edges in H[S] (= S-vertex pairs sharing an edge). + - For each pair of adjacent S-vertices: how many G'-pentagons they + share (= "overlap" that reduces pigeonhole bound). + +Run with: sage experiments/check_S_adjacency.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, + 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_components_dist = {} # (|S|, # connected components in H[S]) -> count + S_edges_dist = {} # (|S|, # edges in H[S]) -> count + pentagon_overlap_dist = {} # (|S|, max overlap) -> 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) + 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: + 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) + # H[S] = induced subgraph + HS = H.subgraph(S) + comps = HS.connected_components() + key = (S_size, len(comps)) + S_components_dist[key] = S_components_dist.get(key, 0) + 1 + # Edges in H[S] + n_edges = HS.size() + key2 = (S_size, n_edges) + S_edges_dist[key2] = S_edges_dist.get(key2, 0) + 1 + # Pentagon overlap: for each pair of adjacent S-vertices, + # count their shared G'-pentagons. + max_overlap = 0 + for u in S: + for v_other in H.neighbors(u): + if v_other not in S or v_other <= u: + continue + # Find G'-pentagons containing both u and v_other + n_shared = 0 + for f in H.faces(): + if len(f) != 5: continue + verts = {a for a, b in f} | {b for a, b in f} + if u in verts and v_other in verts: + n_shared += 1 + max_overlap = max(max_overlap, n_shared) + key3 = (S_size, max_overlap) + pentagon_overlap_dist[key3] = ( + pentagon_overlap_dist.get(key3, 0) + 1) + return (bad_count, S_components_dist, S_edges_dist, pentagon_overlap_dist) + + +def main(max_n=20, time_budget_per_n=1800): + print("Connectivity structure of S-vertices in bad colourings.\n") + grand_bad = 0 + grand_comps = {} + grand_edges = {} + grand_overlap = {} + 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, sc, se, po = test_one(D) + n_bad_n += nb + for k, v in sc.items(): grand_comps[k] = grand_comps.get(k, 0) + v + for k, v in se.items(): grand_edges[k] = grand_edges.get(k, 0) + v + for k, v in po.items(): grand_overlap[k] = grand_overlap.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("\n(|S|, # connected components in H[S]) distribution:") + for k in sorted(grand_comps): + c = grand_comps[k] + print(f" |S|={k[0]}, #comps={k[1]}: {c}") + print("\n(|S|, # edges in H[S]) distribution:") + for k in sorted(grand_edges): + c = grand_edges[k] + print(f" |S|={k[0]}, #edges={k[1]}: {c}") + print("\n(|S|, max # shared pentagons across adjacent S-pairs):") + for k in sorted(grand_overlap): + c = grand_overlap[k] + print(f" |S|={k[0]}, max_overlap={k[1]}: {c}") + + +if __name__ == '__main__': + main()