Files
math-research/papers/face_monochromatic_pairs/experiments/iterated_reduction.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

239 lines
9.2 KiB
Python

"""Iterated reduced-dual reduction with protected edges (Sage).
Implements Algorithm 3.1 from paper.tex:
0. G' = dual of G (a triangulation conjectured to be a minimal
counterexample).
1. Apply Definition 2.1 to G' at a pentagonal face to get H_1; find a
proper 3-edge-colouring phi_1 of H_1.
2. Initialise protected edge set E := {spike, side-0, side-1, merged} from
step 1.
3. Iterate: find a pentagonal face of H_{t-1} whose 10 incident edges are
disjoint from E, pick a valid index i_t (Lemma 2.4), apply
Definition 2.1, extend phi_{t-1} to phi_t, and add the four new named
edges to E.
4. Terminate when no safe pentagonal face exists.
We run on the icosahedron-dodecahedron pair as a concrete trace; the
icosahedron is 4-colourable, so the dodecahedron is 3-edge-colourable and
the algorithm terminates combinatorially.
Run with:
sage experiments/iterated_reduction.py
"""
from sage.all import Graph
from sage.graphs.graph_coloring import edge_coloring
# ---------------------------------------------------------------------------
# Build G' = dodecahedron with the same naming as experiments/reduced_dual.py:
# vertex families a (inner pentagon, will play the role of partial F_v's
# boundary vertices for the first reduction), b, c, d.
# ---------------------------------------------------------------------------
def build_dodecahedron():
edges = []
for i in range(5):
edges.append((('a', i), ('a', (i + 1) % 5))) # inner pentagon
edges.append((('a', i), ('b', i))) # spoke a-b
edges.append((('b', i), ('c', i))) # b-c
edges.append((('b', i), ('c', (i - 1) % 5))) # b-c (other side)
edges.append((('c', i), ('d', i))) # spoke c-d
edges.append((('d', i), ('d', (i + 1) % 5))) # outer pentagon
G = Graph(edges, multiedges=False, loops=False)
assert G.is_planar(set_embedding=True), "dodecahedron not planar?"
return G
# ---------------------------------------------------------------------------
# 3-edge-colour a cubic plane graph; return None if not 3-edge-colourable.
# ---------------------------------------------------------------------------
def proper_3_coloring(G):
classes = edge_coloring(G, value_only=False)
if len(classes) > 3:
return None
out = {}
for k, edge_list in enumerate(classes):
for u, v in edge_list:
out[frozenset([u, v])] = k
return out
# ---------------------------------------------------------------------------
# Face search: pentagonal face whose 10 incident edges are outside `protected`.
# Returns (boundary, externals, A) or None.
# ---------------------------------------------------------------------------
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]
# the external edge at each boundary vertex (the one not on the face)
externals = []
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:
# In a cubic graph each face-boundary vertex has exactly one
# external edge; otherwise something is off.
break
externals.append(frozenset([B_k, outer[0]]))
A.append(outer[0])
else:
if not any(e in protected for e in boundary_edges + externals):
return boundary, externals, A
return None
# ---------------------------------------------------------------------------
# Lemma 2.4: any proper 3-edge-colouring forces the external vector at a
# pentagonal face to have shape (a, b, c, c, c) up to cyclic rotation. The
# valid index i for the reduction is one where positions i+3, i+4 host the
# doubled colour and positions i, i+1, i+2 host three distinct colours.
# ---------------------------------------------------------------------------
def valid_indices(f_vec):
out = []
for i in range(5):
if f_vec[(i + 3) % 5] != f_vec[(i + 4) % 5]:
continue
triple = {f_vec[i], f_vec[(i + 1) % 5], f_vec[(i + 2) % 5]}
if len(triple) == 3:
out.append(i)
return out
# ---------------------------------------------------------------------------
# Apply Definition 2.1 at (F, i): delete the 5 boundary vertices, add v_n
# connected to A[i], A[i+1], A[i+2], add the chord A[i+3]-A[i+4]. Extend the
# colouring: every surviving edge keeps its colour; each new edge takes the
# colour of the deleted external at the same A_k (the unique third colour at
# A_k under the surviving edges).
# ---------------------------------------------------------------------------
def apply_reduction(G, boundary, externals, A, coloring, i, v_n_label):
G_new = G.copy()
for v in boundary:
G_new.delete_vertex(v)
G_new.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])
G_new.add_edges([side_0, spike, side_1, merged])
assert G_new.is_planar(set_embedding=True), "reduction broke planarity"
coloring_new = {
e: c for e, c in coloring.items() if not any(u in boundary for u in e)
}
coloring_new[frozenset(side_0)] = coloring[externals[i]]
coloring_new[frozenset(spike)] = coloring[externals[(i + 1) % 5]]
coloring_new[frozenset(side_1)] = coloring[externals[(i + 2) % 5]]
coloring_new[frozenset(merged)] = coloring[externals[(i + 3) % 5]]
named = {
'spike': frozenset(spike),
'side_0': frozenset(side_0),
'side_1': frozenset(side_1),
'merged': frozenset(merged),
}
return G_new, coloring_new, named
# ---------------------------------------------------------------------------
# Sanity check: verify that a coloring is proper (each vertex sees 3 colours).
# ---------------------------------------------------------------------------
def is_proper_3_coloring(G, coloring):
for v in G.vertex_iterator():
seen = set()
for u in G.neighbor_iterator(v):
seen.add(coloring[frozenset([u, v])])
if len(seen) != G.degree(v):
return False
return True
# ---------------------------------------------------------------------------
# Main driver.
# ---------------------------------------------------------------------------
def run_algorithm(max_iterations=50, verbose=True):
if verbose:
print("STEP 0: G' = dodecahedron (dual of the icosahedron)")
G_prime = build_dodecahedron()
if verbose:
print(f" |V(G')| = {G_prime.order()}, |E(G')| = {G_prime.size()}")
# ---- STEP 1: apply Definition 2.1 to G' at the inner pentagon, i_1 = 0
face_data = find_safe_pentagonal_face(G_prime, set())
if face_data is None:
print(" No pentagonal face in G'.")
return
boundary, externals, A = face_data
H = G_prime.copy()
for v in boundary:
H.delete_vertex(v)
v_n_label = ('v_n', 1)
H.add_vertex(v_n_label)
side_0 = (v_n_label, A[0])
spike = (v_n_label, A[1])
side_1 = (v_n_label, A[2])
merged = (A[3], A[4])
H.add_edges([side_0, spike, side_1, merged])
assert H.is_planar(set_embedding=True)
coloring = proper_3_coloring(H)
if coloring is None:
print(" H_1 is not 3-edge-colourable; algorithm halts.")
return
assert is_proper_3_coloring(H, coloring)
if verbose:
print(f"STEP 1: H_1 = reduced dual at the first face, i_1 = 0")
print(f" |V(H_1)| = {H.order()}, |E(H_1)| = {H.size()}; "
f"3-edge-coloured.")
# ---- STEP 2: initialise the protected set
E = {
frozenset(spike),
frozenset(side_0),
frozenset(side_1),
frozenset(merged),
}
if verbose:
print(f"STEP 2: |E_protected| = {len(E)}")
# ---- STEP 3-4: iterate
for t in range(2, max_iterations + 1):
face_data = find_safe_pentagonal_face(H, E)
if face_data is None:
if verbose:
print(f"\nTerminated at iteration t = {t}: "
f"no pentagonal face avoids E.")
break
boundary, externals, A = face_data
f_vec = [coloring[e] for e in externals]
choices = valid_indices(f_vec)
if not choices:
print(f" iter t = {t}: f-vector {f_vec} has no valid index "
f"(Lemma 2.4 should preclude this --- bug!)")
break
i_t = choices[0]
v_n_label = ('v_n', t)
H, coloring, named = apply_reduction(
H, boundary, externals, A, coloring, i_t, v_n_label)
assert is_proper_3_coloring(H, coloring)
E |= set(named.values())
if verbose:
print(f" iter t = {t:>2}: f = {f_vec}, chose i_t = {i_t}; "
f"|V| = {H.order():>2}, |E_graph| = {H.size():>2}, "
f"|E_protected| = {len(E):>2}")
else:
if verbose:
print(f"\nReached max_iterations = {max_iterations}.")
if verbose:
print(f"\nFinal H: |V| = {H.order()}, |E| = {H.size()}, "
f"|E_protected| = {len(E)}")
if __name__ == '__main__':
run_algorithm()