"""Empirical check: does pure greedy online 4-coloring succeed under the canonical quadrilateral sequencing? The user's conjecture-of-the-day: for the canonical sequence, every non-ring-completion move (anchor drop, level add, join) admits a local 4-coloring choice, and ring completions introduce no new vertices, so the only question is whether the existing coloring agrees on the ring-completion quad's edges. Under proper greedy online coloring this latter check is automatic: every G'-edge constraint is enforced as the second endpoint is colored. So the empirical question becomes: does pure greedy (smallest color avoiding already-colored G'-neighbors) ever get stuck at a non-ring-completion move with all four colors forbidden? We run greedy in a canonical vertex order (depth, then vertex id) and classify every failure as one of: - 'no_color_available': a new vertex saw all 4 colors among colored G'-neighbors - 'ring_completion_monochromatic': a ring completion's quad has a monochromatic edge - 'success': global proper 4-coloring of G'. """ from typing import Any from sage.all import Graph, graphs # type: ignore[attr-defined] # pylint: disable=no-name-in-module from lib.colored_graphs import canonize_and_save_graph from plane_depth_sequencing import ( quadrilateral_sequencing, _quad_vertices, ) def greedy_4_color_under_sequence( g_prime: Graph, sequence: list[frozenset[frozenset[Any]]], depth_labelling: dict[Any, int], ) -> tuple[dict[Any, int], str, int | None]: """Run greedy online 4-coloring under the canonical sequence. Returns (partial_coloring, status, failing_step). status is one of: - 'success' - 'no_color_available' (failing_step is the sequence index) - 'ring_completion_monochromatic' (failing_step is the sequence index) """ coloring: dict[Any, int] = {} slice_vertices: set[Any] = set() adj: dict[Any, set[Any]] = {v: set(g_prime.neighbors(v)) for v in g_prime.vertices()} for step, quad in enumerate(sequence): quad_v = _quad_vertices(quad) new_vertices = quad_v - slice_vertices if not new_vertices: # Ring completion: verify the existing coloring is proper on the quad's edges. quad_v_list = list(quad_v) for i in range(len(quad_v_list)): for j in range(i + 1, len(quad_v_list)): u, v = quad_v_list[i], quad_v_list[j] if v in adj[u] and coloring[u] == coloring[v]: return coloring, 'ring_completion_monochromatic', step continue # Color new vertices in a canonical order (depth, then vertex id). for v in sorted(new_vertices, key=lambda x: (depth_labelling[x], x)): used = {coloring[u] for u in adj[v] if u in coloring} avail = [c for c in (0, 1, 2, 3) if c not in used] if not avail: return coloring, 'no_color_available', step coloring[v] = min(avail) slice_vertices.add(v) return coloring, 'success', None def _outer_cycle_from_embedding(g: Graph) -> list[Any]: """Pick a deterministic outer cycle: the first face Sage returns from the embedding.""" g.is_planar(set_embedding=True) embedding = g.get_embedding() faces = g.faces(embedding) return [u for u, v in faces[0]] def check_graph(g: Graph) -> dict[str, Any]: """Run sequencing + greedy coloring on a single graph. Returns a result dict.""" outer_cycle = _outer_cycle_from_embedding(g) try: seq_result = quadrilateral_sequencing(g, outer_cycle) except Exception as exc: # pylint: disable=broad-except return { 'graph6': g.graph6_string(), 'status': 'sequencing_error', 'error': str(exc), } coloring, status, failing_step = greedy_4_color_under_sequence( seq_result['deep_embedding'], seq_result['sequence'], seq_result['depth_labelling'], ) return { 'graph6': g.graph6_string(), 'status': status, 'failing_step': failing_step, 'coloring_size': len(coloring), 'num_quads': len(seq_result['quadrilaterals']), 'move_codes': seq_result['move_codes'], } def run_enumeration(min_order: int, max_order: int) -> dict[int, dict[str, Any]]: """Iterate over all 3-connected triangulations of order in [min_order, max_order].""" summary: dict[int, dict[str, Any]] = {} for n in range(min_order, max_order + 1): total = 0 by_status: dict[str, int] = {} failures: list[dict[str, Any]] = [] for g in graphs.planar_graphs(n, minimum_connectivity=3, maximum_face_size=3): total += 1 result = check_graph(g) status = result['status'] by_status[status] = by_status.get(status, 0) + 1 if status != 'success': failures.append(result) if total % 200 == 0: print(f" n={n}: checked {total}, statuses so far: {by_status}") summary[n] = {'total': total, 'by_status': by_status, 'failures': failures} print(f"n={n}: total={total}, by_status={by_status}") for f in failures[:5]: print(f" failure {f['graph6']}: {f['status']} at step {f['failing_step']}") if len(failures) > 5: print(f" ... ({len(failures) - 5} more failures)") return summary if __name__ == "__main__": import sys min_order = int(sys.argv[1]) if len(sys.argv) > 1 else 4 max_order = int(sys.argv[2]) if len(sys.argv) > 2 else 9 summary = run_enumeration(min_order, max_order) print() print("=" * 60) print("Final summary:") for n, s in summary.items(): print(f" n={n}: total={s['total']}, by_status={s['by_status']}") # Save up to first 10 failure graphs per order for inspection. saved = 0 for n, s in summary.items(): for f in s['failures'][:10]: try: g = Graph(f['graph6']) canonical, graph_dir = canonize_and_save_graph(g) print(f" saved n={n} {f['graph6']} ({f['status']}) -> {graph_dir}") saved += 1 except Exception as exc: # pylint: disable=broad-except print(f" failed to save {f['graph6']}: {exc}") print(f"Saved {saved} failure graphs total.")