Add Kempe-locked colored edge flip class survey script

Iterates min-degree-5 maximal planar graphs and, for each one G, looks
for any flip-neighbor H and proper 4-coloring phi of H satisfying the
Kempe-locked structure of Lemma 4.3 (phi(u)=phi(v) plus an {a,b}-Kempe
chain for every other color b).  For each such (H, phi), BFS the
colored edge flip class with a 50,000-graph cap and test reached
graphs for isomorphism to G.  Saves the first G for which no such (H,
phi) has G in its colored class.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-14 15:47:20 -04:00
parent 53a9192f65
commit eb7e532382
+197
View File
@@ -0,0 +1,197 @@
"""Survey for min-degree-5 maximal planar graphs G such that no Kempe-locked
proper 4-coloring of any flip-neighbor of G admits a graph isomorphic to G in
its colored edge flip class.
For each min-degree-5 maximal planar graph G of order n:
for each admissible edge uv of G with flip H = G^flip(uv):
for each proper 4-coloring phi of H satisfying:
(a) phi(u) == phi(v), and
(b) for every color b != phi(u), u and v lie in the same connected
component of the subgraph of H induced by vertices of color
phi(u) or b ({phi(u), b}-Kempe chain),
BFS the colored edge flip class C(H, phi) and test each reached graph
for isomorphism to G.
If for some (H, phi) a graph isomorphic to G is reached, G is "found" and we
move on. Otherwise we save G and stop.
"""
from typing import Iterator, Any, cast
from sage.all import Graph, graphs # type: ignore[attr-defined] # pylint: disable=no-name-in-module
from sage.graphs.graph_coloring import all_graph_colorings # type: ignore[attr-defined] # pylint: disable=no-name-in-module
from lib.colored_graphs import canonize_and_save_graph
BFS_LIMIT = 50000
def face_thirds(g: Graph) -> dict[frozenset, tuple]:
"""Map each edge {u, v} of triangulation g to (w, x), the two third vertices
of the triangular faces incident to uv."""
g_emb = g.copy()
if not g_emb.is_planar(set_embedding=True):
raise ValueError("graph is not planar")
incidence: dict[frozenset, list] = {}
for face in g_emb.faces():
if len(face) != 3:
raise ValueError("graph is not a triangulation")
a, b, c = face[0][0], face[1][0], face[2][0]
for u, v, third in [(a, b, c), (b, c, a), (c, a, b)]:
incidence.setdefault(frozenset((u, v)), []).append(third)
return {k: tuple(v) for k, v in incidence.items() if len(v) == 2}
def edge_set(g: Graph) -> frozenset:
"""Hashable canonical representation of g's edges, ignoring labels."""
return frozenset(frozenset(e) for e in g.edges(labels=False))
def kempe_connects(h: Graph, phi: dict[Any, int], a: int, b: int, u: Any, v: Any) -> bool:
"""True iff u and v lie in the same component of the subgraph of h
induced by vertices of color a or b."""
if phi[u] not in (a, b) or phi[v] not in (a, b):
return False
verts = [w for w in h.vertices() if phi[w] in (a, b)]
sub = cast(Graph, h.subgraph(verts))
for comp in sub.connected_components():
if u in comp:
return v in comp
return False
def coloring_passes_filter(h: Graph, phi: dict[Any, int], u: Any, v: Any) -> bool:
"""phi(u) == phi(v), and for every color b != phi(u), there is a
{phi(u), b}-Kempe chain in h from u to v."""
a = phi[u]
if phi[v] != a:
return False
for b in range(4):
if b == a:
continue
if not kempe_connects(h, phi, a, b, u, v):
return False
return True
def color_preserving_flips(g: Graph, phi: dict[Any, int]) -> Iterator[Graph]:
"""Yield every g^flip(u'v') such that the flip is admissible and the new
edge's endpoints w', x' satisfy phi(w') != phi(x') (so phi remains proper)."""
pairs = face_thirds(g)
for u, v in list(g.edges(labels=False)):
thirds = pairs.get(frozenset((u, v)))
if thirds is None or len(thirds) != 2:
continue
w, x = thirds
if g.has_edge(w, x):
continue
if phi[w] == phi[x]:
continue
flipped = g.copy()
flipped.delete_edge(u, v)
flipped.add_edge(w, x)
yield flipped
def canonical_g6(g: Graph) -> str:
"""Canonical graph6 string used to test isomorphism."""
return cast(Graph, g.canonical_label()).graph6_string()
def class_contains_iso_to(h: Graph, phi: dict[Any, int], target_canonical: str,
limit: int = BFS_LIMIT) -> tuple[bool, int, bool]:
"""BFS the colored edge flip class of (h, phi). Return (found, visited_count,
hit_limit), where found is True iff some reached graph has canonical
graph6_string equal to target_canonical."""
visited: set[frozenset] = {edge_set(h)}
if canonical_g6(h) == target_canonical:
return True, len(visited), False
frontier: list[Graph] = [h]
while frontier:
if len(visited) >= limit:
return False, len(visited), True
cur = frontier.pop()
for nxt in color_preserving_flips(cur, phi):
es = edge_set(nxt)
if es in visited:
continue
visited.add(es)
if canonical_g6(nxt) == target_canonical:
return True, len(visited), False
frontier.append(nxt)
return False, len(visited), False
def survey_graph(g: Graph, verbose: bool = False) -> tuple[bool, bool]:
"""Test whether g admits any Kempe-locked flip witness.
Returns (filter_nonempty, found_in_class). filter_nonempty is True iff at
least one (H, phi) satisfies the Kempe-locked criteria. found_in_class
is True iff some such (H, phi) has a graph isomorphic to g in its
colored edge flip class. (When filter_nonempty is False, found_in_class
is also False but the graph should be SKIPPED, not saved.)"""
target_canonical = canonical_g6(g)
pairs = face_thirds(g)
filter_nonempty = False
total_filtered_colorings = 0
for u, v in list(g.edges(labels=False)):
thirds = pairs.get(frozenset((u, v)))
if thirds is None or len(thirds) != 2:
continue
w, x = thirds
if g.has_edge(w, x):
continue
h = g.copy()
h.delete_edge(u, v)
h.add_edge(w, x)
for phi in all_graph_colorings(h, 4, vertex_color_dict=True):
phi = cast(dict, phi)
if not coloring_passes_filter(h, phi, u, v):
continue
filter_nonempty = True
total_filtered_colorings += 1
found, visited, hit_limit = class_contains_iso_to(h, phi, target_canonical)
if verbose:
msg = f" edge ({u},{v}), phi#{total_filtered_colorings}: visited={visited}{' (LIMIT)' if hit_limit else ''}"
print(f"{msg} -> {'found' if found else 'not found'}")
if found:
return True, True
if verbose and not filter_nonempty:
print(" no Kempe-locked colorings — skipping")
return filter_nonempty, False
def survey(min_order: int, max_order: int) -> Graph | None:
"""Iterate min-degree-5 maximal planar graphs in [min_order, max_order].
Return the first G failing the Kempe-locked reachability test, or None."""
for n in range(min_order, max_order + 1):
print(f"=== order {n} ===")
gen = graphs.planar_graphs(
n, minimum_connectivity=3, maximum_face_size=3, minimum_degree=5
)
checked = 0
skipped_vacuous = 0
for g in gen:
checked += 1
filter_nonempty, found = survey_graph(g, verbose=True)
if not filter_nonempty:
skipped_vacuous += 1
continue
if not found:
print(f"order {n}, graph #{checked}: Kempe-locked coloring exists "
f"but G NOT reachable; saving and stopping")
return g
if checked % 10 == 0:
print(f" order {n}: checked {checked} graphs, skipped (vacuous) {skipped_vacuous}")
print(f" order {n} done: {checked} graphs checked, {skipped_vacuous} skipped (vacuous)")
return None
if __name__ == "__main__":
import sys
min_order = int(sys.argv[1]) if len(sys.argv) > 1 else 12
max_order = int(sys.argv[2]) if len(sys.argv) > 2 else 14
result = survey(min_order, max_order)
if result is None:
print("No witness graph found in the surveyed range.")
else:
_, graph_dir = canonize_and_save_graph(result)
print(f"Saved witness to {graph_dir}")