Files
math-research/papers/face_monochromatic_pairs/experiments/check_kempe_class_invariance.py
T
didericis 41227c6a0f papers: rename folders and retitle
- Main paper: dual_decomposition_minimal_counterexamples/ ->
  face_monochromatic_pairs/. Title is now
  "Face-Monochromatic Pairs and the Four Colour Theorem".
- Companion paper: dual_decomposition_iterated_reduction/ ->
  iterated_reduction_in_reduced_dual/. Title is now
  "An Iterated Reduction in the Reduced Dual". Its prose and bibliography
  cite the parent under the new title.
- Update one absolute sys.path reference inside
  check_conj_face_kempe_n15.py that pointed at the old folder.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 15:04:15 -04:00

289 lines
9.5 KiB
Python

"""Investigate the conjecture: H_t* has the same number of Kempe equivalence
classes as H_1.
For each min-deg-5 triangulation G (up to some n) and each reduction index
i_red in {0,...,4}:
- Build H_1 from the first pentagonal face of G' = dual(G).
- Count Kempe classes of H_1.
- Run Algorithm 3.1 to completion with Sage's edge_coloring as phi_1.
- Count Kempe classes of H_t*.
- Print a row.
Run with: sage experiments/check_kempe_class_invariance.py
"""
from sage.all import Graph
from sage.graphs.graph_generators import graphs
from sage.graphs.graph_coloring import edge_coloring
import sys
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)
dual_edges = [(fs[0], fs[1]) for fs in edge_to_faces.values() if len(fs) == 2]
return Graph(dual_edges, multiedges=False, loops=False)
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(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 num_kempe_classes(G):
"""Count the Kempe equivalence classes of proper 3-edge-colorings of G."""
edges, colorings = proper_3_edge_colorings(G)
if not colorings:
return None, 0
n = len(colorings)
parent = list(range(n))
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
def union(x, y):
rx, ry = find(x), find(y)
if rx != ry:
parent[rx] = ry
col_idx = {c: i for i, c in enumerate(colorings)}
for c_idx, col in enumerate(colorings):
for pair in [(0, 1), (0, 2), (1, 2)]:
visited = set()
for start in range(len(edges)):
if col[start] not in pair or start in visited:
continue
cyc = kempe_cycle(edges, col, start, pair)
visited |= cyc
a, b = pair
new_col = list(col)
for k in cyc:
if new_col[k] == a:
new_col[k] = b
elif new_col[k] == b:
new_col[k] = a
new_col = tuple(new_col)
if new_col != col and new_col in col_idx:
union(c_idx, col_idx[new_col])
classes = len({find(i) for i in range(n)})
return n, classes
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:
return None
if 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, 'A': A,
'named': {
'spike': frozenset(spike),
'side_0': frozenset(side_0),
'side_1': frozenset(side_1),
'merged': frozenset(merged),
},
}
def find_safe_pentagonal_face(G, protected):
for face in G.faces():
if len(face) != 5:
continue
boundary = [u for (u, v) in face]
boundary_edges = [frozenset([u, v]) for (u, v) in face]
externals = []
A = []
ok = True
for B_k in boundary:
outer = [w for w in G.neighbor_iterator(B_k) if w not in boundary]
if len(outer) != 1:
ok = False
break
externals.append(frozenset([B_k, outer[0]]))
A.append(outer[0])
if not ok:
continue
if any(e in protected for e in boundary_edges + externals):
continue
return boundary, externals, A
return None
def valid_index(f_vec, A):
for i in range(5):
if f_vec[(i + 3) % 5] != f_vec[(i + 4) % 5]:
continue
if len({f_vec[i], f_vec[(i + 1) % 5], f_vec[(i + 2) % 5]}) != 3:
continue
if A[(i + 3) % 5] == A[(i + 4) % 5]:
continue
return i
return None
def run_algorithm_to_completion(H, phi_1_coloring_dict, named_step1,
vn_start=10000):
H = H.copy()
coloring = dict(phi_1_coloring_dict)
E = set(named_step1.values())
next_vn = vn_start
while True:
H.is_planar(set_embedding=True)
result = find_safe_pentagonal_face(H, E)
if result is None:
break
boundary, externals, A = result
f_vec = [coloring[e] for e in externals]
i_t = valid_index(f_vec, A)
if i_t is None:
break
v_n = next_vn
next_vn += 1
H_new = H.copy()
for v in boundary:
H_new.delete_vertex(v)
H_new.add_vertex(v_n)
side_0 = (v_n, A[i_t])
spike = (v_n, A[(i_t + 1) % 5])
side_1 = (v_n, A[(i_t + 2) % 5])
merged = (A[(i_t + 3) % 5], A[(i_t + 4) % 5])
H_new.add_edges([side_0, spike, side_1, merged])
if H_new.has_multiple_edges() or H_new.has_loops():
break
if not H_new.is_planar(set_embedding=True):
break
H = H_new
coloring = {e: c for e, c in coloring.items()
if not any(u in boundary for u in e)}
coloring[frozenset(side_0)] = f_vec[i_t]
coloring[frozenset(spike)] = f_vec[(i_t + 1) % 5]
coloring[frozenset(side_1)] = f_vec[(i_t + 2) % 5]
coloring[frozenset(merged)] = f_vec[(i_t + 3) % 5]
E |= {frozenset(spike), frozenset(side_0), frozenset(side_1),
frozenset(merged)}
return H, coloring
def main():
print(f"{'n':>3} {'tri#':>5} {'i_red':>5} | "
f"{'|V(H_1)|':>9} {'|E(H_1)|':>9} {'#col(H_1)':>10} {'#K(H_1)':>8} | "
f"{'|V(H_t*)|':>10} {'|E(H_t*)|':>10} {'#col(H_t*)':>11} {'#K(H_t*)':>10}")
print("-" * 120)
for n in [12, 14]:
try:
triangulations = list(graphs.triangulations(n, minimum_degree=5))
except Exception as ex:
print(f" cannot enumerate n={n}: {ex}")
continue
for tri_idx, G in enumerate(triangulations, start=1):
G_copy = G.copy()
G_copy.is_planar(set_embedding=True)
D = dual_of(G_copy)
D.is_planar(set_embedding=True)
# iterate over all pentagonal faces (or just the first)
faces = [f for f in D.faces() if len(f) == 5]
if not faces:
continue
face = faces[0]
for i_red in range(5):
res = apply_reduction(D, face, i_red, 9999)
if res is None:
continue
H_1 = res['H']
# count Kempe classes of H_1
n_col_h1, n_kc_h1 = num_kempe_classes(H_1)
# run algorithm to completion with Sage's edge_coloring as phi_1
classes = edge_coloring(H_1, value_only=False)
if len(classes) > 3:
continue
phi_1_dict = {}
for k, edge_list in enumerate(classes):
for u, v in edge_list:
phi_1_dict[frozenset([u, v])] = k
H_final, _ = run_algorithm_to_completion(H_1, phi_1_dict,
res['named'])
n_col_hf, n_kc_hf = num_kempe_classes(H_final)
same = "same" if n_kc_h1 == n_kc_hf else "DIFFERENT"
print(f"{n:>3} {tri_idx:>5} {i_red:>5} | "
f"{H_1.order():>9} {H_1.size():>9} {n_col_h1:>10} {n_kc_h1:>8} | "
f"{H_final.order():>10} {H_final.size():>10} "
f"{n_col_hf:>11} {n_kc_hf:>10} {same}")
sys.stdout.flush()
if __name__ == '__main__':
main()