"""Test Conjecture 3.8 (strengthening of 3.6 with the {b,c}-Kempe / 3-colour constraint on the new 4-edge face) on all min-degree-5 triangulations up to n = 18. For each (G, F_v, i_red, varphi) with varphi a chord-apex + Kempe coloring, we look for a witness (F, e_1, e_2) of Conjecture 3.6 (clauses 1-3 including the 4-edge-face criterion). Then we: - subdivide e_1, e_2 by X_1, X_2, - add the new edge X_1 X_2, - recolour the (subdivided) Kempe cycle alternately starting from merged so propriety holds and the new edge takes the third colour, - identify f_n (the 4-edge face containing the new edge), and - test clause 4: EITHER partial(f_n) uses all 3 colours, OR the {b, c}-Kempe cycle through X_1 X_2 has only X_1 X_2 itself in partial(f_n). Aggregates per-n. Run with: sage experiments/check_conj_final_scaled.py """ from sage.all import Graph from sage.graphs.graph_generators import graphs import sys import time def dual_of(G): G.is_planar(set_embedding=True) faces = G.faces() edge_to_faces = {} for fi, face in enumerate(faces): for u, v in face: edge_to_faces.setdefault(frozenset((u, v)), []).append(fi) return Graph( [(fs[0], fs[1]) for fs in edge_to_faces.values() if len(fs) == 2], multiedges=False, loops=False) def apply_reduction(G, face, i, v_n_label): boundary = [u for (u, v) in face] if len(set(boundary)) != 5: return None A = [] for B_k in boundary: outer = [w for w in G.neighbor_iterator(B_k) if w not in boundary] if len(outer) != 1: return None A.append(outer[0]) if len(set(A)) != 5 or A[(i+3) % 5] == A[(i+4) % 5]: return None H = G.copy() for v in boundary: H.delete_vertex(v) H.add_vertex(v_n_label) side_0 = (v_n_label, A[i]) spike = (v_n_label, A[(i+1) % 5]) side_1 = (v_n_label, A[(i+2) % 5]) merged = (A[(i+3) % 5], A[(i+4) % 5]) H.add_edges([side_0, spike, side_1, merged]) if H.has_multiple_edges() or H.has_loops(): return None if not H.is_planar(set_embedding=True): return None if not all(H.degree(v) == 3 for v in H.vertex_iterator()): return None return {'H': H, 'named': {'spike': frozenset(spike), 'side_0': frozenset(side_0), 'side_1': frozenset(side_1), 'merged': frozenset(merged)}} def proper_3_edge_colorings(G): edges = list(G.edges(labels=False)) n = len(edges) adj = [[] for _ in range(n)] for i in range(n): u, v = edges[i][0], edges[i][1] for j in range(i): x, y = edges[j][0], edges[j][1] if u in (x, y) or v in (x, y): adj[i].append(j); adj[j].append(i) coloring = [-1] * n results = [] def back(k): if k == n: results.append(tuple(coloring)); return for c in range(3): if all(coloring[j] != c for j in adj[k]): coloring[k] = c back(k + 1) coloring[k] = -1 back(0) return edges, results def kempe_cycle_set(edges, coloring, start_idx, color_pair): a, b = color_pair if coloring[start_idx] not in (a, b): return set() in_sub = set(i for i in range(len(edges)) if coloring[i] in (a, b)) visited = {start_idx}; stack = [start_idx] while stack: cur = stack.pop() u, v = edges[cur][0], edges[cur][1] for j in in_sub: if j in visited: continue x, y = edges[j][0], edges[j][1] if u in (x, y) or v in (x, y): visited.add(j); stack.append(j) return visited def edge_idx(edges, e_frozen): for i, e in enumerate(edges): if frozenset((e[0], e[1])) == e_frozen: return i return None def trace_kempe_cycle(edges, col_list, start_idx, color_pair): cycle_set = kempe_cycle_set(edges, col_list, start_idx, color_pair) incident_at = {} for ei in cycle_set: u, v = edges[ei][0], edges[ei][1] incident_at.setdefault(u, []).append(ei) incident_at.setdefault(v, []).append(ei) start_u, start_v = edges[start_idx][0], edges[start_idx][1] walk = [(start_idx, start_v)] cur_e = start_idx cur_leave = start_v while True: nbrs = incident_at[cur_leave] if len(nbrs) != 2: break nxt = nbrs[0] if nbrs[1] == cur_e else nbrs[1] u2, v2 = edges[nxt][0], edges[nxt][1] leave_next = v2 if u2 == cur_leave else u2 if nxt == start_idx: break walk.append((nxt, leave_next)) cur_e = nxt cur_leave = leave_next return walk def matches_chord_apex_kempe(edges, col, named): idx = {role: edge_idx(edges, ns) for role, ns in named.items()} if any(v is None for v in idx.values()): return False c_spike = col[idx['spike']] c_merged = col[idx['merged']] if c_spike != c_merged: return False c_s0 = col[idx['side_0']]; c_s1 = col[idx['side_1']] kc0 = kempe_cycle_set(edges, col, idx['spike'], (c_spike, c_s0)) if idx['side_0'] not in kc0 or idx['merged'] not in kc0: return False kc1 = kempe_cycle_set(edges, col, idx['spike'], (c_spike, c_s1)) if idx['side_1'] not in kc1 or idx['merged'] not in kc1: return False return True def find_all_36_witnesses(H, edges, col_list, named): """Yield every (F, e_1, e_2) satisfying clauses 1-3 of Conjecture 3.6.""" merged_idx = edge_idx(edges, named['merged']) c_merged = col_list[merged_idx] kempe_cycles = [] for c_prime in range(3): if c_prime == c_merged: continue kc = kempe_cycle_set(edges, col_list, merged_idx, (c_merged, c_prime)) kempe_cycles.append((c_prime, kc)) H.is_planar(set_embedding=True) out = [] for face in H.faces(): face_edge_indices = [] for u, v in face: ei = edge_idx(edges, frozenset((u, v))) if ei is not None: face_edge_indices.append(ei) n_face = len(face_edge_indices) for i in range(n_face): for j in range(i + 1, n_face): e1, e2 = face_edge_indices[i], face_edge_indices[j] if e1 == merged_idx or e2 == merged_idx: continue if col_list[e1] != col_list[e2]: continue gap_a = (j - i - 1) gap_b = (n_face - 2 - gap_a) if gap_a != 1 and gap_b != 1: continue for c_prime, kc in kempe_cycles: if e1 in kc and e2 in kc: if gap_a == 1: e_F = face_edge_indices[i + 1] else: e_F = face_edge_indices[(j + 1) % n_face] out.append({ 'face_edges': face_edge_indices, 'e1': e1, 'e2': e2, 'e_F': e_F, 'kc_color_pair': (c_merged, c_prime), }) return out def check_clause_4(H, edges, col_list, named, witness): """Construct the modified H + recoloring, identify f_n, check clause 4.""" e1, e2 = witness['e1'], witness['e2'] e_F = witness['e_F'] a = col_list[e1] # color of e_1 = e_2 (clauses 1) merged_idx = edge_idx(edges, named['merged']) cyc_a, cyc_b = witness['kc_color_pair'] # = (c_merged, c_other) # On the {cyc_a, cyc_b}-Kempe cycle, e_1 carries colour a, which # equals either cyc_a or cyc_b. The conjecture's "b" is the OTHER # colour on the cycle; we set it accordingly. if a == cyc_a: b = cyc_b elif a == cyc_b: b = cyc_a else: raise RuntimeError( f"e_1 colour {a} not in Kempe pair {(cyc_a, cyc_b)}") c = ({0, 1, 2} - {a, b}).pop() # Subdivided cycle K' alternates blue/green starting from merged = blue walk = trace_kempe_cycle(edges, col_list, merged_idx, (cyc_a, cyc_b)) walk_edges = [w[0] for w in walk] leave_at = [w[1] for w in walk] # K' position counter. For each old cycle edge e in cyclic order, position # increments by 1 normally; for e1/e2, increments by 2 (two halves). # For e1, we record (entry_half_color, exit_half_color) where entry_half # is the half adjacent to entry_vertex and exit_half adjacent to leaving. e1_entry_color = e1_exit_color = None e2_entry_color = e2_exit_color = None e1_entry_vertex = e1_exit_vertex = None e2_entry_vertex = e2_exit_vertex = None other_new_colors = {} # ei -> new color (for cycle edges other than e1/e2) pos = 0 for k, ei in enumerate(walk_edges): leaving = leave_at[k] u, v = edges[ei][0], edges[ei][1] entry = v if leaving == u else u if ei == e1: c_entry = cyc_b if pos % 2 == 0 else cyc_a pos += 1 c_exit = cyc_b if pos % 2 == 0 else cyc_a pos += 1 e1_entry_color = c_entry; e1_exit_color = c_exit e1_entry_vertex = entry; e1_exit_vertex = leaving elif ei == e2: c_entry = cyc_b if pos % 2 == 0 else cyc_a pos += 1 c_exit = cyc_b if pos % 2 == 0 else cyc_a pos += 1 e2_entry_color = c_entry; e2_exit_color = c_exit e2_entry_vertex = entry; e2_exit_vertex = leaving else: nc = cyc_b if pos % 2 == 0 else cyc_a other_new_colors[ei] = nc pos += 1 # Identify which half of e1 is on f_n: it's the half adjacent to e_F. # e_F has 2 endpoints; one is shared with e1, one with e2. e_F_endpoints = set(edges[e_F]) e1_endpoints = set(edges[e1]) e2_endpoints = set(edges[e2]) shared_e1_eF = (e_F_endpoints & e1_endpoints).pop() shared_e2_eF = (e_F_endpoints & e2_endpoints).pop() # The half of e1 on f_n connects X_1 to shared_e1_eF. So if shared_e1_eF # equals e1_entry_vertex, the half on f_n is the "entry half"; else the # "exit half". if shared_e1_eF == e1_entry_vertex: e1_h_color = e1_entry_color else: e1_h_color = e1_exit_color if shared_e2_eF == e2_entry_vertex: e2_h_color = e2_entry_color else: e2_h_color = e2_exit_color # Determine e_F's new colour: if e_F is on the Kempe cycle (other_new_colors) # use that, else original. if e_F in other_new_colors: e_F_color = other_new_colors[e_F] else: e_F_color = col_list[e_F] # f_n's 4 edge colors: [new edge X_1-X_2 = c, e1_h, e_F, e2_h] fn_colors = [c, e1_h_color, e_F_color, e2_h_color] fn_distinct = set(fn_colors) uses_3_colors = (len(fn_distinct) == 3) if uses_3_colors: return True # clause 4(i) satisfied # Otherwise, check clause 4(ii): the {b,c}-Kempe cycle through the new # edge has only X_1-X_2 in f_n's boundary. # We need to actually build the modified graph and trace this cycle. return check_clause_4_kempe_part(H, edges, col_list, named, witness, a, b, c, e1_h_color, e2_h_color, other_new_colors, e1_entry_vertex, e1_exit_vertex, e2_entry_vertex, e2_exit_vertex, e1_entry_color, e1_exit_color, e2_entry_color, e2_exit_color) def check_clause_4_kempe_part(H, edges, col_list, named, witness, a, b, c, e1_h_color, e2_h_color, other_new_colors, e1_ev, e1_xv, e2_ev, e2_xv, e1_ec, e1_xc, e2_ec, e2_xc): """Build modified H' and check whether the {b,c}-Kempe cycle through X1-X2 has only that one edge in partial(f_n).""" e1 = witness['e1']; e2 = witness['e2']; e_F = witness['e_F'] H2 = H.copy() X1 = max(v for v in H.vertices(sort=False) if isinstance(v, int)) + 1 X2 = X1 + 1 H2.add_vertex(X1); H2.add_vertex(X2) e1_uv = tuple(edges[e1]); e2_uv = tuple(edges[e2]) H2.delete_edge(e1_uv); H2.delete_edge(e2_uv) H2.add_edges([(e1_uv[0], X1), (X1, e1_uv[1]), (e2_uv[0], X2), (X2, e2_uv[1]), (X1, X2)]) # Build coloring for H2 new_coloring = {} # Copy non-modified edges: original color (unless in other_new_colors) for ei, c0 in enumerate(col_list): if ei == e1 or ei == e2: continue e_fs = frozenset(edges[ei]) if ei in other_new_colors: new_coloring[e_fs] = other_new_colors[ei] else: new_coloring[e_fs] = c0 # Halves of e1, e2 new_coloring[frozenset((e1_ev, X1))] = e1_ec new_coloring[frozenset((X1, e1_xv))] = e1_xc new_coloring[frozenset((e2_ev, X2))] = e2_ec new_coloring[frozenset((X2, e2_xv))] = e2_xc new_coloring[frozenset((X1, X2))] = c # Determine f_n's edges (in H2): new edge X1-X2, the half of e1 adjacent # to e_F's shared vertex with e1, e_F itself, and the half of e2 adjacent # to e_F's shared vertex with e2. e_F_endpoints = set(edges[e_F]) e1_endpoints = set(edges[e1]) e2_endpoints = set(edges[e2]) shared_e1_eF = (e_F_endpoints & e1_endpoints).pop() shared_e2_eF = (e_F_endpoints & e2_endpoints).pop() fn_edges = { frozenset((X1, X2)), frozenset((X1, shared_e1_eF)), frozenset(edges[e_F]), frozenset((X2, shared_e2_eF)), } # Build edge list of H2 + coloring list H2_edges = list(H2.edges(labels=False)) H2_col_list = [new_coloring[frozenset(e)] for e in H2_edges] # Sanity: verify phi' is a proper 3-edge-colouring of H2 (the conjecture # asserts this; if the construction is wrong, abort). for v in H2.vertex_iterator(): seen = [] for w in H2.neighbor_iterator(v): seen.append(new_coloring[frozenset((v, w))]) if len(set(seen)) != len(seen): raise RuntimeError( f"phi' is not proper at vertex {v}: colors {seen}") # Trace the {b,c}-Kempe cycle through X1-X2 in H2 using new_coloring H2_X1X2_idx = None for i, e in enumerate(H2_edges): if frozenset(e) == frozenset((X1, X2)): H2_X1X2_idx = i; break if H2_X1X2_idx is None: return False kc_bc = kempe_cycle_set(H2_edges, H2_col_list, H2_X1X2_idx, (b, c)) # Count edges in kc_bc that are in fn_edges count = 0 for ei in kc_bc: if frozenset(H2_edges[ei]) in fn_edges: count += 1 return count == 1 def main(max_n=20, time_budget_per_n=7200): rows = [] for n in range(12, max_n + 1): start = time.time() try: triangulations = list(graphs.triangulations(n, minimum_degree=5)) except Exception as ex: rows.append((n, 0, 0, 0, f"cannot enumerate")) continue n_tri = len(triangulations) total_col = 0 total_pass = 0 timed_out = False for tri_idx, G in enumerate(triangulations, start=1): if time.time() - start > time_budget_per_n: timed_out = True; break G.is_planar(set_embedding=True) D = dual_of(G); D.is_planar(set_embedding=True) for face in D.faces(): if len(face) != 5: continue if time.time() - start > time_budget_per_n: timed_out = True; break 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)] for col in cand: witnesses = find_all_36_witnesses(H, edges, list(col), named) if not witnesses: continue total_col += 1 ok = False for w in witnesses: try: if check_clause_4(H, edges, list(col), named, w): ok = True break except Exception: pass if ok: total_pass += 1 elapsed = time.time() - start status = (f"TIMEOUT ({elapsed:.0f}s)" if timed_out else f"complete ({elapsed:.0f}s)") rows.append((n, n_tri, total_col, total_pass, status)) print(f"n={n}: {n_tri} tri, {total_col} cand witnesses, " f"{total_pass} pass clause 4, {status}") sys.stdout.flush() print() print("=" * 70) print(f"{'n':>3} {'#tri':>5} {'#witness':>10} {'#pass_cl4':>10} {'status':>25}") print("-" * 70) for n, n_tri, n_col, n_pass, status in rows: print(f"{n:>3} {n_tri:>5} {n_col:>10} {n_pass:>10} {status:>25}") if __name__ == '__main__': main()